C++从入门到精通——引用(&)

C++的引用


前言

C++的引用是别名,它为已存在的对象提供了另一个名称。一旦引用被初始化指向一个对象,它就不能再指向其他对象。引用必须在声明时初始化,并且必须初始化为有效的对象或字面量。引用通常用于函数参数和返回值,以实现按引用传递和返回。此外,它们也常用于大型对象和数组,以避免复制的开销。C++11引入了右值引用和移动语义,允许更高效的资源管理和性能优化。总的来说,C++的引用是一种强大的工具,能够增强代码的可读性和性能。


一、C++引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

在C++中,引用是一个别名,用于已经存在的变量或对象。引用提供了对变量的间接访问,通过引用,可以通过不同的名称来访问同一变量。

类型& 引用变量名(对象名) = 引用实体;

cpp 复制代码
void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

注意:引用类型必须和引用实体是同种类型的

引用的定义使用&符号,如下所示:

cpp 复制代码
int x = 10;
int &ref = x;

在这个例子中,refx的引用,它是x的别名。现在,refx可以互换使用,任何对ref的更改将反映在x上,反之亦然。

二、引用特性

cpp 复制代码
void TestRef()
{
   int a = 10;
   // int& ra;   // 该条语句编译时会出错
   int& ra = a;
   int& rra = a;
   printf("%p %p %p\n", &a, &ra, &rra);  
}

引用有以下几个特点:

  1. 引用必须在定义时进行初始化,一旦初始化完成后,就无法更改引用的绑定。

  2. 引用必须与其所引用的对象具有相同的类型。

  3. 引用可以作为函数的参数和返回值,通过引用参数传递参数可以避免复制大型对象的开销。

  4. 一个变量可以有多个引用

引用与指针不同,指针是一个对象,可以指向任何其他对象,而引用始终指向同一个对象。另外,引用在使用时不需要解引用操作符(*),因为它本身就是对象的别名。

引用的使用可以简化代码并提高可读性,它常用于函数参数传递、函数返回值、以及在循环中使用。

cpp 复制代码
void increment(int& i) {
    i++;
}

int main() {
    int x = 10;
    increment(x);
    cout << x; // 输出11
    return 0;
}

在上面的例子中,increment函数接受一个引用参数i,对该引用进行递增操作。在main函数中,将变量x传递给increment函数后,x的值被递增为11。因为参数是引用类型,所以对i的修改会直接影响到x

交换

指针

cpp 复制代码
void Swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
int main{
	int x = 0, y = 10;
	cout << x << "  " << y << endl;
	Swap(&x, &y);
	cout << x << "  " << y << endl;
	return 0;
	}

引用

cpp 复制代码
void _Swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
int main{
	int x = 0, y = 10;
	cout << x << "  " << y << endl;
	_Swap(x, y);
	cout << x << "  " << y << endl;
	return 0;
	}

三、常引用

保证值不变

C++中的常引用是指使用const关键字修饰的引用,即被引用的对象不能被修改。常引用和普通引用的主要区别在于,常引用所引用的对象在引用过程中不能被修改。

常引用的语法形式如下:

cpp 复制代码
const T& ref;

其中,T是被引用对象的类型。常引用可以指向任何类型的对象,包括基本类型、自定义类型、指针等。

常引用在函数参数传递中很常用,可以用于避免拷贝大对象,同时又不希望对对象进行修改。在函数定义时,使用常引用作为参数,可以防止函数对参数进行修改。

需要注意的是,引用作为函数参数时,函数内部对引用的修改也会反映到函数外部的变量上。

cpp 复制代码
void print(const int& i) {
    cout << i;
}

int main() {
    int x = 10;
    print(x);
    return 0;
}

在上述例子中,print函数接受一个const引用参数i,这意味着i不可修改。在main函数中,将变量x传递给print函数后,print函数无法修改x的值。这样做可以确保函数不会意外地修改传递给它的参数。

权限的方法

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 复制代码
const int a = 10;
int& ra = a;   // 该语句编译时会出错,a为常量
cpp 复制代码
int p = a;

这样是可取的,因为这是赋值语句

