C++校招常见问题汇总

文章目录

1. this指针干什么用的?

this的原型:A* const this,是不可改变指向的指针,是类方法的第一个参数,static方法没有this指针参数

一个class可以定义很多对象,每个对象都有自己的成员变量以及成员方法,this指针在编译后作为成员方法的第一个参数

在成员方法里访问成员变量的时候,this指针用于区分访问哪个对象的成员变量。成员方法第一个参数都是当前类的一个指针,我们通过对象调用成员方法时,会把对象的地址传入这个方法,该方法通过this指针就能访问该对象的数据成员

obj.fun() ==> fun(&obj)

2. new和delete,什么时候用new[]申请,可以用delete释放?

new和delete是重载的运算符,实际上调用的是operator newoperator delete

如果是自定义类型,而且提供了析构函数,用new[]申请的内存空间就一定要用delete[]释放,因为new[]除了开辟用户需要使用的内存空间,还多开辟了4字节存放对象的个数。因为编译器知道自己总共分配了多少空间,记录对象的个数后,可以在内存中准确划分出每个对象的首地址。

如果是内置类型,或没有提供析构函数的自定义类型new[]开辟空间的起始地址和返回给用户的地址相同,也就是没有存放对象个数的4字节空间。

胡乱猜测:没有提供析构函数,delete不执行析构,分配多少空间就回收多少空间,不用4字节记录对象个数。

可参考:C++运算符重载

3. static关键字的作用(从elf结构、链接过程回答)?

从面向过程角度来说:

符号表中被static修饰的全局变量、函数就从global变成了local,也即从整个项目可见变成当前文件可见,链接的时候不会被覆盖。static全局变量编译完成后就有初始值了

static修饰的局部变量放在数据段(.data或.bss),由于是在数据段,程序一开始就要开辟空间,在执行到相应语句的时候初始化变量。局部变量属于指令,本身不产生符号,通过ebp-偏移量进行访问,但是被static修饰后由于放在数据段,这时候就需要产生符号。

从面向对象角度来说:

static可以修饰成员变量,修饰成员方法,不再产生this指针,变成类所有,通过类作用域调用

4. C++的继承有什么好处?

继承处于类和类之间的关系,好处如下:

  1. 代码复用
  2. 通过继承,在基类里面给所有派生类保留统一的纯虚函数接口,派生类进行重写。通过基类指针或者引用指向派生类对象实现多态(指针指向谁就访问谁的方法),也更符合软件开发的 “开闭原则”(只要是派生类,基类指针都可指向,不用修改)

可参考:C++的继承和多态

5. 讲一下C++的多态

分为静多态(编译时期)和动多态(运行时期)。

静多态的表现形式包括函数重载和类模板,在编译时期就要确定下来。

继承结构中,Base类指针(引用)指向Derive类对象,通过该指针(引用)调用同名覆盖方法(虚函数),该指针指向哪个Derive对象的覆盖对象,就调用哪个Derive类方法

可参考:C++的继承和多态

6. 空间配置器allocator?

allocator主要是给容器使用,作用是将开辟空间allocate和构造对象construct分开,以及将回收空间deallocate和析构对象destroy分开

对于容器而言,如果使用new进行内存管理,当我们申请容器空间时,就会生成很多对象,而这些对象是我们用不上的,这个时候需要用到空间配置器。容器删除元素的时候,只是析构对象,而不是像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个性质:

  1. 每个节点要么是黑色,要么是红色
  2. 根节点是黑色
  3. 叶子节点是黑色
  4. 每个红色节点的两个子节点一定都是黑色, 不能有两个红色节点相连
  5. 任意一节点到每个叶子节点的路径都包含数量相同的黑色结点

性质5又可以推出: 如果一个节点存在黑色子节点,那么该结点肯定有两个子节点,不然走另一条路就会少一层黑色结点。

红黑树插入有3种情况(最多旋转2次),删除有4种情况(最多旋转3次)

