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

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

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

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

**⚙️操作环境:**Visual Studio 2022

目录

一、前言

二、引用的概念

三、常引用(const引用)

[1. 权限只能缩小,不能放大](#1. 权限只能缩小,不能放大)

[2. 具有常属性的临时参数](#2. 具有常属性的临时参数)

[3. const引用的强接受度](#3. const引用的强接受度)

[4. 使用const引用传参,确保函数不修改参数](#4. 使用const引用传参,确保函数不修改参数)

[5. 小结](#5. 小结)

四、使用场景

[1. 引用做函数参数](#1. 引用做函数参数)

[2. 引用做函数返回值](#2. 引用做函数返回值)

[3. 小结](#3. 小结)

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

六、引用和指针的区别

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

[1. 不能返回局部变量的引用](#1. 不能返回局部变量的引用)

[2. 引用不能引用空值](#2. 引用不能引用空值)

[3. 引用的绑定一旦建立,就不能更改](#3. 引用的绑定一旦建立,就不能更改)

[4. 需要小心引用临时对象](#4. 需要小心引用临时对象)

总结

共勉


一、前言

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

二、引用的概念

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


特点:

  • 引用一旦绑定到变量,不能改变绑定对象。
  • 引用不能为 null,必须指向有效的变量。
  • 引用必须在声明时初始化。
  • C++中不存在所谓的"二级引用",因为引用总是直接绑定到一个具体的对象上,而不是其他引用。但是,通过多层次的引用绑定,您可以实现类似的效果。不过,这种方式通常不如直接使用多层指针来得直观和灵活。
cpp 复制代码
int a = 10;
int& ref = a;  // ref 是 a 的引用
ref = 20;      // 相当于修改 a 的值
std::cout << a; // 输出 20
cpp 复制代码
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引用)

常引用是引导绑定到一个常量或者一个不可修改的值。这在提交大型对象时尤其有用,因为可以避免不必要的拷贝,同时保证该对象不被修改。

cpp 复制代码
int a = 10;
const int& ref = a;  // ref 是 a 的常引用
// ref = 20;        // 这样会报错,因为 ref 是常量引用
cpp 复制代码
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;
}
cpp 复制代码
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对象是安全的,但反之则不行。

cpp 复制代码
int a = 10;
const int& ref = a;  // 正确:权限缩小,从非 const 到 const

这里,ref是一个const引用,绑定到一个非const变量a,这是允许的,因为ref的权限比较a小。
cpp 复制代码
错误示例:权限放大(错误)

const int a = 10;
int& ref = a;  // 错误!不能将 const 变量绑定到非 const 引用

这里a是一个const变量,ref而不是一个const引用。尝试将a绑定到ref是不允许的,因为这样会放大权限。

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

临时变量(如字面常量、函数返回的非引用值等)会在表达式结束时思考。为了防止引用临时变量带来的问题,C++ 允许绑定到临时变量的引用必须是const引用,这样可以确保临时变量变量在生命周期内不被修改。

cpp 复制代码
const int& ref = 10;  // 正确:临时变量只能绑定到 const 引用

在这个修改示例中,10是一个临时变量,编译器允许我们将它绑定到const引用ref,
以确保它在引用期间不会被引用。
cpp 复制代码
错误示例:绑定到非const引用

int& ref = 10;  // 错误!不能将临时变量绑定到非 const 引用

这里,10是临时变量,非const引用不能绑定临时对象,因为临时对象用表达式结束时会联想。

3. const引用的强接受度

const引用可以绑定到多种类型的对象,包括:

  • const执行董事
  • const对象
  • 临时对象(如上所述)
  • 字面量

这使得const引用具有非常强的接受度,特别是在提交参数时,可以避免复制,提高效率。

cpp 复制代码
例子:const引用绑定各种对象

int a = 5;
const int& ref1 = a;  // 绑定到非 const 对象
const int& ref2 = 10; // 绑定到字面量

在这个例子中,ref1绑定到非const对象a,ref2绑定到字面量10,两者都是合法的。
cpp 复制代码
更复杂的例子: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等),可以显着提高效率。

cpp 复制代码
例子:传递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的拷贝操作。
cpp 复制代码
稍复杂的例子:传递大型对象的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++修改中,使用引用作为函数参数可以传递大型对象时的开销,特别是当对象需要在函数内部修改时,传递引用能直接原始对象。

cpp 复制代码
void addOne(int& num) {
    num += 1;
}

int main() {
    int a = 5;
    addOne(a);  // 通过引用修改 a
    std::cout << a;  // 输出 6
}
cpp 复制代码
因为形参是实参的临时拷贝,所以要修改实参,以前需要传指针/地址才能做到
现在C++提供的引用就不需要那么麻烦了
但是,要注意,引用不是万能的,只能是:能不用指针就不用(指针有空指针、野指针,疏忽时会很麻烦),
同样,引用也有他的弊端(下文会讲到),但是没指针那么严重
void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}
cpp 复制代码
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. 引用做函数返回值

返回函数引用时,可以返回原始对象的引用,而不是它的复制。这样做的一个好处是,可以继续对返回的对象进行操作。

cpp 复制代码
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
}
cpp 复制代码
int& Count()
{
    static int n = 0;
    n++;
    // ...
    return n;
}

int main() 
{
    std::cout << Count() << std::endl;
    //输出:1

    return  0;
}
cpp 复制代码
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函数返回数据库中元素的引用,这样可以直接修改数据库中的元素。

注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用

引用返回,如果已经还给系统了,则必须使用传值返回。

cpp 复制代码
猜猜这个程序的输出结果是什么,为什么?

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

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

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

cpp 复制代码
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);    // 性能较高,无拷贝
}
cpp 复制代码
#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
    }
}
这个例子显示了传值和传值的效率差异。通过引用引用时,不会创建副本,节省了时间和内存。

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

cpp 复制代码
#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;
}
cpp 复制代码
#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,并且可以指向不同的对象。
  • 语法 :引用使用&,指针使用*->
cpp 复制代码
int a = 10;
int* p = &a;  // 指针 p 指向 a 的地址
int& ref = a; // ref 是 a 的引用

*p = 20;  // 修改指针指向的值
ref = 30; // 修改引用绑定的值
std::cout << a;  // 输出 30
cpp 复制代码
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
}
cpp 复制代码
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
	int a = 10;
	int& ra = a;
	cout << "&a = " << &a << endl;
	cout << "&ra = " << &ra << endl;
	return 0;
}
cpp 复制代码
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
	int a = 10;
	int& ra = a;
	ra = 20;
	int* pa = &a;
	*pa = 20;
	return 0;
}

