📋 摘要

值传递和引用传递的区别?

底层机制

维度值传递引用传递
底层机制调用时触发一次参数拷贝传递参数引用别名,无拷贝
修改参数不影响原始值直接作用于原始值
大对象性能较低(复制整个对象)较高(零拷贝)

代码示例

值传递,函数收到的是独立副本:

C++
void modifyByValue(int x) {
    x = 100; // 只修改副本,不影响原始变量
}

int main() {
    int a = 10;
    modifyByValue(a);
    // a 仍为 10
}

引用传递,函数操作的就是原始变量:

C++
void modifyByReference(int& x) {
    x = 100; // 直接修改原始变量
}

int main() {
    int a = 10;
    modifyByReference(a);
    // a 变为 100
}

使用场景

引用传递:

  • 避免不必要的数据拷贝:大对象(std::vector、std::string、std::list)拷贝开销大,引用传递零拷贝。
  • 允许函数修改实参:希望函数直接修改传入变量时使用。
C++
// 大对象只读:用 const 引用,零拷贝 + 保护原始值
void printVector(const std::vector<int>& vec) {
    for (int v : vec) {
        std::cout << v << " ";
    }
}

值传递:

  • 小型数据:int、char、double、float 等基础类型,拷贝成本低,值传递简洁直观。需要保护原始值时,用 const 引用 即可,不必为了「不修改」而拷贝大对象。
💡 const 引用 —— 兼顾安全与性能

只读大对象参数,优先 const T&(如 const std::string&):既避免拷贝开销,又防止意外修改原始值。这是 C++ 最常用的参数传递模式之一。


C 和 C++ 的区别?

编程范式

C 语言的核心思想是面向过程:把问题分解为一系列函数调用,数据结构和操作分离。

C++ 在此基础上引入了面向对象范式:用类封装数据和方法,通过继承、多态组织代码。同时 C++ 也不排斥过程式编程——写 C 风格的代码在 C++ 里完全合法。

继承与多态

C++
// C++ 继承与虚函数多态
class Animal {
public:
    virtual void speak() { std::cout << "..." << std::endl; }
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof!" << std::endl; }
};

C 语言没有类和继承,要实现类似效果只能用结构体 + 函数指针手动模拟,代码冗长且容易出错。

函数重载

C++
// C++ 支持重载
void print(int x) { std::cout << x << std::endl; }
void print(double x) { std::cout << x << std::endl; }
void print(const std::string& x) { std::cout << x << std::endl; }

// C 语言必须用不同函数名:print_int(), print_double(), print_string()

模板

C++
// C++ 模板 —— 编译期泛型
template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

C 语言没有模板,通常用 #define 宏或 void* 泛型指针来模拟,但都失去了类型安全。

内存管理对比

CC++
分配/释放malloc / freenew / delete
构造函数不调用自动调用构造/析构
智能管理无unique_ptr、shared_ptr(C++11)
RAII无对象生命周期自动管理资源
C++
// C 风格
int* p = (int*)malloc(sizeof(int) * 10);
free(p);

// C++ 风格
int* p = new int[10];
delete[] p;

// C++ 智能指针(推荐)
auto p = std::make_unique<int[]>(10); // 自动释放,无需手动 delete

标准库

C 的标准库主要集中在 I/O、字符串操作、内存管理、数学计算等基础功能:

  • stdio.h、stdlib.h、string.h、math.h

C++ STL 则提供了容器、算法、迭代器、智能指针等完整生态:

  • 容器:vector、list、map、unordered_map、set、queue、stack
  • 算法:sort、find、for_each、transform 等
  • 字符串:std::string(自动内存管理,比 C 的 char* 安全得多)
  • 智能指针:unique_ptr、shared_ptr、weak_ptr
💡 面试加分项

如果面试官追问,可以提一下 C++ 还有 RAII(资源获取即初始化)、移动语义(C++11 std::move)、lambda 表达式、右值引用 等 C 完全没有的特性,这些都是 C++ 在安全和性能上的重要进步。


C++ 的函数重载?它的优点是什么?和重写有什么区别?

什么是函数重载

函数重载(Function Overloading)指:在同一作用域内,多个函数函数名相同,但参数列表不同(参数个数、类型或顺序不同)。编译器根据实参类型在编译期决定调用哪个版本。

C++
void print(int x)    { std::cout << "int: " << x << std::endl; }
void print(double x) { std::cout << "double: " << x << std::endl; }
void print(int x, int y) { std::cout << x << ", " << y << std::endl; }

print(1);        // 调用 print(int)
print(3.14);     // 调用 print(double)
print(1, 2);     // 调用 print(int, int)

重载条件(关键):

  • 参数列表必须不同(个数、类型、顺序任一不同即可)
  • 仅返回类型不同不能构成重载(编译器无法通过返回类型区分)
C++
int  func(int x)  { return x; }
// double func(int x) { return x; }   ❌ 仅返回类型不同,无法重载

优点

  1. 提高可读性与一致性:同一操作(如打印)用同一名字,调用者无需记忆多个函数名(print_int、print_double…)
  2. 接口统一:表达”同一概念、不同输入”的语义
  3. 编译期多态:一种静态多态,无运行时开销

与重写(override)的区别

维度函数重载(overload)函数重写(override)
作用域同一作用域(同类或全局)不同作用域(基类 vs 派生类)
函数名相同相同
参数列表不同相同
返回类型可同可不同相同(或协变返回类型)
virtual不要求基类函数必须是虚函数
绑定时机编译期(静态绑定)运行期(动态绑定)
标记关键字无override(C++11 建议加)
C++
class Base {
public:
    virtual void func(int x) { /* ... */ }   // 基类虚函数
};

class Derived : public Base {
public:
    void func(int x) override { /* ... */ }  // ✅ 重写(override):同名同参,覆盖基类虚函数
    void func(double x) { /* ... */ }        // ✅ 重载(overload):同名不同参,新的函数
};
💡 面试加分项

回答时先讲重载的定义(同作用域、同名、参数列表不同、编译期绑定),再讲优点(可读性、接口统一、编译期多态),最后用一张对比表讲清重载 vs 重写的核心区别——作用域不同、参数列表不同、绑定时机不同(编译期 vs 运行期)。能提到”仅返回类型不同不能重载”这个易错点更好。


