1. this指针干什么用的?
一个class可以定义很多对象,每个对象都有自己的成员变量以及成员方法,this指针在编译后作为成员方法的第一个参数
在成员方法里访问成员变量的时候,this指针用于区分访问哪个对象的成员变量
2. new和delete,什么时候用new[]申请,可以用delete释放?
new和delete是重载的运算符,实际上调用的是operator new 和operator delete
如果是自定义类型,而且提供了析构函数,用new[] 申请的内存空间就一定要用delete[] 释放,因为new[] 除了开辟用户需要使用的内存空间,还多开辟了4字节存放对象的个数。因为编译器知道自己总共分配了多少空间,记录对象的个数后,可以在内存中准确划分出每个对象的首地址。
如果没有提供析构函数, new[] 开辟空间的起始地址和返回给用户的地址相同,也就是没有存放对象个数的4字节空间。
胡乱猜测:没有提供析构函数,delete不执行析构,分配多少空间就回收多少空间,不用4字节记录对象个数。
可参考:C++运算符重载
3. static关键字的作用(从elf结构、链接过程回答)?
从面向过程角度来说:
符号表中被static修饰的全局变量、函数就从global变成了local,也即从整个项目可见变成当前文件可见
static修饰的局部变量放在数据段(.data或.bss),由于是在数据段,程序一开始就要开辟空间,在执行到相应语句的时候初始化变量。局部变量本身不产生符号,通过ebp-偏移量 进行访问,但是被static修饰后由于放在数据段,这时候就需要产生符号。
从面向对象角度来说:
static可以修饰成员变量,修饰成员方法,不再产生this指针,变成归类所有,通过类作用域调用
4. C++的继承有什么好处?
继承处于类和类之间的关系,好处如下:
- 代码复用
- 通过继承,在基类里面给所有派生类保留统一的纯虚函数接口,派生类进行重写。通过基类指针或者引用指向派生类对象实现多态(指针指向谁就访问谁的方法),也更符合软件开发的 “开闭原则”(只要是派生类,基类指针都可指向,不用修改)
可参考:C++的继承和多态
5. 讲一下C++的多态
分为静多态(编译时期)和动多态(运行时期)。
静多态的表现形式包括函数重载和类模板,在编译时期就要确定下来。
继承结构中,Base类指针(引用)指向Derive类对象,通过该指针(引用)调用同名覆盖方法(虚函数),该指针指向哪个Derive对象的覆盖对象,就调用哪个Derive类方法
可参考:C++的继承和多态
6. 空间配置器allocator?
allocator主要是给容器使用,作用是将开辟空间和构造对象分开,以及将回收空间和析构对象分开。 对于容器而言,如果首先就要开辟空间,而不是放入对象,这个时候需要用到空间配置器。容器删除元素的时候,只是析构对象,而不是像delete一样析构对象且回收空间。
7. vector和list的区别?
vector底层是数组,内存连续。插入删除为O(n),随机访问O(1),尾部的插入删除都是O(1)
list底层是双向链表,内存不连续。中间增加删除O(n),首尾增加删除O(1),访问O(n)
可参考:C++STL总结
8. map和multimap的区别
map是一个映射表[key-value],不允许key重复,底层实现是红黑树(一种二叉排序树),主要是对key进行查找比较,快速找到key,时间复杂度为O(log2n)
multimap允许key重复,底层实现是红黑树
红黑树的5个性质:
- 每个节点要么是黑色,要么是红色。
- 根节点是黑色。
- 叶子节点是黑色。
- 每个红色节点的两个子节点一定都是黑色, 不能有两个红色节点相连。
- 任意一节点到每个叶子节点的路径都包含数量相同的黑色结点。
性质5又可以推出: 如果一个节点存在黑色子节点,那么该结点肯定有两个子节点,不然走另一条路就会少一层黑色结点。
红黑树插入有3种情况(最多旋转2次),删除有4种情况(最多旋转3次)
9. C++如何防止内存泄漏?智能指针详述
内存泄漏:分配的堆内存(没有名字,只能用指针指向)没有释放
智能指针体现在把裸指针进行了面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源。所以,智能指针一般都是定义在栈上的
可参考:C++智能指针
10. C++如何调用C语言函数接口?
由于C和C++生成符号的方式不同,C和C++直接的API接口无法直接调用,C语言的函数声明必须在extern "C" 里
extern "C" {
int function_C();
}
可参考:C和C++的区别
11. 那些情况下可能出现访问越界
- 数组访问越界
- STL容器访问越界
- 字符串访问越界
- 字符串没有添加
\0 ,导致访问越界 - 使用类型强转,使得大类型(派生类)的指针指向小类型(基类)的对象,指针解引用直接越界
12. C++中类的初始化列表
可以指定数据成员的初始化方式,尤其是指定成员对象的初始化(成员变量的初始化和定义的顺序有关,和初始化列表的顺序无关)
13. C和C++的区别以及内存分布
引用、函数重载、运算符重载、new/delete/malloc/free、const、inline、模板、OOP语言可使用设计模式、STL、异常处理、智能指针
可参考:C和C++的区别
14. int* const p 和const int* p 的区别
int* const p 表示不可改变p指针的指向,const int* p 表示不可修改p指针指向的值
可参考:C和C++的区别
15. malloc和new的区别
-
malloc是按字节开辟空间的,new开辟内存时需要指定类型(new int()),malloc开辟内存返回的都是void*,而new返回的是对应类型的指针 -
malloc负责开辟空间,new不仅有malloc的功能,还可以进行数据初始化,比如:new int(10)。new有开辟空间和构造的功能。 -
malloc开辟内存失败返回nullptr,而new则会抛出bad_alloc异常 -
我们调用new实际上是调用的operator new
可参考:C++运算符重载
16. map和set的实现原理
set是集合,里面只存放key。map是映射表,存储[key,value]键值对。
底层数据结构都是红黑树,然后需要答一些红黑树的性质,相关算法
17. shared_ptr的引用计数在哪存放
存放在堆内存
可参考:C++智能指针的enable_shared_from_this和shared_from_this机制
18. STL各个容器底层数据结构?
顺序容器: vector(数组)、deque(二维数组)、list(双向循环链表) 容器适配器: stack(deque)、queue(deque)、priority_queue(vector) 关联容器: set系列(红黑树)、map系列(红黑树)
可参考:C++ STL总结
19. vector里empty()和size()的区别?
_NODISCARD bool empty() const noexcept {
auto& _My_data = _Mypair._Myval2;
return _My_data._Myfirst == _My_data._Mylast;
}
_NODISCARD size_type size() const noexcept {
auto& _My_data = _Mypair._Myval2;
return static_cast<size_type>(_My_data._Mylast - _My_data._Myfirst);
}
20. 迭代器的实效问题
迭代器不允许一边读一边修改
当容器调用erase方法后,当前位置到容器末尾元素的所有迭代器全部失效(首元素到插入点的迭代器有效)
当容器调用insert方法后,当前位置到容器末尾元素的所有迭代器全部失效(首元素到插入点的迭代器有效)
insert来说,如果引起容器内存扩容,原来容器的所有的迭代器就全部失效
当通过迭代器更新容器元素以后,要及时对迭代器进行更新,insert/erase都会返回新位置的迭代器
可参考:C++运算符重载
21. C++中struct和class的区别
- 定义类的时候,struct的访问限定默认是public,class的访问限定默认是private
- 继承时,
class Derive:Base 表示私有继承,struct Derive:Base 表示公有继承 - C语言里struct定义的空结构体是0字节,C++里struct定义的空类是1字节
- 为了兼容C语言里的struct,C++里struct定义的类对象初始化也可以用C语言中结构体的初始化方式,如:
Data data = {10,20} - C++支持
template<class T> ,不支持template<struct T>
22. 初始化的全局变量和未初始化的全局变量的区别
.data :存放初始化且初始化值不为0的数据 .bss :存放未初始化和初始化为0的数据
23. 堆和栈的区别
- 堆内存的大小 >> 栈内存的大小
- 堆内存需要手动开辟、释放,栈区开辟函数栈帧,自动清退
- 堆内存从低地址到高地址分配,栈从高地址(栈底)到低地址(栈顶)
24. 构造函数和析构函数可不可以是虚函数
构造函数不能是虚函数,析构函数可以是虚函数
构造函数不能实现成virtual,构造函数构造完成,对象才产生,才有vfptr,才能访问虚函数表。
如果构造一个派生类对象,派生类构造需要先构造基类,基类的构造是一个虚函数,此时发生动态绑定,需要访问派生类的前4个字节vfptr,然后这个时候派生类对象还没产生,访问出错。
基类的指针指向堆上的派生类对象时,delete ptr_base 调用析构函数的时候,由于必须要调用到派生类对象的析构函数,所以必须是动态绑定,此时需要把Base的析构函数实现成virtual。
若是静态绑定,则直接根据指针的类型,调用析构函数,无法调用派生类的析构函数。
可参考:C++中的继承和多态总结
25. 构造函数和析构函数是否可以抛异常
主要考虑内存泄漏、资源释放的问题
构造函数不能抛出异常,若抛出异常则表示对象创建失败,不能调用析构函数,资源无法释放
析构函数只能在释放完所有资源后抛出异常
26. 宏和内联函数的区别
#define 在预编译时期处理,inline 在编译时期处理#define 就是字符串替换,release版本中inline 在函数调用点把函数代码展开,节省函数的调用开销#define 可以定义很多,比如常量、代码段、函数等,inline 只能用于修饰函数#define 不能调试,inline 在Debug版本下可以调试,这时产生函数调用开销
27. 局部变量存放在哪
局部变量通过 栈底指针ebp 偏移访问,存放在栈区
局部变量不产生符号,不属于数据,属于指令的一部分(若是在VS Debug版本下查看反汇编会发现依然有符号,这是VS优化后展示给用户的结果,实际上应该是ebp-偏移量 )
28. 拷贝构造函数为什么传引用&,不传值
class Test{
Test(const Test t){
...
}
};
int main(){
Test t1;
Test t2(t1);
return 0;
}
这时用t1拷贝构造t2,如果传值,则需要用t1初始化形参t,这个时候也需要调用Test的拷贝构造函数(t1.Test(t) ),而调用拷贝构造函数的时候,仍然需要实参初始化形参,再次调用拷贝构造函数,陷入了死循环。
而实际上,编译器也会检查,直接发生编译错误,无法运行
29. 内联函数和普通函数的区别(反汇编角度)
inline函数
- 编译期间在代码调用的地方展开(release版本),有逻辑性的进行文本替换,因此不产生函数符号
- 能够调试,在debug版本(需要调试)inline函数和普通的函数表现一致,只有在release版本才会真正在调用点展开
- 因为需要在编译期间展开,而编译期间针对的是单文件。所以inline函数的作用域只在本文件,debug版本生成local符号
- 有类型检测,安全
- 类体内实现的成员方法直接是inline
核心: 函数调用开销
开辟栈帧 栈帧清退
普通函数
- 不展开
- 能调试
- 作用域在全局,生成global符号
- 有类型检测,安全
可参考:C和C++的区别
30. 如何实现一个不可继承的类
派生类构造的过程:先是执行基类构造函数,然后执行派生类构造函数。
我们将基类的构造函数私有化,此时基类的构造函数对派生类是不可见的,所以无法继承。
31. 什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪?
virtual void fun() = 0;
用纯虚函数定义的类是抽象类,无法实例化对象,可以定义指针和引用
纯虚函数定义在基类里面,基类不代表实体,它的主要任务之一就是 给所有的派生类保留统一的纯虚函数接口,让派生类进行重写。重写以后就 可以使用多态,基类指针指向派生类对象,指向哪个对象就调用哪个对象的方法
虚函数表是在编译阶段产生,运行时加载到.rodata 段
可参考:C++继承和多态总结
32. 说一下C++中的const
编译时期值替换
const定义的叫常量,它的编译方式是把出现常量名字的地方,用常量的值进行替换
const int a = 10;
int* p = (int*)&a;
*p = 100;
cout<<a<<" "<<*p<<endl;
常变量
若用立即数初始化,则编译阶段可以直接用立即数进行替换。 若用变量初始化,则退化为和C语言中一模一样的常变量(只是不能作为左值,编译方式同普通变量)
定义常成员方法 this指针由Test* this 变成const Test* this ,普通对象和常对象都可以调用
可参考:C和C++的区别
33. const与static的区别
面向过程
const修饰:全局变量、局部变量、形参变量 static修饰:全局变量、局部变量、函数(本文件可见)
面向过程
const 常方法(普通对象和常对象都可以调用,只能对成员进行读不能写)、常成员变量(不能被修改,必须在构造函数的初始化列表初始化)
static 静态方法:该方法没有this指针,不依赖于对象,通过类作用域访问 静态成员变量:静态成员变量属于类,不属于某个具体的对象,实现多个对象共享数据的目的。static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。即没有在类外初始化的 static 成员变量不能使用。
34. 四种强制类型转换
const_cast :去掉常量属性
static_cast :类型安全的转换
reinterpret_cast :C风格的类型转换,没有安全可言,随意转换
dynamic_cast :支持RTTI识别的类型转换。基类指针转成相应的派生类对象指针的时候,dynamic_cast会识别该指针是否能够进行转换
35. deque底层数据结构
deque是双端队列容器,底层数据是动态扩容的二维数组。一维数组从2开始,以2倍的方式进行扩容,每次扩容后原来第二维的数组从新的第一维数组的下标oldsize/2 开始存放,上下留出相同的空间,方便deque首尾入队。
由于是双端队列,所以最开始的时候,first 和 last 其实是指向相同地方的 扩容后:
36. 虚函数
一个类如果有虚函数表,那在编译阶段就会产生vfptr,指向虚函数表,该表运行时加载到.rodata
用指针或引用调用虚函数的时候,首先访问对象前4个字节vfptr,然后找到虚函数表取出函数入口地址,进行动态绑定
37. 多态
使用基类的指针或引用指向不同的派生类对象,指向哪个对象就调用哪个对象的方法。
增加新功能或删除已有功能的时候,函数接口不需要改变。不管基类有多少个派生类,接口一套就够了,不需要对每一个对象都设计一套接口。
OOP里面的设计模式离不开多态,达到高内聚、低耦合的目标
38. C++异常处理原理
try{
可能抛出异常的代码
}catch(const string& error){
捕获相应异常类型的对象,进行处理,处理完成后继续向下执行
}
C++中如果在当前函数栈帧上没有找到相应的catch块处理,就会把异常抛给调用方函数。处理了就继续运行,没处理就继续向调用方抛出,直到main函数还没有处理,就向系统抛出,系统发现进程有异常未处理,就直接终止进程。
39. 早绑定(静态)和晚绑定(动态)
早绑定:编译时期完成(汇编代码是call 函数名 ),用对象调用虚函数。 晚绑定:运行时确定(汇编代码是call 寄存器 ),用指针或引用调用虚函数。确定指针类中被调用函数的类型,是虚函数则找到对象前4个字节的vfptr,从vftable拿到函数入口地址
可参考:C++继承和多态总结
40. 指针和引用的区别(汇编角度)
lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,例如:lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。而mov指令则恰恰相反,例如:mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。
可参考:C和C++的区别
41. 交叉引用的问题如何解决
交叉引用问题:无论是定义对象还是使用对象都用shared_ptr,导致交叉引用问题
定义对象时候使用强智能指针shared_ptr,引用的时候使用弱智能指针weak_ptr。当通过weak_ptr访问对象成员时,需要调用weak_ptr的lock提升方法,把weak_ptr提升成shared_ptr,然后再进行对象成员调用。
可参考:C++智能指针
42. C++函数重载的原理
C++生成函数符号是依赖函数名+参数列表
43. 编写一个C++程序需要注意什么
- 首先要分析需求,然后进行概要设计、详细设计。
- 考虑设计的函数、接口、类是否运行在多线程环境、考虑线程安全。
- 考虑代码的可移植性
- 使用现有的代码框架、设计模式等
44. 设计模式知道哪些?具体讲一下
可参考:C++设计模式总结
45. 构造函数抛出异常可能会导致内存泄漏,如何解决?
class Test{
public:
Test(){
p1 = new int();
p2 = new int();
throw "xxxx"
}
~Test(){
delete p1;
delete p2;
}
private:
int* p1;
int* p2;
}
改写成:
class Test{
public:
Test(){
p1 = new int();
p2 = new int();
throw "xxxx"
}
~Test(){ }
private:
unique_ptr<int> p1;
unique_ptr<int> p2;
}
|