C++面试基础知识汇总

C++面试基础知识汇总

  • 思维导图
  • 一、C++基础与预处理
    • 1.C++源文件从文本到可执行文件经历的过程
    • 2.++i和i++的区别
    • 3.有符号变量与无符号变量unsigned
    • 4.不使用中间变量进行a,b值进行交换
    • 5.双冒号,using,namespace
    • 6.include<>和“ ”的区别
    • 7.C++main函数执行完后执行的操作?---atexit()函数
    • 8.extern "C"的用法---正确实现C++代码调用其他C语言代码
    • 9.sizeof的用法
    • 10. #pragma pack的作用
    • 11. 内联函数inline
    • 12. define 和 const 和static
    • 13.const关键字的使用(种类)
    • 14.const关键字的作用(4个)
    • 15.堆和栈的区别
  • 二、指针和引用
    • 1.引用的种类及其常见错误
    • 2. 复杂指针的声明
    • 3. 指针常量与常量指针
    • 4. 什么是野指针
    • 5. new(delete)与malloc(free)的区别
    • 6. 指针和引用的区别
  • 三、字符串
    • 1. 数字转字符串
    • 2. 字符串转数字
    • 3. 编程实现strcpy-字符串复制
    • 4. 实现计算字符串的长度
    • 5. 编程实现字符串每个单词翻转
    • 6. 编程实现strcmp-两个字符串比较大小
  • 四、C++面对对象基础
    • 1.面对对象与面对过程的区别
    • 2. struct 和 class 的区别?
    • 3.相比全局变量,静态变量的优势
    • 4.什么情况下只能用初始化列表?
    • 5.静态成员函数的使用
    • 6.C++默认空类会产生哪些类成员函数
    • 7.构造函数与析构函数
    • 8.析构函数、构造函数与虚函数的关系
    • 9.重载和覆写(重写)的区别
  • 五、继承和多态
    • 1.类的三种继承方式
    • 2.继承时的构造函数初始化问题
    • 3.多态的定义
    • 4.虚函数的实现方式
    • 5.为什么要引入抽象虚类和纯虚函数?
    • 6.虚函数与纯函数的区别
  • 六、STL模板库
    • 1.什么是STL模板库
    • 2.STL容器分类
      • 序列式容器(Sequence containers)
      • 关联式容器(Associative containers)
      • 适配容器
      • 各容器对比
    • 3. 频繁对vector调用push_back()对性能的影响和原因?
    • 4. 只在堆上或者栈上创建对象
    • 5.vector和list的区别
    • 6.vector和deque的区别
    • 7.map和hash map的区别?
    • 8.数据结构:hash_map原理
    • 9. STL map和set的使用的一些问题?
    • 10. 什么是STL算法?
    • 11. STL内存优化问题
    • 12.C++内存管理
  • 七、C++11新特性
    • 1.语法糖
    • 2.右值引用和move语义
    • 3.智能指针

思维导图

C++面试基础知识汇总_第1张图片

一、C++基础与预处理

1.C++源文件从文本到可执行文件经历的过程

预编译阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定
汇编代码,生成汇编文件.

汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件.
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件.

2.++i和i++的区别

++i表示先自增,再打印自增后的值
i++表示先打印原始值,再将i加1
-i++表示先打印-i的值,再将i加1

3.有符号变量与无符号变量unsigned

表达式中存在有符号和无符号共存时,需要将有符号的转为无符号进行运算。
因此当有符号为负数时,会变成一个很大的无符号数。
例如
`7+(-7),其和刚好溢出,其值为0;

7+(-8),差1就可以溢出,因此该和的值为一个很大的数`

4.不使用中间变量进行a,b值进行交换

1.temp法就不讲了
2. 加减法

a=a+b;
b=a-b;
a=a-b;

3.异或^(相同为0,不同为1)

a=a^b;
b=b^a;
a=a^b;

5.双冒号,using,namespace

**命名空间(namespace)**是C++语言特别重要的特性,当第三方供应商提供的库时,为了避免与其他供应商或者用户定义的名字相冲突(命名空间污染),常常将库的内容放置在自己独立的命名空间中。C++标准库也定义了相应命名空间std,用户在使用标准库时必须通过作用域运算符(::),或者使用using关键词来简化命名空间中名字的使用。

**:*是C++里的“作用域分解运算符”。比如声明了一个类A,类A里声明了一个成员函数voidf(),
但没有在类的声明里给出f的定义,那么在类外定义f时,就要写成voidA::f(),表示这个f()函数是类A的成员函数。
  :: 一般还有一种用法,就是直接用在全局函数前,表示是全局函数。当类的成员函数跟类外的一个全局函数同名时,
提示在类内定义的时候,打此函数名默认调用的是本身的成员函数;如果要调用同名的全局函数时,就必须打上::以示区别。
比如在VC里,你可以在调用API函数时,在API函数名前加::。

6.include<>和“ ”的区别

<>表明这是一个标准头文件,查找过程会首先检查预定义的目录,
“”表示该文件是用户提供的头文件,查找时从当前文件目录查找,再从标准位置查找。

7.C++main函数执行完后执行的操作?—atexit()函数

使用atexit()函数来注册程序正常终止是要被调用的函数,并且在main结束之后,调用这些函数的顺序与注册时相反。

8.extern "C"的用法—正确实现C++代码调用其他C语言代码

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;

而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。比如说你用C 开发了一个DLL 库,为了能够让C ++语言也能够调用你的DLL输出(Export)的函数,你需要用extern "C"来强制编译器不要修改你的函数名。

9.sizeof的用法

Sizeof(…)是运算符,在头文件中typedef为unsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。
strlen(…)是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。它的功能是:返回字符串的长度。
eg1、char arr[10] = “What?”;
int len_one = strlen(arr);
int len_two = sizeof(arr);
cout << len_one << " and " << len_two << endl;
输出结果为:5 and 10
计算普通变量的大小
在32为操作系统的环境下,
1.整形变量的大小4字节
2.字符串大小为字符数+1
3.指针大小为4字节,无论哪种类型的指针
计算类对象的大小
对结构体或类的size进行运算时,不是简单的内存数相加,而是需要考虑字节对齐问题.如书面试秘籍上p30.
计算含虚函数的类对象的大小
包含虚函数,就是多了一个指向虚表的指针,
对继承的类,需要包含父类的大小.
虚拟继承的类对象的空间
若类为空类:编译器会安排一个char给空类。大小为1.
多重继承空类,其大小认为1.
类b虚继承类a,编译器为该类安排一个指向父类的指针,因此类的大小为4.
若b虚继承a c 两个类,则有两个指向父类的指针,因此该类的大小为8.
类的静态成员变量存储在静态存储区,因此在该类的存储空间中不包括静态成员变量.
sizeof与strlen
strlen用于计算字符串的长度.

10. #pragma pack的作用

设置步长(内存对齐大小).

11. 内联函数inline

为什么需要内联函数以及其作用
1.替代宏定义,解决函数调用的效率问题;
2. 宏定义不能进行参数的有效性检测;
3. 一般被应用于类中对私有成员变量的存取函数get、set;
4. 内联是以_代码复制为代价,省去了函数调用的开销,因此会消耗更多的内存空间._
与宏定义的区别?
1.宏定义在预编译时展开,内联函数在编译时展开;
2.编译时,内联函数可以直接镶嵌到代码中,而宏定只是简单的文本替换;
3.内联函数可以完成类型检测,语句是否正确等编译功能;
4.宏定义容易出现二义性,必须将参数用括号括起来.

12. define 和 const 和static

define和const区别?
1.起作用的阶段: #define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
2.作用的方式:const常量有数据类型,而宏常量没有数据类型,只是简单的字符串替换。编译器可以对前者进行类型安全检查。而对后者没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。
3.存储的方式:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份,const比较节省空间,避免不必要的内存分配,提高效率。