C++ 中 sizeof 和 strlen 的区别?

核心区别

维度sizeofstrlen
本质运算符(operator)函数(<cstring> 库函数)
计算时机编译期求值运行期求值
返回值返回占用字节数(size_t)返回字符个数(size_t),不包括 \0
适用对象任何类型(变量、类型名、表达式)仅适用于 C 风格字符串(char*)
是否包含 \0数组包含 \0不包含 \0

sizeof —— 编译期运算符

sizeof 是一个运算符(不是函数),在编译期就能确定结果:

C++
int a = 10;
std::cout << sizeof(a) << std::endl;       // 4(int 的大小)
std::cout << sizeof(int) << std::endl;      // 4(直接对类型求值)
std::cout << sizeof(double) << std::endl;   // 8
std::cout << sizeof(char) << std::endl;     // 1

// 数组:返回整个数组的字节数
char str[] = "hello";
std::cout << sizeof(str) << std::endl;      // 6("hello" + '\0')

// 指针:只返回指针本身的大小
char* ptr = "hello";
std::cout << sizeof(ptr) << std::endl;      // 8(64 位系统指针大小)
std::cout << sizeof(char*) << std::endl;    // 8

重要特性:sizeof 的参数不会被求值

C++
sizeof(a + b);   // 不会真正执行 a + b,只是计算结果的类型大小
sizeof(int());   // 不会调用构造函数

strlen —— 运行期函数

strlen 需要遍历字符串直到遇到 \0,在运行期计算:

C++
#include <cstring>

char str[] = "hello";
std::cout << strlen(str) << std::endl;      // 5(不包含 '\0')

char str2[] = "hello\0world";
std::cout << strlen(str2) << std::endl;     // 5(遇到第一个 '\0' 就停止)
std::cout << sizeof(str2) << std::endl;     // 11(整个数组大小)

经典对比

C++
char str[] = "hello world";

std::cout << sizeof(str) << std::endl;   // 12(11 个字符 + '\0')
std::cout << strlen(str) << std::endl;   // 11(不包含 '\0')

char* ptr = "hello world";

std::cout << sizeof(ptr) << std::endl;   // 8(指针大小)
std::cout << strlen(ptr) << std::endl;   // 11(正常遍历到 '\0')

std::cout << sizeof("hello world") << std::endl;  // 12(字符串字面量包含 '\0')
std::cout << strlen("hello world") << std::endl;   // 11
⚠️ 常见陷阱

sizeof 对指针和数组的行为完全不同!这是面试高频考点。在函数参数中,数组退化为指针,所以 sizeof 返回的是指针大小,不是数组大小。

💡 面试加分项

回答时先区分 sizeof 是运算符(编译期求值)、strlen 是函数(运行期求值)。然后强调关键差异:sizeof 返回字节数、strlen 返回字符数且不包含 \0。如果能举例说明数组 vs 指针在 sizeof 下的不同结果(面试高频考点),可以加分。


C++ 中 extern 有什么作用?extern “C” 有什么作用?

extern 的基本作用

extern 关键字告诉编译器:这个变量或函数在其他地方定义,当前文件只是声明引用它。

extern 变量

C++
// file1.cpp —— 定义全局变量
int globalVar = 100;

// file2.cpp —— 声明引用(不重新定义)
extern int globalVar;  // 告诉编译器 globalVar 在别处定义
void print() {
    std::cout << globalVar << std::endl;  // 可以正常使用
}

使用场景:

  • 多个文件需要共享同一个全局变量
  • 避免在每个文件都重新定义变量(违反 ODR 原则)
C++
// ❌ 错误:多个文件定义同名全局变量 → 链接错误(重复定义)
// file1.cpp: int x = 1;
// file2.cpp: int x = 2;

// ✅ 正确:只在一个文件定义,其他文件 extern 声明
// file1.cpp: int x = 1;
// file2.cpp: extern int x;

extern 函数

C++
// file1.cpp
void doSomething() { std::cout << "Hello" << std::endl; }

// file2.cpp
extern void doSomething();  // 声明
doSomething();              // 调用

实际上,普通函数的前置声明(forward declaration)本身就是 extern 的,加不加 extern 效果一样:

C++
void doSomething();        // 等同于 extern void doSomething();

extern “C” —— C/C++ 混编的核心

这是面试的重点。extern "C" 的作用是使用 C 语言的链接规则(linking convention)。

为什么需要 extern “C”?

C 和 C++ 的**名称修饰(name mangling)**规则不同:

  • C 语言:函数名在目标文件中基本保持原名(如 add → _add)
  • C++:编译器会对函数名进行修饰,编码参数类型信息(如 add(int, int) → _Z3addii)

这导致一个问题:C++ 调用了 C 编写的库函数,但链接时找不到——因为名称被修饰了。

Text
C 编译:    add(int, int) →  _add
C++ 编译:  add(int, int) →  _Z3addii
C++ 编译:  add(int, char) →  _Z3addic   (重载,所以必须区分)

extern "C" 告诉 C++ 编译器:按照 C 语言的方式处理这个函数,不做名称修饰:

C++
// math_lib.c(C 语言库)
int add(int a, int b) { return a + b; }

// main.cpp(C++ 程序)
extern "C" {
    int add(int a, int b);  // 按 C 规则链接,不做名称修饰
}

int main() {
    std::cout << add(1, 2) << std::endl;  // 可以正常调用 C 函数
}

完整写法

C++
extern "C" {
    #include "c_library.h"  // 整个头文件的函数都按 C 规则链接
}

或更常见的跨平台写法(头文件中):

C++
#ifdef __cplusplus
extern "C" {
#endif

void my_c_function();
int another_c_function(int x);

#ifdef __cplusplus
}
#endif

__cplusplus 是 C++ 编译器自动定义宏,这样同个头文件可以被 C 和 C++ 同时包含。

使用场景:

  • C++ 项目中调用 C 语言编写的库(如 OpenSSL、SQLite 等大量使用)
  • 提供 C 兼容的 API 接口(其他语言如 Python、Go 通过 FFI 调用 C++ 功能)
💡 面试加分项

