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