左值堪为赋右值莫强寻。引用本非新同体共浮沉。引用的概念和定义引用不是新定义一个变量而是给已存在变量取别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一处内存空间如何使用引用呢在C语言中按位与符号相同的符号在C中就变为了引用符类型 引用别名引用对象int a 10; int b a; // b 是 a 的别名a和b的地址相同如果使用b则会导致a11b11也可以给a同时起多个别名也可再给b起别名int a 10; int b a; // b 是 a 的别名 int x 20; //引用不会改变指向 b x;//不是让 b 变成 x 的引用而是把 x 的值赋给 b 所引用的对象也就是 a。 intb x;//×错误用法重定义不可再将x取别名引用一旦绑定就不能重新绑定到另一个对象不能通过再次写 int b x; 来改变 b 的引用目标如果你想让 x 也有一个引用别名需要换一个名字引用与指针的对比解释例如int*q和intq的区别int*代表q是一个指针类型则表示指针q将会去引用另一个指针引用和指针是 C 中两种重要的间接访问方式它们在很多方面有相似之处但在定义、初始化、重绑定等关键行为上存在明显差异。下面通过表格进行对比。对比项引用指针定义给已存在变量取别名不新开辟内存存储变量地址的变量本身占用内存初始化定义时必须初始化可以先定义后赋值重绑定一旦绑定一个实体不能再绑定其他实体可以随时改变指向重新绑定其他对象空值不能为空必须引用一个有效对象可以为空nullptr表示不指向任何对象内存占用不额外占用内存与引用对象共用空间占用独立的内存空间存放地址使用场景函数参数传递、返回值优化避免拷贝动态内存管理、数据结构链表、树、可选对象下面分别给出引用和指针的简单代码示例。引用示例int a 10; int b a; // b 是 a 的引用别名 b 20; // 修改 b 即修改 aa 变为 20指针示例int a 10; int* p a; // p 指向 a 的地址 *p 20; // 通过指针修改 aa 变为 20 p nullptr; // 指针可以重新指向空值引用的特性和作用引用在定义时必须初始化一个变量可以有多个引用一个引用可以继续有引用引用一旦引用一个实体再不能引用其他实体可以对任何类型做引用 [如变量、指针…]引用在定义时必须初始化若是定义了一个引用类型的变量int那么就必须要去对其进行一个初始化指定一个其引用的对象否则就会报错int a; int b a; int c;前两行合法第三行非法。int a;定义了一个int变量a。如果是局部变量a未初始化值不确定如果是全局/静态变量会被零初始化。int b a;合法。b是a的引用也就是a的别名。对b的操作就是对a的操作。int c;非法。引用必须在定义时初始化不能先声明一个引用之后再让它引用某个对象。编译器通常会报错reference variable c requires an initializer或declaration of reference variable c requires an initializer注意引用一旦初始化后不能重新绑定到另一个对象。如果你需要“先不指向任何对象之后再指向某个对象”应该用指针int* c nullptr; c a;一个变量可以有多个引用int a 10; int b a; int c a; // 合法c 也是 a 的引用 int d b; // 合法d 仍然引用 a引用本身通常理解为别名不拥有对象。原对象销毁后引用会悬空继续使用是未定义行为。一个引用可以继续有引用类似于传递性。当一个变量引用了另一个变量之后其他变量还可以再对其进行一个引用。通过查看地址就可以看出它们也都是属于同一块空间int a 10; int b a; int c b;引用一旦初始化绑定到某个实体就不能再重新绑定到另一个实体int a 1; int b 2; int r a; // r 绑定到 a r b; // 注意这不是让 r 改绑到 b // 而是等价于 a b;也就是说对引用赋值改的是被引用对象的值不是引用本身的绑定关系。如果想要修改指向的对象只能使用指针不能使用引用int* p a; p b; // 可以改指向注意混淆点int a 1; int r1 a; int r2 r1; // r2 也是 a 的引用不是“r1 的引用”可以对任何类型做引用 [如变量、指针…]int a 10; int r1 a; // 引用 int 变量 int* p a; int* r2 p; // 引用指针变量r2 是 p 的别名 r2 nullptr; // 等价于 p nullptr int arr[3] {1, 2, 3}; int (r3)[3] arr; // 引用数组 void func() {} void (r4)() func; // 引用函数 struct S { int x; }; S s; S r5 s; // 引用类对象注意但不能引用 void、位域也不能直接定义引用的引用、引用的指针、引用数组不能引用 voidvoid r; // 错误不能引用位域struct B { int x : 3; }; B b; int r b.x; // 错误位域没有独立地址不能直接声明“引用的引用”int r; // 错误不能有“引用的指针”int* p; // 错误不能有“引用的数组”int arr[3]; // 错误但可以有“数组的引用”int (arr)[3]解释arr 先和 结合所以 arr 是引用。[3] 说明被引用的是一个长度为 3 的数组。int 说明数组元素是 int。运用前必须初始化int a[3] {1, 2, 3}; int (arr)[3] a; // arr 是 a 的别名注意int arr[3]; // 非法不能定义“引用数组”左值引用和右值引用规则不同int r1 10; // 错误非 const 左值引用不能绑右值 const int r2 10; // 正确const 左值引用可以绑右值 int r3 10; // 正确右值引用使用引用的两大场景引用在实践中主要有这些用途和特点引用传参、引用返回可以减少拷贝、提高效率对引用的修改会直接作用到被引用的原对象。引用传参 vs 指针传参两者功能类似都能修改实参但引用写法更简洁自然不用取地址和解引用通常更方便。引用 vs 指针两者功能有重叠但各有特点不能互相完全替代实践中常配合使用。C 引用 vs Java 引用两者差别很大。除了用法不同最关键的是C 引用一旦绑定某个对象就不能再改指向Java 引用可以重新指向别的对象。所以 C 引用更像“别名”不是可重新赋值的指针。用引用替代指针传参主要目的是简化代码避开指针的复杂写法让程序更易读、易用。补充注意引用做返回值时要确保返回的对象生命周期足够长不能返回局部变量否则会产生悬空引用。一作为参数以交换两数的数值为例子指针版本void swap1(int* px, int* py) { int t *px; *px *py; *py t; } swap1(a, b);C引用版本因为rx引用了xry引用了y所以它们是等价的在函数内部使用临时变量对二者进行交换就可以带动外界的变化#includeiostream using namespace std; void Swap(int rx, int ry) { int tmp rx; rx ry; ry tmp; } Swap(x,y);以改变单链表的头结点为例题在先前发布过的文章中单链表的函数调用会出现一级指针和二级指针难以记忆故改用引用效果显著指针版#include cstdio struct ListNode { int val; ListNode* next; }; using PNode ListNode*; void ListPushBack(PNode* phead, int x) { PNode newnode new ListNode; newnode-val x; newnode-next nullptr; if (*phead nullptr) { *phead newnode; } else { PNode tail *phead; while (tail-next ! nullptr) { tail tail-next; } tail-next newnode; } } int main() { PNode plist nullptr; ListPushBack(plist, 1); ListPushBack(plist, 2); ListPushBack(plist, 3); for (PNode cur plist; cur ! nullptr; cur cur-next) { printf(%d - , cur-val); } printf(nullptr\n); return 0; }引用版此时PushFront()内部我们也可以去做一个修改直接使用形参SList即可无需考虑到要对二级指针进行解引用变为一级指针#include cstdio #include cstdlib struct ListNode { int val; ListNode* next; }; using PNode ListNode*; //void ListPushBack(LTNode** phead, int x) //void ListPushBack(LTNode* phead, int x) void ListPushBack(PNode phead, int x) { PNode newnode new ListNode; newnode-val x; newnode-next nullptr; if (phead nullptr) { phead newnode; } else { PNode tail phead; while (tail-next ! nullptr) { tail tail-next; } tail-next newnode; } } int main() { PNode plist nullptr; ListPushBack(plist, 1); ListPushBack(plist, 2); ListPushBack(plist, 3); for (PNode cur plist; cur ! nullptr; cur cur-next) { printf(%d - , cur-val); } printf(nullptr\n); return 0; }个人拙见引用可以类比看作一级指针用到二级指针时则要降为一级指针的引用二作为返回值栈区与静态区的对比int Count() { int n 0; n; return n; }int Count() { static int n 0; n; return n; }普通局部变量放在当前函数的栈帧里也就是内存的栈区。函数调用结束栈帧销毁变量随之消失下次调用重新创建值不会保留。静态局部变量不放在函数栈帧里而是放在静态存储区和全局变量、静态全局变量同一区域。它只在第一次执行到声明处时初始化一次之后一直存在直到整个程序结束才释放。函数返回不会销毁它所以下次调用还能保留上次的值。static只改变局部变量的存储位置和生命周期不改变作用域静态局部变量依然只能在函数内访问外面看不到。全局变量也存放在静态区生命周期同样贯穿整个程序但作用域通常更广具体取决于static/extern。内存分区可粗略分为栈区、堆区、静态区、常量区、代码区。栈区由编译器自动分配释放静态区在程序运行期间一直存在。补充C11 起局部静态变量的初始化是线程安全的。普通局部变量是“随函数生随函数死”静态局部变量是“函数内可见程序级存活”。注意定义变量、调用函数、用 malloc/new 申请堆内存时本质上是系统或运行时借给你一块空间的使用权。只要它还在生命周期内这块空间就受保护别的代码不能随便访问。但“销毁”不等于“空间消失”栈上的局部变量函数结束后栈帧销毁变量生命周期结束堆内存 free/delete 后只是把使用权还给分配器或系统空间本身通常还在内容也不一定被清零。之后这块空间可能被重新分配出去给同一进程的其他代码甚至别的进程。此时再用旧指针、旧地址去访问就是悬垂指针野指针、释放后使用、非法访问。它为什么不一定报错因为编译器检查是静态的、抽查式的不是全程监控运行时也未必检测分配器可能还没复用这块内存旧数据还在于是看起来“正常”。数组越界、野指针也一样不一定立刻崩。但别被“没报错”骗了这属于未定义行为结果随机——可能没事可能崩溃可能数据错乱也可能被安全漏洞利用。正确做法释放后置空指针不访问已释放内存不重复释放必要时用 ASan、Valgrind 等工具检测。释放只是交还使用权不是抹掉空间再访问就是赌本质是未定义行为。函数栈帧销毁后普通函数存放在函数栈帧中的变量存放在自动生成的临时变量中暂存其中再返回静态函数变量编译器不管是否是静态变量仍旧将这个静态变量存放在临时变量中然后再把临时变量拷贝到接收值中静态局部变量虽然生命周期到程序结束函数栈帧销毁后它还在但只要函数返回类型是 int 这种“按值返回”返回的仍然是它的值副本不是这个变量本身注对 int、指针这类标量常见 ABI 直接用寄存器返回静态变量在函数返回后仍然存在这只说明返回它的指针或引用是安全的如果想直接给外界同一个变量必须返回 int* 或 int优化返回引用——通过引用外放避免值拷贝让调用方直接访问静态对象。如果想要省去拷贝值这一步骤就要使用引用返回或者传指针返回值返回return n; 会把 n 的值拿出来构造一个返回值副本/放进寄存器再交给调用方引用返回return n; 返回的是 n 的别名/地址不会拷贝 n 本身注意不仅要求返回值是拷贝值更要求调用时的接收int f() { static int n 0; return n; }int a f(); // a 仍然是副本这里会拷贝 n 的值 int r f(); // r 直接绑定到 n不拷贝 int* p f(); // p 指向 n不拷贝int Count() { static int n 0; n; return n; } int main() { int tmp Count(); int p Count(); p; cout tmp;//是拷贝副本仍为1 cout p;//是引用在p后变为3 return 0; }int Count() { static int n 0; n; return n; } int main() { //如果将tmp置于p后面此时tmp拷贝n的副本变为3 int p Count(); p; int tmp Count(); cout tmp; cout p; return 0; }注意引用返回的前提是被引用对象的生命周期必须长于函数调用。通常用于静态变量、全局变量、动态分配对象或调用方传入的对象。引用返回的危害悬垂引用——栈帧销毁后的幽灵访问int Count() { int n 0;//修改此处不再是static n; return n; } int main() { int p Count(); p; int tmp Count(); cout tmp; cout p; return 0; }这段代码发生了什么第一次Count()栈上创建n地址假设为An 1返回A的引用函数返回栈帧销毁n生命周期结束p绑定到地址A但A上的对象已经无效。p通过悬垂引用写地址A这是未定义行为可能把原来n的值从1改成2也可能破坏别的数据。第二次Count()再次调用函数栈帧很可能复用同一块地址A新n又放在An 0然后n变成1返回A的引用tmp是int所以从A读取值拷贝给tmp。输出tmp可能输出1p仍绑定到A而A现在是第二次Count()的n所以p也可能输出1但这一切没有任何保证编译器、优化等级、栈布局一变结果就完全不同甚至崩溃。总结若返回的是局部变量的引用函数返回后栈帧销毁局部变量生命周期结束引用就变成悬垂引用空间通常还在但使用权已归还就像退房后房间还在、钥匙还在可你再进去就是非法访问。此时通过该引用读写属于未定义行为可能碰巧输出正常也可能崩溃或读到后续栈帧的数据。正确使用引用返回的前提是对象生命周期足够长如 static 变量、全局变量、动态分配对象或调用方传入的对象否则宁可返回值也不要返回局部变量的引用。函数返回引用生命周期决定合法性——返回栈变量是悬垂引用返回静态/全局变量可作左值/右值int add(int a, int b) { int c; c a b; return c; } int main() { int p add(1, 2); cout p; return 0; }这一段代码会被报错问题int p add(1, 2); 为什么错以及如果改成引用返回会怎样这里 add 的返回类型是 int不是 int所以 add(1, 2) 返回的是一个临时的 int 值也就是右值而 前置自增要求操作数必须是可修改的左值不能对临时值、字面量、右值做 如果尝试改为引用返回呢int add(int a, int b) { int c; c a b; return c; // 错误返回局部变量引用 }这时语法上 add(1, 2) 是左值add(1, 2) 能编译通过但是c 是局部变量函数返回后栈帧销毁c 生命周期结束返回的引用变成悬垂引用。这个错误操作在上面有所提及总结若返回静态变量或全局变量既可作为右值使用也可作为左值使用可以成为其他引用的初始值。当作右值时返回该静态变量或全局变量的值。当作左值时相当于就是返回的该静态变量或全局变量。//作为左值时 #include iostream using namespace std; int getNum() { static int n 0; return n; } int main() { // 1. 当作右值读取值 cout getNum() endl; // 输出 0 // 2. 当作左值直接赋值 getNum() 10; cout getNum() endl; // 输出 10 // 3. 当作左值自增 getNum(); cout getNum() endl; // 输出 11 getNum(); cout getNum() endl; // 输出 12 // 4. 当作左值复合赋值 getNum() 5; cout getNum() endl; // 输出 17 // 5. 当作左值绑定到其他引用 int r getNum(); r 100; cout getNum() endl; // 输出 100 // 6. 当作左值取地址 int* p getNum(); *p 200; cout getNum() endl; // 输出 200 return 0; }左值的特点有明确存储位置可以出现在赋值号左边可以被修改可以取地址可以绑定到引用。常引用权限放大 —— 用普通引用绑定 const 变量导致可通过引用修改只读对象。int a 0; int b a; // 合法普通变量 → 普通引用 const int c 1; int d c; // 错误const 变量 → 普通引用权限放大c 是只读的d 是普通引用可修改。如果允许 d 绑定 c那么就能通过 d 修改 c因此会报错如果允许 int d c;那么d 和 c 就绑定到同一块内存d没有 const 属性可以通过 d 修改这块内存等于绕过了 c 的只读限制这不是“歧义”而是直接违反 const 规则编译器不允许权限平移————权限一个级别既无放大也无缩小int a 1; int b a; // 非 const → 非 const const int c 2; const int d c; // const → const权限缩小————本来可读可写现在只让读权限变小安全。int a 1; const int r a; // 合法非 const → const常引用的作用保护数据不被修改可以绑定右值和临时变量并延长其生命周期const int r 10; // 合法 const int r2 a b; // 合法 int c 11;//普通引用不可以绑定右值不合法常用于函数参数;void func(const string s);//既能避免拷贝大对象又能防止修改传入的字符串。既避免拷贝又防止修改。临时变量的暂住证const 引用如何延长它的生命周期double a 3.14; int b a; // 错误 const int b a; // 正确为什么 int b a会报错a 是 doubleb 是 int类型不匹配要把 double 转成 int会产生一个临时变量int 类型非 const 左值引用不能绑定右值或者临时变量。为什么 const int b a正确a 先隐式转换成 int产生临时变量const 左值引用可以绑定右值/临时变量并且会延长临时变量的生命周期让它活到 b 离开作用域这里b引用的是临时变量临时变量具有常性不能直接引用 否则就是放大了权限加上const才能保证其权限不变注意此时的b不再是a的引用是对临时变量的引用可看作是临时变量的别名在类中普通引用对内存的占用情况类中的普通引用成员会占用内存大小等同指针且必须初始化、不可重新绑定。引用的成员必须在构造函数初始化列表中初始化类中的引用成员没有默认值也不能在构造函数体内赋值class Teacher { public: int a; int b; Teacher(int x, int y) : a(x), b(y) {} // 正确初始化列表 };类比结构体多了一些特殊规则尤其是默认构造函数、拷贝赋值、移动语义都会受影响。#include iostream using namespace std; class Teacher1 { public: int a; int b; }; class Teacher2 { public: int x; int y; int a; int b; }; int main() { printf(%d\n, sizeof(Teacher1)); // 1664位 printf(%d\n, sizeof(Teacher2)); // 2464位 return 0; }类中的引用成员底层通常实现为常量指针T* const它必须持久保存“绑定到哪个对象”所以不能像局部引用那样被优化掉。64 位每个引用 8 字节32 位每个引用 4 字节参与内存对齐计算。