const和static的用法?
1.static:
修饰全局变量
存储在静态存储区;未经初始化的全局静态变量自动初始化为 0;作用域为整个文件之内。
修饰局部变量
存储在静态存储;未经初始化的局部静态变量会被初始化为0;作用域为局部作用域,但离开作用域不被销毁。
修饰静态函数
静态函数只能在声明的文件中可见,不能被其他文件引用
修饰类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享,静态成员是类的所有对象中共享的成员,而不属于某一个对象_类中的静态成员必须进行显示的初始化._
修饰类的静态函数
静态函数同类的静态成员变量一个用法,都是属于一个类的方法。
非静态成员函数有 this 指针,而静态成员函数没有 this 指针。
静态成员函数中不能引用非静态成员;类的非静态成员函数可以调用用静态成员函数,但反之不能。
2.const:
修成类成员
在C++中,const成员变量也_不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数_; const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。
修饰类函数
该函数中所有变量均不可改变。

13.const关键字的使用(种类)

1.修饰变量
例如:

const int NUM = 10; //与int const NUM等价
NUM = 9;  //编译错误,不可再次修改.

2.修饰数组
例如使用const关键字修饰数组,使其元素不允许被改变:

const int arr[] = {0,0,2,3,4}; //与int const arr[]等价
arr[2] = 1; //编译错误

3.修饰指针
1).const 修饰 *p,指向的对象只读,指针的指向可变:—指针常量

int a = 9;
int b = 10;
const int *p = &a;//p是一个指向int类型的const值,与int const *p等价
*p = 11;    //编译错误,指向的对象是只读的,不可通过p进行改变
p = &b;     //合法,改变了p的指向

这里为了便于理解,可认为const修饰的是p,通常使用对指针进行解引用来访问对象,因而,该对象是只读的。
2).const修饰p,指向的对象可变,指针的指向不可变:—常量指针

int a = 9;
int b = 10;
int * const p = &a;//p是一个const指针
*p = 11;    //合法,
p = &b;     //编译错误,p是一个const指针,只读,不可变

3).指针不可改变指向,指向的内容也不可变

int a = 9;
int b = 10;
const int * const p = &a;//p既是一个const指针,同时也指向了int类型的const值
*p = 11;    //编译错误,指向的对象是只读的,不可通过p进行改变
p = &b;     //编译错误,p是一个const指针,只读,不可变

14.const关键字的作用(4个)

1.定义常量
const定义的常量,编译器可对其进行静态数据类型安全性检查。
2.修饰函数形式参数
当函数的传入参数为自定义数据类型时,_将值传递改为引用传递可以提高效率,但是引用传递有可能改变传入的数据类型的值,因此需要加const._并且函数体内不能被修改
例:

void fun(A const &a)

3.修饰函数的返回值
返回的参数必须由const类型的同类变量进行接收
例:

const char * getchar();
const char *ch=getchar()

4.修饰类的成员函数
任何不需要修改成员变量的函数都用const修饰,这样如果在其中对变量进行修改或者调用非const类型的函数,编译器就会报错。
例:

int gechar() const;

15.堆和栈的区别

1)堆栈空间分配区别:
栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
2)堆栈的缓存方式区别
栈:是内存中存储值类型的,大小为2M(window,linux下默认为8M,可以更改),超出则会报错,内存溢出
堆:内存中,存储的是引用数据类型,引用数据类型无法确定大小,堆实际上是一个在内存中使用到内存中零散空间的链表结构的存储空间,堆的大小由引用类型的大小直接决定,引用类型的大小的变化直接影响到堆的变化
3)堆栈数据结构上的区别
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。

二、指针和引用

1.引用的种类及其常见错误

1.一般变量的引用

int a=0;
int &rn=a;
rn++;  //a=1

注意:
1).引用没有开辟新的地址空间,因此&rn=&a;指向同一地址
2).引用必须初始化,不然会编译错误
3).引用只能在声明的时候进行初始化,并不能改变引用为其他变量的引用

2.指针变量的引用

int a=1;
int *p=&a;//p指针指向a
int * &pa=p;//pa为指针p的引用,指向a

3.参数引用-返回值

全局变量 float f

1).

float f1();
	return f

返回值需先建立临时变量,再将临时变量返回,因此在接收该返回值时不能用引用来接收。
2).

float& f1();
	return f

返回值不建立临时变量,直接返回全局变量f,因此接收值可以定义为引用,但是这里要注意变量的有效期。
4.传入参数的引用
1).若使用传入值的常量引用
如 void test(const int & a)
则在函数体内不能修改其值
2).若变量a被定义为常量
如 const int a=10;
则定义其引用只能使用

const int & b=a;
int &b=a;是错误的

并且常量引用的值不能再被修改!

2. 复杂指针的声明

1.指针数组
(1)指针数组:它实际上是一个数组,数组的每个元素存放的是一个指针类型的元素。

int* arr[8];

//优先级问题:[]的优先级比
//说明arr是一个数组,而int
是数组里面的内容
//这句话的意思就是:arr是一个含有8和int*的数组
(2)数组指针:它实际上是一个指针,该指针指向一个数组。

int (*arr)[8];

//由于[]的优先级比高,因此在写数组指针的时候必须将arr用括号括起来
//arr先和*结合,说明p是一个指针变量
//这句话的意思就是:指针arr指向一个大小为8个整型的数组。

2.数组指针
1).赋值
同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
2).存储方式
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,多维数组在内存中是按照一维数组存储的,只是在逻辑上是多维的。
数组的存储空间,不是在静态区就是在栈上。

指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
指针:由于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。

3).求sizeof
数组:
数组所占存储空间的内存:sizeof(数组名)
数组的大小:sizeof(数组名)/sizeof(数据类型)

指针:
在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。

4).初始化
数组:
(1)char a[]={“Hello”};//按字符串初始化,大小为6.
(2)char b[]={‘H’,‘e’,‘l’,‘l’};//按字符初始化(错误,输出时将会乱码,没有结束符)
(3)char c[]={‘H’,‘e’,‘l’,‘l’,‘o’,’\0’};//按字符初始化
这里补充一个大家的误区,就是关于数组的创建和销毁,尤其是多维数组的创建与销毁。
(1)一维数组:

int* arr = new int[n];//创建一维数组
delete[] arr;//销毁

(2)二维数组:

int** arr = new int*[row];//这样相当于创建了数组有多少行
for(int i=0;i<row;i++)
{
	arr[i] = new int[col];//到这里才算创建好了
}
//释放
for(int i=0;i<row;i++)
{
	delete[] arr[i];
}
delete[] arr;

指针:
//(1)指向对象的指针:(()里面的值是初始化值)
int *p=new int(0) ; delete p;
//(2)指向数组的指针:(n表示数组的大小,值不必再编译时确定,可以在运行时确定)
int *p=new int[n]; delete[] p;
//(3)指向类的指针:(若构造函数有参数,则new Class后面有参数,否则调用默认构造函数,delete调用析构函数)
Class *p=new Class; delete p;
//(4)指针的指针:(二级指针)
int *pp=new (int)[1];
pp[0]=new int[6];
delete[] pp[0];

3.函数指针
1).函数指针

void test()
{
printf("hehe\n");
}

//pfun能存放test函数的地址
void (* pfun)();
函数指针的形式:类型(*)( ),例如:int (*p)( ).它可以存放函数的地址,在平时的应用中也很常见。
2).函数指针数组
形式:例如int (p[10])( );
因为p先和[ ]结合,说明p是数组,数组的内容是一个int (
)( )类型的指针
函数指针数组在转换表中应用广泛
3).函数指针数组的指针
指向函数指针数组的一个指针,也就是说,指针指向一个数组,数组的元素都是函数指针

void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[10])(const char*) = &pfunArr;
return 0;
}

4.指向函数的指针组

3. 指针常量与常量指针

1.常量指针

const int *p; 
int const *p;

首先是一个指针,指向一个常量,其指向的值不能改变,指针的值(地址)可以改变
1.指针常量

int *const p;

首先是一个常量,其指针的地址不能改变,可以修改指针指向的值

4. 什么是野指针

野指针不是NULL 指针,而是指向垃圾内存的指针,NULL指针可用if判断,野指针不能判断
主要形成原因:
1.指针变量没有初始化,并且也不会自动置为NULL,而是指向一个随机地址
2.释放时在free和delete之后,没有将其置为NULL。

5. new(delete)与malloc(free)的区别

