C++八股文




⼀、作者介绍
⼤家好,我是⼩贺,北交⼤师兄,⽬前就职于宇宙中⼼五道⼝。
简单介绍下这份 C++ ⼋股⽂ PDF
背景:最近秋招找⼯作,很多读者都来问我学习和⾯试的资料,因此整理了下常⻅的
C++ ⾼频⾯试知识点。分享给你。
这本电⼦书总共有 20W 多字 +100 多张图,⽂字都是⼀个字⼀个字敲出来的,
很多图⽚都是⼩贺⼀个点⼀条线画出来的,⾮常的不容易。
这本书整理的知识主要是⾯向程序员的,因为⼩贺本身也是个程序员,所以涉及到的
知识主要是关于程序员⽇常⼯作或者⾯试的 C++ 知识。
适合有⼀点 C++ 基础,但是⼜不怎么扎实,或者知识点串不起来的同学,说⽩了,
这本电⼦书就是为了拯救⾯试突击的你,适合⾯试突击 C++ 后台岗位知识时拿来
看,不敢说 100 % 涵盖了⾯试问题,但是⾄少 90% 是有的。
这⾥允许⼩贺⾃卖⾃夸⼀下:其中加餐篇-图解 STL 源码(第⼗三章-第⼗七章)⼩贺
强烈推荐⼤家看⼀看,虽然只有五篇,但是耗费了⼩贺⼏个⽉的时间输出,真的是
STL 源码剖析的精华中的精华了
举个栗⼦,随便拿出⼀张图,你都能看的出⼩贺的⽤⼼,是真的是图⽂并茂!就是
为了初学者能明明⽩⽩的搞定它!
那这本书要怎么阅读呢?
这本电⼦书不是教科书,肯定没有教科书的知识点全⾯,是⼀本为了准备⾯试的辅助
突击复习资料。
如果你还不是到即将⾯试的时间点,建议多花时间系统学习底层知识,同时扩充知识
⾯,才是⻓久之计。这个时间你需要的是加速度,⽽不是速度。
阅读的顺序可以不⽤从头读到尾,你可以根据你想要了解的知识点,去看哪个章节的
⽂章就好,可以随意阅读任何章节的⽂章。
下⾯这张思维导图是整个电⼦书的⽬录结构:
⼆、勘误事宜
⼆、勘误事宜
⼩贺整理时间仓促,难免会有很多错别字,所以在学习这份电⼦书的同学, 如果你发现有任
何错误或者疑惑的地⽅,欢迎你通过下⽅的邮箱反馈给⼩贺,⼩贺会逐个修正,然后发布新版
本的 C++ PDF,⼀起迭代出更好的 C++ PDF !
勘误邮箱:1952281585@qq.com
三、C++ 语⾔基础篇
1、说⼀下你理解的 C++ 中的四种智能指针
⾯试官你好,⾸先,说⼀下为什么要使⽤智能指针:智能指针其作⽤是管理⼀个指针,避免咋
们程序员申请的空间在函数结束时忘记释放,造成内存泄漏这种情况滴发⽣。
然后使⽤智能指针可以很⼤程度上的避免这个问题,因为智能指针就是⼀个类,当超出了类的
作⽤域是,类会⾃动调⽤析构函数,析构函数会⾃动释放资源。所以智能指针的作⽤原理就是
在函数结束时⾃动释放内存空间,不需要⼿动释放内存空间。
常⽤接⼝
T* get();
T& operator*();
T* operator->();
T& operator=(const T& val);
T* release();
void reset (T* ptr = nullptr);
T 是模板参数, 也就是传⼊的类型;
get() ⽤来获取 auto_ptr 封装在内部的指针, 也就是获取原⽣指针;
operator() 重载 , operator->() 重载了->, operator=()重载了=;
realease() 将 auto_ptr 封装在内部的指针置为 nullptr, 但并不会破坏指针所指向的内容, 函
数返回的是内部指针置空之前的值;
直接释放封装的内部指针所指向的内存, 如果指定了 ptr 的值, 则将内部指针初始化为该值
(否则将其设置为nullptr;
下⾯分别说⼀下哪四种:
1、auto_ptr(C++98 的⽅案,C11 已抛弃)采⽤所有权模式。
auto_ptr<std::string> p1 (new string ("hello"));
auto_ptr<std::string> p2;
p2 = p1; //auto_ptr 不会报错.
此时不会报错,p2 剥夺了 p1 的所有权,但是当程序运⾏时访问 p1 将会报错。所以 auto_ptr
的缺点是:存在潜在的内存崩溃问题!
2、unique_ptr(替换 auto_ptr )
unique_ptr 实现独占式拥有或严格拥有概念,保证同⼀时间内只有⼀个智能指针可以指向该对
象。它对于避免资源泄露特别有⽤。
采⽤所有权模式,还是上⾯那个例⼦
unique_ptr<string> p3 (new string (auto));//#4
unique_ptr<string> p4;//#5
p4 = p3;//此时会报错
编译器认为 p4=p3 ⾮法,避免了 p3 不再指向有效数据的问题。
因此,unique_ptr ⽐ auto_ptr 更安全。
3、shared_ptr(共享型,强引⽤)
shared_ptr 实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在
“最后⼀个引⽤被销毁”时候释放。从名字 share 就可以看出了资源可以被多个指针共享,它使
⽤计数机制来表明资源被⼏个指针共享。
可以通过成员函数 use_count() 来查看资源的所有者个数,除了可以通过 new 来构造,还可
以通过传⼊auto_ptr, unique_ptr,weak_ptr 来构造。当我们调⽤ release() 时,当前指针会释放
资源所有权,计数减⼀。当计数等于 0 时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性 (auto_ptr 是独占的),在使⽤引⽤
计数的机制上提供了可以共享所有权的智能指针。
4、weak_ptr(弱引⽤)
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。
当两个智能指针都是 shared_ptr 类型的时候,析构时两个资源引⽤计数会减⼀,但是两者引
⽤计数还是为 1,导致跳出函数时资源没有被释放(的析构函数没有被调⽤),解决办法:把
其中⼀个改为weak_ptr就可以。
2、C++ 中内存分配情况
栈:由编译器管理分配和回收,存放局部变量和函数参数。
堆:由程序员管理,需要⼿动 new malloc delete free 进⾏分配和回收,空间较⼤,但可能会
出现内存泄漏和空闲碎⽚的情况。
全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量
和静态变量。
常量存储区:存储常量,⼀般不允许修改。
代码区:存放程序的⼆进制代码。
3、C++ 中的指针参数传递和引⽤参数传递
指针参数传递本质上是值传递,它所传递的是⼀个地址值。值传递过程中,被调函数的形式参
数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参
值,从⽽形成了实参的⼀个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作
都是作为局部变量进⾏的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会
变)。
引⽤参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时
存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理
成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的
本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
引⽤传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的⼀个局部变量,但是任何
对于引⽤参数的处理都会通过⼀个间接寻址的⽅式操作到主调函数中的相关变量。⽽对于指针
传递的参数,如果改变被调函数中的指针地址,它将应⽤不到主调函数的相关变量。如果想通
过指针参数传递来改变主调函数中的相关变量(地址),那就得使⽤指向指针的指针或者指针
引⽤。
从编译的⻆度来讲,程序在编译时分别将指针和引⽤添加到符号表上,符号表中记录的是变量
名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,⽽引⽤在符号
表上对应的地址值为引⽤对象的地址值(与实参名字不同,地址相同)。符号表⽣成之后就不
会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),⽽引⽤对象则不能修
改。
4、C++ 中 const 和 static 关键字(定义,⽤途)
static 作⽤:控制变量的存储⽅式和可⻅性。
作⽤⼀:修饰局部变量:⼀般情况下,对于局部变量在程序中是存放在栈区的,并且局部的⽣
命周期在包含语句块执⾏结束时便结束了。但是如果⽤ static 关键字修饰的话,该变量便会存
放在静态数据区,其⽣命周期会⼀直延续到整个程序执⾏结束。但是要注意的是,虽然⽤
static 对局部变量进⾏修饰之后,其⽣命周期以及存储空间发⽣了变化,但其作⽤域并没有改
变,作⽤域还是限制在其语句块。
作⽤⼆:修饰全部变量:对于⼀个全局变量,它既可以在本⽂件中被访问到,也可以在同⼀个
⼯程中其它源⽂件被访问(添加 extern进⾏声明即可)。⽤ static 对全局变量进⾏修饰改变了其
作⽤域范围,由原来的整个⼯程可⻅变成了本⽂件可⻅。
作⽤三:修饰函数:⽤ static 修饰函数,情况和修饰全局变量类似,也是改变了函数的作⽤
域。
作⽤四:修饰类:如果 C++ 中对类中的某个函数⽤ static 修饰,则表示该函数属于⼀个类⽽
不是属于此类的任何特定对象;如果对类中的某个变量进⾏ static 修饰,则表示该变量以及所
有的对象所有,存储空间中只存在⼀个副本,可以通过;类和对象去调⽤。
(补充:静态⾮常量数据成员,其只能在类外定义和初始化,在类内仅是声明⽽已。)
作⽤五:类成员/类函数声明 static
函数体内 static 变量的作⽤范围为该函数体,不同于 auto 变量,该变量的内存只被分配
⼀次,因此其值在下次调⽤时仍维持上次的值;
在模块内的 static 全局变量可以被模块内所⽤函数访问,但不能被模块外其它函数访问;
在模块内的 static 函数只可被这⼀模块内的其它函数调⽤,这个函数的使⽤范围被限制在
声明它的模块内;
在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有⼀份拷⻉;
在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因⽽只能访问类
的 static 成员变量。
static 类对象必须要在类外进⾏初始化,static 修饰的变量先于对象存在,所以 static 修
饰的变量要在类外初始化;
由于 static 修饰的类成员属于类,不属于对象,因此 static 类成员函数是没有 this 指针,
this 指针是指向本对象的指针,正因为没有 this 指针,所以 static 类成员函数不能访问⾮
static 的类成员,只能访问 static修饰的类成员;
static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以加上 virtual
没有任何实际意义;静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个
vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系,
this->vptr->ctable->virtual function。
const 关键字:含义及实现机制
const 修饰基本类型数据类型:基本数据类型,修饰符 const 可以⽤在类型说明符前,也可以
⽤在类型说明符后,其结果是⼀样的。在使⽤这些常量的时候,只要不改变这些常量的值即
可。
const 修饰指针变量和引⽤变量:如果 const 位于⼩星星的左侧,则 const 就是⽤来修饰指针
所指向的变量,即指针指向为常量;如果 const 位于⼩星星的右侧,则 const 就是修饰指针
本身,即指针本身是常量。
const 应⽤到函数中:作为参数的 const 修饰符:调⽤函数的时候,⽤相应的变量初始化
const 常量,则在函数体中,按照 const 所修饰的部分进⾏常量化,保护了原对象的属性。
[注意]:参数 const 通常⽤于参数为指针或引⽤的情况; 作为函数返回值的 const 修饰符:声
明了返回值后,const 按照"修饰原则"进⾏修饰,起到相应的保护作⽤。
const 在类中的⽤法:const 成员变量,只在某个对象⽣命周期内是常量,⽽对于整个类⽽⾔
是可以改变的。因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不
能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const
数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进⾏。
const 成员函数:const 成员函数的主要⽬的是防⽌成员函数修改对象的内容。要注意,const
关键字和 static 关键字对于成员函数来说是不能同时使⽤的,因为 static 关键字修饰静态成员
函数不含有 this 指针,即不能实例化,const 成员函数⼜必须具体到某⼀个函数。
const 修饰类对象,定义常量对象:常量对象只能调⽤常量函数,别的成员函数都不能调⽤。
补充:const 成员函数中如果实在想修改某个变量,可以使⽤ mutable 进⾏修饰。成员变量中
如果想建⽴在整个类中都恒定的常量,应该⽤类中的枚举常量来实现或者 static const。
C ++ 中的 const类成员函数(⽤法和意义)
常量对象可以调⽤类中的 const 成员函数,但不能调⽤⾮ const 成员函数; (原因:对象调
⽤成员函数时,在形参列表的最前⾯加⼀个形参 this,但这是隐式的。this 指针是默认指向调
⽤函数的当前对象的,所以,很⾃然,this 是⼀个常量指针 test * const,因为不可以修改
this 指针代表的地址。但当成员函数的参数列表(即⼩括号)后加了 const 关键字(void
print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test * const,
即不可以通过 this 指针来改变指向对象的值。
⾮常量对象可以调⽤类中的 const 成员函数,也可以调⽤⾮ const 成员函数。
5、C 和 C++ 区别 (函数/类/struct/class)
⾸先,C 和 C++ 在基本语句上没有过⼤的区别。
C++ 有新增的语法和关键字,语法的区别有头⽂件的不同和命名空间的不同,C++ 允许我们
⾃⼰定义⾃⼰的空间,C 中不可以。关键字⽅⾯⽐如 C++ 与 C 动态管理内存的⽅式不同,
C++ 中在 malloc 和 free 的基础上增加了 new 和 delete,⽽且 C++ 中在指针的基础上增加
了引⽤的概念,关键字例如 C++中还增加了 auto,explicit 体现显示和隐式转换上的概念要
求,还有 dynamic_cast 增加类型安全⽅⾯的内容。
函数⽅⾯ C++ 中有重载和虚函数的概念:C++ ⽀持函数重载⽽ C 不⽀持,是因为 C++ 函数
的名字修饰与 C 不同,C++ 函数名字的修饰会将参数加在后⾯,例如,int func(int,double)经
过名字修饰之后会变成_func_int_double,⽽ C 中则会变成 _func,所以 C++ 中会⽀持不同
参数调⽤不同函数。
C++ 还有虚函数概念,⽤以实现多态。
类⽅⾯,C 的 struct 和 C++ 的类也有很⼤不同:C++ 中的 struct 不仅可以有成员变量还可
以成员函数,⽽且对于 struct 增加了权限访问的概念,struct 的默认成员访问权限和默认继
承权限都是 public,C++ 中除了 struct 还有 class 表示类,struct 和 class 还有⼀点不同在
于 class 的默认成员访问权限和默认继承权限都是 private。
C++ 中增加了模板还重⽤代码,提供了更加强⼤的 STL 标准库。
最后补充⼀点就是 C 是⼀种结构化的语⾔,重点在于算法和数据结构。C 程序的设计⾸先考
虑的是如何通过⼀个代码,⼀个过程对输⼊进⾏运算处理输出。⽽ C++ ⾸先考虑的是如何构
造⼀个对象模型,让这个模型能够契合与之对应的问题领域,这样就能通过获取对象的状态信
息得到输出。
C 的 struct 更适合看成是⼀个数据结构的实现体,⽽ C++ 的 class 更适合看成是⼀
个对象的实现体。
6、C++ 和 Java 区别(语⾔特性,垃圾回收,应⽤场景等)
指针:Java 语⾔让程序员没法找到指针来直接访问内存,没有指针的概念,并有内存的⾃动
管理功能,从⽽有效的防⽌了 C++ 语⾔中的指针操作失误的影响。但并⾮ Java 中没有指
针,Java 虚拟机内部中还是⽤了指针,保证了 Java 程序的安全。
多重继承:C++ ⽀持多重继承但 Java 不⽀持,但⽀持⼀个类继承多个接⼝,实现 C++ 中多
重继承的功能,⼜避免了 C++ 的多重继承带来的不便。
数据类型和类:Java 是完全⾯向对象的语⾔,所有的函数和变量必须是类的⼀部分。除了基
本数据类型之外,其余的都作为类对象,对象将数据和⽅法结合起来,把它们封装在类中,这
样每个对象都可以实现⾃⼰的特点和⾏为。Java 中取消了 C++ 中的 struct 和 union 。
⾃动内存管理:Java 程序中所有对象都是⽤ new 操作符建⽴在内存堆栈上,Java ⾃动进⾏
⽆⽤内存回收操作,不需要程序员进⾏⼿动删除。⽽ C++ 中必须由程序员释放内存资源,增
加了程序设计者的负担。Java 中当⼀个对象不再被⽤到时, ⽆⽤内存回收器将给他们加上标
签。Java ⾥⽆⽤内存回收程序是以线程⽅式在后台运⾏的,利⽤空闲时间⼯作来删除。
Java 不⽀持操作符重载。操作符重载被认为是 C++ 的突出特性。
Java 不⽀持预处理功能。C++ 在编译过程中都有⼀个预编译阶段,Java 没有预处理器,但它
提供了 import 与 C++ 预处理器具有类似功能。
类型转换:C++ 中有数据类型隐含转换的机制,Java 中需要限时强制类型转换。
字符串:C++中字符串是以 Null 终⽌符代表字符串的结束,⽽ Java 的字符串 是⽤类对象
(string 和 stringBuffer)来实现的。
Java 中不提供 goto 语句,虽然指定 goto 作为关键字,但不⽀持它的使⽤,使程序简洁易
读。
Java 的异常机制⽤于捕获例外事件,增强系统容错能⼒。
7、说⼀下 C++ ⾥是怎么定义常量的?常量存放在内存的哪个位置?
对于局部常量,存放在栈区;
对于全局常量,编译期⼀般不分配内存,放在符号表中以提⾼访问效率;
字⾯值常量,⽐如字符串,放在常量区。
8、C++ 中重载和重写,重定义的区别
重载
翻译⾃ overload,是指同⼀可访问区内被声明的⼏个具有不同参数列表的同名函数,依赖于
C++函数名字的修饰会将参数加在后⾯,可以是参数类型,个数,顺序的不同。根据参数列表
决定调⽤哪个函数,重载不关⼼函数的返回类型。
重写
翻译⾃ override,派⽣类中重新定义⽗类中除了函数体外完全相同的虚函数,注意被重写的函
数不能是 static 的,⼀定要是虚函数,且其他⼀定要完全相同。要注意,重写和被重写的函数
是在不同的类当中的,重写函数的访问修饰符是可以不同的,尽管 virtual 中是 private 的,派
⽣类中重写可以改为 public。
重定义(隐藏)
派⽣类重新定义⽗类中相同名字的⾮ virtual 函数,参数列表
和返回类型都可以不同,即⽗类中除了定义成 virtual 且完全相同的同名函数才
不会被派⽣类中的同名函数所隐藏(重定义)。
9、介绍 C++ 所有的构造函数
类的对象被创建时,编译系统为对象分配内存空间,并⾃动调⽤构造函数,由构造函数完成成
员的初始化⼯作。
即构造函数的作⽤:初始化对象的数据成员。
⽆参数构造函数:即默认构造函数,如果没有明确写出⽆参数构造函数,编译器会⾃动⽣成默
认的⽆参数构造函数,函数为空,什么也不做,如果不想使⽤⾃动⽣成的⽆参构造函数,必需
要⾃⼰显示写出⼀个⽆参构造函数。
⼀般构造函数:也称重载构造函数,⼀般构造函数可以有各种参数形式,⼀个类可以有多个⼀
般构造函数,前提是参数的个数或者类型不同,创建对象时根据传⼊参数不同调⽤不同的构造
函数。
拷⻉构造函数:拷⻉构造函数的函数参数为对象本身的引⽤,⽤于根据⼀个已存在的对象复制
出⼀个新的该类的对象,⼀般在函数中会将已存在的对象的数据成员的值⼀⼀复制到新创建的
对象中。如果没有显示的写拷⻉构造函数,则系统会默认创建⼀个拷⻉构造函数,但当类中有
指针成员时,最好不要使⽤编译器提供的默认的拷⻉构造函数,最好⾃⼰定义并且在函数中执
⾏深拷⻉。
类型转换构造函数:根据⼀个指定类型的对象创建⼀个本类的对象,也可以算是⼀般构造函数
的⼀种,这⾥提出来,是想说有的时候不允许默认转换的话,要记得将其声明为 explict 的,
来阻⽌⼀些隐式转换的发⽣。
赋值运算符的重载:注意,这个类似拷⻉构造函数,将=右边的本类对象的值复制给=左边的
对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符
的重载,系统也会⽣成默认的赋值运算符,做⼀些基本的拷⻉⼯作。
这⾥区分
A a1, A a2; a1 = a2;//调⽤赋值运算符
A a3 = a1;//调⽤拷⻉构造函数,因为进⾏的是初始化⼯作,a3 并未存在
10、C++ 的四种强制转换
C++ 的四种强制转换包括:static_cast, dynamic_cast, const_cast, reinterpret_cast
static_cast:明确指出类型转换,⼀般建议将隐式转换都替换成显示转换,因为没有动态
类型检查,上⾏转换(派⽣类->基类)安全,下⾏转换(基类->派⽣类) 不安全,所以
主要执⾏⾮多态的转换操作;
dynamic_cast:专⻔⽤于派⽣类之间的转换,type-id 必须是类指针,类引⽤或 void*,对
于下⾏转换是安全的,当类型不⼀致时,转换过来的是空指针,⽽static_cast,当类型不
⼀致时,转换过来的事错误意义的指针,可能造成⾮法访问等问题。
const_cast:专⻔⽤于 const 属性的转换,去除 const 性质,或增加 const 性质, 是四
个转换符中唯⼀⼀个可以操作常量的转换符。
reinterpret_cast:不到万不得已,不要使⽤这个转换符,⾼危操作。使⽤特点: 从底层
对数据进⾏重新解释,依赖具体的平台,可移植性差; 可以将整形转 换为指针,也可以
把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换。
11、指针和引⽤的区别
指针和引⽤都是⼀种内存地址的概念,区别呢,指针是⼀个实体,引⽤只是⼀个别名。
在程序编译的时候,将指针和引⽤添加到符号表中。
指针它指向⼀块内存,指针的内容是所指向的内存的地址,在编译的时候,则是将“指针变量
名-指针变量的地址”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷⻉和赋
值,有 const 和⾮ const 区别,甚⾄可以为空,sizeof 指针得到的是指针类型的⼤⼩。
⽽对于引⽤来说,它只是⼀块内存的别名,在添加到符号表的时候,是将"引⽤变量名-引⽤对
象的地址"添加到符号表中,符号表⼀经完成不能改变,所以引⽤必须⽽且只能在定义时被绑
定到⼀块内存上,后续不能更改,也不能为空,也没有 const 和⾮ const 区别。
sizeof 引⽤得到代表对象的⼤⼩。⽽ sizeof 指针得到的是指针本身的⼤⼩。另外在参数传递
中,指针需要被解引⽤后才可以对对象进⾏操作,⽽直接对引⽤进⾏的修改会直接作⽤到引⽤
对象上。
作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引⽤的实质是传地址,
传递的是变量的地址。
12、 野(wild)指针与悬空(dangling)指针有什么区别?如何避免?
野指针(wild pointer):就是没有被初始化过的指针。⽤ gcc -Wall 编译, 会出现 used
uninitialized 警告。
悬空指针:是指针最初指向的内存已经被释放了的⼀种指针。
⽆论是野指针还是悬空指针,都是指向⽆效内存区域(这⾥的⽆效指的是"不安全不可控")的指
针。 访问"不安全可控"(invalid)的内存区域将导致"Undefined Behavior"。
如何避免使⽤野指针?在平时的编码中,养成在定义指针后且在使⽤之前完成初始化的习惯或
者使⽤智能指针。
13、说⼀下 const 修饰指针如何区分?
下⾯都是合法的声明,但是含义⼤不同:
const int * p1; //指向整形常量 的指针,它指向的值不能修改
int * const p2; //指向整形的常量指针 ,它不能在指向别的变量,但指向(变量)的
值可以修改。
const int *const p3; //指向整形常量 的 常量指针 。它既不能再指向别的常量,指向
的值也不能修改。
理解这些声明的技巧在于,查看关键字const右边来确定什么被声明为常量 ,如果该关键字的
右边是类型,则值是常量;如果关键字的右边是指针变量,则指针本身是常量。
14、简单说⼀下函数指针
从定义和⽤途两⽅⾯来说⼀下⾃⼰的理解:
⾸先是定义:函数指针是指向函数的指针变量。函数指针本身⾸先是⼀个指针变量,该指针变
量指向⼀个具体的函数。这正如⽤指针变量可指向整型变量、字符型、数组⼀样,这⾥是指向
函数。
在编译时,每⼀个函数都有⼀个⼊⼝地址,该⼊⼝地址就是函数指针所指向的地址。有了指向
函数的指针变量后,可⽤该指针变量调⽤函数,就如同⽤指针变量可引⽤其他类型变量⼀样,
在这些概念上是⼤体⼀致的。
其次是⽤途:调⽤函数和做函数的参数,⽐如回调函数。
示例:
char * fun(char * p) {…} // 函数fun
char * (*pf)(char * p); // 函数指针pf
pf = fun; // 函数指针pf指向函数fun
pf(p); // 通过函数指针pf调⽤函数fun
15、堆和栈区别
栈
由编译器进⾏管理,在需要时由编译器⾃动分配空间,在不需要时候⾃动回收空间,⼀般保存
的是局部变量和函数参数等。
连续的内存空间,在函数调⽤的时候,⾸先⼊栈的主函数的下⼀条可执⾏指令的地址,然后是
函数的各个参数。
⼤多数编译器中,参数是从右向左⼊栈(原因在于采⽤这种顺序,是为了让程序员在使⽤
C/C++的“函数参数⻓度可变”这个特性时更⽅便。如果是从左向右压栈,第⼀个参数(即描述
可变参数表各变量类型的那个参数)将被放在栈底,由于可变参的函数第⼀步就需要解析可变
参数表的各参数类型,即第⼀步就需要得到上述参数,因此,将它放在栈底是很不⽅便的。)
本次函数调⽤结束时,局部变量先出栈,然后是参数,最后是栈顶指针最开始存放的地址,程
序由该点继续运⾏,不会产⽣碎⽚。
栈是⾼地址向低地址扩展,栈低⾼地址,空间较⼩。
堆
由程序员管理,需要⼿动 new malloc delete free 进⾏分配和回收,如果不进⾏回收的话,会
造成内存泄漏的问题。
不连续的空间,实际上系统中有⼀个空闲链表,当有程序申请的时候,系统遍历空闲链表找到
第⼀个⼤于等于申请⼤⼩的空间分配给程序,⼀般在分配程序的时候,也会空间头部写⼊内存
⼤⼩,⽅便 delete 回收空间⼤⼩。当然如果有剩余的,也会将剩余的插⼊到空闲链表中,这
也是产⽣内存碎⽚的原因。
堆是低地址向⾼地址扩展,空间交⼤,较为灵活。
16、函数传递参数的⼏种⽅式
值传递:形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
指针传递:也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就
相当于对实参本身进⾏操作。
引⽤传递:实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作
可以直接映射到外部的实参上⾯。
17、new / delete ,malloc / free 区别
都可以⽤来在堆上分配和回收空间。new /delete 是操作符,malloc/free 是库函数。
执⾏ new 实际上执⾏两个过程:1.分配未初始化的内存空间(malloc);2.使⽤对象的构造
函数对空间进⾏初始化;返回空间的⾸地址。如果在第⼀步分配空间中出现问题,则抛出
std::bad_alloc 异常,或被某个设定的异常处理函数捕获处理;如果在第⼆步构造对象时出现
异常,则⾃动调⽤ delete 释放内存。
执⾏ delete 实际上也有两个过程:1. 使⽤析构函数对对象进⾏析构;2.回收内存空间
(free)。
以上也可以看出 new 和 malloc 的区别,new 得到的是经过初始化的空间,⽽ malloc 得到的
是未初始化的空间。所以 new 是 new ⼀个类型,⽽ malloc 则是malloc ⼀个字节⻓度的空
间。delete 和 free 同理,delete 不仅释放空间还析构对象,delete ⼀个类型,free ⼀个字节
⻓度的空间。
为什么有了 malloc/free 还需要 new/delete?因为对于⾮内部数据类型⽽⾔,光⽤ malloc
/free ⽆法满⾜动态对象的要求。对象在创建的同时需要⾃动执⾏构造函数,对象在消亡以前
要⾃动执⾏析构函数。由于 mallo/free 是库函数⽽不是运算符,不在编译器控制权限之内,
不能够把执⾏的构造函数和析构函数的任务强加于 malloc/free,所以有了 new/delete 操作
符。
18、volatile 和 extern 关键字
volatile 三个特性
易变性:在汇编层⾯反映出来,就是两条语句,下⼀条语句不会直接使⽤上⼀条语句对应的
volatile 变量的寄存器内容,⽽是重新从内存中读取。
不可优化性:volatile 告诉编译器,不要对我这个变量进⾏各种激进的优化,甚⾄将变量直接
消除,保证程序员写在代码中的指令,⼀定会被执⾏。
顺序性:能够保证 volatile 变量之间的顺序性,编译器不会进⾏乱序优化。
extern
在 C 语⾔中,修饰符 extern ⽤在变量或者函数的声明前,⽤来说明 “此变量/函数是在别处定
义的,要在此处引⽤”。
注意 extern 声明的位置对其作⽤域也有关系,如果是在 main 函数中进⾏声明的,则只能在
main 函数中调⽤,在其它函数中不能调⽤。其实要调⽤其它⽂件中的函数和变量,只需把该
⽂件⽤ #include 包含进来即可,为啥要⽤ extern?因为⽤ extern 会加速程序的编译过程,这
样能节省时间。
在 C++ 中 extern 还有另外⼀种作⽤,⽤于指示 C 或者 C++函数的调⽤规范。⽐如在 C+
+ 中调⽤ C 库函数,就需要在 C++ 程序中⽤ extern “C” 声明要引⽤的函数。这是给链接器
⽤的,告诉链接器在链接的时候⽤C 函数规范来链接。主要原因是 C++ 和 C 程序编译完成
后在⽬标代码中命名规则不同,⽤此来解决名字匹配的问题。
19、define 和 const 区别(编译阶段、安全性、内存占⽤等)
对于 define 来说,宏定义实际上是在预编译阶段进⾏处理,没有类型,也就没有类型检查,
仅仅做的是遇到宏定义进⾏字符串的展开,遇到多少次就展开多少次,⽽且这个简单的展开过
程中,很容易出现边界效应,达不到预期的效果。因为 define 宏定义仅仅是展开,因此运⾏
时系统并不为宏定义分配内存,但是从汇编 的⻆度来讲,define 却以⽴即数的⽅式保留了多
份数据的拷⻉。
对于 const 来说,const 是在编译期间进⾏处理的,const 有类型,也有类型检查,程序运⾏
时系统会为 const 常量分配内存,⽽且从汇编的⻆度讲,const 常量在出现的地⽅保留的是真
正数据的内存地址,只保留了⼀份数据的拷⻉,省去了不必要的内存空间。⽽且,有时编译器
不会为普通的 const 常量分配内存,⽽是直接将 const 常量添加到符号表中,省去了读取和
写⼊内存的操作,效率更⾼。
20、计算下⾯⼏个类的⼤⼩
class A{}; sizeof(A) = 1; //空类在实例化时得到⼀个独⼀⽆⼆的地址,所以为 1.
class A{virtual Fun(){} }; sizeof(A) = 4(32bit)/8(64bit) //当 C++ 类中有虚
函数的时候,会有⼀个指向虚函数表的指针(vptr)
class A{static int a; }; sizeof(A) = 1;
class A{int a; }; sizeof(A) = 4;
class A{static int a; int b; }; sizeof(A) = 4;
21、⾯向对象的三⼤特性,并举例说明
C++ ⾯向对象的三⼤特征是:封装、继承、多态。
所谓封装
就是把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让信任的类或者对象操
作,对不可信的进⾏信息隐藏。⼀个类就是⼀个封装了数据以及操作这些数据的代码的逻辑实
体。在⼀个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种⽅式,
对象对内部数据提供了不同级别的保护,以防⽌程序中⽆关的部分意外的改变或错误的使⽤了
对象的私有部分。
所谓继承
是指可以让某个类型的对象获得另⼀个类型的对象的属性的⽅法。它⽀持按级分类的概念。继
承是指这样⼀种能⼒:它可以使⽤现有类的所有功能,并在⽆需重新编写原来的类的情况下对
这些功能进⾏扩展。通过继承创建的新类称为“⼦类”或者“派⽣类”,被继承的类称为“基类”、
“⽗类”或“超类”。继承的过程,就是从⼀般到特殊的过程。要实现继承,可以通过“继承”和“组
合”来实现。
继承概念的实现⽅式有两类:
实现继承:实现继承是指直接使⽤基类的属性和⽅法⽽⽆需额外编码的能⼒。
接⼝继承:接⼝继承是指仅使⽤属性和⽅法的名称、但是⼦类必需提供实现的能⼒。
所谓多态
就是向不同的对象发送同⼀个消息,不同对象在接收时会产⽣不同的⾏为(即⽅法)。即⼀个
接⼝,可以实现多种⽅法。
多态与⾮多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调⽤,在编译器编
译期间就可以确定函数的调⽤地址,并产⽣代码,则是静态的,即地址早绑定。⽽如果函数调
⽤的地址不能在编译器期间确定,需要在运⾏时才确定,这就属于晚绑定。
22、多态的实现
多态其实⼀般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器
为函数⽣成符号表时的不同规则,重载只是⼀种语⾔特性,与多态⽆关,与⾯向对象也⽆关,
但这⼜是 C++中增加的新规则,所以也算属于 C++,所以如果⾮要说重载算是多态的⼀种,
那就可以说:多态可以分为静态多态和动态多态。
静态多态其实就是重载,因为静态多态是指在编译时期就决定了调⽤哪个函数,根据参数列表
来决定;
动态多态是指通过⼦类重写⽗类的虚函数来实现的,因为是在运⾏期间决定调⽤的函数,所以
称为动态多态,
⼀般情况下我们不区分这两个时所说的多态就是指动态多态。
动态多态的实现与虚函数表,虚函数指针相关。
扩展:⼦类是否要重写⽗类的虚函数?⼦类继承⽗类时, ⽗类的纯虚函数必须重写,否则⼦
类也是⼀个虚类不可实例化。 定义纯虚函数是为了实现⼀个接⼝,起到⼀个规范的作⽤,规
范继承这个类的程序员必须实现这个函数。
23、虚函数相关(虚函数表,虚函数指针),虚函数的实现原理
⾸先我们来说⼀下,C++中多态的表象,在基类的函数前加上 virtual 关键字,在派⽣类中重
写该函数,运⾏时将会根据对象的实际类型来调⽤相应的函数。如果对象类型是派⽣类,就调
⽤派⽣类的函数,如果是基类,就调⽤基类的函数。
实际上,当⼀个类中包含虚函数时,编译器会为该类⽣成⼀个虚函数表,保存该类中虚函数的
地址,同样,派⽣类继承基类,派⽣类中⾃然⼀定有虚函数,所以编译器也会为派⽣类⽣成⾃
⼰的虚函数表。当我们定义⼀个派⽣类对象时,编译器检测该类型有虚函数,所以为这个派⽣
类对象⽣成⼀个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数
中完成的。
后续如果有⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象
的虚函数表指针去寻找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多
态。
补充:如果基类中没有定义成 virtual,那么进⾏ Base B; Derived D; Base *p = D; p-
>function(); 这种情况下调⽤的则是 Base 中的 function()。因为基类和派⽣类中都没有虚函数
的定义,那么编译器就会认为不⽤留给动态多态的机会,就事先进⾏函数地址的绑定(早绑
定),详述过程就是,定义了⼀个派⽣类对象,⾸先要构造基类的空间,然后构造派⽣类的⾃
身内容,形成⼀个派⽣类对象,那么在进⾏类型转换时,直接截取基类的部分的内存,编译器
认为类型就是基类,那么(函数符号表[不同于虚函数表的另⼀个表]中)绑定的函数地址也
就是基类中函数的地址,所以执⾏的是基类的函数。
24、编译器处理虚函数表应该如何处理
对于派⽣类来说,编译器建⽴虚函数表的过程其实⼀共是三个步骤:
拷⻉基类的虚函数表,如果是多继承,就拷⻉每个有虚函数基类的虚函数表
当然还有⼀个基类的虚函数表和派⽣类⾃身的虚函数表共⽤了⼀个虚函数表,也称为某个
基类为派⽣类的主基类
查看派⽣类中是否有重写基类中的虚函数, 如果有,就替换成已经重写的虚函数地址;
查看派⽣类是否有⾃身的虚函数,如果有,就追加⾃身的虚函数到⾃身的虚函数表中。
Derived *pd = new D(); B *pb = pd; C *pc = pd; 其中 pb,pd,pc 的指针位置是不同的,要
注意的是派⽣类的⾃身的内容要追加在主基类的内存块后。
25、析构函数⼀般写成虚函数的原因
直观的讲:是为了降低内存泄漏的可能性。举例来说就是,⼀个基类的指针指向⼀个派⽣类的
对象,在使⽤完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那 么编译器根据指
针类型就会认为当前对象的类型是基类,调⽤基类的析构函数 (该对象的析构函数的函数地
址早就被绑定为基类的析构函数),仅执⾏基类的析构,派⽣类的⾃身内容将⽆法被析构,造
成内存泄漏。
如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执⾏派⽣类的析构函
数,再执⾏基类的析构函数,成功释放内存。
26、构造函数为什么⼀般不定义为虚函数
虚函数调⽤只需要知道“部分的”信息,即只需要知道函数接⼝,⽽不需要知道对象的具体
类型。但是,我们要创建⼀个对象的话,是需要知道对象的完整信息的。特别是,需要知
道要创建对象的确切类型,因此,构造函数不应该被定义成虚函数;
⽽且从⽬前编译器实现虚函数进⾏多态的⽅式来看,虚函数的调⽤是通过实例化之后对象
的虚函数表指针来找到虚函数的地址进⾏调⽤的,如果说构造函数是虚的,那么虚函数表
指针则是不存在的,⽆法找到对应的虚函数表来调⽤虚函数,那么这个调⽤实际上也是违
反了先实例化后调⽤的准则。
27、构造函数或析构函数中调⽤虚函数会怎样
实际上是不应该在构造函数或析构函数中调⽤虚函数的,因为这样的调⽤其实并不会带来所想
要的效果。
举例来说就是,有⼀个动物的基类,基类中定义了⼀个动物本身⾏为的虚函数 action_type(),
在基类的构造函数中调⽤了这个虚函数。
派⽣类中重写了这个虚函数,我们期望着根据对象的真实类型不同,⽽调⽤各⾃实现的虚函
数,但实际上当我们创建⼀个派⽣类对象时,⾸先会创建派⽣类的基类部分,执⾏基类的构造
函数,此时,派⽣类的⾃身部分还没有被初始化,对于这种还没有初始化的东⻄,C++选择当
它们还不存在作为⼀种安全的⽅法。
也就是说构造派⽣类的基类部分是,编译器会认为这就是⼀个基类类型的对象,然后调⽤基类
类型中的虚函数实现,并没有按照我们想要的⽅式进⾏。即对象在派⽣类构造函数执⾏前并不
会成为⼀个派⽣类对象。
在析构函数中也是同理,派⽣类执⾏了析构函数后,派⽣类的⾃身成员呈现未定义的状态,那
么在执⾏基类的析构函数中是不可能调⽤到派⽣类重写的⽅法的。所以说,我们不应该在构在
函数或析构函数中调⽤虚函数,就算调⽤⼀般也不会达到我们想要的结果。
28、析构函数的作⽤,如何起作⽤?
构造函数只是起初始化值的作⽤,但实例化⼀个对象的时候,可以通过实例去传递参数,从主
函数传递到其他的函数⾥⾯,这样就使其他的函数⾥⾯有值了。规则,只要你⼀实例化对象,
系统⾃动回调⽤⼀个构造函数,就是你不写,编译器也⾃动调⽤⼀次。
析构函数与构造函数的作⽤相反,⽤于撤销对象的⼀些特殊任务处理,可以是释放对象分配的
内存空间;特点:析构函数与构造函数同名,但该函数前⾯加~。
析构函数没有参数,也没有返回值,⽽且不能重载,在⼀个类中只能有⼀个析构函数。 当撤
销对象时,编译器也会⾃动调⽤析构函数。 每⼀个类必须有⼀个析构函数,⽤户可以⾃定义
析构函数,也可以是编译器⾃动⽣成默认的析构函数。⼀般析构函数定义为类的公有成员。
29、构造函数的执⾏顺序?析构函数的执⾏顺序?
构造函数顺序
基类构造函数。如果有多个基类,则构造函数的调⽤顺序是某类在类派⽣表中出现的顺
序,⽽不是它们在成员初始化表中的顺序。
成员类对象构造函数。如果有多个成员类对象则构造函数的调⽤顺序是对象在类中被声明
的顺序,⽽不是它们出现在成员初始化表中的顺序。
派⽣类构造函数。
析构函数顺序
调⽤派⽣类的析构函数;
调⽤成员类对象的析构函数;
调⽤基类的析构函数。
30、纯虚函数 (应⽤于接⼝继承和实现继承)
实际上,纯虚函数的出现就是为了让继承可以出现多种情况:
有时我们希望派⽣类只继承成员函数的接⼝
有时我们⼜希望派⽣类既继承成员函数的接⼝,⼜继承成员函数的实现,⽽且可以在派⽣
类中可以重写成员函数以实现多态
有的时候我们⼜希望派⽣类在继承成员函数接⼝和实现的情况下,不能重写缺省的实现。
其实,声明⼀个纯虚函数的⽬的就是为了让派⽣类只继承函数的接⼝,⽽且派⽣类中必需提供
⼀个这个纯虚函数的实现,否则含有纯虚函数的类将是抽象类,不能进⾏实例化。
对于纯虚函数来说,我们其实是可以给它提供实现代码的,但是由于抽象类不能实例化,调⽤
这个实现的唯⼀⽅式是在派⽣类对象中指出其 class 名称来调⽤。
31、静态绑定和动态绑定的介绍
说起静态绑定和动态绑定,我们⾸先要知道静态类型和动态类型,静态类型就是它在程序中被
声明时所采⽤的类型,在编译期间确定。动态类型则是指“⽬前所指对象的实际类型”,在运⾏
期间确定。
静态绑定,⼜名早绑定,绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发
⽣在编译期间。
动态绑定,⼜名晚绑定,绑定的是动态类型,所对应的函数或属性依赖于动态类型,发⽣在运
⾏期间。
⽐如说,virtual 函数是动态绑定的,⾮虚函数是静态绑定的,缺省参数值也是静态绑定的。这
⾥呢,就需要注意,我们不应该重新定义继承⽽来的缺省参数,因为即使我们重定义了,也不
会起到效果。因为⼀个基类的指针指向⼀个派⽣类对象,在派⽣类的对象中针对虚函数的参数
缺省值进⾏了重定义, 但是缺省参数值是静态绑定的,静态绑定绑定的是静态类型相关的内
容,所以会出现⼀种派⽣类的虚函数实现⽅式结合了基类的缺省参数值的调⽤效果,这个与所
期望的效果不同。
32、深拷⻉和浅拷⻉的区别(举例说明深拷⻉的安全性)
当出现类的等号赋值时,会调⽤拷⻉函数,在未定义显示拷⻉构造函数的情况下, 系统会调
⽤默认的拷⻉函数-即浅拷⻉,它能够完成成员的⼀⼀复制。当数据成员中没有指针时,浅拷
⻉是可⾏的。
但当数据成员中有指针时,如果采⽤简单的浅拷⻉,则两类中的两个指针指向同⼀个地址,当
对象快要结束时,会调⽤两次析构函数,⽽导致指野指针的问题。
所以,这时必需采⽤深拷⻉。深拷⻉与浅拷⻉之间的区别就在于深拷⻉会在堆内存中另外申请
空间来存储数据,从⽽也就解决来野指针的问题。简⽽⾔之,当数据成员中有指针时,必需要
⽤深拷⻉更加安全。
33、什么情况下会调⽤拷⻉构造函数(三种情况)
类的对象需要拷⻉时,拷⻉构造函数将会被调⽤,以下的情况都会调⽤拷⻉构造函数:
⼀个对象以值传递的⽅式传⼊函数体,需要拷⻉构造函数创建⼀个临时对象压⼊到栈空间
中。
⼀个对象以值传递的⽅式从函数返回,需要执⾏拷⻉构造函数创建⼀个临时对象作为返回
值。
⼀个对象需要通过另外⼀个对象进⾏初始化。
34、为什么拷⻉构造函数必需时引⽤传递,不能是值传递?
为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构
造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那
么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造
函数,这就是⼀个⽆限递归。
35、结构体内存对⻬⽅式和为什么要进⾏内存对⻬?
⾸先我们来说⼀下结构体中内存对⻬的规则:
对于结构体中的各个成员,第⼀个成员位于偏移为 0 的位置,以后的每个数据成员的偏移
量必须是 min(#pragma pack() 制定的数,数据成员本身⻓度) 的