回答 extern 时,先讲 extern 变量的基本用法(跨文件共享),然后重点讲 extern "C"——这是高频考点。核心是名称修饰(name mangling):C 语言不做名称修饰,C++ 需要修饰以支持函数重载。extern "C" 就是告诉 C++ 编译器按 C 的方式链接。如果能提到 __cplusplus 宏的跨平台写法,说明你有实际混编经验。


C++ 中数组和指针的区别?

核心区别一览

维度数组指针
本质一块连续的内存空间存储地址的变量
内存分配栈上分配,编译器确定大小本身在栈上,指向的内存可在堆上动态分配
大小声明时确定,不可改变无大小限制,取决于指向的内存
sizeof返回整个数组的字节数返回指针本身的字节数(4/8)
算术运算数组名不能 ++ / --可以 ++ / -- / +n / -n
类型安全声明时绑定类型,编译器检查可强制类型转换,易出现类型不匹配

1. 内存分配

数组:编译器在声明时就在栈上为所有元素分配连续的内存空间:

C++
int arr[5] = {1, 2, 3, 4, 5};
// 栈上分配 5 * sizeof(int) = 20 字节的连续空间
// arr 代表这块内存的起始地址

指针:指针本身只占一个内存单元(通常是 8 字节),存储的是一个地址:

C++
int* ptr;            // 栈上只分配 8 字节(存储地址),尚未指向有效内存
ptr = new int[5];    // 堆上分配 20 字节,ptr 指向堆内存首地址
// 或
ptr = (int*)malloc(5 * sizeof(int));
Text
数组的内存布局:
  栈上: [1][2][3][4][5]   ← arr 指向这里,连续分配,大小固定

指针的内存布局:
  栈上: [ptr: 0x7fff...]  ← 指针变量本身只存地址
  堆上: [ ? ][ ? ][ ? ]   ← ptr 指向这里,动态分配,大小灵活

2. 固定大小 vs 动态大小

数组大小固定:声明时必须确定大小,且必须是编译期常量:

C++
int arr[5];           // ✅ 大小固定为 5
int n = 10;
int arr2[n];          // ❌ C++ 中不允许变长数组(VLA,这是 C99 特性,非标准 C++)

// C++ 中动态大小只能用指针 + new
int* arr3 = new int[n];  // ✅ 堆上动态分配
delete[] arr3;            // 需要手动释放

指针灵活动态:可以在运行时决定大小:

C++
int n;
std::cin >> n;       // 运行时确定大小
int* arr = new int[n];  // ✅ 根据运行时输入分配
delete[] arr;

3. 类型安全性

数组类型检查:

C++
int arr[5] = {1, 2, 3, 4, 5};
arr[0] = "hello";    // ❌ 编译错误:不能把字符串赋给 int 类型

指针类型转换的风险:

C++
int value = 42;
int* iptr = &value;
void* vptr = iptr;   // ✅ 任何类型指针都可以隐式转为 void*
char* cptr = (char*)iptr;  // ⚠️ 强制类型转换,类型不匹配,可能导致未定义行为

指针类型转换频繁时,容易丢失类型信息,产生难以排查的运行时错误。

4. 运算操作

数组名是常量指针,不能修改:

C++
int arr[5] = {1, 2, 3, 4, 5};
// arr++;            ❌ 编译错误:数组名是常量指针,不能进行算术运算
// arr--;            ❌ 同上
// arr = other_arr;  ❌ 数组名不能被赋值
std::cout << arr[0] << std::endl;  // ✅ 但可以通过下标访问

指针可以自由移动:

C++
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;  // ptr 指向数组首元素
std::cout << *ptr << std::endl;   // 输出 1
ptr++;                              // ✅ 指针递增,指向下一个元素
std::cout << *ptr << std::endl;   // 输出 2
ptr += 2;                           // ✅ 跳过两个元素
std::cout << *ptr << std::endl;   // 输出 4

重要知识点:数组名退化为指针

这是面试中最容易被追问的部分。数组名在大多数表达式中会**隐式退化(decay)**为指向首元素的指针:

C++
int arr[5] = {1, 2, 3, 4, 5};

// 在表达式中,数组名退化为指针
int* ptr = arr;     // ✅ arr 退化为 int*,指向 arr[0]
std::cout << *arr;  // ✅ arr 退化为 int*,解引用得到 arr[0] 即 1

但有两个例外,数组名不会退化:

C++
int arr[5] = {1, 2, 3, 4, 5};

// 例外 1:sizeof 运算符
std::cout << sizeof(arr);     // 输出 20(整个数组的字节数),不是 8(指针大小)
std::cout << sizeof(*arr);    // 输出 4(单个元素的字节数)

// 例外 2:取地址运算符 &
std::cout << sizeof(&arr);    // 输出 8(指针大小,这里是指向整个数组的指针 int(*)[5])

函数参数中的退化(重点!):

C++
void func(int arr[]) {
    std::cout << sizeof(arr);  // 输出 8(指针大小),不是 20!
    // 在函数参数中,int arr[] 等价于 int* arr,数组名已经退化为指针
}

这意味着:在 C/C++ 函数参数中,int arr[] 和 int* arr 是完全等价的,编译器都将它们视为指针。这也是为什么 C 语言的函数通常需要一个额外的 size 参数来传递数组长度。


sizeof 对数组和指针的不同结果

C++
int arr[10] = {0};
int* ptr = arr;

std::cout << sizeof(arr) << std::endl;   // 40(10 * sizeof(int))
std::cout << sizeof(ptr) << std::endl;   // 8(指针大小,64 位系统)
std::cout << sizeof(*ptr) << std::endl;  // 4(int 大小)

这个区别是判断”当前操作的对象是数组还是指针”的最常用方法。


现代 C++ 的替代方案

原生数组和指针虽然灵活,但容易出现内存泄漏、越界访问等问题。现代 C++ 提供了更安全的替代方案:

C++
// 替代固定大小数组 → std::array(栈上分配,编译期确定大小)
#include <array>
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << arr.size();      // 5,有大小信息
std::cout << arr.at(10);      // 越界抛出异常,比 arr[10](未定义行为)安全

// 替代动态数组 → std::vector(堆上分配,动态大小)
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.push_back(6);             // 动态扩容
std::cout << vec.size();      // 6,自动管理大小
// 自动内存管理,无需手动 new/delete
对比原生数组std::arraystd::vector
大小编译期固定编译期固定运行期可变
内存栈栈堆
内存管理手动自动自动
边界检查❌.at() ✅.at() ✅
大小信息易丢失(退化为指针)✅ .size()✅ .size()
💡 面试加分项