(1)属性上:new / delete 是c++关键字,需要编译器支持。 malloc/free是库函数,需要c的头文件支持。
(2)参数:使用new操作符申请内存分配时无须制定内存块的大小,编译器会根据类型信息自行计算。而mallco则需要显式地指出所需内存的尺寸。
(3)返回类型:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,故new是符合类型安全性的操作符。而malloc内存成功分配返回的是void *,需要通过类型转换将其转换为我们需要的类型。
(4)分配失败时:new内存分配失败时抛出bad_alloc异常;malloc分配内存失败时返回 NULL。
(5)自定义类型:new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
(6)重载:C++允许重载 new/delete 操作符。而malloc为库函数不允许重载。
(7)内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。其中自由存储区为:C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中
注意:堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。转自https://www.cnblogs.com/QG-whz/p/5060894.html

6. 指针和引用的区别

1.指针与引用的区别:
(1)指针有自己的一块空间,而引用只是一个别名;
(2)使用 sizeof 看一个指针的大小为4字节(32位,如果要是64位的话指针为8字节),而引用则是被引用对象的大小。
(3)指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象的引用。
(4)作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象。
(5)指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变。
(6)指针可以是多级,而引用没有分级
(7)如果返回动态分配内存的对象或者内存,必须使用指针,引用可能引起内存泄漏
2.为什么传引用比指针更安全?
(1).引用不存在空引用,被初始化后不能改变引用的对象
(2).指针可以随时改变指向的对象,还可以不被初始化,const指针也存在空指针,产生野指针。

三、字符串

1. 数字转字符串

使用库函数将数字转换为字符串:
itoa 将整形转字符型

itoa(a,b,10);10表示按照10进制

不使用库函数进行数字转字符

a=b+'0';

2. 字符串转数字

使用库函数将字符串转数字.

atof
atoi
num=atoi(str);

不使用库函数将字符串转数字:
判断ascii码是否在 0,9之间

3. 编程实现strcpy-字符串复制

1.保存目的字符串的位置首地址
2.将被复制字符串复制到目的字符串
while((*strmudi++=*strbei++)!='\0')
3.返回首地址		

1.编程实现memcpy函数-内存复制
复制前n个字符
2.两个的区别
1).strcpy只能复制字符串,memcpy可以复制任意内容,
2).strcpy不需要指定长度,与‘\0’结束;memcpy则是根据第三个参数确定
3).用途不同

4. 实现计算字符串的长度

1.两次自增
while(*str++!='\0')
	len++;
return len;
2.一次自增+头尾指针之差
char * temp=str;
while(*str++!='\0')
return temp-str-1;

5. 编程实现字符串每个单词翻转

1.先挨个单词翻转,再全局翻转
2.先全局翻转,在挨个单词翻转

6. 编程实现strcmp-两个字符串比较大小

while(!(ret=*(* unsigned)src-*(* unsigned)dst))&&*dst)
	src++;
	dst++;

四、C++面对对象基础

1.面对对象与面对过程的区别

什么是过程?过程就是函数。什么是对象,对象具有方法和属性。

面对对象与面对过程的本质区别就是面对过程编程每一个函数就是一个过程执行完就啥也不剩下了,唯一可以输出的就是return但是本质上说也还是过程的一部分,因为一旦执行完return语句本身就不再起作用了,即仅仅走了一个流程而已。所以你会发现面向过程编程想要储存点东西就不得不定义一个全局变量,而函数不过是对全局变量按照流程步骤进行加工处理的过程而已。

然而面对对象编程不是这样。面对对象编程,万事万物皆对象,每当建立一个新的对象,就好像建立了一个实体,她有自己的属性和方法(你给她一个东西他会按照自己的方法处理),如果不对其进行销毁那么,她将会一直存在,而不是仅仅走个过程。你可以随时查找她的属性,也可以随时交给她一些东西处理,即对象不仅仅是一个过程模块,不仅仅是走个过场。

2. struct 和 class 的区别?

1.c语言中的struct和c++中的class

1. c语言中struct只是作为一种复杂的数据类型
只能定义成员变量,不能定义成员函数
2.c++中不仅可以包含成员函数,还有构造函数,拥有class的特性

2.c++中的struct和class

1.内部成员变量及成员函数的默认访问属性:
struct 默认访问属性是 public 的
class 默认的访问属性是private的
2.继承关系中默认访问属性的区别:
struct 默认是 public  
class 是 private
3.class这个关键字还可用于定义模板参数,就等同于 typename;而strcut不用与定义模板参数

3.相比全局变量,静态变量的优势

1.静态数据成员没有进入程序全局名字空间,因此不存在与其他全局变量名字冲突
2.使用静态变量可以隐藏信息,因为静态成员可以使private成员,而全局变量不行

4.什么情况下只能用初始化列表?

1.对于const类型和reference的成员变量,只能使用初始化列表,而不能被赋值
2.类的构造函数需要调用基类的构造函数的时候,必须用使用初始化列表对基类的成员变量进行初始化

5.静态成员函数的使用

1.静态成员不能在类中初始化,使用::在类外初始化
2.静态成员函数不属于类的对象,因此没有this指针,不能访问类的非静态成员
3.静态数据成员可以被类的对象调用
4.静态成员不能受private的作用

6.C++默认空类会产生哪些类成员函数

1.默认构造函数,复制构造函数
2.析构函数
3.取值函数,赋值函数

7.构造函数与析构函数

1.被重载?

1.构造函数可以被重载,因为构造函数有多个,且可以带参数
2.析构函数不能被重载,且不能带参数

2.构造函数之间能不能相互调用?

不行!还会带来副作用;生成的只是在栈上的一个临时对象

3.explicit构造函数的作用

1.在构造函数前面加explicit关键字设计构造函数为显示
2.普通的构造函数能隐式转换
test(int a){};
test t1 = 10;可行
3.在显示构造函数中不能隐式转换
explicit test(int a){};
test t1 = 10;不可行,编译错误
test t1(10);可行

4.虚析构函数的作用
当一个类作为基类时,会把析构函数写成虚函数,用来释放子类的资源,避免内存泄露。

5.拷贝构造函数中的浅拷贝与深拷贝

1.浅拷贝是拷贝的对象与原对象指向同一个外部内容
2.深拷贝为新对象分配新的独立复制

6.拷贝初始化和直接初始化,初始化和赋值的区别?

ClassTest ct1("ab"); 
这条语句属于直接初始化,它不需要调用复制构造函数,直接调用构造函数ClassTest(const char *pc),
所以当复制构造函数变为私有时,它还是能直接执行的。
ClassTest ct2 = "ab"; 
这条语句为复制初始化,它首先调用构造函数 ClassTest(const char* pc) 函数创建一个临时对象,
然后调用复制构造函数,把这个临时对象作为参数,构造对象ct2;
所以当复制构造函数变为私有时,该语句不能编译通过。
ClassTest ct3 = ct1;
这条语句为复制初始化,因为 ct1 本来已经存在,所以不需要调用相关的构造函数,
而直接调用复制构造函数,把它值复制给对象 ct3;所以当复制构造函数变为私有时,该语句不能编译通过。
ClassTest ct4(ct1);
这条语句为直接初始化,因为 ct1 本来已经存在,直接调用复制构造函数,生成对象 ct1 的副本对象 ct4。
所以当复制构造函数变为私有时,该语句不能编译通过。

要点就是拷贝初始化和直接初始化调用的构造函数是不一样的,但是当类进行复制时,类会自动生成一个临时的对象,然后再进行拷贝初始化。拷贝初始化就是复制初始化!!

8.析构函数、构造函数与虚函数的关系

1.析构函数一般写成虚函数的原因?
析构函数执行时先调用派生类的析构函数,其次才调用基类的析构函数。如果析构函数不是虚函数,而程序执行时又要通过基类的指针去销毁派生类的动态对象,那么用delete销毁对象时,只调用了基类的析构函数,未调用派生类的析构函数。这样会造成销毁对象不完全,容易造成内存泄露。
2.构造函数为什么不能是虚函数?
从C++之父Bjarne的回答我们应该知道C++为什么不支持构造函数是虚函数了,简单讲就是没有意义。虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。** 假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?**所以构造函数不能是虚函数。
3.构造函数和析构函数可不可以有虚函数?
总的来说,构造函数和析构函数调用虚函数并不能达到多态的效果,因为在析构和构造过程中,该对象变为一个基类对象,调用的方法都是基类的方法。
构造函数:在基类的构造过程中,虚函数调用从不会被传递到派生类中。代之的是,派生类对象表现出来的行为好象其本身就是基类型。不规范地说,在基类的构造过程中,虚函数并没有被"构造"。简单的说就是,在子类对象的基类子对象构造期间,调用的虚函数的版本是基类的而不是子类的。
析构函数:一旦一个派生类的析构器运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,C++就把它们当做是不存在一样。一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。

