逻辑用语千万条,第一首先往里套。
- 首先,其次,然后,最后。
- 第一,第二,第三,第四。
书籍
- 《C++ primer》 第五版
- 《后台开发》 徐晓鑫
- 《linux高性能服务器编程》 游双
- 《redis设计与实现》黄健宏
- 《muduo库》陈硕
static
static的使用可以分为两类,一类是用在普通变量和函数上,另一类是用在类中。
- 普通变量分为全局变量和局部变量。声明为静态全局变量是在全局区分配内存,并且只在当前文件可见,在文件之外是不可见的。其他文件定义同名变量不会发生冲突。变量的值只在第一次执行时进行初始化。声明为静态局部变量时与全局变量类似,只是作用域为局部作用域。
- 静态普通函数,只在当前文件中可见,其他文件中定义同名函数不会发生冲突。
- static用在类中,首先是静态成员变量,**在类中声明,类外初始化。**所有对象共享一份数据。
- 然后是静态成员函数:所有对象共享同一个函数,静态成员函数只能访问静态成员变量。
多态
多态分类两大类:静态多态和动态多态。静态多态是重载和模板。动态多态:也叫运行时多态,是通过继承和虚函数实现的。在具有继承关系的子类中,子类重写父类的虚函数,通过父类引用或指针指向子类对象时,产生不同的行为叫做多态。
多态的核心在于虚函数表指针,每个对象都有一个虚函数表指针,虚函数表指针指向一张虚函数表,表中记录了虚函数的入口地址,如果子类重写虚函数后,这个地址就会替换掉。多态的好处在于更方便程序的扩展,坏处在于每个对象多了一个4字节的指针,同时每次查询虚函数表需要耗时。
智能指针
C++11有3个智能指针,分别是unique_ptr, shared_ptr和weak_ptr
- unique_ptr独享指针的所有权,无法进行拷贝构造赋值的操作,只能通过move函数进行所有权的转换。
- shared_ptr共享对象,它使用引用计数来保存当前有多少个智能指针在引用这个对象,当引用计数降为0时,对象会被销毁。
- weak_ptr称为弱引用,用于辅助shared_ptr正常工作,主要解决shared_ptr可能会产生的环形引用问题。weak_ptr不会增加对象的引用计数,共享指针可以直接赋值给弱指针,同时弱指针可以使用lock函数来获取shared_ptr对象
malloc和new
malloc和new都是在堆上开辟内存,进行动态的管理。
- new是操作符,malloc是库函数
- malloc只负责开辟内存,没有初始化功能。new不但能开辟内存,还可以初始化。
- malloc必须指定开辟内存的大小,并且返回类型为void*,因此malloc的返回值一般都需要进行类型转换。new可以自动计算出所需内存的大小,并且返回指定类型的指针。
- malloc和new如果操作是内置数据类型两者基本类似,不同在于申请失败时。malloc申请失败时返回**NULL。**new申请失败抛出异常。
- 自定义类型时,new先调用operator new函数申请空间,然后在申请的空间上执行构造函数。
C++内存模型
从高地址到低地址
- 环境变量和命令行参数
- 栈区
- 共享区
- 堆区
- 未初始化数据段.bss
- 初始化数据段.data
- 代码段.text
指针和引用的区别
- 指针保存的是所指对象的地址,而引用是所指对象的别名。指针需要通过解引用间接访问对象的值,引用可以直接访问。
- 指针可以有多级指针,而引用最多两级。并且两个取地址符是右值引用。右值引用是为了减少深拷贝的次数。
- 指针可以不初始化,即使初始化以后也可以改变。而引用必须初始化,同时初始化以后不许改变。
- 引用的本质是指针常量。指针常量不可以修改指向,但是可以修改指向的值。常量指针刚好与之相反。
vector底层原理
首先,vector的基类是三根指针,分别是start/finish/end_of_storage用来指示当前分配到的空间所用的起始位置,终止位置和容量尾部。然后,当finish指针到达end_of_storage的位置时,操作系统会寻找当前容量大小2倍的连续内存空间,并且将旧内存中的数据拷贝到新内存,然后释放旧内存。其次,如果重新分配了内存,原来的迭代器就会失效。频繁的开辟新内存比较耗时。如果可以预知使用的大小,可以使用reserve函数,预先开辟足够大的空间。或者使用swap函数收缩内存空间。
代码生成可执行文件的过程
主要分为四个步骤
- 预编译阶段:对g++编译器指定-E参数,生成.i文件。这个阶段的主要工作是将所有的宏展开,去掉所有的条件预编译指令,将所有的头文件包含进来,删除注释等。
- 编译阶段:对g++编译器指定-S参数,生成.s汇编文件。这个阶段的主要工作是对代码的语法,语义和词法等进行分析。
- 汇编阶段: 对g++编译器指定-c参数,生成.o二进制文件。
- 链接阶段:将各个模块之间的相互引用处理好。把所有的静态库用到的目标文件装入程序中,并进行统一编址,然后进行重定位,即逻辑地址到物理地址的转换。
静态库与动态库
- 静态库:命名方式为lib开头加上自定义的静态库名,然后以.a结尾。静态库实际上是一组目标文件的集合,再链接阶段与调用的程序生成可执行文件。静态库的优点在于:**代码加载速度快,发布程序时,不需要提供对应的库;**缺点时:可执行文件体积大,**同时如果静态库有修改,调用的程序需要重新编译,**而编译的耗时比较久。
- 动态库:命名方式为lib开头加上自定义的动态库名,然后以.so结尾。动态库首先生成与位置无关的目标文件,然后再运行时加载到内存。优点是:动态库可以共享,节省了系统资源,动态库进行修改后,无需重新编译。缺点是加载速度比静态链接慢,发布程序时,需要提供动态库。
符号表
每个目标文件除了拥有自己的数据和二进制代码外,还提供了3个表:
- **未解决符号表:**提供了所有在该编译单元里引用但是定义并不是在本编译单元的符号及其出现的地址。【引用无定义】将extern声明的变量置入未解决符号表。【外部链接】
- **导出符号表:**提供了本编译单元具有定义,并且愿意提供给其他单元使用的符号及地址。【有定义肯让外用】普通变量及其函数被置入导出符号表。
- **地址重定向表:**提供了本编译单元所有对自身地址的引用的记录。static声明的全局变量放入地址重定位表中。【内部链接】
指针常量
int* const p = &a
指针常量必须初始化,一旦初始化完成,就不能再修改它的值,即指针的指向不可变。
引用的本质是指针常量
声明和定义的区别
- 声明是告诉编译器有这个变量和函数的存在,但是需要到其它地方去寻找。
- 定义包含了声明,但是声明不包含定义。
- 定义时才分配存储空间。
C和C++的区别
- 设计思想上: C是面向过程的结构化语言,CPP是面向对象的语言
- 语法上: CPP具有三大特性,封装继承多态 CPP相对于C增加了许多类型安全的功能,比如四种强制类型转换 CPP支持范式编程,如模板类,函数模板等
struct和class的区别
共同点:C++中,可以用struct和class定义类,都可以继承。
不同点:struct默认继承权限和默认访问权限时public class类的默认继承权限和访问权限时private。
volatile关键字
对类型额外修饰的作用,类似于const。告诉编译器不要对这样的对象进行优化,因为该对象的值可能在程序的控制或检测之外被改变。
const关键字
const 可以用于限定变量,指针和函数不可改变,同时明确制定了类型,可以方便编译器做类型检查,也增加了代码的可读性。
const修饰变量必须初始化。如果是全局的const变量,通常放在静态区。在局部声明的const变量放在栈区。
const修饰成员函数时,函数中的成员变量不可改变,除非该变量特别声明为mutable
const可以用来修饰指针,称为常量指针const int *p 指针的指向可以改变,但是不能改变指针指向的值。
const修饰常量的指针叫做指针常量,int* const p 指针的指向不可以修改,指针指向的值可以修改。指针常量必须初始化。
const可以明确指定类型,而宏定义没有数据类型。
define宏是在预处理阶段展开。const常量是编译运行阶段使用。
宏定义不分配内存,变量定义分配内存。
extern关键字
- 引入同一模块在其他文件中定义的全局变量和函数。
- 如果在C++里调用了C库定义函数,那么需要使用
extern "C"
标识这个函数,告诉编译器使用C的方式进行编译,防止C++的编译方式导致命名重整,无法找到对应的C函数。命名重整的原因在于**C++支持函数重载,而C不支持。**所以C++编译时增加了函数参数的标识符。 - extern通常放在为解决符号表中,表示定义不在本文件而引用的变量。
this关键字
- 解决同名冲突
- 返回对象本身
this指针的本质是指针常量,指针的指向不可以修改。
move函数
将左值强制转换为右值引用,右值引用可以减少一次对象的析构和对象的构造。
右值引用可以减少深拷贝的次数。
段错误
段错误通常发生在**访问非法内存地址的时候。**系统会发送一个SIGSEGV11号信号告诉当前进程,进程采取默认的捕获方式,即终止进程。
- 野指针
- 试图修改字符串常量的内容
auto关键字
让编译器能够根据初始值的类型推断变量的类型。当处理复杂类型,比如STL中的类型时,优势最明显。auto p = vt.begin()
四种强制类型转换
- static_cast 低风险的转换,比如整数转浮点数,字符型转整形
- const_cast 去掉const关键字的转换,可以去掉带const的指针和引用
- dynamic_cast 使具有继承关系的基类转换为派生类,如果不可以转换则返回NULL
- reinterpret_cast 指针或引用的转换,风险较高
RTTI
run time type identification 运行时类型识别。**常常结合typeid()和dynamic_cast实现。**可以根据当前调用的指针是何种类型,经过dynamic_cast转换后,调用非虚函数。**dynamic_cast只能用于指针和引用的转换,要转换的类型中必须包含虚函数,转换成功返回子类的地址,失败返回NULL。**typeid返回一个type_info对象的引用。
构造函数不能是虚函数
虚函数是通过虚函数表指针来调用的,而虚函数表指针存在对象内存空间。当一个对象调用构造函数时,该对象还没有实例化,即没有分配内存空间,所以虚函数表指针无法找到。
析构函数尽量是虚函数
析构函数不是虚函数容易引起内存泄漏。
为了实现多态的动态绑定,通常将基类指针指向派生类对象,当指针销毁时,如果析构函数不是虚函数,根据析构函数在继承中的调用顺序,则派生类对象将不会被析构,造成内存泄漏。
析构函数不能抛出异常
析构函数抛异常,则异常点之后的的程序不会执行,如果异常点之后有释放资源的操作,则这部分资源无法释放,导致内存泄漏。noexcept
内存泄漏
不再需要使用的内存单元,没有及时释放。memcheck和valgrind检测内存泄漏的工具。使用RAII资源获取就是初始化和智能指针。
野指针
一些内存的单元已被释放,之前指向它的指针还在被使用。
vector和list的区别
- vector是动态数组,在内存中分配一块连续的内存空间,因此可以使用下标进行快速的随机访问。但是删除和插入需要移动大量的元素。
- list是双向链表,在内存中是不连续的空间,由指针将不同的地址连接在一起。list的插入和删除操作都是O(1)的。
- 数组必须事先设定固定的长度,不能动态的增减,可能会造成资源浪费。链表可以动态的增减。
浅拷贝
由于编译器默认的拷贝构造函数只是简单的位拷贝,可能会导致内存的重复释放。解决浅拷贝的办法通常使用深拷贝,即自己实现拷贝构造函数,在堆上重新分配内存。
内存对齐
union最大成员所占的整数倍,同时能容纳其他的成员。union中变量共用内存,应以最长的为准。
struct按照成员的声明顺序,依次安排内存,偏移量为成员大小的整数倍,最后结构体的大小为最大成员所占大小的整数倍。在C++中,空结构体和空类的内存所占大小为1个字节。C中空结构体所占大小为0。
为什么要有内存对齐:1. 硬件原因:**加速CPU的访问速度。**因为CPU和内存数据交换的基本单位是块,块的大小为2的n次方字节。内存未对齐可能需要多次访问内存。2. 平台原因:不是所有的平台都支持任意地址的数据访问。
#include <iostream>
using namespace std;
typedef union{
long long i; //8 bytes
int k[5]; //4 bytes 最长的成员不是20
char c; // 1 byte
}UDATE;
//联合体共用内存 最长成员为8字节 结果要为8的倍数 同时要能容纳其他成员,即大于等于20字节 所以为24字节
struct data{
int cat; // 4 bytes
UDATE cow; //24 bytes 但是需要先拆开来 最长成员为8字节
double dog; //8 bytes
}too;
//结构体顺序考虑,结果为最大成员的整数倍,如果后一个成员的长度的开始位置不是整数倍需要填充字节
//cat占4个字节 填充4个字节
//起始位置为8 满足整数倍 cow占用24字节
//起始位置为32 满足整数倍 doule占用4字节
//所以结构体总共占用40字节,同时40也是8的倍数。
UDATE temp;
int main(){
cout<<sizeof(temp)<<" "<< sizeof(struct data)<<endl; //24 40
return 0;
}
gdb调试
gdb可以用于分析coredump文件,coredump文件中含有当进程被终止时内存,cpu寄存器和各种函数堆栈信息等。
- 设置断点 b 120
- 运行 r
- 打印遍历p number
- 查看堆栈bt
- 查看循环中的变量 i
- 单步运行n