如上const int a = 10;按照语法来理解是可读但不可写,我们使用一个可读可写的int类型来引用就会报错,所以这种方法是不可取的

cpp 复制代码
const int* a = &m;
int* ra = a;   

这也是权限的放大,不可以

权限的缩小

cpp 复制代码
int a = 10;
const int& ra = a;   

如上 int a = 10;按照语法来理解是可读可写,我们使用一个可读的const int类型来引用是没有问题的,所以这种方法是可取的

cpp 复制代码
int p = a;//p = ra;

这样是可取的,因为这是赋值语句

注意a++ra也会++,因为raa的别名,但是ra不能++,因为ra是常量,总结来说就是不能通过ra来修改

cpp 复制代码
 int* a = &m;
const int* ra = a;   

这也是权限的缩小,可以

权限的平移

cpp 复制代码
 	int a = 10;
   int& ra = a;
   int& rra = a;
const int b = 10;
const int& rb = b;

如上 int a = 10;按照语法来理解是可读可写,我们使用一个可读可写的int类型来引用是没有问题的,所以这种方法是可取的,同理const的修饰也是一样的

类型转换

cpp 复制代码
 double d = 12.34;
 //int& rd = d; // 该语句编译时会出错,类型不同
 const int& rd = d;

如上为什么int& rd = d;不行,而 const int& rd = d;确可以,是因为类型转换会生成临时变量,类型转换是将一个数据类型的值转换为另一个数据类型的值,而不是直接修改原始值。因此,在执行类型转换时,会创建一个新的变量来存储转换后的值,并且可以在需要的地方使用。这时的临时变量是具有常性的,使用int& rd = d; 就相当于权限的放大,会报错,需要使用const修饰

临时变量
cpp 复制代码
int x = 0,y = 1;
//int& p = x + y;是不可以的
const int& p = x + y;//是可以的,和上面一样是临时变量的原因

除了类型转换之外,还有以下几种情况会生成临时变量:

  1. 函数返回值:当一个函数返回一个临时变量时,编译器会在函数结束时生成一个临时变量,并将其复制到函数返回的地方。

  2. 表达式计算:在进行表达式计算时,如果表达式中包含临时变量的创建和销毁,编译器会在需要的地方生成临时变量。

  3. 函数调用:当调用函数时,会将实参传递给形参。如果实参的类型与形参的类型不匹配,编译器可能会生成临时变量来进行类型转换。

  4. 对象初始化:当创建对象时,如果使用了拷贝构造函数,编译器会生成一个临时变量来初始化新对象。

  5. 运算符重载:当重载一个运算符时,可能会生成临时变量来进行操作。

需要注意的是,编译器为了优化性能可能会对临时变量进行优化,比如使用编译器自动生成的构造函数、析构函数等。因此,生成临时变量并不一定会带来显著的性能损耗。

四、引用的使用场景

1. 做参数

cpp 复制代码
void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}

2. 做返回值

cpp 复制代码
int& Count()
{
   static int n = 0;
   n++;
   // ...
   return n;
}

下面代码输出什么结果?为什么?

cpp 复制代码
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;
}

注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

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

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

cpp 复制代码
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
    A a;
    // 以值作为函数参数
    size_t begin1 = clock();
    for (size_t i = 0; i < 10000; ++i)
        TestFunc1(a);
    size_t end1 = clock();
    // 以引用作为函数参数
    size_t begin2 = clock();
    for (size_t i = 0; i < 10000; ++i)
        TestFunc2(a);
    size_t end2 = clock();
    // 分别计算两个函数运行结束后的时间
    cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
    TestRefAndValue();
    return 0;
}

值和引用的作为返回值类型的性能比较

cpp 复制代码
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
    // 以值作为函数的返回值类型
    size_t begin1 = clock();
    for (size_t i = 0; i < 100000; ++i)
        TestFunc1();
    size_t end1 = clock();
    // 以引用作为函数的返回值类型
    size_t begin2 = clock();
    for (size_t i = 0; i < 100000; ++i)
        TestFunc2();
    size_t end2 = clock();
    // 计算两个函数运算完成之后的时间
    cout << "TestFunc1 time:" << end1 - begin1 << endl;
    cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
    //TestRefAndValue();
    TestReturnByRefOrValue();
    return 0;
}

