C++常见八股

cpp

左值/右值/移动构造/noexpect/转发引用引用折叠/forword完美转发

1. 左值 / 右值

面试回答

左值 = 有身份、可持续访问的对象;右值 = 临时结果或允许资源被转移的对象。C++11 后右值分 prvalue(纯右值)和 xvalue(将亡值)。

复制代码
int x = 10;
x;              // lvalue
x + 1;          // prvalue
std::move(x);   // xvalue

易错点

  • 左值 ≠ 能修改,const int x 也是左值。
  • std::move(x) 不创建新对象,只是把 x 标记为 xvalue。

2. 为什么引入右值引用 T&&

面试回答

为了让"即将销毁的对象"的内部资源可以被转移 而非深拷贝,同时支撑完美转发。const T& 虽然能接临时对象,但它是 const,无法修改源对象,因此不能转移资源。

复制代码
std::vector<int> a(1000000);
std::vector<int> b = std::move(a);  // 转移 buffer 所有权,不复制 100 万元素

易错点

  • 不要答"因为临时对象不能传参"------const T& 本来就能接。
  • 真正痛点是 const T& 不能改源对象,无法转移资源。

3. 移动构造

面试回答

T(T&&) 的核心是资源所有权转移:接管源对象的堆资源,把源对象置空,使其保持合法可析构状态。

实现

复制代码
class Buffer {
public:
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;   // 置空,防止 double free
        other.size = 0;
    }
    ~Buffer() { delete[] data; }
private:
    int* data = nullptr;
    size_t size = 0;
};

移动前: other ──→ Heap
移动后: *this ──→ Heap    other ──→ nullptr

易错点

  • 不是"浅拷贝",是所有权转移(浅拷贝会导致 double free,移动构造通过置空避免)。
  • 必须置空源对象,否则析构时 double free。

4. std::move

面试回答

std::move 本身不移动任何数据 ,本质是 static_cast<T&&>,把表达式无条件转成 xvalue,从而允许后续匹配移动构造/移动赋值。

复制代码
Buffer a;
Buffer b = std::move(a);
  └─ std::move(a) → xvalue → 匹配 Buffer(Buffer&&) → 真正转移资源

易错点

  • std::move ≠ 移动std::move = 允许发生移动
  • 写了 std::move 但目标类型没有移动构造 → 退化为拷贝。

5. move 后源对象还能用吗

面试回答

可以。被移动对象处于**有效但未指定(valid but unspecified)**状态:可以析构、可以重新赋值,但不能依赖它移动前的值。

复制代码
std::string a = "hello";
std::string b = std::move(a);
a = "new";    // OK
a.clear();    // OK
// 但不要假设 a 还是 "hello" 或一定是空串

易错点

  • 不要说"move 后再访问是 UB"------不准确。
  • 标准只保证可析构、可赋值,具体值由实现决定。

6. 为什么移动构造写 noexcept

面试回答

noexcept 告诉容器"移动不会抛异常"。vector 扩容时,如果移动可能抛异常,为保证强异常安全会退化为拷贝 ;标记 noexcept 后容器才放心用移动迁移元素。

复制代码
旧空间 [A][B][C]
  ↓ 扩容
新空间 [A][B][C][ ][ ][ ]   ← noexcept 时逐个 move,否则逐个 copy

易错点

  • 不是"不写 noexcept 就不能 move",而是影响容器选择 move 还是 copy
  • 自定义结构体含 string/vector 成员却不写 noexcept 移动构造 → 容器扩容全走拷贝,性能灾难。

7. 有名 T&& 是左值(高频追问)

面试回答

右值引用是变量的类型 ,但只要变量有名字,这个名字作为表达式就是左值。这样可以避免参数在函数内每用一次就自动被移走。

复制代码
void f(Buffer&& arg) {
    g(arg);              // arg 是左值表达式 → 走拷贝
    g(std::move(arg));   // 显式转右值 → 走移动
}

arg 的类型  = Buffer&&
表达式 arg  = lvalue

易错点

  • 类型是右值引用 ≠ 表达式是右值。
  • 函数内想继续移动必须再次 std::move

8. 转发引用 + 引用折叠

面试回答

模板参数 T 需要推导且参数写成 T&& 时,是转发引用(forwarding reference) ,可同时接收左值和右值。推导结果由引用折叠决定。

复制代码
template<typename T>
void Func(T&& arg);

int x;
Func(x);       // 左值 → T = int& → T&& = int& && → int&
Func(10);      // 右值 → T = int  → T&& = int&&

引用折叠(直接背)

复制代码
&  + &  → &
&  + && → &
&& + &  → &
&& + && → &&

口诀:只要有 &,结果就是 &;全是 && 才是 &&

易错点

  • 只有"需要推导的模板参数 T&&"才是转发引用。void f(int&& x) 不是转发引用,只能接右值。
  • const T&& 也不是转发引用。

9. std::forward + 完美转发

面试回答

std::forward<T> 根据模板参数 T 恢复实参原来的值类别:左值进来继续传左值,右值进来恢复右值。配合转发引用实现完美转发

复制代码
template<typename T>
void Wrapper(T&& arg) {
    Target(std::forward<T>(arg));  // 保持原值类别
}

std::string s;
Wrapper(s);                     // 左值 → 转发为左值
Wrapper(std::string("temp"));   // 右值 → 转发为右值

易错点

  • 不写 std::forwardarg 有名字永远是左值 → 右值也被当左值传,移动失效。
  • 完美转发解决 N 个参数的 2^N 重载组合爆炸。

10. std::move vs std::forward

std::move std::forward
目的 允许移动 完美转发
行为 无条件转 xvalue 根据 T 保留原值类别
场景 普通代码、移动实现 泛型模板
左值输入 变成右值 继续保持左值

一句话:move = "我要把你当右值",forward = "你是什么我就传什么"。


11. 两个必背坑

坑 1:const 对象被 move

复制代码
const std::string s = "hello";
std::string t = std::move(s);  // 得到 const string&&

移动构造签名是 string(string&&),无法绑定 const string&&退化为拷贝。因为移动需要修改源对象,const 不允许。

坑 2:返回局部变量写 std::move

复制代码
std::string f() {
    std::string s;
    return s;              // 推荐:允许 NRVO,零开销
    // return std::move(s); // 不要:抑制 NRVO,多一次移动
}

标准规定返回局部对象时自动优先尝试移动,写 std::move 反而妨碍返回值优化。


面试答题链(背这条就够)

复制代码
左值/右值
  → T&& 为什么需要(转移资源,const T& 做不到)
  → 移动构造:所有权转移 + 源对象置空
  → std::move:只做类型转换,不移动
  → noexcept:决定容器扩容用 move 还是 copy
  → 有名 T&& 是左值(函数内需再次 move)
  → 模板 T&& = 转发引用 + 引用折叠
  → std::forward 恢复值类别
  → 完美转发

高频核心:移动构造、std::move、有名右值引用是左值、转发引用、引用折叠、std::forward、noexcept+vector 扩容。左值右值分类只需够支撑以上问题。

堆和栈

一、const 修饰变量

含义:变量初始化后不可修改,成为编译期可检查的常量。

复制代码
const int x = 10;
x = 20;  // ❌ 编译错误:x 是 const

const int y;  // ❌ 编译错误:const 变量必须初始化

面试要点

  • const 变量必须在定义时初始化,之后不能赋值。

  • 类的 const 成员变量必须在构造函数初始化列表中初始化,不能在构造函数体内赋值。

    class Foo {
    const int id;
    public:
    Foo(int i) : id(i) {} // ✅ 初始化列表
    // Foo(int i) { id = i; } // ❌ 构造函数体内赋值不行
    };


二、const 修饰指针:顶层 const vs 底层 const

这是面试最高频的考点,核心是区分const 修饰的是谁

2.1 四种写法

写法 含义 指针本身能改吗 指向的值能改吗
int* p 普通指针
const int* p 指向常量的指针(底层 const) 不能
int* const p 常量指针(顶层 const) 不能
const int* const p 指向常量的常量指针 不能 不能

判断口诀 :看 const*左边还是右边

  • const* 左边 → 修饰指向的值(底层 const,*p 不可改)

  • const* 右边 → 修饰指针本身(顶层 const,p 不可改)

    int x = 10, y = 20;

    const int* p1 = &x; // 底层 const:*p1 不可改,p1 可改
    *p1 = 30; // ❌ *p1 是 const
    p1 = &y; // ✅ p1 本身可改

    int* const p2 = &x; // 顶层 const:p2 不可改,*p2 可改
    *p2 = 30; // ✅
    p2 = &y; // ❌ p2 是 const

    const int* const p3 = &x; // 两者都 const
    *p3 = 30; // ❌
    p3 = &y; // ❌

2.2 顶层 const 和底层 const 的拷贝规则

  • 顶层 const 不影响拷贝:常量指针可以拷给普通指针(因为拷的是地址值,源指针 const 不影响副本)。

  • 底层 const 不能被忽略:指向常量的指针不能直接赋给指向变量的指针(否则就可以通过后者修改常量了)。

    const int* p1 = &x;
    int* p2 = p1; // ❌ 底层 const 不能丢:p1 承诺不修改,p2 可以修改

    int* const p3 = &x;
    int* p4 = p3; // ✅ 顶层 const 可以丢:p3 本身不能改,但拷给 p4 后 p4 可以改


三、const 修饰引用

含义const T& 是常量引用,不能通过引用修改绑定的对象。

复制代码
int x = 10;
const int& ref = x;
ref = 20;   // ❌ ref 是 const 引用
x = 20;     // ✅ 原变量可以改,ref 看到的值也会变

关键特性:const 引用可以绑定到临时对象(右值)和不同类型

复制代码
const int& r1 = 10;          // ✅ 绑定到临时对象(int(10))
int& r2 = 10;                 // ❌ 非常量引用不能绑定右值

double d = 3.14;
const int& r3 = d;            // ✅ 类型转换:创建临时 int,const 引用绑定到临时
int& r4 = d;                  // ❌ 非常量引用不能绑定到转换后的临时

面试要点 :函数参数用 const T& 是推荐写法,既避免拷贝又保证不修改,还能接受右值和临时对象。

复制代码
void Process(const std::string& s);  // ✅ 推荐:可接受左值、右值、临时对象
void Process(std::string& s);        // ⚠️ 只能接受左值,不能传临时对象

四、const 修饰成员函数

含义 :在成员函数声明后加 const,承诺该函数不修改任何成员变量(也不调用非 const 成员函数)。

复制代码
class Counter {
    int count = 0;
public:
    int Get() const {   // const 成员函数
        return count;   // ✅ 只读
        // count = 1;   // ❌ 不能修改成员
        // Increment();  // ❌ 不能调用非 const 成员函数
    }
    void Increment() {  // 非 const 成员函数
        count++;
    }
};

4.1 const 对象只能调用 const 成员函数

复制代码
const Counter c;
c.Get();        // ✅ const 成员函数
c.Increment();  // ❌ const 对象不能调用非 const 成员函数

