0%

认识皮肤

皮肤是人体最大的器官,展开达两平方米,是人体之于外界环境的保护膜,同时也是零距离感受外界的第一道关卡,所以每一寸都遍布了神经细胞。

皮肤主要由三层构成:表皮,真皮以及皮下组合。其中表皮与真皮间的基底膜带决定了皮肤的弹性紧致,而且是在1岁形成不可逆的衰老,无法重生所以只能好好保护延缓其衰老。皮肤细胞从底层出生,衰老于真皮并死亡在表皮并成为保护屏障,正常周期为4周。患有皮肤病的可能会加速该过程至5天,导致表皮的屏障并不完整。

表皮

由于皮肤表面存在着大量微生物群,所以保持皮肤的pH值是非常重要的,应当合理维持完好无损的酸性保护膜,人工清洁剂约5.5,另外对于皮下脂肪过厚的皮肤褶皱,唯一有效办法是减肥。可以用抗真菌软膏,另外带锌软膏可缓解皮肤炎症,带走多余水分。

生长纹无足轻重,关于肤色,是有黑色素影响的。研究表明,橙色皮肤最具吸引力,可多摄入胡萝卜素B达到。颠痕主要由基底膜带大面积受损,表皮过度损耗引起的。对于渗水伤口可用硅霜或贴上硅凝胶伤口贴。治疗疙瘩需要强效疗法如:激光疗法、热针、冷冻疗法以及X射线放射疗法。

真皮

真皮充满紧致的结缔组织,带来稳固性以及拉伸性,任何的日晒、日光浴、烟草、压力、睡眠不足、营养不良以及缺乏运动等会加速弹性纤维的流失。臀部皮肤就是自然老化的参考,面部等受光照影响较大是老化较快的皮肤。另外,皮肤在4度就开始出现冻疮,而冬天皮肤也能适应,所以只在晚上睡前使用保湿即可,白天冬天保湿使皮肤含水量升高,反而容易冻伤。

真皮层拥有大量神经,使皮肤成为最大的神经器官,抚摸有调节中枢神经和社交等作用。同时含有腺体,会分泌激素气味等。

摘自微信号十三手记

引言

什么是麻烦?
是那些你特别想忽略或粗糙嫌弃的事。

麻烦是什么?
是你逃避直面真实自己的托词。

既然如此,还有真正的麻烦吗?
没有,麻烦都是必要的,都是来成就你的礼物。

类似的麻烦为何会反复出现?
因为你一直没有透过直面解决这些麻烦去解除生命中的困惑。

如此一来,不解决麻烦的结果是什么?
你始终不能真正感受自己内心的智慧与力量。

此刻你要明白

在你的人生旅程中,你与别人的整体差距,就是你们解决麻烦的程度的差距,如此,当你遇到你以为的麻烦,请不要嫌弃和忽略,请你力所能及的去具体解决,尽量到位,因为在这些解决行为里,不仅有你值得提升的认知,也有你智慧与力量的创造,也有你生命活性的体验,更加你累积人生成就的大礼。

个人感悟

虽然我们需要惜时,但也要耐心解决问题,大大小小的问题都是提升自我的途径,尽管麻烦也要去接纳。如果重复度大的则可以考虑自动化去解决。

摘自微信号十三手记

引言

为什么那么忙,却没有达到想要的效果?
因为多余动作太多

什么是多余动作?
对事情发展本身不直接着力的动作

多余动作为什么会出现?
因为你害怕不利的结果来得太快,所以,你内心的恐惧感让你不自知的搞了这些动作来减缓进展的速度,虽然你以为这样做可以更好的优化过程,可以更快得到有利的结果。

多余动作会产生什么效果?
你以为会让事态进展更高效,但,其实必定会生出更多跟事件发展本身无关的新问题,进而形成层层阻碍。

此刻你要明白

