C++面向对象知识总结

自闭了,我还是从头学习下C语言吧,我以前肯定没有学过,弃疗了

2020/3/24 更新到1、19

3/25 再次更新到2、9

3/26更新struct unions 字节对齐

3/27STL部分更新

4/6 更新share_ptr源码模仿实现,RB树优点

1、 常见关键字问答

1、static关键字

外部变量虽属于静态 存储方式,但不一定是静态变量,必须由 static加以定义后才能成为静态外部变量,或称静态全局变量。
全局变量(外部变量)的说明之前再冠以static 就构 成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。

全局/局部-变量/函数

  1. 静态局部变量在函数内定义 它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。如果可以再次回来此局部静态变量值依然是最后一次的值不变。

  2. 允许对构造类静态局部量赋初值 例如数组,若未赋以初值,则由系统自动赋以0值。

  3. 非静态全局 变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的而静态全局变量则限制了其作用域, 即只在 定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此 可以避免在其它源文件中引起错误。从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量 后是改变了它的作用域, 限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。

  4. 定义一个内部函数,只需在函数类型前再加一个“static”关键字即可。此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。

  5. 外部函数
    外部函数的定义:在定义函数时,如果没有加关键字“static”,或冠以关键字“extern”,表示此函数是外部函数: [extern] 函数类型 函数名(函数参数表)

类内/外-变量/函数

  1. 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象***享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。
    初始化 必须放在类外 全局 不在任何函数中 主函数中

  2. 类的静态函数
    静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
    在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

在类外
C++静态类型成员变量的初始化顺序和声明的顺序不一致,和初始化语句的先后顺序有关

2、在这里同样可以看到继承关系初始化的一部分

类内静态变量初始化顺序(发送子类继承父类 同时子类父类都有静态):
1)父类的静态变量和静态块。父类的静态变量和静态块的初始化次序是按代码次序执行。
2)子类的静态变量和静态块。子类的静态变量和静态块的初始化次序同父类。
3)父类的非静态变量和非静态块。他们之间初始化次序按代码次序执行。此时如果对象中所有的非静态变量和非静态块没有直接赋值,将执行默认的初始化。(其中非静态变量包括基本类型的变量和对象的引用)
4)父类的构造函数。调用构造函数时,会对实例变量进行初始化。
注意:1), 2)无论类是否产生对象,他们都回执行初始化;3),4)产生对象后才会执行。
5)子类的非静态变量和非静态块。他们之间初始化次序按代码次序执行。
6)子类的构造函数。参考4)

补充:
(a) 基类初始化
(b) 对象成员初时化**简单点想,这里分配空间,构造函数实现的赋值**
© 构造函数的赋值语句

3、public/protected/private

  1. public的变量和函数在类的内部外部都可以访问。
  2. protected的变量和函数只能在类的内部和其派生类中访问。
  3. private修饰的元素只能在类内访问。
基类成员属性 public继承的子类成员属性 protected继承的子类成员属性 private继承的子类成员的属性
public public protected private
protected protected protected private
private 不能继承 不能继承 不能继承

不能继承 但可以用接口public设计的方法去使用

4、C++空类有哪些成员函数

1.首先,空类对象大小为1字节。
2.默认函数有

  • 构造函数 A();
  • 析构函数 ~A(void);
  • 拷贝构造函数 A(const A &a);
  • 赋值运算符 A& operate =(const A &a);

编译器 只有在需要的时候才自己生成,不一定非得自己生成必须调用,想这种空类(简单类) new 和 delete 其实不会调用什么构造析构 本质上只是系统给了空类一个一字节占位罢了

5、构造函数能否为虚函数,析构函数呢?

析构函数:
1.析构函数可以为虚函数,并且一般情况下基类析构函数要定义为虚函数。
2.只有在基类析构函数定义为虚函数时,调用操作符delete销毁指向对象的基类指针时,才能准确调用派生类的析构函数(从该级向上按序调用虚函数),才能准确销毁数据。
3.析构函数可以是纯虚函数,含有纯虚函数的类是抽象类,此时不能被实例化。但派生类中可以根据自身需求重新改写基类中的纯虚函数。

构造函数:
1.构造函数不能定义为虚函数。虚函数对应一个虚函数表(vtable),可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

6、虚函数

