多态:C++面向对象编程的“灵魂”所在
前言
面向对象编程的三大特性:封装,继承,多态。继承了解了,下面来了解多态。
多态的定义和实现
多态是什么?通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲运行时多态。
编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为它们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运行时归为动态。
如以下程序:
int i = 18;
double d = 3.14;cout << i;
cout << d;
i 和 d 调用的都是流插入运算符函数,却能根据不同的数据类型输出不同的结果。同一个函数展现出了多种不同的形态,既能插入输出整型,也能插入输出浮点型,这就是编译时多态(静态多态)。
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。如动物的叫声,猫叫“喵喵”,狗叫“汪汪”。
多态的定义:多态是⼀个继承关系的下的类对象,去调用同⼀函数,产生了不同的行为
实现多态两个必要条件:
•
必须是基类的指针或者引用调用虚函数
•
被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖
具体说明:要实现多态效果,第⼀必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象有指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到
来具体演示展现多态的两个必要条件:
class Animal
{
public:virtual void call(){cout << "动物叫" << endl;}
};class Dog : public Animal
{
public:virtual void call(){cout << "小狗叫" << endl;}
};void Func(Animal* ptr)
{ptr->call();
}
Animal 和 Dog 类的实例化对象调用 Func 函数:
Animal animal;
Dog dog;Func(&animal);
Func(&dog);
运行结果:
在继承的赋值兼容转换中曾提到,基类的指针/引用可以指向基类的对象,也可以指向派生类的对象。传的基类的对象调用的就是基类的call函数,传的派生类的对象调用的就是派生类的call函数。
不仅指针可以,引用也可以。
class Animal
{
public:virtual void call(){cout << "动物叫" << endl;}
};class Dog : public Animal
{
public:virtual void call(){cout << "小狗叫" << endl;}
};void Func(Animal& ptr)
{ptr.call();
}
Animal 和 Dog 类的实例化对象调用 Func 函数:
Animal animal;
Dog dog;Func(animal);
Func(dog);
运行结果:
指针引用都可以,但是不可以是对象,必须要满足实现多态的两个条件。倘若是对象:
class Animal
{
public:virtual void call(){cout << "动物叫" << endl;}
};class Dog : public Animal
{
public:virtual void call(){cout << "小狗叫" << endl;}
};void Func(Animal ptr)
{ptr.call();
}
运行结果:
如果不是虚函数:
class Animal
{
public:void call(){cout << "动物叫" << endl;}
};class Dog : public Animal
{
public:virtual void call(){cout << "小狗叫" << endl;}
};void Func(Animal& ptr)
{ptr.call();
}
运行结果:
由上述的具体例子可以知道,必须要满足实现多态的两个必要条件才能实现多态。
虚函数
类成员函数前面加 virtual 修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加 virtual 修饰。虚函数与虚继承没有关系,只是它们使用的关键字相同。
class Animal
{
public:virtual void call(){cout << "动物叫" << endl;}
};
其中 call 函数就是虚函数。
虚函数的重写/覆盖
虚函数的重写/覆盖:派生类中有⼀个跟基类完全相同的虚函数【即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表(参数的类型,个数,顺序相同)完全相同】,称派生类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,虽然派生类的虚函数在不加 virtual 关键字时,也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
如下所示:
class Animal
{
public:virtual void call(){cout << "动物叫" << endl;}
};class Dog : public Animal
{
public:void call() // 没有写 virtual{cout << "小狗叫" << endl;}
};void Func(Animal& ptr)
{ptr.call();
}
运行结果:
重写的意思是重写实现,也就是重写函数体中的内容。派生类 Dog 虽然继承了 Animal 的虚函数,但是派生类继承的只是 virtual void call() 函数声明,而 call 函数的实现由派生类提供。所以派生类的虚函数不加 virtual 关键字也能构成重写。
如果将派生类的虚函数定义为 private ,那么重写时还能调用派生类中的虚函数吗?
class Animal
{
public:virtual void call(){cout << "动物叫" << endl;}
};class Dog : public Animal
{
private:virtual void call(){cout << "小狗叫" << endl;}
};void Func(Animal& ptr)
{ptr.call();
}
运行结果:
由运行结果可知,能被调用。为什么可以被调用?访问控制是在编译时基于静态类型检查的,而虚函数调用是在运行时基于动态类型确定的。
拓展:
class A
{
public:virtual void func(int val = 1){std::cout << "A->" << val << std::endl;}virtual void test() { func(); }
};class B : public A
{
public:virtual void func(int val = 0){std::cout << "B->" << val << std::endl;}
};int main()
{B* p = new B;p->test();return 0;
}
下面回答一个问题:
程序输出结果是什么?
A: A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确
答案:B
解析:
这里是一个派生类指针指向一个派生类对象,用 p 调用 test 函数,test 存在基类中,test 函数中又调用了 func 函数,func( )显示是这么显示的,但是实际上是 this->func()。
接下来判断 this->func() 构不构成多态?一说到构不构成多态,就得要想到实现多态的那两个必要条件:1.必须是基类的指针/引用调用虚函数;2.被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖。
先判断是否满足第一个条件?判断 this 指针是派生类的还是基类的?有人可能会想,既然 test 函数在 A 类中,那么这里的 this 指针自然是基类的;也有人会想,派生类继承了基类,那么 test 函数被继承到B类中,调用 test 函数的是 B 类型指针,将 test 函数继承下来之后,再由 B 类型的指针调用,那么这里的 this 自然是派生类的。
关于继承需要明白,继承并不是将基类的成员拷贝到派生类中,只是在生成派生类对象时,一部分由基类构成,一部分由派生类构成,派生类对象包含一个基类子对象;此外,在编译时调用函数,会寻找函数的出处,会先在派生类中找,找不到了,再去基类中找。所以这里的 this 是基类的。既然 this 指针是基类的指针,那么就满足第一条。
再来判断是否满足第二条---是否完成虚函数的重写?虽然派生类中 func 函数没有写 virtual ,但是前面有提到过基类中写了 virtual 之后,派生类的函数不写 virtual 也构成重写,并且基类和派生类的 func 函数的函数名相同,返回值类型相同,参数列表相同,所以这里完成了虚函数的重写。
判断结果即为 this->func() 构成多态。既然构成多态,那么 this 指针调用的是基类的 func 函数,还是派生类的 func 函数?因为构成多态,所以调用哪个类的 func 函数与 this 指针指向的对象有关,这里是将 B 对象的指针传给 this ,所以 this 指向的对象是 B 类,那么这里 this指针调用的是 B 类的 func 函数,那么答案出来了?选D?
并不是!!!上文说到,在满足多态的情况下,重写的意思是重写虚函数实现,也就是重写函数体。派生类的 func 函数的声明由基类提供,函数体由派生类提供。也就是说 this 指针调用的 func 函数的全体样貌是:
virtual void func(int val = 1) { std::cout << "B->" << val << std::endl; }
所以 val 使用的缺省值是 1 ,故程序的运行结果为 B->1 。
如果将这道题改一下:
class A
{
public:virtual void func(int val = 1){std::cout << "A->" << val << std::endl;}virtual void test() { func(); }
};class B : public A
{
public:virtual void func(int val = 0){std::cout << "B->" << val << std::endl;}
};int main()
{B* p = new B;p->func();return 0;
}
程序输出结果是什么?
A: A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确
答案:D
解析:
p 指针是派生类指针,调用 B 类的 func 函数,由于是派生类指针,所以不构成多态,调用的func 函数的全貌为:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
函数声明和函数体都由派生类提供,所以 val 的缺省值为 0,故程序的运行结果为 B->0。
虚函数重写的其它问题
协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
如下所示:
class Animal
{
public:virtual Animal* call(){cout << "动物叫" << endl;return nullptr;}
};class Dog : public Animal
{
private:virtual Dog* call(){cout << "小狗叫" << endl;return nullptr;}
};void Func(Animal& ptr)
{ptr.call();
}
Animal 和 Dog 类的实例化对象调用 Func 函数:
Animal animal;
Dog dog;Func(animal);
Func(dog);
运行结果:
析构函数的重写
class A
{
public:~A(){cout << "~A()" << endl;}
};class B : public A {
public:~B(){cout << "~B()->delete:" << m_p << endl;delete m_p;}
protected:int* m_p = new int[10];
};
实例化 A,B 对象:
A a;
B b;
析构函数的调用打印结果:
先构造的后析构,后构造的先析构。先析构 B 对象,调用完派生类的析构函数后,会自动调用基类的析构函数,所以会有 ~B() 和 ~A() ,再析构 A 对象,所以会有 ~A()。
普通对象还好,若是指针:
A* p1 = new A;
A* p2 = new B;delete p1;
delete p2;
析构函数的调用打印结果:
可以发现没有调用B的析构函数。而 B 中申请了额外的资源,这里会造成了内存泄漏问题。
delete p1 分成两部分:p1->析构函数 + operator delete(p1)。普通调用与类型有关,这里都是 A 类型,所以调用的都是 A 类的析构函数。但是这里 A 类型的指针有可能指向基类的对象,也有可能指向派生类的对象。在这种场景下,希望是多态调用,指向谁调用谁的析构函数。
如何满足多态调用呢?这就不得不想到实现多态的两个必要条件:1.必须是基类的指针/引用调用虚函数;2.被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖。这里确实是基类的指针和引用,但是不构成虚函数的重写,要想构成虚函数的重写,需要满足两个条件:1.是虚函数 2.函数名,参数列表,返回值相同。
析构函数没有返回值,也没有参数列表,只需看参数名,但是 A,B 类的析构函数的函数名不相同呀?C++ 为了实现析构函数的重写,特殊处理了析构函数,使之析构函数的函数名相同。所以要想实现虚函数的重写,仅需用 virtual 修饰基类的析构函数即可。
class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A {
public:~B(){cout << "~B()->delete:" << m_p << endl;delete m_p;}
protected:int* m_p = new int[10];
};
析构函数的调用打印结果:
先释放 A 对象的资源,调用 A 类中的析构函数,所以会有 ~A() ,再释放 B 对象的资源,调用 B 类中的析构函数,调用完派生类的析构函数,会自动调用基类的析构函数,所以有 ~B() 和 ~A()。
这也就解释了继承中所说的,析构函数的函数名会被特殊处理为destructor()。
总结:基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成 destructor ,所以基类的析构函数加了 vialtual 修饰,派生类的析构函数就构成重写。这也是为什么基类中的析构函数建议设计为虚函数。
override 和 final 关键字
从上面可以看出,C++ 对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来 debug 会得不偿失。
针对这个问题,C++11 提供了override 关键字,可以帮助用户检测是否重写。它放在派生类的虚函数的末尾,如下所示:
class A
{
public:virtual void func(int val = 1){ cout << "A->" << val << endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(long val = 0) override{ cout << "B->" << val << endl; }
};
按 F7 键的结果:
若不想让派生类重写基类的某个虚函数,那么可以用 final 关键字。它放在基类虚函数的末尾,如下所示:
class A
{
public:virtual void func(int val = 1) final{cout << "A->" << val << endl;}virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0){cout << "B->" << val << endl;}
};
按 F7 键的结果:
函数重载/隐藏/重写的总结
隐藏和重写有重叠:
纯虚函数和抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现,只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。如果我们不期望某个类实例化出对象,我们可以将这个类实现成抽象类。
class Animal
{
public:virtual void call() = 0;
};class Dog : public Animal
{
public:virtual void call(){cout << "小狗叫" << endl;}
};
Animal 类就是抽象类,Animal 类不能实例化出对象,Dog 可以实例化出对象。
如果 Dog 继承了基类之后,不重写纯虚函数,那么 Dog 类也是抽象类。
class Animal
{
public:virtual void call() = 0;
};class Dog : public Animal
{
public:
};
Animal 类和 Dog 类都是抽象类,都不能实例化出对象。
纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
多态的原理
静态多态的原理
int i;
double d;cout << i;
cout << d;
a,b调用的是相同的函数,却实现了不一样的结果,这就是静态多态。
在 C 语言中不允许有同名函数的存在,而 C++ 允许。编译器是如何分辨 a,b 调用的是哪一个函数。在找函数的地址时,它们调用的都是 operator<< 函数,编译器怎么区分得清楚,它们具体调用哪个重载函数?C++ 中存在一种函数名修饰规则,虽然 a,b 调用的函数名相同,但是函数的参数类型却不同, a,b 调用的函数的会被特殊处理。在找 a,b 调用的函数的时,它会带着函数的参数类型去找,如 cout<<a,会变成 operator<<i ;cout<<b,会变成 operator<<d ,一般来说是类型的首字母。
下面我们可以具体的看一看 C++ 的函数名修饰规则。
VS 编译器:
void func(int i);
void func(double d);int main()
{int i = 18;double d = 3.14;func(i);func(d);return 0;
}
func 函数只有函数的声明,所以在调用 func 函数的时,会发生链接错误。
由报错结果可知,func(int) 在 VS 编译器下处理成了 (?func@@YAXH@Z) ;func(double) 在 VS编译器下处理成了 (?func@@YAXN@Z) 。一个是 XH,一个是XN。
g++ 编译器:
将相同代码在Linux系统的编译器下运行,结果为:
也发生了链接错误,只是它这里报错并没有显示 g++ 将 func(int) 和 func(double) 处理成了什么。将函数的定义加上,去底层看看 g++ 到底将 func(int) 和 func(double) 处理成了什么。加上函数定义后,先后输入指令 g++ test.cpp 和 objdump -S a.out 。在显示的诸多信息中找到 main,再看它的上面:
由此可以知道 g++ 编译器的函数名修饰规则,func(int )被处理成了 funci;Func(double) 被处理成了 funcd 。
这仅是一个函数参数,如果有两个或者多个参数,编译器又会如何处理?
void func(int i, double d);int main()
{int i = 18;double d = 3.14;func(i, d);return 0;
}
VS 编译器:
func(int,double) 被处理成了 (?func@@YAXHN@Z)。
g++ 编译器:
func(int,double) 被处理成了 funcid 。
总结:重载函数编译链接时为了方便查找,会对重载的函数名进行修饰处理,每个编译器规则不同,但是都会把形参类型带入函数名。
动态多态的原理
要想知道动态多态的原理,需要了解虚函数指针。
概念引入:
class Basic
{
public:virtual void func1(){cout << "func1()" << endl;}protected:int m_a;char m_ch;
};int main()
{Basic b;cout << sizeof(b) << endl;return 0;
}
该程序在32位机器平台下的运行结果是什么?A.编译报错 B.运行报错 C.8 D.12。
按照类和对象所说的内存对齐,默认对齐数8,int 的默认对齐数是4,取小;char 的默认对齐数是1,取小,最终对齐数必须是默认对齐数中最大对齐数的倍数,也就是 4 的倍数,即为8。所以在32位机器平台下运行结果是 8 。答案为 C?实际运行结果是 8 吗?运行该程序,发现运行结果为12。咦?多出来的4字节是怎么一回事?Basic 类的成员变量不就只有两个吗?
来看监视窗口的对于对象 b 的监视情况:
除了 m_b 和 m_ch 成员,还多⼀个 _vfptr 放在对象的前面(注意有些平台可能会放到对象的最后⾯,这个跟平台有关),该指针就是虚函数表指针。
函数的地址都是在编译时确定,如果编译时该文件中就有函数的地址,则直接 call 函数地址;如果没有,则在链接时去其它文件中找,找不到就报链接错误。既然编译链接时就可以找到函数的地址,为什么还要用指针存储函数的地址?
在 Basic 类中新增几个虚函数:
class Basic
{
public:virtual void func1(){cout << "func1()" << endl;}virtual void func2(){cout << "func2()" << endl;}virtual void func3(){cout << "func3()" << endl;}protected:int m_a;char m_ch;
};
Basic 对象的大小仍然为12个字节,看监视窗口的对于对象 b 的监视情况:
由此可以知道,_vfptr 存储的是虚函数表的地址,虚函数表中存储的是虚函数的地址,只有虚函数的地址才会存储在虚函数表中。虚函数表也简称虚表,本质上是一个函数指针数组。
⼀个含有虚函数的类中都至少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中。
class Animal
{
public:void call(){cout << "动物叫" << endl;}
};class Dog : public Animal
{
public:virtual void call(){cout << "汪汪叫" << endl;}
};class Cat : public Animal
{
public:virtual void call(){cout << "喵喵叫" << endl;}
};void Display(Animal& ptr)
{ptr.call();
}
在 Animal 类继承体系中,传的参数是 Animal 类型,调用的就是 Animal 类中的 call 函数;传的参数是 Dog 类型,调用的就是 Dog 类中的 call 函数,这三个类中都有虚函数,也就存在虚函数表。
Animal 类的虚表存储的是 Animal 的 call 函数的地址;Dog 类的虚表存储的是 Dog 类的 call 函数地址。ptr.call() 的本质就是运行时,ptr 引用/指针指向哪个对象就去这个对象的虚函数表中找到对应的虚函数地址进行调用。这也就反应了为什么多态的一个构成条件就是基类的指针或引用调用虚函数,因为基类的指针/引用既可以指向基类的对象,也可以指向派生类的对象,在寻找虚函数的地址时,无论是基类的还是派生类的都能够找到。
从汇编层面简单看看多态和非多态在调用函数的区别:
多态:
非多态(将基类虚函数的 virtual 关键字去掉):
从两图可以清晰的知道多态与非多态的不同。不满足多态,编译/链接时就确定函数的地址;满足多态,运行时到指向对象的虚函数表中找到对应的虚函数调用。
•
对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
•
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。
虚函数表
•
基类对象的虚函数表中存放基类所有虚函数的地址,同类型的对象共用同⼀张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表
•
派生类由两部分构成,继承下来的基类和自己的成员。⼀般情况下,继承下来的基类中有虚函数表指针,派生类自己就不会再生成虚函数表指针。但是要注意这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派生类对象中的基类对象成员也独立的
class Person
{
public:virtual void identity(){cout << "Person" << endl;}protected:string m_name;int m_age;
};class Student : public Person
{
public:virtual void identity(){cout << "Studnet" << endl;}protected:string m_stuid;
};
相同类型:
Person per1;
Person per2;
监视窗口的对于对象 per1 和 per2 的监视情况:
虚函数指针的地址是一样的。
不同类型:
Person per;
Student stu;
监视窗口的对于对象 per 和 stu 的监视情况:
什么时候会有多个虚函数表?多继承时,若两个基类都有虚函数,那么派生类就有两个虚函数表。
class Teacher
{
public:virtual void teaching(){cout << "teaching" << endl;}
};class Student
{
public:virtual void learning(){cout << "learning" << endl;}
};class AY : public Teacher, public Student
{
public:virtual void teaching(){}virtual void learning(){}
};
实例化一个 AY 对象,对象名为 ay ,监视窗口的对于对象 ay 的监视情况:
从监视窗口可以看出 ay 对象中有两个虚函数指针,也就说明了有两个虚函数表。
•
派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址
•
派生类的虚函数表中包含三个部分:(1)基类的虚函数地址;(2)派生类重写的虚函数地址完成覆盖;(3)派生类自己的虚函数地址
在 Person 类和 Student 类中新增几个虚函数:
class Person
{
public:virtual void identity(){cout << "Person" << endl;}virtual void func1(){}virtual void func2(){}protected:string m_name;int m_age;
};class Student : public Person
{
public:virtual void identity(){cout << "Studnet" << endl;}virtual void func1(){}virtual void fun3(){}protected:string m_stuid;
};
identity 和 func1 函数构成重写,实例化 Person 和 Student 类,监视窗口的对于对象 per 和 stu 的监视情况:
identity 和 func1 函数重写了,所以基类和派生类的虚函数表中的 identity 函数和 func1 函数的地址不一样;func2 没有重写,所以基类和派生类的虚表中的 func2 函数的地址一样。也可以这么理解,派生类的虚表可以理解成是从基类拷贝过来的,对于重写的虚函数,就用派生类的虚函数地址覆盖,没有重写的虚函数,就保持原来的样子。
至于为什么派生类的虚表中没有 func3 虚函数的地址,这是 VS 编译器的问题。可以通过内存窗口看到,输入 stu 对象的虚表指针:
第4个地址就是虚函数 func3 的地址。
•
虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后面放了⼀个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs 系列编译器会再后面放个 0x00000000 标记,g++系列编译不会放)
如下所示:
•
虚函数存在哪?虚函数和普通函数⼀样的,编译好后是⼀段指令,都存在代码段,只是虚函数的地址有存到了虚表中
•
虚函数表存在哪的?虽然C++标准没有规定虚表的实现和存放位置,但所有主流编译器,在实践中都将虚表存储在只读数据段(也就是常量区)
要想验证虚表存在哪?需要取出虚表的地址,虚表是在对象的最开头,所以在32位平台下,虚表的地址就是对象的头4个字节,怎么取出对象的头4个字节?强制类型转换为 int* 类型,然后再解引用,就获取到了虚表的地址。可以用以下代码验证虚表存在常量区:
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈: % p\n", &i);printf("静态区: % p\n", &j);printf("堆: % p\n", p1);printf("常量区: % p\n", p2);cout << endl;Base b;Derive d;Base * p3 = &b;Derive * p4 = &d;printf("Base虚表地址: % p\n", *(int*)p3);printf("Derive虚表地址: % p\n", *(int*)p4);printf("虚函数地址: % p\n", &Base::func1);printf("普通函数地址: % p\n", &Base::func5);return 0;
}
运行结果:
栈: 010FFCDC
静态区: 0069D000
堆: 01570CA0
常量区: 0069AB94 Base虚表地址: 0069AB34
Derive虚表地址: 0069AB74
虚函数地址: 00691492
普通函数地址: 006914CE
虚表的地址都是 0069 开头,而静态区与常量区也是 0069 开头。但是 Base 虚表地址:0069AB34以及 Derive 虚表地址:0069AB74,与常量区:0069AB94 更为接近。
知识回顾
为什么你的代码总是重复?是时候了解模板了-CSDN博客
vim 入门与精要:从零到熟练-CSDN博客
面向对象编程基石:类与对象完全解析_c++的类需要掌握的知识点-CSDN博客