5、C++高频面试真题|41-50
C++ 面试真题(41-50)
41. 🌟C++ 中为什么要使用 nullptr 而不是 NULL?
回答重点
主要原因是 nullptr
有明确的类型,它是 std::nullptr_t
类型,可以避免代码中出现类型不一致的问题。
扩展知识
1)类型安全:NULL
通常被定义为数字 0
(在 C++ 代码中一般是 ##define NULL 0
),它实际上是整型值。这就可能会带来类型不一致的问题,比如传递参数时,编译器无法准确判断是整数 0
还是空指针。而 nullptr
则是 std::nullptr_t
类型的,能够明确表示空指针,使编译器更容易理解代码。
**2)代码可读性:**使用 nullptr
使得代码更具有可读性和可维护性。它明确传达了变量是用作指针而非整数值,例如:
void process(int x) {
std::cout << "Integer: " << x << std::endl;
}
void process(voidptr) {
std::cout << "Pointer: " << ptr << std::endl;
}
int main() {
process(NULL); // int 还是指针?
process(nullptr); // 指针
return 0;
}
在上面的代码中,可以看出 nullptr
能让编译器和程序员清楚地知道调用哪个函数。
**3)避免潜在的错误:**在函数重载和模板中使用 NULL
可能导致编译器选择错误的重载版本。另外,模板编程中特别是涉及类型推断时,NULL
会带来一些不期望的效果。
template<typename T>
void foo(T x) {
std::cout << typeid(x).name() << std::endl;
}
int main() {
foo(0); // 0 是int型
foo(NULL); // 你希望是int还是指针呢
foo(nullptr); // std::nullptr_t
return 0;
}
在上面的代码中,使用 nullptr
可以让我们精确控制模板的类型。
nullptr
与 NULL
对比表
C++ 中 
42. 🌟C++ 中什么是深拷贝?什么是浅拷贝?
回答重点
**1)浅拷贝:**浅拷贝只是简单地复制对象的值,而不复制对象所拥有的资源或内存。也就是说,两个对象共享同一个资源或内存。当一个对象修改了该资源或内存,另一个对象也会受到影响。这种情况通常发生在默认的拷贝构造函数或赋值操作中。
**2)深拷贝:**深拷贝不仅复制对象的值,还会新分配内存并复制对象所拥有的资源。这样两个对象之间就不会共享同一个资源或内存,修改其中一个对象的资源或内存不会影响到另一个对象。
43. C++ 中友元类和友元函数有什么作用?
回答重点
两者主要用于提供访问私有成员和保护成员的权限。
友元关系是一种单向的访问权限,并不会破坏封装性,同时也不会牵涉到类之间的继承关系。友元的使用在以下情况下特别有用:
**1) 友元函数:**允许一个函数访问某个类的私有成员和保护成员。
class MyClass {
private:
int privateMember;
public:
MyClass() : privateMember(0) {}
//声明友元函数friend void
friendFunction(MyClass &obj);
};
void friendFunction(MyClass &obj) {
//访问 privateMember
obj.privateMember = 10;
}
**2) 友元类:**允许另一个类访问某个类的私有成员和保护成员。
class B; //前向声明
class A {
private:
int privateMember;
public:
A() : privateMember(0) {}
//声明B为友元类
friend class B;
};
class B {
public:
void accessA(A &obj) {
//访问 A 的 privateMember
obj.privateMember = 20;
}
};
扩展知识
下面进一步讨论下它们的作用场景和设计考量:
1) 封装与开放:
- 封装是面向对象编程的基本原则之一,它将数据和操作数据的方法绑定到一起,防止外部代码直接访问对象的内部状态。友元的引入让类在需要的时候能够部分地开放它的内部状态,通常不会滥用。
- 友元函数和友元类提供了一种在不破坏封装性的条件下,安全访问私有成员的方式。
2) 友元的替代方案:
- 如果友元机制的使用本质上意味着违反封装性或设计初衷,那么可能需要重新考量类的设计。
- 你可以选择通过公开接口提供访问权限(如 getter/setter 方法),或利用继承、多态等其他 OOP 特性来实现同样的目的。
3) 访问控制复杂度:
- 使用友元可能会增加代码的复杂度,因为它打破了类的封装性,代码的维护变得相对困难。所以,在维护代码时,需要非常小心,确保友元使用的合理性和必要性。
友元是一种方便但需要慎用的工具,合理使用能够简化代码,但滥用则会破坏类的封装性,增加代码维护的难度。建议在实际编程中能够权衡利弊,合理利用这一机制。
44. C++ 如何调用 C 语言的库?
回答重点
可以使用 extern "C"
来告诉编译器按照 C 语言的链接方式处理某些代码:
1)在 C++ 代码中包含 C 语言头文件时,用 extern "C"
进行声明,比如:
extern "C" {
##include "your_c_library.h"
}
2)需要在链接阶段确保 C++ 项目和 C 语言库都被正确链接。可通过编写合适的 CMakeLists.txt 或 Makefile 来实现。
3)也可以不使用 extern "C",源文件后缀名改为.c 也行。
扩展知识
说到 extern "C"
,得从 C 和 C++ 的兼容性说起。C++ 是 C 的增强版本,但它们的编译方式还是有些差异的。C++ 支持函数的重载,而 C 语言不支持。C++ 编译器会对函数进行“名字修饰”(Name Mangling)。
extern "C"
的作用是让编译器按 C 方式编译,避免函数名被修饰,保证 C 语言库里的函数能被正确调用。
举个例子: 假设有一个简单的 C 库 math_library.c
:
// math_library.c
int add(int a, int b) {
return a + b;
}
你先编写一个头文件 math_library.h
:
// math_library.h
##ifndef MATH_LIBRARY_H
##define MATH_LIBRARY_H
int add(int a, int b);
##endif
然后在你的 C++ 项目中这么用:
// main.cpp
##include <iostream>
extern "C" {
##include "math_library.h"
}
int main() {
int result = add(3, 4);
std::cout << "Result: " << result << std::endl;
return 0;
}
最后,确保编译和链接。可以使用以下命令:
g++ -o main main.cpp math_library.c
另外,有几点需要注意:
1)如果你的 C 库里有 C++ 不支持的特性,比如变量长度数组(VLA),需要仔细考虑兼容性。
2)如果 C 库包含了结构体,尤其是那些带有复杂数据类型或指针的结构体,要确保它们在 C++ 中能够正确处理。
3)最好是 C 和 C++ 不要混用,如果要混用,建议做一个封装层,对 C 做一层 C++ 的封装,然后上层的业务代码还是统一使用 C++。
45. 指针和引用的区别
回答重点
- 引用必须在声明时初始化,指针可以不需要初始化
// 引用示例
int a = 10;
int& ref_a = a; // 引用初始化
// int& ref_b; // 错误:引用必须在声明时初始化
// 指针示例
int b = 20;
int* ptr_b = &b; // 指针初始化
int* ptr_c = nullptr; // 指针可以为空
- 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名,引用本身并不存储地址,而是在底层通过指针来实现对原变量的访问
- 引用被创建之后,就不可以进行更改,指针可以更改
##include<iostream>
int main() {
int a = 10;
int b = 20;
// 引用
int& ref_a = a; // ref_a 是 a 的引用
// ref_a = b; // 错误:引用不能被重新绑定
// 指针
int* ptr_a = &a; // ptr_a 是一个指针,指向 a
ptr_a = &b; // 指针可以被重新赋值,现在指向 b
std::cout << "a: " << a << ", b: " << b << std::endl;
// 如果取消注释 ref_a = b; 这行代码,将会导致编译错误
// 输出 "a: 10, b: 20",因为引用不能被重新绑定
*ptr_a = 30; // 通过指针修改 b 的值
std::cout << "a: " << a << ", b: " << b << std::endl; // 输出 "a: 10, b: 30"
return 0;
}
- 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
46. public 继承、protected 继承、private 继承的区别
回答重点
重点说明三种继承方式的区别:
public继承 | protected继承 | private继承 | |
基类public成员 | 子类中仍然是public | 子类中变为protected | 子类中变为private |
基类protected成员 | 子类中仍然是protected | 子类中仍然是protected | 子类中变为private |
基类private成员 | 子类不可访问 | 子类不可访问 | 子类不可访问 |
使用场景 | 最常用的is-a场景,允许外部访问基类的public接口。 | 避免基类的成员不被外部访问,子类的子类还有机会可以访问。 | 外部无法访问基类的任何成员。 |
47. 什么是静态数据成员和静态成员函数?
在 C++ 里,静态数据成员和静态成员函数是类的特殊成员,下面分别介绍它们:
静态数据成员
静态数据成员是用 static
关键字修饰的类的数据成员。它不属于类的某个具体对象,而是被类的所有对象共享,在内存中只有一份拷贝。
所有对象都能访问和修改同一个静态数据成员,可用于记录类相关的公共信息。
必须在类外进行初始化,且初始化时不使用 static
关键字。
##include <iostream>
class MyClass {
public:
static int staticData; // 声明静态数据成员
};
// 在类外初始化静态数据成员
int MyClass::staticData = 10;
int main() {
MyClass obj1, obj2;
std::cout << "obj1的静态数据成员值: " << obj1.staticData << std::endl;
std::cout << "obj2的静态数据成员值: " << obj2.staticData << std::endl;
// 修改静态数据成员的值
obj1.staticData = 20;
std::cout << "修改后obj2的静态数据成员值: " << obj2.staticData << std::endl;
return 0;
}
在这个例子中,staticData
是 MyClass
类的静态数据成员,obj1
和 obj2
共享这一成员,修改 obj1
的 staticData
后,obj2
的 staticData
也会改变。
静态成员函数
静态成员函数是用 static
关键字修饰的类的成员函数,它不依赖于类的具体对象,可直接通过类名调用。
没有 this
指针,因为它不与特定对象关联,不能访问非静态数据成员和非静态成员函数。
主要用于处理类的静态数据成员,提供与类相关的通用功能。
##include <iostream>
class MyClass {
private:
static int staticData;
public:
static void setStaticData(int value) {
staticData = value;
}
static int getStaticData() {
return staticData;
}
};
// 在类外初始化静态数据成员
int MyClass::staticData = 0;
int main() {
// 直接通过类名调用静态成员函数
MyClass::setStaticData(30);
std::cout << "静态数据成员的值: " << MyClass::getStaticData() << std::endl;
return 0;
}

