目录
1.sizeof()是C++的编译特性,而不是函数。
具体看代码及反汇编:
#include <iostream>
using namespace std;
int main()
{
cout << 4 << endl;
cout << sizeof(int) << endl;
getchar();
return 0;
}
我们在sizeof处设置断点,并debug,同时右键点击反汇编:
汇编代码如下:
可以看到:sizeof(int) 和 4 的汇编代码一模一样,也就是说编译器将sizeof(int)转化为了4,它们底层的汇编实现是一样的。
所以这里回答了很多人误解的错误答案:sizeof()是函数,这是错的;
进一步回答:那函数在汇编中是什么样的?
代码如下:
#include <iostream>
using namespace std;
void test()
{
return;
}
int main()
{
test();
cout << 4 << endl;
cout << sizeof(int) << endl;
getchar();
return 0;
}
如之前的操作,进行反汇编:
在汇编中,函数直接调用了call指令,转向对应的函数地址;而在sizeof中,并没有利用call进行跳转到该函数本身的地址上,而是直接push进去了一个4,也就是值的内容。
综上所述,sizeof()是C++的一种运算符(或者操作符),并不是一个函数。
2.C++默认参数
2.1.默认参数的相关注意事项
这部分是基础,值得复习一下:
- C++允许函数设置默认参数,在调用时可以根据情况省略实参
- 默认参数只能按照从右到左的顺序
- 如果函数同时有声明、实现,默认参数只能放在函数声明中
- 默认参数的值可以是常量,全局符号(全局变量、函数名)
2.2 默认参数在汇编中的实现
直接上代码:
#include <iostream>
using namespace std;
int sum(int a, int b = 4)
{
return a + b;
}
int main()
{
cout << sum(1) << endl;
cout << sum(1, 4) << endl;
getchar();
return 0;
}
debug,反汇编:
这里我们只关注关键指令,可以看到,两个sum函数所调用的函数地址以及push的值都是一样的。
而左边的机器码,基本也是一样的,只有call指令的机器码不同,但这并不是说它们的汇编代码不同,而是这条指令中关乎下一指令的地址,故略微不同。
3.C++中的const
- const是常量的意思,被修饰的变量不可更改
- 如果修饰的是类、结构体(的指针),其成员也不可修改
核心问题:以下五个指针分别是什么含义?
int age=10;
const int* p0 = &age;
int const *p1 = &age;
int * const p2 = &age;
const int * const = &age;
int const * const p4 = &age;
以上指针问题可以用以下结论来解决:
const修饰的是其右边的内容
int age=10;
// p1不是常量, *p1是常量
const int* p1 = &age;
// p2不常量, *p2是常量
int const *p2 = &age;
// p3是常量, *p3不是常量
int * const p3 = &age;
// p4是常量, *p4也是常量
const int * const p4 = &age;
// p5是常量, *p5也是常量
int const * const p5 = &age;
类似的,我们将其扩展到struct或class中:
struct Student
{
int age;
};
Student s1 = { 10 };
Student s2 = { 20 };
其操作1为:
因为const修饰的是*p1,也就是解引用的一个Student实例,那么对此实例进行修改或更改其成员变量是不允许的。
操作2:
其const修饰的是一个Student指针,也就是说对其解引用的Student实例进行修改是没有问题的。而要是想修改它所指向的地方,也就是它的地址,则会报错。
综上所述,只要秉持着“const修饰的是其右边的内容”这一原则,我们就可以清楚的直到const到底修饰的是什么,这样什么可以修改或不可以修改便一目了然。
4.C++中的引用
C语言中可以使用指针(Pointer)间接获取、修改某个值
C++中,使用引用(Reference)可以起到跟指针类似的功能
值得注意的地方:
- 引用相当于变量的别名(基本数据类型、枚举、结构体、类、指针、数组等)
- 对引用做计算,就是对引用所指向的变量进行计算
- 在定义的时候就必须初始化,一旦指向了某个变量,就不可以再更改
- 可以利用引用初始化另一个引用,相当于某个变量的多个别名
- 不存在【引用的引用】【指向引用的指针】【引用数组】
引用存在的价值之一:比指针更安全、函数返回值可以被赋值
4.1 引用的本质
- 引用的本质就是指针,只是编译器削弱了它的功能,所以引用就是弱化了的指针
- 一个引用占用一个指针的大小
简单的,从汇编的角度来讲,它们是一样的:
int age = 10;
int* p = &age;
*p = 30;
int& ref = age;
ref = 40;
所以,本质上ref保存的就是age的地址。
而至于引用无法指向另一个对象这个特性,则是由编译器决定的,也就是编译器特性。
4.2 指针能够改变变量值的本质(汇编层面)
有如下代码:
int age = 3;
int* p = &age;
*p = 5;
结果很容易看出,age通过指针p被赋值为5;
我们debug,反汇编:
具体是什么意思呢?
- 通过mov将值赋给age的这块内存。
- 通过lea将 age的地址 赋给rax。
- 将rax中的地址(&age)赋给[p],也就是指针p所占内存。同时开辟了一块8字节的内存区域(qword)
- 将[p]送到rax
- 最后将5 送入到 rax中地址指向的一块内存区域。因为此时rax的内容是一个地址,所以是[rax]
4.3 常引用(const reference)
- 引用可以被const修饰,这样就无法通过引用修改数据了,可以称为常引用
- const必须写在 & 的左边,才能算是常引用
- const引用的特点
- 可以指向临时数据
const int a = 10;
-
可以指向不同类型的数据
-
作为函数参数时(此规则也适用于const指针)
-
可以接受const和非const实参(非const引用,只能接受非const实参)
-
可以跟非const引用构成重载
-
- 可以指向临时数据
int age = 10;
const double &ref = age;
int a = 1;
int b = 2;
const int &ref = 30;
const int &ref = a+b;
const int &ref = func();
-
当常引用指向了不同类型的数据时,会产生临时变量,即引用指向的并不是初始化的那个变量有这样一段代码:
int a = 10;
const long &b = a;
a = 30;
int c = 10;
const int& d = c;
c = 30;
运行结果:
可以看到a的引用b并没有跟随着a的改变而改变,因为b的类型是double,那么我们反汇编看一下
先看正常的例子:const int = int(这里简写
可以看到,汇编通过lea 指令保存了变量c的地址到rax,再通过mov 将c的地址 也就是rax的内容保存到了另一块区域。
再来看看另一个例子:const long = int:
其主要区别在哪?在于最后一行mov ,也就是c=30前一行、a=30的前一行的mov
可以看出,b和d所保存的内容,相对于它们的引用来说是不一样的。
b保存的完全是另一个区域的地址(而不是a的地址),而d保存的就是c的地址!
所以也就说明了为什么常引用指向不同类型会产生临时变量,并且指向的并不是初始变量的问题。
其他关于const引用的用法,都比较简单,和指针同理:
引用的本质就是指针。
其实和上面3中提到的const是一码事,这里不多赘述了。
5.C++中的面向对象
5.1 this和类指针
- this指针存储着函数调用者的地址,或者说this指向了函数调用者,再或者说把调用者 的地址传进去
有如下代码:
class Person {
public:
int id;
int age;
int height;
void display();
};
void Person::display() {
return;
}
int main() {
Person person;
person.id = 10;
person.age = 20;
person.height = 30;
person.display();
Person* p = &person;
p->id = 10;
p->age = 20;
p->height = 30;
p->display();
return 0;
}
可以看到通过直接访问成员和通过类指针访问成员生成的汇编代码是不一样的。
同时也可以注意到,通过获取类实例的首地址,通过成员变量的地址偏移量进行内存的赋值。
从汇编指令的生成数量多少来说,直接访问成员变量确实更优,但这并不代表性能和效率的最优。同时,在许多情况下只能只用指针来进行操作,如在堆上分配内存。
5.2 构造函数
- 构造函数(Constructor),在对象创建的时候自动调用,一般用于完成对象的初始化工作
- 特点:
- 函数名与类同名,无返回值,可以有参数,可以重载,可以有多个构造函数
- 一旦自定义了构造函数,必须使用其中一个自定义的构造函数来初始化对象
- 注意:
- 通过malloc分配的对象不会调用构造函数
这里很好理解:malloc是C语言的库函数,自然不会有什么构造函数这一面向对象的概念,也就不会调用构造函数,只做了分配内存的工作;而C++则不同,new关键字会调用构造函数并且分配内存,由此可见在C++中要谨慎使用malloc,防止对象未初始化等不必要的错误发生。
- 一个广为流传的、很多教程\书记都推崇的错误结论:
默认情况下,编译器会自动生成一个无参的、空的构造函数
这个情况就不会生成无参的空函数:
class A {
public:
int value;
};
A a;
a.value = 1;
可以看到汇编中根本没有call这个调用函数的指令。
其简单理解是什么意思呢?在class中根本没有对value进行赋值操作,那么此时在画蛇添足生成一个空的、无参的构造函数就显得毫无意义,作为编译器自然不会做这样浪费的事情。但如果是
int value=5;这样的话,那么就会调用自动生成的构造函数:
总结一下:对象创建后,需要做一些额外操作时(比如内存操作,函数调用等),编译器一般会为其自动生成构造函数
- C++编译器在某些特定的情况下,会给类生成无参的构造函数:
- 成员变量在声明的同时进行了初始化
- 有定义虚函数
- 虚继承了其他类
- 包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义)
- 父类有构造函数(编译器生成或自定义)
5.2.1 默认情况下,成员变量的初始化
- 但如果自定义了了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化。
5.3 析构函数
- 析构函数在对象销毁的时候自动调用,一般用于完成对象的清理工作
- 函数名义~开头,与类同名,无返回值,无参,不可以重载,有且只有一个析构函数
- 注意:通过malloc分配的对象free的时候不会调用析构函数
- 构造函数、析构函数要声明未public,才能被外界正常使用。
5.3.1 虚析构函数
- 如果存在父类指针指向子类对象的情况,应该将析构函数声明为虚函数
- 这样delete父类指针时,才会调用子类de析构函数,保证析构的完整性。
5.4 继承
成员访问权限:子类内部访问父类的权限,是以下两项中的最小的那个:
- 成员本身的访问权限
- 上一级父类的继承方式
访问权限不影响对象的内存布局
5.4.1 多继承-虚函数
- 如果子类的多个父类都有虚函数,那么子类对象就会产生对应的多张虚表
- 同名成员变量:
- 菱形继承
- 菱形继承带来的问题:
- 最底下子类从积累继承的成员变量冗余、重复
- 最底下子类无法访问积累的成员,有二义性
- 菱形继承带来的问题:
- 虚继承:可以解决菱形继承带来的问题
此时Person称为虚基类,Student和Worker公用m_age,解决二义性。
5.5 多态
- 默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态:如下代码:
#include <iostream>
using namespace std;
class Animal {
public:
void bark() {
cout << "Animal bark" << endl;
}
void run() {
cout << "Animal run" << endl;
}
};
class Dog :public Animal {
void bark() {
cout << "Dog bark" << endl;
}
void run() {
cout << "Dog run" << endl;
}
};
class Cat :public Animal {
void bark() {
cout << "Cat bark" << endl;
}
void run() {
cout << "Cat run" << endl;
}
};
void Bark(Animal* p) {
p->bark();
p->run();
}
int main() {
Bark(new Dog());
Bark(new Cat());
return 0;
}
这里即便new了Dog和Cat,但传入的参数仍然是Animal,故只会根据指针类型调用对应的函数。
- 多态是面向对象非常重要的一个特性
- 同一操作作用于不同的对象,可以有不同的解释,产生不同的结果
- 在运行时,可以识别出真正的对象类型,调用对应子类中的函数
- 多态的要素
- 子类重写父类的成员函数(override)-必须是虚函数
- 父类指针指向子类对象
- 利用父类指针调用重写的成员函数
5.5.1 父类指针、子类指针
- 父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是public)
当然,上图中(Student*) new Person()是不安全的,p->m_score会访问m_age之后的内存,但这并不在Person分配的区域中。
5.5.2 虚函数
- C++中的多态通过虚函数来实现
- 虚函数:被virtual修饰的成员函数
- 只要在父类中声明为虚函数,子类中重写的函数也自动变为虚函数
这里一步一步详细分析,在之前Animal这个例子中我们在成员函数前加上virtual关键字:
class Animal {
public:
virtual void bark() {
cout << "Animal bark" << endl;
}
virtual void run() {
cout << "Animal run" << endl;
}
};
运行结果如下:
可以看到结果终于如期所示。 我们成功的实现了一种多态。
那么他的汇编是什么样的?
这里为了对比添加virtual前后的区别,我们将bark设为virtual,run设为普通成员函数:
class Animal {
public:
virtual void bark() {
cout << "Animal bark" << endl;
}
//非virtual
void run() {
cout << "Animal run" << endl;
}
};
我们运行,debug,反汇编:
对于函数,我们着重关注call指令。
bark()是virtual ,可以看到它的汇编最后是call了一个寄存器里的内容。
而run()不是virtual,可以看到它的汇编最后直接call了一个地址,一个写死的地址。
从表面上看,虚函数所call的是一个寄存器内容,而寄存器内容是可变的,也就意味着虚函数可以call不同的函数。
进一步的来说,虚函数可以根据不同类型的指针来调用不同的函数,这就是大概的虚函数实现多态的原理。
5.5.3 虚函数的实现-虚表
- 虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的函数地址
我们首先做一个简单有趣的实验,sizeof() 一下有无虚函数的class:
int main() {
cout << sizeof(Animal) << endl;
return 0;
}
class Animal {
public:
int age;
void bark() {
cout << "Animal bark" << endl;
}
};
加了virtual:
class Animal {
public:
int age;
virtual void bark() {
cout << "Animal bark" << endl;
}
};
可以看到多了12字节,但需要注意这在不同机器上可能不同。我的是64bit机器。
这意味着:虚函数的背后一定有某些东西在支持,它就是虚表
这里截一张图示意一下,在x86环境下。值得注意的点:
- 虚表地址存储在成员变量前面,也就是cat对象内存空间的前4个字节。
- 虚表内存储着对应虚函数的地址。
- 在寻找到一个虚函数地址后,再取下4个字节的位置,即另一个虚函数的位置地址。
结合这张图,我们再次回到void Bark(Animal *p)这个函数的反汇编中,我们传入一个new Cat(),进行debug:
我们逐行来分析:
//p是变量cat的地址
//eax 取p指向内存的前4个气节,是cat对象的地址
007C2571 mov eax,dword ptr [p]
//取出cat对象最前面的4个字节给edx
//也就是 取出虚表的地址值给edx
007C2574 mov edx,dword ptr [eax]
//这里先不用管
007C2576 mov esi,esp
//取出虚表的最前面4个字节给eax
//也就是取出Cat::bark()的函数调用地址给eax
007C2578 mov ecx,dword ptr [p]
007C257B mov eax,dword ptr [edx]
//call Cat::bark()
007C257D call eax
//先不用管
007C257F cmp esi,esp
007C2581 call __RTC_CheckEsp (07C12F8h)
根据汇编代码以及注释,我们可以很清楚的看出虚函数是如何工作的。
5.5.4 虚表的一些细节
- 所有的对象(不管在全局区,栈、堆)公用同一份虚表
- 如果子类调用了一个不属于它本身的函数,
那么则会去父类中查找,并调用其父类的函数(这是错的),代码如下:
class Animal {
public:
int age;
virtual void bark() {
cout << "Animal bark" << endl;
}
virtual void run() {
cout << "Animal run" << endl;
}
};
class Cat :public Animal {
public:
int size;
void run() {
cout << "Cat run" << endl;
}
};
int main() {
Animal* cat = new Cat();
cat->age = 20;
cat->bark();
cat->run();
return 0;
}
这很简单,但有一点需要注意: 此时cat的虚表仍然保存了两个函数,其第一个4字节的地址保存了Animal::bark()的地址。也就是说:一步到位,直接在cat的虚表里找到了Animal::bark(),而不是回到cat的父类Animal中寻找Animal::bark()。C++没有这种操作,而是事先就把Animal::bark的地址放到Cat的虚表中。如下图。
5.5.5 纯虚函数、抽象类
- 纯虚函数:没有函数体且初始化为0的虚函数,用来定义接口规范
- 抽象类(Abstract Class)
- 含有纯虚函数的类,不可以实例化(不可以创建对象)
- 抽象类也可以包含非纯虚函数,成员变量
- 如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类
5.6 static
需要一个变量,在整个程序运行过程中都存在,而且永远只占一份内存
- 静态成员:被static修饰的成员变量、函数
- 可以通过对象.静态成员、对象->静态成员、类访问(类名::静态成员)
- 静态成员变量
- 存储在数据段(全局区,类似于全局变量),整个程序运行过程中只有一份内存
- 对比全局变量,它可以设定访问权限(public、protected、private),达到局部共享的目的
5.7 友元
6. 内存空间的布局
每个应用都有自己独立的内存空间,其内存空间一般都有以下几种:
- 代码段(代码区)
- 用于存放代码
- 数据段(全局区)
- 用于存放全局变量等
- 栈空间
- 每调用一个函数就会给它分配一段连续的栈空间,等函数调用完后会自动回收这段栈空间
- 自动分配和回收
- 堆空间
- 需要主动去申请和释放
6.0 对象的内存布局
6.0.1 分布
对象的内存可以存在于3种地方:
- 全局区(数据段):全局变量
- 栈空间:函数里的局部变量
- 堆空间:动态申请内存(malloc、new等)
//全局区
Person g_person;
int main() {
//栈空间
Person person;
//堆空间
Person* p = new Person;
return 0;
}
6.0.2 对象的内存布局
struct Person
{
int age;
};
struct Student:Person
{
int m_no;
};
struct GoodStudent:Student
{
int m_money;
};
GoodStudent gs;
gs.m_age = 20;
gs.m_no = 1;
gs.m_money = 666;
父类成员变量在前,子类成员在后
6.1 堆空间
- 在程序运行过程中,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存
- 堆空间的申请\释放
- malloc \ free
- new \ delete
- new [] \ delete []
- 注意:
- 申请堆空间成功后,会返回那一段内存空间的地址
- 申请和释放必须是1对1的关系,不然可能会存在内存泄漏
- 现在很多高级编程语言不需要开发人员去管理内存(Java),屏蔽了很多细节,有利有弊
- 利:提高开发效率,避免内存使用不当或泄露
- 弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手
6.1.1 堆空间的初始化
先上代码:
int* p1 = (int*)malloc(sizeof(int));//p1未初始化
int* p2 = (int*)malloc(sizeof(int));
memset(p2, 0, sizeof(int));//将*p2的每一个字节都初始化为0
int* p1 = new int;//未初始化
int* p2 = new int();//被初始化为0
int* p3 = new int(5);//被初始化为5
int* p4 = new int[3];//数组元素未被初始化
int* p5 = new int[3]();//3个数组元素都被初始化为0
int* p6 = new int[3]{};//3个数组元素都被初始化为0
int* p7 = new int[3]{ 5 };//数组首元素被初始化为5,其他元素被初始化为0
6.2 内存对齐
# TODO
7.运算符重载
8.模板与泛型编程
8.1 模板(template)
- 泛型,是一种将类型参数化以达到代码复用的技术,C++中使用模板来实现泛型
- 模板的使用格式如下:
- template <typename\class T>
- typename和class是等价的
- 模板没有被使用时,是不会被实例化出来的
- 模板的声明和实现如果分离到.h和.cpp中,会导致链接错误
- 一般将模板的声明和实现同一放到一个.hpp文件中
奥说
这里详细讲一下模板实例化。
这里有main.cpp和add.cpp:
C++首先会单独编译各个.cpp文件,生成对应的.obj文件。头文件是用来包含的,不会参与编译,或者说会将头文件的内容替换到引用头文件的.cpp中参与编译。
随后会进行链接,将.obj文件链接生成.exe文件。在这个例子中,最后的.exe中有三个函数:main,int add(),double add()
此时若main函数中调用了add(),则会马上找到对应的add函数。
回到链接的过程中,那么链接的作用是什么呢?
在main函数中,单独编译main,调用add也就是利用了call函数地址操作,但此时call的地址并不是真正的add函数地址,编译器只是看到main中有add的声明,从而通过了编译,但实际中这个地址并不是真正的add地址。
那么此时就是链接起作用的时候了:链接会修复这个函数的地址,将真正的add函数地址交给call去调用。
明确了这些问题,我们来看一下模板在此情景下的一些问题:
将add用模板来实现,同样的,单独编译,肯定是没有问题的。
但问题是:此时的obj会有add的具体函数实现吗?答案是没有。
为什么?说白了,就是没人调用它,T不知道是个啥类型。
所以此时问题就来了:当链接的时候,main中add就会去寻找真正的add实现函数地址,但此时由于是模板未实例化,并没有地址,就会导致链接无法修正地址,链接失败。
所以,一般都会将模板的声明和实现统一放到一个.hpp文件中。
8.2 动态数组-模板实例demo
#pragma once
#include<iostream>
using namespace std;
//数组是一片连续的区域,若添加新元素不能单个的去new,会导致空间不连续,指针无法按索引查找
//故需要重新分配一片更大的区域,将院数据块copy过去
template <typename Item>
class Array {
friend ostream& operator<< <>(ostream& , const Array<Item>& );
//用于指向首元素
Item* m_data;
//元素个数
int m_size;
//容量
int m_capacity;
void checkIndex(int index);
public:
Array(int capacity = 0);
~Array();
void add(int value);
Item get(int index);
int size() const;
Item operator[](int index);
};
template <typename Item>
Array<Item>::Array(int capacity) {
m_capacity = (capacity > 0) ? capacity : 10;
//申请堆空间
m_data = new int[m_capacity];
}
template <typename Item>
Array<Item>::~Array() {
if (m_data == nullptr)return;
delete[] m_data;
}
template <typename Item>
void Array<Item>::add(int value) {
if (m_size == m_capacity) {
//扩容
// 1. 申请一块更大的空间
// 2.将旧空间的数据拷贝到新空间
// 3.释放旧空间
cout << "空间不够" << endl;
}
m_data[m_size++] = value;
}
template <typename Item>
Item Array<Item>::get(int index) {
return m_data[index];
}
template <typename Item>
int Array<Item>::size() const{
return m_size;
}
template <typename Item>
Item Array<Item>::operator[](int index) {
return get(index);
}
template <typename Item>
ostream& operator<< <>(ostream& cout, const Array<Item>& array) {
cout << "[";
int size = array.size();
for (int i = 0; i < size; ++i) {
if (i != 0) { cout << ","; }
cout << array.m_size[i];
}
return cout << "]";
}
template <typename Item>
void Array<Item>::checkIndex(int index) {
if (index < 0 || index >= m_size) {
// throw exception
throw "overflow";
}
}
9.四种类型转换
- C语言风格的类型转换符
- (type)expression
- type(experssion)
- C++中有4个类型转换符
- static_cast
- dynamic_cast
- reinterpret_cast
- const_cast
- 使用格式:xx_cast<type>(expression)
9.1 const_cast
- 一般用于去除const属性,将const转换为非const
其C风格和C++风格的类型转换在汇编中的实现是完全一样的。
9.2 dynamic_cast
- 一般用于多态类型的转换,有运行时安全检测
#include<iostream>
#include "dynamic_array.hpp"
using namespace std;
class Person {
virtual void run() {}
};
class Student :public Person {};
class Car{};
int main() {
Person* p1 = new Person();
Person* p2 = new Student();
cout << "p1=" << p1 << endl;
cout << "p2=" << p2 << endl;
Student* stu1 = dynamic_cast<Student*>(p1);//不安全
Student* stu2 = dynamic_cast<Student*>(p2);//安全
cout << "stu1=" << stu1 << endl;
cout << "stu2=" << stu2 << endl;
return 0;
}
可以看到stu1被赋值为0,其实也就是nullptr。说明dynamic_cast认为他是不安全的,因为将父类指针转换为子类,是不安全的。子类指针可访问的范围肯定比父类要广。
再看另一个例子:
#include<iostream>
#include "dynamic_array.hpp"
using namespace std;
class Person {
virtual void run() {}
};
class Student :public Person {};
class Car{};
int main() {
Person* p1 = new Person();
Person* p2 = new Student();
cout << "p1=" << p1 << endl;
cout << "p2=" << p2 << endl;
Car* c1 = (Car*)p1;
Car* c2 = dynamic_cast<Car*>(p2);
cout << "c1=" << c1 << endl;
cout << "c2=" << c2 << endl;
return 0;
}
可以看到c2为nullptr,很好理解,不同类型的转换是不安全的。
9.3 static_cast
- 对比dynamic_cast,缺乏运行时安全检测
- 不能交叉转换(不是同一继承体系的,无法转换)
- 常用于基本数据类型的转换、非const转为const
- 适用范围较广
9.4 reinterpret_cast
- 属于比较底层的强制转换,没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝
- 可以交叉转换
- 可以将指针和类型互相转换
10.C++11新特性
- auto
- 可以从初始化表达式中推断出变量的类型,大大简化编程工作
- 属于编译器特性,不影响最终的机器码质量,不影响运行效率
- decltype
- 可以获取变量类型
int a=10;
decltype(a) b=20;//int
- nullptr
- 可以解决NULL二义性问题
- 基于范围的for循环
- 更加简洁的初始化方式
10.1 Lambda表达式
类似于JavaScript中的闭包、IOS中的Block,本质就是函数。
结构:[capture list] (params list) mutable exception -> return type { function body }
- capture list:捕获外部变量列表
- params list:形参列表,不能使用默认参数,不能省略参数名
- mutable:用来说用是否可以修改捕获的变量
- exception:异常设定
- return type:返回值类型
- function body:函数体
有时可以省略部分结构
外部变量捕获:
11.异常
- 异常是一种在程序运行过程中可能会发生的错误(比如内存不够)
- 异常没有被处理,会导致程序终止
- throw异常后,会在当前函数中查找匹配的catch,找不到就终止当前函数代码,去上一层查找,如果最终都找不到匹配的catch,整个程序就会终止
12.智能指针(Smart Pointer)
- 传统指针存在的问题
- 需要手动管理内存
- 容易发生内存泄漏(忘记释放、出现异常等)
- 释放之后产生野指针
- 智能指针基本上一定要指向堆空间,如果指向栈空间的话容易导致double free现象
一种简单的智能指针的自实现:
template <typename T>
class SmartPointer {
private:
T* m_obj;
public:
SmartPointer(T* obj) :m_obj(obj) {}
~SmartPointer() {
if (m_obj == nullptr)return;
delete m_obj;
}
T* operator->() {
return m_obj;
}
};
12.1 shared_ptr
- shared_ptr的设计理念
- 多个shared_ptr可以指向同一个对象,当最后一个shared_ptr在作用域范围内结束时,对象才会销毁
- 可以通过一个已存在的智能指针初始化一个新的智能指针
shared_ptr<Person>p1(new Person());
shared_ptr<Person>p2(p1);
- 针对数组的用法
shared_ptr<Person>ptr1(new Person[5]{}, [](Person* p) {delete[] p; });
shared_ptr<Person[]>persons(new Person[5]{});
12.1.1 shared_ptr的原理
- 一个shared_ptr会对一个对象产生强引用(strong reference)
- 每个对象都有个与之对应的强引用技术,记录着当前对象被多少个shared_ptr强引用着
- 可以通过shared_ptr的use_count函数获得强引用计数
- 当有一个新的shared_ptr指针指向对象时,对象的强引用计数就会+1
- 当有一个shared_ptr销毁时(比如作用域结束),对象的强引用计数就会 -1
- 当一个对象的强引用计数为0时(没有任何shared_ptr指向对象时),对象就会自动销毁
12.1.2 shared_ptr的循环引用问题
有如下代码:
class Car {
public:
shared_ptr<Person>m_person;
};
class Person {
public:
shared_ptr<Car>m_car;
};
又有如下操作:
shared_ptr<Person>person(new Person());
shared_ptr<Car>car(new Car());
person->m_car = car;
car->m_person = person;
如图所示:
当作用域结束,person和car的智能指针shared_ptr被销毁,但堆空间中的对象内部的智能指针并未销毁,因为强引用依然存在,计数为1:
这就是循环引用,你引用我,我引用你,最后导致谁都不会被销毁,导致内存泄漏。
解决办法就是wear_ptr
12.1.3 解决循环引用weak_ptr(弱引用)
- weak_ptr会对一个对象产生弱引用
- weak_ptr可以指向对象解决shared_ptr的循环引用问题
weak_ptr会对一个对象产生弱引用,意味着不会增加强引用计数
上图例子中,由于car的weak_ptr弱引用指针指向了Person,则Person对象不会增加强引用计数,此时计数为0,自动销毁。同时Car对象也失去了m_car的强引用,计数也变为0,也自动销毁。这样就解决了循环引用问题。
12.2 unique_ptr
- unique_ptr也会对一个对象产生强引用,它可以确保同一时间只有一个指针指向对象
- 当unique_ptr销毁时(作用域结束时),其指向的对象也就自动销毁了
- 可以使用std::move函数转移unique_ptr的所有权
Ref:汇编语言
汇编语言种类:
- 8086汇编(16bit)
- x86汇编(32bit)
- x64汇编(64bit)
- ARM汇编(嵌入式、移动设备)
- ...
其中,x64汇编根据编译器的不同,有两种书写形式:
- Intel
- AT&T
其大致区别如下:
1.寄存器与内存
- 通常,CPU先将内存中的数据存储到寄存器中,然后在对寄存器中的数据进行运算
- 假设内存有一个值为3的内存空间,打算对他加一,并存储到另一块区域:
- CPU首先将3放到EAX寄存器中:mov eax,3
- 然后将EAX与1相加:add eax,1
- 最后将值赋给内存空间:mov [内存空间],eax
2.x64汇编寄存器
寄存器大致如上图。
在不同的架构的机器上,寄存器的名称也有不同,比如:
64bit:
RAX\RBX\RCX\RDX:通用寄存器
32bit:
EAX\EBX\ECX\EDX:通用寄存器
16bit:
AX\BX\CX\DX:通用寄存器
但x64汇编是兼容以前版本的汇编的,如下图:
如图,x64汇编把低32位用EAX来表示, 低16位用AX来表示。
也就解释了为什么在64bit架构中的反汇编代码依然存在eax这个寄存器。
下图表示了常见的寄存器是如何组成的。