概述
https://blog.csdn.net/shanghairuoxiao/article/details/72876248
C和C++语言基础
参考书籍:《C++ primer》,《effective C++》,《STL源码解析》,《深度搜索C++对象模型》
extern关键字作用
extern声明变量在在外部定义?
extern修饰函数?
extern C的作用?用法?
static关键字作用
static修饰局部变量?
static全局变量?(限定变量在一个编译单元内,一个编译单元就是指一个cpp和它包含的头文件,这个回答可以结合编译需要经历的几个过程来答)
static修饰普通函数?
static修饰成员变量?
static修饰成员函数?
volatile是干啥的
访问寄存器要比访问内存要块,因此CPU会优先访问该数据在寄存器中的存储结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile,告诉CPU每次都从内存去读取数据。
一个参数可以即是const又是volatile的吗?可以,一个例子是只读状态寄存器,是volatile是因为它可能被意想不到的被改变,是const告诉程序不应该试图去修改他。
说说const的作用,越多越好
const修饰全局变量;
const修饰局部变量;
const修饰指针,const int *;
const修饰指针指向的对象, int * const;
const修饰引用做形参;
const修饰成员变量,必须在构造函数列表中初始化;
const修饰成员函数,说明该函数不应该修改非静态成员,但是这并不是十分可靠的,指针所指的非成员对象值可能会被改变
new与malloc区别
new分配内存按照数据类型进行分配,malloc分配内存按照大小分配;
new不仅分配一段内存,而且会调用构造函数,但是malloc则不会。new的实现原理?但是还需要注意的是,之前看到过一个题说int* p = new int与int* p = new int()的区别,因为int属于C++内置对象,不会默认初始化,必须显示调用默认构造函数,但是对于自定义对象都会默认调用构造函数初始化。翻阅资料后,在C++11中两者没有区别了,自己测试的结构也都是为0;
new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化;
new是一个操作符可以重载,malloc是一个库函数;
new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会;
malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作;
new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。因此对于new,正确的姿势是采用try…catch语法,而malloc则应该判断指针的返回值。为了兼容很多c程序员的习惯,C++也可以采用new nothrow的方法禁止抛出异常而返回NULL;
new和new[]的区别,new[]一次分配所有内存,多次调用构造函数,分别搭配使用delete和delete[],同理,delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n;
如果不够可以继续谈new和malloc的实现,空闲链表,分配方法(首次适配原则,最佳适配原则,最差适配原则,快速适配原则)。delete和free的实现原理,free为什么直到销毁多大的空间?
C++多态性与虚函数表
C++多态的实现?
多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。
动态多态实现有几个条件:
(1) 虚函数;
(2) 一个基类的指针或引用指向派生类的对象;
基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。
每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。
虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。
虚函数的作用?
虚函数用于实现多态,这点大家都能答上来
但是虚函数在设计上还具有封装和抽象的作用。比如抽象工厂模式。
动态绑定是如何实现的?
第一个问题中基本回答了,主要都是结合虚函数表来答就行。
静态多态和动态多态。静态多态是指通过模板技术或者函数重载技术实现的多态,其在编译器确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。
虚函数表
虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?
编译器为每一个类维护一个虚函数表,每个对象的首地址保存着该虚函数表的指针,同一个类的不同对象实际上指向同一张虚函数表。
纯虚函数如何定义,为什么对于存在虚函数的类中析构函数要定义成虚函数
为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,如果析构函数没有定义为析构函数,则会调用基类的析构函数,显然只能销毁部分数据。如果要调用对象的析构函数,就需要将该对象的析构函数定义为虚函数,销毁时通过虚函数表找到对应的析构函数。
//纯虚函数定义
virtual ~myClass() = 0;
1
2
析构函数能抛出异常吗
答案肯定是不能。
C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。
(1) 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
(2) 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
构造函数和析构函数中调用虚函数吗?
指针和引用的区别
指针保存的是所指对象的地址,引用是所指对象的别名,指针需要通过解引用间接访问,而引用是直接访问;
指针可以改变地址,从而改变所指的对象,而引用必须从一而终;
引用在定义的时候必须初始化,而指针则不需要;
指针有指向常量的指针和指针常量,而引用没有常量引用;
指针更灵活,用的好威力无比,用的不好处处是坑,而引用用起来则安全多了,但是比较死板。
指针与数组千丝万缕的联系
一个一维int数组的数组名实际上是一个int* const 类型;
一个二维int数组的数组名实际上是一个int (*const p)[n];
数组名做参数会退化为指针,除了sizeof
智能指针是怎么实现的?什么时候改变引用计数?
构造函数中计数初始化为1;
拷贝构造函数中计数值加1;
赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一;
析构函数中引用计数减一;
在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。
share_prt与weak_ptr的区别?
//share_ptr可能出现循环引用,从而导致内存泄露
class A
{
public:
share_ptr<B> p;
};
class B
{
public:
share_ptr<A> p;
}
int main()
{
while(true)
{
share_prt<A> pa(new A()); //pa的引用计数初始化为1
share_prt<B> pb(new B()); //pb的引用计数初始化为1
pa->p = pb; //pb的引用计数变为2
pb->p = pa; //pa的引用计数变为2
}
//假设pa先离开,引用计数减一变为1,不为0因此不会调用class A的析构函数,因此其成员p也不会被析构,pb的引用计数仍然为2;
//同理pb离开的时候,引用计数也不能减到0
return 0;
}
/*
** weak_ptr是一种弱引用指针,其存在不会影响引用计数,从而解决循环引用的问题
*/
C++四种类型转换:static_cast, dynamic_cast, const_cast, reinterpret_cast
const_cast用于将const变量转为非const
static_cast用的最多,对于各种隐式转换,非const转const,void*转指针等, static_cast能用于多态想上转化,如果向下转能成功但是不安全,结果未知;
dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
reinterpret_cast几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
内存对齐的原则
从0位置开始存储;
变量存储的起始位置是该变量大小的整数倍;
结构体总的大小是其最大元素的整数倍,不足的后面要补齐;
结构体中包含结构体,从结构体中最大元素的整数倍开始存;
如果加入pragma pack(n) ,取n和变量自身大小较小的一个。
内联函数有什么优点?内联函数与宏定义的区别?
宏定义在预编译的时候就会进行宏替换;
内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。
内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。
使用宏定义函数要特别注意给所有单元都加上括号,#define MUL(a, b) a * b,这很危险,正确写法:#define MUL(a, b) ((a) * (b))
C++内存管理
C++内存分为那几块?(堆区,栈区,常量区,静态和全局区)
每块存储哪些变量?
学会迁移,可以说到malloc,从malloc说到操作系统的内存管理,说道内核态和用户态,然后就什么高端内存,slab层,伙伴算法,VMA可以巴拉巴拉了,接着可以迁移到fork()。
STL里的内存池实现
STL内存分配分为一级分配器和二级分配器,一级分配器就是采用malloc分配内存,二级分配器采用内存池。
二级分配器设计的非常巧妙,分别给8k,16k,…, 128k等比较小的内存片都维持一个空闲链表,每个链表的头节点由一个数组来维护。需要分配内存时从合适大小的链表中取一块下来。假设需要分配一块10K的内存,那么就找到最小的大于等于10k的块,也就是16K,从16K的空闲链表里取出一个用于分配。释放该块内存时,将内存节点归还给链表。
如果要分配的内存大于128K则直接调用一级分配器。
为了节省维持链表的开销,采用了一个union结构体,分配器使用union里的next指针来指向下一个节点,而用户则使用union的空指针来表示该节点的地址。
STL里set和map是基于什么实现的。红黑树的特点?
set和map都是基于红黑树实现的。
红黑树是一种平衡二叉查找树,与AVL树的区别是什么?AVL树是完全平衡的,红黑树基本上是平衡的。
为什么选用红黑数呢?因为红黑数是平衡二叉树,其插入和删除的效率都是N(logN),与AVL相比红黑数插入和删除最多只需要3次旋转,而AVL树为了维持其完全平衡性,在坏的情况下要旋转的次数太多。
红黑树的定义:
(1) 节点是红色或者黑色;
(2) 父节点是红色的话,子节点就不能为红色;
(3) 从根节点到每个页子节点路径上黑色节点的数量相同;
(4) 根是黑色的,NULL节点被认为是黑色的。
STL里的其他数据结构和算法实现也要清楚
这个问题,把STL源码剖析好好看看,不仅面试不慌,自己对STL的使用也会上升一个层次。
必须在构造函数初始化式里进行初始化的数据成员有哪些
(1) 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
(2) 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
(3) 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
模板特化
(1) 模板特化分为全特化和偏特化,模板特化的目的就是对于某一种变量类型具有不同的实现,因此需要特化版本。例如,在STL里迭代器为了适应原生指针就将原生指针进行特化。
定位内存泄露
(1)在windows平台下通过CRT中的库函数进行检测;
(2)在可能泄漏的调用前后生成块的快照,比较前后的状态,定位泄漏的位置
(3)Linux下通过工具valgrind检测
手写strcpy
char* strcpy(char* dst, const char* src)
{
assert(dst);
assert(src);
char* ret = dst;
while((*dst++ = *src++) != '