虚函数的地址存放于虚函数表之中。运行期多态就是通过虚函数和虚函数表实现的。
类的对象内部会有指向类内部的虚表地址的指针。通过这个指针调用虚函数。
虚函数的调用会被编译器转换为对虚函数表的访问

https://blog.csdn.net/u012630961/article/details/81226351

单继承

这种情况下,派生类中仅有一个虚函数表。这个虚函数表和基类的虚函数表不是一个表(无论派生类有没有重写基类的虚函数),但是如果派生类没有重写基类的虚函数的话,基类和派生类的虚函数表指向的函数地址都是相同的。

如果发生了重写 这里子函数继承的虚函数表将会改变改写函数指向

多继承

多继承情况下,派生类中有多个虚函数表,虚函数的排列方式和继承的顺序一致。派生类重写函数将会覆盖所有虚函数表的同名内容,派生类自定义新的虚函数将会在第一个类的虚函数表的后面进行扩充

​ 假定 A1, A2, A3 B类多继承他们 (Cpp thunk技术)

所谓的thunk就是一段汇编代码,这段汇编代码可以以适当的偏移值来调整this指针以跳到对应的虚函数中去,并调用这个函数,也就是说当使用A1的指针指向B的对象,不需要发生偏移,而使用A2的指针指向B则需要进行偏移sizeof(A1)个字节。并跳转到A1中的函数来执行。这就是通过thunk的jmp指令跳转到这个函数。

7、构造函数和析构函数能否调用虚函数?

1.从语法上讲,调用完全没有问题。但是从效果上看,往往不能达到需要的目的。
2.假设一个基类A的构造函数中调用了一个虚函数。派生类B继承自A 。当用构造函数创建一个B类对象时,先调用基类A的构造函数,而此时编译器认为正在创建的对象的类型是A,所以虚函数是A类的虚函数。析构时同理,派生类成员先被析构了,当进入基类的析构函数时,就认为对象是基类对象调用基类的虚函数。

析构函数能抛出异常吗?

  1. 不能,也不应该抛出。
  2. 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
  3. 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

8、构造函数调用顺序,析构函数呢?

1.首先,基类的构造函数:如果有多个基类,先调用纵向上最上层基类构造函数,如果横向继承了多个类,调用顺序为派生表从左到右顺序。
2.其次,成员类对象的构造函数:如果类的变量中包含其他类(类的组合),需要在调用本类构造函数前先调用成员类对象的构造函数,调用顺序遵照在类中被声明的顺序
3.最后,派生类的构造函数。

4.析构函数与之相反。

构造函数和析构函数调用时机?

1.全局范围中的对象:构造函数在所有函数调用之前执行,在主函数执行完调用析构函数。
2.局部自动对象:建立对象时调用构造函数,函数结束时调用析构函数。
3.动态分配的对象:建立对象时调用构造函数,调用释放时调用析构函数。
4.静态局部变量对象:建立时调用一次构造函数,主函数结束时调用析构函数。

9、纯虚函数是什么?

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。带有纯虚函数的类为抽象类。析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。(必须抽象类)

虚函数与纯虚函数的区别?

  1. 定义一个函数为虚函数,不代表函数为不被实现的函数。
    定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
  2. 定义一个函数为纯虚函数,才代表函数没有被实现。
    定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
  3. 当一个成员函数被声明为虚函数之后,其派生类中同名函数自动成为虚函数,在派生类中重新定义此函数时要求函数名、返回值类型、参数个数和类型全部与基类函数相同。
  4. 纯虚函数只是相当于一个接口名,含有纯虚函数的类不能够实例化。

虚函数机制带来的开销有哪些?

主要是虚表的存储开销、函数通过指针使用带来的时间开销。

10、简单描述虚继承与虚基类?

定义:在C++中,在定义公共基类的派生类的时候,如果在继承方式前使用关键字virtual对继承方式限定,这样的继承方式就是虚拟继承,公共的基类成为虚基类。这样,在具有公共基类的、使用了虚拟继承方式的多个派生类的公共派生类中,该基类的成员就只有一份拷贝

为了避免多继承产生的二义性,在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

多继承有什么问题?

  1. 多继承比单继承复杂,引入了歧义的问题( 如果基类的成员函数名称相同,匹配度相同, 则会造成歧义)
  2. 菱形的多继承,导致虚继承的必要性;但虚继承在大小、速度、初始化/赋值的复杂性上有不小的代价,当虚基类中没有数据时还是比较合适的。

