Skip to content

Latest commit

 

History

History
286 lines (222 loc) · 16.8 KB

README.md

File metadata and controls

286 lines (222 loc) · 16.8 KB

C++

  • 基础知识
  • 面向对象
  • STL
  • 多线程

面向对象

  • 三大特征:封装,继承,多态。
  • 为什么要面向对象?C语言是结构化设计,无法解决重用,扩展和维护的问题。
  • 封装:将数据和操作数据的方法封装到一起。
  • 继承:直接继承已有类的属性和方法,减少代码重复。
  • 多态:对相同的方法做出不同的响应,提高代码灵活性。

特殊成员函数

  1. 构造与析构
  2. 拷贝构造与拷贝赋值
  3. 移动构造与移动赋值

new和malloc的区别?

  • malloc只分配内存不初始化;new不仅分配内存也初始化,new分配内存以后自动调用构造函数。
  • malloc分配内存时必须指定内存大小,而new可以自动计算。malloc分配完成后返回的是void*类型,需要强转,而new返回的是对应类型的指针。
  • malloc分配内存失败时返回NULL,而new分配内存失败时抛出bad_alloc异常。

多态原理

  • 多态分类两大类:静态多态和动态多态。
  • 静态多态是重载和模板。动态多态:也叫运行时多态,是通过继承和虚函数实现的。
  • 在具有继承关系的子类中,子类重写父类的虚函数,通过父类引用或指针指向子类对象时,产生不同的行为叫做多态。
  • 多态的核心在于虚函数表指针,每个对象都有一个虚函数表指针,虚函数表指针指向一张虚函数表,表中记录了虚函数的入口地址,如果子类重写虚函数后,这个地址就会替换掉。
  • 多态的好处:在于更方便程序的扩展。
  • 坏处在于每个对象多了一个4字节的指针,同时每次查询虚函数表需要耗时。

智能指针

  1. unique_ptr独享指针的所有权,无法进行拷贝构造赋值的操作,只能通过move函数进行所有权的转换。
  2. shared_ptr共享对象,它使用引用计数来保存当前有多少个智能指针在引用这个对象,当引用计数降为0时,对象会被销毁。
  3. weak_ptr称为弱引用,用于辅助shared_ptr正常工作,主要解决shared_ptr可能会产生的环形引用问题。weak_ptr不会增加对象的引用计数,共享指针可以直接赋值给弱指针,同时弱指针可以使用lock函数来获取shared_ptr对象。

static

  • static的使用可以分为两类,一类是用在普通变量和函数上,另一类是用在类中。
  1. 普通变量分为全局变量和局部变量。声明为静态全局变量是在全局区分配内存,并且只在当前文件可见,在文件之外是不可见的。其他文件定义同名变量不会发生冲突。变量的值只在第一次执行时进行初始化。声明为静态局部变量时与全局变量类似,只是作用域为局部作用域。
  2. 静态普通函数,只在当前文件中可见,其他文件中定义同名函数不会发生冲突。
  3. static用在类中,首先是静态成员变量,在类中声明,类外初始化。所有对象共享一份数据。
  4. 然后是静态成员函数:所有对象共享同一个函数,静态成员函数只能访问静态成员变量。

explicit

  • 只能用来修饰类的构造函数,阻止构造函数进行隐式转换。

Linux内存模型

  • 从高地址到低地址
  1. 环境变量和命令行参数
  2. 栈区
  3. 共享区
  4. 堆区
  5. 未初始化数据段.bss
  6. 初始化数据段.data
  7. 代码段.text

指针和引用的区别

  1. 指针保存的是所指对象的地址,而引用是所指对象的别名。指针需要通过解引用间接访问对象的值,引用可以直接访问。
  2. 指针可以有多级指针,而引用最多两级。并且两个取地址符是右值引用。右值引用是为了减少深拷贝的次数。
  3. 指针可以不初始化,即使初始化以后也可以改变。而引用必须初始化,同时初始化以后不许改变。
  4. 引用的本质是指针常量。指针常量不可以修改指向,但是可以修改指向的值。常量指针刚好与之相反。

vector底层原理

  • 首先,vector的基类是三根指针,分别是start/finish/end_of_storage用来指示当前分配到的空间所用的起始位置,终止位置和容量尾部。然后,当finish指针到达end_of_storage的位置时,操作系统会寻找当前容量大小2倍的连续内存空间,并且将旧内存中的数据拷贝到新内存,然后释放旧内存。其次,如果重新分配了内存,原来的迭代器就会失效。频繁的开辟新内存比较耗时。如果可以预知使用的大小,可以使用reserve函数,预先开辟足够大的空间。或者使用swap函数收缩内存空间。

