【C++】深入理解引用:从基础到进阶详解
🦄个人主页:小米里的大麦-CSDN博客
🎏所属专栏:C++_小米里的大麦的博客-CSDN博客
🎁代码托管:C++: 探索C++编程精髓,打造高效代码仓库 (gitee.com)
⚙️操作环境:Visual Studio 2022
目录
一、前言
C++中的引用(reference)是一个非常重要的概念,它可以让你创建一个变量的别名,直接操作原始变量而不需要复制。引用在函数参数传递、返回值和效率优化等方面有广泛的应用。下面我们会一步步讲解引用的各个知识点,并搭配上由易到难的代码示例来帮助深入理解。
二、引用的概念
- 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
- 所以,引用本质上只是一个已有变量的别名,它必须在声明时被初始化,且不能更改为其他变量的引用。引用通过直接操作被引用的对象,实现与指针类似的效果,但语法上更简洁,不涉及指针的复杂运算。
- 举几个例子,比如:李逵,在家称为"铁牛",江湖上人称"黑旋风";鲁迅也是周树人;你是齐天大圣,是美猴王,是孙悟空,定不是吗喽。哈哈哈~
特点:
- 引用一旦绑定到变量,不能改变绑定对象。
- 引用不能为 null,必须指向有效的变量。
- 引用必须在声明时初始化。
- C++中不存在所谓的“二级引用”,因为引用总是直接绑定到一个具体的对象上,而不是其他引用。但是,通过多层次的引用绑定,您可以实现类似的效果。不过,这种方式通常不如直接使用多层指针来得直观和灵活。
int a = 10;
int& ref = a; // ref 是 a 的引用
ref = 20; // 相当于修改 a 的值
std::cout << a; // 输出 20
void swap(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 5, b = 10;
swap(a, b); // 使用引用实现交换
std::cout << "a: " << a << ", b: " << b; // 输出 a: 10, b: 5
}
在这个例子中,swap函数使用引用来交换两个整数的值,避免了传值复制带来的开销。
三、常引用(const引用)
常引用是引导绑定到一个常量或者一个不可修改的值。这在提交大型对象时尤其有用,因为可以避免不必要的拷贝,同时保证该对象不被修改。
int a = 10;
const int& ref = a; // ref 是 a 的常引用
// ref = 20; // 这样会报错,因为 ref 是常量引用
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
void print(const std::string& str) {
std::cout << str << std::endl;
}
int main() {
std::string message = "Hello, World!";
print(message); // 通过常引用传递字符串,避免拷贝
}
这里,print函数使用常引用来避免拷贝std::string对象,提高性能,同时确保message不被修改。
1. 权限只能缩小,不能放大
在C++中,引用和指针的权限只能缩小,不能放大。即,const
指针的引用或指针不能指向非const
的指针。话虽这么说,试图把一个非const
的引用/指针赋给一个const
对象是安全的,但反之则不行。
int a = 10;
const int& ref = a; // 正确:权限缩小,从非 const 到 const
这里,ref是一个const引用,绑定到一个非const变量a,这是允许的,因为ref的权限比较a小。
错误示例:权限放大(错误)
const int a = 10;
int& ref = a; // 错误!不能将 const 变量绑定到非 const 引用
这里a是一个const变量,ref而不是一个const引用。尝试将a绑定到ref是不允许的,因为这样会放大权限。
2. 具有常属性的临时参数
临时变量(如字面常量、函数返回的非引用值等)会在表达式结束时思考。为了防止引用临时变量带来的问题,C++ 允许绑定到临时变量的引用必须是const
引用,这样可以确保临时变量变量在生命周期内不被修改。
const int& ref = 10; // 正确:临时变量只能绑定到 const 引用
在这个修改示例中,10是一个临时变量,编译器允许我们将它绑定到const引用ref,
以确保它在引用期间不会被引用。
错误示例:绑定到非const引用
int& ref = 10; // 错误!不能将临时变量绑定到非 const 引用
这里,10是临时变量,非const引用不能绑定临时对象,因为临时对象用表达式结束时会联想。
3. const
引用的强接受度
const
引用可以绑定到多种类型的对象,包括:
- 非
const
执行董事const
对象- 临时对象(如上所述)
- 字面量
这使得
const
引用具有非常强的接受度,特别是在提交参数时,可以避免复制,提高效率。
例子:const引用绑定各种对象
int a = 5;
const int& ref1 = a; // 绑定到非 const 对象
const int& ref2 = 10; // 绑定到字面量
在这个例子中,ref1绑定到非const对象a,ref2绑定到字面量10,两者都是合法的。
更复杂的例子:const引用绑定临时对象
std::string getMessage() {
return "Hello, World!";
}
int main() {
const std::string& msg = getMessage(); // 绑定临时字符串
std::cout << msg; // 输出 Hello, World!
}
这里getMessage()返回,一个临时对象,该临时对象被安全地绑定到const引用msg上。
由于msg是const引用,C++会确保临时对象在引用期间不会被回忆。
4. 使用const
引用传参,确保函数不修改参数
在传递参数时,如果函数不打算修改参数,最好使用const
引用。这样可以避免不必要的拷贝,尤其是对于大型对象(如std::string
、std::vector
等),可以显着提高效率。
例子:传递const引用
void printMessage(const std::string& message) {
std::cout << message << std::endl;
}
int main() {
std::string msg = "Hello!";
printMessage(msg); // 通过 const 引用传参,避免拷贝
}
在这个例子中,printMessage函数通过const引用接收std::string,
保证不会修改声明的msg,同时避免了std::string的拷贝操作。
稍复杂的例子:传递大型对象的const引用
#include <vector>
void processVector(const std::vector<int>& vec) {
for (int i : vec) {
std::cout << i << " ";
}
}
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
processVector(nums); // 通过 const 引用传递 vector,避免拷贝
}
在这个例子中,processVector通过const引用接收std::vector,
保证不修改原始数据,并且避免了传值时的拷贝,提高了效率。
5. 小结
- 权限只能缩小,不能放大:
const
引用可以绑定到非const
对象,但返回不了。- 临时变量具有常量属性:临时对象只能绑定到
const
引用,以防止未定义行为。const
引用的接受度较高:const
引用可以绑定到非const
对象、const
对象、临时对象和字面量,应用场景广泛。- 使用
const
修改修改传参:当函数不需要确定的参数时,使用const
引用可以避免不必要的拷贝,同时保证参数不被引用。这些概念和注意事项有助于更好地理解和使用 C++ 的引用和
const
引用。
四、使用场景
1. 引用做函数参数
在C++修改中,使用引用作为函数参数可以传递大型对象时的开销,特别是当对象需要在函数内部修改时,传递引用能直接原始对象。
void addOne(int& num) {
num += 1;
}
int main() {
int a = 5;
addOne(a); // 通过引用修改 a
std::cout << a; // 输出 6
}
因为形参是实参的临时拷贝,所以要修改实参,以前需要传指针/地址才能做到
现在C++提供的引用就不需要那么麻烦了
但是,要注意,引用不是万能的,只能是:能不用指针就不用(指针有空指针、野指针,疏忽时会很麻烦),
同样,引用也有他的弊端(下文会讲到),但是没指针那么严重
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void scaleArray(int arr[], int size, int factor) {
for (int i = 0; i < size; ++i) {
arr[i] *= factor;
}
}
int main() {
int nums[] = {1, 2, 3, 4, 5};
scaleArray(nums, 5, 2); // 通过引用修改数组的元素
for (int i : nums) {
std::cout << i << " "; // 输出 2 4 6 8 10
}
}
这里,scaleArray函数直接操作数据库,因为数据库在C++中是默认以引用方式提交的。
2. 引用做函数返回值
返回函数引用时,可以返回原始对象的引用,而不是它的复制。这样做的一个好处是,可以继续对返回的对象进行操作。
int& getMax(int& x, int& y) {
return (x > y) ? x : y;
}
int main() {
int a = 10, b = 20;
getMax(a, b) = 30; // 修改较大的值
std::cout << a << " " << b; // 输出 10 30
}
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
std::cout << Count() << std::endl;
//输出:1
return 0;
}
int& getElement(int arr[], int index) {
return arr[index];
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
getElement(arr, 2) = 10; // 修改数组中的第三个元素
for (int i : arr) {
std::cout << i << " "; // 输出 1 2 10 4 5
}
}
在这个例子中,getElement函数返回数据库中元素的引用,这样可以直接修改数据库中的元素。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。
猜猜这个程序的输出结果是什么,为什么? #include <iostream> using namespace std; int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); Add(3, 4); cout << "Add(1, 2) is :" << ret << endl; return 0; } 解释: 在 Add 函数中返回了一个局部变量 c 的引用。 由于 c 是在函数内部声明的一个局部变量,当函数执行完毕后,c 的生存期结束,它的内存会被释放。 这意味着在函数返回之后,任何对这个引用的使用都会导致未定义行为。 具体来说,在 main 函数中调用 Add(1, 2) 并将其结果赋值给 ret 时,ret 成为了局部变量 c 的引用。 但在 Add 函数返回之后,c 不再存在,因此 ret 成为一个无效的引用。 随后,当再次调用 Add(3, 4) 时,Add 函数会正常执行并返回,但此时 ret 仍然指向已被销毁的 c, 因此 cout 操作的结果是不确定的,可能导致程序崩溃或其他未定义行为。 如果确实需要返回引用,并且希望保持某些数据的一致性或状态, 可以考虑使用类成员函数来返回类内部的数据成员的引用。 在这种情况下,这些数据成员的生命周期至少与对象的生命周期一样长,因此是安全的。 但对于局部变量,它们的生命周期仅限于函数的作用域内,所以返回它们的引用是不安全的。
3. 小结
- 基本任何场景都可以用引用传参
- 谨慎用引用做返回值。出了函数作用域,对象不在了,就不能用引用返回u,还在就可以用引用返回
五、传值、传引用效率比较
传值时,函数会创建一个参数的副本,增加的对象可能导致性能大幅增加。而传值引用则不会复制对象,只是传递对象的别名,尤其是对大型对象和容器有利。
void byValue(std::string s) {
// 拷贝 s
}
void byReference(const std::string& s) {
// 通过引用传递,避免拷贝
}
int main() {
std::string largeStr = "This is a very large string";
byValue(largeStr); // 性能较低,拷贝 largeStr
byReference(largeStr); // 性能较高,无拷贝
}
#include <vector>
void processVectorByValue(std::vector<int> vec) {
vec.push_back(100);
}
void processVectorByReference(std::vector<int>& vec) {
vec.push_back(100);
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
processVectorByValue(numbers); // 传值,vec 是 numbers 的拷贝
processVectorByReference(numbers); // 传引用,vec 是 numbers 的别名
for (int i : numbers) {
std::cout << i << " "; // 输出 1 2 3 4 5 100
}
}
这个例子显示了传值和传值的效率差异。通过引用引用时,不会创建副本,节省了时间和内存。
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <iostream>
#include <chrono>
#include <ctime>
struct A {
int a[10000];
};
A g_a; // 全局变量
// 值返回
A TestFunc1() {
return g_a;
}
// 引用返回
A& TestFunc2() {
return g_a;
}
void TestReturnByRefOrValue() {
// 以值作为函数的返回值类型
auto start1 = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < 100000; ++i) {
TestFunc1(); // 使用函数的结果,否则编译器可能优化掉
(void)i; // 防止未使用的警告
}
auto end1 = std::chrono::high_resolution_clock::now();
// 以引用作为函数的返回值类型
auto start2 = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < 100000; ++i) {
TestFunc2(); // 使用函数的结果,否则编译器可能优化掉
(void)i; // 防止未使用的警告
}
auto end2 = std::chrono::high_resolution_clock::now();
// 计算两个函数运算完成之后的时间
auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start1).count();
auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - start2).count();
std::cout << "TestFunc1 time: " << duration1 << " microseconds" << std::endl;
std::cout << "TestFunc2 time: " << duration2 << " microseconds" << std::endl;
}
int main() {
TestReturnByRefOrValue();
return 0;
}
#include <iostream>
#include <chrono>
using namespace std;
struct A {
int a[10000];
};
A g_a; // 全局变量
// 值返回
A TestFunc1() {
return g_a;
}
// 引用返回
A& TestFunc2() {
return g_a;
}
void TestReturnByRefOrValue() {
// 以值作为函数的返回值类型
auto start1 = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < 100000; ++i) {
auto result1 = TestFunc1(); // 使用函数的结果
(void)result1; // 防止未使用的警告
}
auto end1 = std::chrono::high_resolution_clock::now();
// 以引用作为函数的返回值类型
auto start2 = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < 100000; ++i) {
auto& result2 = TestFunc2(); // 使用函数的结果
(void)result2; // 防止未使用的警告
}
auto end2 = std::chrono::high_resolution_clock::now();
// 计算两个函数运算完成之后的时间
auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start1).count();
auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - start2).count();
cout << "TestFunc1 time: " << duration1 << " microseconds" << endl;
cout << "TestFunc2 time: " << duration2 << " microseconds" << endl;
}
int main() {
TestReturnByRefOrValue();
return 0;
}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
六、引用和指针的区别
- 引用:必须在声明时初始化,无法改变引用的对象。
- 指针:可以初始化为
null
,并且可以指向不同的对象。- 语法:引用使用
&
,指针使用*
和->
。
int a = 10;
int* p = &a; // 指针 p 指向 a 的地址
int& ref = a; // ref 是 a 的引用
*p = 20; // 修改指针指向的值
ref = 30; // 修改引用绑定的值
std::cout << a; // 输出 30
void swapPointer(int* x, int* y) {
int temp = *x;
*x = *y;
*y = temp;
}
void swapReference(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 10, b = 20;
swapPointer(&a, &b); // 使用指针交换
swapReference(a, b); // 使用引用交换
std::cout << a << " " << b; // 输出 20 10
}
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。 int main() { int a = 10; int& ra = a; cout << "&a = " << &a << endl; cout << "&ra = " << &ra << endl; return 0; }
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。 int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }
指针更灵活,但需要手动解引用和处理空指针,而引用更安全、语法更简单。
引用和指针的不同点一览:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
七、关于引用的主要注意事项归纳
1. 不能返回局部变量的引用
在函数中,局部变量的生命周期在函数结束后就结束了。如果返回一个局部变量的引用,程序行为会变得不可预测,该引用指向的内存可能已经被释放或被其他数据占用。
错误示例:
int& Count(int x) {
int n = x; // 局部变量 n
n++;
return n; // 返回局部变量的引用,错误!
}
在这个例子中,n是局部变量,函数返回时它的生命周期就结束了,导致返回的引用指向无效的内存。
正确的做法:使用静态变量 静态变量的生命周期从函数第一次调用一直到程序结束,
因此可以安全地返回它的引用。
正确的示例:
int& Count(int x) {
static int n = x; // 静态变量,生命周期贯穿整个程序
n++;
return n; // 返回静态变量的引用
}
在这个例子中,n是静态变量,虽然它是在函数内部定义的,但它的生命周期是整个程序运行期,
因此可以安全地返回它的引用。
2. 引用不能引用空值
绑定到一个合法的变量,不能指向null
或未初始化的引用必须是内存。这是引用与指针的一个主要区别。
错误示例:
int* p = nullptr;
int& ref = *p; // 错误!引用不能指向 null
示例中,p是一个空指针,尝试解引用它并创建引用是未行为定义。
3. 引用的绑定一旦建立,就不能更改
与指针不同,引用在初始化后不能被更改到其他对象。它只能永久绑定到第一个初始化时的对象。
int a = 10;
int b = 20;
int& ref = a; // ref 引用 a
ref = b; // 这不会改变 ref 的绑定对象,而是将 b 的值赋给 a
std::cout << a << " " << b; // 输出 20 20
ref = b;并不会ref引用b,它实际上是把b的值赋予了a,因此a最后b都变成了20。
4. 需要小心引用临时对象
临时对象在表达结束后会引入,因此引用一个临时对象是非常危险的操作。
错误示例:
const int& ref = 5; // 虽然可以编译,但要小心临时对象的生命周期
在这种情况下,编译器会优化,创建一个临时的常量变量,但对非const引用来说,这是不允许的。
总结
- 引用的主要作用是在函数传参和返回值中减少不必要的复制操作,提高程序的运行效率。
const
引用是非常灵活和常用的,能够接收多种类型的对象,包括字面量和临时对象,广泛用于保证数据不被修改。- 注意生命周期和局部变量引用问题,避免程序指向无效内存。
- 权限控制在C++引用中非常重要的保证,引用的权限只能缩小而不能放大,有助于保证数据的安全性。
通过以上的讲解,相信你对 C++ 引用的概念、特性和应用场景有了更深、更全面的理解。本文制作、整理、总结不易,对你有帮助的话,还请留下三连以表支持!感谢!!
共勉
原文地址:https://blog.csdn.net/Huangcancan666/article/details/142307850
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!