- 引用变量
- 1. 什么是引用、如何创建一个引用变量?
- 2. 引用的具体使用方法
- 2.1 引用做函数的形参
- 2.2 引用参数的const用法
- 左值、右值是什么?
- 2.3 结构体的引用
- 2.4 引用做函数返回值
- 2.5 将引用用于类对象
- 3 引用的本质是什么? 引用,指针和值传递的辨析
- 4 什么时候使用引用参数?
参考文献:
C++ primer plus
对象、继承和引用
C++的左值和右值
黑马程序员:引用的本质 引用变量 1. 什么是引用、如何创建一个引用变量?
要搞清楚什么是引用,首先我们要清楚变编程语言的变量名或者叫标识符究竟是什么,比如int a; int *p; 这里面的a或者p,究竟是个什么呢? ———— 这些标识符本质上就是用来代指一个内存单元的.在汇编语言中,比如[0x1101]这就是一个内存单元,里面的0x1101是这个内存单元的地址或者说编号,外面的方括号用来表明直接寻址方式,即表示这个地址所代表的的内存单元。
而用一长串地址加一个方括号表示内存单元太过麻烦,我们就给这个[0x1101]这个内存单元起了一个好听的名字a,这个符号a经过编译器翻译和分配内存单元之后,所形成的的汇编指令,就被翻译成[0x1101]了,我们可以叫这个内存单元a,当然我们也可以叫它b或者dog随便什么名字都可以,一个内存单元当然可以拥有多个名字,至于叫什么其实无所谓,因为指的都是这个内存单元。这个小名b就是引用啦。
int a = 0;
int &b = a;
这个b就是给a这个内存起了个小名b而已,并没有什么特别的。
&在C++中有两种用法,一种是引用,一种是取地址。
同样,指针int *p = &a; 中的p,所代表的也不过是一个普通的内存单元,我们就叫它[0x1102],
1、 我们&p ,也就是取这个内存单元的地址,也就是0x1102这个地址值。
2、我们输出p ,得到的就是这个内存单元里的内容,也就是a的地址呗,就是0x1101
3、 我们输出*p,这是解引用 *** 作,得到的就是a所代表的内存单元存的的内容,或者说是指针p指向的内存单元的内容,就是0
那么我们既然知道了变量a引用变量b指的都是同一个内存单元[0x1101],那呢我们无论或者a或者b中的任何一个进行 *** 作,[0x1101]这个内存单元都会发生相应的改变。
2.引用的注意事项:
1、引用必须初始化: int &b; b = a;是错误的,必须是int &b = a,引用在初始化后,不可以改变:
2、一个别名不允许更改成另一个变量的别名,起别名后,别名只能代表一块内存,不能改变成另一块内存
示例代码
//1. 引用的基本语法
int a = 10;
int &b = a; //给a对应的内存起个别名b,用b也可以 *** 作这款块内存
b = 20;
cout <<"a= "<< a << endl; // 20
cout << "b= "<<b << endl; // 20
b = 100;
cout << "a= " << a << endl; //100
cout << "b= " << b << endl; //100
//2 引用的注意事项:必须初始化,不可改变内存的指向
int c = 10;
int &d = c;
int g = 20;
d = g; //赋值从 *** 作,而不是更改引用
// int& d = g; //这也不是更改引用,这是多次初始化了,也是错的,别名就不能更改
cout << "c= " << c << endl; //c和d的内容一样,因为这就是一块相同的内存区域
cout << "d= " << d << endl;
cout << "g= " << g << endl;
2. 引用的具体使用方法
2.1 引用做函数的形参
函数形参的形式一般分为三种:
- 值传递方式:形参不能改变实参,形参和实参相当于是两块独立的内存,形参内存区数值的变化对于实参的内存单元中的数值是没有影响的。
- 地址传递方式:即指针方式,通过另外一块内存单元(即指针变量)来指向实参的内存区,通过改变这个指针的指向,来直接对实参内存区中不同的单元进行 *** 作。
所以指针做函数形参是可以改变实参的数值的。
- 引用方式:引用做函数形参时,就是相当于对这个实参的内存单元起了一个别名,实际上直接 *** 作的就是实参的内存单元区域,所以引用方式也可以改变函数实参。
以常见的交换函数为例来说明三种形参的差别
#include
//值传递
oid mySwap01(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
//地址传递
oid mySwap02(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//引用传递
oid mySwap03(int &a, int &b) //相当于 int &a_2 = a; 给实参a起了个别名形参a, 形参a *** 作的还是实参a所代表的的那块内存单元,所以可以形参改变实参
{
int temp = a;
a = b;
b = temp;
}
int main(){
using namespace std;
int a = 10;
int b = 20;
mySwap01(a, b); //值传递的方式形参不会改变实参
cout << "a = " << a << endl; //10
cout << "b = " << b << endl; //20
a = 10;
b = 20;
mySwap02(&a, &b); //地址传递的形参可以改变实参
cout << "a = " << a << endl; //20
cout << "b = " << b << endl; //10
a = 10;
b = 20;
mySwap03(a, b); //引用时形参可以改变实参
cout << "a = " << a << endl; //20
cout << "b = " << b << endl; //10
return 0;
}
引用做形参的注意事项
值传递时,实参时允许传入一个常数或者表达式的,比如
double z = cube(x + 2.0);
z = cube(8.0);
z = cube(k);
但是传递引用的限制更严格,实参只能是变量名,而不能是一个常数或者表达式,
如下都是错误的
double refcube(double &ra)
{
ra = ra*ra;
return ra;
}
int main()
{
double z = refcube(3.0); //错误 不能double &ra = 3.0
double x = 10;
double z = refcube(x + 3.0); //错误 不能double &ra = x + 3.0
double z = refcube(x); //正确 ,相当于 double &ra = x
}
2.2 引用参数的const用法
常量引用: const
作用:
- 常量引用用来修饰形参,防止误 *** 作,
- 在函数形参列表中,可以加const修饰形参,防止形参改变实参
注意:仅当参数为const引用时,如果实参与引用参数不匹配,C++将生成临时变量,可以传递参数的种类更丰富一些,而常规变量引用是不允许的。
double refcube(const double &ra)
{
return ra*ra*ra;
}
double side = 3.0;
double *pd = &side;
double &rd = side;
long edge = 5l;
double lens[4] = {2.0, 5.0, 10.0, 12.0};
//side,len[2],rd,*pd都是有名称的1,double类型的数据对象,因此可以欸其创建引用,而不需要临时变量
double c1 = refcube(side);
double c2 = refcube(lens[2]);
double c3 = refcube(rd);
double c4 = refcube(*pd);
//以下三种i情况,数据类型问题或者是表示式、常数,没有名称,都是需要创建临时变量,但不会报错,与常规变量引用不同。
//edge虽然是变量,但是类型却不正确,double不能指向long
double c5 = refcube(edge);
//7.0和side+10.0都是double类型的数据,但是没有名称
double c6 = refcube(7.0);
double c7 = refcube(side+10.0);
一般来说const int &a = 10; 有问题,
但编译器会这样处理:
int tenmp = 10;
int &a = temp ;
这样就正确了
需要注意的是编译器只会对const引用做临时变量处理,而常规变量引用则不会
究其原因是因为常规引用中形参是要改变实参,而创建临时变量则会阻止这种改变,引起错误,所以方法就是直接禁止创建临时变量。
而对于const引用目的则是不想改变实参,只是想用引用方便一些,不需要像值传递时需要复制很大一块空间,所以加个const不允许改变参数值,如果不小心改变了,编译器会报错提醒程序员。所以这个时候允许创建临时变量,使用起来可以更方便,传入参数类型限制可以不用那么严格。
使用const的好处
- 可以避免无意修改数据的编程错误
- 能处理左值数据和右值数据,否则只能接受左值数据
- 能够使函数正确生成并使用临时变量
左值:左值是一个可被引用的数据对象,例如变量、数组元素、结构成员、引用和解除引用的指针都是左值。
(实际占用内存空间的数据),对cpu来说对左值可以进行读和写 *** 作。
(可以写在左边或右边)
右值:包括常量和包含多项的表达式(用引号扩起的字符串除外,他们由地址表示)。基本不占用内存,一般在寄存器里或者是立即数(汇编概念),只能进行读 *** 作。
常规变量和const变量都可视为左值,因为可通过地址访问他们,但常规变量属于可修改的左值,而const变量属于不可修改的左值。
左值引用(使用&声明)是指向左值的,加了const之后也可以指向右值(实际上是创建了个临时变量)
C++11新增了右值引用,是用来指向右值的,用&&声明
double && rref = std::sqrt(36.00); //左值引用就不允许这样做
double j = 15.0;
double && jref = 2.0*j+18.5; //右值引用
std::cout << rref <<'\n'; //6.0
std::cout << jref <<'\n'; //48.5;
2.3 结构体的引用
引用非常适用于结构和类,引入引用机制主要就是为了用于这些类型的,而不是基本的内置类型。
使用方法与使用基本变量引用相同
struct free_throws
{
std::string name;
int made;
int attempts;
float persent;
};
//修改传入的结构
void set_pc(free_throws &ft);
//或者是不修改传入的结构
void set_pc( const free_throws &ft);
2.4 引用做函数返回值
- 作用:引用是可以作为函数的返回值存在的,返回引用的函数实际上是被引用变量的一个别名
free_throws& accumulate(free_throws& target, const free_throws& source)
{
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
函数accumulate(a,b) 实际上就是a这个地址空间的一个别名,可以用来作为左值
- 用法:函数的调用可以作为左值
display(accumulate(team, two));
accumulate(accumulate(team, three), four);
dup = accumulate(team, five);
free_throws &a = accumulate(team, five);
accumulate(dup, five) = four;
// 这条语句更详细的内容见下面的,const用于返回值类型
- 注意:不要返回局部变量引用
//不要返回局部变量引用
int& test01()
{
int a = 10; //局部变量存放在栈区,出了函数之后就被编译器释放负掉了
return a;
}
//返回静态变量引用
int& test02()
{
static int a = 10; //静态变量存放在全局区,整个程序运行结束之后才由 *** 作系统释放
return a;
}
int main()
{
int &ref = test01();
cout << "ref = " <<ref<< endl; //第一次结果正确,是因为编译器做了一次保留
cout << "ref = " << ref<<endl; //第二次结果错误,是因为a的内存已经释放
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl; //两次都正确,因为这块内存一直没有释放
cout << "ref2 = " << ref2 << endl;
return 0;
}
解决方法:
- 函数返回引用时,返回一个传入函数的参数。
形参引用将指向实参数据区,返回引用也可以指向这个实参数据区,不去出现问题(比如上面accumulate( , )采取的方法)
- 用new来分配新的存储空间,
free_throws& clone2(free_throws& ft)
{
free_throws newguy;
newguy = ft;
return newguy; //返回的是一个局部变量,在栈区,函数结束后这段内存空间就被释放了
}
free_throws& clone(free_throws& ft)
{
free_throws* pt = new free_throws{}; //使用new分配新的存储空间,由程序员自己释放,用一个指针指向这个空间
*pt = ft; //向这个空间写入数据
return *pt; //相当于 free_throws &clone(free_throws& ft) = *pt,返回这个*pt所代表的空间的一个引用 即clone(one)就是这个空间的第一个单元一个别名
}
int main()
{
free_throws& jolly = clone(one);
display(jolly); //每次显示结果都一样,因为堆空间是由程序员自己释放的。
display(jolly);
display(jolly);
return 0;
}
但是这个方法也有一点问题,就是调用clone()函数时看不到new,程序员可能会忘了delete这段内存空间。
结构与返回的引用变量应用示例
//strc_ref.cpp -- using structure references
#include
#include
struct free_throws
{
std::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()
{
//初始化结构体成员变量,指定的初始值(3个)比成员数(4个)少,剩下的成员(precent)将被设置为0
free_throws one = { "Ifelsa Branch", 13, 14 };
free_throws two = { "Andor Knott", 10, 16 };
free_throws three = { "Minnie Max", 7, 9 };
free_throws four = { "Whily Looper", 5, 9 };
free_throws five = { "Long Long", 6, 14 };
free_throws team = { "Throwgoods", 0, 0 };
free_throws dup;
set_pc(one);
display(one);
accumulate(team, one);
display(team);
// use return value as argument
display(accumulate(team, two)); //返回引用的函数实际上是返回这个结构的别名,这个别名还可以作为左值,供左值引用来使用
//相当于这两句:
// accumulate(team, two) 返回team对象
// display(team); ft指向team
accumulate(accumulate(team, three), four);
display(team);
//这里是赋值 *** 作,相当于将team结构复制了一份赋值给dup (发生了拷贝)
dup = accumulate(team, five);
std::cout << "Displaying team:\n";
display(team);
std::cout << "Displaying dup after assignment:\n";
display(dup);
set_pc(four);
//下面这条语句如果是按值返回是不能写在左边的,因为返回的是一个右值
//但是用返回引用的方式就可以了,因为返回值是一个左值,可以对这个左值进行写 *** 作(赋值)
accumulate(dup, five) = four;
//上面的两条语句就相当于:
//accumulate(dup, five); dup = dup + five
// dup = five; dup = five
// 其中第二条语句消除了第一条语句的工作,所以这种方法虽然能通过编译,但是不太好。
std::cout << "Displaying dup after ill-advised assignment:\n";
display(dup);
// std::cin.get();
return 0;
}
void display(const free_throws& ft) //形参为结构体常量引用,不能修改实参 ,只是用来打印结构体所有成员变量
{
using std::cout;
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可修改 , source不可修改 ,返回target的引用
{
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
- const应用于返回类型
accumulate(dup, five) = four;
这条语句是可以通过编译的,因为函数返回指向dup的引用,其实是标识了dup这个内存块的,它是由地址的实实在在的一块内存空间的别名,是可以作为左值的。
而常规的函数返回类型返回的是右值,即不能通过地址访问的值,(比如 10 这种常数或者 x + y这种表达式),这种返回值只是位于临时的内存单元中,运行到下一条语句时,他们可能就不存在了
但是很明显,这里的赋值 *** 作four ,覆盖了函数 *** 作的dup空间,所以当你想想要使用引用,又不允许给返回的左值进行赋值 *** 作时,可以使用const修饰
const free_throws & accumulate(free_throws& target, const free_throws& source);
相当于返回一个不可更改的左值(这块内存只能读不能写)。
使用const之后,
display(accumulate(team, two));
//这里仍然可以使用,因为display()的形参也是const free_throws & 类型
accumulate(accumulate(team, three), four);
//这里会报错,因为第一个参数是非const类型
//但这里影响不大
accumulate(team, three);
accumulate(team, four);
//这样就可以了
dup = accumulate(team, five); //从内存读数据没问题
const free_throws &a = accumulate(team, five);
//接收也要加一个const
accumulate(dup, five) = four; //报错
2.5 将引用用于类对象
1. 使用引用方式将类的对象作为函数参数
#include
#include
using namespace std;
//使用引用方式将类的对象作为函数参数
string version1(const string& s1, const string& s2);
const string& version2(string& s1, const string& s2); //has side effect
const string& version3(string& s1, const string& s2); //bad design
int main()
{
string input;
string result;
string copy; //副本,后面用来恢复input
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 string original:" << input << endl; //值传递原始内容不变
result = version2(input, "###");
cout << "Your string enhanced: " << result << endl;
cout << "Your string original:" << input << endl; //引用传递原始内容改变了
//恢复input的内容
cout << "Reseting original string\n";
input = copy;
result = version3(input, "@@@"); //这里程序会崩溃,这块内存都没了,还返回啥
cout << "Your string enhanced: " << result << endl;
cout << "Your string original:" << input << endl;
return 0;
}
string version1(const string& s1, const string& s2) //有const时 ,"***"是char*类型,和引用类型不匹配时,程序能够创建一个正确类型的临时变量,使其能正确传参
{
string temp;
temp = s2 + s1 + s2;
return temp; //这里的temp s1 s2都是一个string类的对象 ,返回一个string类的对象
}
const string& version2(string& s1, const string& s2)
{
s1 = s2 + s1 + s2; //这里string类对象s1是可以修改的
return s1; //返回一个不可修改的string类的对象s1的引用
}
const string& version3(string& s1, const string& s2)
{
string temp;
temp = s2 + s1 + s2;
return temp; //!!!注意这里返回了一个局部变量的引用,函数运行结束之后,temp这块内存就没有了,所以不能发这么写。
}
2. 在函数里,通过基类的引用,来指向派生类的对象,而无需强制类型转换
//filefunc.cpp -- function with ostream & parameter
/*
这个程序要求用户输入望远镜的物镜和目镜的焦距,然后计算每个目镜的放大倍数,放大倍数等于物镜的焦距除以目镜的焦距
*/
#include //和控制台有关
#include //和文件有关
#include
using namespace std;
//在函数里,通过基类的引用,来指向派生类的对象,而无需强制类型转换
void file_it(ostream& os, double fo, const double fe[], int n);
//定义的这个基类的对象的引用ostream& os
//由于ostream是基类,而ofstream是派生类,
// 1. 派生类继承了基类的方法,派生类可以使用基类的特性
// 2. 基类的引用也可以指向派生类的对象!!!!!!!!!!
//这里的意思就是基类引用形参ostream & os ,这里传递进去一个派生类ofstream创立的对象fout作为函数参数也是可以的
const int LIMIT = 5;
int main()
{
fstream fout; //用fstream这个类定义一个对象fout
const char* fn = "ep-data.txt";
fout.open(fn); //用open方法打开文档
if (!fout.is_open())
{
cout << "Can't open " << fn << ". Bye." << endl;
exit(EXIT_FAILURE); //这个宏在#include
}
double objective;
cout << "Enter the focal length of your telescope objective in mm:"; //输入物镜焦距
cin >> objective;
double eps[LIMIT];
for (int i = 0; i < LIMIT; i++) //目镜有好几个呢
{
cout << "EyePieces #" << i + 1 << ": ";
cin >> eps[i];
}
file_it(cout,objective,eps,LIMIT); //cout是终端控制台的对象,终端输出显示
file_it(fout, objective, eps, LIMIT); //fout是文件的一个对象,文件输出显示 ,这里派生类对象fout也可以作为形参传入基类引用中
cout << "Done." << endl;
return 0;
}
void file_it(ostream& os, double fo, const double fe[], int n) //ostream类对象 , 物镜焦距,多个目镜焦距的数组,目镜个数
{
os << "Focal length of objective: " << fo << endl; //输出物镜倍数
os << "f.1. eyepieces" << " magnification" << endl; //输出目镜倍数
for (int i = 0; i < n; i++)
{
os <<" "<< fe[i] << " \t " << int(fo / fe[i] + 0.5) << endl; //放大倍数取整
}
}
3 引用的本质是什么? 引用,指针和值传递的辨析
/普通变量、指针和引用的汇编实现*/
/*
总结:C++中的变量名,不论是int a 的a,还是int* p 的p,其本质都是一个计算机分配的内存单元的标识符,用来指代某个特定的内存单元
变量名本是是一个数值,并不占用内控空间来存储这个数值,而是直接在汇编指令中编译为 [a] 或[1122H],用直接寻址的方式来代表某一块内存
int *p = &a; 指针则是一个实际的内存单元,这个内存单元中存放了[a]这个内存单元的地址值,也就是变量名a被翻译成汇编指令后对应的数值
指针变量名p也是一个数值,被汇编程序翻译成指针变量的内存单元的地址值,[p]这个单元就是指针变量
[p]直接寻址,可以找到指针变量,[p]单元中存放了[a]这个单元的地址,也就是变量名a的数值
指针变量初始化的过程:
int *p = &a;
lea eax, [a]
mov dword ptr[p], eax
取[a]的单元的地址,经寄存器eax中转,放入[p]单元中
指针变量解引用,即用*p来代表a单元,
*p = 4;
mov eax, dword ptr[p] 从指针变量的内存单元中获取内存单元a(变量)的地址
mov dword ptr[eax], 4 寄存器间接寻址在找到[a]这个内存单元,进行赋值
变量赋值
a = 5;
mov dword ptr[a] a直接就代表一块特定的内存单元的地址,这里用的是直接寻址方式
引用
int &q = a;
lea eax, [a]
mov dword ptr[q], eax
指针常量
int* const r = &a;
lea eax, [a]
mov dword ptr[r], eax
可以见到,,引用的本质在汇编层面就是一个指针常量,他也是一个指针,只不过是一个只能指向[a]内存单元的指针,他的指向不可以改变,
所以可以避免指针指向其他位置造成混乱,[q] 和[a]是一个东西,q = a都是那个内存单元的地址的数值,两者是相等的
引用就是一个功能简化后的指针,用起来更方便。
或者直接当成变量的别名即可,一码事。
int* p = &a; // 指针初始化
// lea eax, [a] &a *** 作就是将a的地址放入寄存器中
// mov dword ptr[p], eax 在将这个地址放到指针变量的内存单元中
int& q = a; //引用初始化
// lea eax, [a]
// mov dword ptr[q], eax
int* s; //这个指针变量的声明没有赋值,所以此时海内有开辟对应的指针变量的内存空间,没有相应的汇编指令
s = &a;
// lea eax, [a]
// mov dword ptr[s], eax
*p = 4; //给指针指向的内存单元赋值
// mov eax, dword ptr[p] *p解引用: 将指针变量的内存单元存放的内容(即变量a的地址)送到寄存器中,解引用就是获取指针变量中存放的地址
// mov dword ptr[eax], 4 再采用寄存器间接寻址的方式将4放入变量a的内存单元中,先*p,再赋值,将4送到这个指针指向的内存单元中区
a = 5;
// mov dword ptr[a], 5
a = b;
// mov eax, dword ptr[b]
// mov dword ptr[a], eax
p = &b; //给指针改变指向
// lea eax, [b]
// mov dword ptr[p], eax
int t = b; //常规变量赋值
// mov eax, dword ptr[b]
// mov dword ptr[t], eax
int* const r = &a; //指针常量初始化
// lea eax, [a]
// mov dword ptr[r], eax
q = 6;
// mov eax, dword ptr[q]
// mov dword ptr[eax], 6
4 什么时候使用引用参数?
引用:
- 形参可以修改实参
- 本质是个常量指针,直接指向参数内存,提高程序运行速度,节省内存空间。
对于传递的值不修改的函数
- 数据将对象
- 很小(内置类型或小型结构)———— 值传递
- 数组 ———— 使用const指针
- 数据对象较大 ———— const指针或const引用
- 类对象 ———— const引用 ,传递类对象的标准方式是按引用传递
修改传递的值的函数
- 内置数据类型 ———— 指针
- 数组 ———— 只能指针
- 结构 ———— 引用或指针
- 类对象 ———— 引用
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)