std常用数学函数
  • hypot()用来求三角形的斜边长,其原型为:double hypot(double x, double y);,需要#include <stdio.h>

  • fabs函数是求绝对值的函数,函数原型是extern float fabs(float x),需要#include <math.h>

对double/float数据,一定要使用fabs函数。如果用了abs,就会出现bug,因为返回是int

  • 反正切函数 atan2

atan2返回给定的 X 及 Y 坐标值的反正切值。反正切的角度值等于 X 轴与通过原点和给定坐标点 (Y坐标, X坐标) 的直线之间的夹角。结果以弧度表示并介于-pipi之间(不包括-pi)。 而atan(a/b)的取值范围介于-pi/2pi/2之间,不包括±pi/2

  • std::sin 等三角函数
  • std::fmod

计算两个浮点数相除的余数

1
2
double x = 7.5, y = 2.1;
double result = std::fmod(x, y); // 1.2

floor, ceil, round

std::floorstd::ceil都是对变量进行取整,只不过取整的方向不同。 std::floor是向下取整数,std::ceil是向上取整数。
比如输入3.6,前者输出3,后者输出4。但输入3.2,结果不变。

std::round 才是四舍五入

找最大最小

std::min(const T& a, const T& b); 求两个参数的最小值

std::max(const T& a, const T& b); 求两个参数的最大值

以下库函数需要 #include <algorithm>

minmax_element找出容器中最小和最大元素的迭代器,作为std::pair返回。时间复杂度为 O(n)

1
2
template< class ForwardIt >
std::pair<ForwardIt,ForwardIt> minmax_element( ForwardIt first, ForwardIt last );

1
2
3
4
5
6
7
8
std::vector<int> v = { 1, 2, 5, 4, 100, 0, -199, 33 };

auto result = std::minmax_element(v.begin(), v.end());
// 输出首次出现的最小元素
std::cout << "min element is: " << *result.first << '\n';

// 输出首次出现的最大元素
std::cout << "max element is: " << *result.second << '\n';

C++17 增加了min_element返回迭代器位置, 复杂度为 O(n)max_element返回迭代器位置,复杂度为 O(n)

1
2
3
4
5
6
7
8
9
std::vector<int> v{3, 1, 4, 1, 5, 9};

std::vector<int>::iterator min = std::min_element(v.begin(), v.end());
std::cout << "min element at: " << std::distance(v.begin(), min) << std::endl;
std::cout << "min value is: " << *min << std::endl;

std::vector<int>::iterator max = std::max_element(v.begin(), v.end());
std::cout << "max element at: " << std::distance(v.begin(), max) << std::endl;
std::cout << "max value is: " << *max << std::endl;


判断 inf, nan

1
2
3
4
5
bool isinf( float arg );

bool isfinite( float arg );

bool isnan( float arg );

numeric_limits

模板类,常用于提供很大很小的极值,需要#include <limits>

1
2
3
4
5
cout<<std::numeric_limits<int>::max()<<endl;
cout<<std::numeric_limits<long>::max()<<endl;

cout<<std::numeric_limits<int>::min()<<endl;
cout<<std::numeric_limits<long>::min()<<endl;

结果

1
2
3
4
2147483647
9223372036854775807
-2147483648
-9223372036854775808


C++的新类型 optional, variant, any

std::optional

std::optional<T>代表一个可能存在的T值,实际上是一种Sum Type。常用于可能失败的函数的返回值中,比如工厂函数。在C++17之前,往往使用T*作为返回值,如果为nullptr则代表函数失败,否则T*指向了真正的返回值。但是这种写法模糊了所有权,函数的调用方无法确定是否应该接管T*的内存管理,而且T*可能为空的假设,如果忘记检查则会有SegFault的风险。

使用指针和optional的代码如下:

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
#include <optional>

// c++17 更安全和直观
std::optional<int> func(bool param)
{
if(param)
{
return 1;
}
return nullopt;
}

int* func2(bool param)
{
int p = 1;
int* ptr = &p;
if(param)
{
return ptr;
}
return nullptr;
}


auto f = func(false);
if(f.has_value() )
cout << f.value() << endl;

auto f2 = func2(false);
if(f2)
cout << *f2 << endl;

