C++有用的基础知识
有用的基础知识
基础不牢,地动山摇
1.对象构造时执行的次序问题
- 一个类的实例化过程往往是发生了如下过程:
- 若存在基类,则基类先初始化。
- 若存在成员变量,则按照定义先后的顺序初始化。
- 对象的析构顺序则取之相反,是从外到内、从后到前发生。
- 如代码所示,定义了4个类,A、B、C、Base,其中C继承自Base,A、B、Base基本只输出基本构造、析构与赋值信息,定义如下
查看类A、B、Base的定义代码
copy
- 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
class Base
{
public:
Base() { cout << "base construct\n"; };
~Base(){cout << "base destory\n";}
};
class A
{
public:
A()
{
cout << "A construct\n";
}
A(const A &a)
{
cout << "A copy construct\n";
}
~A(){cout << "A destory\n";}
void operator=(const A &a)
{
cout << "A assign function\n";
}
};
class B
{
public:
B()
{
cout << "B construct\n";
}
B(const B &b)
{
cout << "B copy construct\n";
}
~B(){cout << "B destory\n";}
void operator=(const B &b)
{
cout << "B assign function\n";
}
};
查看测试1
copy
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
class C : public Base
{
public:
B b;
A a;
C(){};
~C(){cout << "C destory\n";};
};
int main()
{
C c;
return 0;
}
运行结果:
base construct
B construct
A construct
C destory
A destory
B destory
base destory
查看测试2
```cpp class C : public Base { private: B _b; A_a; public: C(A& a,B& b):_a(a),Base(){ _b=b; }; ~C(){cout << "C destory\n";}; }; int main() { A a; // 1 B b; // 2 C c(a,b); return 0; } 结果如下: --注释掉的结果是 1 2 代码生成的 //A construct //B construct base construct B construct A copy construct B assign function C destory A destory B destory base destory //B destory //A destory ```- 根据结果有几点可以得出:
- 基类对象仍然是最先构建的。
- 初始化列表不能改变类成员初始化顺序。C类中成员
_b
永远在_a
前面。 - 在初始化列表中初始化的话,会调用复制构造函数。
- 在函数体中初始化会再次调用赋值函数,且是最后执行。
- 当需要对成员变量初始化特定值时,优先使用初始化列表方法。避免先初始化后再赋值的资源浪费行为。
- 注意:静态成员变量的初始化顺序和析构顺序是一个未定义的行为。
- 静态成员变量不初始化则不会分配内存,无法访问。调用就会崩溃。
- 从C++11开始,除 static 数据外,基本都允许在声明处初始化成员变量。但有以下特例:
- 被常量表达式
constexpr
修饰的static可以在声明处初始化。 - const static 整型可以声明处初始化。整型有short、int、long等)。
- 被常量表达式
- 在C++11以前,除枚举类型和 const static 整型外,不可以声明处初始化。
- const non-static类型必须用初始化列表语法初始化。
2.虚函数探究
- 虚函数是指被
virtual
关键字修饰的成员函数,作用是用来实现多态性。可为不同数据类型的实体提供统一的接口。- 虚函数的多态性具体的讲是将函数名动态绑定到函数入口地址(动态联编),而普通的成员函数在编译时就确定了(静态联编)。
- 静态函数不可以声明为虚函数,同时也不能被
const
和volatile
关键字修饰。 - 构造函数也不可以声明为虚函数。同时除了
inline|explicit
之外,构造函数不允许使用其它任何关键字。 - 只要一个类有可能会被其它类所继承, 就应该声明虚析构函数。
- 虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
- 同一个类的各个实例化对象共用一个虚函数表。一个类继承多个基类时,有一个虚函数表,多个虚函数指针。
- 虚函数运行过程
- 当一个类实例化时,若存在虚成员函数,则会在构造函数中初始化一个虚表的指针
vptr
和对应的虚函数表vtable
。- 虚表存储着所有虚函数的函数指针,通过地址偏移来调用对应函数。
vptr
虚表地址在类地址的前8字节。
- 虚表存储着所有虚函数的函数指针,通过地址偏移来调用对应函数。
- 当使用基类的指针或者引用指向派生类虚函数时,会调用指向或者引用的对象的类型。
- 首先获取虚表地址
vptr
,在虚表中vtable
得到需要的函数指针,最后进行调用。
- 首先获取虚表地址
- 当一个类实例化时,若存在虚成员函数,则会在构造函数中初始化一个虚表的指针
- 派生类会继承基类的虚表内容(各函数指针),当新增或者修改时修改该表即可,相同的虚函数的地址不会改变。
virtuallTest.cpp
copy
- 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
using namespace std;
class A
{
private:
int a=0;
public:
virtual void speak()
{
cout << "A: hello!!\n";
}
};
class B : public A
{
public:
void speak()
{
cout << "B: hello--\n";
}
};
class C : public A
{
};
typedef void (*Fun)(void);
int main()
{
A *a = new A;
B *b = new B;
C *c = new C;
// a ->A*指针; (int64_t*)a ->取前8位字节; *(int64_t*)a ->前八个字节的指针解引用,虚指针
// (int64_t*)*(int64_t*)a ->虚表前8位字节;*((int64_t*)*(int64_t*)a) ->虚表第一个项的指针解引用,第一个虚函数指针
cout << "基类A的虚表地址:" << *(int64_t *)a << ", 第一个虚函数地址0x" << hex << *((int64_t *)*(int64_t *)a) << " \n";
cout << "派生类B的虚表地址:" << *(int64_t *)b << ", 第一个虚函数地址0x" << hex << *((int64_t *)*(int64_t *)b) << " \n";
cout << "派生类C的虚表地址:" << *(int64_t *)c << ", 第一个虚函数地址0x" << hex << *((int64_t *)*(int64_t *)c) << " \n";
Fun fun = (Fun) * ((int64_t *)*(int64_t *)a);
fun(); //
A *spearker1 = (A *)b;
spearker1->speak();
delete a;
delete b;
delete c;
return 0;
}
// 输出:
// 基类A的虚表地址:4330995984, 第一个虚函数地址0x10225b120
// 派生类B的虚表地址:10225c138, 第一个虚函数地址0x10225b190
// 派生类C的虚表地址:10225c168, 第一个虚函数地址0x10225b120
// A: hello!!
// B: hello--
-
上述各类在内存中的布局如下图所示
-
static_cast<>()
可以将派生类转换为基类,不会有额外负担。相反不可以。 -
dynamic_cast<>()
可以将基类转换成派生类,是通过查询RTTI信息判断该类是不是本基类派生的子类,不是返回空。- 因为要查询,所以效率相对更差。
-
对于多重继承,有几个父类就有几个虚表指针,详情:https://www.freesion.com/article/3325439236/
参考视频:
https://www.bilibili.com/video/BV1bL4y1p7d5
https://www.bilibili.com/video/BV15g4y1a7F3
3.右值引用
变量、变量名、左值、右值、右值引用、移动构造、移动赋值
-
变量与变量名:
-
变量是内存中的一块区域,是可供程序操作的存储空间。这块区域的值(内容)一般是可以“变”的。(提供一座房子,但住谁不管)
-
变量名是用来标识一块内存区域(变量)。变量名是不会被存储的,可以观察到,在对应汇编中是不存在变量名的。
-
一般说变量,通常指两者的结合。也就是,一个提供具名的、可供程序操作的存储空间。
-
变量初始化与赋值
copy- 1
- 2
- 3
- 4
- 5
void fun() { int b=1; b=2; }
copy- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
fun(): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], 1 mov DWORD PTR [rbp-4], 2 nop pop rbp ret
-
-
在C++中“对象”和“变量”一般可以互换使用,变量是左值,但存储内容是右值。
-
当一个对象(变量)被用作右值时(包含纯右值和将亡值),用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置),一般可以修改其存储内容。
-
不同运算符对元算对象要求也各不相同,有的要左值对象,有的要右值对象。返回结果也有差异,有的得到左值结果,有的是右值结果。
-
一个原则是在需要右值的地方可以用左值替代,但是不能把右值当成左值(位置)使用。当一个左值被当成右值使用时,实际上用的是他的内容(值)。
-
引用,是某个已存在变量的另一个名字。
-
右值引用:也就是必须绑定到右值(纯右值,将亡值)的引用。通过
&&
来获得右值引用。- 可绑定临时对象上或字面常量,延长其生命期。
- 不能将一个右值引用绑定到一个右值类型的变量上。
-
右值引用有两大作用:移动语义和完美转发。能进一步优化性能。
-
可以将一个const的左值引用绑定到右值上。
lrvalue.cpp
copy
- 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
using namespace std;
class A
{
private:
int *ptr;
public:
A()
{
ptr = new int(-1);
};
~A() { delete ptr; };
// 复制构造
A(const A &a) : ptr(new int(*a.ptr))
{
cout << "复制构造" << endl;
}
// 注意没有const 参数对象指针复制为空,即把该对象内存的拿来用,不用另外开辟新内存
A(A &&a) : ptr(a.ptr)
{
a.ptr = nullptr;
cout << "移动构造" << endl;
}
//
A &operator=(A &&a)
{
if (this == &a)
return *this;
// 注意要释放拥有的内存
delete ptr;
ptr = a.ptr;
// 原对象a不能再使用了
a.ptr = nullptr;
cout<<"移动赋值"<<endl;
return *this;
}
};
int main()
{
//右值引用 变量a是一个左值,可以取地址等各种操作
//相当于延长了右值11的生命周期
int &&a = 11;
int *p = &a;
*p += 1; //也可以对其进行操作
cout << a << endl; //输出 12
//可以将一个const的左值引用绑定到右值上
const int &b = 20;
int m = 8;
int n = 22;
// int &&c = m; //不允许将右值引用绑定到左值上
// 可以将左值显示转换成对应的右值引用
// 如move()函数之后应该只对m进行赋值或销毁操作
int &&c = std::move(m);
int &&d = std::move(n);
m = std::move(d);
cout << m << endl;
// 普通构造
A obj0;
// 复制构造 会开辟新内存,并复制有关值
A obj1(obj0);
// std::move()强制将左值转换成右值
// 使用移动构造后,不能再对obj0操作,因为内存已经给obj2了
// 移动构造,不用申请新内存,也能减少不必要的复制
A obj2(std::move(obj0));
// A()是匿名对象,右值
obj2 = A();
return 0;
}
动手打一遍,才能记得住。 -- 鲁迅
本文作者:oniisan
本文链接:https://www.cnblogs.com/oniisan/p/basicCPP.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步