常见C++面试题

这篇博客详细介绍了C++中的关键概念,包括指针与引用的区别、堆栈内存的区别、new与malloc的异同、C与C++的不同、struct与class的区别、const和static的用法、内存管理(堆栈、动态内存、智能指针)、多态(虚函数、纯虚函数)以及内存对齐和构造函数等核心知识点,适合C++面试复习。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(1) 指针和引用的区别

1. 指针是一个实体,而引用仅是个别名

2. 引用只能在定义时被初始化一次,之后不可变;指针可以不用初始化,指针有可能为空,指针值可变;

3. 引用没有const,指针有const,const的指针不可变;

4. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;

5. 指针和引用的自增(++)运算意义不一样;

6. 引用是类型安全的,而指针不是 (引用比指针多了类型检查)

 

(2) 堆和栈的区别

1   stack栈的空间由操作系统自动分配或者释放,heap堆上的空间手动分配或者释放;

2,栈的空间有限(2M),堆是很大的自由存储区(受虚拟内存空间影响);

3,malloc函数和new 操作符,分配的空间都在堆上;

4,申请效率不同,栈申请效率块,堆慢。

5,程序在编译器对变量和函数的分配都在栈上,而且程序运行过程中函数调用时参数的传递也在栈上进行;

 

(3) new 与 malloc的异同处

char *p = (char *)malloc(100 * sizeof(char));

Char* p = new char[100];

1,new 是关键字,malloc是库函数

2,new申请内存无需指定内存块的大小,会根据类型自行判断,malloc需要显示指定内存大小

3,new返回与类型相同的指针,无需类型转换。malloc需要将void*转为需要的类型

4,失败时,new会抛bab_alloc异常,malloc会返回null

5,new可以new一个自定义的类,会调用构造函数。malloc不行

6,new可以重载

 

(4) C和C++的区别

关键字:new, delete,auto等

重载和虚函数概念

类,对象,继承

面向对象

 

(6) Struct和class的区别

1,默认成员访问权限,class为private,struct为public

2,默认继承权限,class为private,struct为public

 

 

(7) define 和const的区别(编译阶段、安全性、内存占用等)

(1)就起作用的阶段而言:#define是在编译的预处理阶段起作用(在预处理阶段进行替换),而const是在编译运行的时候起作用(const修饰的只读变量是在编译的时候确定其值)

(2)就起作用的方式而言:#define只是简单的字符串替换,没有类型检查。而const有对应的类型,是要进行判断的,可以避免一些低级的错误

