您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息
免费发信息

c++引用类型(c++引用的定义)

2024/4/25 23:15:35发布6次查看
本文主要介绍c引用类型(c引用的定义),下面一起看看c引用类型(c引用的定义)相关资讯。
-
引用、继承和派生引用的介绍首先要知道,参数的传递本质上是一个赋值的过程,赋值就是复制记忆。所谓内存复制是指将数据从一个内存复制到另一个内存,对于聚合类型(复杂类型,如结构和类)可能会消耗大量内存。
一个引用可以看作是一个数据的别名,通过这个别名和原来的名字(指向同一个内存)可以找到这个数据。
注意:
引用必须在定义的同时初始化,以后会一直使用,不能引用其他数据。这有点类似于常量(const variable)引用,定义的时候需要添加,使用的时候不能添加。使用的时候,加法就是取地址int a = 99int r = a;cout a , r endl一般来说,在c中,引用作为函数参数来代替指针的作用,同样可以改变数据内容,非常实用。
同时,在c中,引用可以作为函数返回值,但是!!!对本地数据(如本地变量、本地对象、本地数组等)的引用。)无法返回,因为函数调用完成后本地数据会被破坏,下次数据可能就不存在了。
int plus 10(int r){ int m = r 10;返回m;//返回对本地数据的引用}//对于某些编译器,会报错int num 3 = plus 10(num 1);int num 4 = plus 10(num 3);//但是有些编译器是可以运行的,比如gcc,但是num3和num4的值是一样的,因为函数是在栈上运行的,运行之后会放弃所有本地数据的管理权,后面的函数调用会覆盖前面函数的本地数据,这两点会改成最后一个值;语录的精髓:
其实引用只是指针的简单封装,它的底层还是通过指针来实现的。引用占用的内存和指针占用的内存是一样的,32位环境下是4字节,64位环境下是8字节。之所以无法获得引用的地址,是因为编译器进行了内部转换:
int a = 99int r = a;r = 18coutrendl//在编译时,会转换成如下形式:int a = 99int * r = a;* r = 18coutrendl当r取地址时,编译器会隐式转换代码,使代码输出r的内容(a的地址)而不是r的地址,这也是无法获取引用的原因。变量地址的原因。也就是说,不是变量r不占用内存,而是编译器不允许它获取自己的地址。
指针和引用之间的其他区别:
引用必须在定义的时候初始化,以后也一直如此,不能再指向其他数据;指针不 我没有这个限制。指针不 它们在定义时不必赋值,将来可以指向任意数据。
你可以有常量指针,但是没有常量引用,r可以 不要改变重点,添加常量是不必要的。
指针可以有多级,引用只能有一级(如果你学了引用折叠,你可以想想它是不是正确的)。比如int **p是合法的,int r是非法的(c 11加了右值引用,是合法的),下面这个就可以了。
int a = 10int r = a;int rr = r;//指向a的两个地址指针与引用的递增和递减(-)操作的含义不同。使用指针表示它指向下一个数据,使用引用表示它所引用的数据本身加1。
引用通常不能绑定临时数据:
指针和引用只能指向内存,不能指向寄存器或硬盘,因为寄存器和硬盘是不能寻址的。
定义的变量、创建的对象、字符串常量、函数参数、函数体本身、new或malloc分配的内存等。,所有这些都可以用来获取地址。
什么数据可以 t被使用,它将被记录在:
基本类型的数据,如int、double、bool、char,通常小于8个字节,可以存放在一两个寄存器中,所以这些类型的临时数据通常放在寄存器中;但是,对象和结构变量是用户定义的数据类型,它们的大小是不可预测的,所以这些类型的临时数据通常放在内存中。int * p2 =(n 100);//不会,n 100会在寄存器里,常量表达式也会在寄存器里;s1 = {23,45 };s s2 = {90,75 };s * p1 =(s1 s2);//在visual c中是,s1 s2在内存中,但是!!!!gcc can t,因为gcc可以 不要引用任何临时变量!!!bool iso dd(int n){ if(n % 2 = = 0){ return false;}else{返回true} } iso dd(a);//更正iso dd(a9);//错误,有时它 向它传递临时数据很容易。去看看吧
常量引用绑定临时数据:
常量引用:编译器会为临时数据创建一个新的,没有名字的临时变量,把临时数据放到临时变量里,然后把引用绑定到临时变量上。
把它改成常规引用就行了,因为为常规引用创建临时变量是没有意义的。创建和修改的临时变量只是临时变量中的数据,不会影响原始数据,意义不大。对于频繁引用,我们只能通过const引用读取数据的值,但是可以 t修改它的值,所以我们不 不用考虑同步更新的问题,我们不会 t产生两个不同的数据。
bool isodd(const int n){ //更改为频繁引用if(n/2 = = 0){ r《整数在内存中是如何存储的》《小数在内存中是如何存储的》);因为引用的本质也是指针,所以引用的类型转换也是错误的。
int n = 100int * p1 = n;//正确的float * p2 = n;//error int r1 = n;//修正浮点r2 = n;//错了但是!!!通过添加频繁引用可以进行类型转换。
原则:当引用和数据的类型不一致时,如果它们的类型相似,则 数据类型的自动转换,那么编译器会创建一个临时变量,将数据赋给这个临时变量(此时会发生自动类型转换),然后将引用绑定到这个临时变量,这与 绑定对临时数据的常量引用。
int n = 100int r1 = n;//正确的常量float r2 = n;//正确总结:如果函数不要求改变引用值,函数参数应该使用const尽可能;第一,避免临时数据;第二,避免不同类型;第三,常量和非常量参数都是可以接受的;
double volume(const double len,const double width,const double hei){ return len * width * 2 len * hei * 2 width * hei * 2;}double v4 = volume(a 12.5,b 23.4,16.78);double v5 =音量(a b,a c,b c);继承和派生继承介绍了被继承的类称为父类或基类,被继承的类称为子类或派生类。 子类 和 父类和通常被召集在一起,而 基类和和 派生类 通常被召集在一起。意思是一样的。
什么时候?等待继承:类之间必须有很大的相关性,有很多共同的成员函数和成员变量。
继承的成员可以通过子类对象访问,就像它们自己的一样。
继承格式:
类派生类名称:[继承方法]基类名称{派生类新添加的成员};
三种继承
公共继承模式
基类中的公共成员-派生类中成为基类中公共属性的受保护成员-派生类中的私有成员或基类中的受保护属性-不能在派生类中使用的不可见的受保护继承方法。
基类中的公共成员——派生类中的受保护成员成为基类中的受保护成员——派生类中的私有成员或基类中的受保护成员——派生类中不能使用的不可见私有继承方法。
在基类中,public\protected-成为派生类中的私有属性。在派生类中,只有基类中的私有成员才能在类中使用——不能在派生类中使用,不可见的protected属性只能在派生类(类代码)中访问;没别的;
不难发现:
1)继承模式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限,不能超过,即即使基类是公共成员属性,派生类采用受保护的继承,公共成员属性也只能变成受保护的;
2)基类中的私有成员永远不能在派生类中使用,但是可以通过public的set和get函数使用(访问派生类中私有成员的唯一方法是使用基类的非私有成员函数)。
3)如果希望基类的成员被派生类继承,无障碍使用,那么这些成员只能声明为public或者protected。
4)如果您希望基类的成员不公开(不能通过对象访问),但也在派生类中使用,则只能将它们声明为protected。
注意,我们这里说的是基类的私有成员不能在派生类中使用,而不是基类的私有成员不能被继承。其实基类的私有成员是可以继承的,(成员变量)会占用派生类对象的内存,但是在派生类中是不可见的,这就使得它不可用。
若要更改访问权限,请使用using关键字。
使用只能更改基类中公共和受保护成员的访问权限,而不能更改私有成员的访问权限。因为基类中的私有成员在派生类中是不可见的,不能被写入;
//基类people class people {public: void show;protect: char * m _ name;int m _ age};虚空人: :秀{ cout m _ nam:公人{public: void learning;public:使用people ::m _ name;//将protected改为public使用人: : _ age;//将protected改为public float m _ scor:用people :: show;//将public改为private };继承时的名称屏蔽
对于函数来说,无论函数的参数是什么,只要名称相同,就会被遮挡,不会出现重载。如果要调用,使用域名和域解析器;
对于成员变量,只要名称相同,派生类就会覆盖基类,但是基类的成员变量同时存在。此时,从基类继承的get和set方法都是对基类中同名变量的操作,而不是对派生类中同名变量的操作。
类继承的作用域嵌套和对象内存模型假设base是基类,derived是派生类,那么它们的作用域嵌套关系将如下:
编译器将从内向外查找以下类的范围:
通过obj (c类对象c)访问成员变量n时,可以在c类的作用域中找到n这个名字,虽然a类和b类都有n这个名字,但是编译器不会在它们的作用域中查找,所以是不可见的,也就是派生类中的n遮住了基类中的n。
通过obj访问成员函数func时,在c类的作用域中没有找到名字func,b也没有 我找不到它。然后我们继续在a类的范围内搜索,结果找到了func这个名字。当搜索结束时,编译器决定调用类a来做这件事。在域中使用func函数(这个过程叫做名称搜索,而且都是通过名称搜索,除非直接通过域名和域解析器搜索,否则不会有这个过程);
对象内存模型:
没有继承的时候,简单,变量存在于堆或者栈区,函数存在于代码段;
当继承存在时:
所有变量都连续存在于堆区或栈区(成员变量按照派生的层次顺序排列,新加入的成员变量总是在最后,私有和隐藏变量也会在内存中),函数存在于代码区(所有对象都是共享的,但能否使用取决于权限,私有的可以 必要时不使用)。
示例:
obj_a是基类对象,obj_b是派生类对象。假设obj_a的起始地址为0x1000,其内存分布如下图所示:
假设obj_b的起始地址是0x1100,a类中的m_b是私有的,那么它的内存分布如下图所示:
假设obj_c的起始地址是0x1300,并且有屏蔽,其内存分布如下图所示:
总结:在派生类的对象模型中,会包含基类的所有成员变量。这种设计方案的优点是访问效率高,可以在派生类对象中直接访问基类变量,不需要几层间接计算。
在设计基类和派生类的构造函数/析构函数时,派生类的构造函数也要对继承的成员变量进行初始化,但大多数基类都有带有私有属性的成员变量,在派生类中是的,更不用说被派生类的构造函数初始化了。解决这个问题的思路是在派生类的构造函数中调用基类的构造函数。
# includeiostreamusing命名空间std//基类people class people{protect: char * m _ name;int m _ agepublic:人(char*,int);};人: :人(char * nam: m _ nam:公有p: float m _ score;public:学生(char *name,int age,float score);空白display;};//people(姓名,年龄)是调用基类的构造函数,student : : student(char * name,intage,float scor: people(姓名,年龄),m_score(分数){ } void student : : display{ coutm _ name 有着 的年龄年龄 得分为 m _ score 。 endl} int main{ student stu( 小明 , 16, 90.5);stu.display。返回0;}people(name,age)是调用基类的构造函数并将名字和年龄作为实参传递给它,m_score(score)是派生类的参数初始化表。其次,不存在m_score(score)放在前面的问题,它会遵循先调用基类构造函数,再初始化参数初始化表中其他成员变量的原则。
构造函数调用顺序:
当类a-b-c按照a类构造函数-b类构造函数-c类构造函数的顺序执行时,其中a是c的间接基类,b是c的直接基类;派生类的构造函数只能调用直接基类的构造函数,而不能调用间接基类的构造函数,因为c丢弃了b类的构造函数,b会先调用a类的构造函数,相当于c间接(或隐式)调用a的构造函数,如果在c中再次显式调用a的构造函数,a的构造函数就会被调用两次,相应的,初始化工作也会做两次,不仅多余,而且没有必要。
基类构造函数调用规则:
通过派生类创建对象时,必须调用基类的构造函数,这是语法规则。换句话说,在定义派生类构造函数时,最好参考明基类构造函数;如果没有指定,调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数。
#使用命名空间std包含iostreamusing//基类people class people{public:人;//基类的默认构造函数people(char *name,int age);protect: char * m _ nam::p: m _ name( xxx ),m _ age(0){ } people ::people(char * nam: m _ nam:公立people{public:学生;student(char*,int,float);public:虚空显示;privat:浮点m _ scor::stud: m _ score(0.0){ }//派生类默认构造函数student: :学生(char * name,intage,float scor:人(姓名,年龄),m _ score(分数){ } void student: : display{ coutm _ name 有着 的年龄年龄 得分为 m _ score 。 endl} int main{ student stu 1;stu 1 . display;学生stu 2( 小明 , 16, 90.5);stu 2 . display;返回0;}在创建对象stu1时,执行派生类的构造函数student: : student,该函数不指定调用基类的哪个构造函数。从运行结果中可以明显看出,系统默认调用不带参数的构造函数,即people : :人。
创建对象stu2时,执行派生类的构造函数student: :学生(char * name,intage,f)。loat score),它指示基类的构造函数。
对于析构函数
析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不需要显式调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,不需要我们的干预。
析构函数和构造函数的执行顺序正好相反:
创建派生类对象时,构造函数的执行顺序与继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。当派生类对象被销毁时,析构函数的执行顺序与继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。多继承多继承对象内存模型c不仅有单继承,还有多继承。
d:类公共a、私有b、受保护c {//d类的新成员}
d是一个具有多种继承形式的派生类。它以公共继承a类,以私有继承b类,以保护继承c类。d .根据不同的继承方法,得到a、b、c中的成员,确定其在派生类中的访问权限。
多重继承下的构造:
基类构造函数的调用顺序与它们在派生类的构造函数中出现的顺序无关,但与声明的派生类的时基类出现的顺序相同(类似于类中的变量)。和上面的例子一样,先构造a,再构造b,再构造c,最后构造d;
命名:
当两个或多个基类中存在同名的成员(成员变量或成员函数)时,如果直接访问该成员,就会发生命名,编译器不知道使用基类的哪个成员。此时,需要在成员名前添加类名和域解析器::,以便明确指出使用哪个类成员,消除歧义。
内存模型:
直接上例:
#include cstdiousing命名空间std//基类a class a{public: a(int a,int b);protect:国际机场;int m _ b;};a:: a(int a,int b): m _ a(a),m _ b(b){ }//基类bclass b{public: b(int b,int c);protect: int m _ b;int m _ c;};b::b(intb,int c): m _ b(b),m _ c(c){ }//派生类c c:公共a,公共b{public: c(int a,int b,int c,int d);public:虚空显示;privat:国际机场;int m _ c;int m _ d;};c: :c(int a,int b,int c,int d): a(a,b),b(b,c),m_a(a),m_c(c),m _ d(d){ } void c: : display{ printf( a: :m _ a = % d,a: :m _ b = % d \ n ,a::m_a,a: :m _ b);printf( b: :m _ b = % d,b: :m _ c = % d \ n ,b::m_b,b: :m _ c);printf( c: :m _ a = % d,c: :m _ c = % d,c: :m _ d = % d \ n ,c::m_a,c::m_c,m _ d);}int main{ c obj_c(10,20,30,40);obj _ c . display;返回0;}借助指针突破访问权限:
认为指针指向内存地址,对象指针指向对象的内存地址,通过内存模型可以知道private也在连续内存中,所以!!!就用手指pin偏移量可以强制访问私有成员变量;例如:
图中假设obj对象的起始地址为0x1000,m_a(public)、m_b(public)和m_c (private)分别与对象的开头相隔0、4、8个字节。我们称之为距离偏移。
你知道:
int b = p-m _ b;
会转换成:int b = *(int *)((int)p sizeof(int));
实际上是:int b = *(int *)((int)p4);
有:
所以:int c = *(int *)((int)p sizeof(int)* 2);//it ;就这么简单。
虚拟继承1。什么是虚拟继承和虚拟基类?
多重继承容易出现命名:经典钻石继承
第一个问题:在派生类中保存间接基类成员的多个副本,虽然不同的数据可以存储在不同的成员变量中,但在大多数情况下是多余的,因为保存多个成员变量不仅占用更多的存储空间,而且容易导致命名。如果a类有一个成员变量a,那么在d类中直接访问a会导致二义性,而编译器没有 我不知道它是来自路径a-b-d还是来自路径a-c-d
为了消除歧义,我们可以在m_a前面注明来自哪个类(按域处理)。
但是内存中仍然有两个间接基类,很消耗内存,所以为了解决多重继承中的命名和冗余数据问题,提出了虚拟继承,使得派生类中只保留间接基类的一个成员。
//间接基类aclass a{ //虚拟基类protect: int m _ a;};//直接基类b class b:虚公共a {//虚继承protect: int m _ b;};//直接基类c类c:虚公共a {//虚继承protect: int m _ c;};//派生类d类d:公共b,公共c{public: void seta(inta){ m _ a = a;}//更正void setb(int b){ m _ b = b;}//更正void setc(int c){ m_c= c;}//更正void s: int m _ d;};int main{ d d;返回0;虚拟继承的目的是让一个类声明它愿意共享它的基类。其中,这个共享基类称为虚拟基类。
我们可以看到一个问题:虚拟派生的运算必须在虚拟派生的真实需求出现之前完成,也就是在d的需求出现之前,要将b和c设置为虚拟继承;
即虚拟派生只影响指定虚拟基类的派生类进一步派生的类(继承bc的类,比如e继承b,f继承c会有影响),不会影响派生类本身(bc);
在实际开发中,中间层的基类一般会将其继承声明为虚拟继承,不会带来任何问题。使用虚拟继承的类层次结构是由一个人或一个项目组一次性设计的,因为没有考虑到后期的需求,所以不需要修改中间函数;c库iostream只是采用了虚继承。
2.虚拟基类成员的可见性
以钻石继承为例,假设a定义了一个名为x的成员变量,当我们在d中直接访问x时,有三种可能:
如果b和c中都没有x的定义,那么x会被解释为a的成员,此时不存在歧义。如果b或c中的某个类定义了x,就不会有二义性,派生类的x比虚基类的x优先级高。如果x在b和c中都有定义,那么直接访问x会造成二义性。第二类问题:bc包含相同优先级的变量。此时,只能使用领域分析来消除歧义。
3.虚拟继承的构造函数和内存模型;
与继承的构造过程不同,派生类的构造函数必须调用虚基类的构造函数。
#使用命名空间std包含iostreamusing//虚拟基类a class a{public: a(int a);protect:国际机场;};a: : a(int a): m _ a(a){ }//直接从b class b:虚拟公共a {公共echo。2-@.com b(int a,int b);public:虚空显示;protect: int m _ b;};b: :b(int a,int b): a(a),m _ b(b){ } void b: : display{ cout m _ a = 并购 ,m _ b = m _ bendl}//直接派生类c类c:虚公共a{public: c(int a,int c);public:虚空显示;protect: int m _ c;};c: :c(int a,int c): a(a),m _ c(c){ } void c: : display{ cout m _ a = 并购 ,m _ c = m _ c:公共b,公共c{public: d(int a,int b,int c,int d);public:虚空显示;privat:国际机场;};de cho 2-@ . com :d(int a,int b,int c,int d): a(a),b(90,b),c(100,c),m _ d(d){ } void de cho 2-@ . com : display{ cout m _ a = 并购 ,m _ b = m _ b ,m _ c = m _ c ,m _ d = m _ dendl}在最终派生类d的构造函数中,除了b和c的构造函数外,还调用了a的构造函数。因为现在a在最终的派生类d中只有一份内存,如果是b\c构造的,编译器会很困惑,不会 我不知道该初始化哪一个。
虚拟继承时间构造函数的执行顺序与普通继承不同:在最终派生类的构造函数调用列表中,不管每个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,然后按照出现的顺序调用其他构造函数;对于普通的继承,按照构造函数出现的顺序调用它。
对于普通继承,基类子对象总是在派生类对象的前面(即基类成员变量总是在派生类成员变量的前面),无论继承级别有多深,它距派生类对象顶部的偏移量都是固定的(固定位置)。请看下面的例子:
a{protect: int m _ a1级;int m _ a2};b:公共a{protect: int b1级;int b2};c:公共b{protect: int c1级;int c2};d:公共c{protect: int d1级;int d2};int main{ a obj _ a;b obj _ bc obj _ cd obj _ d返回0;} a类所在的内存位置永远在前面。
1)修改上面的代码,使a成为b的虚拟基类:
b:虚拟公共a类
a将移动到后面
2)假设a是b and b的虚基类,是c的虚基类
从上面两张图可以看出,虚拟继承时的派生类对象分为两部分:
没有阴影的部分有固定的偏移量,不会随着继承级别的增加而改变,称为固定部分;阴影部分是虚拟基类的一个子类,偏移量会随着继承级别的增加而变化,这部分称为共享部分。有一个问题:如何计算分摊部分的偏移量?
对于虚拟继承,派生类分为固定部分和共享部分,共享部分放在最后。几乎所有的编纂者都在这一点上达成了共识。主要区别在于如何计算分摊部分的抵消,没有统一的标准。
以下是vs的解决方案示例:
vc引入了虚拟基类表。如果一个派生类有一个或多个虚拟基类,编译器将在派生类对象中插入一个指针,指向虚拟基类表。虚拟基类表实际上是一个数组,数组中的元素存储每个虚拟基类的偏移量字节。
假设a是b and b的虚基类,c是c的虚基类,那么每个对象的内存该模型如下图所示:
虚拟继承表存储了所有虚拟基类(包括直接继承和间接继承)相对于当前对象的偏移量,这样在通过派生类指针访问虚拟基类的成员变量时,无论继承级别有多深,都只需要一次间接转换。
这种方案还可以避免在有多个虚拟基类的情况下,使派生类对象承载太多指针,只需要承载一个指针。例如,假设类a、b、c和d的继承关系是:
内存模型是:
当派生类被赋给基类,数据类型被转换,int类型的数据被赋给float类型的变量时,编译器会先把int类型的数据转换成float类型再赋值;同样,类也可以有数据类型转换,也是数据类型;
但这种转换只在基类和派生类之间有意义,只能将派生类赋给基类,包括将派生类对象赋给基类对象,将派生类指针赋给基类指针,将派生类引用赋给基类引用,这在c中称为向上造型,相应地,将基类赋给派生类称为向下造型。
过渡期间非常安全。
分配的本质是将现有数据写入分配的内存。对象的内存只包含成员变量,所以对象之间的赋值就是成员变量的赋值,不存在成员函数的赋值问题。
这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,不能给派生类对象赋值。(因为基类不包含派生类的成员变量,所以不能给派生类的成员变量赋值。同样,不能在同一基类的不同派生类对象之间进行赋值)。
#使用命名空间std包含iostreamusing//基类class a{public: a(int a);public:虚空显示;public:国际机场;};a: :a(int a): m _ a(a){ } void a: : display{ cout a: m _ a级= m _ a:公共a{public: b(int a,int b);public:虚空显示;public: int m _ b;};b: :b(int a,int b): a(a),m _ b(b){ } void b: : display{ cout b: m _ a级= 并购 ,m _ b = m _ bendl} int main{ a a(10);b b(66,99);//赋值前,a . display;b .显示器;cout - endl//赋值后,a = b;//此时a.m_a变成66;a .显示器;b .显示器;返回0;}除了将派生类对象赋给类基类对象,还可以将派生类指针赋给基类指针:
以下继承关系:
#使用命名空间std包含iostreamusing//基类a class a{public: a(int a);public:虚空显示;protect:国际机场;};a: :a(int a): m _ a(a){ } void a: : display{ cout a: m _ a级= m _ aendl}//中间派生类b class b:公共a{public: b(int a,int b);public:虚空显示;protect: int m _ b;};b: :b(int a,int b): a(a),m _ b(b){ } void b: : display{ cout b: m _ a级= 并购 ,m _ b = m _ bendl}//基类c类c{public: c(int c);public:虚空显示;protect: int m _ c;};c::c(int c): m _ c(c){ } void c:: display{ cout c:级m _ c = m _ c: public b,public c{public: d(int a,int b,int c,int d);public:虚空显示;privat:国际机场;};de cho 2-@ . com :d(int a,int b,int c,int d): b(a,b),c(c),m _ d(d){ } void de cho 2-@ . com : display{ cout d: m _ a级= 并购 ,m _ b = m _ b ,m _ c = m _ c ,m _ d = m _ dendl} int main{ a * pa = new a(1);b *pb = new b(2,20);c *pc =新c(3);d *pd =新d(4,40,400,4000);pa = pd//将pd的指针赋给pa;pa 的地址也是pd s地址,pa-display;//但是函数还是用了pa的函数,和指针类型pb = pd有关;p b- display;pc = pdpc -显示器;cout - endlcout pa = paendlcout pb = pbendlcout pc = pcendlcout pd = pdendl返回0;可以看出,与对象变量之间的赋值不同,对象指针之间的赋值并不复制对象的成员,也不修改对象本身的数据,只是改变指针的方向。
第一个问题:为什么pa的地址变了,指向pd?为什么被调用的函数仍然属于pa?
输出值的解释:pa本来是指向基类a的指针,现在指向了派生类d的对象,使得隐式指针发生了这种变化,也指向了类d的对象,所以最终在display内部使用了类d的成员变量也就不难理解了。
函数调用解释:编译器虽然通过指针访问成员变量,但并不通过指针访问成员函数:编译器通过指针类型访问成员函数。对于pa来说,它的类型是a,无论指向哪个对象,都使用a类的成员函数..具体原因在前面的注释中已经详细解释过了:c函数的编译原理和成员函数的实现;
第二个问题:为什么不是 pc的地址和pd的地址不一样,而pa,pb和pd是一样的?
我们通常认为赋值就是把一个变量的值赋予另一个变量。虽然这种想法是正确的,但是需要注意的是,编译器可能会在赋值之前处理现有的值。比如将double类型的值赋给int类型的变量,编译器会直接擦除小数部分,导致赋值运算符两边的变量值不相等。
将派生类的指针赋给基类的指针时也是如此,编译器也可能在赋值前对其进行处理:
对象的指针必须指向对象的起始位置。对于a类和b类,其子对象的起始地址与d类相同,所以在给pa和pb赋值pd时不需要做任何调整,直接传递已有的值即可;但是,c类子对象与d类对象的开头有一定的偏移。在给pc赋值pd时要加上这个偏移量,这样pc就可以指向c类子对象的起始位置,即执行pc = pd语句,编译器调整了pd的值,导致pc和pd的值不同。
内存模型:
将派生类引用分配给基类引用
引用只是封装了指针,本质上没有区别,所以我猜测把派生类引用赋给基类引用的效果应该和指针一样。
//更改上面main函数中的content int main {d d d (4,40,400,4000);a ra = d;b rb = d;c rc = d;ra . display;rb.display。rc . display;返回0;}果不其然,运行结果是:a:类m _ a = 4 b:类m _ a = 4,m _ b = 40 cecho2-@.co类。m m c = 400
具体分析同指针;
你可以去看看这个博客来增强对向上转化的理解-。
标签:
函数派生类
了解更多c引用类型(c引用的定义)相关内容请关注本站点。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录