0、字符串操作:参考1、参考2
size_t类型
:- x86下,无符号整形
- x64下,无符号长整形
- string求子串:
- 函数签名:
string substr(size_t pos =0,size_t len = npos) const;
,求从从pos
位置开始长度为len
的子串,pos
默认是0,len
默认到string
的结尾
- 函数签名:
- 字符串拼接:
string
中+和+=的效率差距:str = str+a;
是先将右边的两个内容相加得到一个新对象再赋值str+=a;
直接将右边的追加到左边的string对象后面,效率可以和append
持平
snprint
:- 函数签名:
int snprint(char* str, size_t, size,const * format, ...);
,目标字符串str
,拷贝字节数size
,format
格式化字符串,...
可变参
- 函数签名:
strncpy
:- 也可以用于字符串拼接:
strncpy(s1+3,s2,strlen(s2)+1)
,从s1+3
的位置开始拷贝字符串
- 也可以用于字符串拼接:
strcat
- 函数签名:
char *strncat(char *dest, const char *src, size_t n);
,将src
追加到dest
后面,追加长度为n
,一般需要考虑长度为strlen(src)+1
,追加到终止符
- 函数签名:
- 字符串和数字的转换:
to_string
:string to_string(int)
,将数字常量转换为字符串stoi
:int stoi(const string& str,size_t* pos=nullptr,int base=10)
,将base进制
的字符串转化为十进制的数字,pos
是起始位置,stoi
会进行int的范围检查,如果超过范围报错runtime error
atoi
:int atoi(const char* str);
,将字符串转化为数字,不会进行范围检查,如果超过范围,就返回的是对应的上下界isdigit
:int isdigit(int)
用于判断字符是否为0~9的数字,是数字返回非0否则返回0,需要引入头文件#include<ctype.h>
- 字符串拷贝:头文件
#include<string.h>
strcpy
:- 用于将一个字符串(以
'\0'
为结束符),复制到另一个字符串中 - 函数签名
char *strcpy(char *destination, const char *source);
- 需要注意的点:
strcpy
会给destination
,添加结束符;destination
传进来的必须是字符数组
,如果数组长度不够会发生缓冲溢出- 如果
src
字符串中间出现终止符会导致拷贝不完全
- 用于将一个字符串(以
strncpy
:- 将源字符的前
len
字节拷贝到dest
所指的内存上,不会主动添加结束符 - 函数签名:
char *strncpy(char *dest, const char *src, size_t len);
- 需要注意的点:
- 如果要拷贝终止符,需要将长度设置为
strlen(src)+1
; dest
内存长度至少要大于等len
;- 如果
src
长度小于len
,则用NULL
填充dest
,直到填充完len
长度 - 如果
src
长度大于len
,则只复制src
前len
个字符,不会添加结束符
- 如果要拷贝终止符,需要将长度设置为
- 将源字符的前
memcpy
:- 将一段内存的内容按字节拷贝到另一块中,指定拷贝长度,类型不定,不限制于字符串
- 函数签名
void *memcpy(void *destination, const void *source, size_t num);
- 需要注意的点:
- 不能用于内存重叠的情况,可能会发生未定义情况
- 拷贝指定长度,不会遇到
'\0'
停止 destination
的长度必须大于等于num
;
memmove
:- 和
memcpy
类似,将一段内存的东西拷贝到另一个内存,但是可以运行目标内存和源内存重叠,底层拷贝需要判断地址大小进行拷贝; - 函数签名
void *memmove(void *destination, const void *source, size_t num);
- 和
1、进程虚拟地址划分:
- 32位操作系统上,3G用户空间+1G内核空间,从低地址到高地址:
.text段->.rodata段->.data段->.bss段->.heap段->.stack段->内核空间
.text段
:存放指令,只读不写.rodata段
:存放字符串常量,只读不写.data段
:初始化的全局变量和静态变量(全局或局部),初始化值不为0.bss段
:未初始化数据的全局变量和静态变量(全局或局部),或者是初始化为0的全局变量和静态变量(全局或局部).heap段
:new来的数据,程序员分配和回收.stack段
:函数的局部变量等,系统分配和回收
- 不同进程用户空间独立,内核空间共享
- .stack段高地址向低地址增长,.heap段低地址向高地址增长,这两个分区段之间加载动态连接库(.so/.dll)
- const修饰的全局变量是存储在.rodata段的,因为是只读属性
2、函数调用队栈的过程
- 寄存器介绍:
- eip指令指针:指向下一条即将执行指令的地址
- ebp基地址指针:用来指向栈底
- esp栈指针:通常用来指向栈顶
- 函数调用过程:main栈中调用sum函数,后面补充
3、编译连接
- 可执行文件产生的四步:预编译、编译、汇编、连接
预编译
:删除注释,把宏定义和#include全部替换,除了#pragma编译器指令编译
:词法分析,语法分析,代码优化等,生成汇编代码文件汇编
:将汇编代码翻译成机器指令,并生成二进制可重定位文件(.obj文件)连接
:所有的目标文件和静态库文件进行连接:- 1、不同文件之间各个段进行合并(.text、.data等),完成符号解析,找到所有未定义符号的定义的位置,这是因为,当前文件可能引用了其他文件的定义的符号
- 2、符号重定位,将编译阶段没有分配的地址进行分配,就是跑到代码段的指令上给符号地址填成正确的地址,就是如果是
.text段
的指令,就填写.text段
的地址,如果是.data段
的数据,就填写.data段的地址
- 连接分为两种:
- 静态连接:连接的是静态库(.a/.lib),将静态库(声明和实现)和所有源文件的obj文件进行段合并可执行文件;
- 动态连接:连接的是动态库(.so/.dll),在运行的时候进行连接,通过声明在找对应动态连接库的实现,heap、stack之间,比静态连接占用空间小更灵活,方便后面的库升级;
- 可执行文件:windows(PE头的文件)、Linux(ELF文件)
4、形参带默认参数
- 只要给一个形参默认参数,则后面的所有参数都要给默认参数
- 默认参数只能给一次,一般建议在函数声明处给,不是在定义的位置
5、内联函数
inline
标识的函数编译阶段会在调用处全部展开,减少函数调用开销(栈帧的开始和释放是有开销的),有类型检查是安全的- 内联成功的函数,不会在符号表产生符号
inline
只是建议内联,不一定内联成功,比如递归,递归多少次只有运行的时候知道- 内联知道release版本起作用,debug没有用
- 构造函数、析构函数、虚函数声明为内联,在语法上没有问题,
- 构造和析构声明没有意义,因为编译器并不会对构造和析构进行内联操作,因为构造和析构可能会涉及到内存的开辟和释放比较复杂,而且类中的函数,默认都是内联,再去声明也没什么意义
- 虚函数声明为内联,通过派生类指针调用的内联虚函数不会产生内联(因为多态在运行时期),而通过对象调用的虚函数会产生内联(不会发生多态),前提是函数比较简单
- 内联缺点:内联次数太多,可能会导致代码臃肿
6、函数重载:成员函数和非成员函数重载
- 重载条件:作用域相同(优先调用作用域近的)、函数名相同、参数列表不同(参数类型不同、参数个数不同、参数列表顺序不同),编译的时候会根据传入的实参选择重载版本
- C++支持重载:是因为C++函数产生符号是由函数名+参数列表构成的,C中只有函数名决定
- 在CPP文件中,可以通过
extern "C"
来保证编译规则是C的规则 - 普通函数重载:
- 需要注意默认参数导致的函数调用二义性
- 成员函数重载:
- 普通成员方法和虚函数方法之间是重定义,
virtual关键字
不是重载条件 - 静态成员方法和普通方法之间是可以重载的,因为静态方法不需要接收对象指针,但是必须明面上的参数列表要不一样
- 带const属性的只读方法和普通方法是可以重载的,因为接收对象指针的形参类型不一样
- const修饰的常对象,调用的方法只能是常方法,因为普通的this指针,不能接收常指针
- 普通成员方法和虚函数方法之间是重定义,
7、const:const、指针、引用
- C++中
const
修饰的量是常量,不能作为左值,初始化后不能被修改,不能再次出现在等号左边,不可以将常量的地址泄露给普通指针 - C和C++的const
- C中的const:
- const修饰的量叫常变量,不是常量,可以不用初始化
- 虽然也也不能作为左值,但是可以通过内存进行修改值
- 因为是常变量,所以不能用来初始化数组
- C++中的const
-
const修饰的变量必须初始化,const修饰的量叫常量(只读属性):
const int b =200; //b是常量,当凡出现b的地方都会被替换为200 // 依旧可以通过强转指针,修改内存的值,但是不会影响b被200替换
-
因为是常量所以可以用来初始化数组
int arr[b]={};// b被200替换,直接初始化数组
-
如果用变量初始化const修饰的变量,则const修饰的量叫常变量,与C的规则等同
-
- C中的const:
8、const和指针
- const修饰的量是常量,不能直接修改的,不能把常量的地址泄露给引用或者是指针
- const和一级指针
const int *p = &a;
等价于int const* p = &a;
:- 可以修改
p
指向的内存的值,但是不能改变*p
的值 - 不管a是常量还是变量
- 可以修改
int *const p = &a;
p
指向的地址不可以改变,*p
的值可以改变
const int *const p = &a;
- 既不能修改指针的指向
p
,也不可以修改值*p
- 既不能修改指针的指向
- TIP:下面表示的是类型
int * <= const int *
,错误的,不可以将整型常量地址,泄露给指针const int * <= int *
, 可以将普通常量地址赋值给const int *int * a
和int * const b
是同类型,都是int *
类型
- const 和二级指针
- 二级指针:
int a = 10; int *p = &a; int **q = &p
q
存的是p
的地址,*q
是p的值,也就是a
的地址,**q
就是a的值
- TIP:
int** <= const int **
错误const int ** <= int **
错误int ** <= int * const *
错误int * const* <= int **
正确
- 二级指针:
9、指针和引用
- 引用必须初始化,指针可以不初始化,引用比指针安全,指针存在野指针的情况
- 作为函数参数传入,引用比指针方便,指针需要解引用
- 引用只有一级引用,指针可以有多级
- 通过引用修改内存的值,和通过指针解引用修改内存的值,指令操作是一样的
- 左值引用和右值引用:
- 左值引用:
int c = 10; int &b = c;
只能引用左值,不能引用右值 - 右值引用:
int && b = 10;
右值引用只能引用右值(没内存没名字),先创建一个临时量存储10,将临时量的地址给右值引用b
,不能引用左值;右值引用变量b
本身是个左值,对b的引用需要用左值引用 - 常量左值引用:
const int & b = 10;
可以引用右值,但是这个b就只能是只读状态;先创建临时量保存10,再用b引用临时量;
- 左值引用:
10、new和malloc
-
new
和malloc
的区别:malloc
是按字节开辟内存;new
是开辟内存需要指定类型(底层依旧是malloc
)malloc
开辟内存是返回void*
,new
返回的类型对应的指针malloc
只负责内存开辟,new
不仅开辟内存,还可以进行数据的初始化new int(20);
new int[20]();
malloc
开辟内存失败返回nullptr
,new
抛出bad_alloc
类型的异常
-
delete
和free
的区别:- delete是先
析构
再free
- 对于整型这样堆区上的变量而言,delete和free没有区别
- delete是先
-
关于new、new[]和delete、delete[]能不能混用的问题:混用问题
-
对于普通的编译器内置类型,比如int、double之类的,new[]/delete,new/delete[]可以混用,因为只涉及内存开辟和释放
-
对于自定义的数据类型,不能混用,new[]开辟内存构造对象,delete,虽然会删除内存,但是只会调用一次析构,导致其他对象的没有被析构,如果对象有指向外部的资源,可能会导致内存泄露
-
如果new开辟内存构造对象,delete[],会从地址起始的前面四个字节寻找对象个数(未知的),可能会进行很多次析构,具体看编译器;
-
通过
new[]
开辟对象数组的时候(普通内置类型不会多出四个字节,被编译器优化掉了
),会多出四个字节用来记录对象的个数,多出的四个字节在指针地址的前面,delete[]
会先访问前面的四个字节,知道有多少个对象存在,从而在删除内存的时候调用调用每个对象的析构函数:class Test { public: Test(int data = 10) { cout << "Test()" << endl; } ~Test() { cout << "~Test()" << endl; } private: //int* ptr; int ma; }; int main() { Test *p = new Test[5]; delete p; //只会返回第一个对象的内存,导致还有四个对象的内存没有被释放 /* delete p;会让编译器觉得p指向的只是一个对象 */ Test *p = new Test(); delete[] p; //会让编译器觉得p指向的是数组,从p-4的内存中寻找数组的个数 return 0; }
-
-
STL空间配置器问题:
- 将new和delete的构造进行的分割,只有单独的内存开辟和内存释放,对象构造和对象析构
- 是为了避免不必要容器空间底层不必要的对象构造和析构,如果不分离,你在
vector
容器里面开辟10的空间,空间元素是对象,就会进行10次无意义的构造,但是这些空间位置你并没有使用,所以只能是单纯的分配空间,只有在push_back
对象的时候,才进行对象的构造
11、类和对象、this指针
-
OOP语言四大特征:封装、继承、多态、抽象;
-
一个类可以产生很多对象,每个对象都有自己的成员变量,但是不同对象共用一套成员方法,这套成员方法通过this指针来区分不同对象的成员
-
类中实现的方法自动处理为内联函数,内外定义的成员方法,就是普通函数,可以通过加inline变成内联
-
类的大小只和成员变量有关,和成员函数无关
-
this指针
:指向当前对象实例-
除了静态成员方法,所有的成员方法通过编译都会增加一个this指针,来接收对象的地址:
class A{void init(int _m);} => class A{void init(A *this,int _m);}
-
this指针
并不是对象的一部分,不影响对象大小,作用域在类内部,this指针
一般在函数开始前构造,函数结束后销毁 -
thiscall
:仅用于成员函数,被调用者维护栈(自己),从右向左压栈,this指针
通过ecx
寄存器传给被调用者,不是通过堆栈传递 -
在析构函数里面调用
delete this
,会无限递归调用析构,栈溢出,程序崩溃
-
-
访问限定符:
public
:不管是成员函数还是成员方法,可以通过对象在类外调用private
:不管是成员函数还是成员方法,只能通过类内方法调用,一般是通过类的public方法访问private成员变量;可以在类中将类外方法设置为友元(friend
),就可以在类外通过对象访问类的私有成员protected
:主要用于派生继承中,同样是类外不能访问,但是派生类可以访问
-
普通成员变量,每个对象都私有一份数据,静态成员变量,所有对象共用一份,存放在数据段
-
普通成员函数,编译器会自动添加
this
指针, 常方法的this
指针会多处const
属性,只能读不能写,但是可以修改mutable
修饰的成员变量 -
静态成员方法只能访问静态成员变量和静态成员方法,因为这些是不依赖对象的
-
静态成员方法必须在类内声明,在类外定义,因为不占用类的内存
-
指向成员对象或者成员函数的指针,需要添加作用域,并解引用需要依赖于对象,静态成员变量和静态成员方法就不需要
12、析构函数和构造函数
- 构造函数:定义对象的时候,自动调用,可以重载(参数列表不同)
- 如果没有显示的定义构造函数,编译器会提供:
- 默认构造函数(无参构造):空函数,如果自己定义了构造函数,就不会产生默认构造
- 拷贝构造:浅拷贝,拷贝构造必须是引用传递,因为用已有对象进行拷贝构造会导致拷贝构造函数的无穷递归
- 移动构造(C++11后):对象资源转移
explicit
关键字:会禁止隐式的对象生成,作用于普通带参构造,如比,=右边的整形无法通过调用带参构造进行隐式转换;- C++编译器对于对象构造的优化:用临时对象产生新对象的时候,不会产生临时对象,而是直接构造新对象
- 构造函数不能是虚函数,编译器会报错,不允许:
- 一般定义子类对象,vfptr先指向父类的虚函数表,在父类构造完成后,子类的vfptr指向自己的虚函数表,如果构造函数是虚函数,调用构造函数的时候就需要通过vfptr去找对应的虚函数地址,也就是说调用构造函数之前,虚函数指针和虚函数表都没有完成初始化,所以是不被允许的,并且析构函数是创建对象编译器主动调用的,不是我们通过父类指针调用的
- 如果没有显示的定义构造函数,编译器会提供:
- 析构函数:不带参数,只能有一个析构,析构完了,对象就不存在了
- 析构函数一般可以在类外自己调用,但不建议,这不安全,可能导致内存释放后再重复使用
- 析构的顺序和构造的顺序是相反的,临时对象出语句自动析构
- 构造和析构顺序:
- 构造:基类构造(继承顺序)->成员类构造(定义顺序)->派生类构造
- 析构 :与构造相反,派生类析构->成员类析构->基类析构
13、深浅拷贝问题
- 编译器提供的默认拷贝构造是浅拷贝,做的是内存拷贝,就是将内容完成复制一遍
- 如果对象内部有指向堆区的资源,则浅拷贝会导致多个指针指向同一个内存资源,当析构的时候会导致内存的重复释放
- 不只是拷贝构造,编译器默认的赋值重载也是浅拷贝
14、临时对象问题
- 临时对象产生的三种情况
- 以值传递的方式给函数传递参数:用引用传递
- 类型转换生成的临时对象/隐式类型状态以保证函数调用成功:可以通过关键字
explicit
避免隐式转换,或者是直接将临时对象用于对象初始化 - 函数返回对象的时候(被编译器优化掉了,不会有额外的临时对象产生) :用初始化的方式接收临时对象,临时对象会被优化掉,如果是用赋值接收,临时对象不会被优化
15、迭代器
-
类里面实现的嵌套类,底层是对指针的封装,都容器元素元素的访问能像使用指针一样,不需要知道容器底层的数据结构,直接提供一套统一方法容器的方法
-
前置++和后置++:
后置++: int tmep = a; //有中间量的产生,如果是对象重载后置++,就可能产生临时对象 a+=1; return temp; 前置++ a+=1; return a; // 不管是普通的++,还是迭代器的++,前置的效率都要比后置的高点, // 因为后置的有中间变量的产生
-
迭代器失效问题:
- 容器调用erase方法后,从当前位置到容器末尾元素的迭代器全部失效
- 容器调用insert方法之后,从当前位置到容器末尾位置的迭代器全部失效
- insert,引起的容器扩容,内存空间的重新开辟,原来的迭代器就全部失效了,所以需要再迭代器赋值之后避免插入元素;
- 迭代器失效后,对插入/删除的迭代器进行更新操作,用insert和erase的返回值
多态和继承:继承和多态、多重继承和多继承
-
继承的本质:代码复用,子类继承父类,保留父类的结构
-
多态的本质:接口复用,通过父类虚函数实现一个接口,在不同的子类中进行覆盖,有不同的用法
- 静态(编译时期)多态:函数重载、模板实例化
- 动态(运行时期)多态:继承结构中,基类指针指向子类对象,通过指针调用子类中的同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就调用哪个派生类对象的覆盖方法,称之为多态
- 多态底层是通过动态绑定来实现的,基类指针指向谁,就会访问谁的vfptr,进而访问对应的vftable,从虚函数表中调用的当然是对应派生类对象的方法
-
继承关系: 子类会将父类的东西全部继承过来,只是访问限定不同,内存布局是基类的成员在前面,再是子类成员
- 子类public继承父类:
父类public->子类public
,父类protected->子类protected
,父类private->子类只能继承但不可以见(无法访问)
- 子类protected继承父类:
父类public->子类protected
,父类protected->子类protected
,父类private->子类只能继承但不可以见
- private继承:
父类public->子类private
,父类protected->子类private
,父类private->子类只能继承但不可以见
- 子类public继承父类:
-
C++中struct的继承方式默认是public继承,class的继承方式默认是private继承
-
派生类的构造:
- 派生类调用基类的构造函数初始化,初始从基类继承过来的成员
- 派生类再调用自己的构造函数、初始化自己特有的成员
- …派生类声明周期到了
- 调用派生类的析构,释放派生类成员占用的外部资源
- 调用基类的析构,释放派生类内存中,从基类继承来的成员占用的外部资源
- TIP:
派生不能直接初始化继承过来的成员变量,只能通过调用基类的构造函数来初始化
-
重载、覆盖、隐藏:
- 重载关系:必须在相同的作用域中,函数名相同,参数列表不同
- 隐藏关系:继承结构中,派生类会将基类的同名成员(成员变量和成员方法)隐藏调用了,但是还是实际存在的,只是作用域不同,可以通过添加作用域来访问父类的成员
- 覆盖关系:基类和派生类的方法,返回值、函数名、参数列表都相同,如果基类的方法是虚函数,派生的方法就自动变成虚函数,他们之间就是覆盖关系,如果父类不是虚函数就是隐藏关系
-
派生类和基类的类型转换:e.g., 基类
Base
,派生类Derive
;继承关系中,默认支持从派生类到基类的转换
,因为子类包含父类所有的东西,可以当成父类进行给父类赋值,但是父类只能调用父类的成员,而反之不行,父类不能当子类使用- 将子类对象地址赋值给父类指针,类型从下到上,可以进行,如果是将父类对象地址赋值子类指针,由于子类内存比父类大,解引用可能会出现内存非法访问的问题;
Base B; Derive D; B = D; //默认支持,从派生类对象转到基类对象 //D = B; //不支持,不能从基类转到派生类 Base *pB = &D; //支持,从派生类Derive* D到 Base *pB的转换 // 虽然父类指针指向子类,但是通过pB只能调用父类的方法 /* 基类的指针,默认只能访问基类的成员,若想要访问子类成员需要将pB强转为子类指针 (Derive*)pB */ //Derive* pD = &b; // 不支持从 基类 到 派生类 的转型 // 因为子类的指针指向的子类内存比基类的内存要大,可能会出现内存非法访问,不安全
-
虚函数、静态绑定和动态绑定:
- 提前注意点:
Derive d(10); Base* pb = &d; pb->show(); delete pb; **关于show()有三点解释**: - 如果show定义在子类Derive中,则父类指针pb访问不到 - 如果show定义在父类Base中,且非虚函数,直接通过地址调用(父类),是静态绑定 且如果子类有同名隐藏,依旧调用父类的方法 - 如果show定义在父类Base中,是虚函数,则运行时,通过子类对象的vfptr来间接 访问子类的虚函数表,进行调用,是动态绑定 **关于delete pb;的解释**: - 如果父类的析构函数不写成虚函数,delete父类指针,只会调用父类的析构, 会导致子类的析构无法调用,因为是静态绑定 - 如果父类的析构函数是虚函数,delete父类指针的时候,析构函数的调用是动态绑定, 通过vfptr调用的就是子类的析构函数 - 关于父类指针指向的起始地址问题: 父类指针指向子类,指针永远指向子类中,**父类数据的起始地址**
- 虚函数:
- 一个类添加了虚函数、编译阶段会给这个类生成一个
vftable
虚函数表,vftable
中主要存储得是RTTI指针
和虚函数地址
(函数入口地址):- 编译阶段生成虚函数表,这个表会被加载到内存的
.rodata段
,只读数据段 RTTI指针
指向的是运行时类型字符串,父类的话,就是"Base";- 虚函数地址的是有顺序的,是根据类里面定义的顺序存放的
- 编译阶段生成虚函数表,这个表会被加载到内存的
- 一个类添加了虚函数、类的对象内存最开始(前四个字节)的地方就会多存储一个
vfptr指针
,指向类的虚函数表vftable
; - 虚函数不影响对象的内存大小,只影响虚函数表的大小
- 一个类定义多个对象,多个对象共用一个虚函数表,一个类一个虚函数表,编译时期,派生类会生成自己的虚函数表,如果派生类重写了父类的虚函数,虚函数表中的虚函数地址也会被覆盖
- 虚析构:
- 基类的指针,指向new出来的派生类对象的时候, 对指针进行delete,如果父类析构不是虚函数,就是静态绑定,就会导致子类的析构函数得不到调用,所以一般在继承结构中,将父类的析构设置为虚函数;
- TIP:
- 虚函数存在
.text
段,虚函数地址存放在虚函数表中,虚函数表存在.rodata
段,虚函数表的地址存放在虚函数指针中,虚函数指针存放在对象内存中,所以虚函数调用必须依赖对象 - 所以虚函数的实现必须依赖对象,且是可以调用的,static方法不能实现,构造函数不能实现
- 虚函数表在编译时期初始化生成,相同的类共用同一个虚函数表,虚函数指针,在是对象实例化,调用构造函数的时候进行初始化,指向当前类的虚函数表
- 构造函数中调用任何的虚函数都是静态绑定,因为要先生成父类,再生成子类,子类存在需要等构造函数完成
- 父类析构函数调用虚函数,子类已经析构了,这个时候调用的也是父类的虚函数
- 虚函数存在
- 一个类添加了虚函数、编译阶段会给这个类生成一个
- 静态绑定和动态绑定:
Base *pB = &D;
父类指针指向子类- 静态绑定针对的是
普通函数
:- 在编译阶段,就确定了调用函数的地址,父类的指针只能调用父类的普通方法
- 动态绑定针对的是
虚函数
:需要运行的时候根据vfptr
再vftable
中寻址- 因为是虚函数,所以子类可能会重写,需要在运行的时候去根据虚函数指针来调用
- 如果子类重写了方法,则子类的虚函数的地址,会在虚函数表中对父类的虚函数地址进行覆盖,运行的时候调用的就是子类的虚函数
- 如果父类指针,指向的是子类对象,则vfptr指向的虚函数表是子类的,
RTTI指针
标识的是子类类型,反之则是则是父类类型
- 静态绑定针对的是
- 问题:虚函数的调用一定是动态绑定?
- 在构造函数中调用虚函数,是静态绑定
- 不通过引用或者指针调用虚函数,也是静态绑定,因为不会访问到虚函数指针,动态绑定必须用指针或者引用,指针指向哪个对象,就访问哪个对象的虚函数指针,进而访问对应的虚函数表
- 抽象类:
拥有纯虚函数的类
叫抽象类,抽象类不可以再进行实例化,但是定义指针和引用,可以说就是完全是为了多态来实现的,子类必须改写这个纯虚函数;- 纯虚函数:
virtual void bark()=0
- 析构函数是虚函数同理
- 多态注意事项:
- 不同子类对象中的虚函数指针可以通过地址交换,从而可以导致动态绑定虚函数调用混乱;
- 父类指针指向子类对象,编译期间只能看到父类虚函数的情况:比如参数入栈的值、这个虚函数的访问限定();这些都和子类中改写的情况没有关系;
- 虚函数表写入虚函数指针的时机是构造函数栈帧准备好后,代码执行前
- 虚继承:
-
virtual
:修饰成员方法就是虚函数,修饰继承方式,就是虚继承
,被继承的类就是虚基类
:class B : virtual public A{};
其中A就是虚基类 -
如果
类B
虚继承类A
,则类B
中会多出一个虚基类指针vbptr
,指向虚基类表vbtable
,虚基类A
中的数据会被放到类B
内存的后面:正常继承 虚继承 A::vfptr vbptr ma mb mb A::vfptr ma
-
虚基类表
vbtable
在编译时期生成,运行的时候放到数据段(.rodata段),存放的是虚基类数据在派生类中距离vbptr的偏移量 -
需要考虑的点:
- 子类虚继承父类,父类指针指向
堆上的子类对象
,指向的地址是从父类的vfptr开始的,而windows释放父类指针指向的内存也是从vfptr开始的,程序会中断,Linux g++释放内存是从vbptr开始的 - 如果虚继承不涉及堆区的对象,则没有问题,因为没有手动delete内存的操作,如果涉及到了堆内存,可能会导致开辟的地址和释放的地址不一致,导致崩溃
- 构造函数,虚基类的构造优先级大于普通的函数
- 子类虚继承父类,父类指针指向
-
- 多重继承-菱形继承问题:
- 派生类存在成员变量重复、基类构造和析构多次调用
- 通过虚继承可以解决,对内存重复的间接基类做虚继承处理
- 提前注意点:
C++11汇总:
-
nullptr:nullptr和NULL的区别
- C++中
NULL
宏定义出来的是0
,为了区分0和空指针就有了nullptr - C中NULL宏定义出来的是
(void*)0
- C++中
-
final/override/=default/=delete:
fianl
:修饰一个类,表示这个类不能被继承,修饰一个虚函数,表示这个虚函数不可以被子类重写override
:显示的标记子类中需要重写的虚函数,比较直观的知道这个方法是要重写的,由于编码错误导致不是重写(函数签名不同,父类没有这个方法),编译器会报错=default
:修饰构造、析构、拷贝构造、移动构造、赋值重载,让编码进行默认的实现=delete
:修饰构造、析构、拷贝构造、移动构造、赋值重载,让编译器标记delete,不可以被调用
-
bind绑定器(bind、function、lambda):
- 弥补了bind1st 和 bind2nd的局限(只能作用于二元函数对象),可以将对多元函数对象进行绑定参数,并且还可以作用于普通函数、函数指针、lambda表达式等可以用来调用的实体,用来绑定参数并返回一个新的函数对象,底层的函数类型会随着参数的绑定进行改变;也可以使用占位符(命名空间
placeholders
),不进行参数绑定,最多可以有20个,在实际用到的时候传入参数;
- 弥补了bind1st 和 bind2nd的局限(只能作用于二元函数对象),可以将对多元函数对象进行绑定参数,并且还可以作用于普通函数、函数指针、lambda表达式等可以用来调用的实体,用来绑定参数并返回一个新的函数对象,底层的函数类型会随着参数的绑定进行改变;也可以使用占位符(命名空间
-
function函数对象:需要修改和补充没有彻底搞清楚、function源码刨析
- 是个模板类,可以接收bind返回的函数对象、lambda匿名函数对象、普通函数等一些可以调用的目标实体,相当于是进行函数类型保存并封装,可以类似于函数指针的使用,但是底层调用的也是operator()重载;lambda表达式和bind返回的函数对象都只能作用于当前语句,function就解决了他们的问题,可以保存它们的类型并多次使用;一般是function和配合bind完成回调函数的设置;
-
lambda匿名函数对象:
- 语法是
[中括号捕获外部变量](小括号参数列表)->右箭头返回值类型{花括号函数体}
,底层是编译器实现的匿名函数对象,通过小括号运行符重载实现的,参数列表不带mutale
关键字的话,底层的operator
重载默认是只读属性(const修饰的函数);比函数对象轻量,不用定义类;对外部变量的捕获可以是引用、也可以是值传递;
- 语法是
-
智能指针:不带引用计数的智能指针、带引用计数的智能指针
- 裸指针的使用,容易忘记delete,所以利用栈对象出作用域的特性管理裸指针,防止内存泄露,智能指针底层提供了
->
运算符重载和*
运算符重载,能像正常指针来使用 - auto_ptr:
- 独占式的管理内存资源,永远只有最后一个auto_ptr管理资源
- 底层只有裸指针和普通的拷贝构造和赋值重载,资源的转移,在代码复杂的时候不容易看出来,容易引起编码上的安全问题,使用已经转移资源的空指针;
- unique_ptr:
- 也是独占式的管理内存资源,相比于auto_ptr,删除了普通的拷贝构造和赋值重载,提供右值版本的,可以将资源转移摆在明面上,就是正常的左值必须通过
std::move()
才能进行资源转移,直接传左值编译会报错
- 也是独占式的管理内存资源,相比于auto_ptr,删除了普通的拷贝构造和赋值重载,提供右值版本的,可以将资源转移摆在明面上,就是正常的左值必须通过
- shared_ptr:
- 共享式的管理的内存资源,底层有个内存
资源计数器
,每次拷贝或者赋值的时候,资源加1
,每次有shared_ptr被释放的时候,资源减1
,当资源数为0
的时候,释放资源; - 使用
shared_ptr
时候需要注意不能用裸指针重复赋值,会导致内存资源重复释放,因为资源计数器都不一样了,应该优先使用make_shared
比较安全,通过make_shared构造一个shared_ptr对象,从而可以调用拷贝构造进行构造; - 使用get方法获取底层裸指针,要注意资源的安全
- 共享式的管理的内存资源,底层有个内存
- weak_ptr:
- 和share_ptr搭配使用,不改变引用计数,用来观测shared_ptr管理的资源生命周期,没有提供
->
运算符重载和*
运算符重载,如果想使用资源可以通过lock()
方法来将自己提升为shared_ptr
,提升成功的前提是,shared_ptr
管理的资源还存在(use_count>0
);
- 和share_ptr搭配使用,不改变引用计数,用来观测shared_ptr管理的资源生命周期,没有提供
- 循环引用问题:不同的类里面定义了指向对方
shared_ptr
, 彼此相互引用,引用计数增加,导致堆上对象和类里面的智能指针共存
,最后都不能被释放导致资源泄露,可以通过weak_ptr
来解决,weak_ptr
作为资源观测不会改变引用计数; - 自定义删除器问题:用智能指针管理不能正常delete的类型,比如文件句柄socket等,需要自己定义删除器
- 裸指针的使用,容易忘记delete,所以利用栈对象出作用域的特性管理裸指针,防止内存泄露,智能指针底层提供了
-
移动语义
std::move
和完美转发std::forwad
-
左值右值:
- 左值:有名字、有内存、值可以修改
- 右值: 没名字、没内存
-
引用折叠:
& + && = &
、&& + && = &&
-
int &&a = std::move(b)
:a的属性本身就是左值 -
std::move
:可以将左值转换为右值类型,用于匹配右值拷贝和右值赋值,进行资源的转移,底层是通过引用折叠接收左右值,并将他们通过static_cast
强转为对应右值 -
std::forwad
:由于模板推演可以引起引用折叠,所以可以将左值和右值的引用的匹配代码统一写成右值引用,并且可以同时接收左值和右值,然后forward
可以根据模板的类型推演,将右值引用变量转化为传入的参数的类型,如果是传入的是右值,就转成右值,传入的是左值就转成左值,这样不仅保持了右值属性的变量在函数转移中途属性不会变,并且用统一的风格简化代码template<typename Ty> void construct(T* p, Ty&& val) // Ty 是用来匹配左右值的 { /*int a = 0; 比如传入的是左值a,则Ty是int &, 且Ty&& val =>int& &&val=>int &val 如果传入的是右值std::move(a),则Ty是int&&, 且Ty&& val =>int&& &&val int&& &&val => int &&val */ new (p) T(std::forward<Ty>(val));// 定位new,可以匹配左右值构造 //forward可以根据Ty的类型将val转成传入进来的左右值类型 } // 如果没有forward,可能上面的左右值匹配需要写两份代码 void construct(T* p, const T& val) // 接受左值 { cout << "左值构造" << endl; new (p) T(val);// 定位new } void construct(T* p, T&& val) // 接受右值 { cout << "右值构造" << endl; // val虽然用来接收右值,但是本质是左值 new (p) T(std::move(val));// 定位new }
-
-
-
C中的强转:C中的强转
int a = (int)b;
, 将变量b
的类型强制转换为int
型,与C的相比C++提出了4种强转,C++风格的可以更清晰的知道这样的强转是在干嘛; -
const_cast
:-
正常来说,不能将
const
修饰的变量地址泄露给普通指针或者引用,但是可以通过const_cast
移除const
属性来强转为指针或者引用、此外也可以移除volatile
属性 -
用于去掉变量的
const
属性,并强转为指针对应的指针或者引用,其实就相当于去掉指针或者引用的const
属性const int a = 10; double* p1 = (double*) & a; int* p2 = const_cast<int*>(&a); //需要保证&a的类型和<int*>里面的类型要一致 const int g = 20; int &h = const_cast<int &>(g);//去掉const引用const属性
-
int* p1 = (int*) & a;
和int* p2 = const_cast<int*>(&a);
在汇编层面是一样的,但是前者转换的类型不限
,而后者只能需要保持int*
的属性,也就是去掉const
之后的指针类型要保持一致
-
-
static_cast
:- 提供编译器认为安全的类型转换,也就是基本上所有的都能转换,但是转换之间是需要有关联,并且要编译器觉得是安全的(不同类型的指针之间不能转换,因为内存不一致容易出现内存非法访问)
- 可以用于:
- 类层次结构的强转,子类转向父类是安全的,父类指针可以转子类指针,但是没有动态类型检查,需要自己保证安全
- 基本数据类型的强转(long、char、int等)
- 指针类型的转换,不能之间转,需要通过void*作为在中间量
- 不可以用于:
- 不能转换expresssion 的const或者volatile属性
- 不能将实列父类转成子类,因为编译器默认无关联
-
reinterpret_cast
:-
类似于C风格的强制类型转换,但是安全性不高,底层是比特位的拷贝
-
语法:
reinterpret_cast<tyep_id>(expression);
,type_id
和expression
需要保证至少有一个是指针或者引用 -
主要用途:
- 改变指针或者引用的类型,最主要的是不同类型的指针或者引用之间直接的转换(弥补static_cast),但是不安全
- 将指针或者引用转换为一个足够长度的整型,就是说整型必须和系统指针内存大小相匹配
- 将整型转为指针或者引用类型
int* p4 = nullptr; double* b2 = static_cast<double*>(p4); //不成立,b2解引用指向的是8字节内存,不安全 double* b2 = reinterpret_cast<double*>(p4);//成立,底层是C的强转
-
-
dynamic_cast
:-
其他的强转是用于编译时期的转换,而这个主要用于继承结构中,可以支持RTTI类型识别的上下转换(运行时类型检查)
-
父类指针指向子类,可以将父类指针转换为对应的子类类型,如果类型对上就是转换成功,如果失败,转换返回的就是
nullptr
class Base // 抽象类 { public: virtual void func() = 0; }; class Derive1 :public Base { public: void func() { cout << "call Derive-1::func" << endl; } }; class Derive2 :public Base { public: void func() { cout << "call Derive-2::func" << endl; } // Derive2 实现新功能的API接口函数 void derive02func() { cout << "call derive02func::func" << endl; } }; void show(Base *p) { //如果传进来的是Derive2就转换成功,如果不是pd2就是nullptr Derive2* pd2 = dynamic_cast<Derive2*>(p); // Derive2* pd2 = static_cast<Derive2*>(p); //static_cast就是直接转了,没有动态类型的识别 if (pd2 != nullptr) //是Derive2,转换成功 { pd2->derive02func(); } else // p指向的不是 derive2对象,是Derive1 { p->func(); } }
-
基类必须要有虚函数, 否则编译不能通过
-
子类转父类,和
static_cast
效果一样,但是对父类转子类的时候,dynamic_cast
可以进行类型检测会比static_cast
安全,dynamic_cast
必须将父类指针转成对应的指向的类型的,否则返回就是nullptr
- 一般用于父类指针指向子类,但是不能访问子类本身的成员,所以通过动态转换,将父类转成对应的子类指针,如果类型不匹配则会转型失败,比如直接将父类取地址转换(会导致内存非法访问);
-
-
STL
- 六大组件:容器、算法、迭代器、仿函数、容器适配器、空间配置器
- 容器:
-
顺序容器:
vector
:定义
:底层是动态开辟的数组,每次以2倍扩容(GCC
)/1.5倍扩容(VS
) ,在容器中,通过空间配置器进行对象构造和空间开辟,allocate(开辟空间)
,deallocate(释放空间)
,construct(对象构造)
,destroy(对象析构)
- 增删查:
- 因为是数组,底层内存是连续的,所以可以随机存取,时间复杂度是O(1),
- 插入和删除,会导致元素的移动,时间复杂度是O(n),只适用于尾部插入和删除;
- 尾部插入元素的时候,空间内存不够的时候会重新开辟新的空间,并进行内存拷贝,进而会导致性能消耗;
- 基本接口:
push_back()-尾插元素
,insert()-通过迭代器插入
,erase()-通过迭代器删除
,pop_back()-尾删
,size()-元素个数
,capacity()-容量大小
,empty()
,resize()
,reserve()
,swap()
- 关于
resize()
和reserve()
:reserve()
: 预留空间,底层开辟空间,没有添加元素(size不会变大),且只能用于扩容,不能减少容量resize()
:可以开辟空间,并且底层会构造对象(size会变大),如果指定第二个参数,使用第二个参数,如果没有就用默认值,此外可以用于减少容量(size)
- 迭代器失效:
- 插入和删除之后迭代器会失效,需要进行更新操作
- 关于
clear()
:只改变清空size()
,不改变capacity()
,所以vector,底层所占内存是不变的
list
:- 定义: 底层是双向的循环链表,每个节点不仅有数据域,还有指向前一个节点的指针和指向后一个节点的指针
- 增删查:
- 底层链表,内存不连续,不支持随机存取,只能遍历访问,时间复杂度是O(n);
- 可以在任何位置高效的对节点进行插入和删除,时间复杂度O(1);
- 迭代器失效:
- 由于内存空间不是连续的,删除只会影响当前迭代器,或者
erase
返回下一个有效的迭代器 - 插入是不会影响迭代器失效
- 由于内存空间不是连续的,删除只会影响当前迭代器,或者
- 基本接口:
push_back()
,psuh_front()
,insert()
,pop_back()
,pop_front()
,erase()
,
deque
:- 定义: 是个动态开辟二维数组,第一维度存放的是第二维度的指针(new出来的),第二维度是固定的长度的数组空间(4096字节),以第一维度的2陪空间进行扩容,可以支持随机访问(随机访问迭代器),但是由于内部是分段的,所以比vector要复杂一点
- 增删查:
- 由于是双端队列,在头部的插入和删除是O(1),在尾部的插入和删除是O(1),与
vector
相比,可以在头部进行方便的插入和删除 - 在队列中间指定位置进行删除和删除是
O(n)
,但是效率比vector
可能低一点,因为deque
第二维度不是连续的
- 由于是双端队列,在头部的插入和删除是O(1),在尾部的插入和删除是O(1),与
- 基本接口:
push_back()
,push_front()
,insert()
,pop_back()
,pop_front()
,erase()
-
无序关联容器:底层是链式哈希表,是无序的,增删查
O(1)
unordered_set
:无序集合,key不可以重复unordered_multiset
:无序集合,key可以重复unordered_map
:无序映射表,key不可以重复unordered_map
和map
的区别:unordered_map
底层是哈希表,是无序的,增删查的时间复杂度是O(1)
;map
底层是红黑树,红黑树是中序遍历是有序的,增删查需要遍历树,时间复杂度是O(log n)
;
unordered_multimap
:无序映射表,key可以重复- 无序映射表底层结构:底层结构
- 底层是
hashtale
,则有个桶(bucket
)的概念,则hash操作
是通过key
值计算hash code
,再根据对应hash code
找到对应桶,然后在桶里面找到对应的节点 - 通过开链法解决哈希冲突,将
hash code
相同的节点插到链表中,采用的是头插法,即使hash冲突
,时间也是O(1)
,用一个vector
指针数组来存储节点指针,vector
中的每个元素都指向一个链表,链表中的每个节点都有一个指向下一个节点的next
指针,然后还有一个元素是pair结构 - 需要考虑的问题:链表过长可能导致查找一个节点的时间复杂度是
O(n)
,也就是hash退化
问题,为了解决这个问题,引入了负载因子-hashtable元素和bucket个数之间的比值
和最大负载因子
,当负载因子>=最大负载因子
的时候会发送rehash
操作(限制了每个桶的数量无限增加),也就是扩容+重新插入(重新计算hashcode,再插入),从而确保搜索删除的时间复杂度还是O(1); - vs中初始容量是8,以8的倍数扩容,gcc中初始容量是13,以2倍扩容,扩容都是vector的扩容;
- 一般解决
hash冲突
的方式:线性探测
:hash函数计算到重复的位置,则依次向后找,找到空位开链
:unorderd_map底层原理再散列
:发生hash冲突,换一种hash函数,直到不冲突二次探测
:如果使用hash函数计算出来的位置已经有元素了,就通过步长1^2、2^2、3^3....
进行查找
- 底层是
-
有序关联容器:底层是红黑树,是有序的,增删查时间
O(log n)
set
:集合,key不能重复multiset
:集合,key可以重复map
:映射表[key,value],key不可以重复- 底层元素是pair对象,可以通过
make_pair(key,value)
将键值对打包成对象 - 插入:
- 方法一:
mp.insert(pair<int,int>(1,2));
- 方法二:
mp.insert(map<int,int>::value_type(1,2));
- 方法三:
mp.insert(make_pair(1,2));
- 方法四:mp[1] = 2;
- 方法一:
find
和[]
[]
:通过key
着value
,如果找到了就返回,如果没有这个key
,就对这个key
插入默认值find
: 通过key
查找,找到了返回迭代器,没找到返回end()
迭代器- 迭代器失效:
- 由于内存空间不是连续的,删除只会影响到当前的节点,需要通过erase返回下一个节点的有效迭代器
- 底层元素是pair对象,可以通过
multimap
:映射表,key可以重复红黑树
:
-
- 容器适配器:对其他容器进行封装,没有自己的数据结构,内部方法也全是依赖其他容器
stack
:- 定义:栈,先进后出,底层依赖
deque
,因为vector
动态扩容效率低,deque
第二维度大小稳定,而且vector
需要大量连续的空间可能分配失败,而deque
的第二维度是分断的 - 基本操作:
push 入栈
,pop() 出栈
,top() 查看栈顶元素
,size() 元素个数
- 定义:栈,先进后出,底层依赖
queue
:- 定义:队列,先进先出, 底层依赖
deque
,不仅因为vector
动态扩容效率低,而且vector
头部删除效率低,队列需要在头部出队 - 基本操作:
push() 入队
、pop() 出队
、front() 查看对头元素
、back 查看队尾元素
、empty()
、size()
- 定义:队列,先进先出, 底层依赖
priority_queue
:- 定义:优先队列,底层抵赖vector,底层数据结构是通过数组构造的大根堆(默认底层
less<int>
是大根堆,如果将函数对象换成greater<int>
就是小根堆),通过下标获取左右节点 - 基本操作:
push 入队
、pop 出队
、top 查看队顶元素
、empty()
、size()
大根堆
:大顶堆- 定义:完全二叉树,根节点大于左右子树最大值,左右子树又是个大根堆
- 相关操作:
建堆 O(n)
:- 依次从左向右的插入节点,每次插入节点的都进行调整满足大顶堆的性质
调整堆 O(log n)
- 每次插入节点,导致性质丢失,就需要进行调整
- 定义:优先队列,底层抵赖vector,底层数据结构是通过数组构造的大根堆(默认底层
- 迭代器:
- 定义:对指针的封装,可以向指针一样操作,向上隐藏细节,提供遍历容器的接口,可以在不了解容器底层的情况下对容器进行遍历
- 常见迭代器:
iterator 普通正向迭代器
、const_iterator 常量正向迭代器,只读不写
、reverse_iterator 反向迭代器
、const_reverse_iterator 常量反向迭代器
- 算法:接收的都是迭代器参数
- 头文件:
#include<algorithm>
- 常见算法:
- sort:快速排序
- find:遍历查找,成功返回迭代器,失败返回
end()
迭代器 - find_if:
- binary_search: 二分查找,成功返回true,失败返回flase
- for_each: 遍历容器,可以添加函数对象
- 头文件:
其他小知识点
- 大小端编码:
- 小端编码:本地内存存储的字节序,数字有效位低的存放在低地址
- 大端编码: 网络字节序,数字有效位低的存放在高位地址
- 例子:
- 16进制整型
0x12345678
,有效位高到低一次为0x12
、0x34
、0x56
、0x78
,地址是从左向右是低地址到高地址,则在本地内存中的字节序存储是0x78 0x56 0x23 0x12
,转换成网络字节序为0x12 0x23 0x 56 0x78
- 16进制整型
- 内存对齐:参考
-
为什么要内存对齐:
- 可以提高内存访问速度,如果没有内存对应,对一个变量的读取可能需要多次,以至于效率低
- 对齐边界:
char-1
,short-2
,int-4
,float-4
,double-8
- 对齐原则:
- 根据最大对齐边界进行对齐,
-
防止不同平台内存对齐不一致:
#pragma pack(push,1)
、#pragma pack(pop)
-
- C++友元:参考
- 友元函数:定义在类外,不属于当前类的函数,可以在类中声明,并加上
friend
关键字访问,当前类的私有成员, 友元函数可以是其他类的成员函数 - 友元类: 可以将一个类定义为另外一个类的友元,通过添加关键字,友元类的成员函数可以访问当前类的私有成员
- TIP:
- 友元是单向的
- 友元关系不可以传递
- 友元不可以被继承
- 友元函数:定义在类外,不属于当前类的函数,可以在类中声明,并加上
- 文件操作:
设计模式
- 单例模式:
- 观察者模式: