首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
88 阅读
2
nvim番外之将配置的插件管理器更新为lazy
73 阅读
3
2018总结与2019规划
55 阅读
4
PDF标准详解(五)——图形状态
37 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
是金子总会发光的,可你我都是老铁
累计撰写
311
篇文章
累计收到
27
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
页面
归档
友情链接
关于
搜索到
3
篇与
的结果
2019-05-25
Java 继承
之前说过了Java中面向对象的第一个特征——封装,这篇来讲它的第二个特征——继承。一般在程序设计中,继承是为了减少重复代码。继承的基本介绍public class Child extends Parent{ //TODO } 使用继承后,子类中就包含了父类所有内容的一份拷贝。子类与父类重名成员的访问。重名属性当子类与父类中有重名的数据成员时,如果使用子类对象的引用进行调用,那么优先调用子类的成员,如果子类成员中没有找到那么会向上查找找到父类成员。例如public class Parent{ public int num = 10; } public class Child extends Parent{ public int num = 20; public static void main(String[] args){ Parent obj = new Child(); System.out.println(obj.num); //输出10 Child c = new Child(); System.out.println(c.num); //输出20 } }二者同样是创建的Child对象,那么为何一个是10,而一个是20 呢? 在C/C++中经常提到一个概念就是,指针本身存储的是一个地址值,指针本身是没有任何数据类型的,而指针指向的地址有,我们在程序中定义的指针类型决定了代码在访问指针所指向的内存时是如何翻译这段内存的。Java中虽然说没有开放操作内存的能力,但是引用本身也是一个指针。如果将父类类型的引用来保存子类对象的地址,那么从引用类型来看,它只能看到父类的相关内容,所以这里它将对象所在内存当做父类的类型,所以第一个打印语句访问到的是父类的 num 成员。而第二个它能看到子类的整个内存,所以说它优先使用子类的 num 成员。上面是第一种情况,即在外部直接调用,如果通过方法来访问呢public class Parent{ private int num = 10; public int getNum(){ return this.num; } } public class Child extends Parent{ public int num = 20; public int getNum(){ return this.num; } public static void main(String[] args){ Parent obj = new Child(); System.out.println(obj.getNum()); //输出20 Child c = new Child(); System.out.println(c.getNum()); //输出20 } }第一条输出语句实际上使用了Java中的多态特性,这个时候会调用子类的方法,而第二条直接调用子类方法,而在子类方法中通过this调用自身的 num 成员,所以这里会输出两个20不管哪种情况,在通过对象的引用调用对象成员的时候会根据引用类型来翻译对应的内存,找到需要访问的变量,如果子类中未找到则向上查找父类,如果父类也未找到,则会报错。下面来看看Java中继承的内存图来加深这个的理解public class Parent{ public int num = 10; public int getNum(){ return this.num; } } public class Child extends Parent{ public int num = 20; public int getNum(){ return this.num; } public void show(){ int num = 30; System.out.println(num); //30 System.out.println(this.num); //20 System.out.println(super.num); //10 } public static void main(String[] args){ Child obj = new Child(); obj.Show(); obj.getNum(); //20 } }对象的内存分布大致如下:首先JVM加载程序的时候将类定义放到方法区中,此时方法区中有两个类的定义,其中子类中有一个 [[class_super]] 标志,用于标明它的父类是哪个。然后创建main的栈并执行main函数,在main函数中创建了一个Child的对象,那么JVM会根据方法区中的相关内容在堆中创建子类对象, 子类对象中包含一整份的父类的拷贝。然后调用Show方法,它首先根据方法区中的标识查找,在子类中找到了这个方法创建方法栈并调用。在方法中,定义了一个局部变量 num,当访问 num这个变量时会首先在栈中查找,如果找到则访问,如果没有则在子类中查找,如果子类中也没有,则会访问父类。就这样在栈中找到了num。接着访问 this.num。 这个时候JVM会首先到子类中查找,找不到则会进一步查找父类。发现在子类中找到了。接着访问 super.num,那么JVM会主动去父类中查找。show方法执行完了之后,JVM回收它的堆栈,接着调用getNum() 方法,查找方式与调用show的相同。后面的就不再说了,与之前的类似。重写向上面的例子中这样的。当子类与父类的方法名、参数列表相同时,如果用子类对象进行调用,那么会调用子类方法,这个时候父类的同名方法就被重写了。注意:这里并没有强调返回值也一样,其实这里只需要返回值能正常的发生隐式转换即可。这里也没有规定它的访问权限,这里只要求子类方法的访问权限大于等于父类方法的访问权限,也就是在同一条件下保证二者都能访问。虽然没有这两方面的限制,但是一般情况下父类的方法与子类重写的方法在形式上应该是完全一样的。public class Parent{ public int num = 10; public int getNum(int n){ return this.num; } } public class Child extends Parent{ public int num = 20; public int getNum(float f){ return this.num; } }上面的代码并没有实现重写的功能,但是如果程序员自己理解不到为,认为这是一个重写,并且在代码中按照重写在使用,那么编译虽然不会报错,但是在运行过程中可能会出现问题,这个时候可以使用 @Overwrite 注解,来告诉编译器,这里是一个重写,如果编译器判断这个不是重写,那么编译会报错,这样就将运行时错误提升到了编译时,有注意bug的排除。注解与注释有什么相同与区别呢:我认为二者都是用于对程序做一个说明,但是注释是给人看的,注解是给机器看的。重写与重载有什么区别呢?重写是发生在类的继承关系中的,要求函数名、参数列表相同重载并没有规定使用的场合,要求函数名相同、而参数列表不同既然说到重载,我想起来了java与c++中重载的一个区别void sayHello(long f); void sayHello(int);根据上述函数的定义,java和c++中都形成了一个重载的关系,如果在C中使用代码 sayHello(10) 来调用的时候,因为这里的10既可以理解为long也可以理解为int,所以这里发生了二义性,编译会报错,而java中 10 默认为int,所以这里会调用int参数的函数,而想要调用long型的参数,得写成 sayHello(10L)。这算是二者之间和有趣的一个现象。构造函数与C/C++中的相同,调用构造的时候会优先调用父类构造,像上面的代码中,并没有明显的调用构造,在创建类的时候会隐式的调用构造,并在子类构造中隐式调用父类构造。但是一旦手工定义了构造,那么编译器将不再额外提供构造。public class Parent{ Parent(int n){ System.out.println(n); } } public class Child extends Parent{ Child(){ //这个会编译报错 System.out.println("Child"); } }Child构造函数默认会调用父类的无参构造,但是由于父类提供了构造,此时编译器不再提供默认的无参构造,所以这里找不到父类的无参构造,报错。这个代码可以进行如下的修改public class Child extends Parent{ Child(){ //这个会编译报错 super(19); System.out.println("Child"); } //或者 Child(int n){ //隐式调用super(n) System.out.println("Child"); } }这里使用super关键字显示调用父类的带参构造。或者定义一个带参构造,编译器会默认为你添加上调用父类带参构造的过程。在Java中super关键字的作用主要有以下几点:在子类中访问父类成员在子类中调用父类构造函数,这种用法必须保证super在子类构造中是第一条被执行的语句,而且只能有唯一的一条我们说super代表父类,那么this代表的就是类本身,那么它有什么具体的作用,又有哪些需要注意的点呢?this可以访问本类成员变量在本类的成员方法中访问另一个成员方法在本类构造方法中访问另一个构造注意:第3中用法中,this关键字必须是构造方法执行的第一条语句,而且只能有唯一的一条。这条规则与super关键字的相同,那么在构造中既有this又有super的时候该怎么办呢?答案是无法这么使用,Java中规定this和super关键字在构造函数中只能出现一个。Java中的 继承关系在C++中,最让人头疼的是多继承的菱形继承关系,为了防止二义性,Java禁止多继承,只允许单继承。但是java中运行多继承,并且一个父类运行有多个子类。在Java的多级继承中如果出现同名的情况,访问时该怎么办呢?原则仍然相同,根据new出来的对象和左侧保存对象的引用类型来判断,如果是父类类型,则访问成员变量时只能访问父类,如果是访问方法则需要考虑多态。如果创建的是父类则只能访问父类的成员变量和方法
2019年05月25日
4 阅读
0 评论
0 点赞
2016-07-09
C++继承分析
面向对象的三大特性之一就是继承,继承运行我么重用基类中已经存在的内容,这样就简化了代码的编写工作。继承中有三种继承方式即:public protected private,这三种方式规定了不同的访问权限,这些权限的检查由编译器在语法检查阶段进行,不参与生成最终的机器码,所以在这里不对这三中权限进行讨论,一下的内容都是采用的共有继承。单继承首先看下面的代码:class CParent { public: CParent(){ printf("CParent()\n"); } ~CParent(){ printf("~CParent()\n"); } void setNumber(int n){ m_nParent = n; } int getNumber(){ return m_nParent; } protected: int m_nParent; }; class CChild : public CParent { public: void ShowNumber(int n){ setNumber(n); m_nChild = 2 *m_nParent; printf("child:%d\n", m_nChild); printf("parent:%d\n", m_nParent); } protected: int m_nChild; }; int main() { CChild cc; cc.ShowNumber(2); return 0; }上面的代码中定义了一个基类,以及一个对应的派生类,在派生类的函数中,调用和成员m_nParent,我们没有在派生类中定义这个变量,很明显这个变量来自于基类,子类会继承基类中的函数成员和数据成员,下面的汇编代码展示了它是如何存储以及如何调用函数的:41: CChild cc; 004012AD lea ecx,[ebp-14h];将类对象的首地址this放入ecx中 004012B0 call @ILT+5(CChild::CChild) (0040100a);调用构造函数 004012B5 mov dword ptr [ebp-4],0 42: cc.ShowNumber(2); 004012BC push 2 004012BE lea ecx,[ebp-14h] 004012C1 call @ILT+10(CChild::ShowNumber) (0040100f);调用自身的函数 43: return 0; 004012C6 mov dword ptr [ebp-18h],0 004012CD mov dword ptr [ebp-4],0FFFFFFFFh 004012D4 lea ecx,[ebp-14h] 004012D7 call @ILT+25(CChild::~CChild) (0040101e);调用析构函数 004012DC mov eax,dword ptr [ebp-18h] ;构造函数 0040140A mov dword ptr [ebp-4],ecx 0040140D mov ecx,dword ptr [ebp-4];到此ecx和ebp - 4位置的值都是对象的首地址 00401410 call @ILT+35(CParent::CParent) (00401028);调用父类的构造 00401415 mov eax,dword ptr [ebp-4] ;ShowNumber函数 00401339 pop ecx;还原this指针 0040133A mov dword ptr [ebp-4],ecx;ebp - 4存储的是this指针 31: setNumber(n); 0040133D mov eax,dword ptr [ebp+8];ebp + 8是showNumber参数 00401340 push eax 00401341 mov ecx,dword ptr [ebp-4] 00401344 call @ILT+0(CParent::setNumber) (00401005) 32: m_nChild = 2 *m_nParent; 00401349 mov ecx,dword ptr [ebp-4] 0040134C mov edx,dword ptr [ecx];取this对象的头4个字节的值到edx中 0040134E shl edx,1;edx左移1位,相当于edx = edx * 2 00401350 mov eax,dword ptr [ebp-4] 00401353 mov dword ptr [eax+4],edx ;将edx的值放入到对象的第4个字节处 33: printf("child:%d\n", m_nChild); 00401356 mov ecx,dword ptr [ebp-4] 00401359 mov edx,dword ptr [ecx+4] 0040135C push edx 0040135D push offset string "child:%d\n" (0042f02c) 00401362 call printf (00401c70) 00401367 add esp,8 34: printf("parent:%d\n", m_nParent); 0040136A mov eax,dword ptr [ebp-4] 0040136D mov ecx,dword ptr [eax] 0040136F push ecx 00401370 push offset string "parent:%d\n" (0042f01c) 00401375 call printf (00401c70) 0040137A add esp,8 ;setNumber函数 16: m_nParent = n; 004013CD mov eax,dword ptr [ebp-4] 004013D0 mov ecx,dword ptr [ebp+8] 004013D3 mov dword ptr [eax],ecx;给对象的头四个字节赋值从上面的汇编代码可以看到大致的执行流程,首先调用编译器提供的默认构造函数,在这个构造函数中调用父类的构造函数,然后在showNumber中调用setNumber为父类的m_nParent赋值,然后为m_nChild赋值,最后执行输出语句。上面的汇编代码在执行为m_nParent赋值时操作的内存地址是this,而为m_nChild赋值时操作的是this + 4通过这一点可以看出,类CChild在内存中的分布,首先在低地址位分步的是基类的成员,高地址为分步的是派生类的成员,我们随着代码的执行,查看寄存器和内存的值也发现,m_nParent在低地址位m_nChild在高地址位:当父类中含有构造函数,而子类中没有时,编译器会提供默认构造函数,这个构造只调用父类的构造,而不做其他多余的操作,但是如果子类中构造,而父类中没有构造,则不会为父类提供默认构造。但是当父类中有虚函数时又例外,这个时候会为父类提供默认构造,以便初始化虚函数表指针。在析构时,为了可以析构父类会首先调用子类的析构,当析构到父类的部分时,调用父类的构造,也就是说析构的调用顺序与构造正好相反。子类在内存中的排列顺序为先依次摆放父类的成员,后安排子类的成员。C++中的函数符号名称与C中的有很大的不同,编译器根据这个符号名称可以知道这个函数的形参列表,和作用范围,所以在继承的情况下,父类的成员函数的作用范围在父类中,而派生类则包含了父类的成员,所以自然包含了父类的作用范围,在进行函数调用时,会首先在其自身的范围中查找,然后再在其父类中查找,因此子类可以调用父类的函数。在子类中将父类的成员放到内存的前段是为了方便子类调用父类中的成员。但是当子类中有对应的函数,这个时候会直接调用子类中的函数,这个时候发生了覆盖。当类中定义了其他类成员,并定义了初始化列表时,构造的顺序又是怎样的呢?class CParent { public: CParent(){ m_nParent = 0; } protected: int m_nParent; }; class CInit { public: CInit(){ m_nNumber = 0; } protected: int m_nNumber; }; class CChild : public CParent { public: CChild(): m_nChild(1){} protected: CInit m_Init; int m_nChild; };34: CChild cc; 00401288 lea ecx,[ebp-0Ch] 0040128B call @ILT+5(CChild::CChild) (0040100a) ;构造函数 004012C9 pop ecx 004012CA mov dword ptr [ebp-4],ecx 004012CD mov ecx,dword ptr [ebp-4] 004012D0 call @ILT+25(CParent::CParent) (0040101e);先调用父类的构造 004012D5 mov ecx,dword ptr [ebp-4] 004012D8 add ecx,4 004012DB call @ILT+0(CInit::CInit) (00401005);然后调用类成员的构造 004012E0 mov eax,dword ptr [ebp-4] 004012E3 mov dword ptr [eax+8],1;最后调用初始化列表中的操作 004012EA mov eax,dword ptr [ebp-4]综上分析,编译器在对对象进行初始化时是根据各个部分在内存中的排放顺序来进行初始化的,就上面的例子来说,最上面的是基类的所以它首先调用的是基类的构造,然后是类的成员,所以接着调用成员对象的构造函数,最后是自身定义的变量,所以最后初始化自身的变量,但是初始化列表中的操作是先于类自身构造函数中的代码的。由于父类的成员在内存中的分步是先于派生类自身的成员,所以通过派生类的指针可以很容易寻址到父类的成员,而且可以将派生类的指针转化为父类进行操作,并且不会出错,但是反过来将父类的指针转化为派生类来使用则会造成越界访问。下面我们来看一下对于虚表指针的初始化问题,如果在基类中存在虚函数,而且在派生类中重写这个虚函数的话,编译器会如何初始化虚表指针。class CParent { public: virtual void print(){ printf("CParent()\n"); } }; class CChild : public CParent { public: virtual void print(){ printf("CChild()\n"); } }; int main() { CChild cc; return 0; } ;函数地址 @ILT+0(?print@CParent@@UAEXXZ): 00401005 jmp CParent::print @ILT+10(?print@CChild@@UAEXXZ): 0040100F jmp CChild::print ;派生类构造函数 004012C9 pop ecx 004012CA mov dword ptr [ebp-4],ecx 004012CD mov ecx,dword ptr [ebp-4] 004012D0 call @ILT+30(CParent::CParent) (00401023) 004012D5 mov eax,dword ptr [ebp-4] 004012D8 mov dword ptr [eax],offset CChild::`vftable' (0042f01c) 004012DE mov eax,dword ptr [ebp-4] ;基类构造函数 00401379 pop ecx 0040137A mov dword ptr [ebp-4],ecx 0040137D mov eax,dword ptr [ebp-4] 00401380 mov dword ptr [eax],offset CParent::`vftable' (0042f02c)上述代码的基本流程是首先执行基类的构造函数,在基类中首先初始化虚函数指针,从上面的汇编代码中可以看到,这个虚函数指针的值为0x0042f02c查看这块内存可以看到,它保存的值为0x00401005上面我们列出的虚函数地址可以看到,这个值正是基类中虚函数的地址。当基类的构造函数调用完成后,接着执行派生类的虚表指针的初始化,将它自身虚函数的地址存入到虚表中。通过上面的分析可以知道,在派生类中如果重写了基类中的虚函数,那么在创建新的类对象时会有两次虚表指针的初始化操作,第一次是将基类的虚表指针赋值给对象,然后再将自身的虚表指针赋值给对象,将前一次的覆盖,如果是在基类的构造中调用虚函数,这个时候由于还没有生成派生类,所以会直接寻址,找到基类中的虚函数,这个时候不会构成多态,但是如果在派生类的构造函数中调用,这个时候已经初始化了虚表指针,会进行虚表的间接寻址调用派生类的虚函数构成多态。析构函数与构造函数相反,在执行析构时,会首先将虚表指针赋值为当前类的虚表地址,调用当前类的虚函数,然后再将虚表指针赋值为其基类的虚表地址,执行基类的虚函数。多重继承多重继承的情况与单继承的情况类似,只是其父类变为多个,首先来分析多重继承的内存分布情况class CParent1 { public: virtual void fnc1(){ printf("CParent1 fnc1\n"); } protected: int m_n1; }; class CParent2 { public: virtual void fnc2(){ printf("CParent2 fnc2\n"); } protected: int m_n2; }; class CChild : public CParent1, public CParent2 { public: virtual void fnc1(){ printf("CChild fnc1()\n"); } virtual void fnc2(){ printf("CChild fnc2()\n"); } protected: int m_n3; }; int main() { CChild cc; CParent1 *p = &cc; p->fnc1(); CParent2 *p1 = &cc; p1->fnc2(); p = NULL; p1 = NULL; return 0; }上述代码中,CChild类有两个基类,CParent1 CParent2 ,并且重写了这两个类中的函数:fnc1 fnc2,然后在主函数中分别将cc对象转化为它的两个基类的指针,通过指针调用虚函数,实现多态。下面是它的反汇编代码43: CChild cc; 004012A8 lea ecx,[ebp-14h];对象的this指针 004012AB call @ILT+15(CChild::CChild) (00401014) 44: CParent1 *p = &cc; 004012B0 lea eax,[ebp-14h] 004012B3 mov dword ptr [ebp-18h],eax;[ebp - 18h]是p的值 45: p->fnc1(); 004012B6 mov ecx,dword ptr [ebp-18h] 004012B9 mov edx,dword ptr [ecx];对象的头四个字节是虚函数表指针 004012BB mov esi,esp 004012BD mov ecx,dword ptr [ebp-18h] 004012C0 call dword ptr [edx];通过虚函数地址调用虚函数 ;部分代码略 46: CParent2 *p1 = &cc; 004012C9 lea eax,[ebp-14h];eax = this 004012CC test eax,eax 004012CE je main+48h (004012d8);校验this是否为空 004012D0 lea ecx,[ebp-0Ch];this指针向下偏移8个字节 004012D3 mov dword ptr [ebp-20h],ecx 004012D6 jmp main+4Fh (004012df) 004012D8 mov dword ptr [ebp-20h],0;如果this为null会将edx赋值为0 004012DF mov edx,dword ptr [ebp-20h];edx = this + 8 004012E2 mov dword ptr [ebp-1Ch],edx;[ebp - 1CH]是p1的值 47: p1->fnc2(); 004012E5 mov eax,dword ptr [ebp-1Ch] 004012E8 mov edx,dword ptr [eax] 004012EA mov esi,esp 004012EC mov ecx,dword ptr [ebp-1Ch] 004012EF call dword ptr [edx] 004012F1 cmp esi,esp 004012F3 call __chkesp (00401680) ;CChild构造函数 0040135A mov dword ptr [ebp-4],ecx 0040135D mov ecx,dword ptr [ebp-4] 00401360 call @ILT+40(CParent1::CParent1) (0040102d) 00401365 mov ecx,dword ptr [ebp-4] 00401368 add ecx,8;将指向对象首地址的指针向下偏移了8个字节 0040136B call @ILT+45(CParent2::CParent2) (00401032) 00401370 mov eax,dword ptr [ebp-4] 00401373 mov dword ptr [eax],offset CChild::`vftable' (0042f020) 00401379 mov ecx,dword ptr [ebp-4] 0040137C mov dword ptr [ecx+8],offset CChild::`vftable' (0042f01c) 00401383 mov eax,dword ptr [ebp-4] ;CParent1构造函数 00401469 pop ecx 0040146A mov dword ptr [ebp-4],ecx 0040146D mov eax,dword ptr [ebp-4] 00401470 mov dword ptr [eax],offset CParent1::`vftable' (0042f04c);初始化虚表指针 00401476 mov eax,dword ptr [ebp-4] ;CParent2构造函数 004014F9 pop ecx 004014FA mov dword ptr [ebp-4],ecx 004014FD mov eax,dword ptr [ebp-4] 00401500 mov dword ptr [eax],offset CParent2::`vftable' (0042f064);初始化虚表指针 00401506 mov eax,dword ptr [ebp-4] ;虚函数地址 @ILT+0(?fnc2@CChild@@UAEXXZ): 00401005 jmp CChild::fnc2 (00401400) @ILT+5(?fnc1@CParent1@@UAEXXZ): 0040100A jmp CParent1::fnc1 (00401490) @ILT+10(?fnc1@CChild@@UAEXXZ): 0040100F jmp CChild::fnc1 (004013b0) @ILT+20(?fnc2@CParent2@@UAEXXZ): 00401019 jmp CParent2::fnc2 (00401520)内存地址存储的值0012FF6C20 F0 42 000012FF70CC CC CC CC0012FF741C F0 42 000012FF78CC CC CC CC0012FF7CCC CC CC CC从上面的汇编代码中可以看到,在为该类对象分配内存时,会根据继承的顺序,依次调用基类的构造函数,在构造函数中,与单继承类似,在各个基类的构造中,先将虚表指针初始化为各个基类的虚表地址,然后在调用完各个基类的构造函数后将虚表指针覆盖为对象自身的虚表地址,唯一不同的是,派生类有多个虚表指针,有几个派生类就有几个虚表指针。另外派生类的内存分布与单继承的分布情况相似,根据继承顺序从低地址到高地址依次摆放,最后是派生类自己定义的部分,每个基类都会在其自身所在位置的首地址处构建一个虚表。在调用各自基类的构造函数时,并不是笼统的将对象的首地址传递给基类的构造函数,而是经过相应的地址偏移之后,将偏移后的地址传递给对应的构造。在转化为父类的指针时也是经过了相应的地址偏移。在析构时首先析构自身,然后按照与构造相反的顺序调用基类的析构函数。dynamic_cast强制类型转化与static_cast类型转化根据上面的说明我们可以简单的画一张图:这个对象中有三个虚表,分别位于各个基类所在内存的首地址处,如果我们利用多态的特性进行类型转化的话如果采用static_cast的方式会进行静态转化,也就是说它只是简单的将对象的首地址进行类型转化,这个时候如果调用CParent2的函数,在编译的时候不会报错,但是在运行时可能在虚表中(注意如果是这种情况它找到的虚表其实是CParent1的虚表)找不到想要的虚函数,这个时候调用会引起非法内存访问,造成程序崩溃。但是用dynamic_cast可以进行偏移地址的计算,自动偏移到CParent2内存部分的首地址,这个时候调用虚函数不会产生崩溃的问题。所以在有多重继承和多继承的时候尽量使用dynamic_cast进行类型转化。抽象类抽象类是不能实例化的类,只要有纯虚函数就是一个抽象类。纯虚函数是只有定义而没有实现的函数,由于虚函数的地址需要填入虚表,所以必须提供虚函数的定义,以便编译器能够将虚函数的地址放入虚表,所以虚函数必须定义,但是纯虚函数不一样,它不能定义。class CParent { public: virtual show() = 0; }; class CChild : public CParent { public: virtual show(){ printf("CChild()\n"); } }; int main() { CChild cc; CParent *p = &cc; p->show(); return 0; }上面的代码定义了一个抽象类CParent,而CChild继承这个抽象类并实现了其中的纯虚函数,在主函数中通过基类的指针掉用虚函数,形成多态。22: CChild cc; 00401288 lea ecx,[ebp-4] 0040128B call @ILT+0(CChild::CChild) (00401005) 23: CParent *p = &cc; 00401290 lea eax,[ebp-4] 00401293 mov dword ptr [ebp-8],eax 24: p->show(); 00401296 mov ecx,dword ptr [ebp-8] 00401299 mov edx,dword ptr [ecx] 0040129B mov esi,esp 0040129D mov ecx,dword ptr [ebp-8] 004012A0 call dword ptr [edx] ;CChild构造函数 004012F0 call @ILT+25(CParent::CParent) (0040101e) 004012F5 mov eax,dword ptr [ebp-4] 004012F8 mov dword ptr [eax],offset CChild::`vftable' (0042f01c) CParent构造函数 00401399 pop ecx 0040139A mov dword ptr [ebp-4],ecx 0040139D mov eax,dword ptr [ebp-4] 004013A0 mov dword ptr [eax],offset CParent::`vftable' (0042f02c) 004013A6 mov eax,dword ptr [ebp-4]构造函数中仍然是调用了基类的构造函数,并在基类的构造中对虚表指针进行了赋值,但是基类中并没有定义show函数,而是将它作为纯虚函数,那么虚表中存储的的是什么东西呢,这个位置存储的是一个_purecall函数,主要是为了防止误调纯虚函数。菱形继承菱形继承是最为复杂的一种继承方式,它结合了单继承和多继承class CGrand { public: virtual void func1(){ printf("CGrand func1()\n"); } protected: int m_nNum1; }; class CParent1 : public CGrand { public: virtual void func2(){ printf("CParent1 func2()\n"); } virtual void func3(){ printf("CParent1 func3()\n"); } protected: int m_nNum2; }; class CParent2 : public CGrand { public: virtual void func4(){ printf("CParent2 func4()\n"); } virtual void func5(){ printf("CParent2 func5()\n"); } protected: int m_nNum3; }; class CChild : public CParent1, public CParent2 { public: virtual void func2(){ printf("CChild func2()\n"); } virtual void func4(){ printf("CChild func4()\n"); } protected: int m_nNum4; }; int main() { CChild cc; CParent1 *p1 = &cc; CParent2 *p2 = &cc; return 0; }上面的代码中有4个类,其中CGrand类为祖父类,而CParent1 CParent2为父类,他们都派生自组父类,而子类继承与CParent1 CParent2,根据前面的经验可以知道sizeof(CGrand) = 4(vt) + 4(int) = 8,而sizeof(CParent1) = sizeof(CParent2) = sizeof(CGrand) + 4(int) = 12, sizeof(CChild) = sizeof(CParent1) + sizeof(CParent2) + 4(int) = 28;大致可以知道CChild对象的内存分布是CParent1 CParent2 int这种情况,通过反汇编的方式我们可以看出对象的内存分布如下:内存的分步来看,CParent1 CParent2都继承自CGrand类,所以他们都有CGrand类的成员,而CChild类继承自两个类,所以CGrand类的成员在CChild类中有两份,所以在调用m_nNum1成员时会产生二义性,编译器不知道你准备调用那个m_nNum1成员,所以一般这个时候需要指定调用的是哪个部分的m_nNum1成员。同时在转化为祖父类的时候也会产生二义性。而虚继承可以有效的解决这个问题。一般来说虚继承可以有效的避免二义性是因为重复的内容在对象中只有一份。下面对上述例子进行相应的修改,为每个类添加构造函数构造初始化这些成员变量:m_nNum1 = 1;m_nNum2 = 2; m_nNum3 = 3; m_nNum4 = 4;另外再为CParent1 CParent2类添加虚继承。这个时候我们运行程序输入类CChild的大小:sizeof(CChild) = 36;按照之前所说的同样的内容只保留的一份,那内存大小应该会减少才对,为何突然增大了8个字节呢,下面来看看对象在内存中的分步:0012FF5C 30 F0 42 000012FF60 48 F0 42 00 0012FF64 02 00 00 000012FF68 24 F0 42 000012FF6C 3C F0 42 000012FF70 03 00 00 000012FF74 04 00 00 000012FF78 20 F0 42 000012FF7C 01 00 00 00上述内存的分步与我们之前想象的有很大的不同,所有变量的确只有一份,但是总内存大小还是变大了,同时它的存储顺序也不是按照我们之前所说的父类的排在子类的前面,而且还多了一些我们并不了解的数据下面通过反汇编代码来说明这些数值的作用:;主函数部分 73: CChild cc; 004012D8 push 1;是否构造祖父类1表示构造,0表示不构造 004012DA lea ecx,[ebp-24h] 004012DD call @ILT+5(CChild::CChild) (0040100a) 74: printf("%d\n", sizeof(cc)); 004012E2 push 24h 004012E4 push offset string "%d\n" (0042f01c) 004012E9 call printf (00401a70) 004012EE add esp,8 75: CParent1 *p1 = &cc; 004012F1 lea eax,[ebp-24h] 004012F4 mov dword ptr [ebp-28h],eax 76: CParent2 *p2 = &cc; 004012F7 lea ecx,[ebp-24h] 004012FA test ecx,ecx 004012FC je main+46h (00401306) 004012FE lea edx,[ebp-18h] 00401301 mov dword ptr [ebp-34h],edx 00401304 jmp main+4Dh (0040130d) 00401306 mov dword ptr [ebp-34h],0 0040130D mov eax,dword ptr [ebp-34h] 00401310 mov dword ptr [ebp-2Ch],eax 77: CGrand *p3 = &cc; 00401313 lea ecx,[ebp-24h] 00401316 test ecx,ecx;this指针不为空 00401318 jne main+63h (00401323);不为空则跳转 0040131A mov dword ptr [ebp-38h],0 00401321 jmp main+70h (00401330) 00401323 mov edx,dword ptr [ebp-20h];edx = 0x0040f048 00401326 mov eax,dword ptr [edx+4];eax = 0x18这个可以通过查看内存获得 00401329 lea ecx,[ebp+eax-20h]; ebp + eax - 20h = 0x0012ff78, ecx = 0x0012FF78 0040132D mov dword ptr [ebp-38h],ecx;经过偏移后获得这个地址 00401330 mov edx,dword ptr [ebp-38h] 00401333 mov dword ptr [ebp-30h],edx ;CChild构造 0040135A mov dword ptr [ebp-4],ecx 0040135D cmp dword ptr [ebp+8],0 00401361 je CChild::CChild+42h (00401382) 00401363 mov eax,dword ptr [ebp-4] 00401366 mov dword ptr [eax+4],offset CChild::`vbtable' (0042f048) 0040136D mov ecx,dword ptr [ebp-4] 00401370 mov dword ptr [ecx+10h],offset CChild::`vbtable' (0042f03c) 00401377 mov ecx,dword ptr [ebp-4] 0040137A add ecx,1Ch 0040137D call @ILT+0(CGrand::CGrand) (00401005);this 指针向下偏移1ch,开始构造父类 00401382 push 0;保证父类只构造一次 00401384 mov ecx,dword ptr [ebp-4] 00401387 call @ILT+60(CParent1::CParent1) (00401041) 0040138C push 0 0040138E mov ecx,dword ptr [ebp-4] 00401391 add ecx,0Ch 00401394 call @ILT+65(CParent2::CParent2) (00401046) 00401399 mov edx,dword ptr [ebp-4] 0040139C mov dword ptr [edx],offset CChild::`vftable' (0042f030) 004013A2 mov eax,dword ptr [ebp-4] 004013A5 mov dword ptr [eax+0Ch],offset CChild::`vftable' (0042f024) 004013AC mov ecx,dword ptr [ebp-4] 004013AF mov edx,dword ptr [ecx+4] 004013B2 mov eax,dword ptr [edx+4] 004013B5 mov ecx,dword ptr [ebp-4] 004013B8 mov dword ptr [ecx+eax+4],offset CChild::`vftable' (0042f020) 57: m_nNum4 = 4; 004013C0 mov edx,dword ptr [ebp-4] 004013C3 mov dword ptr [edx+18h],4 58: } ;CGrand构造 00401429 pop ecx 0040142A mov dword ptr [ebp-4],ecx 0040142D mov eax,dword ptr [ebp-4] 00401430 mov dword ptr [eax],offset CGrand::`vftable' (0042f054);虚表指针后期会被替代 10: m_nNum1 = 1; 00401436 mov ecx,dword ptr [ebp-4] 00401439 mov dword ptr [ecx+4],1 11: } ;CParent1构造 04014C9 pop ecx 004014CA mov dword ptr [ebp-4],ecx 004014CD cmp dword ptr [ebp+8],0;不再调用祖父类构造 004014D1 je CParent1::CParent1+38h (004014e8) 004014D3 mov eax,dword ptr [ebp-4] 004014D6 mov dword ptr [eax+4],offset CParent1::`vbtable' (0042f07c) 004014DD mov ecx,dword ptr [ebp-4] 004014E0 add ecx,0Ch 004014E3 call @ILT+0(CGrand::CGrand) (00401005);这个时候会跳过这个构造函数的调用通过上面的代码可以看出,为了使得相同的内容只有一份,在程序中额外传入一个参数作为标记,用于表示是否调用祖父类构造函数,当初始化完祖父类后将此标记置0以后不再初始化,另外程序在每个父类中都多添加了一个四字节的成员用来存储一个一个偏移地址,以便能正确的将派生类转化为父类。所以每当多出一个虚继承就多了一个记录偏移量的4字节内存,所以这个类总共多出了8个字节。所以这时候的类所占内存大小为28 + 4 * 2 = 36字节。
2016年07月09日
4 阅读
0 评论
0 点赞
2015-08-21
C++继承
在封装的过程中,我们发现有很多地方有问题,比如我们在封装Windows API 的过程中,每个窗口都有各自的消息处理,而我们封装时不同的窗口要针对不同的消息而编写不同的消息处理函数,不可能所有窗口对于某些消息都进行相同的处理,所以在面向对象的程序设计中,提供了一种新的方式——继承与派生;在c++中将继承的一方称作派生类或者子类,将被继承的一方叫做基类或者父类继承的基本格式如下(CB 继承CA):class CA { public: CA(); ~CA(); } class CB : public CA { public: CB(); ~CB(); }派生类中前面相应大小空间的内存保存的是基类的内容,而后面的部分保存的是派生类的内容,这样派生类就可以拥有基类的所有成员,而不必重写代码达到了代码重用的目的。在设计中一般将类的共性提取出来作为基类,而将不同的部分从基类派生,作为每个类的特性,对于共性的内容我们只需要在基类中编写,而在派生类中直接使用。下面我们来探讨一下,基类与派生类中构造与析构的调用关系,通过写下面一个简单的小例子:class CA { public: CA(){ cout <<"CA()"<<endl; } ~CA(){ cout <<"~CA()" << endl; } }; class CB : public CA { public: CB(){ cout <<"CB()" << endl; } ~CB(){ cout <<"~CB()" << endl; } }; int _tmain(int argc, _TCHAR* argv[]) { CB objB; return 0; }最终的结果是先调用基类的构造函数在调用派生类的构造函数,而对于析构的调用顺序正好相反,先调用派生类在调用基类:对于继承来说有三种:共有继承、私有继承以及保护继承,继承的方式不同,派生类对于基类的各种不同属性之间成员的访问权限不同,下面再给出一个表格用以说明这个问题:通过这个表我们可以总结出一下几点:1)私有成员在任何情况下都不能被派生类访问;2)公有继承下其他基类成员在派生类中的访问属性不变;3)私有继承下其他基类成员在派生类中全部变为私有;4)保护继承下其他类成员在派生类中全部变为保护属性;从这个表中我们可以看出,私有继承与保护继承对于基类的访问属性完全相同,那么它们有何区别呢?保护成员的访问情况与私有相同,即类的保护成员在类内可以访问在类外不能访问,它们二者的区别在这个表中确实没有体现出来,主要的区别可以在下一层的继承中体现比如有三个类继承关系为CC->CB->CA,继承类型分别为,我们知道基类的非私有成员在保护继承下公有的变为保护,保护的仍然为保护,而私有继承则是将所有都变为私有,他们之间如果都是保护继承的方式,那么CA中的其他成员在CB中都变为保护类型那么在CC中仍然能够访问到CA的成员;当他们之间都是以私有继承的方式,那么CA中的成员在CB中都为私有,在CC中就不能访问CA中的成员,所以在一般情况下,我们将基类的数据成员声明为保护类型,这样既起到了封装的作用,又方便派生类的访问;
2015年08月21日
5 阅读
0 评论
0 点赞