9.重载和覆写(重写)的区别

1.重载是编写一个与已有函数同名,但是参数表不同
方法名必须相同
参数表必须不同,与顺序无关
返回值可以不同

2.覆写是指派生类对基类的方法进行完全覆盖
只有虚函数和抽象方法才能被覆写
相同的函数名和参数列表
相同的返回值类型

3.重载是一种语法规则,在编译时完成,不面对对象编程
覆写是运行阶段决定的,是面对对象的特性。

五、继承和多态

1.类的三种继承方式

1.public
保留父类的基础数据类型
2.protect
将父类public改为protect
3.private
将父类的public和protect改为private

注意:
在类的外部不能通过对象和非成员函数访问protect和private成员!!!

2.继承时的构造函数初始化问题

对子类的构造函数进行初始化时,首先要调用对基类进行初始化,因此需要在子类的初始化函数中显示的调用基类的初始化函数—使用初始化列表

3.多态的定义

C++面试基础知识汇总_第2张图片
1.编译时多态
复习一下重载和覆写的概念:

1.重载是编写一个与已有函数同名,但是参数表不同
	方法名必须相同
	参数表必须不同,与顺序无关
	返回值可以不同
2.覆写(重写)是指派生类对基类的方法进行完全覆盖
	只有虚函数和抽象方法才能被覆写
	相同的函数名和参数列表
	相同的返回值类型

函数重载就是一个简单的静态多态

int Add(int left, int right)
{
    return left + right;
}
int Add(left, double right, double mid)
{
    return left + right + mid;
}
int main()
{
    Add(10, 20);
    Add(10.0,20.0,3.0); 
    return 0;
}

可以看出,静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数可以调用就调,没有的话就会发出警告或者报错。

2.运行时多态(动态多态)
它是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。

4.虚函数的实现方式

1.虚函数通过虚函数列表实现
2.每一个类会为分配一个指针指向虚函数表,表中每一项表示一个虚函数的地址
3.虚函数表既有继承性也有多态性
	每个派生类的虚函数表继承父类的虚函数表,如果基类有,那么派生类也会包含。
	若派生类重写了该项虚函数,则派生类的虚函数指重写以后的虚函数

5.为什么要引入抽象虚类和纯虚函数?

纯虚函数:函数就是在基类中没有定义,即函数=0的表达形式,必须在子类中加以实现。
抽象基类:一个基类包含一个或多个纯虚函数,且不能被实例化

方便使用多态
不需要实例化抽象基类

6.虚函数与纯函数的区别

虚函数可以实现,作用是让子类去覆盖他,这样编译器进行后期的动态绑定实现多态,纯虚函数只是声明,留到子类去实现 。

六、STL模板库

1.什么是STL模板库

STL即标准模板库,是一个具有工业强度、高效的C++程序库。他是最新的C++标准函数库的一个子集,包括容器、算法、迭代器等。

2.STL容器分类

C++面试基础知识汇总_第3张图片

**序列式容器(Sequence containers):**如果你以追加方式对一个群集插入六个元素,它们的排列次序将和插入次序一致。STL提供了三个序列式容器:向量(vector)、双端队列(deque)、列表(list),此外你也可以把 string 和 array 当做一种序列式容器。
**关联式容器(Associative containers):**如果你将六个元素置入这样的群集中,它们的位置取决于元素值,和插入次序无关。STL提供了四个关联式容器:集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)。
**适配容器:**除了以上七个基本容器类别,为满足特殊需求,STL还提供了一些特别的(并且预先定义好的)容器配接器,根据基本容器类别实现而成。stack(栈)、queue(队列)、priority_queue(优先队列)。

序列式容器(Sequence containers)

1.vector
vector(向量): 是一种序列式容器,事实上和数组差不多,但它比数组更优越。一般来说数组不能动态拓展,因此在程序运行的时候不是浪费内存,就是造成越界。而 vector 正好弥补了这个缺陷,它的特征是相当于可拓展的数组(动态数组),使用动态数组实现,若内存不够,则会动态重新分配,一般是当前的两倍,把原数组拷贝过去,使用allocator类进行内存管理。。它的随机访问快,在中间插入和删除慢,但在末端插入和删除快。
优缺点:

特点:.拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随机存取,即 [] 操作符,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝,另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了 vector 的效率。
优点:支持随机访问,即 [] 操作和 .at(),所以查询效率高。
缺点:当向其头部或中部插入或删除元素时,为了保持原本的相对次序,插入或删除点之后的所有元素都必须移动,所以插入的效率比较低。
适用场景:适用于对象简单,变化较小,并且频繁随机访问的场景。
注意:其迭代器在插入一次后可能会过期,因为可能会涉及内存扩展后的内存搬移。

常用操作:

插入push_back
删除pop_back
获得容器的容量capacity()
设置容器容量reserve()
insert(迭代器)
erase(迭代器)

2.deque:
deque(double-ended queue)是由一段一段的定量连续空间构成。一旦要在 deque 的前端和尾端增加新空间,便配置一段定量连续空间,串在整个 deque 的头端或尾端。因此不论在尾部或头部安插元素都十分迅速。 在中间部分安插元素则比较费时,因为必须移动其它元素。deque 的最大任务就是在这些分段的连续空间上,维护其整体连续的假象,并提供随机存取的接口。
特点:

特点:按页或块来分配存储器的,每页包含固定数目的元素。
deque 是 list 和 vector 的折中方案。兼有 list 的优点,也有vector 随机线性访问效率高的优点。
优点:支持随机访问,即 [] 操作和 .at(),所以查询效率高;可在双端进行 pop,push。
缺点:不适合中间插入删除操作;占用内存多。
适用场景:适用于既要频繁随机存取,又要关心两端数据的插入与删除的场景。

常用操作:

插入push_back
删除pop_back
头部插入push_front
头部删除pop_front

3.list:
List 由双向链表(doubly linked list)实现而成,元素也存放在堆中,每个元素都是放在一块内存中,他的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,因此它没有提供 [] 操作符的重载。但是由于链表的特点,它可以很有效率的支持任意地方的插入和删除操作。
特点:

特点:没有空间预留习惯,所以每分配一个元素都会从内存中分配,每删除一个元素都会释放它占用的内存。
在哪里添加删除元素性能都很高,不需要移动内存,当然也不需要对每个元素都进行构造与析构了,所以常用来做随机插入和删除操作容器。
访问开始和最后两个元素最快,其他元素的访问时间一样。
优点:内存不连续,动态操作,可在任意位置插入或删除且效率高。
缺点:不支持随机访问。
适用场景:适用于经常进行插入和删除操作并且不经常随机访问的场景。

关联式容器(Associative containers)

1.set和mutiset
set(集合)由红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复。mutiset与set的区别是,可以允许重复出现的元素。
特点:

特点:set 中的元素都是排好序的,集合中没有重复的元素;
map 和 set 的插入删除效率比用其他序列容器高,因为对于关联容器来说,不需要做内存拷贝和内存移动。
优点:使用平衡二叉树实现,便于元素查找,且保持了元素的唯一性,以及能自动排序。
缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响。
适用场景:适用于经常查找一个元素是否在某群集中且需要排序的场景。

常用操作:
插入:insert
删除: erase

举例:

int main(int argc, char* argv[])
{
	set<int> Temp;

	Temp.insert(3);
	Temp.insert(1);
	Temp.insert(2);
	
	set<int>::iterator it;
	for (it = Temp.begin(); it != Temp.end(); it++)
	{
		cout << *it << " ";
	}
	return 0;
}

