C++:引用的本质与应用

一、变量知识回顾

1、变量是一段连续存储空间的别名

2、程序通过变量来申请并命名存储空间;

3、可以通过变量的名字来使用存储空间;

那么问题来了,一段连续的存储空间是否可以有多个别名?

一个人可以有乳名,小名,正名,字,号,同理存储空间应该可以有多个别名,可以通过引用来解决。

二、引用----C++中新增加的概念

C++新增了一种复合类型----引用变量。引用是已经定义的变量的别名,例如将twain作为clement变量的引用,就可以交替使用twain和clement来表示这个变量,就像鲁迅和周树人就是同一个人。那么这种别名有什么用?引用变量的主要用途是作为函数的形参,通过将引用变量用作参数,函数将使用原始数据,而不是数据的副本。这样除指针外,引用也为函数处理大型结构提供了非常方便的途径,同时对于设计类来说,引用也是必不可少的。

创建引用变量

C和C++使用&符号来只是变量的地址。C++还给&符号赋予了另一个含义,用来申明引用。比如将rodents作为rats变量的别名,可以这样做

cpp 复制代码
int rats;
int& rodents = rats;//将rodents作为rats的别名

其中&不是地址运算符,而是类型标识符的一部分,int&就是一个完整的类型。就像声明中的char*表示指向char的指针一样,int&指的是指向int的引用。上述引用声明允许将rats和rodents互换,因为它们指向相同的值,相同的内存单元。

程序实例1:引用的用法

cpp 复制代码
#include <iostream>

int main()
{
    using namespace std;
    int rats = 101;
    int& rodents = rats;//rodents是一个引用

    cout << "rats = " << rats << endl;
    cout << "rodents = " << rodents << endl;

    rodents++; //rodens自增,也会导致rats自增
    cout << "rats = " << rats << endl;
    cout << "rodents = " << rodents << endl;

    cout << "rats address = " << &rats << endl; //rats变量的地址与rodents的地址是一样的
    cout << "rodents address = " << &rodents << endl;


    return 0;
}

输出结果:

cpp 复制代码
rats = 101
rodents = 101
rats = 102
rodents = 102
rats address = 0x61fe14
rodents address = 0x61fe14

结果分析:rats与rodents的值和地址完全相同的,将rodents加1,rats也加1,。

三、引用的本质

引用在声明的时候必须初始化,从这一点出发,引用更接近const指针,一旦与摸个变量相关联起来,就会一直效忠于它。实际上,int& rodents = rats是语句int* const pr = &rats的伪装表示;rodents扮演的角色与表达式*pr相同。

C++中的引质用本上是一个别名,它提供了一种间接访问已声明变量的方式,就像给一个变量取了一个新的名字。引用一旦初始化,就不能改变其绑定的目标,即不能再赋值为另一个变量。引用具有以下特性:

1、引用必须在声明时初始化,并且一旦初始化,就不能改变引用的对象,除非先解除引用再重新绑定;

2、引用不会占用额外的内存空间,因此可以减少内存开销。

3、引用提高程序的效率,因为通过引用可以直接操作目标对象,避免了复制数据的过程。

四、将引用作为函数参数

引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名,这种传递参数的方法称为引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。C语言只能按值传递,按值传递导致被调用函数使用调用程序的值拷贝,要想避开按值传递,就只能使用指针。

程序实例2:不同方式交换两个变量的值

cpp 复制代码
#include <iostream>

void swapr(int& a,int& b);
void swapp(int* p,int* q);
void swapv(int a,int b);

int main()
{
    using namespace std;
    int wallet1 = 300;
    int wallet2 = 500;
    cout << "wallet1 = $" <<wallet1<< endl;
    cout << "wallet2 = $" <<wallet2<< endl;
    cout <<"\n";

    cout << "Using references to swap contents:\n";
    swapr(wallet1,wallet2);//能正常交换
    cout << "wallet1 = $" <<wallet1<< endl;
    cout << "wallet2 = $" <<wallet2<< endl;
    cout <<"\n";

    cout << "Using pointers to swap contents:\n";
    swapp(&wallet1,&wallet2);//能正常交换
    cout << "wallet1 = $" <<wallet1<< endl;
    cout << "wallet2 = $" <<wallet2<< endl;
    cout <<"\n";

    cout << "Using passing by value to swap contents:\n";
    swapv(wallet1,wallet2); //这种方法将会失败
    cout << "wallet1 = $" <<wallet1<< endl;
    cout << "wallet2 = $" <<wallet2<< endl;
    cout <<"\n";

    return 0;
}