如果换成unique_ptr代替optional,后果是多一次堆内存分配。另外如果用了unique_ptr做类成员,这个类就不能被拷贝了,optional没有这个问题。二者的本质区别: unique_ptr是引用语义,optional是值语义

variant

std::variant 是 C++17 引入的一种类型安全的联合体,用来存储多个可能类型中的一种值,且保证使用时的类型安全。相比于传统的 union, std::variant不仅能够存储不同类型的值,还能自动管理复杂类型的构造与析构。

使用 std::variant 可以定义一个变量,该变量可以持有多种不同类型的值,但一次只能存储一种。std::variant 提供了一种类型安全的方式来处理多种类型,可以用在函数参数中以接受这些类型。

构造和赋值:您可以直接使用各种支持的类型初始化 std::variant, 并在需要时将其传递给函数。

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
std::variant<int, float, std::string> v;  // v 可以是 int、float 或 std::string
cout << "size of v: "<<sizeof (v) << endl;
v = 42;
cout << std::get<int>(v) << endl;

v = 12.3f;
cout << std::get<float>(v) << endl;
cout << std::get<1>(v) << endl;

// 可以使用 std::holds_alternative<T>(variant) 来判断 std::variant 当前是否持有某种类型
if(std::holds_alternative<int>(v) )
std::cout << "v holds an int" << std::endl;
else
std::cout << "v doesn't holds an int" << std::endl;

try {
std::cout << std::get<int>(v); // 当前不是 int 类型,会抛出异常
}
catch (const std::bad_variant_access& e)
{
std::cout << "Wrong type access: " << e.what() << std::endl;
}

//使用 v.index() 获取当前存储值的类型在 std::variant 中的索引
std::cout << "current value index: " << v.index() << std::endl; // 0 表示 int,1 表示 float,依次类推

v = "Hello, std::variant!";
cout << std::get<std::string>(v) << endl;

std::variant 可以作为函数参数,接受多种类型的值,这些类型是在定义 std::variant 时指定的

std::variant 主要用于以下场景:

  1. 多类型返回值: 一个函数可能返回不同类型的值,例如成功返回数据,失败返回错误信息
  2. 替代 union: std::variant 是 union 的现代、安全替代品,支持更多的类型和安全检查
  3. std::variant 可以表示有限状态机的状态,每个状态用不同的类型表示

类型安全: 使用 std::visit 可确保访问的值是有效的,避免了类型错误

1
2
3
std::visit([](auto&& arg) {
std::cout << "use visit to print value: " << arg << std::endl; // 打印不同类型的值
}, v);

std::any

std::any是一个可以存储任何可拷贝类型的容器,C语言中通常使用void*实现类似的功能,与void*相比,std::any具有两点优势:

std::any更安全:在类型T被转换成void*时,T的类型信息就已经丢失了,在转换回具体类型时程序无法判断当前的void*的类型是否真的是T,容易带来安全隐患。而std::any会存储类型信息,std::any_cast是一个安全的类型转换。
std::any管理了对象的生命周期,在std::any析构时,会将存储的对象析构,而void*则需要手动管理内存。
std::any应当很少是程序员的第一选择,在已知类型的情况下,std::optional, std::variant和继承都是比它更高效、更合理的选择。只有当对类型完全未知的情况下,才应当使用std::any,比如动态类型文本的解析或者业务逻辑的中间层信息传递。


lambda表达式

Lambda表达式可以直接在需要调用函数的位置定义短小精悍的函数,而不需要预先定义好函数。适合短小不需要复用函数的场景

缺点:

  1. Lamdba表达式语法比较灵活,增加了阅读代码的难度。如果我要找某个函数的调用,搜索函数名即可,但是lambda表达式往往没有名称,我要记住它的位置

  2. 难以调试:lambda表达式往往是一种匿名函数,这意味着函数名称并不明确,所以在调试中很难分清楚是哪个函数出错了。

  3. 存在捕获的性能开销:捕获变量或对象需要花费额外的时间,而这些时间开销可能会在一些精细的程序中成为问题

  4. 不适合复用的场景

原理

编译器会把一个lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符,实现了一个operator()方法

