【C++】深入理解引用:从基础到进阶详解

小米里的大麦 2024-10-01 10:35:02 阅读 54

🦄个人主页:小米里的大麦-CSDN博客

🎏所属专栏:C++_小米里的大麦的博客-CSDN博客

🎁代码托管:C++: 探索C++编程精髓,打造高效代码仓库 (gitee.com)

⚙️操作环境:Visual Studio 2022

目录

一、前言

二、引用的概念

三、常引用(const引用)

1. 权限只能缩小,不能放大

2. 具有常属性的临时参数

3. const引用的强接受度

4. 使用const引用传参,确保函数不修改参数

5. 小结

四、使用场景

1. 引用做函数参数

2. 引用做函数返回值

3. 小结

五、传值、传引用效率比较

六、引用和指针的区别

七、关于引用的主要注意事项归纳

1. 不能返回局部变量的引用

2. 引用不能引用空值

3. 引用的绑定一旦建立,就不能更改

4. 需要小心引用临时对象

总结 

共勉


一、前言

C++中的引用(reference)是一个非常重要的概念,它可以让你创建一个变量的别名,直接操作原始变量而不需要复制。引用在函数参数传递、返回值和效率优化等方面有广泛的应用。下面我们会一步步讲解引用的各个知识点,并搭配上由易到难的代码示例来帮助深入理解。

二、引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。 所以,引用本质上只是一个已有变量的别名,它必须在声明时被初始化,且不能更改为其他变量的引用。引用通过直接操作被引用的对象,实现与指针类似的效果,但语法上更简洁,不涉及指针的复杂运算。举几个例子,比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"鲁迅也是周树人;你是齐天大圣,是美猴王,是孙悟空,定不是吗喽。哈哈哈~

特点:

引用一旦绑定到变量,不能改变绑定对象。引用不能为 null,必须指向有效的变量。引用必须在声明时初始化。C++中不存在所谓的“二级引用”,因为引用总是直接绑定到一个具体的对象上,而不是其他引用。但是,通过多层次的引用绑定,您可以实现类似的效果。不过,这种方式通常不如直接使用多层指针来得直观和灵活。

<code>int a = 10;

int& ref = a; // ref 是 a 的引用

ref = 20; // 相当于修改 a 的值

std::cout << a; // 输出 20

<code>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::stringstd::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,还在就可以用引用返回

五、传值、传引用效率比较

传值时,函数会创建一个参数的副本,增加的对象可能导致性能大幅增加。而传值引用则不会复制对象,只是传递对象的别名,尤其是对大型对象和容器有利。

<code>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. 不能返回局部变量的引用

在函数中,局部变量的生命周期在函数结束后就结束了。如果返回一个局部变量的引用,程序行为会变得不可预测,该引用指向的内存可能已经被释放或被其他数据占用。

<code>错误示例:

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引用来说,这是不允许的。

总结 

引用的主要作用是在函数传参和返回值中减少不必要的复制操作,提高程序的运行效率。<code>const引用是非常灵活和常用的,能够接收多种类型的对象,包括字面量和临时对象,广泛用于保证数据不被修改。注意生命周期和局部变量引用问题,避免程序指向无效内存。权限控制在C++引用中非常重要的保证,引用的权限只能缩小而不能放大,有助于保证数据的安全性。

通过以上的讲解,相信你对 C++ 引用的概念、特性和应用场景有了更深、更全面的理解。本文制作、整理、总结不易,对你有帮助的话,还请留下三连以表支持!感谢!!

共勉



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。