2.map和mutimap
map 由红黑树实现,其元素都是 “键值/实值” 所形成的一个对组(key/value pairs)。每个元素有一个键,是排序准则的基础。每一个键只能出现一次,不允许重复。
map 主要用于资料一对一映射的情况,map 内部自建一颗红黑树,这颗树具有对数据自动排序的功能,所以在 map 内部所有的数据都是有序的。比如一个班级中,每个学生的学号跟他的姓名就存在着一对一映射的关系。
mutimap则是允许出现重复的键值对,但是依旧是有序的。

特点:

自动建立 Key - value 的对应。key 和 value 可以是任意你需要的类型。
根据 key 值快速查找记录,查找的复杂度基本是 O(logN),如果有 1000 个记录,二分查找最多查找 10次(1024)。
增加和删除节点对迭代器的影响很小,除了那个操作节点,对其他的节点都没有什么影响。
对于迭代器来说,可以修改实值,而不能修改 key。
优点:使用平衡二叉树实现,便于元素查找,且能把一个值映射成另一个值,可以创建字典。
缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响。
适用场景:适用于需要存储一个数据字典,并要求方便地根据key找value的场景。

适配容器

1、stack
名字说明了一切,stack 容器对元素采取 LIFO(后进先出)的管理策略。
入队:push
出队:pop
栈顶:top
2、queue
queue 容器对元素采取 FIFO(先进先出)的管理策略。也就是说,它是个普通的缓冲区(buffer)。
插入push_back
删除pop_back
3、priority_queue
priority_queue 容器中的元素可以拥有不同的优先权。所谓优先权,乃是基于程序员提供的排序准则(缺省使用 operators)而定义。Priority queue 的效果相当于这样一个 buffer:“下一元素永远是queue中优先级最高的元素”。如果同时有多个元素具备最髙优先权,则其次序无明确定义。

各容器对比

1.特点对比
C++面试基础知识汇总_第4张图片

C++面试基础知识汇总_第5张图片
2.复杂度
各容器的时间复杂度分析
vector 在头部和中间位置插入和删除的时间复杂度为 O(N),在尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);
deque 在中间位置插入和删除的时间复杂度为 O(N),在头部和尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);
list 在任意位置插入和删除的时间复杂度都为 O(1),查找的时间复杂度为 O(N);
set 和 map 都是通过红黑树实现,因此插入、删除和查找操作的时间复杂度都是 O(log N)。

3. 频繁对vector调用push_back()对性能的影响和原因?

在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。而且,向一个vector添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移到新的空间。

4. 只在堆上或者栈上创建对象

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。

静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

5.vector和list的区别

1.vector类似于数组,有连续的内存空间,方便查询,迭代器支持“+”、“+=”、“<”符号
2.list使用双向链表实现,内存空间不连续,方便插入和删除,只重载了“++”符号
3.迭代器在插入和删除时,vector会失效,list在插入时不会失效。删除时,list的删除元素会失效。

6.vector和deque的区别

deque是双向队列,与vector十分相似。
不同点1:
deque比vector多了两个函数:
push_front() pop_front()这是对队首进行操作,
当程序需要在首尾两端进行删除或者插入式使用deque
不同2:
deque中不存在capacity()和reserve()函数,分别用于获得容器的容量和设置容器容量。
容量 != 容器中元素个数
在vector中添加元素之前最好设置容器大小,提高效率

7.map和hash map的区别?

1.底层数据结构:
map是红黑树 ,查找时间复杂度是log(n);
hashmap是哈希表 ,查询时间复杂度是O(1);

2.map的优点在于可以自动按照键值排序,
hashmap的优点在于他的操作平均时间复杂度是一个常数

3.map是STL标准的一部分,hashmap不是

8.数据结构:hash_map原理

hash_map基于hash table(哈希表)。哈希表最大的优点是:把数据存储和查询消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然后在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容易也是它的特点之一。
其基本原理是:使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫散列函数),使得每个元素的关键字都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。
但是,这不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对不不同的元素,却计算出相同的函数值,这样就产生了“冲突”。换句话说,就是把不同的元素分在了相同的“类”中。总的来说,“直接定址”和“解决冲突”是哈希表的两大特点。
hash_map,首先分配一大片内存,形成许多桶。是利用hash函数,对key进行映射到不同区域进行保存。其插入过程:
1、得到key;
2、通过hash函数得到hash值;
3、得到桶号(一般都为hash值对桶数求模);
4、存放key和value在桶内;
其取值过程是:
1、得到key;
2、通过hash函数得到hash值;    
3、得到桶号;
4、比较桶的内部元素是否与key相等,若不相等,则没有找到;
5、取出相等的记录的value;
hash_map中直接地址用hash函数生成,解决冲突,用比较函数解决。这里可以看出,如果每个桶内部只有一个元素,那么查找的时候只有一次比较。当许多桶没有值时,许多查询就会更快了(指查不到的时候)。
由此可见,要实现哈希表,和用户相关的是:hash函数和比较函数。这两个参数刚好是我们在使用hash_map时需要指定的参数。

9. STL map和set的使用的一些问题?

1.为何map和set的插入删除效率比用其他序列容器高?
关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树

大部分人说,很简单,因为对于关联容器来说,不需要做内存拷贝和内存移动。说对了,确实如此。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。

插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点就OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。

2.为何每次insert之后,以前保存的iterator不会失效?
iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。
vector的迭代器什么时候失效?
相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。

3.为何map和set不能像vector一样有个reserve(设计容器的容量的函数)函数来预分配数据?
**引起它的原因在于在map和set内部存储的已经不是元素本身了,而是包含元素的节点。**也就是说map内部使用的Alloc并不是map声明的时候从参数中传入的Alloc。

4.当数据元素增多时(10000到20000个比较),map和set的插入和搜索速度变化如何?
如果你知道log2的关系你应该就彻底了解这个答案。
**在map和set中查找是使用二分查找,**也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?
最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了。

10. 什么是STL算法?

需要带头文件:algorithm
1.reverse()–翻转一个区间的元素
全局函数,而不是成员函数
有两个参数,一个是操作的头指针,一个尾指针
也可以对数组进行操作

2.find(迭代器1,迭代器2,元素)
查找元素

3.for_each(迭代器1,迭代器2,print)
打印

11. STL内存优化问题

STL内存管理使用二级内存配置器。
如果要分配的区块大于128bytes,则移交给第一级配置器处理。
如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation)
(1) 第一级配置器:

第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。一级空间配置器分配的是大于128字节的空间,如果分配不成功,调用句柄释放一部分内存,如果还不能分配成功,抛出异常。

第一级配置器只是对malloc函数和free函数的简单封装,在allocate内调用malloc,在deallocate内调用free。同时第一级配置器的oom_malloc函数,用来处理malloc失败的情况。

(2) 第二级配置器:

第一级配置器直接调用malloc和free带来了几个问题:

-内存分配/释放的效率低
-当配置大量的小内存块时,会导致内存碎片比较严重
-配置内存时,需要额外的部分空间存储内存块信息,所以配置大量的小内存块时,还会导致额外内存负担.

如果分配的区块小于128bytes,则以内存池管理,第二级配置器维护了一个自由链表数组,每次需要分配内存时,直接从相应的链表上取出一个内存节点就完成工作,效率很高。

自由链表数组: 自由链表数组其实就是个指针数组,数组中的每个指针元素指向一个链表的起始节点。数组大小为16,即维护了16个链表,链表的每个节点就是实际的内存块相同链表上的内存块大小都相同,不同链表的内存块大小不同,从8一直到128

内存分配: allocate函数内先判断要分配的内存大小,若大于128字节,直接调用第一级配置器,否则根据要分配的内存大小从16个链表中选出一个链表,取出该链表的第一个节点。若相应的链表为空,则调用refill函数填充该链表。默认是取出20个数据块。

填充链表 refill: 若allocate函数内要取出节点的链表为空,则会调用refill函数填充该链表。refill函数内会先调用chunk_alloc函数从内存池分配一大块内存,该内存大小默认为20个链表节点大小,当内存池的内存也不足时,返回的内存块节点数目会不足20个。接着refill的工作就是将这一大块内存分成20份相同大小的内存块,并将各内存块连接起来形成一个链表。

内存池: chunk_alloc函数内管理了一块内存池,当refill函数要填充链表时,就会调用chunk_alloc函数,从内存池取出相应的内存。