auto print = []{cout << "Hello World!" << endl; };

编译器会把上面这一句翻译为下面的代码:

1
2
3
4
5
6
7
8
9
10
class print_class
{
public:
void operator()(void) const
{
cout << "Hello World!" << endl;
}
};
//用构造的类创建对象,print此时就是一个函数对象
auto print = print_class();

1.捕获列表。捕获列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据它来判断接下来的代码是否是Lambda函数,捕获列表能够捕捉上下文中的变量以供Lambda函数使用。

2.参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号()一起省略。

3.可变规则。mutable修饰符,但使用很少。默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)

4.异常说明。用于Lamdba表达式内部函数抛出异常,使用也很少

5.返回类型。 可以在不需要返回值的时候也可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。

6.lambda函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

捕获列表

[]包括起来的是捕获列表,捕获列表由多个捕获项组成,并以逗号分隔。捕获列表有以下几种形式:

  • []表示不捕获任何变量
1
2
3
4
5
6
auto function = ([]{
std::cout << "Hello World!" << std::endl;
}
);

function();

从这个例子可以看出,Lambda表达式可以作为仿函数

  • [var]表示值传递方式捕获变量var
1
2
3
4
5
6
7
int num = 100;
auto function = ([num]{
std::cout << num << std::endl;
}
);

function();
  • [=]表示值传递方式捕获所有父作用域的变量(包括this)
1
2
3
4
5
6
7
8
9
int index = 1;
int num = 100;
auto function = ([=]{
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);

function();
  • [&var]表示引用传递捕捉变量var

  • [&]表示引用传递方式捕捉所有父作用域的变量(包括this)

输入参数

除了捕获列表之外,lambda还可以接受输入参数。参数列表是可选的,并且在大多数方面类似于函数的参数列表。

1
2
3
4
5
auto function = [](int first, int second){
return first + second;
};

function(100, 200);


可变规格mutable和异常使用较少,不研究了。


for_each应用实例

1
2
3
int a[4] = {11, 2, 33, 4};
sort(a, a+4, [=](int x, int y) -> bool { return x%10 < y%10; } );
for_each(a, a+4, [=](int x) { cout << x << " ";} );


find_if应用实例

1
2
3
4
5
6
7
8
9
10
int x = 5;
int y = 10;
deque<int> coll = { 1, 3, 19, 5, 13, 7, 11, 2, 17 };

auto pos = find_if(coll.cbegin(), coll.cend(), [=](int i) {
return i > x && i < y;
});

if(pos != coll.end() )
cout << *pos << endl;


Lamdba表达式应用于函数指针与function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <functional>
using namespace std;

int main(void)
{
auto add = [](int a, int b) { return a + b; };
std::function<int(int, int)> Add = [=](int a, int b) { return a + b; };

cout << "add: " << add(1, 2) << endl;
cout << "Add: " << Add(3, 4) << endl;

return 0;
}


异常out_of_range

使用C++容器类访问成员时由于使用问题可能会遇到 terminate called after throwing an instance of ‘std::out_of_range’”或者”Abort message: ‘terminating with uncaught exception of type std::out_of_range

原因如下:

  • 索引超出容器范围
  • 容器为空,在尝试访问一个空容器的元素时,也会触发此异常,因为容器中没有任何元素可供访问。
  • 错误的索引计算

我们可以使用std::out_of_range捕获这个异常

1
2
3
4
5
6
7
8
9
10
11
12
vector<int>  vec;
vec.push_back(1);
vec.push_back(2);

try
{
cout<< vec.at(vec.size() ) <<endl;
}
catch (const std::out_of_range& err)
{
std::cerr << "\nOut of range error:" << err.what()<< endl;
}


CMAKE_CXX_COMPILE_FEATURES

CMAKE_CXX_COMPILE_FEATURES变量用于获取当前C++编译器支持的编译特性列表,列表中是一些定义在CMAKE_CXX_KNOWN_FEATURES(C++已知特性)中的特性名字,比如cxx_lambdas即为当前编译器支持lambda表达式。

1
2
3
4
5
6
7
8
9
10
11
12
message("Your C++ compiler supports these C++ features:")
foreach(i ${CMAKE_CXX_COMPILE_FEATURES})
message("${i}")
endforeach()
# list命令在CMAKE_CXX_COMPILE_FEATURES 查找 cxx_std_20, 如果能找到就说明编译支持C++20
list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_std_20 _cxx20_enable)
if(_cxx11_enable)
message(STATUS "C++ 20 supported")
else()
message(FATAL_ERROR "Compiler not supported C++ 20 standard")
endif()
unset(_cxx20_enable)

