
目录摘要一概念二引用的书写三权限问题四产生临时变量的场景1类型转换2函数采用值返回五引用的使用场景1做函数参数2做函数返回值3错误使用static的情况4函数值返回触发临时变量机制六指针 VS 引用摘要本文讲解C中的引用从引用的书写格式到引用涉及到的权限问题再到如何正确使用引用最后对比指针和引用.....一概念引用不是新定义一个变量而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。比如李逵在家称为铁牛江湖上人称黑旋风但都是指的同一个人需要注意的是引用没有开辟任何的内存空间这是语法上的描述而在底层引用其实和指针一致都是开辟了空间的在使用的时候引用就像一个语法糖包装过的、有着诸多限制的指针。但引用在C仍有自己不可被指针替代的场景而至于为什么语法和底层相悖可以理解为语法说“没有空间”这是一种按约定办事的设计。它给你一个绝对安全、纯粹、无歧义的“别名”抽象你只能也只需按这个约定来思考代码对于我们理解来说是轻松的。而底层开辟空间则是编译器为了在物理机器上实现这个约定而做的“幕后工作”。这个表面矛盾恰恰是C在追求“零开销抽象”时的一种精巧平衡。也就是说为了更安全、更清晰地表达“传递别名”的意图故意在语义上去掉了这些特性。例子验证引用和引用的变量共用同一块内存空间#includeiostream using namespace std; int main() { int a 10; int ra a;//定义引用类型 printf(%p\n, a); printf(%p\n, ra); return 0; }二引用的书写引用书写时有三条准则a.引用在定义时必须初始化b.一个变量可以有多个引用c.引用一旦引用一个实体再不能引用其他实体a任何的引用一定是定义时就初始化的就像黑旋风这个别名被别人创造出来的时候就是李逵的别名二者从一开始就绑定不可能凭空出现黑旋风这个外号但没有匹配的人同理引用定义的时候就要被变量初始化否则后序给该引用赋值两个不同的值编译器不知道该选哪个b李逵不止黑旋风还有铁牛这个别名所以一个变量也可以有两个不同名的引用c黑旋风一开始就是李逵的别名不会变成其他人的别名任何变量的引用都必须从始至终引用该变量代码举例a. 引用在定义时必须初始化int main() { int a 10; int ref a; // 正确诞生即绑定 int ref2; // 错误引用必须初始化就像不能先起个空的外号再找人 return 0; }b. 一个变量可以有多个引用int main() { int a 10; int ref1 a; // 别名1黑旋风 int ref2 a; // 别名2铁牛 int ref3 a; // 别名3李铁牛 // 对任一别名操作都等同于操作 a 本身 ref1 20; cout ref2; // 输出20铁牛也看到了变化 return 0; }解释同一个变量的多个引用都是共用该变量的空间所以对其中一个引用ref1赋值其余引用和变量本身都会被修改c. 引用一旦引用一个实体再不能引用其他实体int main() { int a 10; int b 20; int ref a; // 黑旋风 李逵 ref b; // 这一行不是让 ref 变成 b 的引用 // 而是把 b 的值赋给 ref 引用的实体 (也就是a) // 结果a 20b 20 cout ref的值从 a 变成了 b endl; // 验证ref 仍然紧绑 a cout 但ret的地址从未改变 ref vs a; // 地址完全相同还是一家人 return 0; }解释ref是a的引用再进行refb这行代码是不可能将ref改成b的引用的只能是把b的值赋给了ref让a这个变量和他的引用的值都被修改了而已介绍完引用的书写规则之后我们就可以解释某些数据结构教材上的代码其编写者三权限问题权限的规则可平移可缩小不可放大被const 修饰的变量权限低于普通变量因为前者只可读后者可读可写所以引用一个普通变量则你定义引用类型时可以被const修饰(缩小)也可以没有const修饰(平移)反之引用一个const修饰的变量你定义引用类型时只能是没有const修饰的引用(平移)例子1权限平移int main() { // 普通变量 → 普通引用权限不变 int a 10; int ref a; // 可读可写 → 可读可写权限一致 // const 变量 → const 引用权限不变 const int b 20; const int ref2 b; // 只读 → 只读权限一致 return 0; }例子2权限缩小int main() { int a 10; // 权限缩小普通变量 → const 引用 const int ref a; // 我有读写权但我自愿只读着用安全 a 20; // 可以通过原变量修改 // ref 30; // 错误通过这个引用我只能读不能写 return 0; }例子3权限放大(❌️)int main() { const int b 20; const int cref b;// ✅️ // 权限放大const 变量 → 普通引用编译器直接报错 int ref b; // ❌ 错误b 是只读的不能给一个可读可写的引用 // 同样const 引用 转 普通引用也不行 int ref2 cref; // ❌ 错误cref 只有只读权限不能放大传递 return 0; }解释不仅不可以用int类型的引用去引用一个const int。此外也不能把一个const int 类型的引用变成int同样是权限放大但还有些如下情况比较难以分清1const 类型的值赋值给普通变量不属于权限放大int main() { const int a 10; // a 只读 int rra a; // rra 是一个全新的的变量 其只是接收了a的赋值 得到了a的值 仅此而已 return 0; }解释a是一个const int类型的值将a赋给了rra不存在权限放大rra是全新的整形变量其的值接收了a的赋值了而已2常性的临时变量赋给const引用属于权限平移int main() { int a 10; // 假设 a 是一个普通变量 // 1. ✅ 允许const引用绑定到字面量常量 const int ra 30; // 解析30 是字面量编译器会创建一个常性的临时变量来存它。 // ra 是这个常性临时变量的引用权限平移常性 ↔ 常性。 // 2. ✅ 允许const引用绑定到表达式结果 const int rb a * 3; // 解析a*3 的结果是一个常性的临时变量。 // rb 是这个常性临时变量的引用权限平移。 // 3. ❌ 禁止普通引用绑定到表达式结果 // int rc a * 3; // 错误a*3 的结果是常性临时变量试图用普通可读写引用去引用它权限放大 // 4. ❌ 禁止普通引用绑定到字面量 // int rd 30; // 错误30 对应的常性临时变量同样不能绑定到普通引用权限放大 }解释①a*3和30本质上一样它们都不是已经存在于内存中的、有名字的变量。编译器必须创建一个临时变量来存放结果而这个临时变量在类型上天然带有 常性(被视作const修饰的只读空间)②权限不可放大试图用一个普通引用可读可写权限去操作一个常性临时变量只读权限就是权限方法编译器必定报错。③反之const int的引用权限是只读的。用只读的别名去关联只读的临时空间是完全匹配的权限平移因此被允许。这条规则也直接导致了常量引用const T能够延长临时变量生命周期这一重要特性。注其中的“常量引用const T能够延长临时变量生命周期”指的是普通的 30 这个临时变量在所处行代码一结束生命就结束了而你给它起了个别名ref那编译器认为你确实想长期用它。于是规定这个临时对象不能死要活到ref这个引用结束为止。四产生临时变量的场景1类型转换类型转换时会产生临时变量int main() { double d 12.34; // ❌ 错误权限放大 int rd d; return 0; }解释①警告不是因为类型不匹配因为即使类型不同但double和int都是可读可写应当权限平移不会报错②报错是因为从double到int这是类型转换所以编译器用一个int类型的临时变量来存储d的整形部分然后再把临时变量赋给引用rd重点在于临时变量具有常性也就是只可读不可写把临时变量赋给一个int类型的引用则权限放大// 1. 因为类型不匹配double vs int编译器必须创建一个临时变量 const int temp (int)d; // temp 12且 temp 具有常性 // 2. 然后尝试将 rd 绑定到这个临时变量 int rd temp; // ❌ 这里出错了③总结临时变量temp是常性的只读引用rd是普通int可读写试图用可读写的引用去操作只读的临时变量 权限放大✅️所以解决办法就是用const修饰rd从而和临时变量形成权限平移int main() { double d 12.34; // ✅ 正确权限平移 const int rd d; return 0; }2函数采用值返回函数采用值返回时会产生临时变量#include iostream int add(int x, int y) { int sum x y; // sum 是局部变量 return sum; // ① 这里会把 sum 的值拷贝到一个临时变量中 } // ② sum 随函数结束而销毁 int main() { int result add(3, 4); // ③ 临时变量再拷贝给 result // 或者直接用 std::cout add(5, 6) std::endl; // 临时变量直接参与表达式 return 0; }解释①函数采用值返回时会产生临时变量当函数返回的值是栈上开辟的则会随着栈帧的销毁而销毁所以编译器为了确保能把函数返回值给返回出来会把sum的值拷贝到一个具有常性的临时变量中让用户得到这个临时变量用户常常创建变量来接受这个临时变量②注意临时变量的值赋给int类型的变量不是权限放大在上文第三大点第1小点中解释过这只是一次简单的赋值所以如果我们把 int result add(3, 4); 变成int result add(3, 4);则必定报错一个int类型的引用想要去引用一个临时变量权限放大这也从侧面验证了函数采用值返回时会产生临时变量所以导致二者权限不等#include iostream int add(int x, int y) { int sum x y; // sum 是局部变量 return sum; // ① 这里会把 sum 的值拷贝到一个临时变量中 } // ② sum 随函数结束而销毁 int main() { int result add(3, 4); // ③ 临时变量再拷贝给 result // 或者直接用 std::cout add(5, 6) std::endl; // 临时变量直接参与表达式 return 0; }此外我们让add(3, 4);直接add(3, 4)也可以验证函数采用值返回时会产生临时变量#include iostream int add(int x, int y) { int sum x y; // sum 是局部变量 return sum; // ① 这里会把 sum 的值拷贝到一个临时变量中 } // ② sum 随函数结束而销毁 int main() { add(3, 4); std::cout add(5, 6) std::endl; return 0; }解释add(3, 4)返回的是临时变量临时变量具有常性当然不可以所以必然报错注意①这个函数采用值返回时会产生临时变量的设计是合理的因为函数中的变量会随函数调用完成后随着栈帧一起销毁所以为了精确返回函数返回值编译器采取了此种措施并且可以拓展的一点是只要函数采用值返回时不管这个返回的值是否会随栈帧销毁而销毁编译器都会为了确保正确放回从而产生临时变量②也就是说如果返回的值是static修饰的生命周期虽程序但若函数采用值返回时仍会产生临时变量③所以函数返回时出了函数作用域如果返回对象还已经还给系统了则必须使用传值返回因为编译器会采取临时变量的措施让其正确返回五引用的使用场景引用在日常使用中最常用的场景是1做函数参数2做函数的返回值1做函数参数#include iostream #include string using namespace std; // ❌ 值传递会发生拷贝效率低 void printByValue(string s) { std::cout s std::endl; } // ✅ 引用传递不拷贝直接操作原对象 void printByRef(const string s) { std::cout s std::endl; } int main() { string longStr 这是一个很长的字符串...; printByRef(longStr); // 高效无拷贝 return 0; }解释①引用做参数是因为可以省去不必要的拷贝当实参是一个巨长的链表此时形参采用值接收则会拷贝生成一份巨长的链表这无疑是浪费空间的②其次即使你采用指针接收链表但指针也是占据空间的而引用在语法上是不需要开辟任何空间的所以其在这些地方时优于指针的③引用做函数参数在C中99%场景下会和const结合使用 代表不会对接收到的参数本身进行修改既高效又安全2做函数返回值引用作函数返回值其原因也是想省去不必要的拷贝因为函数采用值返回则一定会触发临时变量的拷贝所以引用作返回值就是想省去这一步但是需要注意了会引入新的问题比如返回变量会随栈帧销毁而销毁此时你还引用返回其实就是造成了类似野指针的问题错误使用引用作函数返回值的例子#include iostream using namespace std; int add(int x, int y) { int sum x y;// sum 是局部变量 return sum; } int main() { int result add(3, 4); cout result endl; return 0; }解释①打印结果是对的我们函数采用引用返回也避免了产生临时变量所带来的拷贝消耗看似是对的但实则已经犯了大错②因为sum是栈上的局部变量其会随栈帧销毁而销毁你result引用的实体早已销毁这类似于野指针错误而打印出7完全是因为那部分空间存储的7未被清除什么时候清理取决于编译器不同编译器结果可能不同比如我们在之后再调用一次add函数对5和5相加此时result会从7变成10#include iostream using namespace std; int add(int x, int y) { int sum x y;// sum 是局部变量 return sum; } int main() { int result add(3, 4); add(5, 5); cout result endl; }解释①之前打印7是因为编译器还没去清除栈帧内的变量而再次调用add产生新的栈帧就会覆盖原有的空间导致result的空间内容被修改所以这就是错误使用引用作为函数返回值的后果②当然这里其实也证明了栈的空间会被不同的函数栈帧重复使用两个相同的add函数前后调用大概率使用的是同一块栈的空间作为栈帧导致两次存放sum的空间是一致的我们也可以通过操作让这个代码打印不出7从而证明随着栈帧销毁sum也不复存在#include iostream using namespace std; int add(int x, int y) { int sum x y; // sum 是局部变量存储在栈上 return sum; // ❌ 危险返回局部变量的引用 } // 新增一个函数用来“污染”之前 add 函数占用的栈空间 void overwriteStack() { int arr[10] { 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 }; // 随意填充数据 for (int i 0; i 10; i) { cout arr[i] ; // 防止编译器优化掉 arr } cout endl; } int main() { int result add(3, 4); // result 指向一块已释放的栈内存值应该是7 overwriteStack(); // 这块栈内存现在被新的函数调用覆盖了 cout result endl; // 输出垃圾值大概率不再是 7 return 0; }解释result变成了9而9是一个不相干的数组的元素值所以也证明result引用的空间早已销毁被新的内容所填充所以为了避免错误的使用引用作为函数的返回值我们需要确保让返回值不会随着栈帧销毁而销毁如果该返回的变量本身就是堆上的那我们无需操作而如果是栈上的像上文那样则我们需要使用static修饰该变量让栈上变量变成静态存储区上的变量(生命周期延长)✅️正确使用引用作为返回值的代码#include iostream using namespace std; int add(int x, int y) { static int sum; // ✅ 静态变量生命周期随程序 sum x y; // 每次调用都覆盖之前的值 return sum; // ✅ 安全返回的是活着的变量 } int main() { int result add(3, 4); // result 绑定到 sumsum 7 add(5, 5); // sum 被覆盖为 10 cout result endl; // ✅️ 输出 10不是 7 return 0; }解释result引用的变量一直存在所以这里不会存在类似野指针的问题而打印结果为10也是正确的因为sum从7变成了10此时我们再用之前全为9的数组来验证一下如果result结果还为7则代表result引用的空间一直都存在#include iostream using namespace std; int add(int x, int y) { static int sum; sum x y; return sum; } // 新增一个函数用来“污染”之前 add 函数占用的栈空间 void overwriteStack() { int arr[10] { 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 }; // 随意填充数据 for (int i 0; i 10; i) { cout arr[i] ; // 防止编译器优化掉 arr } cout endl; } int main() { int result add(3, 4); // result 指向一块已释放的栈内存值应该是7 overwriteStack(); // 这块栈内存现在被新的函数调用覆盖了 但sum变量仍存在 生命周期已被static延长 cout result endl; // result引用空间 仍存在 为7 return 0; }解释证明了result引用的空间仍存在3错误使用static的情况需要注意static所处的那一行只在第一次执行到它时生效一次后面编译器都会跳过改行因之后程序的整个生命周期内static所处行修饰的变量都一直存在这也就意味着如果你把sum的赋值也写在了static修饰sum的那一行那sum只会被赋值一次后序的add调用无法让xy的新值赋给sum可以理解为static 变量只“出生”一次但可以“被赋值”无数次若把sum初始化写在sum定义行等于只给了sum一次改变的机会把sum赋值单独拎出来等于给了它无数次改变的机会。❌️错误写法#include iostream using namespace std; int add(int x, int y) { static int sum x y; // ✅ 静态变量生命周期随程序, ❌️但sum恒为第一次的xy return sum; // ✅ 安全返回的是活着的变量 } int main() { int result add(3, 4); // result 绑定到 sumsum 7 add(5, 5); // sum 被覆盖为 10 cout result endl; // ❌️ 输出 7不是 10 return 0; }解释①即使正确的使用引用作为函数的返回值但是把赋值写在了static所处行导致sum只会被赋值一次后序add(5, 5); 结果10也给不到static②把赋值和static定义写在一行的情况也有就是不再对static修饰的值进行更改的时候所以不能说哪种写法对只能根据要求而定4函数值返回触发临时变量机制总结一张表说清函数采取值返回触发临时变量机制的所有情况返回对象生命周期函数返回类型是否产生临时变量典型示例短局部变量值返回int是必须的安全需要return localVar;短局部变量引用返回int是灾难返回悬空引用return localVar;❌长全局/静态值返回int是语法规定拷贝值return global;长全局/静态引用返回int否直接返回别名return global;解释①不管你返回变量是否随栈帧销毁而销毁只要采取值返回编译器为了安全起见就会触发临时变量机制100%确保你的值被返回尽管变量是全局的静态的也会如此②若采取引用返回此时的确不会生成临时变量但是如果返回变量会销毁则悬空引用类似空指针的错误只有返回变量是全局或静态再搭配上引用返回才是正确的六指针 VS 引用我们在最开始说过引用语法上没有开辟空间但底层上和指针是一致的也是开辟了空间的引用和指针的汇编代码对比完全一致指针和引用的区别面试常问题①. 引用概念上定义一个变量的别名指针存储一个变量地址。②. 引用在定义时必须初始化指针没有要求③. 引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何一个同类型实体④. 没有NULL引用但有NULL指针⑤. 在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32位平台下占4个字节)⑥. 引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小⑦. 有多级指针但是没有多级引用⑧. 访问实体方式不同指针需要显式解引用引用编译器自己处理⑨. 引用比指针使用起来相对更安全在后续的C学习中引用的作用是指针替代不了的无论是拷贝构造还是任何类中成员函数使用引用作为参数引用都是更优秀的 [ 作者 ] shylyly [ 首次发布 ] 2026.7.14❌ [ 最新修改 ] 暂无 [ 声明 ] 由于笔者水平有限文中难免有疏漏或不妥之处还望读者不吝赐教