11、 拷贝构造函数中深拷贝和浅拷贝区别?

1.深拷贝时,当被拷贝对象存在动态分配的存储空间时,需要先动态申请一块存储空间,然后逐字节拷贝内容。
2.浅拷贝仅仅是拷贝指针字面值。
当使用浅拷贝时,如果原来的对象调用析构函数释放掉指针所指向的数据,则会产生空悬指针。因为所指向的内存空间已经被释放了。

12、 拷贝构造函数和赋值运算符重载的区别?

1.拷贝构造函数是函数,赋值运算符是运算符重载。

2.拷贝构造函数会生成新的类对象,赋值运算符不能。

3.拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果被赋值对象有内存分配则需要先把内存释放掉。

4.形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现”=”的地方都是使用赋值运算符,如下:

Student s;
Student s1 = 2;    // 调用拷贝构造函数
Student s2;
s2 = s;    // 赋值运算符操作(没有新对象产生)

5.类中有指针变量指向动态分配的内存资源时,要重写析构函数、拷贝构造函数和赋值运算符

13、拷贝构造函数在什么时候会被调用?

对于拷贝构造来说,归根结底,落脚点在构造函数上。所以调用拷贝构造的时候,一定是这个对象不存在的时候,如下面这句

假设Person是一个类。
Person p(q) //使用拷贝构造函数来创建实例p;
Person p = q; //使用拷贝构造函数来定义实例p时初始化p
f§ //p参数进行值传递时,会调用复制构造函数创建一个局部对象

14、volatile的作用

​ 用来修饰变量的,表明某个变量的值可能会随时被外部改变,因此这些变量的存取不能被缓存到寄存器,每次使用需要重新读取。
​ 假如有一个对象A里面有一个boolean变量a,值为true,现在有两个线程T1,T2访问变量a,T1把a改成了false后T2读取a,T2这时读到的值可能不是false,即T1修改a的这一操作,对T2是不可见的。发生的原因可能是,针对T2线程,为了提升性能,虚拟机把a变量置入了寄存器(即C语言中的寄存器变量),这样就会导致,无论T2读取多少次a,a的值始终为true,因为T2读取了寄存器而非内存中的值。声明了volatile或synchronized 后,就可以保证可见性,确保T2始终从内存中读取变量,T1始终在内存中修改变量。总结:防止脏读,增加内存屏障。

15、const 修饰符

​ 就 const 修饰符而言,它用来告诉编译器,被修饰的这些东西,具有“只读”的特点。在编译的过程中,一旦我们的代码试图去改变这些东西,编译器就应该给出错误提示。

​ 虽然const对于最终代码没有影响,但是**尽可能使用const,将帮助我们避免很多错误,提高程序正确率。**const 全局变量被用来替换一般常量宏定义。因为虽然 const 变量的值不能改变,但是依然是变量,使用时依然会进行类型检查,要比宏定义的直接替换方法更严格一些

注意点

​ 正因为 const 变量的值在给定以后不能改变,所以 const 变量必须被初始化。(如果不初始化,之后还怎么赋值呢?)如果我们不初始化 const 变量,编译时也会有错误提示。

可以使用static const成员函数吗?

不可以,const修饰成员函数其实修饰的是this指针,代表不可以通过函数来修改对象,但是static修饰的成员函数属于类,压根就不会有this指针。

类中const

  1. 对const成员变量的初始化,不能在变量声明的地方,必须在类的构造函数的初始化列表中完成,即使是在构造函数内部赋值也是不行的。
  2. 使用枚举类型
  3. 使用static const

const修饰指针(*)

int x= 1;
const int* a1 = &x;//[1] 指针指向的内容是常量 不得修改
int const * a2 = &x;//[2] 指针指向的内容是常量 不得修改
int* const a3 = &x;//[3] 指针本身是常量 指针所指向的内容是常量 不然报错
const int* const a = &x;//[4] 指针本身和指向的内容均为常量
  • 如果const位于星号*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
  • 如果const位于星号*的右侧,const就是修饰指针本身,即指针本身是常量。

const修饰reference(&)

int x = 1;
int const &a=x;//[1] 1, 2两种定义方式是等价的,
const int &a=x;//[2] 此时的引用a的值不能被更新。
//如:a++ 或则给a赋值这是错误的。
int &const a=x;//[3]这种方式定义是C、C++编译器未定义,虽然不会报错,但是该句效果和int &a一样。 