在事情发展过程中,只有你的行为尽量实事求是的直接着力于事件本身,你才能尽量减少多余动作产生的能耗,你才能让事件在发展过程中更加清晰的优化,你才能让事件的进展更加高效。

个人感悟

对于自我而言,其实是逃避真正问题的根因。有时候明知道面前有必须要解决的问题,但是总是希望从外界或者其他不相关的内容中得到方法,而非从自身出发努力去解决问题。因此往往会偏离有价值的行为,以及有效的解决真正问题,因此才一直无法有效解决问题。

区域生长分割region growing segmentation

算法核心:基于点法线之间角度的比较,尽量将满足平滑约束的相邻点合并在一起,以一簇点集的形式输出,视为相同平面

工作原理:区域增长从有最小曲率值curvature的点。则需要计算所有曲率值并进行排序;因为曲率最小的点位于平坦区域,从平坦区域增长可以减少区域的总数。(曲率与法线的求解方法类似,法线描述点在表面的法向,曲率则是法线间协方差矩阵的特征值,描述法向一致性)

具体过程:

  1. 对未标记点的曲率排序,将最小曲率点放入种子集
  2. 对每个种子的所有邻点计算
  3. 1 每个近邻点与当前点的法线角度差(reg.setSmoothnessThreshold),如果小于阈值则重点考虑,进入2.2判断
  4. 2 该点通过2.1法线角度差检验,如果曲率小于设定的阈值(reg.setCurvatureThreshold),这个点即被添加到种子点集,属于当前平面
  5. 通过两次检验的点,被从原始点云去除
  6. 设置最小点簇的点数reg.setMinClusterSize,以及最大簇reg.segMaxClusterSize
  7. 重复1-3生成min~max个点数的所有平面,并对不同平面标记颜色区分
  8. 直到算法在剩余点中生成的点簇不能满足min,停止工作

ubuntu下opencv CMakeList文件

通过terminal直接apt安装opencv,一般挂载在/usr/local/lib下;此时find_package即可找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# cmake needs this line
cmake_minimum_required(VERSION 3.1)

# Define project name
project(FeatureMapping)

# Find OpenCV, you may need to set OpenCV_DIR variable
# to the absolute path to the directory containing OpenCVConfig.cmake file
# via the command line or GUI
find_package(OpenCV REQUIRED)

# Enable C++11
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)

# Declare the executable target built from your sources
add_executable(main main.cpp)

# Link your application with OpenCV libraries
target_link_libraries(main PRIVATE ${OpenCV_LIBS})

两RGB图间的特征匹配