在chunk_alloc函数内首先判断内存池大小是否足够填充一个有20个节点的链表,若内存池足够大,则直接返回20个内存节点大小的内存块给refill;
若内存池大小无法满足20个内存节点的大小,但至少满足1个内存节点,则直接返回相应的内存节点大小的内存块给refill;
若内存池连1个内存节点大小的内存块都无法提供,则chunk_alloc函数会将内存池中那一点点的内存大小分配给其他合适的链表,然后去调用malloc函数分配的内存大小为所需的两倍。若malloc成功,则返回相应的内存大小给refill;若malloc失败,会先搜寻其他链表的可用的内存块,添加到内存池,然后递归调用chunk_alloc函数来分配内存,若其他链表也无内存块可用,则只能调用第一级空间配置器。
原文链接:https://blog.csdn.net/weixin_43819197/article/details/94407751

总结:
使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc。
如果需要的内存大小小于128bytes,allocate根据size找到最适合的自由链表。
a. 如果链表不为空,返回第一个node,链表头改为第二个node。
b. 如果链表为空,使用blockAlloc请求分配node。
x. 如果内存池中有大于一个node的空间,分配竟可能多的node(但是最多20个),将一个node返回,其他的node添加到链表中。
y. 如果内存池只有一个node的空间,直接返回给用户。
z. 如果连一个node都没有,再次向操作系统请求分配内存。
①分配成功,再次进行b过程。
②分配失败,循环各个自由链表,寻找空间。
I. 找到空间,再次进行过程b。
II. 找不到空间,抛出异常。
用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。

12.C++内存管理

程序内存可以分为以下五种:

1、 栈区(stack):栈的空间是连续的, 先进后出能保证不会产生内存碎片, 由高地址向低地址生长, 编译器自动分配和释放, 用来存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。
2、 堆区(heap):堆则不同, 堆内存不一定是连续的. 分配方式是每次在空闲链表中遍历到第一个大于申请空间的节点,每次分配的空间大小一般不会正好等于申请的内存大小,所以频繁的new操作势必会产生大量的空间碎片 。 堆内存一般由程序员使用malloc、new亦或是STL中的allocator分配 , 若程序员不释放,程序结束时可能由操作系统回收。
3、 全局区(static):全局变量和静态变量存放在此。
4、 常量区:常量字符串放在此,程序结束后由系统释放。
5、 代码区:存放函数体的二进制代码。
C++面试基础知识汇总_第6张图片

七、C++11新特性

在面试中,经常被问的一个问题就是:你了解C++11哪些新特性?一般而言,回答以下四个方面就够了:
“语法糖”:nullptr, auto自动类型推导,范围for循环,初始化列表, lambda表达式等
右值引用和移动语义
智能指针
C++11多线程编程:thread库及其相配套的同步原语mutex, lock_guard, condition_variable, 以及异步std::furture

1.语法糖

比较重重要的就是auto和lambda。

1)nullptr–区分空指针和0
在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。
C++ 不允许直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译char *ch = NULL;时,NULL 只好被定义为 0。

而这依然会产生问题,将导致了 C++ 中重载特性会发生混乱,考虑:

void foo(char *);
void foo(int);

对于这两个函数来说,如果 NULL 又被定义为了 0 那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直观。
为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。
nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
当需要使用 NULL 时候,养成直接使用 nullptr的习惯。

2)自动类型推导—auto、decltype
auto自动类型推导
C语言也有auto关键字,但是其含义只是与static变量做一个区分,一个变量不指定的话默认就是auto。。因为很少有人去用这个东西,所以在C++11中就把原有的auto功能给废弃掉了,而变成了现在的自动类型推导关键字。用法很简单不多赘述,比如写一个auto a = 3, 编译器就会自动推导a的类型为int. 在遍历某些STL容器的时候,不用去声明那些迭代器的类型,也不用去使用typedef就能很简洁的实现遍历了。

auto的使用有以下两点必须注意:

auto声明的变量必须要初始化,否则编译器不能判断变量的类型。
auto不能被声明为返回值,auto不能作为形参,auto不能被修饰为模板参数

关于效率: auto实际上实在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响。另外,auto并不会影响编译速度,因为编译时本来也要右侧推导然后判断与左侧是否匹配。

使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。在以前我们需要这样来书写一个迭代器:

for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr);
而有了 auto 之后可以:
for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);

一些其他的常见用法:

auto i = 5;             // i 被推导为 int
auto arr = new auto(10) // arr 被推导为 int *

注意:auto 不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板):

int add(auto x, auto y);

此外,auto 还不能用于推导数组类型

#include 

int main() {
 auto i = 5;

 int arr[10] = {0};
 auto auto_arr = arr;
 auto auto_arr2[10] = arr;
 return 0;
}

3)decltype
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 sizeof 很相似,
decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型。

int x = 3;  
decltype(x) y = x;

在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
有时候,我们可能需要计算某个表达式的类型,例如:

auto x = 1;
auto y = 2;
decltype(x+y) z;

4)拖尾返回类型、auto 与 decltype 配合
你可能会思考,auto 能不能用于推导函数的返回类型。考虑这样一个例子加法函数的例子,在传统 C++ 中我们必须这么写:

template<typename R, typename T, typename U>
R add(T x, U y) {
    return x+y;
}

这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但事实上我们并不知道 add() 这个函数会做什么样的操作,获得一个什么样的返回类型。

在 C++11 中这个问题得到解决。虽然你可能马上回反应出来使用 decltype 推导 x+y 的类型,写出这样的代码:

decltype(x+y) add(T x, U y);

但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,x 和 y 尚未被定义。为了解决这个问题,C++11 还引入了一个叫做拖尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:

template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
    return x+y;
}

从 C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:

template<typename T, typename U>
auto add(T x, U y) {
    return x+y;
}

5)范围for循环
C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句。
最常用的 std::vector 遍历将从原来的样子:

vector<int> arr(5, 100);
for(vector<int>::iterator it = arr.begin(); it != arr.end(); ++it) {
    cout << *it << endl;

变得非常的简单:

// & 启用了引用
for(auto &it : arr) {    
    cout << it << endl;
}

6)更优雅的初始化列表

引入C++11之前,只有数组能使用初始化列表,其他容器想要使用初始化列表,只能用以下方法:

int arr[3] = {1, 2, 3};
vector<int> v(arr, arr + 3);

在C++11中,我们可以使用以下语法来进行替换:

int arr[3]{1, 2, 3};
vector<int> iv{1, 2, 3};
map<int, string>{{1, "a"}, {2, "b"}};
string str{"Hello World"};

7)Lambda 表达式–匿名函数

[ caputrue ] ( params ) opt -> ret { body; };
capture是捕获列表;
params是参数表;(选填)
opt是函数选项
ret是返回值类型(拖尾返回类型)
body是函数体

[捕获区](参数区){代码区};
auto add = [](int a, int b) {return a + b};

(1).capture是捕获列表
lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。

1) []不捕获任何变量。
2) [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
3) [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)--不能被修改。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
4) [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
5) [bar]按值捕获bar变量,同时不捕获其他变量。
6) [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。

例子::::::

class A
{
 public:
     int i_ = 0;
     void func(int x,int y){
         auto x1 = [] { return i_; };                   //error,没有捕获外部变量
         auto x2 = [=] { return i_ + x + y; };          //OK
         auto x3 = [&] { return i_ + x + y; };        //OK
         auto x4 = [this] { return i_; };               //OK
         auto x5 = [this] { return i_ + x + y; };       //error,没有捕获x,y
         auto x6 = [this, x, y] { return i_ + x + y; };     //OK
         auto x7 = [this] { return i_++; };             //OK
};

int a=0 , b=1;
auto f1 = [] { return a; };                         //error,没有捕获外部变量    
auto f2 = [&] { return a++ };                      //OK
auto f3 = [=] { return a; };                        //OK
auto f4 = [=] {return a++; };                       //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; };                      //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); };                //OK
auto f7 = [=, &b] { return a + (b++); };                //OK

虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。
原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。

int a = 0;
auto f1 = [=] { return a++; };                //error
auto f2 = [=] () mutable { return a++; };       //OK

(2) opt是函数选项
可以填mutable,exception,attribute(选填)
mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
exception说明lambda表达式是否抛出异常以及何种异常。attribute用来声明属性。

2.右值引用和move语义

右值引用是C++11新特性,它实现了转移语义和完美转发,主要目的有两个方面:
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
能够更简洁明确地定义泛型函数,C++中的变量要么是左值、要么是右值。通俗的左值定义指的是非临时变量,而左值指的是临时对象。左值引用的符号是一个&,右值引用是两个&&.
1).左值和右值
在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。

举个例子,int a = b+c,

a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译

2)右值与将亡值
在理解C++11的右值前,先看看C++98中右值的概念:C++98中右值是纯右值,纯右值指的是临时变量值、不跟对象关联的字面量值。临时变量指的是非引用返回的函数返回值、表达式等,例如函数int func()的返回值,表达式a+b;不跟对象关联的字面量值,例如true,2,”C”等。