9. C++如何防止内存泄漏?智能指针详述

内存泄漏:分配的堆内存(没有名字,只能用指针指向)没有释放

智能指针体现在把裸指针进行了面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源

利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源。所以,智能指针一般都是定义在栈上

可参考:C++智能指针

10. C++如何调用C语言函数接口?

由于C和C++生成符号的方式不同,C和C++直接的API接口无法直接调用,C语言的函数声明必须在extern "C"

#ifdef __cplusplus
extern "C"{
#endif
	int fun(int,int);
#ifdef __cplusplus
}
#endif

如果是C编译器编译这段代码,宏不会展开,只能看到函数声明部分的代码

如果是C++编译器编译这段代码,宏会展开,告诉C++编译器,编译和调用这个函数都得按照C语言的符号规则

// 使用C语言的方式生成函数符号
extern "C" {
	// 函数符号function_C,而不是function_C_int
	int function_C();
}

可参考:C和C++的区别

11. 那些情况下可能出现访问越界
  1. 数组、STL容器访问越界
  2. 字符串没有添加\0,导致访问越界
  3. 使用类型强转或static_cast,使得大类型(派生类)的指针指向小类型(基类)的对象,指针解引用直接越界
12. C++中类的初始化列表

可以指定数据成员的初始化方式,尤其是指定成员对象的初始化(成员变量的初始化和定义的顺序有关,和初始化列表的顺序无关)

放在构造函数体内的代码是进行赋值,而不是初始化,执行构造函数体时,所有的成员变量就已经初始化好了

13. C和C++的区别以及内存分布

引用、函数重载、运算符重载、new/delete/malloc/free、const、inline、模板、OOP语言可使用设计模式、STL、异常处理、智能指针

在这里插入图片描述

可参考:C和C++的区别

14. A* const p和const A* p的区别

我们在传递对象时,如果不想发生拷贝,可以通过传地址,用指针接收的形式。如果我们同时也不想改变实参的值,我们就使用const A* p 接收,这样我们可以读*p,但是不能写*p,有效的保护了数据

A* const p表示不能改变指针的指向,可以改变指向的数据。而this指针原理就是这样的,this指针就是一个只能指向当前对象的指针,不能修改指向。我们不能对this赋值,但是能修改this指向的数据成员

非const形参不能接收const实参,这也就是为什么const对象无法调用普通方法

const只能控制在语言级别,而不是在指令级别控制读写性质,也就是说编译器检测到对const量赋值就会报错,但如果我们不显式赋值,编译器就检测不到,我们其实也可以修改const修饰的变量

const int a = 1;
int* p = (int*)&a;
*p = 2;

如果用const修饰成员方法时,这个成员方法第一个参数会变成const A* const this,当我们用常对象调用普通方法时,就会报错,因为常指针无法指向普通对象

class A {
	public:
	void fun() {
		cout << "A::fun()" << endl;
	};
};

int main() {
	const A a;
	a.fun();   // 报错
	return 0;
}

但如果我们希望在常方法中修改成员变量的值,可以用mutable修饰这个成员,被mutable修饰的变量,将永远处于可变的状态,即使在常方法中

15. 构造函数{}之间的赋值和初始化列表之间的区别

执行构造函数初始化列表的时候,是正在构造对象,初始化对象的成员。而执行{}之间的赋值时对象已经构造完成,仅仅就是赋值,不是初始化。正因为const变量在语言级别不能被赋值,所以必须放在初始化列表

可参考:C和C++的区别

16. malloc和new的区别
  1. malloc是按字节开辟空间的,new开辟内存时需要指定类型(new int()),malloc开辟内存返回的都是void*,而new返回的是对应类型的指针

  2. malloc负责开辟空间,new不仅有malloc的功能,还可以进行数据初始化,比如:new int(10)。new有开辟空间和构造的功能。

  3. malloc开辟内存失败返回nullptr,而new则会抛出bad_alloc异常

  4. 我们调用new实际上是调用的operator new

