5、C++高频面试真题|41-50

厨子大约 16 分钟编程语言原创技术面试题解析C++面试程序员求职程序厨

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 可以让我们精确控制模板的类型。

C++ 中 nullptr NULL 对比表
image-20250511162201399
image-20250511162201399

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. 指针和引用的区别

回答重点

  1. 引用必须在声明时初始化,指针可以不需要初始化
// 引用示例
int a = 10;
int& ref_a = a; // 引用初始化
// int& ref_b; // 错误:引用必须在声明时初始化
// 指针示例
int b = 20;
int* ptr_b = &b; // 指针初始化
int* ptr_c = nullptr; // 指针可以为空
  1. 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名,引用本身并不存储地址,而是在底层通过指针来实现对原变量的访问
  2. 引用被创建之后,就不可以进行更改,指针可以更改
##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;
}
  1. 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

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;
}

在这个例子中,staticDataMyClass 类的静态数据成员,obj1obj2 共享这一成员,修改 obj1staticData 后,obj2staticData 也会改变。

静态成员函数

静态成员函数是用 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;
}
image-20250511162217426
image-20250511162217426

48. 静态成员变量为什么要在类外初始化?

回答重点

  1. 存储空间分配

    • 因为静态变量不属于任何特定对象,而是类的共享成员,需要在全局范围内分配存储空间。静态变量的存储空间在程序启动时分配,生命周期贯穿整个程序运行。类内声明仅告知编译器该变量的存在,而类外定义则实际分配内存。
  2. 避免重复定义

    • 如果在类内初始化静态变量,可能会导致多个编译单元中包含该变量的定义,导致链接错误。类外定义确保静态变量只在一个编译单元中定义,避免重复。

扩展知识

  1. 静态成员函数

    • 静态成员函数只能访问静态成员变量,不能访问非静态成员变量,因为它们没有 this 指针。
  2. 静态常量成员

    • 静态常量成员(如 static const int)可以在类内直接初始化,因为它们在编译时已知,并且不需要额外的存储空间。
  3. 内联静态成员

    • 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 的 .debrpm 包)。

image-20250511162229101
image-20250511162229101

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)链接

链接器将所有目标文件和所需的库文件链接起来,生成最终的可执行文件。主要做以下工作:

  • 符号解析:找到所有外部符号(如函数、变量)在目标文件和库文件中的定义。
  • 地址分配:将目标代码放置在内存中的适当位置。
  • 重定位:调整程序内所有的地址引用,使它们指向正确的内存位置。