const修饰函数

const作用于函数还有一种情况是,在函数定义的最后面加上const修饰,比如:
A fun4() const;
其意义上是不能修改除了函数局部变量以外的所在类的任何变量。

const修饰函数参数 函数返回值

保护函数返回的指针指向的内容或则引用的对象不被修改。

保护原指针(引用)所指向的内容;

16、C/C++ 中指针和引用的区别?

  1. 指针有自己的一块空间,而引用只是一个别名;
  2. 使用sizeof看一个指针的大小是4/8(看OS),而引用则是被引用对象的大小;
  3. 指针可以被初始化为NULL,而引用必须被初始化必须是一个已有对象的引用
  4. 作为参数传递时,指针需要被解引用才可以对对象进行操作而直接对引用的修改都会改变引用所指向的对象
  5. 可以有const指针,但是没有const引用;
  6. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
  7. 指针可以有多级指针(**p),而引用至于一级;
  8. 指针和引用使用++运算符的意义不一样;
  9. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

17、 多态实现原理

编译器发现一个类中有虚函数,便会立即为此类生成虚函数表vtable。虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针 vptr指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行 vptr 与 vtable 的关联代码,将 vptr 指向对应的 vtable,将类与此类的 vtable 联系了起来。另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的 this 指针,这样依靠此 this 指针即可得到正确的 vtable。

如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理

简单描述多态?

C++ 多态有两种:静态多态、动态多态。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。

重载 重写 覆盖

重载:
函数内部参数加 默认传参(int a = 0) 不算重载
在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数不同
(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
不同的作用域里声明的函数也不算是重载。重载可以理解为一个类内部的函数重载,较好理解,此处不举例。

**Overwrite(重写):**是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关
键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有
virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

Override 覆盖
是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。

18、new/delete 和 malloc/free

使用new操作符来分配对象内存时会经历三个步骤:

  • 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
  • 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
  • 第三部:对象构造完成后,返回一个指向该对象的指针。

使用delete操作符来释放对象内存时会经历两个步骤:

  • 第一步:调用对象的析构函数。
  • 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。

细节

  1. 自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。

  2. C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

  3. 那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

  4. new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。

    在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型,为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数,set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。

  5. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。

区别

1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;

2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。

3、new不仅分配一段内存,而且会调用构造函数,malloc不会。

4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。

5、new是一个操作符可以重载,malloc是一个库函数。

6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。

  • malloc、calloc函数的实质体现在将一块可用的内存连接为一个链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块,然后将该内存块一分为二(一块与用户申请的大小一样,另一块就是剩下的字节)。接下来,将分配给用户的那块内存地址传给用户,调用free函数时,它将用户释放的内存块连接到空链上,最后空闲链表会被切成很多的小内存片段。
  • realloc是从堆空间上分配内存,当扩大一块内存空间时,realloc试图直接从现存的数据后面的哪些字节中获得附加的字节,如果能够满足需求,自然天下太平,如果后面的字节不够,那么就使用堆上第一个足够满足要求的自由空间块,现存的数据然后就被拷贝到新的位置上,而老块则放回堆空间,这句话传递的一个很重要的信息就是数据可能被移动。
  • clear allocation memory allocation real allocation

7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。

8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。

19、C++内存管理

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

  • 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
  • 数据段:存储程序中已初始化的全局变量和静态变量
  • bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
  • 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
  • 映射区:存储动态链接库以及调用mmap函数进行的文件映射
  • 栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

简单介绍内存池?

​ 内存池是一种内存分配方式。通常我们习惯直接使用new、malloc申请内存,这样做的缺点在于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前,预先申请分配一定数量、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。

简单描述内存泄漏?

内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。

内存中的堆与栈有什么区别?

​ 堆空间的内存是动态分配的,一般存放对象,并且需要手动释放内存。栈空间的内存是由系统自动分配,一般存放局部变量(A a),比如对象的地址等值,不需要程序员对这块内存进行管理,栈是运行时的单位,而堆是存储的单位。堆中的共享常量和缓存 。栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

20、对象存储空间

1.非静态成员的数据类型大小之和。
2.编译器加入的额外成员变量(如指向虚函数的指针)
3.为了边缘对齐优化加入的padding(系统填充)

21、四大智能指针

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。

  1. auto_ptr(c++98的方案,cpp11已经抛弃)采用所有权模式。

  2. unique_ptr(替换auto_ptr)unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。

  3. shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

    • shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
  4. weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

    • 注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

22、函数指针

函数指针是指向函数的指针变量。
函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

23、C++和C语言对比

设计思想上

C语言是面向过程的语言,C++则是面向对象

语法上

  1. 面向对象的三大特性 :

    1. 封装性:数据和代码捆绑在一起,避免外界干扰和不确定性访问。
    2. 继承性:让某种类型对象获得另一个类型对象的属性和方法。
    3. 多态性:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)
  2. C++相比C,增加多许多类型安全的功能,比如强制类型转换

  3. C++支持范式编程,比如模板类、函数模板等