(3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次。它定义的宏常量在内存中存若干个备份;const定义的只读变量在程序中只有一份备份

(4)从代码调试的方便程度而言:const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经进行替换了

(5)就内存分配而言:编译器通常不为普通的const只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高

const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。

 

、const的优点

(1)const常量有数据类型,而宏常量没有数据类型,编译器可以对const进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误

(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试

(4)const在内存中只存储了一份,节省了空间,避免不必要的内存分配,提高了效率

 

 

(8) 在C++中const和static的用法(定义,用途)

Const 的用法:

const常量: 不能修改

const指针(顶层和底层):顶层不能改指针,底层不能改指向的对象。

引用的对象是const,也必须要用const引用去接

                const int val = 1024;

                const int &refVal = val;

const函数参数:函数内部不能修改该参数

const函数返回值:比如const int * fun1( ){   } 指针指向的对象不能修改,值传递就无所谓

const修饰成员函数:其实是修饰*this,不能在函数内部修改成员变量(除了 mutable修饰的变量外)

                                也不能调用非const成员函数(因为它们有可能会该成员变量)

const修饰成员变量:不能再类定义时初始化,只能再构造函数初始化列表中进行

const修饰类对象:const对象只能调用const成员函数,不能调非const成员函数。(没有this指针)

 

static的用法

static全局变量和全局函数:变量和函数都只在本文件内可见,比如A.cpp定义的static变量在B.cpp不可见

static局部变量:生存周期为整个源程序

static变量(全局和局部):都默认初始化为0

static类成员变量:该变量属于整个类,不属于某个对象,要在类外部进行初始化,通过类和对象都可以访问

static类成员函数:只能掉用静态成员函数,不能调非静态成员函数

全局变量、文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化。这里的变量包含内置数据类型和自定义类型的对象。放在全局区,不会占用类的内存。

 

(11) 计算下面几个类的大小:sizeof在编译阶段求值

指针的大小:sizeof(int*)   4(32)位系统

数组的长度:int a[10]  sizeof(a)   40

退化的数组:

void fun(int a[10])

{

              int n = sizeof(a);   //结果为4,常见的陷阱

}

int* a = new int[10]; int n = sizeof (a);  //结果为4

结构体:要考虑内存对齐class和struct

            1、对于结构体的第一个成员,将它在整个结构体在内存中分布的偏移量看成0,以后的每一个数据成员的                偏移量必须是 min{#pragma pack()指定的数,这个数据成员的本身的数据长度} 的倍数。

            2、每个数据成员完成在结构体内部对齐的时候,还要进行整个结构体在内存中的对齐,整个结构体的大小为 min{#pragma pack()指定的数,这个结构体中数据长度最大的数据成员的长度} 的倍数。

 

class A {};: sizeof(A) = 1; 为了确保类的实例有唯一的地址

class A { virtual Fun(){} };: sizeof(A) = 4(32位机器)/8(64位机器);

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;

 

(12) 给一个代码,求输出结果

class A

{

public:

A(int x){}

}

问:A a = 1;是否正确, 如果正确, 那么它调用了哪些函数?

这类题目更常见的是在基类和子类有不同实现方法。(虚函数相关,栗子很多,不多说了)

(13) C++的STL介绍(这个系列也很重要,建议侯捷老师的这方面的书籍与视频),其中包括内存管理allocator,函数,实现机理,多线程实现等

     STL提供六大组件:    

1、容器:各种数据结构,如:vector、list、deque、set、map、主要用来存放数据。

        2、算法:各种常见算法,如:sort、search、copy、erase

        3、迭代器:扮演算法和容器中的中介。所有的容器均有自己独特的迭代器,实现对容器内数据的访问

        4、仿函数:行为类似函数,可作为算法的某种策略。

        5、配接器(adapters): 修饰容器、仿函数、迭代器接口

        6、配置器(allocators):负责空间配置和管理,配置器是一个实现了动态空间配置、空间管理、空间释放的class template.

allocator是STL中的内存分配器,基于空闲列表,先分配一块固定大小的内存池,然后实施小对象的快速分配和释放,避免内存碎片,局限于STL容器使用

uploading.4e448015.gif正在上传…重新上传取消

 

(14) STL源码中的hash表的实现

uploading.4e448015.gif正在上传…重新上传取消

 

(15) STL中unordered_map和map的区别

map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。

 

unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。

 

红黑树:平衡二叉搜索树

1. 节点是红色或者黑色

2. 根节点是黑色

3. 每个叶子的节点都是黑色的空节点(NULL)

4. 每个红色节点的两个子节点都是黑色的。

5. 从任意节点到其每个叶子的所有路径都包含相同的黑色节点。

最后通过变色和左旋和右旋来进行调整来使来整个红黑树继续保持上述的性质

 

 

(16) STL中vector的实现

vector采用的数据结构很简单:线性的连续空间

扩充空间需要经过的步骤:重新配置2倍的空间,元素移动到新空间,释放旧内存空间。(vector机制将旧空间中的备用空间也拷贝到新空间来了,感觉没必要)

一旦vector空间重新配置,则指向原来vector的所有迭代器都失效了,因为vector的地址改变了。

 

(17) vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。

可以一开始给它分配好size

数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);

 

链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).

 

树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

 

注意:经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*ite

 

(18) C++中的重载和重写的区别:

函数重载(overload)

函数重载是指在一个类中声明多个名称相同但参数列表不同的函数,这些的参数可能个数或顺序,类型不同,但是不能靠返回类型来判断。特征是:

(1)相同的范围(在同一个作用域中);

(2)函数名字相同;

(3)参数不同;

(4)virtual 关键字可有可无(注:函数重载与有无virtual修饰无关);

(5)返回值可以不同;

 

函数重写(也称为覆盖 override)

函数重写是指子类重新定义基类的虚函数。特征是:

(1)不在同一个作用域(分别位于派生类与基类);

(2)函数名字相同;

(3)参数相同;

(4)基类函数必须有 virtual 关键字,不能有 static 。

(5)返回值相同,否则报错;

(6)重写函数的访问修饰符可以不同;

 

重定义(也称隐藏)

(1)不在同一个作用域(分别位于派生类与基类) ;

(2)函数名字相同;

(3)返回值可以不同;

(4)参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆);

(5)参数相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆);

 