回答时可以按照”内存分配 → 大小灵活性 → 类型安全 → 运算操作”四个维度展开,然后重点补充一个面试常考点:数组名退化为指针的机制(sizeof 和函数参数中的特殊行为)。最后如果能提到 std::array 和 std::vector 作为现代 C++ 的替代方案,以及它们相比原生数组的优势(边界检查、自动内存管理、不丢失大小信息),可以很好地展现你对现代 C++ 实践的理解。


C++ 中 struct 和 class 的区别?

核心区别

struct 和 class 几乎完全一样,唯一的本质区别是默认访问权限:

structclass
默认成员访问权限publicprivate
默认继承权限public 继承private 继承
C++
struct S {
    int x;      // 默认 public
};
S s;
s.x = 10;       // ✅ 可直接访问

class C {
    int x;      // 默认 private
};
C c;
// c.x = 10;    ❌ 编译错误,private 不能外部访问

其它方面完全一致

除了默认权限,两者能力完全相同:

  • 都能有成员函数、构造函数、析构函数
  • 都能继承、多态(虚函数)
  • 都能有静态成员、运算符重载
  • 都能用模板
C++
struct Base { virtual void f() {} };   // struct 也能多态
class Derived : public Base { void f() override {} };

使用习惯(约定俗成)

structclass
语义纯数据聚合(POD、简单数据结构)封装对象(有行为、不变量)
典型场景Point{ x, y }、配置项、消息结构带方法的对象、RAII 类、业务实体
💡 面试加分项

回答时一句话给出核心:“唯一区别是默认访问权限——struct 默认 public,class 默认 private”,再补充”默认继承权限也随之不同(struct public 继承、class private 继承)“,最后说明两者能力完全等价、只是习惯上 struct 用于纯数据结构、class 用于封装对象。


C++ 中 struct 和 union 的区别?如何使用 union 做优化?

核心区别

structunion
内存分配每个成员各自独立占用内存所有成员共享同一块内存
成员有效性所有成员同时有效同一时刻只有一个成员有效
大小所有成员大小之和 + padding最大成员的大小(+ padding)
用途组合多个不同类型数据同一内存空间的多种解读方式
C++
struct S {
    char a;   // 1 字节
    int  b;   // 4 字节
    char c;   // 1 字节
};
// sizeof(S) = 12(各成员独立 + padding)

union U {
    char a;   // 1 字节
    int  b;   // 4 字节
    char c;   // 1 字节
};
// sizeof(U) = 4(只取最大成员 int 的大小)

如何使用 union 做优化

union 的核心价值是复用内存、节省空间:当多个字段不会同时使用时,让它们共享内存。

C++
// 优化前:普通 struct,所有字段同时占内存
struct Message {
    int type;          // 1=整数 2=浮点 3=字符串指针
    int intValue;      // 占用
    double doubleValue;// 占用
    char* strValue;    // 占用
};
// sizeof(Message) 很大

// 优化后:union 让互斥字段共享内存
struct Message {
    int type;
    union {             // 三个 value 共享一块内存,节省空间
        int intValue;
        double doubleValue;
        char* strValue;
    } data;
};
// 只占 type + max(int, double, char*) 的大小

典型场景:网络协议消息、不同类型数据但同一时刻只用一种、内存受限的嵌入式系统。

⚠️ union 的使用注意
  • 同一时刻只能存一个成员,写入新成员会覆盖旧成员
  • 读取非当前激活成员是未定义行为(UB)
  • 现代 C++ 更推荐类型安全的 std::variant(C++17)替代 union
C++
#include <variant>
std::variant<int, double, std::string> v;  // 类型安全,记住当前类型
v = 42;
int i = std::get<int>(v);   // 安全访问
💡 面试加分项

回答时先讲核心区别(struct 成员各自独立、union 成员共享内存、同一时刻只有一个有效),再用”消息结构优化”的例子说明 union 的内存复用价值,最后点出 union 的陷阱(访问非激活成员是 UB)和 std::variant 的现代替代。


C++ 中 new 和 malloc 的区别?delete 和 free 的区别?

new vs malloc

维度newmalloc
本质运算符(operator)库函数(<cstdlib>)
返回类型类型安全的指针(T*)void*,需强转
失败行为抛 std::bad_alloc 异常返回 NULL
计算大小自动(sizeof(T))手动(需传字节数)
构造函数自动调用不调用
能否重载可重载不可重载
C++
#include <cstdlib>

// new:类型安全,自动算大小,自动调构造函数
int* p1 = new int(10);              // 分配 + 初始化为 10
MyClass* p2 = new MyClass();        // 自动调用构造函数

// malloc:返回 void*,手动算大小,不调构造函数
int* p3 = (int*)malloc(sizeof(int));  // 需强转 + 手动 sizeof
*p3 = 10;
MyClass* p4 = (MyClass*)malloc(sizeof(MyClass));  // 不调构造函数,成员未初始化!

delete vs free

维度deletefree
本质运算符库函数
析构函数自动调用不调用
数组形式delete[] 释放数组无需区分(free 一次即可)
C++
// delete:自动调用析构函数
MyClass* p = new MyClass();
delete p;          // 先调 ~MyClass(),再释放内存

// free:不调析构函数,可能泄漏资源
MyClass* p2 = (MyClass*)malloc(sizeof(MyClass));
free(p2);          // 只释放内存,不调析构(若有资源会泄漏!)

// 数组
int* arr = new int[10];
delete[] arr;      // 必须用 delete[] 释放数组

核心总结

Text
new    = malloc + 构造函数
delete = 析构函数 + free

new/delete 是 C++ 对象生命周期管理的一部分,会调用构造/析构;
malloc/free 只是纯粹的内存分配/释放,不涉及对象语义。
⚠️ 混用会出错

new 分配的内存必须用 delete 释放,malloc 分配的必须用 free 释放。混用(new 配 free、malloc 配 delete)是未定义行为。

💡 面试加分项