48. 静态成员变量为什么要在类外初始化?
回答重点
存储空间分配:
- 因为静态变量不属于任何特定对象,而是类的共享成员,需要在全局范围内分配存储空间。静态变量的存储空间在程序启动时分配,生命周期贯穿整个程序运行。类内声明仅告知编译器该变量的存在,而类外定义则实际分配内存。
避免重复定义:
- 如果在类内初始化静态变量,可能会导致多个编译单元中包含该变量的定义,导致链接错误。类外定义确保静态变量只在一个编译单元中定义,避免重复。
扩展知识
静态成员函数:
- 静态成员函数只能访问静态成员变量,不能访问非静态成员变量,因为它们没有
this
指针。
- 静态成员函数只能访问静态成员变量,不能访问非静态成员变量,因为它们没有
静态常量成员:
- 静态常量成员(如
static const int
)可以在类内直接初始化,因为它们在编译时已知,并且不需要额外的存储空间。
- 静态常量成员(如
内联静态成员:
- C++17 引入了内联静态成员,可以在类内直接初始化静态变量,编译器会自动处理存储和链接问题。
class MyClass {
public:
static int staticVar; // 类内声明
static const int staticConstVar = 10; // 静态常量整型成员,类内初始化
};
int MyClass::staticVar = 0; // 类外定义和初始化
int main() {
MyClass::staticVar = 5; // 访问静态变量
return 0;
}
49. 🌟C++ 动态库和静态库的区别?
回答重点
动态库(也称为共享库)和静态库是常用的两种库形式,它们有以下主要区别:
1)动态库在运行时加载,提供共享的库文件,如 .dll
(Windows)或 .so
(Linux),而静态库在编译时被直接合并到可执行文件中,通常是 .lib
(Windows)或 .a
(Linux)。
2)动态库在内存中可以被多个程序共享,因此节省了内存和磁盘空间,但会引入一些加载开销。对于静态库,每个使用它的程序都会有一份拷贝,二进制文件较大,但启动速度通常较快。
3)重点:动态库可以在库升级时无需重新编译依赖该库的程序,只需要更新库文件即可。而静态库由于依赖程序直接包含了库的代码,所以需要重新编译依赖程序进行更新。
4)在调试和部署方面,动态库更复杂,因为它们需要在运行时找到所需的库文件。如果库文件不存在或版本不匹配,程序可能无法正常运行。静态库则不会有这种问题,因为所有依赖都已经编译进了可执行文件。
扩展知识
除了上面提到的主要区别,动态库和静态库在开发过程中还有一些其他注意事项和优化技巧,可以进一步了解:
**1)符号解析:**静态链接在编译时就解析了符号,这意味着所有函数和变量的引用都在编译期解决,而动态链接在运行时解析符号,这会略微增加加载时间。
**2)版本控制:**动态库通常可以使用版本号来管理不同版本的库同时存在,例如在 Linux 系统中,库文件名中可以包含版本号,如 libxyz.so.1.2.3
。这可以允许旧版程序继续使用旧版本的库,而新版程序使用新版本的库。
**3)编译选项:**在创建动态库时,通常需要使用如 -fPIC
(Position Independent Code,位置无关代码)选项来生成可以在任意内存地址运行的代码。而静态库通常不需要考虑这一点,因为它们最终会被编译进可执行文件。
**4)链接顺序:**在链接静态库时,链接顺序可能会影响到最终的可执行文件,尤其是在处理有依赖关系的多个库时。动态库则相对宽松,因为它们的依赖关系可以在运行时解决。
**5)打包和分发:**动态库通常需要一并打包分发,确保目标系统中存在正确版本的库。常见的方法包括使用容器(如 Docker)或者打包系统(如 Windows 安装包及 Linux 的 .deb
和 rpm
包)。