(19) C ++内存管理(热门问题)

堆、栈、自由存储区、全局/静态存储区和常量存储区

栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

 

堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

 

自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

 

全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

 

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

 

常见内存错误及其对策:

内存分配未成功,却使用了它:assert(p!=nullptr)

内存分配虽然成功,但尚未初始化就引用它。

内存分配成功并初始化,但越界了。

忘记释放内存,造成内存泄漏。

释放了内存却继续使用它。

智能指针的循环引用  a引用b b又引用a

(20) 介绍面向对象的三大特性,并且举例说明每一个。

封装,继承,多态

(21) 多态的实现(和下个问题一起回答)

概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。

 

表现:如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数,此为多态的表现;

 

实现:基类函数有virtual关键字,有类继承,继承类重写了虚函数,子类对象赋给基类指针

原理:有virtual关键字,有vptr指针,指向相应的虚函数表(父类和子类都有虚函数表),当执行父类构造函数时,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。

在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。

 

(22) C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(热门,重要)

 

(23) 实现编译器处理虚函数表应该如何处理

 

(24) 析构函数一般写成虚函数的原因

一个类有子类

会有资源释放不完全的情况

(1)如果基类没有虚析构函数,结果将是不确定的,实际发生时,派生类的析构函数永远不会被调用。

(2)如果基类有虚析构函数的话,最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。

 

(25) 构造函数为什么一般不定义为虚函数

1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

 

2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

 

(26) 构造函数或者析构函数中调用虚函数会怎样

简单的说就是,在子类对象的基类子对象构造期间,调用的虚函数的版本是基类的而不是子类的。不要在类的构造或者析构过程中调用虚函数,因为这样的调用永远不会沿类继承树往下传递到子类中去

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。

同样,进入基类析构函数时,对象也是基类类型。

C++不允许(构造还没构造,析构已不能使用)

《Effective C++》条款9:永远不要在构造函数或析构函数中调用虚函数 。

 

(27) 纯虚函数

格式:

class Cshape

{

public:

    virtual void Show()=0;

};

1)当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;

2)这个方法必须在派生类(derived class)中被实现;

 

违背这两点时,编译器都会报错

接口和抽象类https://blog.youkuaiyun.com/wjeson/article/details/17096335

 

(28) 静态绑定和动态绑定的介绍

1. 静态绑定发生在编译期,动态绑定发生在运行期;

2. 对象的动态类型可以更改,但是静态类型无法更改;

3. 要想实现动态,必须使用动态绑定;

4. 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

 

(29) 引用是否能实现动态绑定,为什么引用可以实现

可以,指针或引用是在运行期根据他们绑定的具体对象确定(因为它们只是指代1个地址或者别名)

 

(30) 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)

       对一个已知对象进行拷贝,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数。如果类内又指针,在构造时后调用一次构造函数,析构时调用两次析构函数,两个对象的指针成员所指内存相同,内存却被释放了两次,会导致崩溃。

       浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

 

(31) 对象复用的了解,零拷贝的了解

 

(32) 介绍C++所有的构造函数

默认构造函数:就是默认初始化的

普通构造函数:相对与默认有参数

拷贝构造函数:就是const class &A 要用引用,不然会无休止的调用下去

赋值构造函数:就是=操作符的重载,如果有动态生成的对象的话,那么它做赋值,原先的动态空间一定要被释放掉,然后再赋上新的值,所以必须得自己重载=,实现delete。

 

 

(33) 什么情况下会调用拷贝构造函数(三种情况)

对象初始化

函数参数传递