具体可参考官方教程https://docs.opencv.org/4.x/d7/dff/tutorial_feature_homography.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <opencv4/opencv2/highgui.hpp>
#include <opencv4/opencv2/opencv.hpp>
#include <opencv4/opencv2/imgproc.hpp>
#include <opencv4/opencv2/features2d.hpp>
#include <opencv4/opencv2/core.hpp>
#include <opencv4/opencv2/calib3d.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char **argv)
{
cout << CV_VERSION << endl;

Mat img1 = imread("001.JPG");
Mat img2 = imread("002.JPG");

Ptr<SIFT> detector = SIFT::create();
vector<KeyPoint> keypoints1, keypoints2;
Mat descriptors1, descriptors2;
detector->detectAndCompute(img1, noArray(), keypoints1, descriptors1);
detector->detectAndCompute(img2, noArray(), keypoints2, descriptors2);

Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create(DescriptorMatcher::FLANNBASED);

vector<vector<DMatch>> knn_matches;
matcher->knnMatch(descriptors1, descriptors2, knn_matches, 2);

const float ratio_thresh = 0.5f;
vector<DMatch> good_matches;
for(size_t i=0; i<knn_matches.size();i++)
{
if(knn_matches[i][0].distance < ratio_thresh * knn_matches[i][1].distance)
{
good_matches.push_back(knn_matches[i][0]);
}
}

Mat img_matches;
drawMatches(img1, keypoints1, img2, keypoints2, good_matches, img_matches);


//-- Localize the object
std::vector<Point2f> obj;
std::vector<Point2f> scene;
for( size_t i = 0; i < good_matches.size(); i++ )
{
//-- Get the keypoints from the good matches
obj.push_back( keypoints1[ good_matches[i].queryIdx ].pt );
scene.push_back( keypoints2[ good_matches[i].trainIdx ].pt );
}
Mat H = findHomography( obj, scene, RANSAC );
cout << H << endl;
//-- Get the corners from the image_1 ( the object to be "detected" )
std::vector<Point2f> obj_corners(4);
obj_corners[0] = Point2f(0, 0);
obj_corners[1] = Point2f( (float)img1.cols, 0 );
obj_corners[2] = Point2f( (float)img1.cols, (float)img1.rows );
obj_corners[3] = Point2f( 0, (float)img1.rows );
std::vector<Point2f> scene_corners(4);
perspectiveTransform( obj_corners, scene_corners, H);
//-- Draw lines between the corners (the mapped object in the scene - image_2 )
line( img_matches, scene_corners[0] + Point2f((float)img1.cols, 0),
scene_corners[1] + Point2f((float)img1.cols, 0), Scalar(0, 255, 0), 4 );
line( img_matches, scene_corners[1] + Point2f((float)img1.cols, 0),
scene_corners[2] + Point2f((float)img1.cols, 0), Scalar( 0, 255, 0), 4 );
line( img_matches, scene_corners[2] + Point2f((float)img1.cols, 0),
scene_corners[3] + Point2f((float)img1.cols, 0), Scalar( 0, 255, 0), 4 );
line( img_matches, scene_corners[3] + Point2f((float)img1.cols, 0),
scene_corners[0] + Point2f((float)img1.cols, 0), Scalar( 0, 255, 0), 4 );
//-- Show detected matches

namedWindow("Matches", WINDOW_NORMAL);
imshow("Matches", img_matches );
// imwrite("good_res.jpg", img_matches);


waitKey();
return 0;
}

/*
输出H:
[0.9771928708495958, -0.008932567941541457, 129.4806431662592;
0.0342771681684575, 0.9114878090650101, 163.0011063784843;
3.322036688562577e-05, -1.090291446290035e-05, 1]

*/

值得注意的是,本版本是opencv 4.5,与Opencv2/3有很多语法不一样了;关于特征点和描述子最大的区别:特征点是特征坐标KeyPoint,描述子是描述邻域信息的Mat。

特征匹配总体的流程:
1.首先读取图片,创建检测特征SIFT,ORB,SURF(xfeature)等
2.如果检测子包含描述子,则直接detectAndCompute
3.一般用BFMatcher直接对描述子点对vector 进行暴力匹配,这里是优化加速版用FLANN的knn,注意的是ORB可用HAMMING距离,其他则不可。
4.接着对KNN点对进行基于距离的筛选
5.对置信度高的点对进行RANSAC的单应变换矩阵求解

至此基本完成特征匹配的功能,得到的单应矩阵描述了图片间的变换关系;这是很多CV高级应用的基础,如校正,拼接,SLAM等。

文章摘自微信公众号:十三先生手记

何谓需求

你清晰什么是真正的需求吗?
必定不清晰

那什么是真正的需求?
对境需求,不是过去的需求,更不是未来的需求。

什么是境的需求?
是适合你当时的实际需求。

需求有几种?
两种:人事物本身,以及附加给人事物的概念

人事物本身是什么意思?
是人事物本来的功能和价值。

概念需求是什么意思?
是附加到人事物的功能和价值上的概念,是让你产生欲望后误以为自己需求的迷障。

谁附加的概念?
是你自己或者是外界。

此刻要你深深的明白

