1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > 【C++引用超详细笔记 引用 指针和值传递的汇编级辨析 对象 继承和引用】

【C++引用超详细笔记 引用 指针和值传递的汇编级辨析 对象 继承和引用】

时间:2024-03-09 10:19:19

相关推荐

【C++引用超详细笔记  引用 指针和值传递的汇编级辨析 对象 继承和引用】

文章目录

引用变量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; // 20cout << "b= "<<b << endl; // 20b = 100;cout << "a= " << a << endl; //100cout << "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<iostream>//值传递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; //10cout << "b = " << b << endl; //20a = 10;b = 20;mySwap02(&a, &b);//地址传递的形参可以改变实参cout << "a = " << a << endl; //20cout << "b = " << b << endl; //10a = 10;b = 20;mySwap03(a, b);//引用时形参可以改变实参cout << "a = " << a << endl; //20cout << "b = " << b << endl; //10return 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.0double x = 10; double z = refcube(x + 3.0); //错误 不能double &ra = x + 3.0double 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不能指向longdouble 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.0std::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 <iostream>#include <string>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)将被设置为0free_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 argumentdisplay(accumulate(team, two)); //返回引用的函数实际上是返回这个结构的别名,这个别名还可以作为左值,供左值引用来使用//相当于这两句:// accumulate(team, two) 返回team对象// display(team); ft指向teamaccumulate(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);elseft.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);//接收也要加一个constaccumulate(dup, five) = four; //报错

2.5 将引用用于类对象

1. 使用引用方式将类的对象作为函数参数

#include<iostream>#include<string>using namespace std;//使用引用方式将类的对象作为函数参数string version1(const string& s1, const string& s2);const string& version2(string& s1, const string& s2); //has side effectconst string& version3(string& s1, const string& s2); //bad designint main(){string input;string result;string copy; //副本,后面用来恢复inputcout << "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 <iostream> //和控制台有关#include <fstream> //和文件有关#include <cstdlib>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这个类定义一个对象foutconst char* fn = "ep-data.txt";fout.open(fn); //用open方法打开文档if (!fout.is_open()){cout << "Can't open " << fn << ". Bye." << endl;exit(EXIT_FAILURE); //这个宏在#include <cstdlib>}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;leaeax, [a] movdword ptr[p], eax取[a]的单元的地址,经寄存器eax中转,放入[p]单元中指针变量解引用,即用*p来代表a单元,*p = 4;moveax, dword ptr[p] 从指针变量的内存单元中获取内存单元a(变量)的地址movdword ptr[eax], 4 寄存器间接寻址在找到[a]这个内存单元,进行赋值变量赋值a = 5;movdword 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], eaxint* 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], 5a = b;// mov eax, dword ptr[b]// mov dword ptr[a], eaxp = &b;//给指针改变指向// lea eax, [b]// mov dword ptr[p], eax int t = b; //常规变量赋值// mov eax, dword ptr[b]// mov dword ptr[t], eaxint* const r = &a;//指针常量初始化// lea eax, [a]// mov dword ptr[r], eaxq = 6;// mov eax, dword ptr[q]// mov dword ptr[eax], 6

4 什么时候使用引用参数?

引用:

形参可以修改实参本质是个常量指针,直接指向参数内存,提高程序运行速度,节省内存空间。

对于传递的值不修改的函数

数据将对象很小(内置类型或小型结构)———— 值传递数组 ———— 使用const指针数据对象较大 ———— const指针或const引用类对象 ———— const引用 ,传递类对象的标准方式是按引用传递

修改传递的值的函数

内置数据类型 ———— 指针数组 ———— 只能指针结构 ———— 引用或指针类对象 ———— 引用

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。