# C++ 面试题
# 程序编译过程
预处理
- 完成宏定义
#define
内容替换 - 处理所有的条件预编译指令,如
#if #endif
- 把
#include
文件内容复制到.cpp
文件中(递归执行) - 删除所有注释
- 完成宏定义
编译:转化为汇编代码,主要工作是检查一些语法规则,代码优化(寻找合适的寻址方式,使用位运算来替代乘法运算,删除多余的指令)
汇编:将汇编代码转化为二进制格式的文件(机器码)
链接:将多个目标文件以及库文件链接成最终的可执行文件
静态链接:在链接阶段将库文件的函数和数据合并到应用程序中,组成一个最终的可执行文件.
运行速度快:可执行程序中具备运行阶段所需要的所以东西
空间浪费:可执行程序中有所以目标文件的副本。当多个程序对同一个目标文件有依赖关系时,那么这个目标文件在内存中会有多个副本
当其中一个库文件需要修改,整个程序需要重新编译
动态链接:在程序运行阶段才把多个目标文件链接成一个完整的程序.
运行效率会有所损耗
节省空间:共享库
更新方便:只需要重新编译修改的目标文件
# C/C++ 内存空间
- 栈:用于存储局部变量,由编译器自动管理分配与释放,效率很高,但是内存有限
- 堆:动态内存空间,由应用程序去控制。如若程序结束没有释放,则会由操作系统自动回收.
- 代码区:存放函数体二进制代码
- 全局 / 静态存储区:用于存储全局变量和静态变量。在以前 C 语言中,这部分分为初始化的
.data
和未初始化.bss
. 现在 C++ 中则没有这样的划分,区域内的变量会被默认初始化为 0. - 常量存储区:用于存储常量,不允许修改
# 堆与栈
- 栈内存是由编译器自动管理的,堆可由程序员控制,对于开辟的内存需要主动释放,否则容易产生内存泄漏
- 栈的内存增长方向是向着地址空间减小的方向,堆内存的增长方向是朝着地址空间增大的方向.
- 栈分配的内存都是连续空间,同时是一个先进后出的数据结构,其不存在内存碎片的问题。堆所分配的内存空间不一定连续,会存在内存碎片的问题
- 栈的分配效率要高于堆的分配效率。操作系统内有专门的寄存器用于存储栈的地址和栈顶指针地址。堆的内存分配是调用 C/C++ 的库函数,分配空间时还需要按照不同的算法去搜索足够大的空间进行分配。同时由于内存碎片问题,操作系统会进行内存紧缩的操作,需要额外的系统开销.
# 函数调用的过程
# 计算机内部数据的存储
整数:通过补码的形式进行存储(整数的补码是本省,负数的补码是取反 + 1)
float
的存储,遵循IEEE
规范,占用 32 位(4 字节).- 符号位(1bit)
- 指数位(8bit)
- 尾数部分(23bit)
double
的存储,遵循IEEE
规范,占用 64 位(8 字节).- 符号位(1bit)
- 指数位(11bit)
- 尾数部分(52bit)
# static
static
修饰局部变量:改变变量的存储位置(由栈区转移到静态存储区),延长了变量的生命周期;static
局部变量只在当前的作用域内有效,在作用外无法访问.static
修饰全局变量:只能在当前源文件内有效,在其他源文件内无法访问. (无static
修饰的全局变量可以在其他源文件中使用 extern 申明其他源文件中的全局变量,即可在当前源文件中使用)static
修饰函数:和static
用于修饰全局变量具有一样的特性static
修饰成员变量:静态成员变量,为类所以,可供所以对象共享,不占用实例的存储空间,同时其初始化必须定义外类外部static
修饰成员函数:静态成员函数,没有隐藏的this
指针,因此函数体内不能访问该类的非静态成员变量static
修饰的变量默认初始化为 0
# const
const
修饰的变量值不可变修改const
成员变量:不能在类外初始化,只能通过构造函数初始化列表进行初始化const
修饰的函数为常函数,不能改变类的成员变量(可以修改被mutable
修饰的变量)const
对象不能调用非const
成员函数(防止类的非常函数对const
对象的成员变量进行修改)
# mutable
mutable
与const
是相对的,表示一个变量的易变的;被mutable
修饰是成员变量可以再const
修饰的函数内被修改(也可以被常对象进行直接修改)
# explicit
用于修饰类的构造函数,防止其他对象隐式的转化为该类对象,只能显示的进行类型转化
# volatile
- 用于修饰变量,表示其值随时可能发送变化,编译器不会对访问该变量的代码进行优化,可以保证访问变量的稳定(从内存中读取)
volatile
指针- 多线程下的
volatile
:当多个线程都需要用到某一个变量时,应该用volatile
,防止编译器把变量从内存装入 CPU 寄存器中
# extern
- 用于变量的声明(不分配内存空间,不用赋予初值)
当我们需要再 A 文件中使用 B 文件的中的函数或者全局变量的时候,有两种做法:
- 通过
include
将 B 的头文件导入进 A 文件中,可能存在递归导入很多无关紧要的函数或变量,其会导致程序的编译(预编译)变慢- 在 A 文件中
extern
所需要使用的 B 文件的函数或变量,在makefile
时将他们的.o
文件放在一起做link
# C++ 三大特性
- 封装、继承、多态(封装和继承可以实现
代码的重用
,多态可以实现接口的重用
)封装:将数据和实现过程包裹起来,隐藏代码的实现细节,通过定义的接口来访问数据成员,使代码模块化. (直接体现面向对象,代码重用,权限控制)
public
:公有权限,类内可以访问,类外也可以访问protected
:保护权限,类内可以访问,类外不可以访问private
:私有权限,类内可以访问,类外不可以访问
继承:无需重新编写代码而可以直接使用现有类的所以功能,同时可以对现有类进行功能上的扩展,是一个从一般到特殊的过程.
- 权限继承:表示基类在子类中的最高权限(破坏继承:
friend
,using
) - 多继承:通过多继承可以得到更多类的数据和方法,实现更大程度的代码复用,当然也存在一些问题,如菱形继承.
- 权限继承:表示基类在子类中的最高权限(破坏继承:
多态:通过父类指针指向多个派生的子类对象时,调用父类的一个接口,可以实现多种不同的行为即为多态.
被
virtual
修饰的成员函数即为虚函数,如基类的虚函数为空即为纯虚函数,这个类即可为抽象类,不能被实例化,否则就是一个虚类,可被实例化.在编译阶段,虚类内会隐藏的存储一个虚函数表指针的成员变量,指向一张虚函数表(存储在全局 / 静态存储区)该类的所以对象共用这张虚函数表.
派生类继承虚类时,其会复制一份父类的虚函数表,类内的虚函数指针指向这张新的虚函数表,如派生类重写了虚函数则会将对应的虚函数表对应的指针项做一个修改,如果派生类定义了新的虚函数,则会在新的虚函数表后追加新的函数与地址的映射. (如若派生类继承了多个父类,即多继承,那么这个派生类可能存储多个虚函数表指针)
虚析构函数:基类中析构函数前使用
virtual
修饰。当基类指针指向派生类对象时,使用基类指针去释放空间时,则会去先调用派生类的析构函数,再去调用基类的析构函数;若不加virtual
则不会触发动态绑定(多态),只会调用基类的析构函数,导致内存泄漏. (虽然虚构函数名不一样,其实也是一种重写,系统内部对析构函数做了特殊处理,将析构函数名称都改成了destructor
)虚函数地址在运行时绑定(函数地址在运行阶段确定),使用了虚函数表的机制,所以在调用的时候会增加一次内存开销. (虚函数的缺点)
协变:基类和派生类虚函数的返回值不同(基类返回基类对象的指针或引用,派生类返回派生类对象的指针或引用),其也是一种多态
#include <iostream>
using namespace std;
class User {
public:
virtual User *get() { cout << "return user" << endl; return this; }
};
class Manager : public User {
public:
virtual Manager *get(){
cout << "return manager" << endl;
return this;
}
};
int main(){
Manager mg;
User* ptr = &mg;
ptr->get();
return 0;
}
# 虚函数与纯虚函数
纯虚函数是在基类中声明
# 菱形继承
# 为什么 C++ 默认析构函数不是虚函数
虚函数需要虚函数表和虚函数表指针,会占用内存空间。如果一个类没有子类,那么就没有必要将析构函数设置为虚函数
# 重载、重写、重定义
- 重载:多个同名函数,他们的参数个数或者参数类型不同;(编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就变成了不同的函数),也可以称之为静态多态,函数地址在编译器就已经确定了
- 重定义:继承中的同名隐藏,当派生类中有一个函数与基类的函数名相同,不管参数是否相同,只要该函数不为虚函数,发生了重定义.
- 重写:派生类中重写了基类的虚函数,其中函数名、参数列表和返回值都相同
# final 和 override
final
:修饰虚函数,表示该虚函数不能再被重写。修饰类表示该类不能被继承override
:检查派生类虚函数是否重写了基类的某个虚函数,如果没有则编译器报错;
# struct 与 class
- C++ 中的
struct
默认public
共有权限,class
默认private
私有权限 - C 语言中的
struct
只是一个变量的集合体,只可以用于封装数据,而不备面向对象的一些特性
# struct 与 union
# new 和 malloc 的区别
new
和 malloc
都是用于分配内存的,其中 new
是 C++ 中的操作符,可以被重载, malloc
是 C 语言中的库函数,不可以被重载
- 参数不同;new 可以自动计算所分配的对象的内存大小,同时返回值为指向该对象的指针. malloc 则需要传入需要分配的内存的字节数,返回一个
void *
指针 - new 分配失败会抛出异常
bac_alloc
,malloc 分配失败会返回NULL
- new 分配的内存在
free store
(自由存储区)上,malloc 分配的内存在堆上(其中自由存储区是 C++ 中的一个抽象的概念,new 的底层调用的逻辑是先调用operator new
分配内存,由系统决定或者用户重载operator new
决定,其次是调用对象的构造函数,初始化成员变量) malloc
分配的内存是虚拟内存,而new
分配的内存是物理内存。因为 new 调用了对象的构造函数,对对象成员进行了初始化(发生了缺页中断,使得虚拟内存映射到了物理内存)
# delete 与 free
delete
和 free
都是用于释放内存的,其中 delete
是 C++ 中的操作符,可以被重载,free 是 C 语言中的库函数,不可以被重载
- 参数不同;delete 需要给出释放的对象的类型指针,free 可以是
void *
类型的指针(delete 底层调用的逻辑是先调用对象的析构函数,再调用operator delete
释放对象所占内存)(free 只需要提供void *
就可以释放申请的所以内存:malloc
在分配内存时,不仅仅是分配了用户所需要的内存空间的大小,还会在该空间上部分配额外的一部分空间用于存储此次分配的内存的描述信息)
注意:
delete
和free
被调用后,指针也不会指向空,如果没有其他用途,需要把它设置为nullptr
,否则会出现野指针.
# free 回收的内存是立即返回给操作系统吗?
- 不是的。被
free
回收的内存会被ptmalloc
使用双链表保存起来,当用户下一次申请内存空间的时候,会尝试先从这些内存中寻找合适的返回. - 可以避免频繁的系统调用,提高程序效率.
ptmalloc
也会尝试对小块进行合并,避免过多的内存碎片
C++11 is the second major version of C++ and the most important update since C98. A large number of changes were introduced to both standardize existing practices and improve the abstractions available to the C programmers.
# new [] 与 delete []
new[]
会先调用operateor new[]
分配内存,然后再分配的内存的前几个字节写入数组的大小,然后进行 n 次构造函数
# malloc 与 free 实现原理
# C++ 中的几种 new
new
:plain new
,分配内存失败会抛出异常nothrow new
:分配内存失败不抛出异常,而是返回nullptr
placement new
:不会分配内存,在已有的内存空间上重新构造对象
# C++ 强制类型转换
static_cast<T>()
:不进行类型检查,不安全- 用于层次结构中基类与派生类间指针的转换
a. 上行转换(派生类指针转化为基类指针),安全
b. 下行转化(基类指针转化为派生类指针),不安全
- 基本数据类型的转化
- 将空指针转化为目标类型的指针
dynamic_cast
:专门用于派生类与基类之间的类型转化,基类中必须有虚函数;会进行类型的检查,是一种安全类型转化(转换失败,指针为nullptr
)- 运行时会进行类型的检查
- 不能用于内置基本数据类型的转化
- 如果转化成功的话会返回指向类的指针或引用,转换失败的话则会返回
NULL
reinterpret_cast
:转化过程仅仅是简单的比特位拷贝,不安全const_cast<T>()
:用于修改变量的const
或volatile
属性,变量类型与转换后的类型一致(只能修改底层const
)
# 指针与引用
- 存储的是一个地址;而引用只是变量的别名.
- 指针可不进行初始化,也可以指向空,并且指向可以改变;引用必须初始化,且只可绑定一个变量。这也导致了在使用指针时,往往需要判空操作,而引用是一种安全的指针,一定不为空(引用的底层是通过指针来实现).
sizeof
指针得到的是指针的大小,sizeof
引用得到的是引用绑定对象的大小- 指针可以是多级的,引用只有一级
# 常引用
保护传递给函数的数据在函数内不被改变.
const 类型& 引用名
在 C++ 中,临时对象都是
const
类型的,const 类型的对象转换为非 const 类型是非法的.string foo();
void bar(string &s);
bar(foo()); // 非法
var("hello world"); // 非法
# 野指针
- 野指针:指向已被释放的内存空间或者指向没有访问权限的内存空间
- 指针未被初始化、内存释放后未将指针设置为
nullptr
、指针超过了变量的作用范围(越界)都可能导致野指针的产生
# C++ 中的顶层 const 与底层 const
顶层const
:const
修饰的变量本身是一个常量,无法修改;(指的是指针,出现在*
的右边)底层const
:const
修饰的变量所指向的对象是一个常量(出现在*
左边)const int a = 100; // 顶层 const
auto other b = a; //other 类型为 int
/* --------------- */
int a = 100, b = 10;
const int *ptr = &a; // 底层 const, 常量指针,表示指针所指向的内容无法通过该指针进行修改,但是可以改变指针的指向
auto other1 = ptr; //other1 类型为 const int*
*other1 = 10; // error!
other1 = &b; // right!
decltype(ptr) other2 = ptr; //other2 类型为 const int *
*other2 = 10; // error!
other2 = &b; // right!
/* --------------- */
int a = 100, b = 10;
int *const ptr = &a; // 顶层 const,指针常量,表示指针是一个常量,不可修改指针的指向,但是可以通过指针去修改所指向空间的内容
auto other1 = ptr; //other1 类型为 int *
*other1 = 10; // right!
other1 = &b; // right!
decltype(ptr) other2 = ptr; //other2 的类型为 int* const
*other2 = 10; // right!
other2 = &b; // error!
# 常量指针与指针常量
- 常量指针:
int const* p = a
,指针的指向可以改变,而不能通过该指针去改变所指向的内容 - 指针常量:
int *const p = a
,指针的指向不可改变,可以通过指针去改变指向的内容.
# 数组指针与指针数组
数组指针:
int (*p)[n]
,指向一个整形的一维数组,这个数组的长度是 n,在执行p+1
操作时,p
要跨越 n 个整形数据长度指针数组:
int *p[n]
,[]
优先级高于*
,是一个具有 n 个指针类型的数组.int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
//a 理解成一个一维数组,元素为 a [0], a [1], a [2], 其中 a [0] 的类型为 (int *), a 与 & amp;a [0] 等价,类型为 int (*p)[4]
# 函数指针
函数指针的类型是由其返回类型和参数列表共同决定的
int (*pf)(const int&, const int&); //pf 为一个函数指针
// 区别于 int *pf (const int&, const int&); pf 为一个函数声明,函数返回值为 int
函数指针的赋值
指针名=函数名
指针名=&函数名
# 宏定义与 typedef
- 宏定义主要用于定义常量或者一些书写复杂的内容,
typedef
主要用于定义类型的别名 - 宏定义在预处理阶段完成,只会进行文本的替换,不进行类型的检查;
typedef
在编译阶段完成,会进行类型的检查
# const 与 define
# inline 内联函数
- 把 inline 函数体复制到函数调用处(避免了函数调用所需要的压栈和出栈操作,提高了程序运行效率)
- 特性类似于宏定义,但 inline 会做类型的检查
- 不能包含循环、递归复杂操作
inline
函数有多个返回点,会使用goto
跳转- 在类中定义的成员函数,除了虚函数,往往会自动转化为内联函数
- 缺点:
- 代码膨胀,占用代码区的内存空间
- 是否内敛由编译器决定,不可控.
# this 指针
- this 是一个隐含于每一个非静态成员函数中的特殊指针,指向被实例化的对象。当调用对象成员函数时,会将对象的地址赋值给
this
指针,并会隐式的将this
指针传入 tihs
为一个右值,并由const
修饰
# 初始化列表(C++11)
- 通过花括号来进行初始化。直接在变量名后面跟上初始化列表来进行对象的初始化
# 类成员函数初始化方法
赋值初始化
:在函数体内进行赋值. (其是在所有成员被分配内存之后才进行的,此时会触发成员函数的默认构造函数,同时进入函数体赋值往往会产生临时对象,和拷贝赋值函数)列表初始化
:在冒号后使用初始化列表进行初始化.(给数据成员分配内存时进行的,在函数体执行之前)对于基本数据类型而言,两种在速度方面没有太大的差别,但对于复杂一些的数据类型,列表初始化速度会快于赋值初始化
什么情况下必须用初始化列表?
- 初始化一个引用成员变量时
- 初始化一个常量成员时
- 当父类没有默认构造函数的时
- 当成员变量没有默认构造函数时
# auto(C++11)
auto
可以自动推导变量类型;使用auto
声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译阶段将auto
占位符替换成其真正的类型.对于有
const
和volatile
修饰的变量,auto
只能推断底层const
或volatile
,顶层const
或volatile
会被忽略,需要自己添加不能在函数的参数中使用,不能用于定义数组,不能用于类的非静态成员的初始化(只能用于类的静态常量成员变量的初始化)
不能用于模版参数的类型推导
类的成员变量并不属于类,而属于具体的实例。如果没有创建实例,那么就没有办法进行自动类型推导.
# decltype(C++11)
我们希望从表达式(函数返回值)中推断出想要定义的变量的类型,但是却不想用表达式的值去初始化变量,这种情况
auto
显得无力了
推断表达式类型作为变量的定义类型
推断函数返回值(实际不会调用,仅推断),推导出的对象类型与函数返回值一致
当函数返回的是一个纯右值,需要忽略掉前面的
const
或volatile
无论是底层
const
或volatile
还是顶层const
或volatile
都会被保留表达式是一个左值,或者被
()
包围,使用decltype
推导出来是表达式类型的引用(如果有const
或volatile
则需要加上)int a = 10;
decltype(a) b = a; //b 的类型为 int
decltype((a)) c = a; //c 的类型为 int&,绑定 a 对象
# 范围 for 循环(C++11)
# NULL
与 nullptr
NULL
来自 C 语言,由宏定义实现,nullptr
是 C++11 新增的关键字在 C 语言中
NULL
被定义成(void *)0
,在 C++ 中则被定义成0
,故引入nullptr
替代了NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
当有一个函数被重载时,且参数都是指针类型时,调用函数时需要明确强制转换成对应类型,否则编译器无法确定需要调用哪一个函数
#include <iostream>
using namespace std;
void fun(char* p){
cout<< "char* p" <<endl;
}
void fun(int* p){
cout<< "int* p" <<endl;
}
void fun(int p){
cout<< "int p" <<endl;
}
int main(){
fun((char*)nullptr);// 语句 1
fun(nullptr);// 语句 2
fun(NULL);// 语句 3
return 0;
}
// 运行结果:
// 语句 1:char* p
// 语句 2: 报错,有多个匹配
//3:int p
# lambda 表达式
- 匿名函数。一个
lambda
表达式具有一个返回值、一个参数列表和一个函数体。与函数不同的是,lambda
表达式可以定义在函数体内部,格式为:[capture list](parameter list) opt ->return type {function body}
,不能用默认参数. capture list
:捕获列表[]
:不捕获任何变量[&]
:捕获外部作用域中的所有变量,并作为引用在函数体内使用(按引用捕获)[=]
:捕获外部作用域中的所有变量,并作为副本在函数体内使用(按值捕获),拷贝的副本在匿名函数体内部是只读的,不可改变.[=, &foo]
:按值捕获外部作用域内的所以变量,按引用捕获变量foo
[this]
:捕获当前实例的this
指针
(parameter list)
:参数列表,和普通函数的参数列表一样,如果没有参数列表可以省略不写opt
选项,不需要可以省略mutable
:可以修改按值拷贝进来的副本(注意修改的是副本)exception
:指定函数抛出的异常
return type
:一般情况下,不指定lambda
表达式的返回值,编译器会根据return
语句自动推导返回值类型,但是初始化列表不能用于返回值的自动推导
# 左值引用与右值引用
- 左值引用:对左值的引用(可以出现在等式的左边,也可以出现在等式的右边,是具名的,同时可以取地址,
const左值引用
可以引用右值); 避免对象的拷贝,在一定程度上让程序脱离了危险的指针 - 右值引用:右值(只能出现在等式的右边,不能取地址,纯右值 [字面值,返回的非引用的函数调用,后置自增 / 减,算术表达式,逻辑表达式,比较表达式], 将亡值 [C++11 引入,会触发移动构造或者移动赋值,并进行资源转移])往往是没有名称,在实际开发中我们可能需要对右值进行修改,需要借助右值引用(
类型&& 变量名=右值
)- 移动语义:将一个临时对象(将亡值)的资源转移到另外一个对象中去,可以减少不必要的资源的销毁和开辟,提高运行效率
move()
:将左值强制转化为右值引用,通过右值引用来使用,实现移动语义.- 完美转发
# 大小端存储
- 大端存储:高位存储在低字节中
- 小端存储:低位存储在低字节中
在
Socket网络编程
中,有主机字节序和网络字节序
# 内存对齐
理论上计算机对于任何变量的访问都可以从任务位置开始,然而实际上系统会对这些变量的存放做一些限制,通常将某个变量的地址设置为某个数 N
的整数倍。这就是内存对齐.
内存是以字节为基本单位,但是对于处理器往往是按字节块来存取数据。进行内存对齐,主要是为了加快内存的存取速度.
# 内存池
- 预先申请分配一定数量的内存块留作备用,当有新的内存需求时,就从内存池中分出一部分内存块,对于使用完的内存块将其放回内存池。若内存池不够,在继续申请新的内存. (防止频繁的内存申请与释放所带来的开销,提高分配效率,同时可以避免内存碎片)
# 类指针的管理
class My_String{ | |
public: | |
My_String(){ | |
data_ptr = nullptr; | |
length = 0; | |
} | |
My_String(string other): length(0), data_ptr(nullptr){ | |
this->length = other.size(); | |
if(this->data_ptr != nullptr) delete[] this->data_ptr; | |
this->data_ptr = new char[this->length]; | |
memcpy(this->data_ptr, other.c_str(), this->length); | |
} | |
My_String(int n){ | |
this->data_ptr = new char[n]; | |
this->length = n; | |
} | |
// 拷贝构造 | |
My_String(const My_String& other){ | |
this->length = other.length; | |
this->data_ptr = new char[this->length]; | |
memcpy(this->data_ptr, other.data_ptr, this->length); | |
} | |
// 拷贝赋值运算符重载 | |
My_String& operator=(const My_String& other){ | |
if(this != &other){ | |
if(this->data_ptr != nullptr) delete[] this->data_ptr; | |
this->length = other.length; | |
this->data_ptr = new char[this->length]; | |
memcpy(this->data_ptr, other.data_ptr, this->length); | |
} | |
return *this; | |
} | |
// 移动构造 | |
My_String(My_String&& other){ | |
printf("移动构造\n"); | |
this->data_ptr = other.data_ptr; | |
this->length = other.length; | |
memcpy(this->data_ptr, other.data_ptr, this->length); | |
other.data_ptr = nullptr; | |
other.length = 0; | |
} | |
// 移动赋值运算符重载 | |
My_String& operator=(My_String&& other){ | |
if(this->data_ptr != nullptr) delete[] this->data_ptr; | |
this->length = other.length; | |
this->data_ptr = other.data_ptr; | |
other.data_ptr = nullptr; | |
other.length = 0; | |
return *this; | |
} | |
void out_print(){ | |
for(int i = 0; i < this->length; i ++ ) cout << this->data_ptr[i]; | |
} | |
private: | |
char *data_ptr; | |
int length; | |
}; |
# shared_ptr
是线程安全的吗?
shared_ptr
通过引用计数去管理对象,其是线程安全的,但shared_ptr
所指向的内存空间,即自身并不是线程安全的. (即当多个线程同时去访问shared_ptr
所指向的空间时,可能会存在问题)
# STL 六大组件
- 容器、算法、迭代器、适配器、仿函数、空间分配器
# 为什么 stack 的 pop () 和 top () 要分离
- 通过
pop()
来弹出并返回栈顶值这种方式不安全,可能导致原始数据丢失;pop () 在函数返回时,会发生对象的拷贝,如果弹出对象比较大,同时堆内内存比较紧张,可能无法分配住够的内存会抛出异常,无法正确的返回栈顶元素,但是此时栈顶元素已经弹出. - 通过引用或者指针可以解决这个问题
# map、set、multimap、multiset(关联容器)
- 底层实现是
红黑树
set
和multiset
会对元素进行排序,set
不存储重复元素,multiset
可以存储重复元素map
和multimap
存储的元素为 k-v 键值对,会更加 key 进行排序,map
中不允许重复 key,multimap
可以重复 key
# unordered_set 与 set
unordered_set
内部无序,通过哈希来实现,对于非标准类型需要提供判等函数,插入,查询,删除的时间复杂度都是O(1)
,但是不稳定,当数据量大冲突大时,时间复杂度最坏退化为O(n)
;set
内部有序,通过红黑树来实现,对于非标准类型需要提供比较函数,插入,查询,删除的时间复杂度比较稳定log2(n)
.
# 哈希函数
- 平方取中发:去关键字的平方值的中间几位作为哈希地址
# 哈希冲突
- 开放地址法(再散列):线性探测,再平方探测、伪随机探测
- 拉链法
- 再哈希
# vector 的扩容机制
vector
是一个动态数组,当插入元素使,若capacity
和size
相等,则会发生扩容;vector
扩容并不是在原有空间进行扩充,而是在堆内申请一块更大空间的区域,将原来的数据复制过去,同时释放原有空间。至于这个扩容系数,得看具体的实现,得看取舍(内存和效率的取舍),GCC
下的扩容系数是 2
# vector(clear(), swap(), shrink_to_fit(),resize(),reserve())
clear()
:清空内容,不释放内存(capacity 不变,size 变成 0)swap()
:清空内容,释放内存 (capacity,size 均变成 0)shrink_to_fit()
:可能会释放内存,使 capacity 与 size 适配(capacity 与 size 相等)resize(n)
:会改变vector
的 sizereserve()
:不会改变 size,但是可能会改变 capacity. 主要目的是为了优化性能,避免在添加元素时频繁进行内存分配
# vector 与 list
vector
的内存空间是连续的,可以在o(1)
内实现随机存取,但是需要再内部进行插入和删除时,需要O(n)
.list
是双向链表,内存空间不一定连续,往往是离散的,O(n)
完成随机存取,但是可以O(1)
实现删除和插入操作
# clear 的时间复杂度
- 无论是顺序容器还是关联容器在使用
clear()
时,时间复杂度都是O(n)
;因为执行clear()
需要调用元素的析构函数,这个析构函数会逐个进行。不过当存储的数据类型是基本数据类型的时候,不需要虚构,系统可能会做一些优化,vector
容器可以使得复杂度降为常数级.
# C 智能指针(C11)
智能指针是一个类,用来存储指向动态内存空间的对象指针,负责自动释放动态内存,防止堆内存泄漏.
auto_ptr
:C98 引入,由于其不够安全,被unique_ptr
取代,C11 废弃;unique_ptr
:与auto_ptr
一样采用独占所有权模式,同一时间只能有一个指针可以指向某个对象,但是unique_ptr
禁止了拷贝操作,unique_ptr
采用了移动赋值move()
函数来进行控制权转移.shared_ptr
:共享所有权的一个智能指针。允许多个指针指针指向同一个对象,并使用引用计数来管理指向对象的指针(成员函数use_count()
可获得引用计数),该对象和相关资源会在最后一个引用被销毁时释放shared_ptr
:内部的引用计数是线程安全的,但是对象的读取需要加锁.shared_ptr
循环计数问题:创建了两个shared_ptr
分别指向两个对象,而这两个对象内的一个共享指针分别又指向了对方,造成了循环计数,使得两个对象的空间都无法被释放.struct ListNode
{
int _data;
shared_ptr<ListNode> ptr;
ListNode(int data):_data(data){}
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode(1));
shared_ptr<ListNode> node2(new ListNode(2));
cout << node1.use_count() << endl; // 1
cout << node2.use_count() << endl; // 1
node1->ptr = node2;
node2->ptr = node1;
cout << node1.use_count() << endl; // 2
cout << node2.use_count() << endl; // 2
return 0;
}
// 常用的解决方案是讲成员函数内的 shared_pre 改成弱指针 weak_ptr
weak_ptr
:一种不控制对象生命周期的智能指针,不会影响share_ptr
的引用计数,只是提供一种访问其管理对象的方式
# shared_ptr
的实现
template<typename T> | |
class SmartPtr{ | |
public: | |
// 显式构造函数 | |
explicit SmartPtr(T* ptr): m_data(ptr), m_count(ptr ? new int(1) : nullptr){} | |
// 拷贝构造 | |
SmartPtr(const SmartPtr& other): m_data(other.m_data), m_count(other.m_count){ | |
if(m_count) (*m_count) ++ ; | |
} | |
// 拷贝赋值操作符 | |
SmartPtr& operator=(const SmartPtr& other){ | |
if(this == other) return *this; | |
release(); | |
m_data = other.m_data; | |
m_count = other.m_count; | |
if(m_count) (*m_count) ++ ; | |
return *this; | |
} | |
// 移动构造 | |
SmartPtr(SmartPtr&& other){ | |
m_data = other.m_data; | |
m_count = other.m_count; | |
if(m_count){ | |
other.m_count = nullptr; | |
other.m_data = nullptr; | |
} | |
} | |
// 移动赋值运算符 | |
SmartPtr& operator=(SmartPtr&& other){ | |
if(m_data) release(); | |
m_data = other.m_data; | |
m_count = other.m_count; | |
other.m_data = nullptr; | |
other.m_count = nullptr; | |
return *this; | |
} | |
// 析构函数 | |
~SmartPtr(){ | |
release(); | |
} | |
T& operator*() const { return *m_data; } | |
T* operator->() const { return m_data; } | |
// 获取引用计数 | |
int use_count(){ | |
return m_count ? (*m_count) : 0; | |
} | |
// 获取裸指针 | |
T* get(){ | |
return m_data; | |
} | |
private: | |
T* m_data; | |
int* m_count; | |
void release(){ | |
if(m_count && --(*m_count) == 0){ | |
delete m_data; | |
delete m_count; | |
m_data = nullptr; | |
m_count = nullptr; | |
} | |
} | |
}; |
# 内存泄漏
- 堆内存泄漏(
Heap leak
) - 在释放对象数组时没有使用
delete []
而使用delete
- 没有将基类的析构函数定义为虚函数
- 缺少拷贝构造函数和拷贝赋值函数
Linux 下内存泄漏检查工具:Valgrind
# 深拷贝与浅拷贝
- 浅拷贝:C++ 默认的方式(如果程序员不主动编写拷贝构造函数和赋值构造,编译器将以浅拷贝的方式生成缺省的函数),简单的将成员函数值进行复制. (当成员变量出现指针时,则会导致多个指针指向同一片内存空间,新旧对象共享内存,当其中一个对象释放内存,再调释放其他对象时就会出现问题,同一片内存空间被释放了多次)
- 深拷贝:必须显示的提供拷贝构造函数和赋值构造,新旧对象不共享内存
# C++ 类默认生成的函数
- 无参构造函数、析构函数、拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数、重载取地址符,均是
public
# 对象复用与零拷贝
# STL 内存优化
# const
当以编译初始化的方式定义了一个
const
对象时,例如const int bufsize=512
,编译器将在编译过程中把用到该变量的地方都替换成对应的值。const
对象被设定为仅在文件内有效,当多个文件中出现了同名的const
变量时,其实等同于在不同文件下分别定义了独立的变量;当某一个const
对象需要再多个文件间共享时,我们需要对于const
变量不管是声明还是定义都添加extern
关键字。
# 对常量的引用
double dval = 3.14; | |
const int &r = dval; |
等价于
const int temp = dval; | |
const int &r = temp; |
系统在内部会将对常数的引用绑定在一个临时对象上,实际并未绑定 dval
;通过这种设计可以防止非法的操作。
# 顺序容器
vector
:可变长数组。支持快速随机访问。在尾部之外的其他位置插入或者删除元素可能比较慢deque
:双端队列。支持快速随机访问。在头部或者尾部插入、删除元素速度很快list
:双向列表。forward_list
:单向列表。array
:固定大小数组。支持快速随机访问。不能添加或者删除元素。string
:与vector
类似,但专门用于存储字符
# 指针空值类型
C 与 C 内定义的 NULL
含义不同;C 内定义: #define NULL 0
, 而 C 内的定义: #define NULL ((void *)0)
.C 与 C 都是强类型语言,从 C 到 C 语言的类型定义变的更加严格,C++ 内不允许 void( *)
类型的变量隐式转化为其他类型。
// 宏定义 | |
#ifndef NULL | |
#ifdef __cplusplus | |
#define NULL 0 | |
#else | |
#define NULL ((void *)0) | |
#endif | |
#endif |
nullptr
无法隐式的转化为整形,但是可以隐式匹配指针类型
# 使用结构化绑定来解包绑定的返回值
结构化绑定是 C++17 新特性,其可以结合语法糖自动推导类型,并可以从组对、元祖和结构体中提取单独的变量(解包)
使用结构化绑定是为了能过更加简单的为绑定的多个变量进行赋值
// 对 pair 的结构化绑定
std::pair<int, int> divide_remainder(int dividend, int divisor);
auto [a, b] = divide_remainder(16, 3);
// 对 tuple 进行结构化绑定
std::tuple<std::string, std::chrono::system_clock::time_point, unsigned>
const auto [name, valid_time, price] = stock_info("INTC");
// 对自定义结构体进行结构化绑定
struct employee{
unsigned id;
string name;
string role;
unsigned salary;
};
std::vector<employee> employees;
/* 注意:在适当时候使用引用,尽量减少不必要的复制 */
for (const auto &[id, name, role, salary] : employees){
cout << "Name: " << name
<< "Role: " << role
<< "Salary: " << salary << '\n';
}
STL
中的基础数据结构都能通过结构化绑定来直接进行访问map<string, size_t> animal_population;
for (const auto &[species, count] : animal_population) {
cout << "There are " << count << " " << species
<< " on this planet.\n";
}
Node:与 C 的语法特征不同,将复杂结构体作为返回值传回会耗费大量的时间,因为对象需要在返回函数中进行初始化,之后将这个对象拷贝到相应容器中返回给调用端。现代编译器支持返回值优化 (RVO, return value optimization) 技术,这项技术可以省略中间副本的拷贝。
# 带初始化的 if 和 switch
# 括号初始化
C++11
引入了新的括号初始化语法 {}
,其不仅允许集合式的初始化,而且还是对常规构造函数的调用。遗憾的是,当与 auto
类型变量结合使用时,这种方式很容易出现错误, c++17
增强了这一系列初始化规则。
# 参考博文
- [1] C++ 菱形继承问题和虚继承分析 - CSDN 博客