由于你的贪心,你分不清自己真正的需求。所以,你过去总被人事物的附加概念牵动而消耗了大量资源和精力,所以只有你真正懂得了你要的是人事物本身的功能和价值,你才不会被这些附加的概念干扰和误导,你才真正懂得了珍惜物命,你才真正懂得了珍惜缘分,你才真正懂得了珍惜人生。

预备知识

SLAM:Simultaneous Localization and Mapping

同步定位及建图:搭载特定传感器的主体,在没有环境先验信息情况下,在运动过程中建立环境模型,同时估计自己位姿的过程。根据传感器分为深度相机,RGB相机和激光。不同传感器特点不一样,应用环境以及遇到的难点不同。本书主要是视觉SLAM:围绕RGB相机,也会引入Depth相机。

CV之初,人们想象有一天计算机将和人一样,通过眼睛去观察世界,理解周围的物体,探索未知的环境–这是一个美妙而又充满浪漫色彩的梦想,让无数科研人员日夜为之奋斗。

SLAM相关的应用点:室内扫地机,移动机器人;户外的自动驾驶以及空中无人机;乃至虚拟现实和增强现实设备。

SLAM研究的三十年,将主干分为四个大模块:

  • 前端里程计(求pose):估计相邻图像相机的运动以及局部地图样子
  • 后端优化(优化pose):根据多个时刻的相机位姿以及回环检测进行优化,得到全局一致的轨迹和地图
  • 建图:根据轨迹建立与任务要求对应的地图
  • 回环检测(优化全局pose):判断是否到达过先前的位置

1.定位问题:我在什么地方?
2.建图问题:周围环境是什么样?
方法很多:室内可以在地板铺设导引线,在墙上贴二维码,在桌子放置无线电定位设备;室外可以安装定位设备(GPS)等
传感器主要两类:1.安装在本体上,如轮式编码器,IMU,相机,激光; 2.安装在环境上,上述提到的

不同传感器的特性以及SLAM的影响

单目:图像是三维空间的二维投影,必须移动(Motion)才能估计结构(Structure).motion后只能根据视差估计相对的值,具有尺度不确定性。
问题:平移才能计算深度以及无法确定真实尺度,导致单目SLAM困难多多,但是成本较低。

双目:利于基线解决尺度不确定性,但视差的匹配依然效率不高,需要引入GPU和FPGA加速才能实时输出整张图的距离信息。双目测量距离受基线大小影响,所以无人车上一般搭载很大的家伙
问题:对于SLAM而言,传感器的配置与标定较为复杂。

RGB-D相机:通过TOF物理测量手段直接获得全图的深度信息
问题:低端TOF测量范围窄,噪声大,视野小;普遍存在日光干扰、无法测量投射材质以及高反高吸收率材质;所以主要用于室内应用,室外难以使用。

目前而言,如果工作环境理想:静态、刚体、光照变化小、没有人为干扰的场景,SLAM是相当成熟。

整书结构

1.数学基础

  • SLAM概述
  • 三维空间运动
  • 李群和李代数
  • 针孔模型及opencv
  • 非线性优化

2.技术实践

  • 特征点法的视觉里程计
  • 直接法的视觉里程计
  • 里程计实践
  • 后端优化BA
  • 后端优化位姿图
  • 回环检测
  • 地图构建
  • SLAM的未来

基础知识须知:

  • 高等数学、线性代数、概率论
  • C++语言和Linux基础

课后习题

1.有线性方程 Ax = b,当我们知道 A, b,想要求解 x 时,如何求解?这对 A 和 b 需要哪些条件?提示:从 A 的维度和秩角度来分析
1)b等于0, n元齐次线性方程组$A_(m*n)x=0$有非0解的充必条件是系数矩阵的秩$Rank(A)<n$
2)b不等于0,非齐次线性方程组$Ax=b$有解的充必条件是系数矩阵的秩等与增广矩阵的秩; $R(A)=R(B)=n$,则有唯一解,$R(A)=R(B)<n$则有无穷多解

  1. 高斯分布是什么?它的一维形式是什么样子?它的高维形式是什么样子?
    又名正态分布,由于中心极限定理,任何分布的抽样分布样本足够大时,其渐进分布都是高斯分布。