回答时抓住一条主线:new = malloc + 构造函数,delete = 析构函数 + free。再展开差异:类型安全 vs void*、失败抛异常 vs 返回 NULL、自动算大小 vs 手动 sizeof、可重载 vs 不可重载。最后强调不能混用,以及现代 C++ 推荐用智能指针而非裸 new/delete。


C++ 中 this 指针的作用?

什么是 this 指针

this 是成员函数内部的一个隐含指针,指向调用该成员函数的当前对象。每个非静态成员函数都能使用 this。

C++
class Person {
public:
    Person(int age) {
        this->age = age;   // this 指向当前正在构造的对象
    }
    int age;
};

作用

  1. 区分同名局部变量和成员变量(最常见)
C++
class Person {
    int age;
public:
    void setAge(int age) {
        this->age = age;   // 左边是成员变量,右边是形参
    }
};
  1. 返回对象自身(实现链式调用)
C++
class Counter {
    int count = 0;
public:
    Counter& add(int n) {
        count += n;
        return *this;   // 返回当前对象
    }
};

Counter c;
c.add(1).add(2).add(3);   // 链式调用
  1. 在成员函数中访问成员(隐式使用)
C++
void func() {
    // 实际上编译器会隐式加上 this->
    // this->member = ...;
}

特性

  • this 是常量指针(T* const),不能修改指向
  • 只能在非静态成员函数中使用(静态成员函数没有 this)
  • 本质是成员函数的一个隐藏参数
💡 面试加分项

回答时先讲定义(成员函数内指向当前对象的隐含指针),再讲三大作用(区分同名变量、返回 *this 链式调用、隐式访问成员),最后补充特性(this 是 T* const 常量指针、静态成员函数没有 this)。


C++ 中 memcpy 和 memmove 有什么区别?

核心区别

两者都是内存拷贝函数,区别在于是否处理内存重叠(overlap):

memcpymemmove
重叠处理不保证(重叠是未定义行为)保证正确(先复制到临时缓冲区/调整拷贝方向)
性能更快(直接拷贝)略慢(重叠时需中转)
原型void* memcpy(void* dest, const void* src, size_t n)void* memmove(void* dest, const void* src, size_t n)

内存重叠问题

当 src 和 dest 指向的内存区域有重叠时,memcpy 可能出错,memmove 保证正确:

C++
#include <cstring>

char str[] = "hello world";

// 把 "hello" 向后移动 6 字节,但 src 和 dest 重叠了
memcpy(str + 6, str, 5);   // ❌ 未定义行为,结果可能错误
memmove(str + 6, str, 5);  // ✅ 正确处理重叠

为什么 memcpy 处理重叠会出错

memcpy 直接按地址顺序拷贝,重叠时可能覆盖掉还没拷贝的源数据。memmove 会判断 src 和 dest 的相对位置,选择从前往后或从后往前拷贝,或用临时缓冲区,保证正确。

C++
// memmove 的"安全"策略:根据重叠方向选择拷贝顺序
// 如果 dest < src(目标在前),从前往后拷贝
// 如果 dest > src(目标在后),从后往前拷贝

使用建议

  • 确定不重叠 → 用 memcpy(更快)
  • 可能重叠(如字符串插入、数组移位)→ 用 memmove(安全)
💡 面试加分项

回答时一句话点出核心:“memcpy 不处理内存重叠,memmove 保证重叠时也正确”。然后举例说明重叠场景,解释 memmove 通过判断拷贝方向/临时缓冲区保证正确,最后给出选择建议:不确定重叠就用 memmove。


C++ 的 strcpy 和 memcpy 有什么区别?

核心区别

strcpymemcpy
用途拷贝字符串拷贝任意内存
结束条件遇 \0 停止按指定字节数拷贝
参数char* strcpy(char* dest, const char* src)void* memcpy(void* dest, const void* src, size_t n)
需要长度否(自动找 \0)是(必须传 n)
处理 \0会拷贝 \0不关心 \0,按字节拷贝
C++
#include <cstring>

// strcpy:拷贝字符串,遇 \0 停止,会带上 \0
char src[] = "hello";
char dest[10];
strcpy(dest, src);       // dest = "hello\0"

// memcpy:按字节拷贝,需要指定长度
char src2[] = "hello";
char dest2[10];
memcpy(dest2, src2, 5);  // 只拷贝 5 字节,不含 \0
// dest2 是 "hello" 但没结束符,需要自己处理
memcpy(dest2, src2, 6);  // 拷贝 6 字节含 \0,等价 strcpy

关键差异

  1. strcpy 遇 \0 停止:无法拷贝含 \0 的数据(如二进制);memcpy 按字节数拷贝,能拷任何数据
  2. strcpy 不需要长度:但依赖源字符串正确以 \0 结尾,否则越界读;memcpy 需要显式传长度
  3. 安全性:两者都有缓冲区溢出风险,strcpy 更危险(无长度限制),现代 C++ 推荐 strcpy_s 或 std::string
C++
// strcpy 的安全隐患:源字符串太长会溢出
char small[3];
char* long_str = "this is a very long string";
// strcpy(small, long_str);   ❌ 缓冲区溢出!

// 现代替代
std::string s = "hello";   // 自动管理内存,安全
💡 面试加分项

回答时抓住三点:用途不同(strcpy 字符串 vs memcpy 任意内存)、结束条件不同(遇 \0 vs 指定字节数)、长度要求不同(strcpy 自动 vs memcpy 手动)。最后补充安全性问题——strcpy 无长度限制易溢出,现代用 std::string 替代。


C++ 中堆内存和栈内存的区别?

核心区别一览

维度栈(Stack)堆(Heap)
分配方式编译器自动分配释放程序员手动 new/delete(或智能指针)
分配速度快(移动栈指针即可)慢(查找空闲块、维护空闲链表)
大小限制小(默认约 1~8MB)大(受系统虚拟内存限制)
生命周期作用域结束自动释放手动释放,否则内存泄漏
碎片无(连续分配)有(频繁分配释放产生碎片)
适用场景局部变量、函数参数大对象、动态大小、跨作用域存活

栈内存(Stack)

栈由编译器管理,函数调用时自动分配局部变量,函数返回时自动回收:

C++
void func() {
    int a = 10;        // 栈上分配
    int arr[100];      // 栈上分配连续空间
    // 函数返回时自动释放,无需手动管理
}

