目录
[1. lvalue / xvalue / prvalue 的区别 + 例子](#1. lvalue / xvalue / prvalue 的区别 + 例子)
[2. 为什么 std::move 只是转右值引用?什么时候真的触发移动?](#2. 为什么 std::move 只是转右值引用?什么时候真的触发移动?)
[3. std::forward vs std::move 与适用场景](#3. std::forward vs std::move 与适用场景)
[4. 拷贝构造 / 移动构造 / 拷贝赋值 / 移动赋值 / 析构 的调用时机](#4. 拷贝构造 / 移动构造 / 拷贝赋值 / 移动赋值 / 析构 的调用时机)
[5. Rule of 3 / 5 / 0 是什么?工程中如何取舍?](#5. Rule of 3 / 5 / 0 是什么?工程中如何取舍?)
[6. 含 / 不含虚函数的对象内存布局差异](#6. 含 / 不含虚函数的对象内存布局差异)
[7. 多重继承与虚继承对对象布局、指针转换的影响](#7. 多重继承与虚继承对对象布局、指针转换的影响)
[8. override / final / =default / =delete 的语义与用法](#8. override / final / =default / =delete 的语义与用法)
[9. explicit 解决什么问题?隐式转换风险](#9. explicit 解决什么问题?隐式转换风险)
[10. 友元(friend)的作用与边界](#10. 友元(friend)的作用与边界)
[11. mutable 的典型应用场景?与 const 成员函数的关系?](#11. mutable 的典型应用场景?与 const 成员函数的关系?)
[12. sizeof 常见陷阱:空类、虚函数、padding 等](#12. sizeof 常见陷阱:空类、虚函数、padding 等)
1. lvalue / xvalue / prvalue 的区别 + 例子
大白话:这三种是 C++11 之后"值类别"的三种主要形态。
-
lvalue(左值)
-
有名字 、有可持久的地址 ,可以放在
&后取地址。 -
典型:变量、函数、返回左值引用的表达式。
-
例子:
int x = 42; // x 是 lvalue int& r = x; // r 是 lvalue
-
-
xvalue(将亡值)
-
一种"即将被销毁"的右值,通常是绑定到右值引用的对象。
-
典型:
std::move(x)的结果,返回T&&的函数结果。 -
例子:
int x = 42; int&& rx = std::move(x); // std::move(x) 是 xvalue
-
-
prvalue(纯右值)
-
算出来的临时值,没有身份,只是一个"值",不能取地址(对临时再取地址是另一回事)。
-
典型:字面量、返回非引用的函数结果。
-
例子:
int y = 42; // 42 是 prvalue int f(); int z = f(); // f() 这个表达式是 prvalue
-
简单记法:
-
lvalue:有名字、能取地址。
-
xvalue:右值 + 有资源 + 将要被移动/销毁。
-
prvalue:纯粹的"数字/值"。
2. 为什么 std::move 只是转右值引用?什么时候真的触发移动?
-
std::move的实现本质就是:template<typename T> constexpr typename std::remove_reference<T>::type&& move(T&& t) { return static_cast<typename std::remove_reference<T>::type&&>(t); } -
它完全不做移动操作 ,只做一个
static_cast:把一个对象(通常是 lvalue)标记为 xvalue(右值引用)。
真正的"移动"在什么时候发生?
当这个 xvalue 被用来:
-
调用移动构造:
std::string s = "hello"; std::string t = std::move(s); // 这里调用 std::string 的移动构造函数 -
调用移动赋值:
std::string s = "hello"; std::string t; t = std::move(s); // 调用移动赋值 -
或者传入 STL 容器 / 算法时,它们内部根据值类别选择调用 move ctor/assign。
总结一句:
-
std::move只是"贴标签:这个东西可以被当作右值使用了", -
真正的移动是后面那个构造函数/赋值运算符里发生的。
3. std::forward vs std::move 与适用场景
-
std::move-
无条件地把参数当成右值:总是 xvalue。
-
用途:你就是要把对象"交出去不再用",比如类的
operator=、转移资源等。 -
例子:
void sink(std::string&& s); std::string x = "hi"; sink(std::move(x)); // 一定以右值方式传入
-
-
std::forward<T>-
条件转发:如果原本是右值,就转成右值;如果原本是左值,就保持左值。
-
前提:配合转发引用(又叫万能引用)使用:
template<typename T> void wrapper(T&& x) { foo(std::forward<T>(x)); // 保持 x 的"原始值类别" } -
适用场景:完美转发(在泛型函数里,把参数原样转发给另一个函数)。
-
简记:
-
"我就是要把它当右值处理" →
std::move -
"我不知道它是左值还是右值,但要原样转发" →
std::forward
4. 拷贝构造 / 移动构造 / 拷贝赋值 / 移动赋值 / 析构 的调用时机
假设类为 T。
-
拷贝构造
T(const T&)-
用一个 lvalue T 初始化新对象时:
T a; T b = a; // 拷贝构造 T c(a); // 拷贝构造
-
-
移动构造
T(T&&)-
用一个 xvalue / 右值 T 初始化新对象时:
T a; T b = std::move(a); // 移动构造 T c(T{}); // 移动构造(T{} 是 prvalue)
-
-
拷贝赋值
T& operator=(const T&)-
已存在对象,被一个 lvalue 赋值:
T a, b; b = a; // 拷贝赋值
-
-
移动赋值
T& operator=(T&&)-
已存在对象,被一个 xvalue / 右值 T 赋值:
T a, b; b = std::move(a); // 移动赋值
-
-
析构
~T()-
对象生命周期结束时:
-
变量离开作用域
-
delete删除对象 -
容器销毁其元素
-
-
说明:返回值优化 (RVO) / NRVO 可能会让一些预期中的拷贝/移动被省略,但从抽象语义上可按拷贝/移动构造理解。
5. Rule of 3 / 5 / 0 是什么?工程中如何取舍?
-
Rule of 3
-
如果你需要自己写 析构函数,通常也需要写:
-
拷贝构造函数
-
拷贝赋值运算符
-
-
因为这三者都涉及资源管理(堆内存、文件句柄等)。
-
-
Rule of 5(C++11 之后)
-
有资源管理时,除了上面三个,还要考虑:
-
移动构造函数
-
移动赋值运算符
-
-
一般五个要成套考虑。
-
-
Rule of 0
-
最好根本就不要自己管理资源;
-
资源交给 RAII 类(如
std::vector,std::string,std::unique_ptr等)管理; -
自己的类里只用这些成员对象,不写任何特殊成员函数,让编译器生成默认的就好。
-
工程实践:
-
优先遵守 Rule of 0:只用标准容器/智能指针。
-
如果真的要自己 hold 原始资源(
new/ 文件句柄 / socket 等),要么再封一个 RAII 的小类 + 你的大类里只用这个小类成员;
要么就老老实实按 Rule of 5 全部写好,防止资源泄漏/双重释放。
6. 含 / 不含虚函数的对象内存布局差异
标准没规定具体布局,但主流 ABI(如 Itanium)通常是:
-
不含虚函数的类:
-
对象内存大致是:
-
从第一个非静态成员开始,按声明顺序排布
-
中间可能插入填充字节(padding)保证对齐
-
-
例如:
struct A { int x; char c; double d; }; -
布局:
[int x][char c][padding][double d]。
-
-
含虚函数的类:
-
每个对象里通常多一个 虚函数表指针(vptr):
-
通常位于对象内存的起始位置(实现细节)。
-
vptr指向类的虚函数表(vtable)。
-
-
大致布局:
[vptr][成员1][成员2]...[padding...]
-
影响:
-
sizeof会变大(至少包含一个指针大小)。 -
通过基类指针调用虚函数时,先通过
vptr找到vtable再调用对应函数。
7. 多重继承与虚继承对对象布局、指针转换的影响
多重继承:
struct Base1 { int a; };
struct Base2 { int b; };
struct Derived : Base1, Base2 { int c; };
-
Derived对象里会包含两个 base 子对象:[Base1 子对象][Base2 子对象][Derived 自身成员]
-
Base1*指向的是Derived对象的开头; -
Base2*指向的是Derived对象中间偏移的位置; -
所以:
Derived*转为Base2*时,编译器需要做指针调整(加上偏移)。
虚继承:
struct VBase { int x; };
struct A : virtual VBase {};
struct B : virtual VBase {};
struct C : A, B {};
-
VBase是虚基类,最终在C中只保留一个共享的 VBase 子对象。 -
对象中通常存在一些"虚基表指针"(vbptr)或额外信息,帮助运行时从
A/B子对象找到那一个VBase子对象。 -
任何从
C*转到VBase*、或朴素A*/B*再转VBase*的过程中,编译器都要做更复杂的指针调整。
总结:
-
多重继承 → 一个派生对象里有多个基类子对象 → 从派生指针到各基类指针要做不同的偏移。
-
虚继承 → 多条继承链共用一个虚基子对象 → 需要额外的信息来定位虚基 → 指针转换更复杂,布局更复杂。
8. override / final / =default / =delete 的语义与用法
-
override-
声明这是一个重写基类虚函数的函数。
-
编译器会检查签名是否与基类虚函数完全匹配,不匹配就报错。
struct Base {
virtual void foo(int);
};
struct Derived : Base {
void foo(int) override; // 正确
// void foo(double) override; // 编译错误:没真 override
};
-
-
final-
用在虚函数上:禁止在派生类中继续重写。
-
用在类上:禁止该类被继承。
struct Base {
virtual void foo() final; // 不能再被 override
};struct A final {}; // 不能再继承 A
// struct B : A {}; // 编译错误
-
-
=default-
显式要求编译器生成默认实现(默认构造,拷贝构造等)。
struct S {
S() = default; // 显式要一个默认构造
S(const S&) = default; // 默认拷贝构造
~S() = default; // 默认析构
};
-
-
=delete-
显式禁止某个函数被使用(禁用拷贝、禁用某种参数等等)。
struct NonCopyable {
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 禁用拷贝
NonCopyable& operator=(const NonCopyable&) = delete;
};void foo(int) = delete; // 禁止调用 foo(int)
-
9. explicit 解决什么问题?隐式转换风险
默认情况下,如果构造函数只带一个实参(或有默认值能当作一个实参使用),就可以参与隐式转换:
struct X {
X(int); // 没有 explicit
};
void f(X);
f(10); // 10 隐式转换为 X(10),然后传给 f
问题:
-
容易在不经意间发生转换,产生模棱两可或性能/逻辑问题。
-
常见风险:构造函数或
operator T()参与条件判断、重载解析等。
explicit 的作用:
-
阻止构造函数/转换函数参与隐式转换,只能显式调用:
struct X { explicit X(int); explicit operator bool() const; }; X x1(10); // OK // f(10); // 编译错误,不能隐式转换为 X if (x1) { } // C++11 前不行;C++11 后 `if (static_cast<bool>(x1))`
对转换运算符的隐式转换风险:
-
如果有
operator bool()未加explicit,对象可能在if(obj)、算式中被隐式转成 bool,还可能影响与其他重载的解析,带来难以发现的 bug。
10. 友元(friend)的作用与边界
-
作用:
-
允许某个函数 或类访问当前类的 private/protected 成员:
class A { friend void foo(A&); friend class B; private: int x; }; -
常用于:
-
运算符重载(如对称的
operator+,operator<<) -
两个类之间需要紧密协作(如容器和迭代器)
-
-
-
边界与注意点:
-
友元不是"成员",只是增加访问权限。
-
友元关系不传递 、不继承:
- A 是 B 的友元,不意味着 A 是 B 子类的友元。
-
滥用 friend 会破坏封装,增加耦合。
-
常见做法:把友元限制在少数必要的辅助函数和内部类上。
-
11. mutable 的典型应用场景?与 const 成员函数的关系?
mutable:
-
标记某个非静态成员,即使对象是
const,也允许修改这个成员。 -
典型用途:逻辑上是"缓存/统计信息"之类的非逻辑状态。
例子 1:缓存计算结果
class BigCalc {
public:
int value() const {
if (!cached_) {
result_ = expensive_compute();
cached_ = true;
}
return result_;
}
private:
int expensive_compute() const;
mutable bool cached_ = false;
mutable int result_ = 0;
};
例子 2:const 方法中加锁
class ThreadSafe {
public:
int get() const {
std::lock_guard<std::mutex> lk(mutex_);
return data_;
}
private:
int data_;
mutable std::mutex mutex_;
};
和 const 成员函数的关系:
-
const成员函数禁止修改(非mutable的)成员。 -
mutable成员是特例:在const成员函数中可以修改,用于遵守"逻辑常量性"(logical constness)。
12. sizeof 常见陷阱:空类、虚函数、padding 等
-
空类大小不为 0
struct Empty {}; sizeof(Empty); // >= 1标准要求每个对象有唯一地址,所以至少 1 字节。
-
含虚函数的类
struct A { virtual void f(); }; sizeof(A); // 至少 = 一个指针大小(存 vptr) + padding实际可能 > 一个指针大小,因为还要考虑对齐和成员。
-
对齐与填充(padding)
struct S { char c; int i; char d; };布局中会在
c和i之间插入 padding,以保证i对齐;
sizeof(S)可能远大于sizeof(char)*2 + sizeof(int)。 -
继承中的 sizeof
-
带虚继承、多重继承时,对象中可能有额外指针/偏移表,
sizeof也会比你想的复杂。 -
不要简单地"字段相加"。
-
-
表达式
sizeof不求值int f(); sizeof(f()); // 只看类型,不会调用 f()