C++知识点
智能指针:
auto_ptr
,shared_ptr
(多个指针指向同一个对象,引用计数),unique_ptr
(独占,禁止多个指针指向同一个对象),weak_ptr
(只引用,不计数,用于弥补shared_ptr的环形引用问题)不控制所指向的对象的生命周期。
内联函数
inline
定义处生效,编译期替换,省去的是函数调用的时间,需要足够简单。函数相比宏更加安全。
内存地址空间
栈、堆、全局/静态存储区、常量存储区、代码区。
- 栈:存放函数的局部变量,由编译器自动分配和释放
- 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收
- 自由存储区:和堆十分相似,存放由 new 分配的内存块,由 delete 释放内存
- 全局区/静态区:存放全局变量和静态变量
- 常量存储区:存放的是常量,不允许修改
堆和自由存储区的区别
- 自由存储是 C++ 中通过 new 与 delete 动态分配和释放对象的抽象概念,而堆是 C 语言和操作系统的术语,是操作系统维护的一块动态分配内存
- new 所申请的内存区域在 C++ 中成为自由存储区。藉由堆实现的自由存储,可以说 new 所申请的内存区域在堆上
- 堆和自由存储区有区别,并非等价。使用 new 来分配内存,程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了
参数传递
指针传参(地址值,被调函数的局部变量,在栈中开辟内存空间存放形成一个实参的副本)、引用传递(栈空间中开辟的内存变量存放的是实参变量的地址,使用时间接寻址)、编译时的符号表
static关键字
主要改变存储方式和可见性(隐藏):- 修饰局部变量 位置:静态数据区,生命周期:一直到程序结束,作用域不变
- 修饰局部变量:位置:static区,作用域:仅本文件。
- 修饰普通函数:同局部变量。
- 修饰成员函数:无this指针,不能访问普通成员变量,只能访问静态成员变量,表示只属于该类而不属于该类的某个对象。
static 静态成员变量:
- 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和 private/public/protected 访问规则。
- 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
- 静态成员变量可以作为成员函数的参数可选参数,而普通成员变量不可以
static 定义静态变量,静态函数
- static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间
- static 作用于全局变量和函数,改变了全局变量的作用域,使得全局变量只能在定义它的文件中使用,在源文件中不具有全局可见性
- static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数
const关键字限制为只读
- 指针(是)常量:指针常量。
- const成员变量:对象的生命周期内是只读状态。 常量对象只能调用常量函数
define 和 const 的区别(编译阶段、安全性、内存占用等)
- 编译阶段:define 是在编译预处理阶段起作用,const 是在编译阶段和程序运行阶段起作用
- 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的只读变量是有类型的,是要进行判断的,可以避免一些低级的错误
- 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份;const 定义的只读变量在程序运行过程中只有一份
- 调试:define 定义的不能调试,因为在预编译阶段就已经进行替换了;const 定义的可以进行调试 const 的优点:
- 有数据类型,在定义式可进行安全性检查
- 可调式
- 占用较少的空间
类模板和函数模板区别
实例化前者是程序员指定类型,后者是编译器进行自动类型推导。
临时对象
直接创建并初始化在外部存储中,省去了拷贝和析构的花费。
不能virtual
普通函数,内联函数(无多态行为时可以),友元函数,静态成员函数,构造函数。
异常
try-catch找不到就调用terminate函数
保证不抛出异常noexcept关键字,优化性能,析构函数必须保证绝对不会抛出异常。
重载(overload)重写(override)重定义(覆盖)
- 重载:参数类型,参数数量,参数顺序。
- 重写(override):对virtual函数override,除函数块以内其他都要相同。
- 重定义(覆盖):子类重定义父类中有相同名称的非虚函数。
类型强制转换
static_cast、const_cast、dynamic_cast、reinterpret_cast
- static_cast:用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型
- 用于基本数据类型的转换
- 用于类层次之间的基类和派生类之间指针或者引用的转换(不要求必须包含虚函数,但必须是有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的;进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的,最好用dynamic_cast进行下行转换。
- 可以将空指针转化成目标类型的空指针
- 可以将任何类型的表达式转化成void类型
- const_cast:强制去掉常量属性,不能用于去掉变量的常量性,只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)
- reinterpret_cast:改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型
- dynamic_cast:
- 其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查
- 只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回NULL;不能用于基本数据类型的转换
- 在向上进行转换时,即子类的指针转换成父类的指针和static_cast效果是一样的,(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)
函数指针
指向函数的指针变量:经常作为回调函数
char* func(char* p) {} //普通函数
char* (*pf)(char *p) {} //pf就是一个函数指针
pf = func;
pf(p);
出现野指针的情形
- 指针定义的时候未初始化
- 指针指向动态分配的内存空间在释放(delete 或 free)后,未置为 NULL,让人误以为是合法指针
- 指针操作超过了变量的作用范围。例如:在函数中将一个局部变量的地址作为函数的返回值,这里编译器会给出警告,因为离开该函数后,局部变量的空间就会释放掉,返回的地址(指针)相当于是野指针。
new/delete是操作符和malloc/free是库函数
- new:①调用malloc分配内存空间(失败返回bad_alloc)②调用构造函数初始化(失败调用delete)返回空间首地址。无需指定内存块大小。可重载。自由存储区。类型安全。
- delete:①析构函数②free掉 new和delete主要因为:自定义类型需要自动执行析构和构造函数,而malloc和free是库函数而不是运算符,不在编译器控制权限之内。
extern
- 跨文件访问(include)加速程序的编译过程
- 指明使用C库函数解决名字匹配问题。
volatile
禁止优化,防止优化后乱序。
define
与编译阶段,无类型检查,展开和替换,文本替换。
struct内存对齐问题
长度最长的成员变量的整数倍。提高cpu读取内存的速度,寄存器每次读取固定长度字节的内容
union大小
要么是最大的那个成员的大小,要么需要将最大的那个成员补齐为4的倍数
union u1{double a; int b;}; //8
union u2{char str[13]; int a;}; //16
union u3{char str[13]; char c;} //13
enum大小
class类的大小
空类1bit,virtual指针大小,static不算,内存对齐问题。
十六进制转十进制
int a;
while (cin >> hex >> a) {
cout << a << endl;
}
常量字符串
事实上C++标准禁止转换一个string为char*,会报warning
char *str = "123abc";
cout << sizeof(str) << endl; //4 指针大小
cout << sizeof(*str) << endl; //1 指针指向第一个字符的大小
cout << *str << endl; //1 指字符'1'
cout << str << endl; //123abc
cout << strlen(str) << endl; //6
字符串常量"\2018"占3个字节。
在main函数之前执行的代码
//1.gcc扩展标记这个函数应当在main之前完成
__attribute((constructor))void before() {
printf("before main1");
}
//2.全局static变量的初始化在程序初始阶段,先于main函数执行
int test1() {
cout << "before main2" << endl;
return 1;
}
static int i = test1();
//3.lambda
int a = []() {
cout << "main function" << endl;
return 0;
}();
int main() {
cout << "main function" << endl;
return 0;
}
字符串常量和字符数组
char arr[]="hello"; //字符数组
char *arr2="hello"; //字符串常量
- char arr[]="hello",此处的赋值是将常量区的字符串"hello"拷贝到了堆栈区的数arr的空间了。数组arr是在堆栈区开辟了空间,此时是可以修改字符串的值,因为修改的是堆栈区的字符串的值。另外此时的数组名p是堆栈区中的"hello"的首地址。
- char *arr2="hello",指针arr2是存储在堆栈区,但字符串是常量,存储在常量区,只是指针arr指向了存储在常量区的字符串首地址,此时不能改变常量区的字符串的值。
const char arr[]="hello"; //这里hello本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
const char *arr2="hello"; //字符串hello保存在常量区,const本来是修饰arr2指向的值不能通过arr2去修改,但是字符串hello在常量区,本来就不能改变,所以加不加const效果都一样
OOP三大特性
封装,继承(实现继承,接口继承),多态(早晚绑定)
- 多态:继承+虚函数,静态多态:函数重载,编译时期确定调用哪个函数。动态多态:父类中的虚函数在子类中重写,运行期间决定调用的函数。避免了父类大量重载代码臃肿难以维护。
- 虚函数表**:保存该类中虚函数的地址。定义一个子类对象时会为这个类对象生成虚函数指针,指向该类型的虚函数表,该函数指针的初始化在构造函数中完成。父类指针指向子类对象,会根据虚函数指针找虚函数的地址。
- 虚析构**:父类指针指向子类对象,没有virtual的话编译器在销毁时根据是父类指针会按照父类的析构函数,在内存中会丢失子类的那部分内存。
构造函数不能virtual
必须清楚的知道要构造的对象类型。虚函数的调用是通过对象实例化后对象的虚函数表指针找到虚函数的地址进行调用,虚函数表指针也不存在。
构造函数和析构执行的顺序
- 构造函数:父类构造--成员类构造--子类构造
- 析构函数顺序:子类析构--成员类析构--父类析构
纯虚函数
让子类只继承函数的接口并且对其重写,否则子类将会是抽象类无法实例化。
深浅拷贝
当类中有指针变量且该类对象发生了赋值拷贝操作,会造成两个指针指向了同一块内存地址,当对象析构时两类对象会对同一块内存析构两次。深拷贝要求重载赋值运算符,会在堆内存中另外申请空间来存储数据。
拷贝构造发生在
①由一个对象生成另一个对象②对象作为函数形参以值传递的形式传入函数,会在产生一个临时对象压入栈中。③值传递从函数返回产生临时对象。
fork:子进程拷贝父进程页表,读时共享写时拷贝分配内存。返回值
STL六大组件
容器,迭代器,算法,仿函数,适配器,配置器。
仿函数
重载了operator的类或者类模板,可以用于改变算法策略。
迭代器:
类模板,行为类似指针,可以用于遍历容器,访问容器变量,type_traits区分原生指针和class type。需要操作符重载,返回对象的引用,输出时加*。类模板只能推导对象的类型(class type),对于原生指针和加const的指针需要偏特化处理。
STL函数区别
- resize:改变的是.size(),也即finish指针的位置,并对size内进行填充。
- reserve:改变的是.capacity(),也即end_of_storige的指针位置。
- emplace_back:直接在容器内构造对象,不需要拷贝+强制类型转换
大端小端
- 大端:高字节存放在低地址中,低字节存放在内存高地址,也称网络字节序。
- 小端:主机字节序。
枚举:
所以枚举变量的大小,实质是常数所占内存空间的大小
编译期间不会分配内存空间。
深拷贝和浅拷贝:
主要出现在类中含有指针类型的成员变量,当浅拷贝的时候,两个对象共用同一个指针,当析构的时候会发生两次析构。深拷贝就需要重载等号运算符,在堆区创建一个新的指针。
析构函数写成虚函数
避免内存泄漏,当父类指针指向一个子类对象,析构时如果父类不是虚析构,该对象就会走父类对象的析构函数,这样该对象中含有的子类内容没有被析构,会造成内存泄漏。
构造函数不能写成虚函数
- 从存储空间的角度考虑,构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
- 从使用的角度来看,虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的
- 从实现上来看,虚函数表是在创建对象之后才有的,因此不能定义成虚函数
- 从类型上来看,在创建对象时需要明确其类型
类的初始化顺序
设定本类虚函数指针、初始化列表、调用成员变量构造函数、本身构造函数。
初始化列表初始化顺序
与初始化列表无关,取决于在类中定义的顺序。
类的内存模型排布
虚函数指针,父类成员变量,子类成员变量。
内存对齐
方便CPU读取内存,寄存器每次读取一小块的字节,如果内存没有对齐就需要更多次数的读取完整的数据。
RAII
资源获取即初始化
编译器默认生成的默认构造函数:无参数,小心POD陷阱:int、float、double、void*、Object*不会被初始化为0.
std::move
不拷贝只移动,原来的对象会被清空,可以用于auto_ptr
explicit
将拷贝构造函数声明为explicit避免隐式拷贝。
printf
对函数参数的扫描是从后往前的,压栈,第一个找到的参数就是字符指针,根据偏移量移动堆栈指针。
右值引用
必须绑定到右值的引用,实现move语义
decltype、auto
自动推导,decltype可用于表达式和非静态类型。
- 语法格式不同:
auto var = value; decltype(表达式) var2 [= value];
- auto根据=右面的初始值value推导出变量var的类型;
- decltype根据后面括号中的表达式推导出变量的类型,和=右面的value没有关系;
- auto要求变量必须初始化,即定义变量的时候必须赋值;
- decltype将变量的类型和初始值分开
final、override
禁止对虚函数重写,显式地重写虚函数。
default、delete
使用默认,禁止
静态断言
static_assert(),可用来省略复杂的if-else语句,assert是一个宏。
tuple
tuple<int,int,str> tmp(1,2,"str");
get<0>(tmp)=10;
int a,b,s;
tie(a,b,s)=tmp;
什么是指令乱序?
从编译器的角度其实是对我们写的代码的一种优化,按照机器的角度讲一些指令代码执行顺序进行改变,优化程序实际执行的效率。 分析:之所以出现编译器乱序优化是因为编译器能在很大范围内进行代码分析,从而做出更优的执行策略,可以充分利用处理器的乱序执行功能。
指令乱序的问题:编译器优化产生的指令乱序可能会导致多线程程序产生意外的结果。
如何解决指令乱序问题?
内存屏障。
内存屏障,是一类同步指令,是对内存随机访问的操作中的一个同步点。此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。因为指令乱序执行的存在,就需要内存屏障保证程序执行的可靠。
Linux进程区分段及存储数据
Linux的每个进程都有各自独立的4G逻辑地址,其中0-3G是用户态空间,3-4G是内核态空间,不同进程相同的逻辑地址会映射到不同的物理地址中。逻辑地址分段自下而上为:
- 代码段。分为只读存储区和代码区,存放字符串是常量和程序机器代码和指令
- 数据段。存储已初始化的全局变量和静态变量。
- bss段。存储未初始化的全局变量和静态变量,及初始化为0的全局变量和静态变量
- 堆。 当进程未调用malloc时是没有堆段的,malloc/free开辟的内存空间,向上生长
- 映射区。存储动态链接库以及调用mmap函数进行的文件映射
- 栈。存储函数的返回地址、参数、局部变量、返回值,向下生长。
堆和栈区别
- 申请方式:栈是系统自动分配,堆是程序员主动申请
- 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上
栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的
- 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片
- 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制
GCC编译流程
- 预处理 -E .c --- .i
- 编译阶段 -s .i --- .s
- 汇编阶段 -c .s --- .o
- 链接阶段
静态库动态库区别和GCC加载库
- 静态库:编译时链接,浪费空间资源,修改需重新编译
- 动态库:运行时链接,节省空间浪费时间,修改时仅需要修改库即可
- GCC加载静态库:
-
- 将所有的.c文件编译成.o目标文件
gcc -c add.c
生成add.ogcc -c max.c
生成max.o
-
- 对生成的.o目标文件打包生成静态库
ar crv libfoo.a add.o max.o //libfoo.a是库的名字
- ar:做库的命令
- c:创建库
- r:将方法添加到库里
- v:显示过程,可以不要
-
- 使用静态库
gcc -o main main.c -static -L. -lfoo //这里写的foo是去掉前后缀后库的名字
- -L:指定路径 .代表当前路径
- -l:指定库名
- GCC编译加载动态库
-
- 对生成的.o文件处理生成共享库,共享库的名字为libfoo.so
gcc -shared -fPIC -o libfoo.so add.o max.o
- -shared 表示输出结果是共享库类型的
- -fPIC 表示使用地址无关代码(Position Independent Code)技术来生产输出文件
-
- 库的使用
cp libfoo.so /usr/lib //将库拷贝到系统库路径下
(不推荐)- export更改LD_LIBRARY_PATH当前终端的环境变量
- 修改/etc/ld.so.conf文件,加入库文件所在目录的路径,然后 运行ldconfig 目录名字,该命令会重建/etc/ld.so.cache文件即可
- 上面三种选一个即可
gcc -o main main.c -lfoo
预处理、编译、汇编、链接
- 预处理:
g++ -E test.cpp -o test.i
- 编译:
g++ -s test.i -o test.s
- 汇编:
g++ -c test.s -o test.o
- 链接:
g++ test.o -o test
- 调试:
g++ -g test.cpp -o test
- O2优化:
g++ -O2 test.cpp -o test
或g++ test.cpp -O2 test
- 库文件:
g++ -llog test.cpp
- 库路径:
g++ -L/home/centos -lmytest test.cpp
- 头文件:
g++ -I/myinclude test.cpp
- 警报:
g++ -Wall test.cpp
链接静态库
g++ swap.cpp -c -I../include
ar rs libswap.a swap.o
g++ main.cpp -Iinclude -Lsrc -lswap -o staticmain
生成静态库libswap.a
g++ main.cpp -lswap -Lsrc -Iinclude -o staticmain
生成可执行文件staticmain
extern-c的结果和CPP编译的结果
C++文件调用C文件会造成编译后链接出错,因为gcc和g++在编译之后的函数名符号表不同。
解决办法:加入extern "C"
语句就会按照C语言的连接方式寻找对应的函数名。
重载的底层原理
C++才有重载:会根据参数列表的参数重新命名函数用于区分不同的调用。
编译型语言和解释形语言
区别:翻译的时间不同,由此带来了跨平台性和效率的不同。
数组和指针
- 不同:概念、赋值、访问数据、sizeof意义和结果。
- 联系:数组传参时会退化成指针(效率高)
explicit
explicit关键字的作用就是防止对象间实现=赋值,防止类==构造函数的隐式自动转换==,类构造函数默认情况下即声明为implicit(隐式),另外explicit只用于==单==参数的构造函数,或者除了第一个参数以外的其他参数都有默认值.
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换
大小端字节序
- 大端:16进制高地址存放在内存的低地址,则为大端字节序
- 小端:16进制低地址存放在内存的低地址,则为小端字节序 代码判断:联合体是同一块内存被联合体中的所有成员公用
union U{int a' char b;};
U u;
u.a = 0x01020304
if (u.b == 0x04) {
cout << "little" << endl;
}
虚函数可以内联么?
内联函数在编译时起作用,当虚函数不表现多态性质的时候可以内敛(运行时)。
只在堆上/栈上创建对象
- 只在堆上(只能动态创建):将析构函数设置为私有,这样编译器在为类对象分配栈空间时检查析构函数,当析构函数不可访问时就不能在栈区创建对象。
- 只在栈区(只能静态创建):也即不能使用new创建,将new和delete重载为私有即可。
this指针
谁调用指向谁,*this表示对象本身。
- this只能调用非静态成员
- this指针使用
- 当形参与成员变量名相同时,用this指针来区分
- 为实现对象的链式引用,在类的非静态成员函数中返回对象本身,可以用
return *this
,this指向对象,//this表示对象本身。
常函数常对象
void func() const //常函数,此处func为类成员函数
const Person p2; //常对象
- 常函数修饰的是this指针,不允许修改this指针指向的值,如果执意要修改常函数,可以在成员属性前加mutable。
- 常对象不允许修改属性,不可以调用普通成员函数,可以调用常函数。
delete this合法性
合法但有前提:
- 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的
- 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
- 必须保证成员函数的 delete this 后面没有调用 this 了
- 必须保证 delete this 后没有人使用了
类的大小遵循结构体的对齐规则
- 类的大小与普通成员函数和静态成员无关(包括:普通成员函数、静态成员函数、静态数据成员、静态常量数据成员),与普通数据成员有关
- 虚函数对类的大小有影响,是因为虚函数指针的影响
- 虚继承对类的大小有影响,是因为虚基表指针带来的影响
- 空类的大小是 1
空类大小
sizeof(空class) = 1,为了确保两个不同对象的地址不同。
初始化列表
初始化列表的使用条件
- const类型的数据
- 引用类型的数据
友元
友元主要是为了访问类中的私有成员,会破坏C++的封装性,尽量不使用。
- 友元全局函数
- 友元函数声明可以在类中的任何地方,一般放在类定义的开始或结尾
- 一个函数可以是多个类的友元函数,只需要在各个类中分别声明
- 友元函数在类内声明,类外定义,定义和使用时不需加作用域和类名,与普通函数无异。
- 友元类
- 友元不可继承
- 友元是单向的,类A是类B的友元类,但类B不一定是类A的
- 友元不具有传递性,类A是类B的友元类,类B是类C的友元类,但类A不一定是类C的友元类。
- 友元成员函数
- 使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员
运算符重载
- 运算符重载的目的是扩展C++中提供的运算符的适用范围,使之能作用于对象,或自定义的数据类型
- 运算符重载的实质是函数重载,可以重载为普通成员函数,也可以重载为成员函数
- 运算符重载也是多态的一种,和函数重载称为静态多态,表示函数地址早绑定,在编译阶段就确定好了地址
- 重载运算符(),[] ,->, =的时候,运算符重载函数必须声明为类的成员函数
- 重载运算符<<,>>的时候,运算符只能通过全局函数配合友元函数进行重载
- 不要重载&&和||运算符,因为无法实现短路原则。
++i和i++
int& int::operator++() {
*this += 1;
return *this;
}
const int int::operator++(int) {
int oldVal = *this;
++(*this);
return oldVal;
}
继承中的同名处理
- 父类和子类成员属性同名,用子类声明对象调用子类属性,若想调用父类成员,则加上父类的作用域
- 父类和子类成员函数同名,子类函数不会覆盖父类的成员,只是隐藏起来,用子类声明对象调用子类成员函数,若想调用父类函数(包括重载),则加上父类的作用域
- 若子类中没有与父类同名的成员函数,子类声明对象后,可以直接调用父类成员函数。
菱形继承
- 问题:浪费空间;二义性
- 解决:在继承方式之前加上virtual
class Animal{
public:
int m_Age;
};
class Sheep:virtual public Animal{ int m_sheep;};
class Camel :virtual public Animal{ int m_camel;};
class Son :public Sheep, public Camel{
int m_son
};
虚函数表指针的位置排布
如果一个类带有虚函数,那么该类实例对象的内存布局如下:
首先是一个虚函数指针,接下来是该类的成员变量,按照成员在类当中声明的顺序排布,整体对象的大小由于内存对齐会有空白补齐。其次如果基类没有虚函数但是子类含有虚函数此时内存子类对象的内存排布也是先虚函数表指针再各个成员。如果将子类指针转换成基类指针此时编译器会根据偏移做转换。
虚函数的缺点
- 相比普通函数,虚函数调用需要2次跳转,会降低CPU缓存的命中率。
- 运行时绑定,编译器不好优化。
虚函数指针和虚函数表
- 前提发生了多态,每个类中都有虚函数表,最开始的父类创建虚函数表,后面的子类继承父类的虚函数表,然后对虚函数重写
- 虚函数重写(覆盖)的实质就是重写父类虚函数表中的父类虚函数地址;
- 实现多态的流程:虚函数指针->虚函数表->函数指针->入口地址,虚函数表(vftable)属于类,或者说这个类的所有对象共享一个虚函数表;虚函数指针(vfptr)属于单个对象。
- 在程序调用时,先创建对象,编译器在对象的内存结构头部添加一个虚函数指针,进行动态绑定,虚函数指针指向对象所属类的虚函数表。
- 虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个函数的指针。如果子类对父类中的一个或多个虚函数进行重写,子类的虚函数表中的元素顺序,会按照父类中的虚函数顺序存储,之后才是自己类的函数顺序。
- 编译器根本不会去区分,传进来的是子类对象还是父类对象,而是关心调用的函数是否为虚函数。如果是虚函数,就根据不同对象的vptr指针找属于自己的函数。父类对象和子类对象都有vfptr指针,传入对象不同,编译器会根据vfptr指针,到属于自己虚函数表中找自己的函数。即:vptr--->虚函数表------>函数的入口地址,从而实现了迟绑定(在运行的时候,才会去判断)。
虚函数是通过虚函数表来实现的,虚函数表里面保存了虚函数的地址,这张表保存在含有虚函数的类的实例对象的内存空间中,虚函数表的指针存放在对象实例的最前面的位置. 虚函数表是在编译阶段建立的,也就是说在程序的编译过程中会将虚函数的地址放在虚函数表中。
构造函数不能是虚函数
因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针
虚函数和纯虚函数
- 仅仅发生继承时,创建子类对象后销毁,函数调用流程为:父类构造函数->子类构造函数->子类析构函数->父类析构函数;
- 当发生多态时(父类指针或引用指向子类对象),通过父类指针在堆上创建子类对象,然后销毁,调用流程为:父类构造函数->子类构造函数->父类析构函数,不会调用子类析构函数,因此子类中会出现内存泄漏问题。
解决方法:将父类中的析构函数设置为虚函数,设置后会先调用子类析构函数,再调用父类析构函数
- 纯虚析构
- 纯虚析构需要类内声明,类外实现
- 纯虚析构也是虚函数,该类也为抽象类
- 子类不会继承父类的析构函数,当父类纯虚析构没有实现时,子类不是抽象类,可以创建创建对象。
纯虚函数调用错误原因
一般由以下几种原因导致:
- 从基类构造函数直接调用虚函数。
- 从基类析构函数直接调用虚函数。
- 从基类构造函数间接调用虚函数。
- 从基类析构函数间接调用虚函数。
- 通过悬空指针调用虚函数。 其中1,2编译器会检测到此类错误。3,4,5编译器无法检测出此类情况,会在运行时报错。
构造函数和析构函数执行函数体过程时,实例的虚表指针指向的是构造函数和析构函数本身所属的那部分的类的虚函数表,此时执行的虚函数都实际调用的是该类本身的虚函数,所以如果在基类的析构或者构造函数当中调用虚函数且该虚函数本身在基类当中是纯虚函数那么就会出现纯虚函数调用。
直接调用指的是函数内部直接调用虚函数,间接调用指的是函数内部调用其他的非虚函数内部直接或间接调用了虚函数。
函数指针和指针函数
- 函数指针(指向函数的指针):本质上是一个指针,只不过这个指针指向了一个函数,保存的是这个函数的地址(函数名)。
int (*p)(int, int)//表示所指向的函数返回值是int型,参数是两个int型.
- 指针函数(函数的返回值是指针):本质上是一个函数,只不过返回值是指针类型。
int* fun(int x, int y){}//这个fun函数的返回值是指针类型
构造函数的调用次数
有一个类A
- 如果定义该类型的一个数组
A t[5];
,数组长度是几,就会调用几次构造函数 - 如果定义该类型的指针数组
A *p[5];
,不会调用构造函数,这里数组中存放的是指向A类型的对象的指针
不能重载的运算符
.
成员访问运算符 ,保证访问成员的功能不能被改变.*
成员指针访问运算符,保证访问成员的功能不能被改变::
域运算符,运算对象是类型而不是变量sizeof
长度运算符,运算对象是类型而不是变量或一般表达式,不具备重载特征?:
条件运算符,运算对象是类型而不是变量或一般表达式,不具备重载特征
静态成员函数不能被声明为虚函数:
- 静态成员不属于任何类对象或类实例,所以即使加上virtual也没有任何意义
- 静态与非静态成员函数之间一个最主要的区别就是:静态成员函数没有this指针。调用类中的虚函数时,是通过虚表以及指向虚表的指针vptr才能完成虚函数的调用,并且只能用this指针来访问。对于静态成员函数没有this指针,无法访问vptr.
静态成员函数不能为const函数:
当声明一个类(Test)的非静态成员函数为const时,this指针相当于Test const *
,对于非const成员函数,this指针相当于Test *
. 但是static成员函数没有this指针,所以用const来修饰static没有任何意义。(volatile的道理也是如此)
运算符重载函数声明为一般函数还是友元函数
- 运算符重载可以重载为类的成员函数,也可以是类的友元函数。
- 一般情况下,单目运算符重载为类的成员函数,双目运算符重载为类的友元函数
代码是否会出错
class A
{
public:
A(int x){}
};
问:A a = 1;是否正确, 如果正确, 那么它调用了哪些函数?, 这里会进行隐式转化 A a(1).implicit 正确 这类题目更常见的是在基类和子类有不同实现方法。
多态的实现和应用场景
- 多态通过虚函数实现
- 应用场景:在实际开发中,一个功能有多种实现方式,流程相同,但是具体的细节有区别,例如:支付功能,可以进行支付宝支付、微信支付等
构造函数中应该避免抛出异常
- 构造函数中抛出异常后,对象的析构函数将不会被执行
- 构造函数抛出异常时,本应该在析构函数中被delete的对象没有被delete,会导致内存泄露
- 当对象发生部分构造时,已经构造完毕的子对象(非动态分配)将会逆序地被析构。
初始化列表的异常怎么捕获?
- 初始化列表构造,当初始化列表出现异常时,程序还未进入函数体,因此函数体中的try-catch不能执行,catch也无法处理异常。可以通过函数try块解决该问题。
- 函数try块中的try出现在表示构造函数初始值列表的冒号以及表示构造函数体的花括号之前,与这个try关联的catch既能处理构造函数体抛出的异常,也能处理成员初始化列表抛出的异常。
析构函数如何处理异常?
- 若析构函数抛出异常,调用std::abort()来终止程序
- 在析构函数中catch捕获异常并作处理,吞下异常;
- 如果客户需要对某个操作函数运次期间抛出的异常做出反应,class应该提供普通函数执行该操作,而非在析构函数中。
析构函数不应该抛出异常
- 其他正常,仅析构函数异常。 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
- 其他异常,且析构函数异常。 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
虚继承
虚继承是为了解决多重继承中命名冲突和数据冗余问题而提出的。例如:类D继承类B1,B2,而类B1,B2都继承自类A,因此在类D中出现两次类A中的变量和函数,那为了节省内存空间可以将B1,B2对A的继承定义为虚继承,而A就成了虚基类。
class A{};
class B1:public virtual A{};
class B2:public virtual A{};
class D:public B1,public B2{};
这样使用虚继承就能够确保在派生类D中只保存一份A的成员变量和成员函数。 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类,这个被共享的基类就称为虚基类。
虚基类的成员可见性问题: 假设A中定义了一个名为x的成员变量,当我们直接访问x时,会有三种可能:
如果B1,B2中都没有定义x,那么x将被解析为A的成员,不存在二义性; 如果B1或B2其中的一个类定义了x,那么也不存在二义性的问题,派生类的x比虚基类的x的优先级更高; 如果B1和B2中都定义了x,那么直接访问x会产生二义性问题
实现编译器处理虚函数表应该如何处理
- 编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。
- 如果派生类没有重新定义基类的虚函数A,则派生类的虚函数表中保存的是基类的虚函数A的地址,也就是说基类和派生类的虚函数A的地址是一样的
- 如果派生类重写了基类的某个虚函数B,则派生的虚函数表中保存的是重写后的虚函数B的地址,也就是说虚函数B有两个版本,分别存放在基类和派生类的虚函数表中
- 如果派生类重新定义了新的虚函数C,派生类的虚函数表保存新的虚函数C的地址
构造函数或者析构函数中调用虚函数会怎样
- 程序可以正常运行
- 但是无法达到虚函数调用的效果,当用基类的指针指向派生类的对象时,在调用基类的构造函数时,若出现虚函数的调用,程序的本意是调用派生类中的虚函数,但是当虚函数出现在构造函数或者析构函数中时,调用的是其所在类(基类)的虚函数。派生类对象构造期间进入基类的构造函数时,对象的类型变成了基类类型,而不是派生类类型。同样进入基类析构函数时,对象也是基类类型。
纯虚函数
纯虚函数在类中声明时,加上=0, 含有纯虚函数的类称为抽象基类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
- 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
- 抽象类可以声明为抽象类指针,可以声明抽象类的引用;
- 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。
静态绑定和动态绑定的介绍
静态类型和动态类型:
- 静态类型:变量在声明时的类型,是在编译期确定的。静态类型不能更改
- 动态类型:目前所指对象的类型,是在运行期确定的。动态类型可以更改 静态绑定和动态绑定:
- 静态绑定是指程序在编译的过程中确定对象的类型(静态类型)
- 动态绑定是指程序在运行期间确定对象的类型(动态类型) 静态绑定和动态绑定的区别: 发生的时期不同:如上
- 对象的静态类型不能更改,动态类型可以更改
- 要想实现多态,必须进行动态绑定
- 在继承体系中,只有虚函数是动态绑定,其他都是静态绑定 编译时多态和运行时多态
- 编译时多态:在程序编译过程中出现, 发生在模板和函数重载中(泛型编程)
- 运行时多态:在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数 编译时多态和运行时多态的区别:
- 编译时多态发生在程序编译过程中,运用泛型编程来实现,在编译时完成,提升程序的运行效率,但是对于无法实现模板的分离编译对于大程序编译时十分耗时
- 编译时多态无法处理异质对象的集合(异质对象是通过异质类定义的,异质类是指存储类型不一致的数据对象)
- 运行时多态体现了面向对象的特征,但是虚函数会占用一定的存储空间
- 运行时多态发生在程序的运行过程中,编译器无法进行优化处理 引申出:显示接口和隐式接口
- 显示接口:能够明确来源的接口,例如在运行时多态中,能够明确的知道所调用的函数是来源于哪个类
- 隐式接口:无法确定来源的接口,例如对于函数重载和模板,不知道是调用哪个实现
对象复用的了解,零拷贝的了解
- 零拷贝:不需要cpu参与在内存之间复制数据的操作
- 对象的复用:享元模式,通过创建一个对象池,以避免对象的重复创建
C++所有的构造函数
构造函数的作用:当创建对象时,系统分配了内存空间后,会自动调用相应的构造函数
- 默认构造函数:没有参数,如果创建了一个类,没有定义任何构造函数,系统会自动生成默认的构造函数
- 一般构造函数:带有参数,一个类可以有若干个一般构造函数,前提是参数的数量或者类型不同(C++重载函数原理)
- 拷贝构造函数:参数为该类的常量引用对象,如果类中没有定义拷贝构造函数,系统会默认生成一个默认的拷贝构造函数,默认生成的拷贝构造函数都是浅拷贝的
- 赋值构造函数:区别于以上构造函数,以上构造函数都没有函数的返回类型,这里虽然称为“赋值构造函数”,其实是“重载了赋值运算符的函数”,该函数的返回类型是该类的引用类型,参数是该类的常量引用对象。
什么情况下会调用拷贝构造函数(三种情况)
- 创建类的对象时,有两种情形:代入法
classA obj(1,2);
赋值法classB obj1 = obj;
- 当类的对象作为函数的参数时,由实参到形参的复制过程会调用拷贝构造函数
- 当类的对象作为函数的返回值时,会将返回值通过调用拷贝构造函数复制给一个临时对象,并传到函数的调用处
调试程序的方法,(程序异常退出如何排查,问到过)
- IDE设置断点进行调试
- Linux中没有IDE,可以打印log
- 打印中间结果
- 生成core文件
成员初始化列表
用来完成类中成员函数的初始化操作,初始化的顺序和在类中声明的顺序有关。
- 对于内置类型而言,在构造函数中使用初始化列表和在构造函数体中进行赋值,性能没什么差别
- 但是对于自定义类型而言,使用初始化列表可以使变量在初始化的时候直接调用拷贝构造函数即可;如果是在函数体中进行赋值,会先调用默认的构造函数创建该对象,然后调用赋值构造函数进行赋值。
C++的调用惯例(简单一点C++函数调用的压栈过程)
将参数压栈,然后压入函数返回地址,进行函数调用,通过跳转指令进入函数,将函数内部的变量去堆或栈上开辟空间,执行函数功能,执行完成,取回函数返回地址,进行接下来的执行过程
迭代器
行为类似指针,主要用于遍历容器解引用访问成员(把迭代器指向的对象拿出来)。
- 大量的运算符重载
- 是一种类模板,泛型编程的思想
- 使用函数模板的参数类型推导机制获得迭代器的型别(迭代器所指向的对象的类型)。该方法只能推导参数,而不能推导返回值类型。 迭代器的型别主要有两大类:原生指针,类类型型别
- 原生指针:不能用class template进行推导,需要对原生指针进行特殊化处理:偏特化和萃取。 偏特化:如果一个类模板中拥有一个以上的模板参数,可以针对某个(不是全部)模板参数进行特化。
萃取traits:中间层模板类iterator_traits
专门用于萃取迭代器的特性,而value type
正是迭代器的特性之一。
- const偏特化:如果一个迭代器的类型是pointer-to-const,它的value type应该是non-const型别,解决方法还是偏特化。
迭代器的型别
value type
:迭代器所指对象的类型。difference_type
:表示两个迭代器之间的距离。reference_type
:迭代器所知类型的引用。pointer_type
:相应的指针类型。iterator_category
:标识迭代器的移动特性。- input iterator:不允许修改对象,只读。
- output iterator:区间内只写操作。
- forward iterator:单向移动,可读写,每次只能移动一步,支持input iterator和output iterator所有操作。
- bidirectional iterator:双向移动,可读写。
- random access iterator:随机访问迭代器。
vector
类中包含:迭代器、构造函数、属性。 主要是用过三个指针操作数据:start、finish(和size有关)、end_of_storage(和capacity有关)。 begin() 返回的是start位置。
end() 返回的是finish,finish指向最后一个元素的后一个位置。
- 构造函数:主要操作以上三个指针进行多种方式的初始化操作(构造函数重载)。初始化要么全部初始化成功,要么全部失败。
- 析构函数:直接调用deallocate空间配置器,从头到尾释放数据,最后将内存还给空间配置器。
~vector() {
destroy(start, finish); //全局性的函数,destroy有两种调用方式:点,范围。
deallocate();
}
- push_back
- 判断finish指针和end_of_storage两指针是否重合,如果不重合就construct该对象并移动finish指针。
- 否则表示该空间没有位置了,调用insert_aux重新寻找一块更大的连续空间再进行插入。重新配置-移动数据-释放原空间。
- 注:如果原空间大小为0就分配一个空间,否则就分配当前空间的两倍。
- 注:一旦引起了空间重新配置,指向原vector的所有迭代器就都失效了。
void push_back(const T& x) {
if (finish != end_of_storage) {
construct(finish, x);
++finish;
} else {
insert_aux(end(), x);
}
}
- pop_back
void pop_back() {
--finish;
destroy(finish);
}
- erase 可以清除一个点也可以是一个范围。clear()底层就是调用从begin()到end()范围的erase函数。
- insert 和erase类似
- erase和insert都可能造成迭代器失效问题 元素的整体迁移导致原指针失效
list双向链表
迭代器的移动特性为双向迭代器
插入和删除不会造成原有list迭代器失效。
- splice函数
cache.splace(cache.begin(), cache, iter); //将iter放到cache.begin()前面。
list1.insert(++list1.begin(),3,9);//插入3个9
deque双端队列
常数时间对队头或队尾操作,空间分段连续。
deque采用一块所谓的map作为中控器(一小块连续空间),其中每一个元素都是指针,指向另外一块较大的连续线性空间,称之为缓冲区。缓冲区才是deque的存储空间主题。
deque的每个缓冲区又设计了3+1个迭代器.3个表示当前缓冲区的状态,1个指向map。
deque除了维护一个指向map的指针外也维护start(对应begin())和finish(对应end())两个迭代器分别指向缓冲区的第一个元素和最后一个缓冲区的最后一个元素。deque的begin()和end()不是一开始就指向map中控器的开头和结尾的,而是指向所有节点的最中央位置:这样的话头尾两端扩充的可能性一样大。
- deque的缓冲区 缓冲区的大小是一个全局函数决定的
struct __deque_iterator {
...
typedef T value_type;
T* cur; //缓冲区现在位置
T* first; //缓冲区的头
T* last; //缓冲区的尾
typedef T** map_pointer;
map_pointer node; //指向map
...
}
- deque的迭代器 operator需要先切换再判断是否已经达到缓冲区的末尾。超出该buffer范围就会调用内部的set_node改换下一个buffer区域
- deque构造函数的核心:create_map_and_nodes函数
- push_back、pop_back、push_front、pop_front 形式上看就像vector前边开口了
- erase和insert deque为了保证更高的效率,会先比较删除和插入元素的位置是处于中间偏后还是偏前面,总是移动较少的元素达到目的。
- clear 先删除中间的buffer,再删除两边的buffer,因为两边的buffer未必全部填满。
stack
是以deque为底层的适配器,堵住了deque的一端并隐藏了deque的迭代器使之不能遍历。
queue
是以deque为底层的适配器,只能一边进另一边出的数据结构,不可遍历。
priority_queue
优先队列也即堆。底层是使用vector描述堆结构+建堆操作heap:左子节点放在vec[2*i]
,右子节点放在vec[2*i+1]
。以大根堆为例,堆顶为最大值,每插入的新元素都放在vector的末尾(堆的叶子节点),然后对比其父节点的值进行up上浮操作。
- make_heap函数 将数组变成堆存放,默认大根堆也即make_heap之后vector的第一个元素为最大值(不是排序)。
- 优先队列也没有迭代器,不可遍历。
- 优先队列只能从尾部插入元素,头部删除元素。
红黑树RB-tree
有序组织的树结构
- 性质
- 每个节点要么黑色要么红色。
- 根节点黑色
- 叶子节点黑色
- 红色节点的子节点一定是黑色
- 从根节点到叶子节点的每一条路径上的黑色节点数量相同
- 红黑树应用
- C++ STL:set/multy_set、map/multy_map
- Linux:虚拟内存管理、epoll内核实现、进程调度
- Nginx:Timer管理机制
- RB-tree基本操作 旋转:在添加或删除结点之后会破坏红黑树结构,旋转就是为了保持红黑树的特性。
旋转中的左旋操作:对节点x进行左旋,也就是说让节点x成为左节点。
- 插入方式
- insert_unique不允许插入重复键值
- insert_equal允许插入重复键值
hashtable散列表(哈希表)
使用散列函数将大集合中的数值映射到一个较小的集合中
SGI中的哈希表使用的是开链法(数组+链表)实现,此外还有开放寻址法。
- 哈希表扩容 当vector挂载的链表比较多,超过了负载因子的时候就会发生扩容操作。
- 创建一个新桶,该桶是原来桶两倍大最接近的质数(判断n是不是质数的方法:用n除2到sqrt(n)范围内的数) ;
- 将原来桶里的数通过指针的转换,插入到新桶中(注意STL这里做的很精细,没有直接将数据从旧桶遍历拷贝数据插入到新桶,而是通过指针转换两个桶的地址)
- 通过swap函数将新桶和旧桶交换,销毁新桶
其实就是vector扩容。扩容的倍数一般是当前size的两倍最近的一个质数。每一个值就是一个bucket.扩容之后还需要进行重新映射,所以键值存储顺序会发生变化,迭代器失效。
set/multiset
底层就是红黑树,不允许更改元素的值,因为这样会破坏红黑树的结构。 set和multyset的区别就是前者底层调用的是insert_unique不允许出现重复,后者调用insert_equal允许有重复。
unorder_set/unordered_multyset
底层就是hashtable,区别同上。
map/multymap
和set/multyset很类似,只是元素由单个值变成了键值对pair类型,其中pair的first最为键等同于set的值不可以更改。其他都差不多,实现了[]的运算符重载。 使用[]的时候,如果没有key会自动创建。
unorderedmap_map/unordered_multymap
和unordered_set/unordered_multymap差不多。
遍历变换函数
for_each(beg, end, func)
将[beg,end)内的元素依次调用func函数,beg和end是迭代器。
最大值最小值
max(a, b)
max(a, b, cmp)
自定义比较规则max({a, b, c, d, ...})
同时比较多个元素返回最大值max_element(beg, end)
返回[beg,end)内的最大元素对应的迭代器(位置),需要解引用取出最大值。max_element(beg, end, cmp)
自定义比较规则。 min同理。
排序
sort(beg, end)
sort(beg, end, cmp)
partion(beg, end, func)
元素重新排序,使用func函数把true元素放在false元素之前stable_sort(beg, end)
稳定排序
随机
random_shuffle(beg, end)
统计
count(beg, end, val)
count_if(beg, end, func)
用func代替==统计
查找
find(beg, end, val)
找到就返回iter没找到就返回endfind_if(beg, end, func)
func代替==查找lower_bound(beg, end, val)
二分查找第一个大于等于val的iterupper_bound(beg, end, val)
二分查找第一个大于val的iter
替换
replace(beg, end, old_val, new_val)
replace(beg, end, func, new_val)
func函数下为ture的值改成new_val
STL swap函数
- 除了数组,其他容器在交换后本质上是将内存地址进行了交换,而元素本身在内存中的位置是没有变化
- swap在交换的时候并不是完全将2个容器的元素互换,而是交换了2个容器内的内存地址。