基础语法注重与 C 不同的特性。包括基础的数据类型、对函数的增强、一些其他特性。

数据类型

强类型

C++ 的 “强类型” 特性体现在对类型的严格区分和对隐式转换的限制上,C 语言允许宽松的隐式转换(弱类型特征)。这种设计的核心目标是在编译期发现类型不匹配的错误,减少运行时异常,提升代码安全性。其核心思路是:除非开发者明确指示(显式转换),否则编译器默认禁止可能不安全的类型混用

强类型语言的核心特征

不同类型之间的操作受到严格限制,类型转换(尤其是隐式转换)需要满足明确的规则,不允许随意的、可能导致歧义的类型混用

C++ 的强类型体现在:

  • 每种类型有明确的语义和操作限制(如 intfloat 不能随意混用);
  • 隐式转换仅在 “安全且无歧义” 的场景下允许(如 intlong);
  • 多数跨类型操作需要显式转换(如 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 语言:允许隐式窄化转换(如 doubleint 可能丢失小数部分):
  • C++ 语言:禁止隐式窄化转换(编译错误):
double d = 3.14;
// int x = d;  // C++编译错误:不允许double隐式转为int(窄化)
int x = static_cast<int>(d);  // 必须显式转换(明确告知编译器接受截断)

注意:列表初始化({})对窄化转换的检查更严格,完全禁止任何可能丢失信息的转换。

类型转换

在 C++ 中,四种强制类型转换(static_castdynamic_castconst_castreinterpret_cast)各有其特定的用途和限制。以下是它们的详细解析:


1. static_cast

  • 作用
    • 编译时类型检查:在编译阶段进行类型合法性验证。
    • 基本类型转换:如 intdoublefloatint
    • 类层次结构中的转换:支持上行转换(派生类 → 基类,安全)和下行转换(基类 → 派生类,需谨慎)。
    • void* 指针转换:将 void* 转换为具体类型的指针。
  • 使用场景
  1. 基本类型转换
  •   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); // 合法,派生类指针转基类指针
    
  • 下行转换(需谨慎)

  1.  Base* b = new Derived();
     Derived* d = static_cast<Derived*>(b); // 合法,但无运行时检查
    
    
    
    
    
    
  • 下行转换风险:若基类指针实际指向非目标类型对象,可能导致未定义行为。
  • 不支持跨继承链转换:如无继承关系的类之间转换会报错。

2. dynamic_cast

  • 作用
    • 运行时类型检查:依赖 RTTI(Run-Time Type Information),仅适用于多态类型(包含虚函数的类)。
    • 安全向下转换:检查转换是否合法,失败时返回 nullptr(指针)或抛出异常(引用)。
  • 使用场景
  1. 多态类型的安全向下转换

  2.  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 限制(需谨慎,可能导致未定义行为)。
  • 使用场景
  1. 修改 const 对象的指针
  •   const int a = 42;
      int* p = const_cast<int*>(&a);
      *p = 43; // 未定义行为(修改 const 对象)
    
  • 调用非 const 成员函数

  1.  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

  • 作用

    • 底层指针/地址转换:将指针类型转换为不相关类型,或指针与整数类型互转。
    • 无类型检查:完全依赖程序员对内存布局的理解,高度危险。
  • 使用场景

  1. 指针类型转换
  •   int a = 10;
      int* p = &a;
      char* c = reinterpret_cast<char*>(p); // 将 int* 转为 char*
    
  • 指针与整数互转

  1.  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*
  1. 优先使用 static_cast:大多数类型转换需求可通过 static_cast 满足。
  2. 多态类型使用 dynamic_cast:需运行时类型检查时(如向下转换)。
  3. 谨慎使用 const_cast:仅在明确知道后果时修改 const 属性。
  4. 避免 reinterpret_cast:除非处理底层硬件或协议,否则尽量避免。

自动类型推导(C++11)

C++11 及后续标准引入了自动类型推导机制,核心工具包括 autodecltype 以及 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):结合 autodecltype 的优势

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)

四、autodecltypedecltype(auto) 的对比总结