C++11对C++98中的右值进行了扩充。
在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。

其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。

将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。

3)左值引用与右值引用
左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。

右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。

int &a = 2;       # 左值引用绑定到右值,编译失败
int b = 2;        # 非常量左值
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2;  # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2;  # 常量左值引用绑定到右值,编程通过

右值引用的格式::

参数(右值)的符号必须是右值引用符号,即“&&”。
参数(右值)不可以是常量,因为我们需要修改右值。
参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。

4)move()— 将左值变成一个右值
右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值,例如:

int a;
int &&r1 = c;             # 编译失败
int &&r2 = std::move(a);  # 编译通过

3.智能指针

由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见。
原文链接:https://blog.csdn.net/xt_xiaotian/article/details/5714477
用智能指针便可以有效缓解这类问题,本文主要讲解参见的智能指针的用法。包括:

std::auto_ptr、
boost::scoped_ptr、
boost::shared_ptr、
boost::scoped_array、
boost::shared_array、
boost::weak_ptr、
boost:: intrusive_ptr。

你可能会想,如此多的智能指针就为了解决new、delete匹配问题,真的有必要吗?看完这篇文章后,我想你心里自然会有答案。
于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放有它管理的堆内存。所有智能指针都重载了“operator->”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。

访问智能指针包含的裸指针则可以用 get() 函数。由于智能指针是一个对象,所以if (my_smart_object)永远为真,要判断智能指针的裸指针是否为空,需要这样判断:if (my_smart_object.get())。

智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。

1.std::auto_ptr
std::auto_ptr 属于 STL,当然在 namespace std 中,包含头文件 #include 便可以使用。std::auto_ptr 能够方便的管理单个堆内存对象。
我们从代码开始分析:

void TestAutoPtr() {
std::auto_ptr<Simple> my_memory(new Simple(1));   // 创建对象,输出:Simple:1
if (my_memory.get()) {    // 判断智能指针是否为空
	my_memory->PrintSomething();                    // 使用 operator-> 调用智能指针对象中的函数
	my_memory.get()->info_extend = "Addition";      // 使用 get() 返回裸指针,然后给内部对象赋值
	my_memory->PrintSomething();                    // 再次打印,表明上述赋值成功
	(*my_memory).info_extend += " other";           // 使用 operator* 返回智能指针内部对象,然后用“.”调用智能指针对象中的函数
	my_memory->PrintSomething();                    // 再次打印,表明上述赋值成功
  }
}                                                   // my_memory 栈对象即将结束生命期,析构堆对象 Simple(1)
结果为:
Simple: 1
PrintSomething:
PrintSomething: Addition
PrintSomething: Addition other
~Simple: 1

上述为正常使用 std::auto_ptr 的代码,一切似乎都良好,无论如何不用我们显示使用该死的 delete 了。
其实好景不长,我们看看如下的另一个例子:

void TestAutoPtr2() {
  std::auto_ptr<Simple> my_memory(new Simple(1));
  if (my_memory.get()) {
    std::auto_ptr<Simple> my_memory2;   // 创建一个新的 my_memory2 对象
    my_memory2 = my_memory;             // 复制旧的 my_memory 给 my_memory2
    my_memory2->PrintSomething();       // 输出信息,复制成功
    my_memory->PrintSomething();        // 崩溃
  }
}

最终如上代码导致崩溃,如上代码时绝对符合 C++ 编程思想的,居然崩溃了,跟进 std::auto_ptr 的源码后,我们看到,罪魁祸首是“my_memory2 = my_memory”,这行代码,my_memory2 完全夺取了 my_memory 的内存管理所有权,导致 my_memory 悬空,最后使用时导致崩溃。
所以,使用 std::auto_ptr 时,绝对不能使用“operator=”操作符。作为一个库,不允许用户使用,却没有明确拒绝,多少会觉得有点出乎预料。

看完 std::auto_ptr 好景不长的第一个例子后,让我们再来看一个:

if (my_memory.get()) {
    my_memory.release();
  }
}
执行结果为:
Simple: 1

看到什么异常了吗?我们创建出来的对象没有被析构,没有输出“~Simple: 1”,导致内存泄露。当我们不想让 my_memory 继续生存下去,我们调用 release() 函数释放内存,结果却导致内存泄露(在内存受限系统中,如果my_memory占用太多内存,我们会考虑在使用完成后,立刻归还,而不是等到 my_memory 结束生命期后才归还)

正确的代码应该为:

 if (my_memory.get()) {
    Simple* temp_memory = my_memory.release();
    delete temp_memory;
  }
}if (my_memory.get()) {
    my_memory.reset();  // 释放 my_memory 内部管理的内存
  }
}

原来 std::auto_ptr 的 release() 函数只是让出内存所有权,这显然也不符合 C++ 编程思想。

总结auto_ptr:

1.尽量不要使用“operator=”。会造成指针悬空
2.记住 release() 函数不会释放对象,仅仅归还所有权。
3.std::auto_ptr 最好不要当成参数传递
4.于 std::auto_ptr 的“operator=”问题,有其管理的对象不能放入 std::vector 等容器中。

因为auto_ptr的各种bug,C++11标准基本废弃了这种类型的智能指针,转而带来了三种全新的智能指针:

shared_ptr, 基于引用计数的智能指针,会统计当前有多少个对象同时拥有该内部指针;当引用计数降为0时,自动释放
weak_ptr, 基于引用计数的智能指针在面对循环引用的问题将无能为力,因此C++11还引入weak_ptr与之配套使用,weak_ptr只引用,不计数
unique_ptr: 遵循独占语义的智能指针,在任何时间点,资源智能唯一地被一个unique_ptr所占有,当其离开作用域时自动析构。资源所有权的转移只能通过std::move()而不能通过赋值

2.unique_ptr
std::unique_ptr是C++11中新定义的智能指针,用于取代auto_ptr。unique_ptr不仅可以代理new创建的单个对象,也可以代理new[]创建的数组对象,就是说它结合了scoped_ptr和scoped_array两者的能力。

unique_ptr的基本能力跟scoped_ptr一样,同样可以在作用域内管理指针,也不允许拷贝和赋值。

3.boost::scoped_ptr和boost::scoped_array
boost::scoped_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件 #include 便可以使用。boost::scoped_ptr 跟 std::auto_ptr 一样,可以方便的管理单个堆内存对象,特别的是,boost::scoped_ptr 独享所有权,避免了 std::auto_ptr 恼人的几个问题。
我们还是从代码开始分析:

void TestScopedPtr() {
  boost::scoped_ptr<Simple> my_memory(new Simple(1));
  if (my_memory.get()) {
    my_memory->PrintSomething();
    my_memory.get()->info_extend = "Addition";
    my_memory->PrintSomething();
    (*my_memory).info_extend += " other";
    my_memory->PrintSomething();
    my_memory.release();           // 编译 error: scoped_ptr 没有 release 函数
    std::auto_ptr<Simple> my_memory2;
    my_memory2 = my_memory;        // 编译 error: scoped_ptr 没有重载 operator=,不会导致所有权转移
  }
}

首先,我们可以看到,boost::scoped_ptr 也可以像 auto_ptr 一样正常使用。但其没有 release() 函数,不会导致先前的内存泄露问题。其次,由于 boost::scoped_ptr 是独享所有权的所以明确拒绝用户写“my_memory2 = my_memory”之类的语句,可以缓解 std::auto_ptr 几个恼人的问题。