4.2 函数重载:const 与非 const 版本

复制代码
class Buffer {
    int data[100];
public:
    // 非 const 对象调用,返回可修改引用
    int& operator[](int i) { return data[i]; }

    // const 对象调用,返回 const 引用
    const int& operator[](int i) const { return data[i]; }
};

Buffer b;
b[0] = 42;        // ✅ 调用非 const 版本

const Buffer cb;
cb[0] = 42;       // ❌ 调用 const 版本,返回 const int&,不能赋值
int x = cb[0];    // ✅

面试要点 :STL 容器的 operator[]begin()/end()at() 等都有 const/非 const 两个重载版本,这是标准库的惯用法。


五、const 修饰函数参数

5.1 值传递的 const 参数

复制代码
void Func(const int x);  // x 在函数内不可修改

值传递时参数是副本,const 只是限制函数内不修改这个副本。对调用方无影响,通常只在函数实现需要保护参数时使用。

5.2 指针/引用传递的 const 参数(最常用)

复制代码
void Print(const std::string& s);   // ✅ 推荐:避免拷贝 + 保证不修改
void Process(const int* arr, int n); // ✅ 指针底层 const:不修改数组内容

面试要点

  • 大对象传参优先用 const T&,避免拷贝开销。
  • 指针参数加底层 const 表示函数不会修改指向的数据。
  • 顶层 const 参数(如 int* const p)对调用方无意义,只在函数内限制指针本身不被改。

六、const 修饰函数返回值

6.1 返回 const 值

复制代码
const int GetValue();  // 对内置类型无意义,返回的是右值本来就不能改

对内置类型返回 const 没有实际意义(返回值本身就是右值)。但对类类型有意义:

复制代码
class Matrix { ... };
const Matrix operator+(const Matrix& a, const Matrix& b);

Matrix a, b, c;
(a + b) = c;  // ❌ 如果返回 const,不能对临时对象赋值(防止无意义代码)

6.2 返回 const 引用 / const 指针

复制代码
class Config {
    std::string name_;
public:
    const std::string& GetName() const { return name_; }  // ✅ 返回 const 引用
};

Config c;
c.GetName() = "hacked";  // ❌ 返回 const 引用,不能通过返回值修改内部成员

面试要点 :返回成员变量的引用时,如果不希望外部修改内部状态,必须返回 const T&。否则外部可以通过返回值篡改对象内部数据,破坏封装。


七、mutable:突破 const 限制

含义 :被 mutable 修饰的成员变量,即使在 const 成员函数中也可以被修改。

复制代码
class Cache {
    mutable int hit_count = 0;  // mutable 成员
    int data = 0;
public:
    int Get() const {
        hit_count++;  // ✅ mutable 成员在 const 函数中可修改
        return data;
    }
};

适用场景

  • 缓存统计(如上面的 hit_count)

  • 互斥锁(mutable std::mutex,const 函数中加锁)

  • 延迟计算(lazy evaluation,const 函数中首次调用时计算并缓存)

    class ThreadSafeCounter {
    mutable std::mutex mtx; // mutable 锁
    int count = 0;
    public:
    int Get() const {
    std::lock_guardstd::mutex lock(mtx); // ✅ const 函数中可以锁 mutable 成员
    return count;
    }
    };


八、const_cast:去掉 const 属性

含义const_cast 可以去掉指针或引用的底层 const(也可以加上 const),是唯一能去掉 const 的 C++ 转型运算符。

复制代码
const int x = 10;
int* p = const_cast<int*>(&x);  // 去掉底层 const
*p = 20;  // ⚠️ 未定义行为!x 本身是 const 对象,修改它是 UB

安全用法:对象本身不是 const,只是当前通过 const 指针/引用访问时,可以安全地去掉 const。

复制代码
void Log(const char* str);  // 旧 C 接口,参数应该是 const 但声明没写

void Func(const std::string& s) {
    Log(const_cast<char*>(s.c_str()));  // 去掉 const 传给旧接口
    // 安全:Log 不会修改字符串,只是接口声明不规范
}

面试要点

  • const_cast 只能改变底层 const,不能改变类型本身(不能用 const_castint*double*)。
  • 真正的 const 对象 去掉 const 后修改是未定义行为
  • 常见合理用途:调用 const 不正确的旧接口、在非 const 函数中复用 const 版本的实现(或反过来)。

九、constexpr:编译期常量

constexpr 是 C++11 引入的,比 const 更强的编译期常量约束。

const constexpr
含义 运行时常量(初始化后不可改) 编译期常量(值在编译期确定)
初始化时机 可以运行时初始化 必须编译期可计算
能用于数组大小 不一定(如果是运行时常量则不行) 一定可以
能用于模板参数 不一定 一定可以
复制代码
const int a = 10;           // 编译期常量(字面量初始化)
const int b = rand();       // 运行时常量(运行时才知道值)
constexpr int c = 10;       // ✅ 编译期常量
constexpr int d = rand();   // ❌ rand() 不是编译期可计算

int arr1[a];   // ✅ a 是编译期常量(但取决于编译器,严格来说 a 是 const 不是 constexpr)
int arr2[c];   // ✅ c 是 constexpr,一定可以
int arr3[b];   // ❌ b 是运行时常量,不能做数组大小(VLA 扩展除外)

constexpr 函数:函数可以在编译期求值(如果参数都是编译期常量),也可以在运行期调用。

复制代码
constexpr int Factorial(int n) {
    return n <= 1 ? 1 : n * Factorial(n - 1);
}

int a = Factorial(5);   // 编译期计算 = 120
int b = Factorial(rand());  // 运行期计算

十、面试高频考点速查

考点 答案
const int* pint* const p 区别 前者指向的值不可改(底层 const),后者指针本身不可改(顶层 const)
const 成员函数能修改成员吗 不能,除非成员是 mutable
const 对象能调用非 const 成员函数吗 不能,只能调用 const 成员函数
const T& 能绑定临时对象吗 能,这是 const 引用的重要特性
函数参数为什么推荐 const T& 避免拷贝 + 保证不修改 + 能接受右值和临时对象
返回成员引用为什么要 const 防止外部通过返回值篡改对象内部状态
mutable 作用 允许 const 成员函数修改该成员(用于缓存、锁、延迟计算)
const_cast 安全吗 去掉 const 后修改真正的 const 对象是 UB;对象本身非 const 时安全
constconstexpr 区别 const 是运行时常量,constexpr 是编译期常量
顶层 const 拷贝时会被忽略吗 会(源指针 const 不影响副本)
底层 const 拷贝时会被忽略吗 不会(不能把 const 指针赋给非 const 指针)

十一、面试答题模板

"const 是 C++ 中用于保证不可变性的关键字,主要用法有:修饰变量使其初始化后不可修改;修饰指针时分顶层 const(指针本身不可改)和底层 const(指向的值不可改);修饰引用形成 const 引用,可以绑定临时对象;修饰成员函数承诺不修改成员变量,const 对象只能调用 const 成员函数;修饰函数参数避免拷贝并保证不修改;修饰返回值防止外部篡改内部状态。另外 mutable 允许 const 函数修改特定成员,const_cast 可以去掉底层 const,constexpr 是更强的编译期常量。核心思想是 const 正确性(const-correctness)------该不可变的地方都加 const,让编译器帮你检查错误。"

一、static 的五种用法

1. 修饰局部变量(静态局部变量)

含义 :在函数内部用 static 修饰的变量,生命周期贯穿整个程序 ,但作用域仅限于函数内部。函数调用结束后变量不销毁,下次调用时保留上次的值。

cpp 复制代码
void Counter() {
    static int count = 0;  // 只在第一次调用时初始化
    count++;
    std::cout << count << " ";
}

Counter();  // 1
Counter();  // 2
Counter();  // 3
// count 不能在函数外访问

特点

  • 只初始化一次,第一次执行到定义语句时初始化。
  • 存储在静态存储区,不在栈上。
  • C++11 起初始化是线程安全的(Magic Statics):多线程同时首次进入函数,只有一个线程执行初始化,其余阻塞等待。

2. 修饰全局变量 / 函数(内部链接)

含义 :在全局作用域或命名空间中加 static,该变量/函数具有内部链接(internal linkage),只在当前翻译单元(.cpp 文件)内可见,其他文件无法访问。

cpp 复制代码
// file1.cpp
static int g_count = 0;       // 只在 file1.cpp 内可见
static void Helper() { }      // 只在 file1.cpp 内可见

// file2.cpp
// extern int g_count;  // ❌ 链接错误:g_count 是内部链接,file2 看不到

作用 :避免多个 .cpp 文件中同名全局变量/函数的符号冲突

C++ 推荐替代 :用匿名命名空间 代替 static 全局变量/函数,语义相同但更符合 C++ 风格:

cpp 复制代码
namespace {
    int g_count = 0;   // 等价于 static int,内部链接
    void Helper() { }
}

3. 修饰类成员变量(静态成员变量)

含义 :被 static 修饰的成员变量属于类本身 ,不属于某个对象,所有对象共享同一份

cpp 复制代码
class Player {
public:
    static int player_count;  // 声明:静态成员变量
    int hp;
};

// 类外定义和初始化(C++17 前必须在类外)
int Player::player_count = 0;

int main() {
    Player::player_count++;   // 通过类名访问
    Player p1, p2;
    p1.player_count++;        // 也可以通过对象访问(但本质还是类的)
    std::cout << Player::player_count;  // 2,所有对象共享
}

特点

  • 不占用对象的内存空间,存储在静态存储区

  • 必须在类外定义(C++17 前),否则链接错误。

  • C++17 起可以用 inline static类内直接初始化

    cpp 复制代码
    class Player {
        inline static int player_count = 0;  // C++17,类内初始化
    };
  • 静态成员变量的类型可以是类自身(用于单例等模式)。


4. 修饰类成员函数(静态成员函数)

含义 :被 static 修饰的成员函数属于类本身没有 this 指针,只能访问静态成员变量和其他静态成员函数。

cpp 复制代码
class Math {
    static int instance_count;
public:
    static int Add(int a, int b) {   // 静态成员函数
        return a + b;
        // return hp;  // ❌ 不能访问非静态成员(没有 this)
    }
    static int GetCount() {
        return instance_count;  // ✅ 可以访问静态成员变量
    }
};

Math::Add(3, 4);  // 通过类名调用,不需要对象

特点

  • 没有 this 指针,不能访问非静态成员。
  • 不能是 virtualconstvolatile(这些都依赖 this)。
  • 可以通过类名调用,也可以通过对象调用。
  • 静态成员函数可以作为回调函数 (因为没有 this,签名和普通函数指针兼容)。

5. 修饰类对象(静态对象)

static 修饰对象时,本质就是静态局部变量或静态全局变量的一种,只是对象类型是类。

cpp 复制代码
void Func() {
    static MyObject obj;  // 静态局部对象:第一次调用时构造,程序结束时析构
    obj.DoSomething();
}