初识SLAM

目标:

  • 理解VSLAM各个模块
  • 搭建环境
  • 掌握基本CMAKE

传感器

传感器类型:机器人自身和环境上
自身传感:mono, stereo, rgbd, lidar, event camera, imu, 编码器及新式传感器

一些相关术语:

  • 尺度不确定性:2d的rgb图像没有真实尺度信息
  • 运动motion: 相机外参R, t
  • 结构structure: 物体的远近和大小

经典VSLAM框架


流程:

  • 1.传感器信息读取融合:将所有观测数据正确读入并预处理
  • 2.视觉里程计VO:估算相邻图像间的运动以及局部地图的样子,又称前端
  • 3.后端优化:接受不同时刻VO的相机位姿,以及回环检测的信息,一并优化获得全局一致的轨迹和地图
  • 4.回环检测:判断机器人是否曾经到达过先前的位置,如果有则放到步骤3优化
  • 5.建图:根据估计的轨迹,建立与任务要求对应的地图

总体而言,如果把工作环境限定在静态、刚体以及光照变化不明显、没有人为干扰的场景,那么VSLAM系统相当成熟了

为什么叫里程计? 因为它和实际的里程计一样,只计算相邻时刻的运动,而和再之前过去的信息没有关联。所以属于短时记忆的方案
因此也引起累计漂移:随着相邻估计的误差叠加,后续的误差会更大和不可控
因而引入后端优化回环检测的技术来校正

后端主要处理SLAM过程的噪声问题,具体是如何从带有噪声的数据中估计整个系统的状态,以及这个状态估计的不确定性有多大(Maximum-A-Posteriori),这里包括自身轨迹和地图。主要涉及滤波和非线性优化的算法。

回环检测主要让机器人具有识别曾到达过场景的能力,具体实施可以是二维码图片或者图像间的相似性。

最后建图包括度量和拓扑地图:2D栅格地图、2D拓扑地图、3D点云地图、3D网格地图;
度量地图:强调精确表示地图中物体的位置关系
拓扑地图:强调地图元素之间的关系,考虑节点间的连通性

SLAM数学建模

主要分为运动方程和观测方程,根据这两个方程是否线性以及噪声是否服从高斯分布进行分类,主要信息是位姿以及观测的路标

CMAKE的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
add_executable(exe main.cpp) # 编译执行文件

add_library(lib lib.cpp) # 编译静态库 .a

add_library(lib_shared SHARED lib.cpp) # 编译动态库 .so

add_executable(useLib useLib.cpp) # cpp include动态库的头文件调用
target_link_libraries(useLib lib_shared) # 将库文件链接到编译后的文件,最后链接生成最后的执行文件useLib



# 将库hello和共享库hello_shared安装到/usr/local/lib下
INSTALL(TARGETS hello hello_shared
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)

# 将库的头文件安装到/usr/local/include下
INSTALL(FILES libHelloSLAM.h DESTINATION include/libHelloSLAM)


# 另外目录的项目调用,引入头文件
include_directories(/usr/local/include/libHelloSLAM)

# 链接库文件
target_link_libraries(useHello /usr/local/lib/libhello_shared.so)

库文件是压缩包,包含编译好的二进制函数,但不知道函数是什么样和调用形式,所以需要结合头文件使用。只有拿到头文件和库文件才可以调用库。

你有恐惧吗? 想必有的
你想解除恐惧吗? 必定想的
可为什么你无法真正解除? 因为你不知道恐惧从何而来,更不知道什么是恐惧。

那么恐惧从何而来?

从曲解真相而来

恐惧是什么?

是你的人生大礼给你的提醒。

