C++引用概念及用法全解

C++引用概念及用法全解

一、什么是引用?

引用是C++中的一种语法特性,可以理解为变量的别名。引用本身不占用独立的内存空间,它和被引用的变量共享同一块内存。

引用的基本用法

1

2

3

4

5

6

7

8

9

10

11

12

intmain() {

inta = 10;

int& ra = a;// ra是a的别名

a += 10;

cout << ra << endl;// 输出20

ra += 10;

cout << a << endl;// 输出30

return0;

}

关键点:对引用的操作就是对原变量的操作,两者完全等价。

二、引用的三大特点

1. 定义引用时必须初始化

1

2

int& ra;// ❌ 错误:引用必须初始化

int& ra = a;// ✅ 正确

原因:引用是变量的别名,别名必须依附于某个已存在的变量,不能独立存在。

2. 不存在空引用

1

2

int& ra = NULL;// ❌ 错误:不能给引用赋空值

int* p = NULL;// ✅ 指针可以为空

原因:引用必须绑定到一个有效的变量,不能指向空地址。指针是独立的变量,可以存储空地址,但引用不是变量,它只是别名。

3. 引用一旦绑定,不能改变指向

1

2

3

inta = 10, b = 20;

int& ra = a;

ra = b;// 这不是让ra指向b,而是把b的值赋给a(即a = 20)

原因:引用在初始化时绑定了目标变量,之后所有对引用的操作都直接作用于该变量,无法重新绑定到其他变量。

对比指针

1

2

3

4

inta = 10, b = 20;

int* p = &a;// 指针可以改变指向

p = &b;// 现在p指向b

*p = 100;// 修改b的值为100

三、引用作为函数参数

引用的本质:引用是变量的别名,不是副本。对引用的操作就是对原变量的操作。

基于这个特性,引用最常用的场景是作为函数参数,主要解决两个问题:

1. 在函数内部修改外部变量

C语言中,函数参数默认是传值:形参是实参的副本,函数内部修改形参,外面的实参不受影响。

1

2

3

4

5

6

7

8

9

10

11

12

// C语言:传值,交换失败

voidSwap(inta,intb) {

inttmp = a;

a = b;

b = tmp;

}

intmain() {

intx = 10, y = 20;

Swap(x, y);

// x和y还是10和20,没变

}

想要修改实参,必须传指针

1

2

3

4

5

6

// C语言:传指针,交换成功

voidSwap(int* a,int* b) {

inttmp = *a;

*a = *b;

*b = tmp;

}

C++的引用可以做到同样的事,语法更简洁:

1

2

3

4

5

6

7

8

9

10

11

12

// C++:传引用,交换成功

voidSwap(int& a,int& b) {

inttmp = a;

a = b;

b = tmp;

}

intmain() {

intx = 10, y = 20;

Swap(x, y);// 调用时和传值一样,但实参被改了

cout << x <<" "<< y << endl;// 输出20 10

}

优点:调用时不用取地址,函数内部不用解引用,代码更清晰。

2. 避免拷贝,提高效率

当参数是大型对象(如结构体、类)时,传值会拷贝整个对象,开销很大。传引用只传递地址(4/8字节),效率更高。

1

2

3

4

5

// 传值:拷贝整个Student对象(几十字节)

voidPrintStudent(Student s) { ... }

// 传引用:只传递地址(4/8字节),不拷贝

voidPrintStudent(constStudent& s) { ... }

注意:对于intchar这类小型数据,传值反而更快,因为直接复制到寄存器,不需要间接访问。所以小型数据用传值,大型对象用const引用

3. 三种参数传递方式对比

方式本质能否修改实参效率(小型数据)效率(大型对象)
传值副本❌ 不能
传指针地址✅ 能
传引用别名✅ 能

四、普通引用 vs const引用

1. 普通引用(可读可写)

1

2

3

4

inta = 10;

int& ra = a;// 普通引用

ra = 100;// ✅ 可以修改

cout << ra;// ✅ 可以读取

2. const引用(只读)

1

2

3

4

inta = 10;

constint& cra = a;// 常引用,只读

cout << cra;// ✅ 可以读取

cra = 100;// ❌ 错误:不能通过常引用修改

3. 权限规则:看原变量的权限

引用的权限不能超过原变量。

  • 原变量是普通变量(可读可写)→ 引用可以是普通引用,也可以是常引用
  • 原变量是常量(只读)→ 引用只能是常引用

1

2

3

4

5

6

7

8

inta = 10;// 普通变量:可读可写

constintb = 20;// 常量:只读

int& ra = a;// ✅ 普通引用引用普通变量