通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。

六、引用和指针的区别

引用和指针的注意点

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

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. 引用比指针使用起来相对更安全

注意:

cpp 复制代码
int* ptr = NULL;
int& r = ptr;

这样编译器是不会报错的

cpp 复制代码
int main()
{
    int* ptr = NULL;
    int& r = *ptr;
    cout << r << endl;
    return 0;
}

这样才会报错,因为指针的解引用是在使用的阶段,在底层是r存放的是ptr的地址

七、测试代码展示

&.cpp

cpp 复制代码
#include <iostream>
using namespace std;
//void TestRef()
//{
//    int a = 10;
//    int& ra = a;//<====定义引用类型
//    printf("%p\n", &a);
//    printf("%p\n", &ra);
//}
void TestRef()
{
    int a = 10;
    // int& ra;   // 该条语句编译时会出错
    int& ra = a;
    int& rra = a;
    printf("%p %p %p\n", &a, &ra, &rra);
}
void increment(int& i) {
    i++;
}
void Swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
void _Swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
//int main()
//{
//    //TestRef();
//    //int x = 10;
//    //increment(x);
//    //cout << x; // 输出11
//    int x = 0, y = 100;
//    cout << x << "  " << y << endl;
//    //Swap(&x, &y);
//    _Swap(x, y);
//    cout << x << "  " << y << endl;
//    return 0;
//}
//void print(const int& i) {
//    cout << i;
//}
//
//int main() {
//    int x = 10;
//    /*int a = 10;
//    const int& ra = a;
//    int p = a;*/
//    const int a = 10;
//    //int& ra = a;   // 该语句编译时会出错,a为常量
//    int p = a;
//    print(p);
//    return 0;
//}
//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;
//}
#include <time.h>
//struct A { int a[10000]; };
//void TestFunc1(A a) {}
//void TestFunc2(A& a) {}
//void TestRefAndValue()
//{
//    A a;
//    // 以值作为函数参数
//    size_t begin1 = clock();
//    for (size_t i = 0; i < 10000; ++i)
//        TestFunc1(a);
//    size_t end1 = clock();
//    // 以引用作为函数参数
//    size_t begin2 = clock();
//    for (size_t i = 0; i < 10000; ++i)
//        TestFunc2(a);
//    size_t end2 = clock();
//    // 分别计算两个函数运行结束后的时间
//    cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
//    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
//}
//struct A { int a[10000]; };
//A a;
 值返回
//A TestFunc1() { return a; }
 引用返回
//A& TestFunc2() { return a; }
//void TestReturnByRefOrValue()
//{
//    // 以值作为函数的返回值类型
//    size_t begin1 = clock();
//    for (size_t i = 0; i < 100000; ++i)
//        TestFunc1();
//    size_t end1 = clock();
//    // 以引用作为函数的返回值类型
//    size_t begin2 = clock();
//    for (size_t i = 0; i < 100000; ++i)
//        TestFunc2();
//    size_t end2 = clock();
//    // 计算两个函数运算完成之后的时间
//    cout << "TestFunc1 time:" << end1 - begin1 << endl;
//    cout << "TestFunc2 time:" << end2 - begin2 << endl;
//}
//int main()
//{
//    //TestRefAndValue();
//    TestReturnByRefOrValue();
//    return 0;
//}
int main()
{
    int* ptr = NULL;
    int& r = *ptr;
    cout << r << endl;
    return 0;
}

相关推荐
m0_748236113 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
颜淡慕潇8 分钟前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes
倔强的石头10611 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
Jackey_Song_Odd12 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo61715 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
A懿轩A1 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
机器视觉知识推荐、就业指导1 小时前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Evand J2 小时前
LOS/NLOS环境建模与三维TOA定位,MATLAB仿真程序,可自定义锚点数量和轨迹点长度
开发语言·matlab
LucianaiB2 小时前
探索CSDN博客数据:使用Python爬虫技术
开发语言·爬虫·python