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;
}

相关推荐
Aerkui10 分钟前
Python标准库-copy
开发语言·python
刚入门的大一新生14 分钟前
C++初阶-C++的讲解1
开发语言·c++
.生产的驴39 分钟前
SpringBoot 接口限流Lua脚本接合Redis 服务熔断 自定义注解 接口保护
java·大数据·数据库·spring boot·redis·后端·lua
一切皆有迹可循42 分钟前
SpringBoot整合MinIO快速入门:实现分布式文件存储与管理
spring boot·分布式·后端
Swift社区2 小时前
从表格到序列:Swift 如何优雅地解 LeetCode 251 展开二维向量
开发语言·leetcode·swift
hweiyu002 小时前
Python从入门到精通全套视频教程免费
开发语言·python
洛可可白2 小时前
Spring Boot中自定义注解的创建与使用
java·spring boot·后端
唐人街都是苦瓜脸3 小时前
Java RPC 框架是什么
java·开发语言·rpc
追逐时光者3 小时前
一款 .NET 开源、免费、轻量级且非侵入性的防火墙软件
后端·.net
黑不溜秋的3 小时前
Ubuntu24.04 编译 Qt 源码
开发语言·qt