说几个C++11的新特性?

1. auto类型推导:让编译器通过初值推断变量的类型(auto定义的变量必须要有初始值),编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响。
   2. ​    范围for循环:遍历给定序列的每个元素并对序列中的每个值执行某种操作。
      ​    lambda函数:用于定义并创建匿名的函数对象,以简化编程工作。
   3. Override:override关键字保证了派生类中声明重写的函数与基类虚函数有相同的签名,可避免一些拼写错误
   4. final 关键字:final限定某个类不能被继承或某个虚函数不能被重写。
   5. 空指针常量nullptr消除NULL的二义性问题。因为c++中NULL就是0,0 既可以表示整型,也可以表示一个空指针(void *)。nullptr有类型,且可以被隐式转换为指针类型。
   6. 线程支持、智能指针等

24、初始化成员列表

必须用初始化列表的:

  1. 类中有const成员。
  2. 类中有reference成员。
  3. 调用一个基类的构造函数,而该函数有一组参数。
  4. 调用一个数据成员对象的构造函数,而该函数有一组参数。

25、重载和函数模板的区别?

  1. 重载需要多个函数,这些函数彼此之间函数名相同,但参数列表中参数数量和类型不同。在区分各个重载函数时我们并不关心函数体。

  2. 模板函数是一个通用函数,函数的类型和形参不直接指定而用虚拟类型来代表。但只适用于参数个数相同类型不同的函数。

26、类模板是什么?

  1. 用于解决多个功能相同、数据类型不同的类需要重复定义的问题。

  2. 在建立类时候使用template及任意类型标识符T,之后在建立类对象时,会指定实际的类型,这样才会是一个实际的对象。

  3. 类模板是对一批仅数据成员类型不同的类的抽象,只要为这一批类创建一个类模板,即给出一套程序代码,就可以用来生成具体的类。

27、this

this指针是什么?

  1. this指针是类的指针,指向对象的首地址。

  2. this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this。所以this指针只能在成员函数中使用。在静态成员函数中不能用this。(静态成员是类的 不是对象的显然没有this)

  3. this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。

delete this

  1. 类的成员函数中能不能调用delete this?
    可以。假设一个成员函数release,调用了delete this。那么这个对象在调用release方法后,还可以进行其他操作,比如调用其他方法。前提是:被调用的方法不涉及这个对象的数据成员和虚函数,否则会出现不可预期的问题。

  2. 为什么是不可预期的问题?
    这涉及到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。但是其中的值是不确定的。

  3. 类的析构函数中调用delete this,会发生什么?
    导致栈溢出。delete的本质是为将被释放的内存调用一个或多个析构函数,然后,释放内存。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

在类的构造函数里面直接使用 memset(this,0,sizeof(*this)) 来初始化整个类里会发生什么?

将所有非静态成员变量置0。当有虚函数的时候,虚函数表指针vptr会被置成空。

28、 C++四大cast转换

  1. reinterpret_cast:可以用于任意类型的指针之间的转换,对转换的结果不做任何保证

  2. dynamic_cast:这种其实也是不被推荐使用的,更多使用static_cast,dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常

  3. const_cast:对于未定义const版本的成员函数,我们通常需要使用const_cast来去除const引用对象的const,完成函数调用。另外一种使用方式,结合static_cast,可以在非const版本的成员函数内添加const,调用完const版本的成员函数后,再使用const_cast去除const限定。

  4. static_cast:完成基础数据类型;同一个继承体系中类型的转换;任意类型与空指针类型void* 之间的转换。

  5. 为什么不使用C的强制转换?

    C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