代码生成可执行文件的过程

  • 主要分为四个步骤
  1. 预编译阶段:对g++编译器指定-E参数,生成.i文件。这个阶段的主要工作是将所有的宏展开,去掉所有的条件预编译指令,将所有的头文件包含进来,删除注释等。
  2. 编译阶段:对g++编译器指定-S参数,生成.s汇编文件。这个阶段的主要工作是对代码的语法,语义和词法等进行分析。
  3. 汇编阶段: 对g++编译器指定-c参数,生成.o二进制文件。
  4. 链接阶段:将各个模块之间的相互引用处理好。把所有的静态库用到的目标文件装入程序中,并进行统一编址,然后进行重定位,即逻辑地址到物理地址的转换。

静态库与动态库

  1. 静态库:命名方式为lib开头加上自定义的静态库名,然后以.a结尾。静态库实际上是一组目标文件的集合,再链接阶段与调用的程序生成可执行文件。静态库的优点在于:代码加载速度快,发布程序时,不需要提供对应的库;缺点是:可执行文件体积大,同时如果静态库有修改,调用的程序需要重新编译,而编译的耗时比较久。
  2. 动态库:命名方式为lib开头加上自定义的动态库名,然后以.so结尾。动态库首先生成与位置无关的目标文件,然后再运行时加载到内存。优点是:动态库可以共享,节省了系统资源,动态库进行修改后,无需重新编译。缺点是加载速度比静态链接慢,发布程序时,需要提供动态库。

指针常量

int* const p = &a
  • 指针常量必须初始化,一旦初始化完成,就不能再修改它的值,即指针的指向不可变。
  • 引用的本质是指针常量

声明和定义的区别

  1. 声明是告诉编译器有这个变量和函数的存在,但是需要到其它地方去寻找。
  2. 定义包含了声明,但是声明不包含定义,定义时才分配存储空间。

struct和class的区别

  1. 共同点:C++中,可以用struct和class定义类,都可以继承。
  2. 不同点:struct默认继承权限和默认访问权限时public class类的默认继承权限和访问权限时private。

const关键字

  • const可以用于限定变量,指针和函数不可改变,同时明确指定了类型,可以方便编译器做类型检查,也增加了代码的可读性。
  1. const修饰变量必须初始化。如果是全局的const变量,通常放在静态区。在局部声明的const变量放在栈区。
  2. const修饰成员函数时,函数中的成员变量不可改变,除非该变量特别声明为mutable
  3. const可以用来修饰指针,称为常量指针const int *p 指针的指向可以改变,但是不能改变指针指向的值。
  4. const修饰常量的指针叫做指针常量,int* const p 指针的指向不可以修改,指针指向的值可以修改。指针常量必须初始化。

const和define的区别

  • const可以明确指定数据类型,而宏定义没有数据类型。
  • define宏是在预处理阶段展开,const常量是在编译运行阶段使用。
  • define宏不分配内存,变量定义分配内存。

extern关键字

  1. 引入同一模块在其他文件中定义的全局变量和函数。
  2. 如果在C++里调用了C库定义函数,那么需要使用extern "C" 标识这个函数,告诉编译器使用C的方式进行编译,防止C++的编译方式导致命名重整,无法找到对应的C函数。命名重整的原因在于C++支持函数重载,而C不支持,所以C++编译时增加了函数参数的标识符。

this关键字

  1. 解决同名冲突
  2. 返回对象本身
  • this指针的本质是指针常量,指针的指向不可以修改。

std::move()

  • 将左值强制转换为右值引用,右值引用可以减少一次对象的析构和对象的构造。
  • 右值引用可以减少深拷贝的次数。

段错误

  • 段错误通常发生在访问非法内存地址的时候。系统会发送一个SIGSEGV11号信号告诉当前进程,进程采取默认的捕获方式,即终止进程。
  1. 野指针
  2. 试图修改字符串常量的内容

auto关键字

  • 让编译器能够根据初始值的类型推断变量的类型。当处理复杂类型,比如STL中的类型时,优势最明显。auto p = vt.begin()

四种强制类型转换

  1. static_cast低风险的转换,比如整数转浮点数,字符型转整形。
  2. const_cast去掉const关键字的转换,可以去掉带const的指针和引用。
  3. dynamic_cast使具有继承关系的基类转换为派生类,如果不可以转换则返回NULL。
  4. reinterpret_cast指针或引用的转换,风险较高。

RTTI

  • 运行时类型识别
  • run time type identification
  • 常常结合typeid()和dynamic_cast实现。可以根据当前调用的指针是何种类型,经过dynamic_cast转换后,调用非虚函数。
  • dynamic_cast只能用于指针和引用的转换,要转换的类型中必须包含虚函数,转换成功返回子类的地址,失败返回NULL。
  • typeid返回一个type_info对象的引用。

构造函数不能是虚函数

  • 虚函数是通过虚函数表指针来调用的,而虚函数表指针存在对象内存空间。当一个对象调用构造函数时,该对象还没有实例化,即没有分配内存空间,所以虚函数表指针无法找到。

析构函数尽量是虚函数

  • 析构函数不是虚函数容易引起内存泄漏。