指针更灵活,但需要手动解引用和处理空指针,而引用更安全、语法更简单。
引用和指针的不同点一览:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用 在定义时必须初始化,指针没有要求
  3. 引用 在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同引用 结果为引用类型的大小 ,但指针 始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

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

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

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

cpp 复制代码
错误示例:

int& Count(int x) {
    int n = x;  // 局部变量 n
    n++;
    return n;   // 返回局部变量的引用,错误!
}
在这个例子中,n是局部变量,函数返回时它的生命周期就结束了,导致返回的引用指向无效的内存。


正确的做法:使用静态变量 静态变量的生命周期从函数第一次调用一直到程序结束,
因此可以安全地返回它的引用。
cpp 复制代码
正确的示例:

int& Count(int x) {
    static int n = x;  // 静态变量,生命周期贯穿整个程序
    n++;
    return n;          // 返回静态变量的引用
}
在这个例子中,n是静态变量,虽然它是在函数内部定义的,但它的生命周期是整个程序运行期,
因此可以安全地返回它的引用。

2. 引用不能引用空值

绑定到一个合法的变量,不能指向null或未初始化的引用必须是内存。这是引用与指针的一个主要区别。

cpp 复制代码
错误示例:

int* p = nullptr;
int& ref = *p;  // 错误!引用不能指向 null
示例中,p是一个空指针,尝试解引用它并创建引用是未行为定义。

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

与指针不同,引用在初始化后不能被更改到其他对象。它只能永久绑定到第一个初始化时的对象。

cpp 复制代码
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. 需要小心引用临时对象

临时对象在表达结束后会引入,因此引用一个临时对象是非常危险的操作。

cpp 复制代码
错误示例:

const int& ref = 5;  // 虽然可以编译,但要小心临时对象的生命周期

在这种情况下,编译器会优化,创建一个临时的常量变量,但对非const引用来说,这是不允许的。

总结

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

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

共勉

相关推荐
努力变厉害的小超超1 小时前
ArkTS中的组件基础、状态管理、样式处理、class语法以及界面渲染
笔记·鸿蒙
捕鲸叉5 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer5 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq5 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
aloha_7896 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
青花瓷7 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
dsywws7 小时前
Linux学习笔记之vim入门
linux·笔记·学习
幺零九零零8 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
捕鲸叉8 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式
Dola_Pan9 小时前
C++算法和竞赛:哈希算法、动态规划DP算法、贪心算法、博弈算法
c++·算法·哈希算法