C++的不合理之处

以下功能不太合理或者比较冷门,应该尽量少使用

  • Exception

  • 多继承

  • 继承指定类型是public还是private,几乎全是public

  • delete []

  • char和string之间的关系

  • 隐式转换

  • 模板

  • 友元

  • 数组类型,能退化成指针,但不能当返回值类型,还不能用另一个数组初始化。


RDP算法

这个算法很简单,可以说是对路径的拉直。可以用于处理A*。也可以说是无约束的降采样,可以消除小的抖动,但无法保证路径仍然安全。

有时路径规划获得的点太多,或者录制机器人路径获得的点太多,可以使用RDP算法,对得到的点进行缩减,用少量目标点表示路径。

首先,将起始点和终点连成一条线,找到剩下的点中距离这条线垂直距离最大的点P,记住点P和最大距离max,如果max小于设定的距离epsilon,则直接返回起始点和终止点就可以了。由于最大距离离这条直线也不远,因此可以把所有的点都看做在这条直线上,相当于把整个路径按照首尾拉直了。

如果max大于设定的距离epsilon,那么开始递归,以点P为中心将线段分为两部分,每一部分都重复上述过程,直到递归结束,相当于对两部分各自拉直。

参考:
一种路径优化方法-拉直法
矢量数据压缩算法提取直线(RDP)


opencv基本使用

void cv::circle(InputOutputArray img, Point center, int radius, const Scalar& color, int thickness = 1, LineTypes lineType = LINE_8, int shift = 0);

  • img: 输入输出参数,表示待绘制的目标图像。
  • center: 输入参数,表示圆心坐标,是一个 cv::Point 类型的对象。
  • radius: 输入参数,表示圆的半径。
  • color: 输入参数,表示绘制圆的颜色以及透明度,是一个 cv::Scalar 类型的对象。
  • thickness: 可选参数,表示圆线条的宽度。默认值为 1 表示绘制一个像素宽度的圆,如果设置为负值,则表示绘制一条填充的圆。
  • lineType
    : 可选参数,表示圆边界的类型,可以取以下几个值:
    cv::LINE_4: 表示绘制四个相邻的点的圆边界,默认值。
    cv::LINE_8: 表示绘制八个相邻的点的圆边界。
    cv::LINE_AA: 表示绘制抗锯齿的圆边界。

  • shift: 可选参数,表示坐标点像素值所占用的位数,默认值为 0。

矩形

void cv::rectangle(InputOutputArray img, Rect rect, const Scalar& color, int thickness = 1, LineTypes lineType = LINE_8, int shift = 0)

  • img: 输入输出参数,表示待绘制的目标图像。
  • rect: 输入参数,表示矩形,是一个 cv::Rect 类型的对象,可以通过传递左上角和右下角坐标的方式来定义一个矩形。
  • color: 输入参数,表示绘制矩形的颜色以及透明度,是一个 cv::Scalar 类型的对象。
  • thickness: 可选参数,表示矩形边框的宽度。默认值为 1 表示绘制一个像素宽度的矩形,如果设置为负值,则表示绘制一条填充的矩形。
  • lineType: 可选参数,表示矩形边框的类型,可以取以下几个值:
    cv::LINE_4: 表示绘制四个相邻的点的矩形边框,默认值。
    cv::LINE_8: 表示绘制八个相邻的点的矩形边框。
    cv::LINE_AA: 表示绘制抗锯齿的矩形边框。
  • shift: 可选参数,表示坐标点像素值所占用的位数,默认值为 0。

Rect函数的基本形式是Rect(x, y, width, height),其中x和y代表矩形左上角的坐标,widthheight分别代表矩形的宽度和高度。

如果创建一个Rect对象rect(100, 50, 50, 100),有以下常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
rect.area();     //返回rect的面积 5000

