C++中的引用
2026/7/16 3:21:36 网站建设 项目流程

目录

摘要:

一:概念

二:引用的书写

三:权限问题

四:产生临时变量的场景

1:类型转换

2:函数采用值返回

五:引用的使用场景

1:做函数参数

2:做函数返回值

3:错误使用static的情况

4:函数值返回触发临时变量机制

六:指针 VS 引用


摘要:

本文讲解C++中的引用,从引用的书写格式,到引用涉及到的权限问题,再到如何正确使用引用,最后对比指针和引用.....


一:概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风",但都是指的同一个人!

需要注意的是,引用没有开辟任何的内存空间,这是语法上的描述,而在底层,引用其实和指针一致,都是开辟了空间的,在使用的时候,引用就像一个语法糖包装过的、有着诸多限制的指针。但引用在C++仍有自己不可被指针替代的场景

而至于为什么语法和底层相悖,可以理解为:

语法说“没有空间”,这是一种按约定办事的设计。它给你一个绝对安全、纯粹、无歧义的“别名”抽象,你只能也只需按这个约定来思考代码,对于我们理解来说是轻松的。而底层开辟空间,则是编译器为了在物理机器上实现这个约定而做的“幕后工作”。这个表面矛盾,恰恰是C++在追求“零开销抽象”时的一种精巧平衡。也就是说,为了更安全、更清晰地表达“传递别名”的意图,故意在语义上去掉了这些特性。

例子:验证引用和引用的变量共用同一块内存空间

#include<iostream> 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 = 20,b = 20 cout << "ref的值从" << a << "变成了" << b << endl; // 验证:ref 仍然紧绑 a cout << "但ret的地址从未改变 " << &ref << " vs " << &a; // 地址完全相同,还是一家人 return 0; }

解释:ref是a的引用,再进行ref=b;这行代码是不可能将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,同样是权限放大

但还有些如下情况,比较难以分清!!

1:const 类型的值赋值给普通变量,不属于权限放大

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*330本质上一样:它们都不是已经存在于内存中的、有名字的变量。编译器必须创建一个临时变量来存放结果,而这个临时变量在类型上天然带有 常性(被视作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 绑定到 sum,sum = 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调用无法让x+y的新值赋给sum

可以理解为:static 变量只“出生”一次,但可以“被赋值”无数次;若把sum初始化写在sum定义行,等于只给了sum一次改变的机会;把sum赋值单独拎出来,等于给了它无数次改变的机会。

❌️错误写法:

#include <iostream> using namespace std; int& add(int x, int y) { static int sum = x + y; // ✅ 静态变量,生命周期随程序, ❌️但sum恒为第一次的x+y return sum; // ✅ 安全:返回的是活着的变量 } int main() { int& result = add(3, 4); // result 绑定到 sum,sum = 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
❌ [ 最新修改 ] 暂无
📜 [ 声明 ] 由于笔者水平有限,文中难免有疏漏或不妥之处,还望读者不吝赐教

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询