可参考:C++运算符重载

17. map和set的实现原理

set是集合,里面只存放key。map是映射表,存储[key,value]键值对。

有序的关联容器,底层数据结构都是红黑树,然后需要答一些红黑树的性质,相关算法

18. shared_ptr的引用计数在哪存放

因为引用计数对象也是new出来的,所以存放在堆区

可参考:C++智能指针的enable_shared_from_this和shared_from_this机制

19. STL各个容器底层数据结构?

顺序容器: vector(数组)、deque(二维数组)、list(双向循环链表)
容器适配器: stack(deque)、queue(deque)、priority_queue(vector)
关联容器: set系列(红黑树)、map系列(红黑树)

可参考:C++ STL总结

20. 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);
}
21. 迭代器的失效问题

迭代器不允许一边读一边修改

当容器调用erase方法后,当前位置到容器末尾元素的所有迭代器全部失效(首元素到插入点的迭代器有效)

当容器调用insert方法后,当前位置到容器末尾元素的所有迭代器全部失效(首元素到插入点的迭代器有效)

insert来说,如果引起容器内存扩容,原来容器的所有的迭代器就全部失效

当通过迭代器更新容器元素以后,要及时对迭代器进行更新,insert/erase都会返回新位置的迭代器

可参考:C++运算符重载

