【重要】文章是自己看了以后思考了以后全文手打的,但理解学习过程中有参考以下内容:
动态编联和静态编联部分
内存分区部分
C++用new和不用new创建类对象部分
一、用new创建对象和不用new创建对象的区别
假设前提:已有定义:
class A{
......;//无任何成员定义
}
1.new创建类对象:A *a = new A;
- ①语句创建过程:
A *a=NULL;//此时只是在栈中定义了一个A型指针变量指向NULL
a=new A;//此时在堆中分配内存,生成A型对象(同时因为我是在堆上,生存周期无线延长,可以通过指针控制它,并且一般不需要进行拷贝) - ②内存释放:需要指针接收,使用完需要delete销毁(释放内存,防止之后改变了指针指向,但该段内存未释放从而造成内存泄漏,之后也要a = NULL,防止野指针),此时会执行析构函数;
- ③返回:得到的变量a是指针,需要寻址访问,具有指针特性,一处初始化,多处使用
- 【注意】若遇此种形式:A *a = new B;//B为A的派生类,且A、B都有定义test函数
则调用a-> test()时,可能是调用自A也可能是调用自B ,就要看是否是虚函数,见下《虚函数创建的实质》讲解
2.不用new 创建类对象:A a;
- ①语句创建过程:
A a;//此时直接在栈中分配了内存空间用以存放A型变量a,也即对象a - ②内存释放: 使用完后系统自动释放,析构函数会自动执行(且因为是在栈上,所以中途调用其他函数跳出不算,也不是整个程序走完后释放,而是所在局部区域,随着{}结束之后销毁;而函数中return一个对象的本质,是对它进行了拷贝);
- ③返回: 得到的变量a是对象
- 【补充】且此时只能发生静态编联,无法实现虚函数多态
二、虚函数创建的实质
1、虚函数的实现
- ①大多数编译器通过vtbl(virtual table)和vptr(virtual table pointer)来实现对虚函数的调用
- ②当一个类声明了虚函数,或者重定义了父类中的虚函数,或者直接继承了虚函数,这个类都会产生自己的vtbl和vptr,同时该类的每个对象都会包含一个vptr去指向该类的vpbl
- ③vtbl表中指针顺序 对应 其对应的虚函数被声明的顺序
- vptr:每个对象都会有自己的一个vptr指针(【a】同一个类的所有对象的指针值相同)
该指针存放在该对象所占内存区域的首地址处,【b】指向把它实例化的类的vtbl; - vtbl:【c】每个派生类对应每个基类都会有自己的一个vtbl指针数组,【d】但对应同一个基类,若无虚函数覆盖或增改时,则所有派生类的vtbl指针数组是一样的,都是基类中定义的虚函数,且实质是:所有vtbl指针数组中每个数组元素的指针值,也即地址,是同一个虚函数(都只是基类中的那个虚函数)
【a】【b】【c】【d】见实例讲解
- vptr:每个对象都会有自己的一个vptr指针(【a】同一个类的所有对象的指针值相同)
2、实例
#include<iostream>
using namespace std;
class A {//第一个基类
public:
virtual void Func1(){};
virtual void Func2(){};
virtual void Func3(){};
virtual void Func4(){};
void fuck(){};
};
class A2{//第二个基类
public:
virtual void Func(){};
};
class B : public A {//基类A的派生类
public:
virtual void Func1(){};
virtual void Func5(){};
void fuck(){};
};
class C :public B,public A2{//基类A2和派生类B的派生类
public:
virtual void Func6(){};
};
int main()
{
A *a = new A;
A *b1 = new B;
B *b2 = new B;
B *b3 = new B;
C *c = new C;
A a2;
system("pause");//(1)
delete a; a = NULL;
delete b1; b1 = NULL;
delete b2; b2 = NULL;
delete b3; b3 = NULL;
delete c; c = NULL;
return 0;
}
1.
【a】同一个类的所有对象的vptr指针值相同:
可见对象b2和b3的vptr指针的值,都是0x00edcc30
2.
【b】指向把它实例化的类的vtbl
可见对象b2、b3的vptr指针与对象a的vptr指针指向不同,b2、b3指向类B的vpbl,a指向类A的vpbl
3.
【c】每个派生类对应每个基类都会有自己的一个vtbl指针数组
类C多继承自类B(B又继承自类A)和类A2(是一个基类),最终有两个vptr指针指向两个vpbl,分别是0x00edca74地址处的类A的表和0x00edcb1c地址处的类A2的表
4.
【d】对应同一个基类,若无虚函数覆盖或增改时,则所有派生类的vtbl指针数组是一样的,其实质是:所有vtbl指针数组中每个数组元素的指针值,也即地址,是同一个虚函数(都只是基类中的那个虚函数)
可见b2未对函数Fun2()、Fun3()、Fun4()重定义,于是vpbl中元素对应的地址与a中完全相同,但因对Fun1()进行了重定义,于是覆盖了原来的Fun1()
【注】见网上其他地方说,派生类中新定义的虚函数会放置在第一个父类的vtbl中的最后,如图:
所以我在类B中新添加类Func5()函数,在类C中添加了Func6()函数,但我观察了b2、b3和c的vpbl,均未发现:
此处不解
三、动态编联和静态编联
1、动态编联
- 必须在指针指向的对象创建出来后才能决定究竟用那个函数(也正是在代码运行前无法确定运行的函数,所以才被称作“虚函数”)
2、静态编联
- 在编译时就决定了函数是哪个类的函数的方式
3、为何说虚函数利用动态编联可以实现多态
1.对于一个类成员函数的调用:
A *b1 = new B;
b1->fuck();//注意这里的fuck()函数并非虚函数,所以是静态编联
- ①表面上看来是在一个对象内部去调用函数fuck(),但代码背后的实质是:所有函数都存放在一个代码区,也即是说:
【b1->fuck()】被转换成了【A::fuck(b1)】:把对象b1传进去,才告诉了编译器具体是哪个对象调用的这个函数。
但是,对于fuck()函数来说,类A有,类B也有啊,究竟是选哪个类的函数执行? - ②这时候就要想起:fuck()因为非虚函数,所以不支持动态编联,即在编译时就会确定调用函数对象
- ③而因为是在编译阶段,也就是内存中A啊B啊的对象还没有存在(前面说 A a;会在栈中分配内存空间,但是这里是A *a,只是声明一个A类型的指针变量),所以只能根据a这个指针的类型来决定使用哪个函数
- ④所以,此时a所调用的fuck()函数,是A::fuck()
【结论】此时无法达到多态,B中的fuck()简直白定义了!!
2.于是虚函数登场:
A *b1 = new B;
b1->Func1();//注意这里的Func1()函数为虚函数,所以是动态编联
- ①然而现在希望调用的是B中的Func1()函数
- ②因为Func1()是虚函数,所以施行动态编联
- ③当所指的函数是虚函数时,编译器将不会再做类似以下的转换:
【b1->fuck()】被转换成了【A::fuck(b1)】,而是变成:
【b1->Func1()】转换成【b1->vptr->vpbl->Func1()】(即此时虚函数的机制:vptr和vpbl登场) - ④因为此时必须要在b1指向的对象里面找,所以必须等到b1指向的对象被创建出来才能确定调用的是哪个函数,所以必须是运行时
- ⑤所以结合之前对vptr和vpbl的介绍,可想而知,最终指向的是B类中重新定义了的Func1()函数
- ⑥至此,实现了多态
【2017.03.04补充】
注意:虚函数的动态编联仅在基类指针或引用绑定派生类对象时发生