50. 🌟 介绍下 C++ 程序从编写到可执行的整个过程?
回答重点
总共分 5 步:
1)编写代码:编写 C++ 源代码,保存为 .cpp
、.cc
.h
文件。
2)预处理:预处理器根据源代码中的预处理指令(如 ##include
替换、##define
替换等)对代码进行处理,生成纯净的源代码。
3)编译:编译器(如 g++
或 clang++
)将预处理后的源代码翻译成汇编代码。
4)汇编:汇编器(如 as
)将汇编代码转换成机器码,生成目标文件(.o
文件)。
5)链接:链接器(如 ld
)将多个目标文件和库文件链接在一起,生成最终的可执行文件。
扩展知识
下面深入理解各个步骤:
1)编写代码
开发者主要参与的就是这个步骤,通过 C++ 编写逻辑业务代码,这部分代码是高级语言编写的,人类易读易写。
2)预处理
预处理器主要做以下工作:
- 处理头文件:如
##include
指令会替换为头文件内容。 - 宏替换:如
##define
指令用相应的代码替换宏。 - 条件编译:根据条件指令(如
##ifdef
、##ifndef
等)决定是否编译部分代码。 - 去除注释:清理掉所有注释。
- 添加行号:添加行号和文件名标识,方便编译器产生警告和调试信息
预处理的输出仍是 C++ 代码,但没有了预处理指令。
3)编译
编译器将预处理后的代码转化为汇编代码,主要做以下工作:
- 词法分析:语法扫描,利用有限状态机的算法将源码中的字符串分割成一系列记号,如加减乘除数字括号等。
- 语法分析:检查代码语法,将源代码转为语法树。
- 语义分析:检查变量类型、函数调用是否合法等,比如浮点型整数赋值给指针,编译器就会报错。
- 优化:对中间代码进行优化,提高代码运行效率,比如 3+4=7。
- 生成汇编:根据优化后的中间代码生成汇编代码。
汇编代码是与硬件无关的低级代码。
4)汇编
汇编器将汇编代码转化为与目标机器相关的机器码,生成目标文件。每个 .cpp
源文件(模块)通常会生成一个对应的 .o
目标文件。这些文件包含机器指令以及一些符号表信息,用于后续链接。
5)链接
链接器将所有目标文件和所需的库文件链接起来,生成最终的可执行文件。主要做以下工作:
- 符号解析:找到所有外部符号(如函数、变量)在目标文件和库文件中的定义。
- 地址分配:将目标代码放置在内存中的适当位置。
- 重定位:调整程序内所有的地址引用,使它们指向正确的内存位置。