constint& cra = a;// ✅ 常引用引用普通变量

int& rb = b;// ❌ 错误:普通引用不能引用常量(权限超了)

constint& crb = b;// ✅ 常引用引用常量

一句话:变量是什么权限,引用就不能超过这个权限。

五、const引用的万能特性

const引用可以引用任何东西:普通变量、常变量、字面常量,甚至临时对象。

1. 引用普通变量

1

2

inta = 10;

constint& cra = a;// ✅

2. 引用常变量

1

2

constintb = 20;

constint& crb = b;// ✅

3. 引用字面常量

1

constint& crx = 30;// ✅ 可以!

背后原理

const引用绑定到字面常量时,编译器会生成一个临时变量来保存这个常量,然后让引用指向这个临时变量。

1

2

3

// 编译器实际做的事:

inttmp = 30;

constint& crx = tmp;

注意:引用本身不占内存,但被引用的对象(临时变量)需要占内存。这个临时变量由编译器自动创建和管理,它的生命周期和引用一致。

4. 引用表达式结果

1

2

inta = 10, b = 20;

constint& cr = a + b;// ✅ 表达式结果是临时变量,const引用可以绑定

六、引用的常见误区

误区1:引用可以重新绑定

1

2

3

4

inta = 10, b = 20;

int& ra = a;// ra是a的别名

ra = b;// ❌ 不是让ra指向b,而是把b的值赋给a(a变成20)

误区2:引用占用空间

引用本身不占用独立内存,它只是原变量的别名。sizeof引用的结果是原变量的大小。

1

2

3

inta = 10;

int& ra = a;

cout <<sizeof(ra);// 输出4(int的大小),不是指针的大小

原因sizeof作用于引用时,返回的是被引用对象的大小。

误区3:引用可以指向引用

1

2

3

inta = 10;

int& ra = a;

int&& rra = ra;// ❌ 错误:不存在"引用的引用"

原因:引用是别名,不是对象。不能给别名再起别名吗?语法上不允许。

七、不能返回局部变量的引用或地址

先看一个错误示例

1

2

3

4

5

6

7

8

9

Student& func() {

Student sa{"S001","Alice","F", 20 };

returnsa;// 返回局部变量的引用

}

intmain() {

Student& p = func();

printf("%s", p.s_name);// 可能崩溃,可能乱码,可能碰巧正确

}

为什么错了?

困惑1:返回地址本身有什么错?

返回地址本身没错。return &sa返回的是一个数字(内存地址),这个操作是合法的。错的是函数结束后你还去用这个地址访问内存

困惑2:为什么函数结束后就不能用了?

函数调用时,系统会在栈上给局部变量分配空间。函数结束后,这块空间被系统回收,标记为“可重用”。但回收不是清零,这块内存还在,只是不再属于你了。

下次调用其他函数时,系统可能会把这块空间分配给别的变量,里面的内容随时可能被覆盖。所以你去访问它,结果是不可预测的。

困惑3:返回值和返回引用有什么区别?

1

2

3

4

5

6

7

8

9

10

11

// 返回引用:返回地址,危险

Student& func1() {

Student sa{ ... };

returnsa;// 返回地址,sa销毁后地址失效 ❌

}

// 返回值:返回副本,安全

Student func2() {

Student sa{ ... };

returnsa;// 返回副本,sa销毁不影响副本 ✅

}

区别

  • 返回引用:把原件的地址告诉别人,原件没了,地址就成了空地址
  • 返回值:复印一份交给别人,原件没了,复印件还在

困惑4:返回局部变量的地址,和返回局部变量本身,有什么区别?

看返回类型:

  • 返回类型是Student&Student*:返回的是地址(危险)
  • 返回类型是Student:返回的是副本(安全)

1

2

3

Student& func1() {returnsa; }// 返回类型是引用 → 地址

Student* func2() {return&sa; }// 返回类型是指针 → 地址

Student func3() {returnsa; }// 返回类型是对象 → 副本

类比理解

你在酒店开房:

  • sa就是你住的房间
  • 返回引用或地址:你把房间号告诉别人
  • 退房(函数结束):房间被回收,不再属于你
  • 别人拿着房间号回去找:房间可能住了别人,可能在打扫,可能已经被改成仓库
  • 去用这个房间号 → 结果不可预测

返回值:你退房时把房间里的东西复印一份带走,原来的房间怎么变都跟你无关。

正确的做法

1. 返回值(拷贝)

1

2

3

4

Student func() {

Student sa{ ... };

returnsa;// 返回副本

}

2. 返回静态局部变量

1

2

3

4

5

6

7