此刻要你深深明白

二元有相世界,恐惧来自于你对真相的无视或者曲解。如此,只要你无视或者曲解了真相,必定会形成相应的恐惧;如此,你内心恐惧的形成是为了提醒你搞清楚恐惧背后的真相;如此,若是你逃避真相,你必定会反复感受到恐惧;如此,当你心有恐惧,请直面你所恐惧的东西,并搞清楚其真相,当你一旦真正清楚真相,恐惧必定就会随之而消失;与此同时,必定你就能在搞清楚真相的过程中收获到你人生的大礼。

OOP特性:

  • 抽象
  • 封装和数据隐藏
  • 多态
  • 继承
  • 代码的可重用性

接口分离: 1.提供类的声明 2.提供类成员函数

析构函数只有在构造函数用了new分配内存,才需要出来delete释放内存。否则无需工作。

const放在函数括号后:即为const成员函数,作用指不修改调用对象。

this指针的引入: 用来指向调用当前成员函数的对象(this作为隐藏参数传递给方法)

类静态成员变量: static const int Len = 30; 只能是整型或枚举的静态常量

类结合操作符重载

成为多态的重要一部分,隐藏了内部操作,强调了抽象的实质意义。
C++操作符重载要点:

  1. 重载后的操作符至少有一个操作数是用户定义的类型
  2. 使用操作符不能违反该操作符原有的句法规则
  3. 无法定义新的操作符
  4. 不能重载sizeof . .* :: ?: typeid *_cast

友元包含函数,类和成员函数

为何需要? 在为类重载二元操作符时,需要用到友元关系,方便使用。
存在的主要目的是作为类扩展接口的组成部分。
e.g Time乘以double可以用成员函数重载,但double乘以Time时不能,除非要求用户不能如此调用。否则应该引入友元函数重载。
一个常用的友元重载则是 cout << Time,而非用成员函数的 Time<< cout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 前一种成员函数重载 Time*double
Time Time::operator*(double mul) const
{
Time result;
long totalminutes = hours * mul * 60 + minutes * mul;
result.hours = totalminutes /60;
result.minutes = totalminutes %60;
return result;
}

// 后一种友元重载 double*Time
// 第一步:原型放入类的声明之中
friend Time operator*(double m, const Time & t);
// 第二步:定义编写
Time operator*(double mul, const Time & t)
{
Time result;
long totalminutes = t.hours * mul * 60 + t.minutes * mul;
result.hours = totalminutes /60;
result.minutes = totalminutes %60;
return result;
}

为了cout <<连续可输出,友元声明如下:

1
2
3
4
5
ostream & operator<< (ostream & os, const Time & t)
{
os << t.hours << " hours" << t.minutes<< " minutes";
return os;
}

接受单一参数的构造函数为类的类型转换提供了蓝图(blueprint)

蓝图是一个有意思的词语,后续多态也会继续接触到,是一个隐性类型表征;在类的类型转换上,需要尤其注意编译器二义性转换的问题。
警告:谨慎地使用隐式转换函数。explicit定义类的构造函数,则相关对象的类型转换需要显式调用,不能隐式转换。

类声明描述了如何分配内存,但并不执行分配内存

static int num;的初始化是在类声明之外,int className::num=0;

1
2
3
4
5
6
7
StringBad sailor = sports;

//等价于
StringBad sailor = StringBad(sports);

// 则原型为
StringBad (const StringBad &); //由于不知道更新类静态变量,导致计数出问题

隐式成员函数

C++自动提供下列成员函数

  • 默认构造函数
  • 复制构造函数
  • 赋值操作符
  • 默认析构函数
  • 地址操作符
    析构用了delete的类,所有对象生成的构造函数都应该使用new,否则会引起浅复制析构的错误。绝对避免试图删除已经删除的数据的行为!