Animal *animal = new Cat();
  • 为了实现多态的动态绑定,通常将基类指针指向派生类对象。
  • 当指针销毁时,如果析构函数不是虚函数,根据析构函数在继承中的调用顺序,则派生类对象的析构函数将不会被执行,造成内存泄漏。

析构函数不能抛出异常

  • 析构函数抛异常,则异常点之后的的程序不会执行,如果异常点之后有释放资源的操作,则这部分资源无法释放,导致内存泄漏。noexcept

内存泄漏

  • 不再需要使用的内存单元,没有及时释放。
  • memcheck和valgrind检测内存泄漏的工具。
  • 使用RAII资源获取就是初始化和智能指针。

野指针

  • 一些内存的单元已被释放,之前指向它的指针还在被使用。

vector和list的区别

  1. vector是动态数组,在内存中分配一块连续的内存空间,因此可以使用下标进行快速的随机访问。但是删除和插入需要移动大量的元素。
  2. list是双向链表,在内存中是不连续的空间,由指针将不同的地址连接在一起。list的插入和删除操作都是O(1)的。
  3. 数组必须事先设定固定的长度,不能动态的增减,可能会造成资源浪费。链表可以动态的增减。

内存对齐

  • 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;
} 

volatile

  • 用来告诉编译器不要对该变量做任何优化,编译器每次操作该变量时,一定要从内存中取出,而不是使用寄存器中与存在的值,因为值可能已经发生了改变。 应用场景:
  1. 并行设备的硬件寄存器(如状态寄存器)。
  2. 多线程中共享变量。

mmu内存管理器

  • 主要作用:虚拟内存到物理内存的地址映射。 设置修改内存访问级别。

虚拟内存的作用

  1. 主存容量有限
  2. 分隔进程,保证进程空间彼此不受干扰
  3. 基于局部性原理进行页面替换 虚拟内存的大小由计算机的地址总线决定 cache名字和TLB命中没有必然联系,是两种独立的机制。 CPU和Cache之间交换的单位是字节,Cache和内存之间交换的单位是块。

coredump文件

  • gdb可以用于分析coredump文件。coredump文件含有进程被终止时内存/CPU寄存器和各种函数调用栈的信息。

  • 产生coredump文件的原因:

  1. 内存访问越界
  2. 多线程使用了线程不安全的函数
  3. 多线程读写的数据未加锁保护
  4. 栈溢出
  • core文件没有符号表信息,必须结合可执行文件才可调试

模板特化

  • 全特化:模板参数被指定未确定的类型
  • 偏特化:模板参数没有被全部确定,需要编译器在编译时进行确定。只能偏特化类模板,不能偏特化函数模板。
  • 别名模板和变量模板属于语法糖

右值引用

  • 右值引用指向要被销毁的对象。右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
  • move函数将左值转换为右值,调用move函数后源对象只能赋值或销毁。

override

  • override在子类中标记某个函数,表示想要覆盖已有的虚函数,如果没有覆盖,编译器会报错。 加作用域运算符调用特定类的虚函数

内联函数的优劣

  1. 优点:减少函数调用的开销,包括寄存器值的保存和实参的拷贝等。
  2. 缺点:增加函数体积,可能导致cache装不下,从而减少了cache的命中率。
  • inline只是一个请求,编译器有权拒绝。

拷贝构造函数

调用场景:

  1. 一个对象以值传递传参
  2. 一个对象以值传递的方式从函数返回
  3. 一个对象通过另一个对象初始化

空类

  • 占有一个字节
  • 有构造,析构,拷贝,赋值运算符,取地址运算符。
  • 构造函数可以被重载,析构函数不可以被重载且不能带参数。

explicit

  • explicit取消隐式转换,类中构造函数默认是implicit

  • explicit关键字的作用是防止类构造哈桑农户的隐式自动转换,只对有一个参数的构造函数有效。

堆和栈的区别

  1. 栈连续,堆不一定连续。
  2. 申请方式不同。栈由操作系统自动分配,堆需要程序员自己申请。
  3. 生长方向不同。栈由高地址向地址生长,是一块连续的内存区域。堆由地址向高地址生长,是不连续的内存区域。在一个链表中记录空间内存地址。
  4. 分配速度。栈由系统分配,速度较快。堆使用new分配,速度较慢,且容易产生内部碎片。

static的作用

  • static可以用来修饰函数和变量。修饰全局变量和局部变量时都是放在静态区,static变量只初始化一次,在程序结束时销毁,全局和局部的区别在于作用域不同。static可以修饰普通成员函数,表明这个函数只在本文件中有效。static修饰类成员变量是,这些变量为这个类所共享,static修饰类成员函数时,也是所有对象共享这个函数,该函数中没有this指针。同时static类成员函数中只能调用static修饰的函数。

静态存储区

存放的static修饰的全局变量和局部变量,const修饰的变量以及字符串。

数据段和静态区的区别

  • 数据段存放的是代码的二进制指令。静态区是变量。