Student& func() {

staticStudent sa{ ... };// 静态变量,生命周期贯穿整个程序

returnsa;

}Student& func() {

staticStudent sa{ ... };// 静态变量,生命周期贯穿整个程序

returnsa;

}

3. 在堆上分配

1

2

3

4

Student* func() {

Student* p =newStudent{ ... };// 堆上分配,手动管理

returnp;

}

一句话总结

返回局部变量的地址本身没错,错的是函数结束后你还去用这个地址。因为这块内存已经被系统回收,不再属于你了。

八、能不能定义引用数组?

结论:不能定义“元素为引用的数组”,但可以定义“数组的引用”。

1. 不能定义元素为引用的数组

1

int& rbr[5];// ❌ 错误:不能定义引用数组

原因:数组是一段连续的内存空间,每个元素都需要占用内存。引用本身不占内存,它只是别名,无法作为数组元素存在。

2. 可以定义数组的引用

1

2

intar[5] = {1,2,3,4,5};

int(&rar)[5] = ar;// ✅ 正确:rar是数组ar的引用

含义rar是整个数组ar的别名,不是元素为引用的数组。

3. 对比

写法含义是否正确
int& rbr[5]元素为引用的数组❌ 错误
int(&rar)[5]数组的引用✅ 正确

记忆技巧:括号位置决定一切。&靠近数组名,表示引用整个数组;&靠近类型,表示元素是引用。

九、数组名退化问题

1. 数组名退化的规则

数组名在三种情况下不会退化,保持为整个数组:

  • sizeof(ar)
  • &ar(取数组地址)
  • 引用数组int(&rar)[n] = ar

其他情况数组名退化为数组首元素的地址。

1

2

3

4

5

6

7

intar[5] = {1,2,3,4,5};

cout <<sizeof(ar);// 20(5*4,未退化)

int(*p)[5] = &ar;// 取数组地址,未退化

int(&rar)[5] = ar;// 引用数组,未退化

int* q = ar;// 退化:ar变成int*

2. 数组名作为函数参数:退化问题

C++中,数组名作为参数传递给函数时,会退化为指针,丢失数组大小信息。

1

2

3

4

voidfuna(intbr[5]) {

cout <<sizeof(br);// 输出8(指针大小),不是20

// 函数内部不知道数组有多大,只能通过额外参数传入长度

}

如果希望函数内部能知道数组的大小,可以用数组的引用作为参数:

1

2

3

4

voidfunb(int(&rar)[5]) {

cout <<sizeof(rar);// 输出20,保留了数组大小

// 函数内部可以直接用sizeof获取数组长度

}

关键点:用数组的引用作为参数,可以保留数组的大小信息,避免退化。

十、引用与指针的区别

1. 语法规则上的区别

对比项引用指针
本质变量的别名存储变量的地址
内存分配不分配独立内存分配4/8字节内存
使用方式直接使用需要解引用(*)
能否改变指向不能
必须初始化
是否可为空不能可以(NULL/nullptr)
sizeof结果原变量的大小指针本身的大小(4/8)
多级无(无引用的引用)有(二级、n级指针)
++操作改变原变量的值改变指向的地址

1

2

3

4

5

6

7

// 演示++操作的区别

intar[5] = {10,20,30,40,50};

int& ra = ar[0];

int* p = &ar[0];

++ra;// ar[0]变成11

++p;// p指向ar[1]

2. 汇编层面的本质

从汇编角度看,指针和引用没有区别,都是地址。

1

2

3

4

inta = 10;

int& ra = a;

int* p = &a;

// 底层实现:ra和p都是存储a的地址

引用在底层也是用指针实现的,只是编译器做了语法糖,让使用更方便、更安全。

3. 一句话总结

维度引用指针
语法层面别名,更安全地址,更灵活
汇编层面都是地址都是地址

使用建议:能用引用就用引用,需要改变指向或可为空时再用指针。

总结

知识点核心要点
引用本质变量的别名,不占独立内存
三大特点必须初始化、不能为空、不能改指向
const引用只读,可引用普通变量/常变量/字面常量
权限规则引用权限不能超过原变量
const引用万能可绑定字面常量和临时对象,编译器自动生成临时变量
引用作为参数可修改外部变量、避免大对象拷贝
返回局部变量不能返回局部变量的引用或地址,函数结束后内存被回收
数组的引用可定义数组引用int(&rar)[5],不能定义引用数组int& rbr[5]
数组名退化sizeof/&/数组引用 三种情况不退化,函数参数会退化
引用 vs 指针引用更安全(不能为空、不能改指向),指针更灵活

到此这篇关于C++引用全解的文章就介绍到这了