书中的解决方案:使用deep copy,每个对象有自己的数据,而不是引用。
增加复制构造函数和赋值操作符,使类正确管理对象使用的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 拷贝函数深复制
StringBad::StringBad(const StringBad & st)
{
num_strings++;
len = st.len;
str = new char[len+1];
std::strcpy(str, st.str); // 复制构造,深复制而非隐式浅复制
}

// 赋值深复制:并非创建对象,而是对已有对象操作
StringBad & StringBad::operator=(const StringBad & st)
{
if(this == &st) // assigned to itself
return *this;
delete [] str; // free old
len = st.len;
str = new char [len+1];
strcpy(str, st.str);
return *this;
}

适配数组指针的释放语法

1
2
3
4
5
6
7
8
9
10
11
char words[15]="bad idea";
char * p1 = words;
char * p2 = new char;
char * p3;
// delete p1,p2,p3; suitable way

// insuitable way, undefined
delete [] p1;
delete [] p2;
delete [] p3;

new 对应 delete, delete[] 对应new []

初始化列表

执行在构造函数之前,因此可用于对const常量进行赋值,对声明为引用的类成员也类似。

  • 只能用于构造函数
  • 必须以此初始化非静态const数据成员
  • 必须以此初始化引用数据成员

继承is-a

用virtual虚函数以及动态指针来实现多态(dynamic binding动态编译,需额外开销),派生可自动向基类类型转换,称为向上强制转换。反之则不可,需显式转换。
引出C++指导原则之一:不要为不使用的特性付出代价

虚函数工作原理

编译器处理虚函数会增加一个隐藏成员指向该函数的地址,若派生重定义了虚函数,则该指针指向新的函数地址。
多态在内存和执行带来一定的成本:1.每个对象因存储地址而增大 2.编译器要为每个类创建虚函数地址表(数组) 3.每个函数调用需要额外查找表中的地址
重载的虚基函数在派生实现时改动需要全部一起改动,称为类型协变

应当把所有派生重新定义的函数再基类设置为虚函数,如果强制需重新定义则=0成纯虚函数

public/protected/private继承

派生公共继承关系是is-a,继承了基类的接口;其他两种是has-a关系,继承了成员成为私有,只可在声明内部使用;

构造函数,析构函数,=号,友元不能自动继承,需重新声明并实现。
protect继承则派生声明不能直接访问基类私有成员,通过public方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//单例模式
class TheOnlyInstance
{
public:
static TheOnlyInstance * GetTheOnlyInstance()
{
static TheOnlyInstance obj;
return &obj;
}

protect:
TheOnlyInstance(){}
private:
// other data
};
// 调用时 TheOnlyInstance * p = TheOnlyInstance::GetTheOnlyInstance();

传对象函数尽量用引用,避免构造和析构的开销;可以将派生对象用等号赋给基类对象,但相反则需提前明确定义。传引用可明确派生对象的类型,否则值引用可能会被编译器自动类型转换,发生意想不到的事情。也可使用dynamic_cast<const baseDMA &>(hs)的方式强制类型转换。

计时,常用于测试代码的运行时间和效率。

以往用法ctime

1
2
3
4
5
6
7
#include <ctime>
using namespace std;

clock_t start = clock();
func();
clock_t end = clock();
cout << "spend " << (double)(end-start)/CLOCKS_PER_SEC <<" second" << endl; // 精确到毫秒

chrono用法

1
2
3
4
5
6
7
8
9
10
#include <chrono>
using namespace std;
using namespace chrono;

auto start = system_clock::now();
func();
auto end = system_clock::now();
auto duration = duration_cast<microseconds>(end-start);
cout <<"spend " << double(duration.count()) * microseconds::period::num / microseconds::period::den << " second" << endl;

其中,auto为自动类型;除了system_clock,还可用steady_clock和high_resolution_clock; microseconds表示微妙,甚至还有nanoseconds纳秒;num和den表示计时单位的分子和分母。