22. C++中struct和class的区别
  1. 定义类的时候,struct的访问限定默认是public,class的访问限定默认是private
  2. 继承时,class Derive:Base表示私有继承,struct Derive:Base表示公有继承
  3. Linux下,C语言里struct定义的空结构体是0字节,C++里struct定义的空类是1字节(空结构体有多大?
  4. 为了兼容C语言里的struct,C++里struct定义的类对象初始化也可以用C语言中结构体的初始化方式,如:Data data = {10,20},但是C++的class不支持这种C风格的结构体初始化方式
  5. C++class支持定义模板类参数,template<class T>,不支持template<struct T>
23. 初始化的全局变量和未初始化的全局变量的区别

.data:存放初始化且初始化值不为0的数据
.bss:存放未初始化和初始化为0的数据

24. 堆和栈的区别
  1. 堆内存的大小 >> 栈内存的大小
  2. 堆内存需要手动开辟、释放,栈区开辟函数栈帧,自动清退
  3. 堆内存从低地址到高地址分配,栈从高地址(栈底)到低地址(栈顶)
25. 构造函数和析构函数可不可以是虚函数

构造函数不能是虚函数,析构函数可以是虚函数

构造函数不能实现成virtual,通常来说,vfptr存放在对象空间,任何虚函数被执行的流程,都是在对象空间种找到vfptr,找到vftable,才能执行。根据这个逻辑,虚函数执行前对象必须存在,也就是任何一个虚函数执行前构造函数必须执行完

如果构造函数没执行,对象不存在,也就不存在vfptr,也就无法执行虚函数

如果构造一个派生类对象,派生类构造需要先构造基类,基类的构造是一个虚函数,此时发生动态绑定,需要访问派生类的前4个字节vfptr,然后这个时候派生类对象还没产生,访问出错

基类的指针指向堆上的派生类对象时,delete ptr_base调用析构函数的时候,由于必须要调用到派生类对象的析构函数,所以必须是动态绑定,此时需要把Base的析构函数实现成virtual

若是静态绑定,则直接根据指针的类型,调用析构函数,无法调用派生类的析构函数

可参考:C++中的继承和多态总结

26. 构造函数和析构函数是否可以抛异常

主要考虑内存泄漏、资源释放的问题

构造函数不能抛出异常,若抛出异常则表示对象创建失败,不能调用析构函数,资源无法释放,我们捕获构造函数中可能抛出的异常,保证已经分配的堆内存被释放

析构函数只能在释放完所有资源后抛出异常

27. 宏和内联函数的区别
  1. #define在预编译时期处理,inline在编译时期处理
  2. #define就是字符串替换,release版本中inline在函数调用点把函数代码展开,节省函数的调用开销
  3. #define可以定义很多,比如常量、代码段、函数等,inline只能用于修饰函数
  4. #define不能调试,inline在Debug版本下可以调试,这时产生函数调用开销
28. 局部变量存放在哪

局部变量通过 栈底指针ebp偏移访问,存放在栈区

局部变量不产生符号,不属于数据,属于指令的一部分(若是在VS Debug版本下查看反汇编会发现依然有符号,这是VS优化后展示给用户的结果,实际上应该是ebp-偏移量

29. 拷贝构造函数为什么传引用&,不传值
class Test{
	// 会产生编译错误
	Test(const Test t){
		...
	}
};

int main(){
	Test t1;
	Test t2(t1);
	return 0;
}

这时用t1拷贝构造t2,如果传值,则需要用t1初始化形参t,这个时候也需要调用Test的拷贝构造函数(t1.Test(t)),而调用拷贝构造函数的时候,仍然需要实参初始化形参,再次调用拷贝构造函数,陷入了死循环。

而实际上,编译器也会检查,直接发生编译错误,无法运行

30. 内联函数和普通函数的区别(反汇编角度)

inline函数

  1. 编译期间在代码调用的地方展开(release版本),有逻辑性的进行文本替换,因此不产生函数符号
  2. 能够调试,在debug版本(需要调试)inline函数和普通的函数表现一致,只有在release版本才会真正在调用点展开
  3. 因为需要在编译期间展开,而编译期间针对的是单文件。所以inline函数的作用域只在本文件,debug版本生成local符号
  4. 有类型检测,安全
  5. 类体内实现的成员方法直接是inline

核心: 函数调用开销

开辟栈帧
在这里插入图片描述
栈帧清退
在这里插入图片描述

普通函数

  1. 不展开
  2. 能调试
  3. 作用域在全局,生成global符号
  4. 有类型检测,安全

可参考:C和C++的区别

31. 如何实现一个不可继承的类

派生类构造的过程:先是执行基类构造函数,然后执行派生类构造函数。

我们将基类的构造函数私有化,此时基类的构造函数对派生类是不可见的,所以无法继承。

32. 什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪?
virtual void fun() = 0;

用纯虚函数定义的类是抽象类,无法实例化对象,可以定义指针和引用

纯虚函数定义在基类里面,基类不代表实体,它的主要任务之一就是 给所有的派生类保留统一的纯虚函数接口,让派生类进行重写。重写以后就 可以使用多态,基类指针指向派生类对象,指向哪个对象就调用哪个对象的方法

虚函数表是在编译阶段产生,运行时加载到.rodata

可参考:C++继承和多态总结

33. 说一下C++中的const

编译时期值替换

const定义的叫常量,它的编译方式是把出现常量名字的地方,用常量的值进行替换

	const int a = 10;
	int* p = (int*)&a;
	*p = 100;
	cout<<a<<" "<<*p<<endl; // 10 100   
	// 立即数初始化,a的内存已经改了,但是编译时期替换,指令就是输出10
	int b = 10;
	const int a = b;
	int* p = (int*)&a;
	*p = 100;
	cout<<a<<" "<<*p<<endl; // 100 100   变量初始化,编译时期不会进行替换

常变量

若用立即数初始化,则编译阶段可以直接用立即数进行替换。
若用变量初始化,则退化为和C语言中一模一样的常变量(只是不能作为左值,编译方式同普通变量,不会在使用的地方直接替换为立即数)

定义常成员方法
this指针由Test* this变成const Test* this,普通对象和常对象都可以调用

可参考:C和C++的区别

34. const与static的区别

面向过程

const修饰:全局变量、局部变量、形参变量
static修饰:全局变量、局部变量、函数(本文件可见,把符号的作用域从global改成local,链接的时候看不见)

面向对象

const
常方法(普通对象和常对象都可以调用,只能对成员进行读不能写)、常成员变量(不能被修改,必须在构造函数的初始化列表初始化)

static
静态方法:该方法没有this指针,不依赖于对象,通过类作用域访问

静态成员变量:静态成员变量属于类,不属于某个具体的对象,实现多个对象共享数据的目的。static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。即没有在类外初始化的 static 成员变量不能使用

35. 四种强制类型转换

const_cast:去掉常量属性

static_cast:类型安全的转换

reinterpret_cast:C风格的类型转换,没有安全可言,随意转换

dynamic_cast:支持RTTI识别的类型转换。基类指针转成相应的派生类对象指针的时候,dynamic_cast会识别该指针是否能够进行转换

36. deque底层数据结构

deque是双端队列容器,底层数据是动态扩容的二维数组。一维数组从2开始,以2倍的方式进行扩容,每次扩容后原来第二维的数组从新的第一维数组的下标oldsize/2开始存放,上下留出相同的空间,方便deque首尾入队

由于是双端队列,所以最开始的时候,firstlast其实是指向相同地方的
在这里插入图片描述
扩容后:
在这里插入图片描述

37. 虚函数

一个类如果有虚函数表,那在编译阶段就会产生vfptr,指向虚函数表,该表运行时加载到.rodata

用指针或引用调用虚函数的时候,首先访问对象前4个字节vfptr,然后找到虚函数表取出函数入口地址,进行动态绑定

38. 多态

使用基类的指针或引用指向不同的派生类对象,指向哪个对象就调用哪个对象的方法。

增加新功能或删除已有功能的时候,函数接口不需要改变。不管基类有多少个派生类,接口一套就够了,不需要对每一个对象都设计一套接口。

OOP里面的设计模式离不开多态,达到高内聚、低耦合的目标

39. C++异常处理原理
try{
	可能抛出异常的代码
}catch(const string& error){
	捕获相应异常类型的对象,进行处理,处理完成后继续向下执行
}

C++中如果在当前函数栈帧上没有找到相应的catch块处理,就会把异常抛给调用方函数。处理了就继续运行,没处理就继续向调用方抛出,直到main函数还没有处理,就向系统抛出,系统发现进程有异常未处理,就直接终止进程。

40. 早绑定(静态)和晚绑定(动态)

早绑定:编译时期完成(汇编代码是call 函数名),用对象调用虚函数。
晚绑定:运行时确定(汇编代码是call 寄存器),用指针或引用调用虚函数。确定指针类中被调用函数的类型,是虚函数则找到对象前4个字节的vfptr,从vftable拿到函数入口地址

可参考:C++继承和多态总结

41. 指针和引用的区别(汇编角度)

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++的区别

42. 交叉引用的问题如何解决

交叉引用问题:无论是定义对象还是使用对象都用shared_ptr,导致交叉引用问题

定义对象时候使用强智能指针shared_ptr,引用的时候使用弱智能指针weak_ptr。当通过weak_ptr访问对象成员时,需要调用weak_ptr的lock提升方法,把weak_ptr提升成shared_ptr,然后再进行对象成员调用。

可参考:C++智能指针

43. C++函数重载的原理

C++生成函数符号是依赖函数名+参数列表

44. 编写一个C++程序需要注意什么
  1. 首先要分析需求,然后进行概要设计、详细设计
  2. 设计合适的类,接口,是否使用多态,是否运行在多线程环境、考虑线程安全
  3. 考虑代码的可移植性,使用语言级别的接口,还是使用系统提供的接口
  4. 参考现有的代码框架、设计模式等
45. 设计模式知道哪些?具体讲一下

可参考:C++设计模式总结

46. 构造函数抛出异常可能会导致内存泄漏,如何解决?
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;
}
  • 3
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bugcoder-9905

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值