这是实现**单例模式(Meyers' Singleton)**的标准方式:

cpp 复制代码
class Singleton {
public:
    static Singleton& GetInstance() {
        static Singleton instance;  // 第一次调用时构造,线程安全(C++11)
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
};

二、初始化时机详解

这是面试核心考点。静态存储期对象的初始化分为两个阶段

2.1 静态初始化(Static Initialization)

在程序加载时、任何代码执行之前完成,分为两步:

  1. 零初始化(Zero Initialization):所有静态存储期对象先被清零(包括静态局部变量、全局变量、类静态成员)。
  2. 常量初始化(Constant Initialization) :如果初始化表达式是编译期常量(如 = 0= 3.14constexpr),在编译期就确定值,直接写入,不需要运行时执行。
cpp 复制代码
int g_a = 0;          // 零初始化 + 常量初始化,编译期确定
int g_b = 100;        // 常量初始化,编译期确定
static int g_c = 42;  // 同上

2.2 动态初始化(Dynamic Initialization)

初始化表达式需要运行时计算(如函数调用、构造函数),在 main 函数之前执行。

cpp 复制代码
int g_d = ComputeValue();   // 动态初始化:运行时调用函数
std::string g_e = "hello";  // 动态初始化:调用 string 构造函数

2.3 各类 static 的初始化时机

类型 初始化时机 线程安全
全局静态变量 main 之前,程序启动阶段 单线程初始化(启动阶段)
静态局部变量 第一次执行到定义语句时 C++11 起线程安全(Magic Statics)
类静态成员变量 main 之前,和全局变量同时期 单线程初始化
静态成员函数 无初始化概念(代码) ---

三、静态初始化顺序问题(SIOF)

Static Initialization Order Fiasco 是面试高频追问。

3.1 问题是什么

不同翻译单元(.cpp 文件)之间的全局静态变量,初始化顺序未定义。如果一个全局变量的初始化依赖另一个翻译单元的全局变量,可能用到未初始化的值。

cpp 复制代码
// a.cpp
int g_a = 10;

// b.cpp
extern int g_a;
int g_b = g_a * 2;  // ⚠️ 危险:如果 b.cpp 先初始化,g_a 还是 0,g_b = 0
                     // 不同翻译单元间初始化顺序未定义

3.2 解决方法:Construct On First Use

把全局变量改成函数内的静态局部变量,通过函数访问:

cpp 复制代码
// a.cpp
int& GetA() {
    static int a = 10;  // 第一次调用时初始化
    return a;
}

// b.cpp
int& GetA();  // 声明
int g_b = GetA() * 2;  // ✅ 安全:调用 GetA() 时 a 才初始化

原理:静态局部变量在第一次使用时才初始化,打破了翻译单元间的顺序依赖。这也是 Meyers' Singleton 的核心思想。


四、销毁时机

静态对象的销毁顺序和初始化顺序相反 ,通过 atexit() 注册析构函数,在 main 结束后、程序退出前执行。

类型 销毁时机
全局静态变量 main 结束后,程序退出前
静态局部变量 第一次使用后初始化,程序退出前销毁(按初始化逆序)
类静态成员变量 和全局变量同时期销毁

注意:如果在静态对象的析构函数中访问另一个已经销毁的静态对象,会导致未定义行为(对应 SIOF 的销毁版本,叫 Dead Reference Problem)。Construct On First Use 也不能完全解决销毁顺序问题。


五、面试高频考点速查

考点 答案
静态局部变量什么时候初始化 第一次执行到定义语句时,只初始化一次
静态局部变量初始化线程安全吗 C++11 起安全(Magic Statics),多线程同时首次进入只有一个线程初始化
全局静态变量什么时候初始化 main 之前,程序启动阶段
静态成员变量必须在哪里初始化 C++17 前必须类外定义;C++17 起可用 inline static 类内初始化
静态成员函数能访问非静态成员吗 不能,没有 this 指针
静态成员函数能是 virtual 吗 不能,virtual 依赖 this
不同翻译单元全局静态变量初始化顺序 未定义(SIOF 问题)
SIOF 怎么解决 Construct On First Use(函数内静态局部变量)
static 全局变量和匿名命名空间 都实现内部链接,C++ 推荐匿名命名空间
静态成员变量占对象内存吗 不占,存在静态存储区,所有对象共享
Meyers' Singleton 原理 函数内 static 局部变量,首次调用初始化,线程安全

六、面试答题模板

"static 在 C++ 中有五种用法:修饰局部变量使其生命周期贯穿程序但作用域限于函数内,第一次执行到时初始化且 C++11 起线程安全;修饰全局变量/函数使其具有内部链接,只在当前翻译单元可见;修饰类成员变量使其属于类本身、所有对象共享,C++17 前必须类外定义;修饰类成员函数使其没有 this 指针、只能访问静态成员;修饰对象实现单例等模式。初始化时机上,全局静态变量和类静态成员在 main 之前初始化,静态局部变量第一次使用时初始化。需要注意不同翻译单元间全局静态变量初始化顺序未定义(SIOF),解决方法是 Construct On First Use。"

一、四种 cast 用法总结

1. static_cast:静态转换(编译期检查,良性转换)

用途

  • 基本类型间转换:int → doublechar → int

  • 向上转型(派生类 → 基类):安全,编译器自动支持

  • 向下转型(基类 → 派生类):不做运行时检查,需程序员保证安全

  • void* → 具体类型指针

  • 显式调用单参数构造函数:static_cast<string>("hello")

  • 枚举与整数互转

    double d = static_cast(int_val); // 基本类型
    Base* b = static_cast<Base*>(derived_ptr); // 向上转型,安全
    Derived* d = static_cast<Derived*>(base_ptr); // 向下转型,不检查!
    int* p = static_cast<int*>(void_ptr); // void* 转回具体类型

误区

  • 向下转型不做运行时类型检查 ,如果 base_ptr 实际不是 Derived*,结果是 UB。多态类型的安全向下转型应该用 dynamic_cast
  • 不能去掉 const (要用 const_cast)。
  • 不能转换不相关的指针类型 (如 int* → double*,要用 reinterpret_cast)。

2. dynamic_cast:动态转换(运行时类型检查,多态专用)

用途

  • 多态类型(基类有虚函数)的安全向下转型

  • 交叉转型(cross cast):两个无继承关系的多态类型之间转换

  • 转换失败时:指针返回 nullptr,引用抛出 std::bad_cast 异常

    class Base { virtual void Foo() {} };
    class Derived : public Base { };

    Base* b = new Derived();
    Derived* d = dynamic_cast<Derived*>(b); // ✅ 成功,b 实际是 Derived
    if (d == nullptr) { /* 转换失败处理 */ }

    Base* b2 = new Base();
    Derived* d2 = dynamic_cast<Derived*>(b2); // 返回 nullptr(b2 不是 Derived)

    // 引用版本
    try {
    Derived& r = dynamic_cast<Derived&>(*b2); // 抛 bad_cast
    } catch (const std::bad_cast& e) { }

前提条件 :基类必须至少有一个虚函数(多态类型),否则编译错误。

误区

  • 不能用于非多态类型(基类没有虚函数 → 编译错误)。
  • 引用转换失败抛异常,不是返回 null,必须用 try-catch 或确保类型正确。
  • 运行时开销(查 RTTI 类型信息),性能敏感的热路径中慎用。
  • 不能去掉 const,不能转基本类型。

3. const_cast:常量转换(唯一能去掉 const 的 cast)

用途

  • 去掉(或加上)指针/引用的底层 const

  • 唯一能去掉 const 的 C++ 转型运算符

    const int x = 10;
    int* p = const_cast<int*>(&x); // 去掉底层 const

    void OldAPI(char* str); // 旧接口参数没写 const,但实际不修改
    void Func(const std::string& s) {
    OldAPI(const_cast<char*>(s.c_str())); // 安全用法:传给不修改的旧接口
    }

误区

  • 去掉 const 后修改真正的 const 对象是 UB 。如果对象本身定义时就是 const(如 const int x = 10),去掉 const 修改它是未定义行为。只有对象本身非 const、只是当前通过 const 指针/引用访问时,去掉 const 才安全。
  • 只能改变 const 属性,不能改变类型本身const_cast<int*>(double_ptr) 编译错误。
  • 不能去掉顶层 const (如 int* const p,顶层 const 本来就可以通过拷贝自然去掉,不需要 cast)。

4. reinterpret_cast:重新解释转换(底层位模式,最危险)

用途

  • 指针 → 整数(如把指针存为 uintptr_t

  • 整数 → 指针

  • 一种指针 → 另一种不相关指针(如 int* → char*

  • 函数指针 → 数据指针(部分平台)

  • 完全按位模式重新解释,不做任何检查

    int x = 0x41424344;
    char* p = reinterpret_cast<char*>(&x); // 按字节查看 int 的内存表示

    uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); // 指针转整数
    void* ptr2 = reinterpret_cast<void*>(addr); // 整数转回指针

误区

  • 几乎所有结果都是实现定义(implementation-defined)的,不可移植。不同平台、不同编译器结果可能不同。
  • 不能去掉 const (要用 const_cast)。
  • 不能用于多态向下转型 (要用 dynamic_cast)。
  • 对齐问题:转换后的指针可能不满足目标类型的对齐要求,访问时崩溃。
  • 函数指针和数据指针互转在某些平台(如 Harvard 架构)上不合法。
  • 严格别名规则(strict aliasing):通过 reinterpret_cast 后的指针访问不同类型可能违反严格别名,导致 UB。安全做法是用 char*/std::byte* 查看字节,或用 memcpy

二、四种 cast 对比表

static_cast dynamic_cast const_cast reinterpret_cast
检查时机 编译期 运行时 编译期 无检查
能否向下转型 能,不检查 能,运行时检查 不能 不能
要求多态类型 是(必须有虚函数)
能否去掉 const 不能 不能 唯一能 不能
能否转不相关类型 不能 不能 不能
运行时开销 有(RTTI 查询)
安全性 较高 高(失败可检测) 中(需保证对象非 const) 最低
典型用途 良性转换、向上转型 多态向下转型 对接旧接口 底层位操作、指针整数互转

三、cast 使用选择口诀

复制代码
需要转基本类型/向上转型      → static_cast
需要安全向下转型(多态)      → dynamic_cast
需要去掉 const                → const_cast
需要按位重新解释/指针整数互转 → reinterpret_cast

面试要点 :C 风格强制转换 (T)x 会依次尝试 const_cast → static_cast → reinterpret_cast,行为不明确、不易搜索,C++ 中应优先使用命名 cast,让意图清晰可见。


四、static 的定义

static 是 C++ 中的存储类说明符(storage class specifier),用于指定实体的存储期(storage duration)和链接属性(linkage)。

具体含义取决于使用位置:

使用位置 定义
局部变量 指定静态存储期:变量在程序整个运行期间存在,第一次执行到定义时初始化,之后保留值,函数返回不销毁,作用域仍限于函数内
全局变量/函数 指定内部链接(internal linkage):该实体只在当前翻译单元(.cpp)内可见,其他翻译单元无法访问,避免符号冲突
类成员变量 指定该成员属于类本身而非类的实例:所有对象共享同一份,不占用对象内存,存储在静态存储区
类成员函数 指定该函数属于类本身 :没有 this 指针,只能访问静态成员,可通过类名直接调用

一句话定义static 让变量/函数/成员脱离实例或函数调用栈的生命周期,获得静态存储期或内部链接,从而在程序级别持久存在或在翻译单元内隔离。

一、C++ 标准智能指针

1. unique_ptr:独占所有权

定义 :独占所管理对象的所有权,不可拷贝,只能移动。对象在 unique_ptr 析构时自动 delete。零开销(和裸指针一样大)。

复制代码
#include <memory>

std::unique_ptr<int> p1(new int(42));
auto p2 = std::make_unique<int>(42);  // C++14,推荐工厂函数

// std::unique_ptr<int> p3 = p1;  // ❌ 不可拷贝
std::unique_ptr<int> p3 = std::move(p1);  // ✅ 可移动,p1 变为 nullptr

p3.reset();          // 释放对象,p3 变为 nullptr
int* raw = p3.get(); // 获取裸指针,不释放所有权

自定义删除器

复制代码
struct FileDeleter {
    void operator()(FILE* f) const { fclose(f); }
};
std::unique_ptr<FILE, FileDeleter> file(fopen("a.txt", "r"));

面试要点

  • 零开销,优先使用。
  • 不可拷贝、可移动,表达"独占"语义。
  • make_unique 异常安全(C++14 引入)。
  • 可作为工厂函数返回值(移动语义)。

2. shared_ptr:共享所有权

定义 :多个 shared_ptr 共享同一个对象,通过引用计数 管理生命周期。最后一个 shared_ptr 析构时(强引用计数归零),对象被销毁。

复制代码
auto p1 = std::make_shared<int>(42);  // 推荐:一次分配对象+控制块
std::shared_ptr<int> p2 = p1;          // 拷贝,强引用计数 +1
std::cout << p1.use_count();            // 2

p1.reset();  // 强引用计数 -1,变为 1,对象还活着
p2.reset();  // 强引用计数归零,对象被 delete

控制块(Control Block)结构

复制代码
┌──────────────────────────┐
│  强引用计数 (strong ref)  │  ← 管理对象生命周期
│  弱引用计数 (weak ref)    │  ← 管理控制块生命周期
│  删除器 (deleter)         │
│  分配器 (allocator)       │
└──────────────────────────┘
  • 强引用计数归零 → 销毁对象(调用 deleter)
  • 弱引用计数归零 → 销毁控制块本身

make_shared 的优势 :对象和控制块一次内存分配 ,比 shared_ptr<T>(new T) 少一次分配,缓存友好。缺点是对象和控制块同生共死,弱引用存在时对象内存无法释放(控制块还在,内存是一起分配的)。

线程安全

  • ✅ 引用计数的增减是原子操作 ,多线程拷贝/销毁 shared_ptr 安全。
  • 对象本身的读写不是线程安全的,多线程同时修改指向的对象需要自己加锁。
  • ❌ 多线程同时对同一个 shared_ptr 实例做 reset/赋值不是线程安全的(需要加锁或用 atomic_load/atomic_store)。

3. weak_ptr:不拥有所有权的观察者

定义 :从 shared_ptr 构造,不增加强引用计数 ,不影响对象生命周期。用于观察对象是否还活着,需要时通过 lock() 获取 shared_ptr

复制代码
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;  // 强引用计数不变,仍为 1

if (auto p = wp.lock()) {     // lock() 返回 shared_ptr,对象活着则成功
    std::cout << *p;           // 安全使用
} else {
    std::cout << "对象已销毁";
}

wp.expired();  // 检查对象是否已销毁(强引用计数是否为0)

核心用途:解决循环引用

复制代码
// 问题:A 和 B 互相持有 shared_ptr,循环引用,永远不会释放
class A { std::shared_ptr<class B> b; };
class B { std::shared_ptr<A> a; };

// 解决:其中一方改用 weak_ptr
class A { std::shared_ptr<class B> b; };
class B { std::weak_ptr<A> a; };  // 不增加强引用,打破循环

二、UE 智能指针

UE 有自己独立的智能指针库(SharedPointer.hUniquePtr.h),不使用 std::shared_ptr。原因:跨平台一致性、自定义内存分配器、与 UE 对象系统集成、性能可控。

1. TUniquePtr:对应 std::unique_ptr

独占所有权,不可拷贝、可移动,零开销。

复制代码
#include "Templates/UniquePtr.h"

TUniquePtr<int> P1 = MakeUnique<int>(42);
TUniquePtr<int> P2 = MoveTemp(P1);  // UE 用 MoveTemp 代替 std::move

P1.Reset();
int* Raw = P1.Get();

std::unique_ptr 基本一致,UE 中用于管理非 UObject 的原生 C++ 对象。


2. TSharedPtr:对应 std::shared_ptr

共享所有权,引用计数管理。UE 的 TSharedPtr 有一个关键特性:线程安全模式可选

复制代码
#include "Templates/SharedPointer.h"

// 默认非线程安全(ESPMode::Fast)
TSharedPtr<int> P1 = MakeShared<int>(42);
TSharedPtr<int> P2 = P1;

// 显式指定线程安全模式
TSharedPtr<int, ESPMode::ThreadSafe> P3 = MakeShared<int, ESPMode::ThreadSafe>(42);

ESPMode 两种模式

模式 引用计数操作 适用场景 开销
ESPMode::Fast 非原子(普通整数加减) 单线程游戏逻辑、UI 零开销
ESPMode::ThreadSafe 原子操作 多线程资源管理、异步加载 原子操作开销

面试要点 :UE 默认是 Fast 模式(非线程安全),这和 std::shared_ptr 默认线程安全不同。游戏主线程逻辑用 Fast 模式避免不必要的原子开销,多线程场景才显式指定 ThreadSafe


3. TSharedRef:UE 特有,非空共享指针

定义TSharedRef永远不为空TSharedPtr。必须在构造时初始化,不能默认构造,不能 Reset() 到空。

复制代码
TSharedRef<int> R1 = MakeShared<int>(42);  // ✅ 必须初始化
// TSharedRef<int> R2;  // ❌ 不能默认构造(会是空的)

TSharedPtr<int> P1 = R1;   // ✅ TSharedRef 隐式转 TSharedPtr
// TSharedRef<int> R2 = P1; // ❌ 不能直接转,P1 可能为空
TSharedRef<int> R2 = P1.ToSharedRef();  // ✅ 需显式转换(P1 为空会崩溃/断言)

TSharedRef vs TSharedPtr

TSharedRef TSharedPtr
能否为空 不能,必须初始化 可以,默认为空
默认构造 不允许 允许
是否需要判空 不需要 需要(IsValid()
适用场景 引用一定存在的对象(如单例、已加载资源) 可能为空的对象(如异步加载中、可选引用)

面试要点TSharedRef 是 UE 独有的概念,标准库没有对应物。它的价值是编译期保证非空,减少运行时判空,表达"这个引用一定有效"的语义。


4. TWeakPtr:对应 std::weak_ptr

不拥有所有权的观察者,不增加强引用计数。

复制代码
TSharedPtr<int> Sp = MakeShared<int>(42);
TWeakPtr<int> Wp = Sp;

if (TSharedPtr<int> Pinned = Wp.Pin()) {  // 对应 std::weak_ptr::lock()
    // 对象活着,安全使用
}

Wp.IsValid();   // 检查是否有效
Wp.Reset();     // 释放弱引用

5. TSharedFromThis:对应 std::enable_shared_from_this

继承后可以在成员函数中获取自身的 TSharedPtr/TSharedRef

复制代码
class FMyClass : public TSharedFromThis<FMyClass> {
public:
    TSharedRef<FMyClass> GetSelf() {
        return AsShared();  // 返回自身的 TSharedRef
    }
};

TSharedRef<FMyClass> Obj = MakeShared<FMyClass>();
TSharedRef<FMyClass> Self = Obj->GetSelf();  // 安全获取自身引用

注意:对象必须由 MakeShared 创建(被 TSharedPtr 管理),否则 AsShared() 会崩溃。


三、UE 智能指针的关键约束

1. 不能用于 UObject

UE 的 UObject 有自己的垃圾回收(GC)系统 ,基于 UPROPERTY 引用标记。TSharedPtr/TSharedRef 不能管理 UObject,否则会和 GC 冲突(双重释放或 GC 提前回收)。

复制代码
// ❌ 错误:UObject 不能用 TSharedPtr 管理
TSharedPtr<UMyObject> BadObj = MakeShared<UMyObject>();

// ✅ 正确:UObject 用 NewObject + UPROPERTY 引用,由 GC 管理
UMyObject* GoodObj = NewObject<UMyObject>();
UPROPERTY() UMyObject* GoodObjRef;  // GC 会追踪这个引用

面试要点 :UE 中两套内存管理体系并存------原生 C++ 对象用智能指针,UObject 用 GC。两者不能混用。

2. 不支持多态继承的智能指针转换

TSharedPtr<Derived>TSharedPtr<Base> 需要显式 StaticCastSharedPtr/ConstCastSharedPtr,不能像裸指针那样隐式向上转型(实际上 UE 支持隐式向上转换,但向下转换需要显式 cast)。


四、标准库 vs UE 智能指针对应表

标准库 UE 对应 核心区别
std::unique_ptr TUniquePtr 基本一致
std::make_unique MakeUnique 基本一致
std::shared_ptr TSharedPtr UE 默认非线程安全(Fast),可选 ThreadSafe
std::make_shared MakeShared 基本一致
std::weak_ptr TWeakPtr 方法名不同(lock()Pin()
std::enable_shared_from_this TSharedFromThis 基本一致
无对应 TSharedRef UE 特有,编译期保证非空

五、面试高频考点

考点 答案
shared_ptr 控制块里有什么 强引用计数、弱引用计数、删除器、分配器
make_shared 优势 对象+控制块一次分配,少一次 malloc,缓存友好
shared_ptr 线程安全范围 引用计数增减是原子的;对象本身读写、同一实例的赋值不是线程安全的
循环引用怎么解决 一方改用 weak_ptr,不增加强引用
weak_ptr 怎么获取对象 lock()(UE 是 Pin()),返回 shared_ptr,对象已销毁则为空
unique_ptr 能拷贝吗 不能,只能移动
UE 为什么不用 std::shared_ptr 跨平台一致性、自定义分配器、线程安全模式可选、与UE体系集成
TSharedRefTSharedPtr 区别 TSharedRef 永远非空,必须初始化,不需要判空
ESPMode::FastThreadSafe 区别 Fast 用普通整数计数(零开销,单线程),ThreadSafe 用原子操作(多线程安全)
TSharedPtr 能管理 UObject 不能,UObject 有自己的 GC 系统,混用会冲突
TSharedFromThis 作用 在成员函数中安全获取自身的 TSharedPtr/TSharedRef
make_shared 的缺点 对象和控制块一起分配,弱引用存在时对象内存无法单独释放

六、面试答题模板

"C++ 标准智能指针有三种:unique_ptr 独占所有权、不可拷贝可移动、零开销;shared_ptr 共享所有权,通过控制块里的强引用和弱引用计数管理生命周期,make_shared 可以一次分配对象和控制块;weak_ptr 不拥有所有权,用于打破循环引用和观察对象生命周期。UE 有自己的智能指针体系:TUniquePtr 对应 unique_ptrTSharedPtr 对应 shared_ptr 但默认是非线程安全的 Fast 模式,可选 ThreadSafe 模式;UE 特有的 TSharedRef 保证永远非空,不需要判空;TWeakPtr 对应 weak_ptr。需要特别注意的是 UE 的智能指针不能管理 UObject,因为 UObject 有自己的 GC 系统,两者不能混用。"

一、TCP 与 UDP 区别

维度 TCP UDP
全称 传输控制协议 用户数据报协议
连接性 面向连接(三次握手建立) 无连接(直接发)
可靠性 可靠(确认、重传、排序、去重) 不可靠(尽力而为,丢了不重传)
数据形式 面向字节流(无消息边界) 面向数据报(有边界,一次一个完整包)
传输效率 较低(握手、确认、重传开销) 高(头部仅 8 字节,无额外开销)
流量/拥塞控制 有(滑动窗口、慢启动、拥塞避免)
通信模式 仅点对点 一对一、一对多(广播)、多对多(组播)
头部大小 20~60 字节 8 字节
适用场景 文件传输、网页、邮件 游戏、直播、DNS、语音视频

二、TCP 三次握手与四次挥手

2.1 三次握手

复制代码
客户端                             服务器
  | ---- SYN (seq=x) ------------> |   1. 客户端发 SYN,进入 SYN_SENT
  | <--- SYN+ACK (seq=y, ack=x+1)- |   2. 服务器回 SYN+ACK,进入 SYN_RCVD
  | ---- ACK (ack=y+1) ----------> |   3. 客户端回 ACK,双方进入 ESTABLISHED

为什么是三次不是两次?

  • 两次握手无法确认客户端的接收能力和服务器的发送能力。
  • 防止已失效的连接请求(网络中延迟的旧 SYN)突然到达服务器,导致错误建立连接、浪费资源。

2.2 四次挥手

复制代码
客户端                             服务器
  | ---- FIN (seq=u) ------------> |   1. 客户端发 FIN,进入 FIN_WAIT_1
  | <--- ACK (ack=u+1) ----------- |   2. 服务器回 ACK,进入 CLOSE_WAIT
  |                                  (服务器可能还有数据要发)
  | <--- FIN (seq=w) ------------- |   3. 服务器发 FIN,进入 LAST_ACK
  | ---- ACK (ack=w+1) ----------> |   4. 客户端回 ACK,进入 TIME_WAIT

为什么是四次不是三次? TCP 全双工,两个方向分别关闭。服务器收到 FIN 后可能还有数据没发完,不能立即发 FIN,所以 ACK 和 FIN 分两步。

2.3 TIME_WAIT 与 2MSL

客户端发完最后一个 ACK 后等待 2MSL(报文最大生存时间,Linux 默认约 30s~2min)。

为什么等 2MSL?

  1. 保证最后一个 ACK 能到达服务器:ACK 丢了服务器会重发 FIN,客户端在 TIME_WAIT 期间还能重发 ACK。
  2. 让本次连接的所有残留报文在网络中消失,防止旧连接的延迟报文被新连接误收。

三、TCP 半连接队列与全连接队列(高频考点)

TCP 三次握手过程中,服务器内核维护两个队列:

3.1 半连接队列(SYN 队列 / SYN Backlog)

时机 :服务器收到客户端的 SYN 并回了 SYN+ACK 之后、尚未收到客户端最终 ACK 之前(三次握手未完成),连接处于 SYN_RCVD 状态,放入半连接队列

复制代码
客户端 SYN → 服务器回 SYN+ACK → 连接放入半连接队列(SYN_RCVD)
                                      ↓
                              等客户端 ACK(可能永远不来,即 SYN 洪水攻击)
  • 大小由 /proc/sys/net/ipv4/tcp_max_syn_backlog 控制。
  • 半连接队列满了之后,新的 SYN 会被丢弃(或发 SYN Cookie,取决于 tcp_syncookies 配置)。

3.2 全连接队列(Accept 队列)

时机 :服务器收到客户端的第三次握手 ACK,三次握手完成,连接进入 ESTABLISHED 状态,从半连接队列移到全连接队列 ,等待应用层调用 accept() 取走。

复制代码
客户端 ACK → 三次握手完成 → 连接从半连接队列移到全连接队列(ESTABLISHED)
                                              ↓
                                      等应用层 accept() 取走
  • 大小由 listen(fd, backlog)backlog 参数和 /proc/sys/net/core/somaxconn 共同决定(取较小值)。
  • 全连接队列满了之后,新完成握手的连接会被丢弃(或忽略 ACK 让客户端重传,取决于 tcp_abort_on_overflow)。

3.3 两个队列的关系

复制代码
客户端                    服务器内核                          应用层
  │  SYN                   │                                  │
  │ ─────────────────────> │  放入半连接队列(SYN_RCVD)        │
  │  SYN+ACK               │                                  │
  │ <───────────────────── │                                  │
  │  ACK                   │  从半连接队列移除                  │
  │ ─────────────────────> │  放入全连接队列(ESTABLISHED)     │
  │                        │  ─────────────────────────────> │  accept() 取走

3.4 面试追问点

问题 答案
SYN 洪水攻击原理 攻击者发大量 SYN 但不回 ACK,半连接队列被占满,正常连接无法建立
SYN Cookie 作用 半连接队列满时,服务器不分配队列资源,而是把连接信息编码到 ISN(初始序列号)中,客户端回 ACK 时再解码恢复,防御 SYN 洪水
全连接队列满了会怎样 新完成握手的连接被丢弃,客户端以为握手成功但服务器没有,后续数据会触发 RST
怎么查看队列溢出 netstat -stimes the listen queue of a socket overflowed
listen 的 backlog 参数 控制全连接队列大小,实际值受 somaxconn 上限约束

四、TCP 超时重传与重传计时器(高频考点)

4.1 什么是超时重传

TCP 发送方发出一个数据包后,启动一个重传计时器(Retransmission Timer) 。如果在计时器超时之前没有收到接收方的 ACK 确认,就认为包丢了,重新发送该数据包。

复制代码
发送方                              接收方
  │ ---- 数据包(seq=100) ----------> │
  │ 启动重传计时器(RTO)             │
  │                                  │ (包丢了,接收方没收到)
  │ 计时器超时(RTO 到了)            │
  │ ---- 重传数据包(seq=100) ------> │
  │ <--- ACK(ack=101) -------------- │  收到 ACK,取消计时器

4.2 RTO(Retransmission TimeOut,重传超时时间)

RTO 不是固定值,TCP 根据网络状况动态计算

  • RTT(Round Trip Time):数据包发出到收到 ACK 的往返时间。
  • SRTT(Smoothed RTT):平滑后的 RTT,用加权移动平均计算,避免单次波动影响太大。
  • RTTVAR(RTT Variance):RTT 的偏差/抖动。
  • RTO = SRTT + 4 × RTTVAR(RFC 6298 公式)。

RTO 最小 1 秒(Linux 实现中下限通常 200ms~1s),最大 120 秒。

4.3 指数退避(Exponential Backoff)

如果重传后还是没收到 ACK,下一次的 RTO 会翻倍(指数退避):

复制代码
第一次超时:RTO = 1s,重传
第二次超时:RTO = 2s,重传
第三次超时:RTO = 4s,重传
第四次超时:RTO = 8s,重传
...
直到达到最大重传次数(tcp_retries2,默认 15 次,约 9~30 分钟),放弃连接

指数退避的目的:网络拥塞时,越重传越慢,避免加剧拥塞。

4.4 快速重传(Fast Retransmit)

不等计时器超时,发送方如果收到3 个重复的 ACK(表示接收方一直没收到某个包,后面的包都到了),就立即重传丢失的包,不用等 RTO 超时。

复制代码
发送方发了 1,2,3,4,5 号包
接收方收到 1 → ACK 2
接收方收到 3 → ACK 2(重复,等 2 号)
接收方收到 4 → ACK 2(重复)
接收方收到 5 → ACK 2(第 3 个重复 ACK)
发送方收到 3 个重复 ACK → 立即重传 2 号包(不等超时)

4.5 面试追问点

问题 答案
RTO 怎么计算的 基于 SRTT 和 RTTVAR 动态计算,RTO = SRTT + 4×RTTVAR
为什么用指数退避 网络拥塞时逐步降低发送速率,避免雪上加霜
快速重传触发条件 收到 3 个重复 ACK,不等超时直接重传
重传多少次后放弃 Linux 默认 tcp_retries2=15 次,约 9~30 分钟
超时重传和快速重传区别 超时重传等计时器超时(慢),快速重传靠重复 ACK 触发(快)
已发送但未确认的数据存在哪 发送方的发送缓冲区中,直到收到 ACK 才能释放

五、TCP 粘包、长连接、心跳包

5.1 粘包

TCP 面向字节流,无消息边界。发送方发了两个包,接收方可能一次收到粘在一起的数据,也可能一个包被拆成两次。

解决方法(应用层)

方法 原理 适用
固定长度 每个包固定 N 字节 简单但浪费空间
分隔符 包之间用特殊字符分隔 数据中不能出现分隔符
长度字段 头部加包体长度,先读长度再读包体 最常用,游戏标准做法
自定义协议 固定头(魔数+版本+长度+消息ID)+ 包体 游戏/工业级

5.2 长连接

连接建立后保持打开,多次请求复用同一连接,避免频繁握手挥手开销。需要心跳包保活。

5.3 心跳包

长连接中定期发小数据包,确认对方在线、防止 NAT/防火墙断连。连续几次收不到心跳就认为断线,触发重连。游戏一般用应用层心跳(如 10s 一次),不用 TCP KeepAlive(默认 2 小时太迟钝)。


六、UDP 怎么改可靠

在应用层实现 TCP 的可靠机制:

  1. 序列号:每个包加递增序号,用于排序、去重。
  2. 确认应答(ACK):接收方回 ACK 确认收到。
  3. 超时重传:发送方启动计时器,超时没收到 ACK 就重传。
  4. 滑动窗口:连续发送窗口内的包,不用等每个 ACK,实现流量控制。
  5. 快速重传:收到多个重复 ACK 立即重传。
  6. 选择性确认(SACK):ACK 中告知哪些包收到了,只重传丢失的。
  7. 拥塞控制:根据丢包率调整发送速率。

本质:在 UDP 头部之上加一个应用层协议头,在应用层复刻 TCP 的可靠机制。KCP、QUIC、UE 可靠 UDP 都是这个思路。


七、UDP 粘包问题澄清

UDP 不会粘包! UDP 面向数据报,每个数据报独立、有边界。一次 recvfrom 只能收到一个完整的 UDP 包,不会两个包粘在一起,也不会一个包被拆两次。

粘包是 TCP 独有的问题(面向字节流,无边界)。

UDP 真正的问题是:丢包、乱序、重复、缓冲区溢出丢包。另外 UDP 大包超过 MTU 会在 IP 层分片,一个分片丢了整个包就废了,所以应用层需要自己控制包大小或自己分片重组。


八、UE 怎么在 UDP 基础上实现可靠传输

  • NetDriver / NetConnection:管理 UDP socket 和每个客户端连接。
  • 可靠/不可靠标记Reliable 保证送达有序(重要事件),Unreliable 不保证(高频状态同步,丢了下一帧更新)。
  • 序列号 + ACK:每个包有序列号,接收方回 ACK,ACK 中带丢失包列表(选择性确认)。
  • 重传队列:可靠包发送后存入队列,收到 ACK 移除,超时或 ACK 报告丢失则重传。
  • 包合并(Bunch):多个小消息合并到一个 UDP 包,减少包数量。
  • 信道(Channel):Control/Actor/Voice 等逻辑信道独立管理可靠队列。
  • 增量同步 + 相关性 + 频率控制:只同步变化的属性、只同步玩家周围的 Actor、控制同步频率,减少带宽。

九、UDP 优化

手段 说明
增大收发缓冲区 SO_RCVBUF/SO_SNDBUF,减少溢出丢包
批量收发 recvmmsg/sendmmsg,减少系统调用
应用层合包 多个小消息合并成一个 UDP 包
控制包大小 避免 IP 分片,单包 < MTU(~1400 字节)
网卡多队列/RSS 多核并行处理网络包
FEC 前向纠错 发冗余数据,接收方直接恢复丢包,不用等重传(低延迟场景)

十、TCP 和 UDP 可以同时占用同一个端口号吗?

可以。 端口号在传输层协议内部独立编址,TCP 和 UDP 各有自己的端口空间。内核通过协议+IP+端口四元组区分,协议不同就是不同通道,互不干扰。例如 DNS 同时监听 TCP 53 和 UDP 53。


十一、HTTP 与 HTTPS 区别

维度 HTTP HTTPS
端口 80 443
加密 明文 TLS/SSL 加密
证书 不需要 需要 CA 证书
安全性 低(可窃听/篡改/冒充) 高(加密+完整性+身份认证)
性能 稍低(TLS 握手+加解密开销)
握手 TCP 三次握手 TCP 三次握手 + TLS 握手

HTTPS 核心是 TLS:非对称加密(RSA/ECDHE)交换密钥,对称加密(AES)传输数据,HMAC 校验完整性,证书验证身份。


十二、KCP 协议

基于 UDP 的快速可靠传输协议 ,核心思想是用带宽换低延迟

关键特性:

  • 快速重传:收到 2 个重复 ACK 就重传(TCP 要 3 个)。
  • 选择性重传:只重传丢失的包。
  • 非退让流控:丢包时窗口减小比 TCP 温和,不轻易降速。
  • RTO 不指数退避:重传超时增长比 TCP 慢。
  • 拥塞控制可关闭:内网/专用链路可以关拥塞控制,固定速率发。
  • 纯算法:KCP 只负责算法,需要自己实现 UDP 收发。

弱网(丢包 10%)下 TCP 延迟可能几百毫秒,KCP 可控制在几十毫秒,代价是多耗 10%~30% 带宽。广泛用于手游、音视频。


十三、epoll(非重点,简要了解)

epoll 是 Linux 的 IO 多路复用机制,用红黑树管理 fd、就绪链表返回事件,比 select/poll 高效,适合高并发。两种触发模式:LT(水平触发,默认)和 ET(边缘触发,需非阻塞+循环读)。

游戏客户端岗位通常不深入考 epoll(偏服务端),了解概念即可。如果是服务端方向或 UE DS(专用服务器)相关,需要重点掌握。


网络面试答题链(速记)

复制代码
TCP/UDP 区别
  → 三次握手(为什么三次)
  → 半连接队列 / 全连接队列(SYN 洪水、SYN Cookie)
  → 四次挥手(为什么四次)
  → TIME_WAIT / 2MSL
  → 超时重传(RTO 计算、指数退避、快速重传)
  → TCP 粘包(字节流无边界,长度字段解决)
  → UDP 改可靠(序列号+ACK+重传+滑动窗口)
  → UE 可靠 UDP 实现
  → KCP(用带宽换延迟)
  → HTTP/HTTPS(TLS 加密)








### 一、const 修饰变量

**含义**:变量初始化后不可修改,成为编译期可检查的常量。

const int x = 10;

x = 20; // ❌ 编译错误:x 是 const

const int y; // ❌ 编译错误:const 变量必须初始化

复制代码
**面试要点**:

- `const` 变量必须在定义时初始化,之后不能赋值。
- 类的 `const` 成员变量必须在**构造函数初始化列表**中初始化,不能在构造函数体内赋值。

class Foo {

const int id;

public:

Foo(int i) : id(i) {} // ✅ 初始化列表

// Foo(int i) { id = i; } // ❌ 构造函数体内赋值不行

};

复制代码
---

### 二、const 修饰指针:顶层 const vs 底层 const

这是面试最高频的考点,核心是区分**const 修饰的是谁**。

### 2.1 四种写法

| 写法 | 含义 | 指针本身能改吗 | 指向的值能改吗 |
| --- | --- | --- | --- |
| `int* p` | 普通指针 | 能 | 能 |
| `const int* p` | 指向常量的指针(底层 const) | 能 | **不能** |
| `int* const p` | 常量指针(顶层 const) | **不能** | 能 |
| `const int* const p` | 指向常量的常量指针 | **不能** | **不能** |

**判断口诀**:看 `const` 在 `*` 的**左边还是右边**。

- `const` 在 `*` **左边** → 修饰指向的值(底层 const,`*p` 不可改)
- `const` 在 `*` **右边** → 修饰指针本身(顶层 const,`p` 不可改)

int x = 10, y = 20;

const int* p1 = &x; // 底层 const:*p1 不可改,p1 可改

*p1 = 30; // ❌ *p1 是 const

p1 = &y; // ✅ p1 本身可改

int* const p2 = &x; // 顶层 const:p2 不可改,*p2 可改

*p2 = 30; // ✅

p2 = &y; // ❌ p2 是 const

const int* const p3 = &x; // 两者都 const

*p3 = 30; // ❌

p3 = &y; // ❌

复制代码
### 2.2 顶层 const 和底层 const 的拷贝规则

- **顶层 const 不影响拷贝**:常量指针可以拷给普通指针(因为拷的是地址值,源指针 const 不影响副本)。
- **底层 const 不能被忽略**:指向常量的指针不能直接赋给指向变量的指针(否则就可以通过后者修改常量了)。

const int* p1 = &x;

int* p2 = p1; // ❌ 底层 const 不能丢:p1 承诺不修改,p2 可以修改

int* const p3 = &x;

int* p4 = p3; // ✅ 顶层 const 可以丢:p3 本身不能改,但拷给 p4 后 p4 可以改

复制代码
---

### 三、const 修饰引用

**含义**:`const T&` 是常量引用,不能通过引用修改绑定的对象。

int x = 10;

const int& ref = x;

ref = 20; // ❌ ref 是 const 引用

x = 20; // ✅ 原变量可以改,ref 看到的值也会变

复制代码
**关键特性:const 引用可以绑定到临时对象(右值)和不同类型**

const int& r1 = 10; // ✅ 绑定到临时对象(int(10))

int& r2 = 10; // ❌ 非常量引用不能绑定右值

double d = 3.14;

const int& r3 = d; // ✅ 类型转换:创建临时 int,const 引用绑定到临时

int& r4 = d; // ❌ 非常量引用不能绑定到转换后的临时

复制代码
**面试要点**:函数参数用 `const T&` 是推荐写法,既避免拷贝又保证不修改,还能接受右值和临时对象。

void Process(const std::string& s); // ✅ 推荐:可接受左值、右值、临时对象

void Process(std::string& s); // ⚠️ 只能接受左值,不能传临时对象

复制代码
---

### 四、const 修饰成员函数

**含义**:在成员函数声明后加 `const`,承诺该函数**不修改任何成员变量**(也不调用非 const 成员函数)。

class Counter {

int count = 0;

public:

int Get() const { // const 成员函数

return count; // ✅ 只读

// count = 1; // ❌ 不能修改成员

// Increment(); // ❌ 不能调用非 const 成员函数

}

void Increment() { // 非 const 成员函数

count++;

}

};

复制代码
### 4.1 const 对象只能调用 const 成员函数

const Counter c;

c.Get(); // ✅ const 成员函数

c.Increment(); // ❌ const 对象不能调用非 const 成员函数

复制代码
### 4.2 函数重载:const 与非 const 版本

class Buffer {

int data100;

public:

// 非 const 对象调用,返回可修改引用

int& operator\[\](int i) { return datai; }

复制代码
// const 对象调用,返回 const 引用
const int& operator[](int i) const { return data[i]; }

};

Buffer b;

b0 = 42; // ✅ 调用非 const 版本

const Buffer cb;

cb0 = 42; // ❌ 调用 const 版本,返回 const int&,不能赋值

int x = cb0; // ✅

复制代码
**面试要点**:STL 容器的 `operator[]`、`begin()/end()`、`at()` 等都有 const/非 const 两个重载版本,这是标准库的惯用法。

---

### 五、const 修饰函数参数

### 5.1 值传递的 const 参数

void Func(const int x); // x 在函数内不可修改

复制代码
值传递时参数是副本,`const` 只是限制函数内不修改这个副本。对调用方无影响,通常只在函数实现需要保护参数时使用。

### 5.2 指针/引用传递的 const 参数(最常用)

void Print(const std::string& s); // ✅ 推荐:避免拷贝 + 保证不修改

void Process(const int* arr, int n); // ✅ 指针底层 const:不修改数组内容

复制代码
**面试要点**:

- 大对象传参优先用 `const T&`,避免拷贝开销。
- 指针参数加底层 `const` 表示函数不会修改指向的数据。
- 顶层 `const` 参数(如 `int* const p`)对调用方无意义,只在函数内限制指针本身不被改。

---

### 六、const 修饰函数返回值

### 6.1 返回 const 值

const int GetValue(); // 对内置类型无意义,返回的是右值本来就不能改

复制代码
对内置类型返回 `const` 没有实际意义(返回值本身就是右值)。但对类类型有意义:

class Matrix { ... };

const Matrix operator+(const Matrix& a, const Matrix& b);

Matrix a, b, c;

(a + b) = c; // ❌ 如果返回 const,不能对临时对象赋值(防止无意义代码)

复制代码
### 6.2 返回 const 引用 / const 指针

class Config {

std::string name_;

public:

const std::string& GetName() const { return name_; } // ✅ 返回 const 引用

};

Config c;

c.GetName() = "hacked"; // ❌ 返回 const 引用,不能通过返回值修改内部成员

复制代码
**面试要点**:返回成员变量的引用时,如果不希望外部修改内部状态,必须返回 `const T&`。否则外部可以通过返回值篡改对象内部数据,破坏封装。

---

### 七、mutable:突破 const 限制

**含义**:被 `mutable` 修饰的成员变量,即使在 `const` 成员函数中也可以被修改。

class Cache {

mutable int hit_count = 0; // mutable 成员

int data = 0;

public:

int Get() const {

hit_count++; // ✅ mutable 成员在 const 函数中可修改

return data;

}

};

复制代码
**适用场景**:

- 缓存统计(如上面的 hit_count)
- 互斥锁(`mutable std::mutex`,const 函数中加锁)
- 延迟计算(lazy evaluation,const 函数中首次调用时计算并缓存)

class ThreadSafeCounter {

mutable std::mutex mtx; // mutable 锁

int count = 0;

public:

int Get() const {

std::lock_guardstd::mutex lock(mtx); // ✅ const 函数中可以锁 mutable 成员

return count;

}

};

复制代码
---

### 八、const_cast:去掉 const 属性

**含义**:`const_cast` 可以去掉指针或引用的底层 const(也可以加上 const),是唯一能去掉 const 的 C++ 转型运算符。

const int x = 10;

int* p = const_cast<int*>(&x); // 去掉底层 const

*p = 20; // ⚠️ 未定义行为!x 本身是 const 对象,修改它是 UB

复制代码
**安全用法**:对象本身不是 const,只是当前通过 const 指针/引用访问时,可以安全地去掉 const。

void Log(const char* str); // 旧 C 接口,参数应该是 const 但声明没写

void Func(const std::string& s) {

Log(const_cast<char*>(s.c_str())); // 去掉 const 传给旧接口

// 安全:Log 不会修改字符串,只是接口声明不规范

}

复制代码
**面试要点**:

- `const_cast` 只能改变底层 const,不能改变类型本身(不能用 `const_cast` 把 `int*` 转 `double*`)。
- 对**真正的 const 对象**去掉 const 后修改是**未定义行为**。
- 常见合理用途:调用 const 不正确的旧接口、在非 const 函数中复用 const 版本的实现(或反过来)。

---

### 九、constexpr:编译期常量

`constexpr` 是 C++11 引入的,比 `const` 更强的编译期常量约束。

|  | const | constexpr |
| --- | --- | --- |
| 含义 | 运行时常量(初始化后不可改) | 编译期常量(值在编译期确定) |
| 初始化时机 | 可以运行时初始化 | 必须编译期可计算 |
| 能用于数组大小 | 不一定(如果是运行时常量则不行) | 一定可以 |
| 能用于模板参数 | 不一定 | 一定可以 |

const int a = 10; // 编译期常量(字面量初始化)

const int b = rand(); // 运行时常量(运行时才知道值)

constexpr int c = 10; // ✅ 编译期常量

constexpr int d = rand(); // ❌ rand() 不是编译期可计算

int arr1a; // ✅ a 是编译期常量(但取决于编译器,严格来说 a 是 const 不是 constexpr)

int arr2c; // ✅ c 是 constexpr,一定可以

int arr3b; // ❌ b 是运行时常量,不能做数组大小(VLA 扩展除外)

复制代码
**constexpr 函数**:函数可以在编译期求值(如果参数都是编译期常量),也可以在运行期调用。

constexpr int Factorial(int n) {

return n <= 1 ? 1 : n * Factorial(n - 1);

}

int a = Factorial(5); // 编译期计算 = 120

int b = Factorial(rand()); // 运行期计算

复制代码
---

### 十、面试高频考点速查

| 考点 | 答案 |
| --- | --- |
| `const int* p` 和 `int* const p` 区别 | 前者指向的值不可改(底层 const),后者指针本身不可改(顶层 const) |
| const 成员函数能修改成员吗 | 不能,除非成员是 `mutable` |
| const 对象能调用非 const 成员函数吗 | 不能,只能调用 const 成员函数 |
| `const T&` 能绑定临时对象吗 | 能,这是 const 引用的重要特性 |
| 函数参数为什么推荐 `const T&` | 避免拷贝 + 保证不修改 + 能接受右值和临时对象 |
| 返回成员引用为什么要 const | 防止外部通过返回值篡改对象内部状态 |
| `mutable` 作用 | 允许 const 成员函数修改该成员(用于缓存、锁、延迟计算) |
| `const_cast` 安全吗 | 去掉 const 后修改真正的 const 对象是 UB;对象本身非 const 时安全 |
| `const` 和 `constexpr` 区别 | const 是运行时常量,constexpr 是编译期常量 |
| 顶层 const 拷贝时会被忽略吗 | 会(源指针 const 不影响副本) |
| 底层 const 拷贝时会被忽略吗 | 不会(不能把 const 指针赋给非 const 指针) |

---

### 十一、面试答题模板

> 
> "const 是 C++ 中用于保证不可变性的关键字,主要用法有:修饰变量使其初始化后不可修改;修饰指针时分顶层 const(指针本身不可改)和底层 const(指向的值不可改);修饰引用形成 const 引用,可以绑定临时对象;修饰成员函数承诺不修改成员变量,const 对象只能调用 const 成员函数;修饰函数参数避免拷贝并保证不修改;修饰返回值防止外部篡改内部状态。另外 `mutable` 允许 const 函数修改特定成员,`const_cast` 可以去掉底层 const,`constexpr` 是更强的编译期常量。核心思想是 const 正确性(const-correctness)------该不可变的地方都加 const,让编译器帮你检查错误。"

一、const 修饰变量

面试回答

const 修饰的变量初始化后不可修改,成为编译期可检查的常量。必须在定义时初始化,之后不能赋值。

实现

复制代码
const int x = 10;
x = 20;        // ❌ 编译错误
const int y;   // ❌ 必须初始化

// 类的 const 成员必须在初始化列表初始化
class Foo {
    const int id;
public:
    Foo(int i) : id(i) {}       // ✅ 初始化列表
    // Foo(int i) { id = i; }   // ❌ 函数体内赋值不行
};

易错点

  • 类的 const 成员变量不能在构造函数体内赋值,必须用初始化列表。
  • const 变量是"初始化后不可改",不是"编译期常量"------const int b = rand(); 是合法的运行时常量。

二、const 修饰指针:顶层 const vs 底层 const

面试回答

核心是区分 const 修饰的是指针本身 还是指针指向的值 。看 const* 的左边还是右边:左边 = 修饰值(底层 const),右边 = 修饰指针(顶层 const)。

实现

写法 含义 指针能改吗 指向的值能改吗
int* p 普通指针
const int* p 指向常量的指针(底层) 不能
int* const p 常量指针(顶层) 不能
const int* const p 两者都 const 不能 不能
复制代码
int x = 10, y = 20;

const int* p1 = &x;   // 底层 const
*p1 = 30;              // ❌ 值不可改
p1 = &y;               // ✅ 指针可改

int* const p2 = &x;    // 顶层 const
*p2 = 30;              // ✅ 值可改
p2 = &y;               // ❌ 指针不可改

顶层 const 和底层 const 在各场景下的忽略规则(高频考点)

场景 顶层 const 底层 const
拷贝赋值 被忽略(源指针 const 不影响副本) 不能忽略(不能把 const 指针赋给非 const 指针)
auto 推导 被忽略(auto 不保留顶层 const) 保留(auto 保留底层 const)
值传递参数 被忽略(参数是副本,顶层 const 不影响调用方) 不能忽略(实参底层 const 必须匹配或更严格)
函数签名/重载 不算(顶层 const 参数不参与重载) 算(底层 const 参数参与重载)
const_cast 不需要(拷贝自然去掉) 可以去掉/加上(const_cast 的唯一用途)

1. 拷贝时

复制代码
// 顶层 const 拷贝时被忽略
int* const p3 = &x;
int* p4 = p3;               // ✅ 顶层 const 被忽略,p4 是普通指针

// 底层 const 拷贝时不能忽略
const int* p1 = &x;
int* p2 = p1;                // ❌ 底层 const 不能丢
const int* p5 = p1;          // ✅ 保留底层 const

2. auto 推导时

复制代码
const int* const p = &x;     // 顶层 + 底层 const
auto q = p;                   // q 的类型是 const int*(底层保留,顶层被忽略)

const int a = 10;
auto b = a;                   // b 的类型是 int(顶层 const 被忽略)
b = 20;                       // ✅ b 可以改

const auto c = a;             // 要保留顶层 const 需显式写 const auto
// c = 20;                    // ❌

3. 值传递参数时

复制代码
// 顶层 const 参数:对调用方无意义,参数是副本
void f1(int* const p);        // 函数内 p 不可改,但调用方传 int* 或 int* const 都行
void f1(int* p);              // ❌ 和上面是同一个函数,不能重载(顶层 const 不算签名)

// 底层 const 参数:影响调用方
void f2(const int* p);        // 可以传 const int* 和 int*(int* 可隐式转 const int*)
void f2(int* p);              // ✅ 可以重载(底层 const 算签名)

const int* cp = &x;
f2(cp);                       // ✅ 调 const int* 版本
int* np = &x;
f2(np);                       // ✅ 两个版本都能匹配,优先调 int* 版本(更精确)

4. const_cast 时

复制代码
// const_cast 只能操作底层 const
const int* p1 = &x;
int* p2 = const_cast<int*>(p1);  // ✅ 去掉底层 const

// 顶层 const 不需要 const_cast,拷贝时自然去掉
int* const p3 = &x;
int* p4 = p3;                     // ✅ 顶层 const 拷贝时被忽略

易错点

  • 底层 const 不能丢是高频考点:指向常量的指针不能直接赋给指向变量的指针。
  • const int* pint const* p 完全等价(const 在 * 左边都行),别被写法迷惑。
  • auto 不保留顶层 const,要保留需写 const auto
  • 顶层 const 参数不参与函数重载,底层 const 参数参与。

三、const 修饰引用

面试回答

const T& 是常量引用,不能通过引用修改绑定对象。关键特性是可以绑定到临时对象(右值)和不同类型,普通引用不行。

实现

复制代码
int x = 10;
const int& ref = x;
ref = 20;    // ❌ 不能通过 ref 修改
x = 20;      // ✅ 原变量可以改,ref 看到的值也变

// 绑定临时对象
const int& r1 = 10;    // ✅ 绑定到临时 int(10)
int& r2 = 10;           // ❌ 普通引用不能绑右值

// 绑定不同类型(创建临时对象)
double d = 3.14;
const int& r3 = d;      // ✅ 创建临时 int,const 引用绑定
int& r4 = d;            // ❌

易错点

  • 函数参数推荐用 const T&:避免拷贝 + 保证不修改 + 能接受右值和临时对象。
  • void Process(std::string& s) 只能接受左值,传临时对象会编译错误。

四、const 修饰成员函数

面试回答

成员函数声明后加 const,本质是给隐含的 this 指针加底层 const(this 的类型变为 const T* const),因此不能通过 this 修改非 mutable 成员,也不能调用非 const 成员函数。const 对象(const 引用/指针)只能调用 const 成员函数。

注意:不是"不修改任何成员变量"------mutable 成员是例外,可以在 const 函数中修改。

实现

复制代码
class Counter {
    int count = 0;
public:
    int Get() const {       // const 成员函数
        return count;       // ✅ 只读
        // count = 1;       // ❌ 不能改非 mutable 成员
        // Increment();     // ❌ 不能调非 const 函数
    }
    void Increment() { count++; }
};

const Counter c;
c.Get();         // ✅
c.Increment();   // ❌ const 对象不能调非 const 函数

mutable:const 成员函数的例外

mutable 修饰的成员变量,即使在 const 成员函数中也可以被修改。用于逻辑上不改变对象状态、但物理上需要修改的成员。

复制代码
class Cache {
    mutable int hit_count = 0;  // mutable 成员
    int data = 0;
public:
    int Get() const {
        hit_count++;  // ✅ const 函数中可修改 mutable 成员
        return data;
    }
};

// 线程安全:mutable 锁
class ThreadSafeCounter {
    mutable std::mutex mtx;  // mutable 锁
    int count = 0;
public:
    int Get() const {
        std::lock_guard<std::mutex> lock(mtx);  // ✅ const 函数中可以锁 mutable 成员
        return count;
    }
};

mutable 适用场景:缓存统计、互斥锁、延迟计算(lazy evaluation)。

const / 非 const 重载

复制代码
class Buffer {
    int data[100];
public:
    int& operator[](int i) { return data[i]; }              // 非 const 对象
    const int& operator[](int i) const { return data[i]; }  // const 对象
};

STL 容器的 operator[]begin()/end()at() 都有这两个重载版本。

重载有效和无效的方式(高频考点)

情况 能否重载 原因
成员函数 const vs 非 const ✅ 能 成员函数的 const 是签名的一部分(隐含的 this 指针底层 const 不同)
值参数顶层 const(int vs const int ❌ 不能 顶层 const 不算签名,参数是副本
指针参数顶层 const(int* vs int* const ❌ 不能 顶层 const 不算签名
指针参数底层 const(int* vs const int* ✅ 能 底层 const 算签名
引用参数底层 const(int& vs const int& ✅ 能 底层 const 算签名
仅返回值 const 不同 ❌ 不能 返回值不参与重载决议
复制代码
// ✅ 有效:成员函数 const 重载
int Get();
int Get() const;

// ✅ 有效:指针/引用底层 const 重载
void f(int* p);
void f(const int* p);
void f(int& r);
void f(const int& r);

// ❌ 无效:值参数顶层 const(重复定义)
void g(int x);
void g(const int x);        // 和上面是同一个函数

// ❌ 无效:指针参数顶层 const(重复定义)
void h(int* p);
void h(int* const p);       // 和上面是同一个函数

// ❌ 无效:仅返回值不同
int k();
const int k();              // 不能仅靠返回值重载

易错点

  • const 成员函数的本质是 this 指针变为 const T*(底层 const),不是"不修改任何成员"------mutable 成员可以改。
  • 成员函数的 const 算签名的一部分,可以和非 const 版本重载。
  • 顶层 const 参数不参与重载,底层 const 参数参与。
  • mutable 只能修饰非静态成员变量,不能修饰局部变量、函数或类;不能滥用,是为"物理修改但逻辑不变"的场景设计的。

五、const 修饰函数参数

面试回答

大对象传参优先用 const T&,既避免拷贝又保证不修改,还能接受右值。指针参数加底层 const 表示函数不修改指向的数据。

实现

复制代码
void Print(const std::string& s);    // ✅ 推荐:避免拷贝 + 不修改
void Process(const int* arr, int n); // ✅ 底层 const:不修改数组
void Func(const int x);               // 值传递 const:只限制函数内不改副本,对调用方无意义

易错点

  • 顶层 const 参数(如 int* const p)对调用方无意义,只在函数内限制指针本身,且不参与重载。
  • 小对象(int、float)值传递就行,不需要 const T&(引用反而有间接寻址开销)。

六、const 修饰函数返回值

面试回答

返回成员变量的引用时,如果不希望外部修改内部状态,必须返回 const T&,否则外部可以通过返回值篡改对象内部数据,破坏封装。

实现

复制代码
class Config {
    std::string name_;
public:
    const std::string& GetName() const { return name_; }  // ✅ 返回 const 引用
};

Config c;
c.GetName() = "hacked";  // ❌ 返回 const 引用,不能赋值

返回 const 值对内置类型无意义(返回值本身就是右值),对类类型有意义------可以防止对临时对象赋值(如 (a+b)=c)。

易错点

  • 返回 T& 而不是 const T&,外部就能直接修改内部成员,等于把私有成员暴露了。
  • 返回局部变量的引用是悬垂引用,和 const 无关,直接 UB。
  • 仅返回值 const 不同不能重载。

七、const_cast:去掉 const 属性

面试回答

const_cast 是唯一能去掉 const 的 C++ 转型运算符,只能改变指针/引用的底层 const,不能改变类型本身。对真正的 const 对象去掉 const 后修改是未定义行为。

实现

复制代码
const int x = 10;
int* p = const_cast<int*>(&x);
*p = 20;  // ⚠️ UB!x 本身是 const 对象

// 安全用法:对象本身非 const,只是当前通过 const 访问
void Log(char* str);  // 旧接口声明不规范,但实际不修改
void Func(const std::string& s) {
    Log(const_cast<char*>(s.c_str()));  // ✅ 安全
}

const_cast 与顶层/底层 const

  • const_cast 只能操作底层 const(指针/引用指向的东西的 const 性)。

  • 顶层 const 不需要 const_cast,拷贝时自然去掉。

    const int* p1 = &x;
    int* p2 = const_cast<int*>(p1); // ✅ 去掉底层 const

    int* const p3 = &x;
    int* p4 = p3; // ✅ 顶层 const 拷贝时被忽略,不需要 const_cast

易错点

  • const_cast 不能改变类型:const_cast<double*>(int_ptr) 编译错误。
  • 去掉 const 后修改真正的 const 对象是 UB(编译器可能把 const 变量放只读段,修改触发段错误)。
  • 合理用途:对接 const 声明不正确的旧接口、在非 const 函数中复用 const 版本实现。
  • const_cast 不能去掉顶层 const(也不需要)。

八、constexpr:编译期常量

面试回答

constexpr 是 C++11 引入的,比 const 更强的约束------值必须在编译期确定const 只是"运行时不可改",constexpr 是"编译期就能算出来"。

实现

const constexpr
含义 运行时常量 编译期常量
初始化 可以运行时初始化 必须编译期可计算
数组大小 不一定 一定可以
模板参数 不一定 一定可以
复制代码
const int a = 10;            // 编译期常量(字面量初始化)
const int b = rand();        // 运行时常量(合法!)
constexpr int c = 10;        // ✅ 编译期常量
constexpr int d = rand();    // ❌ rand() 不是编译期可计算

int arr2[c];   // ✅ constexpr 一定可以做数组大小
int arr3[b];   // ❌ 运行时常量不能做数组大小

constexpr 函数:参数都是编译期常量时在编译期求值,否则运行期求值。

复制代码
constexpr int Factorial(int n) {
    return n <= 1 ? 1 : n * Factorial(n - 1);
}
int a = Factorial(5);         // 编译期 = 120
int b = Factorial(rand());    // 运行期计算

易错点

  • const 不等于编译期常量,const int b = rand(); 是合法的。
  • constexpr 变量必须用编译期可计算的表达式初始化。
  • C++11 的 constexpr 函数限制很多(只能有 return 语句),C++14 起放松。

九、面试速查表

考点 答案
const int* p vs int* const p 前者值不可改(底层),后者指针不可改(顶层)
const 成员函数能改成员吗 不能改非 mutable 成员,mutable 成员可以改
const 成员函数的本质 隐含的 this 指针变为 const T*(底层 const)
const 对象能调非 const 函数吗 不能,只能调 const 成员函数
const T& 能绑临时对象吗 能,这是关键特性
函数参数为什么推荐 const T& 避免拷贝 + 不修改 + 能接受右值
返回成员引用为什么要 const 防止外部通过返回值篡改内部状态
mutable 作用 允许 const 函数修改该成员(缓存、锁、延迟计算)
mutable 属于哪里 属于 const 成员函数的例外机制,和 const 成员函数一起讲
const_cast 安全吗 改真正的 const 对象是 UB;对象本身非 const 时安全
const vs constexpr const 是运行时常量,constexpr 是编译期常量
顶层 const 拷贝时能忽略吗
底层 const 拷贝时能忽略吗 不能
auto 推导保留顶层 const 吗 不保留,要保留需写 const auto
auto 推导保留底层 const 吗 保留
顶层 const 参数参与重载吗 不参与
底层 const 参数参与重载吗 参与
成员函数 const 能重载吗 能(this 指针底层 const 不同)
仅返回值 const 不同能重载吗 不能
const_cast 能去掉顶层 const 吗 不能(也不需要,拷贝自然去掉)
const_cast 能去掉底层 const 吗 能(这是它的唯一用途)
值传递顶层 const 对调用方有意义吗 没有,参数是副本

十、面试答题模板

"const 是 C++ 中保证不可变性的关键字,主要用法:修饰变量初始化后不可改;修饰指针分顶层 const(指针本身不可改)和底层 const(指向的值不可改);修饰引用形成 const 引用,可以绑定临时对象;修饰成员函数本质是给隐含的 this 指针加底层 const,因此不能修改非 mutable 成员,const 对象只能调 const 函数,mutable 是 const 函数的例外,用于缓存、锁等物理修改但逻辑不变的场景;修饰函数参数用 const T& 避免拷贝并保证不修改;修饰返回值用 const T& 防止外部篡改内部状态。顶层 const 在拷贝、auto 推导、值传递时会被忽略,不参与函数重载;底层 const 不能被忽略,参与重载。另外 const_cast 可以去掉底层 const,constexpr 是更强的编译期常量。核心思想是 const 正确性------该不可变的地方都加 const,让编译器帮你检查错误。"

相关推荐
qq_199886871 小时前
第6板块 第2节 CUDA运行时API与驱动API 核心笔记
c++·人工智能·gpu算力
青山是哪个青山1 小时前
LeetCode 188:买卖股票的最佳时机 IV
算法
暮雨封夕1 小时前
跳表(Skip List)详解:原理、实现、使用场景与实际应用
数据结构
a187927218311 小时前
【算法】树(二):队列与祖先——BFS 骨架三配件、短路透传与合流
算法·leetcode·二叉树··bfs·算法讲解·树遍历
miller-tsunami1 小时前
排序的相关知识点
数据结构·排序算法
syagain_zsx2 小时前
算法基础篇 · 03 枚举(C++ 题解)
c++·算法·二进制·枚举
weixin_307779132 小时前
C++代码实现MATLAB中的ode23tb函数功能
开发语言·c++·算法·matlab
S_WEAN3 小时前
C++STL - string类的模拟实现
c++
S_WEAN3 小时前
【C++】STL - string类
c++