堆内存(Heap)

堆由程序员通过 new/malloc 申请,需显式释放:

C++
int* p = new int;          // 堆上分配
int* arr = new int[100];   // 堆上分配数组
// ... 使用 ...
delete p;                  // 手动释放
delete[] arr;
⚠️ 常见陷阱

堆内存忘记 delete 会内存泄漏;栈内存虽然自动管理,但容量小,局部大数组或深递归容易导致栈溢出。

💡 面试加分项

回答时按「分配方式 → 速度 → 大小 → 生命周期 → 碎片」对比,重点强调:栈自动、快、小;堆手动、慢、大。能提到「栈溢出」和「内存泄漏」两个典型问题更好。


C++ 的栈溢出是什么?

什么是栈溢出

栈溢出(Stack Overflow)指程序在栈上分配的内存超过了栈的最大容量,导致栈空间被耗尽,通常触发段错误(Segmentation Fault)或程序崩溃。

常见原因

  1. 局部大数组:在函数内声明超大数组
  2. 递归过深 / 无限递归:每次递归调用都占用栈帧,层级过深耗尽栈
  3. 局部变量过多:函数内累积大量局部变量

代码示例

C++
// 1. 局部大数组 —— 超出栈容量(默认约 8MB)
void func() {
    int big[10 * 1024 * 1024];   // 约 40MB,远超默认栈大小 → 栈溢出
}

// 2. 无限递归 —— 每次调用都压栈,最终耗尽
void recurse() {
    recurse();   // 没有终止条件,栈帧无限增长 → 栈溢出
}

// 3. 深递归 —— 递归层数过深
int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);   // 层数深时也可能栈溢出
}

如何避免

  • 大对象改用堆分配(new / std::vector)
  • 递归转迭代,或用尾递归(编译器优化)
  • 控制递归深度,必要时增大栈大小
C++
// 大数组放堆上
std::vector<int> big(10 * 1024 * 1024);   // 堆上分配,不受栈大小限制
⚠️ 注意

栈溢出通常是运行时错误,编译能通过但运行崩溃,调试时表现为段错误(Segmentation Fault)。

💡 面试加分项

一句话定义:栈上分配超过栈容量。然后讲两大原因(局部大数组、递归过深),并给出解决方案(改堆分配、递归转迭代)。


C++ 中为什么要使用 nullptr 而不是 NULL?

核心区别

维度NULLnullptr
本质宏,通常是 0 或 (void*)0关键字,类型是 std::nullptr_t
类型int 或 void*(实现相关)专属指针空值类型
类型安全会被误解析为整数只能转换为指针,类型安全
重载决议可能匹配到 int 重载精确匹配指针重载
标准C 遗留C++11 引入

问题:NULL 的类型歧义

NULL 本质是宏,展开后是 0,会引发重载歧义:

C++
void func(int x)     { std::cout << "int" << std::endl; }
void func(char* p)   { std::cout << "pointer" << std::endl; }

func(NULL);    // 可能调用 func(int),而不是 func(char*),不符合预期
func(nullptr); // 明确调用 func(char*),类型安全

nullptr 的优点

C++
// nullptr 有专属类型 std::nullptr_t,能隐式转换为任意指针类型
int* p1 = nullptr;          // ✅
char* p2 = nullptr;         // ✅
std::nullptr_t n = nullptr; // 专属类型

// NULL 是整数 0,模板推导时会被当作 int
template<typename T>
void f(T t) { /* ... */ }
f(NULL);     // T 推导为 int
f(nullptr);  // T 推导为 std::nullptr_t
💡 面试加分项

核心一句话:NULL 是宏(本质是 0),nullptr 是 C++11 引入的类型安全空指针常量(std::nullptr_t)。重点讲重载决议时 NULL 可能误匹配 int 重载,而 nullptr 精确匹配指针。


C++ 中 include <a.h> 和 include "a.h" 有什么区别?

核心区别

两者都是预处理指令,在编译前把被包含文件的内容原样插入到当前文件。区别在于查找头文件的搜索路径顺序:

#include <a.h>#include "a.h"
查找路径只从系统/标准库目录查找先查当前源文件所在目录,再查系统目录
典型用途标准库、第三方库头文件项目自定义头文件
是否查当前目录否是(优先)

查找顺序

Text
#include <a.h>   →  ① 系统 include 目录(如 /usr/include)→ ② 编译器 -I 指定的目录
#include "a.h"   →  ① 当前源文件所在目录 → ② 系统 include 目录 → ③ 编译器 -I 指定的目录

代码示例

C++
#include <iostream>    // 标准库头文件,用尖括号
#include <vector>      // 系统目录查找
#include "my_header.h" // 项目自定义头文件,用双引号(先查当前目录)
💡 面试加分项

回答时先点明「两者都是预处理阶段把文件内容插入当前文件」,再讲唯一实质区别——查找路径顺序不同:尖括号只查系统目录,双引号先查当前目录再查系统目录。习惯上标准库用尖括号、自定义头文件用双引号。


C++ 是否可以 include 源文件?

结论

技术上可以(预处理阶段会原样展开 .cpp 文件),但工程上强烈不推荐,是坏实践。

为什么可以但不推荐

#include 本质是文本替换,不区分文件扩展名,.cpp、.h、任意文本都能被包含。但 include 源文件会带来严重问题:

C++
// main.cpp
#include "util.cpp"   // ❌ 把整个源文件展开进来,重复定义

// util.cpp
int add(int a, int b) { return a + b; }

问题:重复定义

如果 util.cpp 同时被编译和包含,会触发 ODR(单一定义规则)违规:

Text
编译命令:g++ main.cpp util.cpp -o app
结果:add 函数被定义两次 → 链接错误(multiple definition)

正确做法

头文件放声明,源文件放定义:

C++
// util.h —— 只放声明
int add(int a, int b);

// util.cpp —— 放定义
#include "util.h"
int add(int a, int b) { return a + b; }

// main.cpp —— 包含头文件
#include "util.h"
int main() {
    return add(1, 2);
}
⚠️ 例外说明

模板和 inline 函数因需要在编译期看到完整定义,会放在头文件中,这是规范允许的特例,不是「include 源文件」。

💡 面试加分项