//引用的方式交换两个变量的值
void swapr(int& a,int& b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}


//指针的方式交换两个变量的值
void swapp(int* p,int* q)
{
    int temp;
    temp = *p;
    *p = *q;
    *q = temp;
}

//按值传递的方式交换两个变量的值
void swapv(int a,int b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

输出结果

cpp 复制代码
wallet1 = $300
wallet2 = $500

Using references to swap contents:
wallet1 = $500
wallet2 = $300

Using pointers to swap contents:
wallet1 = $300
wallet2 = $500

Using passing by value to swap contents:
wallet1 = $300
wallet2 = $500

**结果分析:**引用和指针的方法都能正常交换wallet1与wallet2的值,按值传递不能正常交换。

按引用传递 swapr(wallet1,wallet2)和按值传递 swapv(wallet1,wallet2)看起来是一样的,只能通过函数原型或函数定义才知道它们之间的区别。

在函数swapr(int& a,int& b)中,变量a,b是wallet1与wallet2的别名,所以交换a,b的值相当于交换wallet1,wallet2;

在函数swapv(int a,int b)中,变量a,b是复制了wallet1与wallet2的值,所以交换a,b的值不影响wallet1,wallet2的值;

五、引用的属性和特别之处

先看程序实例

程序实例3:

cpp 复制代码
#include <iostream>

double cube(double a);
double refcube(double& ra);

int main()
{
    using namespace std;
    double x = 3.0;

    cout << cube(x) << " cube of " << x << endl;
    cout << refcube(x) << " refcube of " << x << endl;

    return 0;
}



double cube(double a)
{
    a *= a * a;

    return a;
}

double refcube(double& ra)
{
    ra *= ra * ra;

    return ra;
}

输出结果

cpp 复制代码
27 cube of 3
27 refcube of 27

refcube()函数修改了x的值,而cube()函数没有修改x的值。这是提醒我们一般情况下就使用按值传递。变量a位于cube()中,它被初始化为x的值,本质上a和x是不同的东西,修改a,不影响x;但是refcube()函数使用引用参数,ra与x是同一个东西,修改ra就是修改x。如果函数既想使用传递的值,又不对其进行修改,那就得使用常量引用。

六、 常量引用const

程序中经常将引用参数声明位常量数据的引用,有如下理由:

1、使用const可以避免无意中修改数据;

2、使用const使函数能够处理const和非const实参,否则只能接受非const数据;

3、使用const引用使函数能够正确生成并并使用临时变量。

临时变量、引用参数与const

如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这么操作。

编译器在什么条件下产生临时变量:

1、实参的类型正确,但不是左值;

2、实参的类型不正确,但是可以转换成正确的类型;

什么是左值

左值参数是可被引用的数据对象,如变量、数组元素、结构成员、引用、解除引用的指针等;

非左值包括字面常量,和包括多项的表达式;

const变量也可以视为左值,因为可以通过地址访问它们,只是变量的属性是不可修改。

程序实例4:const引用

cpp 复制代码
#include<stdio.h>

int main()
{
	int a = 4;
	const int& b = a; //通过const引用,使变量a拥有只读属性

	int* p = (int*)&b;

	*p = 5; //只读常量不能直接修改值,但是可以通过指针来修改值

	printf("a = %d\n", a);
	printf("b = %d\n", b);

	return 0;
}

输出结果

cpp 复制代码
a = 5
b = 5

当使用常量对const引用进行初始化的时候,C++编译器会给常量只分配空间,并将引用名作为这段空间的别名。使用常量对const引用初始化之后,将生成一个只读变量。

七、引用的存储空间

我们知道引用是一个变量的别名,那它有自己的存储空间吗?

我们知道指针是有自己的存储空间的,引用又跟指针很相似,应该有自己的存储空间吧。

程序实例6:

cpp 复制代码
#include<stdio.h>

struct Test
{
	char& r;
};

int main()
{
	char c = 'c';
	char& rc = c;
	Test ref = { c };

	printf("sizeof(char&) = %d\n", sizeof(char&)); //char类型的引用还是1个字节
	printf("sizeof(rc&) = %d\n", sizeof(rc)); //char引用的变量也是一个字节

	printf("sizeof(Test) = %d\n", sizeof(Test));//test结构体里面只有一个引用
	printf("sizeof(ref.r) = %d\n", sizeof(ref.r));//char类型的引用,是一个字节

	return 0;
}

输出结果:

cpp 复制代码
sizeof(char&) = 1
sizeof(rc&) = 1
sizeof(Test) = 4
sizeof(ref.r) = 1

结果分析:

sizeof(Test) = 4占4个字节,指针也是4个字节。

八、将引用用于结构

实际上,C++引入引用参数,就是为结构体和类服务的,而不是为基本的int char float等内置类型服务。使用结构体引用参数的方式与使用基本变量引用是相同的。

假设有这么个结构体

cpp 复制代码
struct free_throws
{
    string name;    //名字
    int made;       //罚球命中
    int attempts;   //罚球次数
    float percent;  //命中率
};

则可以这样编写函数原型,在函数中将指向该结构体的引用作为参数

cpp 复制代码
void set_pc(free_throws& ft);

如果不希望函数修改传入的结构体,可以这么操作

cpp 复制代码
void display(const free_throws& ft);

程序实例7:结构体引用

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

struct free_throws
{
    string name;    //名字
    int made;       //罚球命中
    int attempts;   //罚球次数
    float percent;  //命中率
};

void display(const free_throws& ft);
void set_pc(free_throws& ft);
free_throws& accumulate(free_throws& target, const free_throws& source);

int main()
{
    //部分初始化
    free_throws one = {"Yao Ming", 13, 14};
    free_throws two = {"Yi Jianlian", 10, 16};
    free_throws three = {"Sun Yue", 7, 9};
    free_throws four = {"Lao wang", 5, 9};
    free_throws five = {"Wu Yifan", 6, 14};
    free_throws team = {"China", 0, 0};

    //不初始化
    free_throws dup;

    set_pc(one); //计算1号球员的命中率
    display(one);//显示1号球员的参数
    cout << '\n';
    
    accumulate(team, one);//把1号球员的信息统计进球队
    display(team);//显示球队的信息
    cout << '\n';

    //use return value as argument
    display(accumulate(team, two)); //把2号球员的信息统计进球队,再显示球队的信息
    cout << '\n';
    
    accumulate(accumulate(team, three), four); //把3号球员的信息统计进球队之后,再把4号球员的信息统计进球队
    display(team);//显示球队信息
    cout << '\n';

    //use return value in assignment
    dup = accumulate(team, five);//把5号球员的信息统计进球队之后,把结果给dup

    cout << "displaying team:\n";
    display(team); //显示球队的信息
    cout << '\n';

    cout << "displaying dup after assignment:\n";
    display(dup);//显示dup的信息
    cout << '\n';
    set_pc(four); //计算4号球员的命中率


    //乱操作,前面把第五个球员的信息录入dup之后,重新将4号球员的信息覆盖掉dup
    accumulate(dup, five) = four; //
    cout << "displaying dup after ill-advised assignment:\n";
    display(dup);

    return 0;
}

//显示球员信息
void display(const free_throws& ft)
{
    cout << "name: " << ft.name << '\n';
    cout << " made: " << ft.made << '\t';
    cout << " attempts: " << ft.attempts << '\t';
    cout << " percent: " << ft.percent << '\n';
}

//计算球员命中率
void set_pc(free_throws& ft)
{
    if(ft.attempts != 0)
    {
        ft.percent = 100.0f * float(ft.made)/float(ft.attempts);
    }
    else
    {
        ft.percent = 0;
    }
}

//计算球队的信息
free_throws& accumulate(free_throws& target, const free_throws& source)
{
    target.attempts += source.attempts;
    target.made += source.made;

    set_pc(target);

    return target;
}

输出结果:

cpp 复制代码
name: Yao Ming
 made: 13        attempts: 14    percent: 92.8571

name: China
 made: 13        attempts: 14    percent: 92.8571

name: China
 made: 23        attempts: 30    percent: 76.6667

name: China
 made: 35        attempts: 48    percent: 72.9167

displaying team:
name: China
 made: 41        attempts: 62    percent: 66.129

displaying dup after assignment:
name: China
 made: 41        attempts: 62    percent: 66.129

displaying dup after ill-advised assignment:
name: Lao wang
 made: 5         attempts: 9     percent: 55.5556

结果分析:

起始代码里面的注释已经写得非常清楚了。

返回引用与传统返回机制的不同

复制代码
函数 free_throws& accumulate(free_throws& target, const free_throws& source),返回的是结构体引用,当然返回结构体也是可以的,函数可以写成这样:
free_throws accumulate(free_throws& target, const free_throws& source);但是效率是不一样的。

传统返回机制与按值传递函数参数类似,计算关键字return后面的表达式,并将结果返回给调用函数。从概念上来讲,这个返回值是先被复制到一个临时的位置,调用函数再从这个临时位置去取这个值。分析下面的代码

cpp 复制代码
double m = sqrt(16.0);

cout << sqrt(25.0);

第一条语句,值4.0被复制到一个临时位置,然后再复制给m;第二条语句,值5.0被复制到一个临时位置,然后再传递给cout;

再回过头来看 dup = accumulate(team, five) ;如果**accumulate()**函数返回的是一个结构体而不是返回引用,那就要把整个结构体复制到一个临时位置,再将这个位置的值拷贝给dup,还好我们示例的结构体不大,如果结构体很大,内容很多,那不仅拷贝需要时间,还要占用一块额外的内存。

九、将引用用于类对象

将类对象传递给函数时,C++通常的做法是使用引用,例如可以通过引用,将类string、ostream、istream、ofstream、ifstream等类的对象作为参数。

下面看一个实例程序,使用string类,并演示不同的设计方案

程序实例8:创建一个函数,将指定的字符串加入到另一个字符串的前面和后面

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

string version1(const string& s1, const string& s2);

const string& version2(string& s1, const string& s2);

const string& version3(string& s1, const string& s2);

int main()
{
    string input;
    string copy;
    string result;

    cout << "enter a string: ";
    getline(cin, input);

    copy = input;

    cout <<"your string as entered: " << input << endl;

    result = version1(input, "***");
    cout << "your string enhanced: " << result << endl;
    cout <<"your original string as entered: " << input << endl;

    result = version2(input, "###");
    cout << "your string enhanced: " << result << endl;
    cout <<"your original string as entered: " << input << endl;

    cout << "resetting original string.\n";
    input = copy;
    result = version2(input, "@@@");
    cout << "your string enhanced: " << result << endl;
    cout <<"your original string as entered: " << input << endl;



    return 0;
}




string version1(const string& s1, const string& s2)
{
    string  temp;
    temp = s2 + s1 + s2;

    return temp;
}


//会有边际效应
const string& version2(string& s1, const string& s2)
{
    s1 = s2 + s1 + s2;

    return s1;
}

//垃圾设计
const string& version3(string& s1, const string& s2)
{
    string temp;
    temp = s2 + s1 + s2;

    return temp; //返回局部变量的引用
}

返回结果

cpp 复制代码
enter a string: hello world
your string as entered: hello world
your string enhanced: ***hello world***
your original string as entered: hello world
your string enhanced: ###hello world###
your original string as entered: ###hello world###
resetting original string.
your string enhanced: @@@hello world@@@
your original string as entered: @@@hello world@@@

结果分析:

程序中三个函数,version1()函数最简单,它接受两个string参数,并使用string类的相加功能,来创建一个满足要求的新字符串。这两个函数参数都是const引用,虽然不使用 引用,而是直接使用string参数也能满足要求,但是我们知道使用引用更高效,因为不需要创建新的string对象,不需要复制。引用参数用const,就是让函数不改变原有的参数。

const string& version2(string& s1, const string& s2) 函数不创建临时string对象temp,而是直接修改原来的string,该函数可以修改s1,不能修改s2,但是s1指向input的引用,该函数会修改input的原始值,这是一种错误的做法;

version3()返回一个指向函数内声明的变量的引用,函数内的变量,用完就不存在了,返回它的引用是一个危险的做法,大错特错,会导致程序崩溃。虽然有的强大的编译器没有报错,还输出的符合预期的结果。

相关推荐
曙曙学编程几秒前
初级数据结构——栈
数据结构
ROBIN__dyc2 分钟前
数组
算法
严文文-Chris7 分钟前
【B+树特点】
数据结构·b树
严文文-Chris10 分钟前
B-树特点以及插入、删除数据过程
数据结构·b树
湖南罗泽南19 分钟前
Windows C++ TCP/IP 两台电脑上互相传输字符串数据
c++·windows·tcp/ip
欧阳枫落26 分钟前
python 2小时学会八股文-数据结构
开发语言·数据结构·python
手握风云-39 分钟前
零基础Java第十六期:抽象类接口(二)
数据结构·算法
可均可可1 小时前
C++之OpenCV入门到提高005:005 图像操作
c++·图像处理·opencv·图像操作
zyx没烦恼1 小时前
【STL】set,multiset,map,multimap的介绍以及使用
开发语言·c++
机器视觉知识推荐、就业指导1 小时前
基于Qt/C++与OpenCV库 实现基于海康相机的图像采集和显示系统(工程源码可联系博主索要)
c++·qt·opencv