基础语法注重与 C 不同的特性。包括基础的数据类型、对函数的增强、一些其他特性。
数据类型
强类型
C++ 的 “强类型” 特性体现在对类型的严格区分和对隐式转换的限制上,C 语言允许宽松的隐式转换(弱类型特征)。这种设计的核心目标是在编译期发现类型不匹配的错误,减少运行时异常,提升代码安全性。其核心思路是:除非开发者明确指示(显式转换),否则编译器默认禁止可能不安全的类型混用。
强类型语言的核心特征
不同类型之间的操作受到严格限制,类型转换(尤其是隐式转换)需要满足明确的规则,不允许随意的、可能导致歧义的类型混用。
C++ 的强类型体现在:
- 每种类型有明确的语义和操作限制(如
int和float不能随意混用); - 隐式转换仅在 “安全且无歧义” 的场景下允许(如
int→long); - 多数跨类型操作需要显式转换(如
void*→int*)。
1. void* 的转换
- C 语言:
void*可以隐式转换为任何指针类型,无需显式转换 - C++ 语言:
void*不能隐式转换为其他指针类型,必须显式转换:
void* ptr = new int[10];
// int* int_ptr = ptr; // C++编译错误:必须允许void*隐式转换
int* int_ptr = static_cast<int*>(ptr); // 必须显式转换2. 整数与指针的转换
- C 语言:整数和指针可以随意隐式转换(风险极高):
- C++ 语言:禁止整数与指针的隐式转换,必须显式转换(且不推荐):
int x = 0x7fffffff;
// int* ptr = x; // C++编译错误:整数不能隐式转为指针
int* ptr = reinterpret_cast<int*>(x); // 必须显式转换(不推荐)3. 枚举类型的转换
- C 语言:枚举常量本质是
int,可与整数随意隐式转换: - C++ 语言:枚举是独立类型,与整数的转换需显式进行:
enum Color { RED, GREEN };
// Color c = 1; // C++编译错误:整数不能隐式转为枚举
Color c = static_cast<Color>(1); // 必须显式转换
// int x = RED; // C++11前允许(兼容C),但现代C++建议显式转换
int x = static_cast<int>(RED);更严格的是 C++11 引入的 enum class(强类型枚举),完全禁止与整数隐式转换:
enum class Color { RED, GREEN };
// int x = Color::RED; // 编译错误:强类型枚举不能隐式转为整数4. 布尔类型的转换
-
C 语言:任何整数 / 指针都可隐式视为 “布尔值”(0 为假,非 0 为真):
-
C++ 语言:虽然也允许整数 / 指针隐式转为
bool(兼容 C 的常见场景),但限制更严格:- 仅允许 “零值→
false,非零值→true” 的转换; - 反之,
bool转为整数时,true固定为 1(C 中可能因实现不同而变化):
bool b = true; int x = b; // C++中x必为1(C中可能是其他非零值) - 仅允许 “零值→
5. 窄化转换(Narrowing Conversion)
- C 语言:允许隐式窄化转换(如
double→int可能丢失小数部分): - C++ 语言:禁止隐式窄化转换(编译错误):
double d = 3.14;
// int x = d; // C++编译错误:不允许double隐式转为int(窄化)
int x = static_cast<int>(d); // 必须显式转换(明确告知编译器接受截断)注意:列表初始化({})对窄化转换的检查更严格,完全禁止任何可能丢失信息的转换。
类型转换
在 C++ 中,四种强制类型转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)各有其特定的用途和限制。以下是它们的详细解析:
1. static_cast
- 作用
- 编译时类型检查:在编译阶段进行类型合法性验证。
- 基本类型转换:如
int转double、float转int。 - 类层次结构中的转换:支持上行转换(派生类 → 基类,安全)和下行转换(基类 → 派生类,需谨慎)。
void*指针转换:将void*转换为具体类型的指针。
- 使用场景
- 基本类型转换:
-
double d = 3.14; int i = static_cast<int>(d); // 输出 3 -
上行转换(安全):
-
class Base {}; class Derived : public Base {}; Derived d; Base* b = static_cast<Base*>(&d); // 合法,派生类指针转基类指针 -
下行转换(需谨慎):
-
Base* b = new Derived(); Derived* d = static_cast<Derived*>(b); // 合法,但无运行时检查
- 下行转换风险:若基类指针实际指向非目标类型对象,可能导致未定义行为。
- 不支持跨继承链转换:如无继承关系的类之间转换会报错。
2. dynamic_cast
- 作用
- 运行时类型检查:依赖 RTTI(Run-Time Type Information),仅适用于多态类型(包含虚函数的类)。
- 安全向下转换:检查转换是否合法,失败时返回
nullptr(指针)或抛出异常(引用)。
- 使用场景
-
多态类型的安全向下转换:
-
class Base { public: virtual ~Base() {} // 必须有虚函数 }; class Derived : public Base {}; Base* b = new Derived(); Derived* d = dynamic_cast<Derived*>(b); // 成功,d != nullptr if (d) { // 使用 d } else { // 转换失败 }
- 仅限多态类型:基类必须有虚函数,否则编译错误。
- 性能开销:运行时类型检查会增加额外开销,不适合频繁调用。
- 交叉转换限制:无法在无继承关系的类之间转换。
3. const_cast
- 作用
- 添加或移除
const/volatile属性:修改对象的访问权限,而非实际类型。 - 典型用途:绕过
const限制(需谨慎,可能导致未定义行为)。
- 添加或移除
- 使用场景
- 修改
const对象的指针:
-
const int a = 42; int* p = const_cast<int*>(&a); *p = 43; // 未定义行为(修改 const 对象) -
调用非
const成员函数:
-
class MyClass { public: void non_const_func() { /* ... */ } }; const MyClass obj; MyClass* p = const_cast<MyClass*>(&obj); p->non_const_func(); // 未定义行为(修改 const 对象)
- 风险性:修改
const对象可能导致未定义行为,需严格控制使用场景。 - 仅限指针/引用:
const_cast只能用于指针或引用类型。
4. reinterpret_cast
-
作用
- 底层指针/地址转换:将指针类型转换为不相关类型,或指针与整数类型互转。
- 无类型检查:完全依赖程序员对内存布局的理解,高度危险。
-
使用场景
- 指针类型转换:
-
int a = 10; int* p = &a; char* c = reinterpret_cast<char*>(p); // 将 int* 转为 char* -
指针与整数互转:
-
int* p = new int(42); uintptr_t addr = reinterpret_cast<uintptr_t>(p); // 指针转整数 int* p2 = reinterpret_cast<int*>(addr); // 整数转指针
- 危险性:转换后若访问非法内存,可能导致崩溃或未定义行为。
- 平台依赖:指针与整数的大小和对齐方式可能因平台不同而变化。
- 仅限专家使用:需充分理解内存布局和目标平台特性。
对比总结
| 转换类型 | 用途 | 安全性 | 适用场景 | 示例 |
|---|---|---|---|---|
static_cast | 编译时检查的类型转换 | 高 | 基本类型、类继承转换 | int → double |
dynamic_cast | 运行时类型检查 | 中 | 多态类型的向下转换 | Base* → Derived* |
const_cast | 添加/移除 const/volatile 属性 | 低 | 修改 const 对象的访问权限 | const int* → int* |
reinterpret_cast | 低层指针/地址转换 | 极低 | 指针类型互转、指针与整数互转 | int* → char* |
- 优先使用
static_cast:大多数类型转换需求可通过static_cast满足。 - 多态类型使用
dynamic_cast:需运行时类型检查时(如向下转换)。 - 谨慎使用
const_cast:仅在明确知道后果时修改const属性。 - 避免
reinterpret_cast:除非处理底层硬件或协议,否则尽量避免。
自动类型推导(C++11)
C++11 及后续标准引入了自动类型推导机制,核心工具包括 auto、decltype 以及 C++14 新增的 decltype(auto)。这些特性大幅简化了代码编写(尤其是模板和泛型编程中),同时保持了类型安全性。
auto:根据初始化表达式推导变量类型
auto 的核心功能是让编译器根据变量的初始化表达式自动推导其类型,无需显式声明。其设计初衷是简化复杂类型(如模板迭代器、lambda 表达式类型)的声明。
- 基本用法与推导规则
auto 必须结合初始化表达式使用(变量必须初始化),编译器会根据表达式的类型推导变量类型,并忽略顶层 const/volatile 和引用。
// 基础类型推导
auto a = 10; // a的类型:int(推导自整数字面量10)
auto b = 3.14; // b的类型:double(推导自浮点字面量3.14)
auto c = 'a'; // c的类型:char
auto d = true; // d的类型:bool
// 忽略顶层const和引用
const int x = 5;
auto x1 = x; // x1的类型:int(顶层const被忽略)
auto& x2 = x; // x2的类型:const int&(加&后保留底层const)
int y = 10;
int& ry = y;
auto z = ry; // z的类型:int(引用被忽略,推导为被引用类型)
auto& rz = ry; // rz的类型:int&(加&后保留引用)关键规则:
-
auto推导时会 “退化” 类型(类似数组名退化为指针、函数名退化为函数指针):int arr[5] = {1,2,3,4,5}; auto arr1 = arr; // arr1的类型:int*(数组名退化) auto& arr2 = arr; // arr2的类型:int(&)[5](加&后保留数组类型) -
若初始化表达式是引用,
auto会推导为被引用的类型(而非引用本身),除非显式添加&。 -
C++11 中
auto仅用于变量声明,C++14 扩展了其适用场景: -
函数返回类型推导:
// C++14起支持,编译器根据return语句推导返回类型 auto add(int a, double b) { return a + b; // 返回类型:double(int隐式转换为double) }注意:若函数有多个 return 语句,所有返回类型必须可推导为同一类型,否则编译错误。
-
lambda 表达式参数:
// C++14起,lambda参数可使用auto(本质是泛型lambda) auto sum = [](auto a, auto b) { return a + b; }; sum(1, 2); // 推导为int+int,返回3 sum(3.14, 2.7); // 推导为double+double,返回5.84 -
3.
auto的限制 -
不能用于未初始化的变量:
auto x;(编译错误,无初始化表达式无法推导)。 -
不能用于函数参数(非 lambda):
void func(auto x) {}(C++17 前不支持,C++20 起可用于模板函数简化)。 -
不能推导数组类型(除非用引用):如上述
arr1会退化为指针。
decltype:推导表达式的精确类型
decltype(“declare type” 的缩写)用于推导表达式的类型,但不执行表达式(仅分析类型)。与 auto 不同,它会完整保留表达式的类型信息(包括 const、volatile、引用等),且无需初始化变量。
1. 基本用法
语法:decltype(表达式) 变量名;(变量可初始化也可不初始化)。
int x = 10;
const int& rx = x;
// 推导变量类型
decltype(x) a; // a的类型:int(x是int)
decltype(rx) b = x; // b的类型:const int&(rx是const int&,保留引用和const)
// 推导表达式类型
decltype(x + 3) c; // c的类型:int(x+3是int类型的表达式)
decltype(x * 1.5) d; // d的类型:double(int*double结果为double)2. 特殊规则:表达式的值类别影响推导结果
decltype 的推导结果与表达式的 “值类别”(lvalue/xvalue/prvalue)密切相关:
- 若表达式是左值(可取地址的对象),
decltype(表达式)推导为 “左值引用类型”。 - 若表达式是纯右值(临时对象),
decltype(表达式)推导为表达式的类型本身。
最典型的例子是 “变量名加括号” 的情况:
int y = 20;
// 情况1:表达式是变量名(左值,但decltype对变量名特殊处理)
decltype(y) e; // e的类型:int(变量名直接推导为其类型)
// 情况2:表达式是带括号的变量名(视为左值表达式)
decltype((y)) f = y; // f的类型:int&((y)是左值表达式,推导为引用)其他例子:
int arr[5];
decltype(arr) g; // g的类型:int[5](数组名是左值,但变量名直接推导为数组类型)
decltype((arr)) h; // h的类型:int(&)[5]((arr)是左值表达式,推导为数组引用)3. 适用场景
decltype 的核心价值在于保留类型的精确信息,适合以下场景:
-
声明与表达式类型一致的变量(无需初始化):
vector<int> v; decltype(v.begin()) it; // it的类型:vector<int>::iterator(与v.begin()返回类型一致) -
模板中推导复杂类型:
template <typename T, typename U> void func(T t, U u) { decltype(t + u) result; // 推导t+u的类型,无需知道T和U具体是什么 result = t + u; } -
定义函数返回类型(配合尾置返回类型,C++11 起):
// 尾置返回类型:用decltype推导返回类型(依赖参数类型) template <typename T, typename U> auto add(T t, U u) -> decltype(t + u) { return t + u; }
三、decltype(auto):结合 auto 与 decltype 的优势
C++14 引入 decltype(auto),它的行为是:用 auto 的语法(根据初始化推导),但采用 decltype 的推导规则(保留精确类型)。主要用于函数返回类型推导,解决 auto 在转发场景中丢失引用 /const 的问题。
1. 基本用法
-
变量声明:与
decltype类似,但必须初始化(因为带auto)。int x = 10; int& rx = x; decltype(auto) a = x; // a的类型:int(同decltype(x)) decltype(auto) b = rx; // b的类型:int&(同decltype(rx),保留引用) decltype(auto) c = (x); // c的类型:int&(同decltype((x)),左值表达式推导为引用) -
函数返回类型:完美转发返回值的类型(保留引用、const 等)。 例如,实现一个 “转发函数”,返回另一个函数的结果,且保留其类型:
int global = 100; int& get_ref() { return global; } // 返回int& int get_val() { return global; } // 返回int // 用decltype(auto)推导返回类型,保留原函数的返回类型 decltype(auto) forward_ref() { return get_ref(); } // 返回int& decltype(auto) forward_val() { return get_val(); } // 返回int int main() { forward_ref() = 200; // 合法:forward_ref返回int&,可赋值 // forward_val() = 300; // 错误:forward_val返回int(右值),不可赋值 return 0; }
2. 与 auto 的关键区别
auto 推导会忽略引用和顶层 const,而 decltype(auto) 会完整保留:
int x = 5;
const int& rx = x;
auto a = rx; // a的类型:int(忽略引用和const)
decltype(auto) b = rx;// b的类型:const int&(保留引用和const)四、auto、decltype、decltype(auto) 的对比总结
| 特性 | auto | decltype(表达式) | decltype(auto) |
|---|---|---|---|
| 核心功能 | 推导变量类型(忽略顶层 const / 引用) | 推导表达式类型(保留所有类型信息) | 用 auto 语法,按 decltype 规则推导 |
| 是否需要初始化 | 必须(变量初始化) | 可选(可仅声明类型) | 必须(带 auto 特性) |
| 引用 /const 处理 | 忽略顶层,需显式 & 保留 | 完整保留(依赖表达式值类别) | 完整保留(同 decltype) |
| 典型场景 | 简化变量声明(如迭代器、lambda) | 模板类型推导、函数返回类型定义 | 转发函数返回类型(保留原类型) |
| 示例 | auto it = v.begin(); | decltype(v.begin()) it; | decltype(auto) func() { return x; } |
五、使用建议
- 日常变量声明优先用
auto,简化代码(如auto result = compute();)。 - 需保留精确类型(如引用、const、数组)时用
decltype(如模板中定义关联类型)。 - 函数返回值需要 “原样转发” 时用
decltype(auto)(如包装函数、转发器)。 - 避免过度使用自动推导:复杂场景下显式类型更易读(如
int比auto更清晰时)。
类型别名
类型名
✅ 在 C++ 中:
可以直接使用:
node xx;
因为 在 C++ 中,struct 标签(tag)本身就是类型名,不需要加 struct 关键字。
例如:
struct node {
int val;
node* next;
};
node xx; // ✅ 正确,C++ 允许
struct node yy; // ✅ 也正确,但冗余
推荐写法:
node xx;—— 更简洁,是 C++ 风格。
⚠️ 在 C 语言 中:
你必须使用:
struct node xx;
因为在 C 中,struct node 是一个整体类型名,node 本身不是类型,而是结构体的标签(tag)。
struct node {
int val;
struct node* next;
};
struct node xx; // ✅ 正确
node yy; // ❌ 错误!'node' 不是类型
不过,可以用 typedef 来简化:
typedef struct node {
int val;
struct node* next;
} node;
node xx; // ✅ 现在可以这样写了
typedef:类型别名关键字
typedef 是 C 语言引入的关键字,用于为已有类型创建别名,属于编译阶段的语法,会进行类型检查。
- 简化复杂类型
- 为结构体 / 联合体创建别名
- 函数指针别名
// 为 "int (*)(int, int)" 类型创建别名 typedef int (*MathFunc)(int, int); int add(int a, int b) { return a + b; } MathFunc func = add; // 简化函数指针定义
特点:
- 作用域限制:
typedef定义的别名受作用域约束 - 无法与模板结合(C++11 前):不能直接为模板类型创建别名
using:C++11 引入的类型别名(更灵活)
using 在 C++11 中被扩展为类型别名声明,功能上与 typedef 类似,但语法更清晰,且支持模板别名。
-
基本类型别名(与
typedef等效)using IntPtr = int*; IntPtr a, b; // 与 typedef 效果相同,a 和 b 都是 int* using ULL = unsigned long long; ULL num = 1234567890ULL; -
模板别名(
typedef不支持)// 为模板创建别名(C++11 新特性) template <typename T> using Vec = std::vector<T>; // Vec<int> 等价于 std::vector<int> Vec<int> nums = {1, 2, 3}; // 简化模板类型的使用 -
在类或命名空间中定义
namespace MyTypes { using String = std::string; } MyTypes::String str = "hello"; // 作用域内使用
特点:
- 语法更直观:
using 别名 = 原类型的形式比typedef 原类型 别名更易读,尤其是复杂类型// typedef 形式(较难理解) typedef void (*FuncPtr)(int, std::string); // using 形式(更清晰) using FuncPtr = void (*)(int, std::string); - 支持模板别名:可简化模板的使用,这是
typedef无法实现的// 为 map 创建别名,固定 key 类型为 string template <typename Value> using StringMap = std::map<std::string, Value>; StringMap<int> counts; // 等价于 std::map<std::string, int> - 与
auto和decltype结合更自然int x = 5; using XType = decltype(x); // XType 为 int 类型 XType y = 10;
const与类型安全
const 是 C++ 中实现类型安全和代码健壮性的利器。它通过编译时检查,强制执行程序员关于”不可变性”的意图,有效防止了大量因意外修改数据而导致的运行时错误。
- 核心价值: 将安全检查从运行时提前到编译时。
- 主要应用: 变量、指针/引用、函数参数、成员函数、返回值。
- 与类型安全的关系:
const是类型系统的一部分,它扩展了类型信息,使得编译器能够进行更严格的检查,确保程序的行为符合设计预期。
1. const 的核心概念
const (constant) 用于声明不可变性(Immutability)。一旦一个对象或值被声明为 const,任何试图修改它的操作都会在编译时报错,从而在编译时就捕获潜在的错误。
基本用法
const int x = 10; // x 是常量,不能修改
// x = 20; // 编译错误!
int y = 5;
const int* ptr1 = &y; // ptr1 指向一个常量整数 (指针可变,指向的内容不可变)
int* const ptr2 = &y; // ptr2 是一个常量指针 (指针本身不可变,指向的内容可变)
const int* const ptr3 = &y; // 指针和指向的内容都不可变
// ptr1 = &x; // OK: 可以改变 ptr1 指向
// *ptr1 = 100; // 编译错误: 不能通过 ptr1 修改内容
// ptr2 = &x; // 编译错误: ptr2 本身是常量,不能改变指向
*ptr2 = 100; // OK: 可以通过 ptr2 修改内容- 记忆法则: 从右向左读。
const int*→ “指向常量 int 的指针”。int* const→ “指向 int 的常量指针”。
2. const 与函数
const 在函数声明和定义中扮演着至关重要的角色,用于保证函数的不修改性(Non-mutating behavior)。
a. const 成员函数
- 语法: 在成员函数的参数列表后加上
const。 - 含义: 承诺该函数不会修改调用它的对象的任何非
mutable成员变量。 - 作用:
- 类型安全: 编译器会检查函数体内是否有修改成员的代码,有则报错。
- 允许调用:
const对象只能调用const成员函数。 - 重载: 可以基于
const进行函数重载。
b. const 参数
- 按值传递:
void func(const T val)。在函数体内val不可修改。对于大型对象,通常更推荐按 const 引用传递。 - 按引用传递:
void func(const T& ref)。这是最常用、最推荐的方式。它避免了拷贝开销,同时保证函数不会修改传入的对象。
void printVector(const std::vector<int>& vec) { // 高效且安全
for (int n : vec) {
std::cout << n << " ";
}
// vec.push_back(10); // 编译错误! 不能修改
}c. const 返回值
- 返回
const对象/引用:const T& func() const。返回一个不可修改的引用。这可以防止用户对返回值进行赋值操作(虽然很少需要这样做)。
const std::string& getName() const { return name; }
// getName() = "NewName"; // 如果返回 const,这句会编译错误3. const 与指针/引用
这是 const 最容易混淆但也最重要的应用。
| 声明 | 含义 |
|---|---|
const T* ptr 或 T const* ptr | 指向常量的指针。指针可以改变指向,但不能通过指针修改内容。 |
T* const ptr | 常量指针。指针本身不能改变指向,但可以通过指针修改内容。 |
const T* const ptr | 指向常量的常量指针。指针和内容都不可变。 |
const T& ref | 常量引用。不能通过引用修改绑定的对象。这是函数参数的黄金标准。 |
最佳实践: 尽可能使用 const T& 作为函数参数,除非你需要修改它或需要独占所有权(此时考虑移动语义)。
4. const 与类型安全 (Type Safety)
const 是 C++ 类型系统中实现编译时检查的关键机制,极大地增强了类型安全。
a. 防止意外修改 (Accidental Modification)
这是最直接的安全保障。const 将程序员的”意图”(这个对象不应该被修改)明确地告诉编译器,编译器会在编译时强制执行。
void process(const std::vector<int>& data) {
// data.push_back(1); // 编译错误! 明确防止了对输入数据的意外修改
// ... 只读操作 ...
}b. 接口契约 (Interface Contract)
const 成员函数定义了类的只读接口。用户知道调用这个函数是安全的,不会改变对象的状态。这使得代码更可预测、更易推理。
c. 允许对 const 对象进行操作
没有 const 成员函数,const 对象将几乎无法使用(只能调用构造函数和析构函数)。const 使得创建和使用只读对象成为可能。
const std::vector<int> primes = {2, 3, 5, 7, 11};
size_t count = primes.size(); // OK, size() is const
// primes.push_back(13); // 编译错误! primes 是 constd. 与标准库的协同工作
STL 容器和算法大量使用 const。例如:
std::find接受const迭代器。- 容器的
begin()有const重载版本,返回const_iterator。 std::sort要求迭代器指向的元素支持比较,但不会修改元素(除非你传入的比较函数有副作用,但这不推荐)。
const std::vector<int> numbers = {3, 1, 4, 1, 5};
auto it = std::find(numbers.begin(), numbers.end(), 4); // OK
// *it = 10; // 编译错误! it 是 const_iteratore. mutable 关键字 - 有限的例外
有时,一个成员函数逻辑上是”只读”的(不改变对象的逻辑状态),但需要修改某个成员变量(如缓存、调试计数器)。mutable 允许在 const 成员函数中修改被它修饰的成员。
class Counter {
mutable int access_count; // 即使对象是 const,也能修改
int value;
public:
int getValue() const {
++access_count; // OK: mutable
return value;
}
int getAccessCount() const { return access_count; }
};mutable 是类型安全的,因为它不改变对象的可观察状态,只是优化或记录内部信息。
5. const 相关的常见陷阱与最佳实践
陷阱
const正确性: 确保所有本应是const的函数都声明为const。遗漏const会使得const对象无法调用该函数。- 指针的
const: 混淆const T*和T* const。使用清晰的命名或注释。 - 返回
const引用: 通常不需要,除非你想阻止func() = value这种罕见操作。 const与多态:const成员函数可以被const对象调用,但不能被非const对象的const版本覆盖(重载解决)。
最佳实践
- “const 优先”原则: 从设计之初就考虑
const。函数如果不修改对象,就声明为const。 - 函数参数: 优先使用
const T&传递大型对象。对于内置类型(int,double等),按值传递通常更高效。 - 成员函数: 尽可能将不修改对象状态的成员函数声明为
const。 - 变量: 如果一个变量在初始化后不应再修改,用
const声明它。 - 迭代器: 使用
const_iterator遍历const容器。 constexpr(C++11): 对于在编译时就能确定的常量,使用constexpr,它比const更强大(可用于模板参数、数组大小等)。
常量表达式
C++ 中的常量表达式(Constant Expression) 是指在编译期就能确定值的表达式,它允许程序在编译阶段完成计算、内存分配等操作,从而提升运行时效率并增强类型安全性。C++11 引入了 constexpr 关键字来显式声明常量表达式,后续标准(C++14/C++17/C++20)不断扩展其能力,使其成为现代 C++ 的核心特性之一。
一、常量表达式的核心价值
- 编译期计算:将部分运行时的计算提前到编译期完成,减少程序运行时的开销。
- 类型安全:编译期即可可以验证表达式的有效性,避免运行时错误。
- 优化机会:编译器可基于常量表达式进行更深度的优化(如常量折叠、死代码消除)。
- 支持编译期内存分配:如
constexpr数组可在编译期确定大小并分配内存。
二、constexpr 的基本用法
constexpr 可用于修饰变量、函数和构造函数,表明它们的值或返回结果可以在编译期确定。
1. constexpr 变量
用 constexpr 声明的变量必须在编译期初始化,且其值必须是常量表达式。
// 基础类型常量表达式
constexpr int max_size = 1024; // 编译期确定值为1024
constexpr double pi = 3.1415926;
// 表达式初始化(需为编译期可计算)
constexpr int a = 10 + 20; // 合法:10+20是编译期常量
constexpr int b = max_size / 2; // 合法:基于其他constexpr变量
// 错误示例:运行时才能确定的值不能初始化constexpr变量
int x = 5;
// constexpr int c = x; // 编译错误:x是运行时变量与 const 的区别:
const变量仅表示 “只读”,其值可能在运行时初始化(如const int d = rand();,值在运行时确定)。constexpr变量必须在编译期确定值,是 “编译期常量” 的强化版本。
2. constexpr 函数
constexpr 函数是可以在编译期被调用并计算结果的函数。其返回值在编译期调用时是常量表达式,在运行时调用时与普通函数一致。
C++11 中的限制
- 函数体只能有一条
return语句(不允许复杂逻辑)。 - 参数和返回值必须是 “字面类型”(可在编译期构造的类型,如基础类型、数组、某些结构体等)。
// C++11:简单constexpr函数
constexpr int add(int a, int b) {
return a + b; // 仅一条return语句
}
constexpr int sum = add(10, 20); // 编译期调用,sum=30(常量表达式)
int x = 5, y = 6;
int runtime_sum = add(x, y); // 运行时调用,与普通函数一致C++14 及以后的扩展
- 允许函数体包含多条语句(条件分支、循环等)。
- 支持局部变量(但必须是
constexpr或编译期可初始化)。
// C++14:复杂constexpr函数(支持循环和分支)
constexpr int factorial(int n) {
if (n <= 1) return 1;
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int f5 = factorial(5); // 编译期计算:f5=1203. constexpr 构造函数与常量对象
对于自定义类型,constexpr 构造函数允许在编译期创建对象(即 “常量对象”)。
class Point {
private:
int x, y;
public:
// constexpr构造函数(C++11起)
constexpr Point(int x_, int y_) : x(x_), y(y_) {}
// constexpr成员函数(返回成员变量)
constexpr int getX() const { return x; }
constexpr int getY() const { return y; }
};
// 编译期创建Point对象(常量对象)
constexpr Point origin(0, 0);
constexpr int x = origin.getX(); // 编译期获取x值(0)C++20 进一步允许 constexpr 构造函数中包含更复杂的逻辑(如循环、条件判断)。
三、常量表达式的应用场景
-
数组大小定义: 数组大小必须是编译期常量,
constexpr可动态计算大小:constexpr int n = 5; int arr[factorial(n)]; // 数组大小为120(编译期确定) -
模板参数: 模板参数必须是编译期常量,
constexpr函数的返回值可直接作为参数:template <int N> struct Buffer { char data[N]; }; Buffer<add(3, 7)> buf; // 等价于Buffer<10>,编译期确定N=10 -
std::array初始化:std::array的大小需编译期确定,结合constexpr可实现编译期初始化:#include <array> constexpr int size = 4; std::array<int, size> arr = {1, 2, 3, 4}; // 编译期确定大小 -
编译期算法: 复杂逻辑(如排序、查找)可通过
constexpr函数在编译期完成:// 编译期计算斐波那契数列 constexpr int fib(int n) { return (n <= 1) ? n : fib(n-1) + fib(n-2); } constexpr int fib10 = fib(10); // 编译期计算:55
四、常量表达式的限制
-
字面类型要求:参与常量表达式的类型必须是 “字面类型”(Literal Type),即:
- 基础类型(
int、double等)、引用、指针。 - 不含虚函数或虚基类的类,且其所有成员和基类都是字面类型。
- 基础类型(
-
函数副作用:
constexpr函数不能有副作用(如修改全局变量、I/O 操作),因为编译期执行无法产生运行时副作用。 -
动态内存:C++11/14 中
constexpr函数不允许使用动态内存(new/delete),C++20 起允许但限制严格(编译期分配的内存必须在编译期释放)。
五、总结
常量表达式(constexpr)是 C++ 编译期编程的核心工具,它通过以下方式增强程序:
- 性能:将计算从运行时提前到编译期,减少运行开销。
- 安全性:编译期验证表达式有效性,避免运行时错误。
- 灵活性:支持编译期动态计算(如数组大小、模板参数),突破了传统常量的限制。
随着 C++ 标准的演进,constexpr 的能力不断扩展(从简单函数到复杂逻辑),已成为现代 C++ 中编写高效、安全代码的重要手段。
函数
函数重载(Function Overloading)
- 函数重载允许在同一作用域内定义多个同名函数,根据参数列表(参数类型、数量或顺序) 区分。
- 返回类型不同不能作为重载依据(编译器无法仅通过返回类型区分调用)。
默认参数(Default Arguments)
- C++ 允许为函数参数指定默认值,调用时若省略该参数,编译器会自动填入默认值。
- 默认参数必须从右向左定义(若某个参数有默认值,其右侧所有参数都必须有默认值)。
引用参数(Reference Parameters)
C++ 引入引用(&) 作为函数参数,允许函数直接操作实参本身(而非副本),替代 C 语言的指针传递,更安全且语法简洁。
- 避免大对象的拷贝(提升性能)。
- 直接修改实实参(无需指针的
*和&操作)。 - 常量引用:不修改实参,避免拷贝
Lambda 表达式(匿名函数)(C++11)
C++11 引入的lambda 表达式(匿名函数)是一种在代码中就地定义匿名函数的语法,主要用于简化 “临时性功能代码” 的编写,尤其适合作为算法的回调函数或短小的函数对象。它的核心价值是减少代码冗余,让逻辑更紧凑。
一、lambda 表达式的基本语法
lambda 表达式的完整语法结构如下:
[capture-list] (parameter-list) mutable noexcept -> return-type {
// 函数体
}各部分含义:
[capture-list](捕获列表):定义 lambda 外部的变量如何被内部访问(核心特性,后文详解)。(parameter-list)(参数列表):与普通函数的参数列表一致(可省略,若为空)。mutable(可选):允许 lambda 内部修改按值捕获的变量(默认不可修改)。noexcept(可选):声明 lambda 不会抛出异常。-> return-type(返回类型):指定返回类型(可省略,由编译器自动推导)。{函数体}:lambda 的执行逻辑。
二、最简单的 Lambda 表达式
最简化的 lambda 可以省略参数列表、返回类型,仅保留捕获列表(可为空)和函数体:
#include <iostream>
int main() {
// 无参数、无捕获、无返回值的lambda
auto hello = []() {
std::cout << "Hello, Lambda!" << std::endl;
};
hello(); // 调用lambda,输出:Hello, Lambda!
return 0;
}auto用于存储 lambda 表达式(lambda 的类型是编译器生成的匿名类型,无法显式写出)。- 可直接调用,无需命名:
[]() { std::cout << "直接调用"; }();
三、核心部分:捕获列表([capture-list])
捕获列表控制 lambda 如何访问定义它的作用域中的变量,是 lambda 与普通函数的关键区别。常见捕获方式:
| 捕获方式 | 含义 |
|---|---|
[] | 不捕获任何外部变量 |
[var] | 按值捕获变量 var(副本,不可修改) |
[&var] | 按引用捕获变量 var(可修改原变量) |
[=] | 按值捕获所有使用到的外部变量 |
[&] | 按引用捕获所有使用到的外部变量 |
[=, &var] | 默认按值捕获,仅 var 按引用捕获 |
[&, var] | 默认按引用捕获,仅 var 按值捕获 |
[this] | 在类中捕获当前对象(*this) |
- 示例:不同捕获方式的效果
#include <iostream>
int main() {
int a = 10, b = 20;
// 1. 按值捕获a,按引用捕获b
auto func1 = [a, &b]() {
// a = 100; // 错误:按值捕获的变量不可修改(除非加mutable)
b = 200; // 正确:按引用捕获可修改原变量
std::cout << "a=" << a << ", b=" << b << std::endl; // a=10, b=200
};
func1();
std::cout << "外部b=" << b << std::endl; // 外部b被修改为200
// 2. 按值捕获所有变量(a和b),并允许修改副本(mutable)
auto func2 = [=]() mutable {
a = 100; // 允许修改副本(不影响外部a)
b = 200;
std::cout << "内部a=" << a << ", 内部b=" << b << std::endl; // 100, 200
};
func2();
std::cout << "外部a=" << a << ", 外部b=" << b << std::endl; // 10, 200(无变化)
// 3. 按引用捕获所有变量
auto func3 = [&]() {
a = 30;
b = 40;
};
func3();
std::cout << "外部a=" << a << ", 外部b=" << b << std::endl; // 30, 40(被修改)
return 0;
}四、参数列表与返回类型
lambda 的参数列表和返回类型与普通函数类似,但更灵活:
- 参数列表
-
支持普通参数、默认参数(C++14 起)、可变参数等:
// 带参数和默认参数的lambda auto add = [](int x, int y = 5) { return x + y; }; std::cout << add(3); // 3+5=8 std::cout << add(3, 4); // 3+4=7 -
C++14 起支持
auto作为参数类型(泛型 lambda):// 泛型lambda(参数类型自动推导) auto sum = [](auto a, auto b) { return a + b; }; sum(1, 2); // 3(int+int) sum(3.14, 2.7); // 5.84(double+double)
- 返回类型
-
若函数体仅有一条
return语句,返回类型可省略(编译器自动推导):auto multiply = [](int x, int y) { return x * y; // 自动推导返回类型为int }; -
若函数体有复杂逻辑(如分支返回不同类型),需显式指定返回类型:
auto divide = [](double x, double y) -> double { // 显式指定返回double if (y == 0) return 0; return x / y; };
五、lambda 在 STL 中的典型应用
lambda 最常用的场景是作为 STL 算法的回调函数(如排序、遍历、条件判断),替代繁琐的函数对象或全局函数。
- 配合 STL 算法(如
sort、for_each)实现简洁的回调逻辑。
#include <algorithm>
#include <vector>
int main() {
vector<int> nums = {3, 1, 4, 1, 5};
// 用lambda作为排序规则(降序)
sort(nums.begin(), nums.end(),
[](int a, int b) { return a > b; }); // 匿名函数:比较a和b
// 用lambda遍历输出
for_each(nums.begin(), nums.end(),
[](int x) { cout << x << " "; }); // 输出:5 4 3 1 1
return 0;
}六、lambda 的类型与存储
-
lambda 的类型是编译器生成的匿名非 union 类类型(称为 “闭包类型”),因此必须用
auto或模板参数接收。 -
可将 lambda 存储在
std::function(C++11 起)中,实现更灵活的回调管理:#include <functional> int main() { // 存储lambda到std::function std::function<int(int, int)> func = [](int a, int b) { return a + b; }; func(2, 3); // 5 return 0; } -
无捕获的 lambda 可隐式转换为函数指针,有捕获的则不能:
// 无捕获lambda → 函数指针 auto lambda = [](int x) { return x * 2; }; int (*func_ptr)(int) = lambda; // 合法 func_ptr(5); // 10
七、注意事项
-
生命周期问题:按引用捕获的变量必须确保在 lambda 调用时仍有效(避免悬垂引用):
auto get_lambda() { int x = 10; return [&x]() { return x; }; // 危险:x在函数返回后销毁,引用失效 } -
mutable的使用:仅允许修改按值捕获的副本,不影响外部变量:int x = 5; auto func = [x]() mutable { x++; return x; }; func(); // 返回6(副本被修改) std::cout << x; // 仍为5(外部x不变) -
性能:lambda 通常与手写函数效率相同(编译器优化),但复杂捕获可能引入微小开销。
Const 成员函数
在类的成员函数后加 const,表示该函数不会修改类的成员变量,是对成员函数的 “只读” 约束。
- 增强代码可读性(明确函数不修改对象状态)。
- 确保
const对象只能调用const成员函数(安全性)。
其他编程范式
命名空间(namespace)
- C++ 特性:避免全局命名冲突。
- 示例:
namespace MyLib {
int func() { return 42; }
}
int main() {
std::cout << MyLib::func(); // 使用命名空间
}初始化列表表达式(Initializer List)(C++11)
C++11 引入初始化列表({}),作为一种通用的表达式语法,统一了不同场景的初始化方式(如变量、容器、函数参数)。
- 用简洁的
{}语法表示 “值的集合”,使表达式更直观,支持多种类型的初始化。 - 示例:初始化列表表达式的应用
#include <vector>
#include <map>
// 接受初始化列表作为参数的函数
void print(std::initializer_list<int> list) {
for (int x : list) {
std::cout << x << " ";
}
}
int main() {
// 初始化容器
std::vector<int> vec = {1, 2, 3}; // 列表初始化
std::map<int, std::string> map = {{1, "one"}, {2, "two"}};
// 作为函数参数
print({4, 5, 6}); // 输出:4 5 6
// 直接作为表达式使用
auto list = {7, 8, 9}; // list类型为std::initializer_list<int>
return 0;
}nullptr 表达式(C++11)
C++11 引入 nullptr 作为空指针常量,替代 C 中的 NULL(NULL 本质是 0),避免整数与指针的混淆:
void func(int x) {}
void func(void* p) {}
int main() {
func(NULL); // 调用func(int)(C中歧义)
func(nullptr); // 调用func(void*)(明确指向指针重载)
}范围 for 表达式(C++11)
C++11 简化了容器遍历的表达式,直接迭代元素而非通过索引或迭代器:
std::vector<int> nums = {1, 2, 3};
for (int x : nums) { // 范围for,等价于遍历nums的每个元素
std::cout << x;
}类型转换表达式增强
C++ 提供更安全的类型转换表达式(替代 C 的强制转换):
static_cast:编译期类型转换(如int→double);dynamic_cast:运行时多态类型转换(带类型检查);reinterpret_cast:底层二进制转换(如指针→整数),比 C 的强制转换更明确。
结构化绑定(C++17)
std::pair<int, std::string> p = {42, "hello"};
auto [id, name] = p; // 解包
折叠表达式(C++17)
template<typename… Args>
void print(Args… args) {
(std::cout << … << args) << '\n'; // 折叠表达式
}
泛型编程
泛型编程(Generic Programming)是一种独立于具体数据类型的编程范式,核心思想是:编写与类型无关的通用代码,通过 “参数化类型” 实现代码复用,同时保证类型安全和高效性。C++ 是泛型编程的典型实现者,其核心工具是模板(Template),标准模板库(STL)是泛型编程的经典应用。C++ 通过函数模板和类模板实现泛型,模板是 “代码生成器”—— 编译器根据传入的具体类型,自动生成对应版本的函数或类。
C++ 标准模板库(STL)核心组件(容器、算法、迭代器)全基于模板实现:
- 容器(Containers):如
vector<T>、list<T>、map<K, V>,通过类模板支持任意元素类型。 - 算法(Algorithms):如
sort、find,通过函数模板可操作任意容器(依赖迭代器)。 - 迭代器(Iterators):作为容器与算法的 “桥梁”,模拟指针行为,使算法独立于具体容器类型。
Static
在 C++ 中,static 关键字是一个多功能的关键字,用于控制变量、函数和类成员的生命周期、作用域和访问权限。以下是 static 在 C++ 中的详细用法和示例:
一、静态局部变量
在函数内部使用 static 修饰的变量称为静态局部变量。
- 生命周期:从程序开始到结束,变量的值在函数调用之间保留。
- 作用域:仍局限于函数内部。
- 存储位置:静态存储区(全局数据区),而非栈区。
- 初始化:静态局部变量只初始化一次,后续调用函数时不再重新初始化。
- 如果不使用
static,普通局部变量每次函数调用后会被销毁,重新初始化为 0。 - 静态局部变量适用于需要跨函数调用保留状态的场景(如计数器、缓存等)。
二、静态全局变量
在文件作用域(函数外部)使用 static 修饰的全局变量或函数称为静态全局变量和静态函数。
- 作用域:仅限于定义它的文件(内部链接),其他文件无法访问。
- 链接属性:静态全局变量具有内部链接(Internal Linkage),而非外部链接(External Linkage)。
- 目的:避免命名冲突,实现文件级别的封装。
- 如果全局变量不需要跨文件访问,使用
static可以避免命名冲突。 - 静态函数的作用域同样限于定义它的文件。
三、静态成员变量
在类中使用 static 修饰的成员变量称为静态成员变量。
- 归属:属于类本身,而非类的某个对象。
- 共享性:所有对象共享同一个静态成员变量。
- 访问权限:受类的访问修饰符(
public/private)控制。 - 定义与初始化:必须在类外定义并初始化(C++17 之前)。
- 静态成员变量不能直接通过对象访问(除非是
public),推荐通过类名::静态成员变量访问。 - 静态成员变量的初始化必须在类外完成。
四、静态成员函数
在类中使用 static 修饰的成员函数称为静态成员函数。
- 归属:属于类本身,而非类的某个对象。
- 访问权限:受类的访问修饰符控制。
- 限制:
- 不能访问非静态成员变量(因为没有
this指针)。 - 只能访问静态成员变量和其他静态成员函数。
- 不能访问非静态成员变量(因为没有
- 静态成员函数通常用于提供与类相关的工具函数(如数学计算、工厂方法等)。
五、静态初始化顺序问题
当多个翻译单元(源文件)中的静态变量相互依赖时,可能出现静态初始化顺序问题(Static Initialization Order Problem)。
- 使用函数返回静态局部变量(Meyers’ Singleton 模式)
六、总结对比
| 用法 | 作用域 | 生命周期 | 存储位置 | 典型用途 |
|---|---|---|---|---|
| 静态局部变量 | 函数内部 | 程序运行期间 | 静态存储区 | 跨调用保留状态 |
| 静态全局变量 | 定义文件 | 程序运行期间 | 静态存储区 | 文件级封装,避免命名冲突 |
| 静态成员变量 | 类 | 程序运行期间 | 静态存储区 | 类级别的共享数据 |
| 静态成员函数 | 类 | 无(函数代码) | 代码区 | 操作静态成员或类相关功能 |
引用
C++ 中的 引用(Reference) 是一个强大而核心的语言特性,它从 C++ 诞生之初就存在,但在现代 C++(C++11 及以后)中其用法得到了进一步深化和扩展。引用不仅是“别名”,更是实现高效、安全、现代编程范式(如 RAII、移动语义、函数式编程)的基石。
下面我们从 基本概念、语法、用途、与指针的区别、现代用法、常见陷阱 等方面,全面详解 C++ 引用的用法。
一、引用的基本概念
- 引用是某个已存在变量的 别名(alias)。
- 一旦初始化,就绑定到一个对象,不能再绑定到其他对象。
- 操作引用就是操作原对象。
int x = 10;
int& ref = x; // ref 是 x 的引用(别名)
ref = 20; // 等价于 x = 20
std::cout << x; // 输出 20二、引用的语法与类型
1. 左值引用(Lvalue Reference)—— T&
最常见的引用,绑定到具名的、可寻址的对象(左值)。
int a = 5;
int& r1 = a; // OK:绑定左值
// int& r2 = 5; // 错误:不能绑定到右值(字面量)✅ 主要用途
- 函数参数传递(避免拷贝)
- 返回值(返回容器中的元素)
- 范围
for循环
void modify(int& x) {
x = 100; // 直接修改原变量
}
std::vector<int> vec = {1, 2, 3};
for (int& item : vec) {
item *= 2; // 修改原元素
}2. 常量左值引用(const T&)—— 最通用的参数类型
void print(const std::string& str) {
std::cout << str;
}✅ 优势
-
避免拷贝大对象(如
std::string,std::vector)。 -
可以绑定到右值(临时对象):
void func(const int& x); func(42); // OK:临时对象绑定到 const& func(a + b); // OK -
最安全的只读参数传递方式。
✅ 现代 C++ 建议:对于大对象,函数参数优先使用
const T&。
3. 右值引用(Rvalue Reference)—— T&&(C++11 引入)
绑定到临时对象(右值),用于实现 移动语义(Move Semantics) 和 完美转发(Perfect Forwarding)。
std::string create() {
return "Hello"; // 返回临时对象
}
std::string&& rref = create(); // 绑定到临时对象✅ 移动语义示例
class MyString {
char* data;
public:
// 移动构造函数
MyString(MyString&& other) noexcept {
data = other.data; // 窃取资源
other.data = nullptr; // 防止析构时释放
}
};- 避免不必要的深拷贝,极大提升性能。
✅ std::move:将左值转换为右值引用
std::string str = "Hello";
std::string str2 = std::move(str); // str 被“移动”,不再有效
std::move不移动任何东西,只是类型转换(T&→T&&)。
4. 转发引用(Forwarding Reference) / 万能引用(Universal Reference)
由 Scott Meyers 提出,指在模板中使用 T&&:
template<typename T>
void wrapper(T&& arg) {
// arg 是转发引用
real_func(std::forward<T>(arg)); // 完美转发
}- 如果
arg是左值,T推导为Type&,T&&变成Type&(引用折叠)。 - 如果
arg是右值,T推导为Type,T&&是Type&&。
配合 std::forward 实现 完美转发:保持参数的左值/右值属性。
三、引用与指针的区别
| 特性 | 引用(Reference) | 指针(Pointer) |
|---|---|---|
| 初始化 | 必须初始化 | 可不初始化(可为 nullptr) |
| 重新绑定 | 不能,始终绑定原对象 | 可以指向不同对象 |
| 空值 | 不能为 null(但可悬空) | 可为 nullptr |
| 操作符 | . 直接访问 | -> 或 * 解引用 |
| 内存占用 | 通常不额外占用(编译器优化) | 占用指针大小内存 |
| 数组 | 不能有“引用数组” | 可以有指针数组 |
| 语法 | 更简洁,像普通变量 | 需 * 和 & |
✅ 引用更安全、更易用,应优先使用。 ✅ 指针更灵活,用于动态内存、可选值、数组遍历。
四、引用的现代用法与最佳实践
- 函数参数传递策略
| 类型 | 推荐参数类型 |
|---|---|
内置类型(int, double) | T(传值) |
大对象(std::string, std::vector) | const T&(只读)T&(修改) |
| 可移动对象(临时对象) | T&&(移动)或 const T& |
| 需要转移所有权 | std::unique_ptr<T> 或 T&& |
- 返回值优化
// 返回引用:避免拷贝,返回容器元素
int& at(std::vector<int>& vec, size_t i) {
return vec[i];
}
// 返回右值引用:支持移动
std::string create_string() {
return std::string("Hello"); // 自动移动(RVO/NRVO)
}- 范围
for循环
for (const auto& item : container) { ... } // 只读
for (auto& item : container) { ... } // 修改
for (auto&& item : container) { ... } // 万能引用,处理混合类型五、常见陷阱与注意事项
1. 悬空引用(Dangling Reference)
int& bad() {
int x = 10;
return x; // 错误:x 局部变量,函数结束即销毁
} // 返回悬空引用!✅ 永远不要返回局部变量的引用或指针。
2. 引用必须初始化
int& r; // 错误:未初始化
int x = 5;
int& r = x; // OK3. 不能有“引用的引用”
int& & r = x; // 错误(但模板中通过引用折叠可实现)4. const 引用延长临时对象生命周期
const std::string& s = "Hello" + "World"; // 临时 string 对象生命周期延长✅ 这是合法且有用的特性。
六、总结:C++ 引用的核心要点
| 引用类型 | 语法 | 用途 |
|---|---|---|
| 左值引用 | T& | 修改传入对象、返回元素引用 |
| 常量左值引用 | const T& | 只读传参,可绑定右值,最常用 |
| 右值引用 | T&& | 移动语义、接收临时对象 |
| 转发引用 | T&&(模板中) | 完美转发,配合 std::forward |
最终建议
- 函数参数:大对象用
const T&,需要修改用T&,需要移动用T&&。 - 避免返回局部变量的引用。
- 优先使用引用代替指针,除非需要
nullptr或重新绑定。 - 理解
std::move和std::forward的区别:std::move(x):表示“你可以移动x”std::forward<T>(x):表示“如果x原来是右值,就移动它”
- 现代 C++ 中,引用是实现高效、安全代码的基石,尤其是结合移动语义和泛型编程。
🎯 一句话总结: “引用是对象的别名,左值引用用于别名与修改,
const引用用于高效只读,右值引用用于移动和性能优化。”