函数返回值

 

(34) 结构体内存对齐方式和为什么要进行内存对齐?

对齐规则:

1.第一个成员在结构体变量偏移量为0 的地址处

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数为编译器默认的一个对齐数与该成员大小中的较小值。vs中默认值是8 Linux默认值为4

3.结构体总大小为最大对齐数的整数倍

4.如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍

 

为什么要内存对齐

平台原因(移植原因)

性能原因

结构体的内存对齐是拿空间来换取时间的做法

 

(35) 内存泄露的定义,如何检测与避免?

定义:指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。

类型:

1.堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak.

2.系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

检测:Valgrind工具,mencheck

避免:事前使用智能指针,事后查。

 

(36) 手写实现智能指针类(34-37我没遇见过)

(37) 调试程序的方法

归纳法、演绎法、测试法,回溯法,暴力法。设置断点,进入调试,单步运行,进入函数,查看变量。linux gbk,win windbg

 

(38) 遇到coredump要怎么调试

 

(39) 内存检查工具的了解

Valgrind工具,mencheck

 

(40) 模板的用法与适用场景

代码可重用,泛型编程,在不知道参数类型下,函数模板和类模板

 

(41) 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?

成员初始化列表:

在类构造函数中,不在函数体内对变量赋值,而在参数列表后,跟一个冒号和初始化列表。

初始化和赋值对内置类型的成员没有什么大的区别,像上面的人一个构造函数都可以。对非内置类型成员变量,为了避免两次构造,推荐使用类构造函数初始化列表。但是有时候必须使用带初始化列表的构造函数。

①:成员类型是没有默认构造函数的类。若没有提供显示初始化,则类创建对象时会调用默认构造函数,如果没有默认构造函数,则必须显示初始化。

②:const成员或者引用类型的成员。因为const对象或者引用类型只能初始化,不能赋值。

为什么成员初始化列表效率更高?

 

因为对于非内置类型,少了一次调用默认构造函数的过程。

 

(42) 用过C11吗,知道C11新特性吗?(有面试官建议熟悉C11)

lambda 智能指针 移动,auto,范围for,decltype,array,forward_list,tuple,正则表达式库,随机数库,bitset运算

 

(43) C++的调用惯例(简单一点C++函数调用的压栈过程)

 

(44) C++的四种强制转换

https://www.cnblogs.com/larry-xia/p/10325643.html

 

动态库顺序

1.编译目标代码时指定的动态库搜索路径;

2.环境变量LD_LIBRARY_PATH指定的动态库搜索路径;

3.配置文件/etc/ld.so.conf中指定的动态库搜索路径;//配置后要运行 ldconfig命令才能生效

4.默认的动态库搜索路径/lib;

5.默认的动态库搜索路径/usr/lib;

 

45、volatile有什么作用?

 

volatile定义变量的值是易变的,每次用到这个变量的值都要重新读取这个变量的值,而不是读寄存器内的备份;

多线程中几个被任务共享的变量需要定义成volatile类型。

 

20、野指针是什么?

  • 野指针也叫空悬指针,不是指向null的指针,指向垃圾内存的指针;
  • 产生的原因及解决办法:
    • 指针变量未及时初始化=>定义指针变量要及时初始化,要么置空
    • 指针free或delete之后没有及时置空=>释放操作后立即置空

 

来自 <http://harlon.org/2018/03/23/cpluscplusbase/>

 

21、堆和栈的区别?

  • 申请方式不同:
    • 栈由系统分配;
    • 堆由程序员手动分配。
  • 增长方向不同:
    • 栈是从高地址向低地址增长;
    • 堆是从地址向高地址增长;
  • 大小限制不同:
    • 栈顶和栈底是之前预设好的,大小固定,可以通过ulimit -a查看,由ulimit -s修改;
    • 堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
  • 申请效率不同:
    • 栈由系统分配,速度快,不会有碎片;
    • 堆由程序员分配,速度慢,会有碎片。

 

来自 <http://harlon.org/2018/03/23/cpluscplusbase/>

 

压栈过程

vector注意事项

析构函数虚函数调用

inline函数

父子类有什么不能 继承

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值