特性autodecltype(表达式)decltype(auto)
核心功能推导变量类型(忽略顶层 const / 引用)推导表达式类型(保留所有类型信息)auto 语法,按 decltype 规则推导
是否需要初始化必须(变量初始化)可选(可仅声明类型)必须(带 auto 特性)
引用 /const 处理忽略顶层,需显式 & 保留完整保留(依赖表达式值类别)完整保留(同 decltype
典型场景简化变量声明(如迭代器、lambda)模板类型推导、函数返回类型定义转发函数返回类型(保留原类型)
示例auto it = v.begin();decltype(v.begin()) it;decltype(auto) func() { return x; }

五、使用建议

  1. 日常变量声明优先用 auto,简化代码(如 auto result = compute();)。
  2. 需保留精确类型(如引用、const、数组)时用 decltype(如模板中定义关联类型)。
  3. 函数返回值需要 “原样转发” 时用 decltype(auto)(如包装函数、转发器)。
  4. 避免过度使用自动推导:复杂场景下显式类型更易读(如 intauto 更清晰时)。

类型别名

类型名

✅ 在 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* ptrT 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 是 const

d. 与标准库的协同工作

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_iterator

e. 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 相关的常见陷阱与最佳实践

陷阱

  1. const 正确性: 确保所有本应是 const 的函数都声明为 const。遗漏 const 会使得 const 对象无法调用该函数。
  2. 指针的 const: 混淆 const T*T* const。使用清晰的命名或注释。
  3. 返回 const 引用: 通常不需要,除非你想阻止 func() = value 这种罕见操作。
  4. const 与多态: const 成员函数可以被 const 对象调用,但不能被非 const 对象的 const 版本覆盖(重载解决)。

最佳实践

  1. “const 优先”原则: 从设计之初就考虑 const。函数如果不修改对象,就声明为 const
  2. 函数参数: 优先使用 const T& 传递大型对象。对于内置类型(int, double 等),按值传递通常更高效。
  3. 成员函数: 尽可能将不修改对象状态的成员函数声明为 const
  4. 变量: 如果一个变量在初始化后不应再修改,用 const 声明它。
  5. 迭代器: 使用 const_iterator 遍历 const 容器。
  6. constexpr (C++11): 对于在编译时就能确定的常量,使用 constexpr,它比 const 更强大(可用于模板参数、数组大小等)。

常量表达式

C++ 中的常量表达式(Constant Expression) 是指在编译期就能确定值的表达式,它允许程序在编译阶段完成计算、内存分配等操作,从而提升运行时效率并增强类型安全性。C++11 引入了 constexpr 关键字来显式声明常量表达式,后续标准(C++14/C++17/C++20)不断扩展其能力,使其成为现代 C++ 的核心特性之一。

一、常量表达式的核心价值

  1. 编译期计算:将部分运行时的计算提前到编译期完成,减少程序运行时的开销。
  2. 类型安全:编译期即可可以验证表达式的有效性,避免运行时错误。
  3. 优化机会:编译器可基于常量表达式进行更深度的优化(如常量折叠、死代码消除)。
  4. 支持编译期内存分配:如 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=120

3. 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 构造函数中包含更复杂的逻辑(如循环、条件判断)。

三、常量表达式的应用场景

  1. 数组大小定义: 数组大小必须是编译期常量,constexpr 可动态计算大小:

    constexpr int n = 5;
    int arr[factorial(n)];  // 数组大小为120(编译期确定)
  2. 模板参数: 模板参数必须是编译期常量,constexpr 函数的返回值可直接作为参数:

    template <int N>
    struct Buffer { char data[N]; };
     
    Buffer<add(3, 7)> buf;  // 等价于Buffer<10>,编译期确定N=10
  3. std::array 初始化std::array 的大小需编译期确定,结合 constexpr 可实现编译期初始化:

    #include <array>
    constexpr int size = 4;
    std::array<int, size> arr = {1, 2, 3, 4};  // 编译期确定大小
  4. 编译期算法: 复杂逻辑(如排序、查找)可通过 constexpr 函数在编译期完成:

    // 编译期计算斐波那契数列
    constexpr int fib(int n) {
        return (n <= 1) ? n : fib(n-1) + fib(n-2);
    }
    constexpr int fib10 = fib(10);  // 编译期计算:55

四、常量表达式的限制

  1. 字面类型要求:参与常量表达式的类型必须是 “字面类型”(Literal Type),即:

    • 基础类型(intdouble 等)、引用、指针。
    • 不含虚函数或虚基类的类,且其所有成员和基类都是字面类型。
  2. 函数副作用constexpr 函数不能有副作用(如修改全局变量、I/O 操作),因为编译期执行无法产生运行时副作用。

  3. 动态内存: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 的参数列表和返回类型与普通函数类似,但更灵活:

  1. 参数列表
  • 支持普通参数、默认参数(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)
  1. 返回类型
  • 若函数体仅有一条 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 算法(如 sortfor_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

七、注意事项

  1. 生命周期问题:按引用捕获的变量必须确保在 lambda 调用时仍有效(避免悬垂引用):

    auto get_lambda() {
        int x = 10;
        return [&x]() { return x; };  // 危险:x在函数返回后销毁,引用失效
    }
  2. mutable 的使用:仅允许修改按值捕获的副本,不影响外部变量:

    int x = 5;
    auto func = [x]() mutable { x++; return x; };
    func();  // 返回6(副本被修改)
    std::cout << x;  // 仍为5(外部x不变)
  3. 性能: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 中的 NULLNULL 本质是 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:编译期类型转换(如 intdouble);
  • 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):如 sortfind,通过函数模板可操作任意容器(依赖迭代器)。
  • 迭代器(Iterators):作为容器与算法的 “桥梁”,模拟指针行为,使算法独立于具体容器类型。

Static

在 C++ 中,static 关键字是一个多功能的关键字,用于控制变量、函数和类成员的生命周期作用域访问权限。以下是 static 在 C++ 中的详细用法和示例:


一、静态局部变量

在函数内部使用 static 修饰的变量称为静态局部变量

  1. 生命周期:从程序开始到结束,变量的值在函数调用之间保留。
  2. 作用域:仍局限于函数内部。
  3. 存储位置:静态存储区(全局数据区),而非栈区。
  4. 初始化:静态局部变量只初始化一次,后续调用函数时不再重新初始化。
  • 如果不使用 static,普通局部变量每次函数调用后会被销毁,重新初始化为 0。
  • 静态局部变量适用于需要跨函数调用保留状态的场景(如计数器、缓存等)。

二、静态全局变量

在文件作用域(函数外部)使用 static 修饰的全局变量或函数称为静态全局变量静态函数

  1. 作用域:仅限于定义它的文件(内部链接),其他文件无法访问。
  2. 链接属性:静态全局变量具有内部链接(Internal Linkage),而非外部链接(External Linkage)。
  3. 目的:避免命名冲突,实现文件级别的封装。
  • 如果全局变量不需要跨文件访问,使用 static 可以避免命名冲突。
  • 静态函数的作用域同样限于定义它的文件。

三、静态成员变量

在类中使用 static 修饰的成员变量称为静态成员变量

  1. 归属:属于类本身,而非类的某个对象。
  2. 共享性:所有对象共享同一个静态成员变量。
  3. 访问权限:受类的访问修饰符(public/private)控制。
  4. 定义与初始化:必须在类外定义并初始化(C++17 之前)。
  • 静态成员变量不能直接通过对象访问(除非是 public),推荐通过 类名::静态成员变量 访问。
  • 静态成员变量的初始化必须在类外完成。

四、静态成员函数

在类中使用 static 修饰的成员函数称为静态成员函数

  1. 归属:属于类本身,而非类的某个对象。
  2. 访问权限:受类的访问修饰符控制。
  3. 限制
    • 不能访问非静态成员变量(因为没有 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 推导为 TypeT&&Type&&

配合 std::forward 实现 完美转发:保持参数的左值/右值属性。


三、引用与指针的区别

特性引用(Reference)指针(Pointer)
初始化必须初始化可不初始化(可为 nullptr
重新绑定不能,始终绑定原对象可以指向不同对象
空值不能为 null(但可悬空)可为 nullptr
操作符. 直接访问->* 解引用
内存占用通常不额外占用(编译器优化)占用指针大小内存
数组不能有“引用数组”可以有指针数组
语法更简洁,像普通变量*&

引用更安全、更易用,应优先使用。 ✅ 指针更灵活,用于动态内存、可选值、数组遍历。


四、引用的现代用法与最佳实践

  1. 函数参数传递策略
类型推荐参数类型
内置类型(int, doubleT(传值)
大对象(std::string, std::vectorconst T&(只读)
T&(修改)
可移动对象(临时对象)T&&(移动)或 const T&
需要转移所有权std::unique_ptr<T>T&&
  1. 返回值优化
// 返回引用:避免拷贝,返回容器元素
int& at(std::vector<int>& vec, size_t i) {
    return vec[i];
}
 
// 返回右值引用:支持移动
std::string create_string() {
    return std::string("Hello");  // 自动移动(RVO/NRVO)
}
  1. 范围 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; // OK

3. 不能有“引用的引用”

int& & r = x; // 错误(但模板中通过引用折叠可实现)

4. const 引用延长临时对象生命周期

const std::string& s = "Hello" + "World";  // 临时 string 对象生命周期延长

✅ 这是合法且有用的特性。


六、总结:C++ 引用的核心要点

引用类型语法用途
左值引用T&修改传入对象、返回元素引用
常量左值引用const T&只读传参,可绑定右值,最常用
右值引用T&&移动语义、接收临时对象
转发引用T&&(模板中)完美转发,配合 std::forward

最终建议

  1. 函数参数:大对象用 const T&,需要修改用 T&,需要移动用 T&&
  2. 避免返回局部变量的引用
  3. 优先使用引用代替指针,除非需要 nullptr 或重新绑定。
  4. 理解 std::movestd::forward 的区别
    • std::move(x):表示“你可以移动 x
    • std::forward<T>(x):表示“如果 x 原来是右值,就移动它”
  5. 现代 C++ 中,引用是实现高效、安全代码的基石,尤其是结合移动语义和泛型编程。

🎯 一句话总结: “引用是对象的别名,左值引用用于别名与修改,const 引用用于高效只读,右值引用用于移动和性能优化。”