目录
一、引用的概念
二、引用的特性
1、定义时必须初始化
2、一个变量可以有多个引用
3、引用一旦绑定实体就不能更改
三、const引用(常引用)
1、const引用的基本特性
2、临时对象与const引用
3、临时对象的特性
4、const 引用作为函数形参
1. 基本示例(类型不匹配时创建临时对象)
2. 数值类型转换(临时对象存储转换后的值)
3. 如果形参是 非 const 引用,则不允许绑定临时对象
4. 类对象的隐式转换(临时对象 + const 引用)
总结
四、引用的使用场景
1、引用作为函数参数
引用传参的工程价值
2、引用作为函数返回值
返回引用最佳实践
注意:
五、引用与指针的区别(超重要!!!)
引用与指针的工程选择准则
C++引用与其他语言引用的本质区别
关键差异点
六、引用的优点
七、数据结构中的引用实践
1、C风格二级指针实现解析
关键点解析
调用方式
2、C++引用风格实现解析
关键改进
调用方式
3、两种实现的底层对比
内存布局示例
八、现代C++中的引用演进(后面会学到,现在先了解)
1、右值引用(C++11引入)
2、完美转发
3、结构化绑定(C++17)
九、引用使用的注意事项
1、避免悬垂引用
2、接口设计原则
3、多线程环境
一、引用的概念
引用(Reference)是C++中一种重要的复合类型,它不是定义一个新的变量,而是为已存在的变量提供一个别名。编译器不会为引用变量单独分配内存空间,引用变量与其引用的实体共享同一块内存空间。
C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的>>和<<,这里引用也和取地址使用了同⼀个符号&,大家注意使用方法角度区分就可以。(吐槽一下,这个问题其实挺坑的,个人觉得用更多符号反而更好,不容易混淆)
基本语法形式:
类型& 引用变量名 = 引用实体;
重要说明:引用类型必须与引用实体是同种类型。
示例代码:
#include <iostream>
using namespace std;int main() {int a = 10;int& b = a; // 给变量a取一个别名bcout << "a = " << a << endl; // 输出10cout << "b = " << b << endl; // 输出10b = 20; // 通过引用修改变量值cout << "a = " << a << endl; // 输出20cout << "b = " << b << endl; // 输出20return 0;
}
二、引用的特性
1、定义时必须初始化
-
引用必须在声明时进行初始化,不能先声明后赋值
-
正确示例:
int a = 10; int& b = a; // 正确:定义时初始化
-
错误示例:
int c = 10; int &d; // 错误:未初始化 d = c;
2、一个变量可以有多个引用
int a = 10;
int& b = a;
int& c = a;
int& d = a;
此时,b、c、d都是变量a的别名。
3、引用一旦绑定实体就不能更改
-
引用在初始化后不能改为指向其他实体
-
示例:
int a = 10; int& b = a; int c = 20; b = c; // 这不是改变引用指向,而是将a的值改为20
三、const引用(常引用)
上面提到,引用类型必须和引用实体是同种类型的。但是仅仅是同种类型,还不能保证能够引用成功,我们若用一个普通引用类型去引用其对应的类型,但该类型被const所修饰,那么引用将不会成功。
常引用(const reference)用于引用常量或临时对象,具有以下特点:
-
可以引用常量
-
可以延长临时对象的生命周期
-
不能通过常引用修改被引用的对象
示例:
int main() {const int a = 10;// int& ra = a; // 错误:不能用普通引用引用常量const int& ra = a; // 正确// int& b = 10; // 错误:不能引用字面常量const int& b = 10; // 正确double pi = 3.14159;// int& rpi = pi; // 错误:类型不匹配const int& rpi = pi; // 正确:会发生隐式转换return 0;
}
我们可以将被const修饰了的类型理解为安全的类型,因为其不能被修改。我们若将一个安全的类型交给一个不安全的类型(可被修改),那么将不会成功。
1、const引用的基本特性
const引用是C++中一种特殊的引用类型,具有以下重要特性:
-
引用const对象:必须使用const引用来引用const对象
const int a = 10; const int& ra = a; // 正确 int& rb = a; // 错误:不能使用非const引用引用const对象,这是权限的放大
-
引用普通对象:const引用可以引用普通对象,这是权限的缩小
int b = 20; const int& rb = b; // 正确:权限缩小
-
权限规则:
-
权限可以缩小(从可修改到只读)
-
但不能放大(从只读到可修改)
-
2、临时对象与const引用
C++中有几种常见情况会产生临时对象(以下各点都同理):临时对象具有常性!!!(重要!!!)
-
表达式结果:
int a = 5; const int& rb = a * 3; // 正确:a*3的结果存储在临时对象中 int& rc = a * 3; // 错误:临时对象具有常性
-
类型转换:
double d = 12.34; const int& rd = d; // 正确:类型转换产生临时int对象 int& re = d; // 错误:临时对象具有常性
3、临时对象的特性
临时对象(temporary object)是编译器在需要暂存表达式求值结果时自动创建的未命名对象,具有以下特点:
-
常性:临时对象默认具有const属性(常性)
int a = 5; const int& r1 = a * 2; // 正确:临时对象具有常性,可以用 const 引用绑定 int& r2 = a * 2; // 错误:临时对象是 const 的,不能绑定非 const 引用
-
生命周期:临时对象通常会在表达式结束时销毁,但如果绑定到
const
引用,其生命周期会延长至该引用的作用域结束。#include <iostream>int main() {// 临时int直接使用 - 表达式结束就"消失"std::cout << "临时int值: " << 42 << std::endl;// 绑定到const引用 - 生命周期延长const int& ref = 123; // 临时123会一直存在直到main结束std::cout << "通过引用访问: " << ref << std::endl;return 0; }
说明:第一个42是纯临时值,用完即"消失";第二个123因为绑定到const引用ref,所以会一直存在直到main函数结束
-
隐式创建:编译器自动生成临时对象的情况:(超重要!!!)
-
表达式求值结果
int x = 10, y = 20; const int& sum = x + y; // x + y 的结果存储在临时对象中
当表达式的结果需要存储时,编译器会生成临时对象。
-
类型转换中间结果
double d = 3.14; const int& intVal = d; // 生成临时 int 对象存储截断后的值(3)
当隐式类型转换发生时,编译器会生成临时对象存储转换后的值。
-
函数返回值(未使用移动语义时)
std::string createString() {return "Temporary"; // 返回临时对象 }int main() {const std::string& s = createString(); // 临时对象的生命周期延长std::cout << s << std::endl; // 正确:"Temporary"return 0; }
当函数返回一个临时对象时,如果没有优化(如 RVO/NRVO),编译器会生成临时对象。
-
4、const
引用作为函数形参
当 const
引用作为函数形参 时,如果传入的实参类型不匹配(但可以隐式转换),编译器会自动创建临时对象来存储转换后的值,并让 const
引用绑定到这个临时对象。(学到后面再回看)
1. 基本示例(类型不匹配时创建临时对象)
void print(const std::string& str) {std::cout << str << std::endl;
}int main() {print("Hello"); // "Hello" 是 const char[6],编译器生成临时 std::string 对象return 0;
}
发生了什么?
-
"Hello"
的类型是const char[6]
,而print
的参数是const std::string&
。 -
编译器隐式调用
std::string
的构造函数,生成一个临时std::string
对象。 -
const std::string& str
绑定到这个临时对象,临时对象的生命周期延长至print
函数结束。
2. 数值类型转换(临时对象存储转换后的值)
void printInt(const int& num) {std::cout << num << std::endl;
}int main() {double d = 3.14;printInt(d); // 生成临时 int 对象存储截断后的值(3)return 0;
}
发生了什么?
-
d
是double
类型,而printInt
的参数是const int&
。 -
编译器生成一个临时
int
对象,存储d
截断后的值(3
)。 -
const int& num
绑定到这个临时int
对象。
3. 如果形参是 非 const 引用
,则不允许绑定临时对象
void modify(int& num) { // 非 const 引用num = 100;
}int main() {modify(42); // 错误!临时对象不能绑定到非 const 引用return 0;
}
为什么不行?
-
生成临时对象,但是它不能绑定到非const引用中。
-
临时对象是
const
的,不能通过非 const 引用
修改。 -
C++ 禁止这种行为,避免逻辑错误(修改一个即将销毁的临时对象没有意义)。
4. 类对象的隐式转换(临时对象 + const
引用)
class MyString {
public:MyString(const char* s) { std::cout << "构造临时 MyString\n"; }
};void printStr(const MyString& s) {std::cout << "使用 MyString\n";
}int main() {printStr("Hello"); // 生成临时 MyString 对象return 0;
}
输出:
说明:
-
"Hello"
触发MyString
的构造函数,生成临时对象。 -
const MyString& s
绑定到这个临时对象,生命周期延长至printStr
结束。
总结
情况 | 是否生成临时对象? | 是否合法? |
---|---|---|
const T& 形参 + 可隐式转换的实参 | ✔️ 生成 | ✔️ 合法 |
const T& 形参 + 完全匹配的实参 | ❌ 不生成 | ✔️ 合法 |
T& 形参(非 const 引用) + 临时对象 | ✔️ 生成 | ❌ 非法 |
关键点:
-
const
引用可以延长临时对象的生命周期,避免悬垂引用。 -
非
const
引用不能绑定临时对象,因为临时对象是只读的。 -
这种机制使得
const
引用在接受字面量、表达式结果、类型转换结果时更加灵活和安全。
这在 C++ 的函数参数传递、返回值优化(RVO)、隐式转换等场景中非常重要!
四、引用的使用场景
1、引用作为函数参数
引用作为函数参数可以实现高效传参,避免拷贝开销,同时可以修改实参的值。
示例(交换函数)
还记得C语言中的交换函数,学习C语言的时候经常用交换函数来说明传值和传址的区别。现在我们学习了引用,可以不用指针作为形参了:
void Swap(int& a, int& b) {int tmp = a;a = b;b = tmp;
}
因为在这里a和b是传入实参的引用,我们将a和b的值交换,就相当于将传入的两个实参交换了。
引用传参的工程价值
引用传参在C++中主要有两大优势:
-
性能优化:避免大型对象拷贝带来的性能损耗
-
语义明确:通过引用明确表达函数可能修改参数值的意图
// 性能敏感场景:传递大型结构体
void ProcessLargeData(const BigData& data) { // const引用避免拷贝// 只读操作...
}// 需要修改参数的场景
void TransformData(Matrix& matrix) { // 非const引用表明会修改参数matrix.invert();
}
2、引用作为函数返回值
引用可以作为函数返回值,但需要注意:
-
不能返回局部变量的引用(除非是static局部变量)(因为在函数内部定义的普通的局部变量会随着函数调用的结束而被销毁)
-
可以返回类成员变量、全局变量或动态分配内存的引用(不会随着函数调用的结束而被销毁的数据)
-
可以返回函数参数中的引用
引用返回值的使用需要特别注意生命周期管理:
// 安全示例:返回静态变量或成员变量的引用
int& GetStaticValue() {static int value = 0; // 静态变量生命周期与程序相同return value;
}// 危险示例:返回局部变量的引用
int& DangerousFunction() {int local = 42; // 局部变量将在函数返回后被销毁return local; // 编译器警告:返回局部变量的引用
}
返回引用最佳实践
-
返回对象成员变量时
-
返回函数内静态变量时
-
返回动态分配的对象时(需配合智能指针)
-
返回参数中传入的引用时
注意:
如果函数返回时,出了函数作用域,返回对象还未还给系统,则可以使用引用返回;如果已经还给系统了,则必须使用传值返回。
五、引用与指针的区别(超重要!!!)
在C++中,指针和引用就像一对性格迥异的孪生兄弟。指针如同大哥,引用则像小弟,二者在实践中相得益彰。虽然功能有所重叠,但各自拥有独特的特点,彼此不可替代。
虽然引用在底层实现上通常是通过指针实现的,但在语法和使用上有显著区别:
特性 | 引用 | 指针 |
---|---|---|
开辟内存空间 | 不开辟内存空间(引用只是一个变量的取别名) | 要开辟内存空间(指针存储一个变量的地址) |
初始化要求 | 必须初始化 | 可以不初始化 |
可修改性 | 一旦绑定实体就不能更改 | 可以随时改变指向 |
NULL值 | 不能为NULL | 可以为NULL |
sizeof结果 | 引用类型的大小 | 指针的大小始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte) |
自增操作 | 实体值增加1 | 指向下一个同类型对象 |
多级间接访问 | 不支持多级引用 | 支持多级指针 |
访问方式 | 自动解引用 | 需要显式解引用 |
安全性 | 更高(引用很少出现空指针和野指针的问题) | 相对较低(指针很容易出现空指针和野指针的问题) |
在语法概念上,引用就是一个别名,没有独立的空间,其和引用实体共用同一块空间。
int main() {int a = 10;int& ra = a; // 底层通常实现为指针ra = 20; // 自动解引用int* pa = &a; // 显式指针*pa = 20; // 显式解引用return 0;
}
但是在底层实现上,引用实际是有空间的,底层实现示例(汇编层面):
从汇编角度来看,引用的底层实现也是类似指针存地址的方式来处理的。
引用与指针的工程选择准则
场景 | 推荐选择 | 理由 |
---|---|---|
必须重新绑定 | 指针 | 引用一旦绑定不可更改 |
可能为nullptr | 指针 | 引用不能为null |
容器存储 | 指针 | 引用不是对象,不能直接存储 |
函数参数 | 常引用优先 | 更清晰的语义,更安全的const保证 |
操作符重载 | 引用 | 更自然的语法 |
多态操作 | 指针或引用 | 根据是否需要重新绑定决定 |
C++引用与其他语言引用的本质区别
// C++引用示例
int a = 10;
int& ref = a; // 永久绑定到a
int b = 20;
// ref = b; // 不是重新绑定,而是赋值操作
// Java"引用"示例
Integer a = new Integer(10);
Integer ref = a; // 可以重新指向其他对象
Integer b = new Integer(20);
ref = b; // 合法操作,ref现在指向b
关键差异点
-
绑定灵活性:C++引用是永久绑定,Java引用可重新指向
-
空值处理:C++引用不能为null,Java引用可以为null
-
内存管理:C++引用不涉及内存管理,Java引用与GC机制紧密相关
六、引用的优点
-
更清晰的语法:引用使代码更易读,避免了指针的复杂语法
-
更安全:引用必须初始化且不能为NULL,减少了空指针风险
-
更高效:避免了值传递的拷贝开销
-
支持运算符重载:使自定义类型的运算符重载更自然
七、数据结构中的引用实践
在部分采用C代码实现的数据结构教材中,作者会使用C++的引用替代指针传参来简化程序,避免复杂的指针操作。但由于许多学生尚未掌握引用这一概念,反而增加了理解难度。
在传统C风格数据结构改造中,同时引用也可以显著提升代码可读性,例子如下:
1、C风格二级指针实现解析
在传统C语言中,由于没有引用概念,修改链表头指针需要使用二级指针:
// 节点结构定义
typedef struct Node {int value;struct Node* next;
} Node;// 创建新节点
Node* createNode(int value) {Node* newNode = (Node*)malloc(sizeof(Node));newNode->value = value;newNode->next = nullptr;return newNode;
}// 在链表头部插入节点
void insertNode(Node** head, int value) {Node* newNode = createNode(value);newNode->next = *head; // 新节点指向原头节点*head = newNode; // 修改头指针指向新节点
}
关键点解析
-
二级指针的必要性:为了修改调用方的头指针,需要传递指针的地址
-
解引用操作:通过
*head
访问实际的头指针 -
操作顺序:必须先设置新节点的next指针,再更新头指针
调用方式
Node* head = nullptr; // 空链表
insertNode(&head, 10); // 必须传递头指针的地址
insertNode(&head, 20);
2、C++引用风格实现解析
C++引用提供了更直观的语法来修改指针:
struct Node {int value;Node* next;
};Node* createNode(int value) {Node* newNode = new Node;newNode->value = value;newNode->next = nullptr;return newNode;
}// 使用引用简化链表操作
void insertNode(Node*& head, int value) {Node* newNode = createNode(value);newNode->next = head; // 直接使用head引用head = newNode; // 直接修改head引用
}
关键改进
-
语法简化:
Node*&
表示对指针的引用,无需二级指针 -
直观操作:直接操作
head
就像操作原始指针一样 -
类型安全:引用必须初始化,避免了空指针风险
调用方式
Node* head = nullptr; // 空链表
insertNode(head, 10); // 直接传递指针,无需取地址
insertNode(head, 20);
3、两种实现的底层对比
内存布局示例
调用方:[head指针] -> [节点A] -> [节点B] -> nullptr
C风格:调用栈保存head指针的地址,然后函数内通过解引用修改head指针
C++风格:调用栈保存head指针的引用(编译器通常用指针实现),然后函数内直接操作引用
八、现代C++中的引用演进(后面会学到,现在先了解)
1、右值引用(C++11引入)
void process(std::string&& str) { // 移动语义支持// 可以安全"窃取"str的资源
}
2、完美转发
template<typename T>
void relay(T&& arg) { // 通用引用process(std::forward<T>(arg)); // 保持值类别
}
3、结构化绑定(C++17)
std::map<int, std::string> m;
for (auto& [key, value] : m) { // 引用绑定到map元素// 直接修改map中的值
}
九、引用使用的注意事项
1、避免悬垂引用
-
不要返回局部变量的引用
-
不要返回临时对象的引用
-
注意lambda捕获引用时的生命周期
2、接口设计原则
-
输入参数:const引用优先
-
输出参数:非const引用明确修改意图
-
返回值:值返回优先,必要时返回引用
3、多线程环境
-
共享数据的引用访问需要同步
-
避免跨线程传递局部变量的引用
引用作为C++的核心特性,其正确使用需要开发者深入理解其语义和限制。在性能关键代码中合理使用引用,可以显著提升程序效率,同时保持代码的清晰性和安全性。