由于 boost::scoped_ptr 独享所有权,当我们真真需要复制智能指针时,需求便满足不了了,如此我们再引入一个智能指针,专门用于处理复制,参数传递的情况,这便是如下的 boost::shared_ptr。
原文链接:https://blog.csdn.net/xt_xiaotian/article/details/5714477

boost::scoped_array 属于 boost 库,定义在 namespace boost 中,包含头文件 #include 便可以使用。
boost::scoped_array 便是用于管理动态数组的。跟 boost::scoped_ptr 一样,也是独享所有权的。

我们还是从代码开始分析:

void TestScopedArray() {
      boost::scoped_array<Simple> my_memory(new Simple[2]); // 使用内存数组来初始化
      if (my_memory.get()) {
        my_memory[0].PrintSomething();
        my_memory.get()[0].info_extend = "Addition";
        my_memory[0].PrintSomething();
        (*my_memory)[0].info_extend += " other";            // 编译 error,scoped_ptr 没有重载 operator*
        my_memory[0].release();                             // 同上,没有 release 函数
        boost::scoped_array<Simple> my_memory2;
        my_memory2 = my_memory;                             // 编译 error,同上,没有重载 operator=
      }
    }

boost::scoped_array 的使用跟 boost::scoped_ptr 差不多,不支持复制,并且初始化的时候需要使用动态数组。另外,boost::scoped_array 没有重载“operator*”,其实这并无大碍,一般情况下,我们使用 get() 函数更明确些。

3.boost::shared_ptr和boost::shared_array
boost::shared_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件 #include 便可以使用。在上面我们看到 boost::scoped_ptr 独享所有权,不允许赋值、拷贝,boost::shared_ptr 是专门用于共享所有权的,由于要共享所有权,其在内部使用了引用计数。boost::shared_ptr 也是用于管理单个堆内存对象的。

我们还是从代码开始分析:

void TestSharedPtr(boost::shared_ptr<Simple> memory) {  // 注意:无需使用 reference (或 const reference)
  memory->PrintSomething();
  std::cout << "TestSharedPtr UseCount: " << memory.use_count() << std::endl;
}
void TestSharedPtr2()
 {
  boost::shared_ptr<Simple> my_memory(new Simple(1));
  if (my_memory.get())
   {
    my_memory->PrintSomething();
    my_memory.get()->info_extend = "Addition";
    my_memory->PrintSomething();
    (*my_memory).info_extend += " other";
    my_memory->PrintSomething();
   }
  std::cout << "TestSharedPtr2 UseCount: " << my_memory.use_count() << std::endl;
  TestSharedPtr(my_memory);
  std::cout << "TestSharedPtr2 UseCount: " << my_memory.use_count() << std::endl;
  //my_memory.release();// 编译 error: 同样,shared_ptr 也没有 release 函数

}

boost::shared_ptr 也可以很方便的使用。并且没有 release() 函数。关键的一点,boost::shared_ptr 内部维护了一个引用计数,由此可以支持复制、参数传递等。boost::shared_ptr 提供了一个函数 use_count() ,此函数返回 boost::shared_ptr 内部的引用计数。查看执行结果,我们可以看到在 TestSharedPtr2 函数中,引用计数为 1,传递参数后(此处进行了一次复制),在函数TestSharedPtr 内部,引用计数为2,在 TestSharedPtr 返回后,引用计数又降低为 1。 当我们需要使用一个共享对象的时候,boost::shared_ptr 是再好不过的了。

在此,我们已经看完单个对象的智能指针管理,关于智能指针管理数组,我们接下来讲到。
boost::shared_array 属于 boost 库,定义在 namespace boost 中,包含头文件 #include 便可以使用。

由于 boost::scoped_array 独享所有权,显然在很多情况下(参数传递、对象赋值等)不满足需求,由此我们引入 boost::shared_array。跟 boost::shared_ptr 一样,内部使用了引用计数。

我们还是从代码开始分析:

void TestSharedArray(boost::shared_array<Simple> memory) {  // 注意:无需使用 reference (或 const reference)
  std::cout << "TestSharedArray UseCount: " << memory.use_count() << std::endl;
}
void TestSharedArray2() {
  boost::shared_array<Simple> my_memory(new Simple[2]);
  if (my_memory.get()) {
    my_memory[0].PrintSomething();
    my_memory.get()[0].info_extend = "Addition 00";
    my_memory[0].PrintSomething();
    my_memory[1].PrintSomething();
    my_memory.get()[1].info_extend = "Addition 11";
    my_memory[1].PrintSomething();
    //(*my_memory)[0].info_extend += " other";  // 编译 error,scoped_ptr 没有重载 operator*
  }
  std::cout << "TestSharedArray2 UseCount: " << my_memory.use_count() << std::endl;
  TestSharedArray(my_memory);
  std::cout << "TestSharedArray2 UseCount: " << my_memory.use_count() << std::endl;

}

跟 boost::shared_ptr 一样,使用了引用计数,可以复制,通过参数来传递。

至此,我们讲过的智能指针有 std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array。这几个智能指针已经基本够我们使用了,90% 的使用过标准智能指针的代码就这 5 种。可如下还有两种智能指针,它们肯定有用,但有什么用处呢,一起看看吧。
原文链接:https://blog.csdn.net/xt_xiaotian/article/details/5714477

4.boost::weak_ptr
boost::weak_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件 #include 便可以使用。

在讲 boost::weak_ptr 之前,让我们先回顾一下前面讲解的内容。似乎 boost::scoped_ptr、boost::shared_ptr 这两个智能指针就可以解决所有单个对象内存的管理了,这儿还多出一个 boost::weak_ptr,是否还有某些情况我们没纳入考虑呢?

回答:有。首先 boost::weak_ptr 是专门为 boost::shared_ptr 而准备的。 有时候,我们只关心能否使用对象,并不关心内部的引用计数。boost::weak_ptr 是 boost::shared_ptr 的观察者(Observer)对象,观察者意味着 boost::weak_ptr 只对 boost::shared_ptr 进行引用,而不改变其引用计数,当被观察的 boost::shared_ptr 失效后,相应的 boost::weak_ptr 也相应失效。

我们还是从代码开始分析:

 void TestWeakPtr() {
      boost::weak_ptr<Simple> my_memory_weak;
      boost::shared_ptr<Simple> my_memory(new Simple(1));
      std::cout << "TestWeakPtr boost::shared_ptr UseCount: " << my_memory.use_count() << std::endl;
      my_memory_weak = my_memory;
      std::cout << "TestWeakPtr boost::shared_ptr UseCount: " << my_memory.use_count() << std::endl;
}
    执行结果为:
Simple: 1
TestWeakPtr boost::shared_ptr UseCount: 1
TestWeakPtr boost::shared_ptr UseCount: 1
~Simple: 1

我们看到,尽管被赋值了,内部的引用计数并没有什么变化,当然,读者也可以试试传递参数等其他情况。

现在要说的问题是,boost::weak_ptr 到底有什么作用呢?从上面那个例子看来,似乎没有任何作用,其实 boost::weak_ptr 主要用在软件架构设计中,可以在基类(此处的基类并非抽象基类,而是指继承于抽象基类的虚基类)中定义一个 boost::weak_ptr,用于指向子类的 boost::shared_ptr,这样基类仅仅观察自己的 boost::weak_ptr 是否为空就知道子类有没对自己赋值了,而不用影响子类 boost::shared_ptr 的引用计数,用以降低复杂度,更好的管理对象。
————————————————
本文内容是在以下优秀博主的基础上进行转载或者理解后的分析,谢谢各位大佬的探路。若文章有侵权,请联系我,立马删除,谢谢。
转载:https://blog.csdn.net/xt_xiaotian/article/details/5714477
https://www.cnblogs.com/loskyer/p/8984142.html
https://blog.csdn.net/jiange_zh/article/details/79356417
https://blog.csdn.net/helloworld_ptt/article/details/86544110
https://blog.csdn.net/gcs6564157/article/details/65443202
https://blog.csdn.net/LanerGaming/article/details/80768543
https://www.cnblogs.com/linuxAndMcu/p/10254542.html

你可能感兴趣的:(c++)