目录
1.sizeof()是C++的编译特性,而不是函数。
2.C++默认参数
2.1.默认参数的相关注意事项
2.2 默认参数在汇编中的实现
3.C++中的const
4.C++中的引用
4.1 引用的本质
4.2 指针能够改变变量值的本质(汇编层面)
4.3 常引用(const reference)
5.C++中的面向对象
5.1 this和类指针
5.2 构造函数
5.2.1 默认情况下,成员变量的初始化
5.3 析构函数
5.3.1 虚析构函数
5.4 继承
5.4.1 多继承-虚函数
5.5 多态
5.5.1 父类指针、子类指针
5.5.2 虚函数
5.5.3 虚函数的实现-虚表
5.5.4 虚表的一些细节
5.5.5 纯虚函数、抽象类
5.6 static
5.7 友元
6. 内存空间的布局
6.0 对象的内存布局
6.0.1 分布
6.0.2 对象的内存布局
6.1 堆空间
6.1.1 堆空间的初始化
6.2 内存对齐
7.运算符重载
8.模板与泛型编程
8.1 模板(template)
8.2 动态数组-模板实例demo
9.四种类型转换
9.1 const_cast
9.2 dynamic_cast
9.3 static_cast
9.4 reinterpret_cast
10.C++11新特性
10.1 Lambda表达式
11.异常
12.智能指针(Smart Pointer)
12.1.3 解决循环引用weak_ptr(弱引用)
12.2 unique_ptr
Ref:汇编语言
1.寄存器与内存
2.x64汇编寄存器
3.在C++中嵌入汇编
具体看代码及反汇编:
#include
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
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++的一种运算符(或者操作符),并不是一个函数。
这部分是基础,值得复习一下:
直接上代码:
#include
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指令的机器码不同,但这并不是说它们的汇编代码不同,而是这条指令中关乎下一指令的地址,故略微不同。
核心问题:以下五个指针分别是什么含义?
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到底修饰的是什么,这样什么可以修改或不可以修改便一目了然。
C语言中可以使用指针(Pointer)间接获取、修改某个值
C++中,使用引用(Reference)可以起到跟指针类似的功能
值得注意的地方:
引用存在的价值之一:比指针更安全、函数返回值可以被赋值
简单的,从汇编的角度来讲,它们是一样的:
int age = 10;
int* p = &age;
*p = 30;
int& ref = age;
ref = 40;
所以,本质上ref保存的就是age的地址。
而至于引用无法指向另一个对象这个特性,则是由编译器决定的,也就是编译器特性。
有如下代码:
int age = 3;
int* p = &age;
*p = 5;
结果很容易看出,age通过指针p被赋值为5;
我们debug,反汇编:
具体是什么意思呢?
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是一码事,这里不多赘述了。
有如下代码:
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;
}
可以看到通过直接访问成员和通过类指针访问成员生成的汇编代码是不一样的。
同时也可以注意到,通过获取类实例的首地址,通过成员变量的地址偏移量进行内存的赋值。
从汇编指令的生成数量多少来说,直接访问成员变量确实更优,但这并不代表性能和效率的最优。同时,在许多情况下只能只用指针来进行操作,如在堆上分配内存。
这里很好理解:malloc是C语言的库函数,自然不会有什么构造函数这一面向对象的概念,也就不会调用构造函数,只做了分配内存的工作;而C++则不同,new关键字会调用构造函数并且分配内存,由此可见在C++中要谨慎使用malloc,防止对象未初始化等不必要的错误发生。
默认情况下,编译器会自动生成一个无参的、空的构造函数
这个情况就不会生成无参的空函数:
class A {
public:
int value;
};
A a;
a.value = 1;
可以看到汇编中根本没有call这个调用函数的指令。
其简单理解是什么意思呢?在class中根本没有对value进行赋值操作,那么此时在画蛇添足生成一个空的、无参的构造函数就显得毫无意义,作为编译器自然不会做这样浪费的事情。但如果是
int value=5;这样的话,那么就会调用自动生成的构造函数:
总结一下:对象创建后,需要做一些额外操作时(比如内存操作,函数调用等),编译器一般会为其自动生成构造函数
成员访问权限:子类内部访问父类的权限,是以下两项中的最小的那个:
访问权限不影响对象的内存布局
此时Person称为虚基类,Student和Worker公用m_age,解决二义性。
#include
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,故只会根据指针类型调用对应的函数。
当然,上图中(Student*) new Person()是不安全的,p->m_score会访问m_age之后的内存,但这并不在Person分配的区域中。
这里一步一步详细分析,在之前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不同的函数。
进一步的来说,虚函数可以根据不同类型的指针来调用不同的函数,这就是大概的虚函数实现多态的原理。
我们首先做一个简单有趣的实验,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环境下。值得注意的点:
结合这张图,我们再次回到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)
根据汇编代码以及注释,我们可以很清楚的看出虚函数是如何工作的。
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的虚表中。如下图。
需要一个变量,在整个程序运行过程中都存在,而且永远只占一份内存
每个应用都有自己独立的内存空间,其内存空间一般都有以下几种:
对象的内存可以存在于3种地方:
//全局区
Person g_person;
int main() {
//栈空间
Person person;
//堆空间
Person* p = new Person;
return 0;
}
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;
父类成员变量在前,子类成员在后
先上代码:
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
# TODO
奥说
这里详细讲一下模板实例化。
这里有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文件中。
#pragma once
#include
using namespace std;
//数组是一片连续的区域,若添加新元素不能单个的去new,会导致空间不连续,指针无法按索引查找
//故需要重新分配一片更大的区域,将院数据块copy过去
template
class Array {
friend ostream& operator<< <>(ostream& , const Array- & );
//用于指向首元素
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
Array- ::Array(int capacity) {
m_capacity = (capacity > 0) ? capacity : 10;
//申请堆空间
m_data = new int[m_capacity];
}
template
Array- ::~Array() {
if (m_data == nullptr)return;
delete[] m_data;
}
template
void Array- ::add(int value) {
if (m_size == m_capacity) {
//扩容
// 1. 申请一块更大的空间
// 2.将旧空间的数据拷贝到新空间
// 3.释放旧空间
cout << "空间不够" << endl;
}
m_data[m_size++] = value;
}
template
Item Array- ::get(int index) {
return m_data[index];
}
template
int Array- ::size() const{
return m_size;
}
template
Item Array- ::operator[](int index) {
return get(index);
}
template
ostream& operator<< <>(ostream& cout, const Array- & 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
void Array- ::checkIndex(int index) {
if (index < 0 || index >= m_size) {
// throw exception
throw "overflow";
}
}
其C风格和C++风格的类型转换在汇编中的实现是完全一样的。
#include
#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(p1);//不安全
Student* stu2 = dynamic_cast(p2);//安全
cout << "stu1=" << stu1 << endl;
cout << "stu2=" << stu2 << endl;
return 0;
}
可以看到stu1被赋值为0,其实也就是nullptr。说明dynamic_cast认为他是不安全的,因为将父类指针转换为子类,是不安全的。子类指针可访问的范围肯定比父类要广。
再看另一个例子:
#include
#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(p2);
cout << "c1=" << c1 << endl;
cout << "c2=" << c2 << endl;
return 0;
}
可以看到c2为nullptr,很好理解,不同类型的转换是不安全的。
int a=10;
decltype(a) b=20;//int
类似于JavaScript中的闭包、IOS中的Block,本质就是函数。
结构:[capture list] (params list) mutable exception -> return type { function body }
有时可以省略部分结构
外部变量捕获:
一种简单的智能指针的自实现:
template
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;
}
};
shared_ptrp1(new Person());
shared_ptrp2(p1);
shared_ptrptr1(new Person[5]{}, [](Person* p) {delete[] p; });
shared_ptrpersons(new Person[5]{});
有如下代码:
class Car {
public:
shared_ptrm_person;
};
class Person {
public:
shared_ptrm_car;
};
又有如下操作:
shared_ptrperson(new Person());
shared_ptrcar(new Car());
person->m_car = car;
car->m_person = person;
如图所示:
当作用域结束,person和car的智能指针shared_ptr被销毁,但堆空间中的对象内部的智能指针并未销毁,因为强引用依然存在,计数为1:
这就是循环引用,你引用我,我引用你,最后导致谁都不会被销毁,导致内存泄漏。
解决办法就是wear_ptr
weak_ptr会对一个对象产生弱引用,意味着不会增加强引用计数
上图例子中,由于car的weak_ptr弱引用指针指向了Person,则Person对象不会增加强引用计数,此时计数为0,自动销毁。同时Car对象也失去了m_car的强引用,计数也变为0,也自动销毁。这样就解决了循环引用问题。
汇编语言种类:
其中,x64汇编根据编译器的不同,有两种书写形式:
其大致区别如下:
寄存器大致如上图。
在不同的架构的机器上,寄存器的名称也有不同,比如:
64bit:
RAX\RBX\RCX\RDX:通用寄存器
32bit:
EAX\EBX\ECX\EDX:通用寄存器
16bit:
AX\BX\CX\DX:通用寄存器
但x64汇编是兼容以前版本的汇编的,如下图:
如图,x64汇编把低32位用EAX来表示, 低16位用AX来表示。
也就解释了为什么在64bit架构中的反汇编代码依然存在eax这个寄存器。
下图表示了常见的寄存器是如何组成的。