29、为什么内联函数,构造函数,静态成员函数不能为virtual函数

  1. 内联函数
    内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数为虚函数。

  2. 构造函数
    构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在**构造函数执行时,对象尚未形成,**所以不能将构造函数定义为虚函数

  3. 静态成员函数
    静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别。

  4. 友元函数
    C++不支持友元函数的继承,对于没有继承性的函数没有虚函数

30、 inline 内联函数

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

编译器对 inline 函数的处理步骤

  1. 将 inline 函数体复制到 inline 函数调用点处;
  2. 为所用 inline 函数中的局部变量分配内存空间;
  3. 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
  4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。

优缺点

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  4. 内联函数在运行时可调试,而宏定义不可以。

缺点

  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

31、union 和 struct

  1. 在存储多个成员信息时,编译器会自动给struct每个成员分配存储空间,struct 可以存储多个成员信息,而Union每个成员会用同一个存储空间,只能存储一个成员的信息。

  2. 都是由多个不同的数据类型成员组成,但在任何同一时刻,Union只存放了一个被先选中的成员,而结构体的所有成员都存在。

  3. 对于Union的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对于struct 的不同成员赋值 是互不影响的。

union 特性

union也可以定义成员函数,包括构造函数和析构函数。与struct不同的是,它不能作为基类被继承。

union不能拥有静态数据成员或引用成员,因为静态数据成员实际上并不是共用体的数据成员,它无法和共用体的其它数据成员共享空间。对于引用变量,引用本质上是一个指针常量,它的值一旦初始化就不允许修改。如果共用体有引用成员,那么共用体对象一创建初始化后就无法修改,只能作为一个普通的引用使用,这就失去了共用体存在的意义。

32、字节对齐

不同硬件平台对存储空间的处理上存在很大的不同。某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放。例如Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常,因此在这种架构下编程必须保证字节对齐。

但最常见的情况是,如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的Intel处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。

常常结构体算出来大小有问题也是这个原因

2、 一些奇怪的知识点

1、在main执行之前执行的代码可能是什么?

全局对象的构造函数。

C++中,也利用全局变量和构造函数的特性,通过全局变量的构造函数在main()函数之前执行

2、如果同时定义了两个函数,一个带const,一个不带,会有问题吗?

参考回答: 不会,这相当于函数的重载。只要const对象调用const函数, 只有const的 只能调用const函数

class B{
    public:
    void f(int a) const { //只能放后面
        cout << "1" << a << endl;
    }
    void f(int a) {
        cout << "2" << a << endl;
    }
};
主函数:
    B a;
    int b = a.f(1);
    const B dd;
    dd.f(2);
输出 21\n12

3、请你说说你了解的RTTI

Run-Time Type identification

参考回答: 运行时类型检查,在C++层面主要体现在dynamic_cast和typeid,VS中虚函数表的-1位置存放了指向type_info的指针。对于存在虚函数的类型,typeid和dynamic_cast都会去查询type_info

https://blog.csdn.net/hihozoo/article/details/50856159

  • type_id
    • 当需要获得一个对象的类信息时,总是选择typeid 而不是 dynamic_cast,因为typeid检测一个类型或者非强制转换的值时,只耗费常数时间。而 dynamic_cast 需要在运行时,对对象进行参数推导。
    • typeid 只对多态类型(该类至少包含一个virtual成员函数)有效
  • dynamic_cast 小结:
    • 没有虚函数,不能动态转换。
    • 转换的依据是,type_info 信息。

4、如何定义一个只能在堆上生成对象的类

  1. 只能在堆上,析构函数设为protected。
    编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。

  2. 类中必须提供一个destroy函数,调用delete this,来进行内存空间的释放。类对象使用完成后,必须调用destroy函数。

  3. 用new建立对象,destroy销毁对象很奇怪。可以用一个静态成员函数将new封装起来。同时将构造函数设为protected。

5、如何定义一个只能在栈上生成对象的类?

只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。

6、静态链接与动态链接?

​ 静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时链接。
​ 静态链接浪费空间 ,这是由于多进程情况下,每个进程都要保存静态链接函数的副本。更新困难 ,当链接的众多目标文件中有一个改变后,整个程序都要重新链接才能使用新的版本。但是静态链接运行效率高。
​ 动态链接当系统多次使用同一个目标文件时,只需要加载一次即可,节省内存空间。程序升级变得容易,当升级某个共享模块时,只需要简单的将旧目标文件替换掉,程序下次运行时,新版目标文件会被自动装载到内存并链接起来,即完成升级。

