CPP引用
引用
一、C++ 引用
C++ 中的引用 (Reference) 是一种特殊的变量,本质上是一个已存在的变量的别名。它提供了一种间接访问其他变量的方式。
引用本身不占用额外的内存空间来存储数据,而是与一个已存在的变量绑定,通过引用访问或修改的就是该绑定的变量。因此可以说引用是一个已存在变量的另一种访问方式。
1、引用的使用
语法:
数据类型 &别名 = 变量名(已存在);
示例:
int main(){ int a = 10; int &b = a; //声明引用变量 只是绑定到已有的变量 a 上,成为它的另一个名字或者说别名。cout << "a = " << a << endl; //10cout << "b = " << b << endl; //10cout << "a地址 = " << &a << endl; //a地址和b地址是一样的cout << "b地址 = " << &b << endl; b = 20; cout << "a = " << a << endl; cout << "b = " << b << endl;
}
从上面示例代码中可以看到,修改了引用变量 b 的值,那么变量 a 的值也发生了变化,说明它们是指向同一块内存中的变量。引用即是一个变量的别名。
2、引用的注意事项
(1) 声明引用必须进行初始化:
在 C++ 中,引用在声明时必须初始化。因为引用是已存在变量的别名,它不可以“空引用”,必须与某个已有变量关联。
int x = 5;
int& ref = x; // 正确,引用 ref 被初始化为 x 的别名int& ref2; // 错误:引用必须在声明时初始化
(2) 引用初始化后,不可以改变:
一旦引用绑定到一个变量,就不能改变引用指向的对象,它始终引用最初绑定的变量。
int x = 10;
int y = 20;
int& ref = x; // ref 是 x 的引用ref = y; // 这是赋值操作,将 y 的值赋给 x,而不是改变引用的目标
cout << ref << endl; // 输出 20,x 被修改为 20,但 ref 依然指向 x,而不是 y
(3) 引用不能为 NULL
:
引用不能是空引用,也不能指向 NULL
。引用必须在创建时与一个有效的对象关联,不能指向一个“空”位置。
int x = 10;
int& ref = x; // 正确:ref 引用 xint* ptr = nullptr;
int& ref2 = *ptr; // 错误:不能为 NULL 引用
引用必须指向有效对象,这与指针不同,指针可以为 NULL
。
(4) 一个变量可以有多个引用:
一个变量可以有多个引用,它们都指向同一个内存地址,并且共享同一个值。
int x = 10;
int& ref1 = x; // ref1 是 x 的引用
int& ref2 = x; // ref2 也是 x 的引用ref1 = 20; // 修改 ref1 所引用的值,也就是 x
cout << x << endl; // 输出 20
cout << ref2 << endl; // 输出 20
(5) 引用不能指向常量或临时变量:
普通引用不能绑定到常量或临时变量,因为它们无法修改常量的值或持有临时变量的地址。
int x = 10;
int& ref1 = x; // ref1 是 x 的引用
// int& ref2 = 20; // 错误:引用不能指向常量或临时变量,10 是一个临时变量
二、引用作为参数
由于引用提供了一种轻量级的、不增加额外存储开销的方式来操作已存在的变量,因此常用于函数参数传递、返回值优化等场景,以提高代码的效率和可读性。
1、避免复制成本
引用作为函数的参数,可以减少值传递带来的开销,尤其是在传递大型对象时,能避免昂贵的深拷贝。
2、修改原始数据
使用引用参数可以实现对实参的直接修改。
#include <iostream>
using namespace std;
void swap(int* a,int* b) {int temp = *a;*a = *b;*b = temp;
}
void swapWithXor(int &a, int &b) {if (a != b) { // 避免当a和b是同一个变量时值被清零a = a ^ b; // 第一步:a存储a和b的异或结果b = a ^ b; // 第二步:b = (a^b)^b = a(恢复a的原始值)a = a ^ b; // 第三步:a = (a^b)^a = b(恢复b的原始值)}
}
void swap(int& num1, int& num2){ int temp = num1; num1 = num2num2 = temp;
}
int main(){ int a = 10; int b = 20; swap(a, b); cout << "a = " << a << endl; cout << "b = " << b << endl;
}
运行结果发现,a 和 b 的值发生了改变,也就是说通过引用传参相当与给变量起了一个别名,最终都是对同一个变量的操作。
3、对比指针
使用引用和指针作为函数参数的区别:
(1)直接使用引用变量进行赋值和修改,语法简洁,不需要解引用操作符 (*)。
(2)引用和指针都可以作为函数参数来实现数据的共享和修改,使用指针需要程序员自行管理内存和检查指针的有效性。而引用不能为 nullptr ,因为在定义时必须要进行初始化,总是指向某个有效的对象,所以这在一定程度上强制了函数调用时必须提供有效的对象。
void func(int& var){ var = 100; //不需要处理空值,因为引用在使用前必须初始化,保证值是有效的
}
int* func(int* var){ if (var != nullptr) { //需要检查指针的有效性return var;}
}
int main(){ int a = 10; func(a); int* result = func(&a); delete result; //手动释放内存result = nullptr;
}
(3)使用指针作为函数参数,可以在函数内部改变指针的指向,这是引用类型参数所不具备的。
void func1(int* var){ var = new int(10);//指向新的地址
}
三、引用作为函数的返回值
用引用作为函数的返回值,最大的好处是,在内存中不产生被返回值的副本。
1、引用作为函数返回值
我们来看一段代码:
//定义全局变量
int temp;
int fun1(){ temp = 10;return temp;
}
int& fun2(){ temp = 10;return temp;
}
int main() { // 1. 返回值类型int a = func1(); // 调用 func1 获取返回值a = 99; // 修改 a,不影响 tempcout << temp << endl; // 输出 10,因为 func1 返回的是值// 2. 返回引用类型int& b = func2(); // 调用 func2 获取返回引用b = 88; // 修改 b,同时修改 tempcout << temp << endl; // 输出 88,因为 func2 返回的是引用
}
上述代码中会出现两层拷贝,当执行语句 int a = fun1 (); 的时候会先创建一个临时变量,把返回值拷贝给隐藏的临时变量,然后再把临时变量的值再拷贝给 a,假设这个临时变量是 t,相当于做了这两个赋值的步骤:
t = temp;
a = t;
而返回引用在内存中不会产生副本,是原有变量的一个别名,这样就避免产生临时变量,相比返回普通类型的执行效率更高。
2、使用引用作为函数返回值注意事项
局部变量不要作为引用返回,函数执行完成后,局部变量会被销毁。
#include <iostream>
using namespace std;
int& test(){ int a = 1int& b = a; //局部变量不要作为引用返回,函数执行完成后,局部变量内存会被释放,导致返回的引用成为悬挂引用。return b;
}
int main(){ int& a = test(); cout << "返回结果:" << a << endl; //返回结果:-858993460
}
如果要返回局部变量,可以使用 static 修饰,静态变量存在于全局区,全局区上的数据在程序结束后释放。
int& test(){ static int a = 1; int& b = a; return b;
}
3、函数的调用可以作为左值
(1)左值
在 C++ 中,表达式分为左值和右值。
左值 (lvalue):指的是持久的对象,通常指代表达式结束后依然存在的对象。
特点:
- 左值在内存中有明确的地址,可以取地址。
- 左值可以被修改,即可以出现在赋值语句的左侧。
- 左值可以出现在赋值表达式的左边或右边。
int a = 10; // 'a' 是左值,10 是右值
int* p = &a; // 正确,取变量地址,'a' 是左值
int b = a + 1; // 正确,使用左值
(2)右值
右值 (rvalue):指的是临时的对象,通常指表达式结束后不再存在的对象。
特点:
- 右值不能取地址。
- 右值不能出现在赋值语句的左侧。
int a = 10;
int* p2 = &(a+1); // 错误,表达式(a+1)的结果是一个右值,右值不能取地址
int x = 10;
int y = 20;// 同样,下面的尝试也是无效的
(x + y) = 100; // 编译器错误:表达式 (x + y) 是一个右值,不能出现在赋值语句的左边
(3)函数调用作为左值
当函数返回引用时,函数的调用可以作为左值。
int a;
int& test(){ a = 10; return a;
}
int main(){ int& num1 = test(); //返回的是 a 的引用//num1是 a 的引用,因此输出10。cout << num1 << endl; //test()返回a的引用,所以可以将a的值改为 20test() = 20;cout << num1 << endl; //输出num1的值
}
四、常引用
1、常引用的定义
通过 const 关键字定义常量引用,必须在声明时初始化。
const int &a = 10;
上述代码中,10 是一个字面值常量,本身不是变量,不能直接被引用。但是使用 const 修饰后,编译器会创建一个临时变量,相当于下面的代码:
int temp = 10;
const int &a = temp;
此时,如果想要修改 a 的值,是不被允许的。会提示表达式必须是可修改的左值。
a=100;
常量引用主要用来修饰形参,提高安全性,防止形参改变实参。
如下代码,向 test () 函数传入实参变量 a,test () 函数执行之后,输出 a 的值是 100。也就是说函数执行完成后,会对外部的变量 a 产生影响。
int a = 10;
const int& ref = a;
-
这里
ref
是对a
的常量引用(const int&
),表示通过ref
不能修改a
的值(ref = 20;
是错误的)。 -
但是
a
本身是非const变量,可以直接修改a
(a = 20;
是合法的)。 -
const int&
的重点是:引用本身是不能修改所引用对象的内容。
2、指针常量和常量指针:
const
可以修饰指针的两部分:指针本身 和 指针指向的内容。具体的含义如下:
-
const int* p
或int const* p
:p
可以改变指向不同的内存位置,但 不能修改指向的内容。
void func(const int* p) {// *p = 10; // 错误:不能修改指针指向的内容int b = 20;p = &b; // 正常:可以修改指针的指向 }
-
int* const p
:p
是常量指针,指针本身不能修改,但 可以修改指向的内容。
void func(int* const p) {*p = 10; // 正常:可以修改指针指向的内容int b = 20;// p = &b; // 错误:不能修改指针的指向 }
-
const int* const p
:p
是常量指针,指针本身不能修改,指针指向的内容也不能修改。
void func(const int* const p) {// *p = 10; // 错误:不能修改指针指向的内容// p = &b; // 错误:不能修改指针的指向 }
总结:
const
左边修饰指针的内容(const int* p
):指针指向的内容不可通过指针修改,但指针本身可以指向其他地址,指针指向的变量也可以直接被赋值修改。const
右边修饰指针本身(int* const p
):指针本身指向的地址不可修改,但指针指向的变量的值可以被修改。
类型 | 含义 | 是否能修改值 | 是否能修改指针 |
---|---|---|---|
const int* p | 常量指针:不能通过 p 修改指向的值 | ❌ 否 | ✅ 是 |
int* const p | 指针常量:指针地址不可变,值可改 | ✅ 是 | ❌ 否 |
const int* const p | 指向常量的常量指针,两者都不可变 | ❌ 否 | ❌ 否 |
const 在 * 左边,指向内容不能变 → 常量指针
const 在 * 右边,指针地址不能变 → 指针常量
把const当作常量就行了!