一句话回答:「可以,但不要这么做」。核心是 #include 是文本替换,include 源文件会导致重复定义(链接错误)。正确做法是头文件放声明、源文件放定义。


C++ 中命名空间有什么作用?如何使用?

作用

命名空间(namespace)用于避免命名冲突:把全局标识符(变量、函数、类、类型)按逻辑分组,不同命名空间的同名符号互不干扰。

核心问题:命名冲突

C++
// 两个库都定义了 print,直接放全局会冲突
namespace LibA {
    void print() { std::cout << "A" << std::endl; }
}
namespace LibB {
    void print() { std::cout << "B" << std::endl; }
}

LibA::print();   // ✅ 通过命名空间限定,互不冲突
LibB::print();

使用方式

  1. 定义命名空间
C++
namespace MySpace {
    int value = 10;
    void func() { /* ... */ }
    class MyClass { /* ... */ };
}
  1. 限定访问(:: 作用域运算符)
C++
std::cout << MySpace::value << std::endl;   // 通过 :: 限定访问
  1. using 声明(引入单个名字)
C++
using std::cout;      // 只引入 cout
cout << "hi";         // 可直接用
  1. using 指令(引入整个命名空间)
C++
using namespace std;  // 引入整个 std,可能污染全局,头文件里慎用
  1. 嵌套与别名
C++
namespace A { namespace B { int x = 1; } }
namespace AB = A::B;      // 别名
std::cout << AB::x << std::endl;

常见陷阱

做法说明
头文件中写 using namespace std;污染所有包含者的命名空间,不推荐
匿名命名空间内部符号具有内部链接,用于替代 static
💡 面试加分项

核心:避免命名冲突。讲完定义后展示「限定访问 / using 声明 / using 指令」三种用法,再提醒头文件里不要 using namespace std,可提匿名命名空间。


C++ 如何调用 C 语言的库?

整体流程

C++ 调用 C 库需要解决**名称修饰(name mangling)**不一致的问题,完整流程分两步:

  1. 头文件声明用 extern "C" 包裹:让 C++ 编译器按 C 规则链接,不做名称修饰
  2. 编译/链接 C 目标文件:把 C 源码编成 .o,与 C++ 程序一起链接

第一步:头文件用 extern “C” 包裹

C++
// c_lib.h —— C 库头文件
#ifdef __cplusplus
extern "C" {
#endif

int add(int a, int b);
void say_hello();

#ifdef __cplusplus
}
#endif

__cplusplus 是 C++ 编译器自动定义的宏,保证该头文件既能被 C 编译器、也能被 C++ 编译器包含。

第二步:编译链接 C 目标文件

Bash
# 1. 用 C 编译器编译 C 源码为目标文件
gcc -c c_lib.c -o c_lib.o

# 2. 用 C++ 编译器编译 C++ 源码,并链接 C 目标文件
g++ main.cpp c_lib.o -o app
C++
// main.cpp
#include "c_lib.h"
int main() {
    return add(1, 2);   // 直接调用 C 函数
}

为什么要 extern “C”

C++ 为支持函数重载,会对函数名做名称修饰(如 add(int,int) → _Z3addii),而 C 编译出的符号基本保持原名(_add)。不做 extern "C" 包裹时,C++ 按修饰后的名字去找符号,链接器会报未定义引用(undefined reference)。

ℹ️ 与 extern “C” 章节的关系

名称修饰的底层原理详见 C++ 基础-初级。本节聚焦「调用 C 库」的完整工程流程。

💡 面试加分项

按「两步流程」回答:头文件 extern "C" 包裹 + 编译链接 C 目标文件。核心是名称修饰导致符号不匹配,extern "C" 让 C++ 按 C 规则链接。能提到 __cplusplus 宏和 gcc/g++ 分工更好。


指针和引用的区别是什么?

核心区别一览

维度指针引用
本质存储地址的变量变量的别名(alias)
是否占内存占内存(存地址)通常不占(编译器实现,可能有隐藏指针)
初始化可不初始化(野指针风险)必须初始化
可否为空可为 nullptr不能为空
可否改指向可重新指向其他对象不可改(绑定后终身不变)
解引用需 *自动(无需 *)
取地址&p 得到指针自身地址&r 得到所引用对象地址
多级支持多级指针 int**无多级引用
自增p++ 移动指针r++ 是引用对象自增

代码示例

C++
int a = 10;
int* p = &a;     // 指针:存 a 的地址
int& r = a;      // 引用:a 的别名

*p = 20;         // 通过指针修改,需要解引用
r = 30;          // 通过引用修改,无需解引用

p = nullptr;     // ✅ 指针可为空、可改指向
// r = b;        // ❌ 引用一旦绑定,不能再绑定其他对象

必须初始化 & 不能为空

C++
int& r;          // ❌ 编译错误:引用必须初始化
int* p;          // ⚠️ 指针可不初始化,但会成野指针(危险)

作为函数参数(引用传递)

C++
void swap(int& x, int& y) {   // 引用传参,直接修改实参
    int t = x; x = y; y = t;
}
💡 面试加分项

抓住四句核心:指针是「存地址的变量」、引用是「别名」;引用必须初始化、不可为空、不可改指向;指针更灵活但更危险。可补充「sizeof 引用得对象大小、sizeof 指针得指针大小」和「引用底层常用指针实现」。


C++ 中为什么 new 和 delete 一定要配对使用?

核心原因:构造/析构必须配对

new = 分配内存 + 调用构造函数,delete = 调用析构函数 + 释放内存。两者必须配对,才能保证对象的生命周期完整,正确执行构造和析构。

C++
class MyClass {
public:
    int* data;
    MyClass()  { data = new int[100]; }   // 构造:申请资源
    ~MyClass() { delete[] data; }          // 析构:释放资源
};

MyClass* p = new MyClass();   // 分配内存 + 调构造函数
delete p;                     // 调析构函数 + 释放内存
// 若漏掉 delete,析构不执行,data 指向的内存泄漏

三条配对规则

  1. new 配 delete,new[] 配 delete[]
  2. 数组必须用 delete[],否则只析构第一个元素、其余泄漏,且是未定义行为
  3. 不能与 malloc/free 混用
C++
int* a = new int;        // new 配 delete
delete a;