7、为什么要有lambda表达式?

​ 用于定义并创建匿名的函数对象,以简化编程工作。

8、什么是std::move()以及什么时候使用它?

​ std::move()是C ++标准库中用于转换为右值引用的函数。当需要在其他地方“传输”对象的内容时,可以使用move,而无需复制。 使用std :: move,对象也可以在不进行复制(并节省大量时间)的情况下获取临时对象的内容。避免不必要的深拷贝。

9、参数的压栈顺序

C程序栈的内存生长方式是往低地址内存生长,这也说明为什么局部变量无法申请太大内存,因为栈内容有限。此外,这个例子说明,函数参数的入栈的顺序是从右往左的!。参数入栈顺序具体的还与编译器相关,涉及到C语言中调用约定所采用的方式:

C调用约定在返回前,要作一次堆栈平衡,也就是参数入栈了多少字节,就要弹出来多少字节.这样很安全.

有一点需要注意:stdcall调用约定如果采用了不定参数,即VARARG的话,则和C调用约定一样,要由调用者来作堆栈平衡.

3、STL

主要核心分为三大部分:容器(container)、算法(algorithm)和迭代器(iterator),另外还有容器适配器(container adaptor)和函数对象(functor)等其它标准组件。

  • 容器
    • 顺序容器
      • vector、deque、list、array、forward_list
    • 关联式容器
      • set、multiset、map、multimap // 红黑树实现
      • unordered_set、unordered_multiset、unordered_map、unordered_multimap // hash表实现
    • 容器适配器
      • stack、queue //默认用deque来实现数据结构的队列的功能
      • priority_queue //默认用vector来实现,其中保存的元素按照某种严格弱序进行排列,队首元素总是值最大的
  • 空间适配器allocator 迭代器 iterator
    • allocator模板类定义在头文件memory.h中
    • 迭代器虽然有头文件 但是还是靠各自的实现,毕竟底层不一样。

STL第二级配置器具体实现思想

  • 如果要分配的区块大于128bytes,则移交给第一级配置器处理。
  • 如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的自由链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。

4、 面试数据结构相关

红黑树定义和性质

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

  • 性质1:每个节点要么是黑色,要么是红色。
  • 性质2:根节点是黑色。
  • 性质3:每个叶子节点(NIL)是黑色。
  • 性质4:每个红色结点的两个子结点一定都是黑色。
  • 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

红黑树的查询性能略微逊色于AVL树,因为他比avl树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较,但是,红黑树在插入和删除上完爆avl树,avl树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于avl树为了维持平衡的开销要小得多

5、 C语言的一些底层实现

智能指针share_ptr

template <typename T>
class smart_ptrs {

public:
    smart_ptrs(T*); //用普通指针初始化智能指针
    smart_ptrs(smart_ptrs&); // 拷贝构造
    T* operator->(); //自定义指针运算符
    T& operator*(); //自定义解引用运算符
    smart_ptrs& operator=(smart_ptrs&); //自定义赋值运算符
    ~smart_ptrs(); //自定义析构函数
private:
    int *count; //引用计数
    T *p; //智能指针底层保管的指针
};
//构造函数
template <typename T>
	smart_ptrs<T>::smart_ptrs(T *p): count(new int(1)), p(p) {
}
template <typename T>
//对普通指针进行拷贝,同时引用计数器加1,因为需要对参数进行修改,所以没有将参数声明为const
smart_ptrs<T>::smart_ptrs(smart_ptrs &sp): count(&(++*sp.count)), p(sp.p)  {}
//指针运算符
template <typename T>
T* smart_ptrs<T>::operator->() {return p;}
//定义解引用运算符
template <typename T>
T& smart_ptrs<T>::operator*() {return *p;}
//定义赋值运算符,左边的指针计数减1,右边指针计数加1,当左边指针计数为0时,释放内存:
template <typename T>
smart_ptrs<T>& smart_ptrs<T>::operator=(smart_ptrs& sp) {
    ++*sp.count;
    if (--*count == 0) { //自我赋值同样能保持正确
        delete count;
        delete p;
    }
    this->p = sp.p;
    this->count = sp.count;
    return *this;
}
// 定义析构函数:
template <typename T>
smart_ptrs<T>::~smart_ptrs() {
    if (--*count == 0) {
        delete count;
        delete p;
    }
}