rect.size(); //返回rect的尺寸 [50 × 100]

rect.tl(); //返回rect的左上顶点的坐标 [100, 50]

rect.br(); //返回rect的右下顶点的坐标 [150, 150]

rect.width(); //返回rect的宽度 50

rect.height(); //返回rect的高度 100

rect.contains(Point(x, y)); //返回布尔变量,判断rect是否包含Point(x, y)点


用opencv处理轮廓

Rect boundingRect(InputArray points)

寻找包裹轮廓的最小正矩形。矩形的边界与图像边界平行。 唯一一个参数是输入的二维点集,可以是 vector 或 Mat 类型。只需要 #include "opencv2/opencv.hpp"

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
#include<opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
int main(){
Mat src = imread("/home/user/test.jpg");
imshow("src", src);

Mat gray, bin_img;
cvtColor(src, gray, COLOR_BGR2GRAY); //将原图转换为灰度图
imshow("gray", gray);

//二值化
threshold(gray, bin_img, 150, 255, THRESH_BINARY_INV);
imshow("bin_img", bin_img);

//寻找最外围轮廓
vector<vector<Point> >contours;
findContours(bin_img, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);

//绘制边界矩阵
RNG rngs = { 12345 };
Mat dst = Mat::zeros(src.size(), src.type());
for (int i = 0; i < contours.size(); i++)
{
Scalar colors = Scalar(rngs.uniform(0, 255), rngs.uniform(0, 255), rngs.uniform(0, 255));
drawContours(dst, contours, i, colors, 1);
Rect rects = boundingRect(contours[i]);
rectangle(dst, rects, colors, 2);
}
imshow("dst", dst);

waitKey(0);
}

RotatedRect minAreaRect(InputArray points)

寻找包裹轮廓的最小斜矩形。唯一一个参数是输入的二维点集,可以是 vector 或 Mat 类型。只需要 #include "opencv2/opencv.hpp"

boundingRect 返回结果的区别是:矩形的边界不必与图像边界平行

RotatedRect旋转矩形结构体

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
    Mat gray, bin_img;
cvtColor(src, gray, COLOR_BGR2GRAY); //将原图转换为灰度图
imshow("gray", gray);

//二值化
threshold(gray, bin_img, 150, 255, THRESH_BINARY_INV);
imshow("bin_img", bin_img);

//寻找最外围轮廓
vector<vector<Point> >contours;
findContours(bin_img, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);

//绘制最小边界矩阵
RNG rngs = { 12345 };
Mat dst = Mat::zeros(src.size(), src.type());
Point2f pts[4];
for (int i = 0; i < contours.size(); i++)
{
Scalar colors = Scalar(rngs.uniform(0, 255), rngs.uniform(0, 255), rngs.uniform(0, 255));
drawContours(dst, contours, i, colors, 1);
RotatedRect rects = minAreaRect(contours[i]);
// 确定旋转矩阵的四个顶点
rects.points(pts);
for (int i = 0; i < 4; i++)
line(dst, pts[i], pts[(i + 1) % 4], colors, 2);

}
}

double pointPolygonTest(InputArray contour, Point2f pt, bool measureDist)

计算点与轮廓的距离及位置关系,只需要 #include "opencv2/opencv.hpp"

  • contour: 所需检测的轮廓对象

  • pt: Point2f 类型的pt, 待判定位置的点

  • measureDist: 是否计算距离的标志, 当其为true时, 计算点到轮廓的最短距离, 当其为false时, 只判定轮廓与点的位置关系, 具体关系如下:

① 返回值为-1, 表示点在轮廓外部

② 返回值为0, 表示点在轮廓上

③ 返回值为1, 表示点在轮廓内部

如果不需要知道具体的距离,建议将第三个参数设为false,这样速度会提高2到3倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//计算点到轮廓的距离与位置关系
Mat srcImg = imread("1.png");
imshow("src", srcImg);

Mat dstImg = srcImg.clone();
cvtColor(srcImg, srcImg, CV_BGR2GRAY);
threshold(srcImg, srcImg, 100, 255, CV_THRESH_BINARY);
imshow("threshold", srcImg);