int* b = new int[10];    // new[] 必须配 delete[]
delete[] b;
// delete b;  ❌ 错误:数组不用 delete[] 是未定义行为

int* c = (int*)malloc(sizeof(int));
// delete c;  ❌ 错误:malloc 分配的内存不能用 delete 释放
free(c);                  // ✅ malloc 配 free

不配对的后果

情况后果
new 后漏 delete内存泄漏 + 析构不执行
new[] 用 delete未定义行为,通常只析构一个元素
new/delete 与 malloc/free 混用未定义行为
💡 面试加分项

一句话点核心:new/delete 不仅要管内存,还要管构造/析构,配对才能保证对象生命周期完整。展开三条规则:new↔delete、new[]↔delete[]、不能与 malloc/free 混用。详见 C++ 基础-初级。


数组指针和指针数组有什么区别?

核心区别:语法与含义

两者名字相近但含义完全相反,关键在于运算符优先级:[](下标)的优先级高于 *(解引用),所以:

名称声明形式本质读法
指针数组int* arr[5]数组,元素是指针“先下标后指针”:arr 是数组,元素是 int*
数组指针int (*p)[5]指针,指向整个数组“先指针后下标”:p 是指针,指向 int[5]

关键记忆点:

  • int* arr[5]:[] 先与 arr 结合 → arr 是含 5 个元素的数组,元素类型是 int*。
  • int (*p)[5]:括号让 * 先与 p 结合 → p 是指针,指向 int[5] 这种「5 个 int 的数组」。

代码示例

C++
#include <iostream>

int main() {
    // 1. 指针数组:数组里存了 5 个 int* 指针
    int a = 1, b = 2, c = 3, d = 4, e = 5;
    int* ptrArr[5] = { &a, &b, &c, &d, &e };   // 元素是指针
    for (int i = 0; i < 5; ++i) {
        std::cout << *ptrArr[i] << " ";        // 先取下标拿到指针,再解引用
    }
    // 输出:1 2 3 4 5

    // 2. 数组指针:一个指针指向整个数组
    int arr[5] = { 1, 2, 3, 4, 5 };
    int (*p)[5] = &arr;   // p 指向 arr 这个「长度为 5 的 int 数组」

    std::cout << (*p)[0] << std::endl;   // 先解引用拿到数组,再下标访问 → 1
    std::cout << (*p)[3] << std::endl;   // → 4
    return 0;
}

与二维数组的经典关系

指针数组常用于构建/传递二维数组,数组指针则常用于指向二维数组的某一行:

C++
#include <iostream>

void print2D(int (*matrix)[3], int rows) {   // 数组指针:指向「每行 3 个 int」
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < 3; ++j) {
            std::cout << matrix[i][j] << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    int grid[2][3] = { {1, 2, 3}, {4, 5, 6} };

    // 数组名 grid 退化为指向「int[3]」的数组指针,正好匹配参数
    print2D(grid, 2);

    // 指针数组:每行长度可以不同(锯齿数组)
    int r0[] = { 1, 2, 3 };
    int r1[] = { 4, 5 };
    int* jagged[2] = { r0, r1 };   // 两行长度不同,指针数组天然支持
    return 0;
}
💡 面试加分项

回答时先给一句「读法」口诀——int* arr[5] 是数组(元素是指针),int (*p)[5] 是指针(指向数组),并点出根本原因是 [] 优先级高于 *、括号改变了结合顺序。能举例「二维数组传参时 int (*p)[N]」和「指针数组构建锯齿数组」,说明真正掌握了它们的使用场景。

ℹ️ 关联

数组名的退化机制(int[5] 隐式转 int*)是理解数组指针的铺垫,详见 C++ 基础-初级。


C++ 中位域(bit field)是什么?有什么用途?

什么是位域

位域(bit-field)允许在结构体/联合体中,以位(bit)为单位指定成员占用的宽度,从而把多个小字段压缩到同一个字节/字里。语法是成员后加 : 宽度:

C++
#include <iostream>

struct Flags {
    unsigned int read  : 1;   // 占 1 位
    unsigned int write : 1;   // 占 1 位
    unsigned int exec  : 1;   // 占 1 位
    unsigned int mode  : 2;   // 占 2 位,可表示 0~3
};
// 四个字段共占 5 位,实际按 unsigned int 对齐,sizeof(Flags) 通常为 4

核心用途

用途说明
节省内存多个布尔/小范围字段共享一个字,比「每字段一个 int」省得多
网络协议头解析精确对应报文头里的位字段(如 TCP/IP 首部标志位)
硬件寄存器直接映射寄存器的位布局,读写某一位/某几位
权限/标志位集合用位表示一组开关状态(读、写、执行权限)
C++
#include <iostream>

// 典型:权限标志位
struct Perm {
    unsigned int read  : 1;
    unsigned int write : 1;
    unsigned int exec  : 1;
};

int main() {
    Perm p{};
    p.read = 1;
    p.exec = 1;
    std::cout << "read=" << p.read
              << ", write=" << p.write
              << ", exec=" << p.exec << std::endl;   // 1, 0, 1
    return 0;
}

使用注意(易踩坑点)

  1. 位域类型一般用 unsigned int / unsigned char 等无符号类型:用带符号类型时,某一位作为符号位可能导致意外结果。
  2. 不能对位域取地址:位域可能不是字节对齐的,&p.read 非法。
  3. 布局与编译器/平台相关:位域的分配顺序、跨越字节的方式是实现定义的,跨平台协议解析需谨慎核对。
  4. 位域宽度不能超过类型位数,且通常要同类型才能合并存储。
C++
// ❌ 不能取地址
struct F { unsigned int a : 1; };
F f{};
// auto* p = &f.a;   // 编译错误:不能对位域取地址
💡 面试加分项

回答时先给定义(在 struct 里按位指定成员宽度,压缩存储),再讲四大用途(省内存、协议头、硬件寄存器、标志位集合),最后补几个坑——位域不可取地址、布局是平台/编译器相关、建议用无符号类型。能提到「跨平台协议解析时位域布局不可移植」体现对底层布局的清醒认知。

ℹ️ 关联

位域常用于「如何用 union 做内存优化」「struct 内存对齐」这类问题中,作为按位压缩的补充手段。结构体对齐与 union 详见 C++ 基础-初级。