vector<vector<Point>> contours;
vector<Vec4i> hierarcy;
findContours(srcImg, contours, hierarcy, CV_RETR_TREE, CV_CHAIN_APPROX_NONE);//TREE:提取所有轮廓 NONE:画出轮廓所有点
cout << "contours.size()=" << contours.size() << endl;
for (int i = 0; i < contours.size(); i++)//遍历每个轮廓
{
for (int j = 0; j < contours[i].size(); j++)//遍历轮廓每个点
{
cout << "(" << contours[i][j].x << "," << contours[i][j].y << ")" << endl;
}
}
double a0 = pointPolygonTest(contours[0], Point(3, 3), true);//点到轮廓的最短距离
double b0 = pointPolygonTest(contours[0], Point(212, 184), false);//点与轮廓的位置关系:-1表示外部;0表示在轮廓上;1表示轮廓内部

求轮廓面积 cv::contourArea

double contourArea( InputArray contour, bool oriented = false );

  • InputArray类型的contour,输入的向量,二维点(轮廓顶点),可以为std::vector或Mat类型。
  • bool类型的oriented,面向区域标识符。若其为true,会返回一个带符号的面积值,正负取决于轮廓的方向。
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
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main(void)
{
Mat A = Mat::zeros(500, 500, CV_8UC1);
circle(A, Point2i(100, 100), 3, 255, -1);
circle(A, Point2i(300, 400), 50, 255, -1);
circle(A, Point2i(250, 100), 100, 255, -1);
circle(A, Point2i(400, 300), 60, 255, -1);

std::vector<std::vector<cv::Point> > contours; // 创建轮廓容器
std::vector<cv::Vec4i> hierarchy;

cv::findContours(A, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE, cv::Point());
if (!contours.empty() && !hierarchy.empty())
{
std::vector<std::vector<cv::Point> >::const_iterator itc = contours.begin();
// 遍历所有轮廓
int i = 1;
while (itc != contours.end())
{
double area = cv::contourArea(*itc);
cout << "第" << i << "个轮廓的面积为:" << area << endl;
i++;
itc++;
}
}
imshow("A", A);
waitKey(0);
system("pause");
return 0;
}

第一个圆的面积并不是9π,而是20。面积值是按照轮廓的内部面积进行计算的,会损失一些。

参考: cv::contourArea


深入探究虚函数 (二) 虚函数表vtbl和虚表指针vptr

虚函数表是通过一块内存来存储虚函数的地址,它实际是一个函数指针数组,每一个元素都是虚函数的指针,虚函数表的最后一个元素是一个空指针。假如虚函数类型为int,函数指针就是int*类型.虚函数表将被该类的所有对象共享,每个对象内部包含了一个虚表指针,指向虚函数表

有虚函数的类的最开始部分就是虚指针,它指向虚函数表起始地址,类型为int**(如果虚函数类型int),表中存放虚函数的地址。通过这个指针可以获取到该类对象的所有虚函数,包括父类的。

在编译期,编译器完成了虚表的创建,而虚指针在构造函数期间被初始化

因为派生类会继承基类的虚函数表,所以通过虚函数表,我们就可以知道该类对象的父类,在转换的时候就可以用来判断对象有无继承关系。派生类中增加的虚函数,如果覆盖了基类的虚函数,虚函数表中会替换相应的基类虚函数,地址换成派生类的;如果没有覆盖基类的虚函数,就添加到原虚函数表后面,以空指针结尾. 所以说派生类的虚函数表中的函数地址不是连续的,基类的是连续的。

类Base的虚表如下图:
Base.png
如果派生类Derived没有覆盖基类的虚函数,它的虚函数表如下图:

如果覆盖vFunc1,则替换Base的vFunc1;如果还定义了一个虚函数vFunc3,那么继续往虚函数表之后填,最后一个数组成员还是空指针

更详细的说明

当类有虚函数时候,类的第一个成员变量是一个虚函数指针,而this指针的值和第一个成员变量的地址相同(this指针指向第一个成员变量)。因此当有虚函数时,this指针的值等于虚函数指针的地址 this==&_vptr;

参考:
C++ 虚函数表
深入C++对象模型&虚函数表