C++ 类公有继承,多态,虚函数,抽象基类

目录

一、公有继承

   1、基类构造函数与析构函数的自动调用

2、初始化列表调用基类构造函数反汇编

3、构造函数体中调用基类构造函数反汇编

4、子类调用父类方法

5、子类方法重定义

6、基类引用/指针的兼容

7、protected 访问控制

二、多态

1、多态定义与动态联编

2、虚析构函数

3、虚函数的继承

4、虚函数的实现

三、抽象基类

1、抽象基类定义

2、接口 


一、公有继承

      C++中没有单独关键字表示继承,而是在类定义时用冒号表示继承,冒号后面是继承的类,可以用public,private或者protected修饰,分别表示公有继承,私有继承和保护继承,默认是私有继承。A继承B,称B是基类或者父类,A是派生类或者子类,公有继承下,派生类会继承基类所有的public成员,派生类的子类可以访问派生类从基类继承的所有public成员以及派生类另外添加的public成员。无论哪一种继承,派生类都不能访问基类的private成员,只能通过public成员间接访问。先看一个简单示例,再逐一说明细节:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	Base(int i, int j);
	~Base();
	void show();
	void set(int a, int b);
};

Base::Base() {
	a = 1;
	b = 2;
	cout << "Base()\n";
}

Base::Base(int i, int j) {
	a = i;
	b = j;
	cout << "Base(int i,int j)\n";
}

Base::~Base() {
	cout << "~Base()\n";
}

void Base::show() {
	cout << "a=" << a << ",b=" << b << endl;
}

void Base::set(int a, int b) {
	this->a = a;
	this->b = b;
}

//冒号表示ClassA继承自Base,public表示公有继承,默认是私有继承
class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	ClassA(int i,int j,int k);
	~ClassA();
	void show();
	void set(int a, int b,int c);
};

//默认调用基类的构造函数
ClassA::ClassA(){
	c=3;
	cout << "ClassA()\n";
}

ClassA::ClassA(int i,int j,int k):Base(i,j),c(k){
	cout << "ClassA(int i,int j,int k)\n";
}

ClassA::~ClassA(){
	cout << "~ClassA()\n";
}

void ClassA::show(){
	//调用父类方法,Base是类名限定
	Base::show();
	cout<<",c="<<c<<endl;
}

void ClassA::set(int a,int b,int c){
	Base::set(a,b);
	this->c=c;
}

int main(){
	ClassA a;
	a.show();
	a.set(4,5,6);
	a.show();

	ClassA b(7,8,9);
	b.show();
	return 0;
}

  执行结果如下:

   

   1、基类构造函数与析构函数的自动调用

         上述示例中子类在执行构造时先执行父类的构造函数,如果未显示定义则自动调用默认构造函数,再执行子类的构造函数;当对象销毁调用析构函数时,顺序是相反的,先调用子类的构造函数,再调用父类的构造函数。先看一个更复杂的示例:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	Base(int i, int j);
	~Base();
};

Base::Base() {
	a = 1;
	b = 2;
	cout << "Base()\n";
}

Base::Base(int i, int j) {
	a = i;
	b = j;
	cout << "Base(int i,int j)\n";
}

Base::~Base() {
	cout << "~Base()\n";
}


class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	ClassA(int i,int j,int k);
	~ClassA();
};

//默认调用基类的构造函数
ClassA::ClassA(){
	c=3;
	cout << "ClassA()\n";
}

ClassA::ClassA(int i,int j,int k):Base(i,j),c(k){
	cout << "ClassA(int i,int j,int k)\n";
}

ClassA::~ClassA(){
	cout << "~ClassA()\n";
}

class ClassB: public ClassA {
private:
	int d;
public:
	ClassB();
	ClassB(int i,int j,int k,int h);
	~ClassB();
};

//默认调用基类的构造函数
ClassB::ClassB(){
	d=3;
	cout << "ClassB()\n";
}
//ClassB::ClassB(int i,int j,int k,int h):d(h){
//	cout << "ClassB(int i,int j,int k,int h)\n";
//}

ClassB::ClassB(int i,int j,int k,int h):ClassA(i,j,k),d(h){
	cout << "ClassB(int i,int j,int k,int h)\n";
}

//报错Base不是直接基类
//ClassB::ClassB(int i,int j,int k,int h):Base(i,j),d(h){
//	cout << "ClassB(int i,int j,int k,int h)\n";
//}

ClassB::~ClassB(){
	cout << "~ClassB()\n";
}


int main(){
	//加上大括号表示一个代码段,让析构函数提前执行
	{
	ClassB a;
	}
	{
	ClassB b(7,8,9,10);
	}
	return 0;
}

 执行结果如下:

 

 跟第一个示例结果一样,子类构建时按照继承顺序从基类开始依次调用构造函数,销毁时按相反的顺序从子类开始依次调用析构函数。因为子类无法直接访问父类的私有属性,所以没法直接初始化父类私有属性,只能通过父类的构造方法初始化属性,如果私有属性未初始化在父类方法执行过程中访问私有属性结果不确定,同理销毁时只能通过父类的析构方法释放父类可能占用的资源。注意非直接父类,如Base是ClassB的非直接父类,对子类是不可见的,子类无法访问非直接父类的public成员,只能通过直接父类访问。

上述示例都是使用成员初始化列表的方式初始化,如果在构造函数中初始化了?示例如下:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	Base(int i, int j);
	~Base();
};

Base::Base() {
	a = 1;
	b = 2;
	cout << "Base()\n";
}

Base::Base(int i, int j) {
	a = i;
	b = j;
	cout << "Base(int i,int j)\n";
}

Base::~Base() {
	cout << "~Base()\n";
}


class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	ClassA(int i,int j,int k);
	~ClassA();
};

//默认调用基类的构造函数
ClassA::ClassA(){
	c=3;
	cout << "ClassA()\n";
}

ClassA::ClassA(int i,int j,int k){
	Base(i,j);
	c=k;
	cout << "ClassA(int i,int j,int k)\n";
}

ClassA::~ClassA(){
	cout << "~ClassA()\n";
}

class ClassB: public ClassA {
private:
	int d;
public:
	ClassB();
	ClassB(int i,int j,int k,int h);
	~ClassB();
};

//默认调用基类的构造函数
ClassB::ClassB(){
	d=3;
	cout << "ClassB()\n";
}


ClassB::ClassB(int i,int j,int k,int h){
	ClassA(i,j,k);
	d=h;
	cout << "ClassB(int i,int j,int k,int h)\n";
}


ClassB::~ClassB(){
	cout << "~ClassB()\n";
}


int main(){
	//加上大括号表示一个代码段,让析构函数提前执行
	{
	ClassB a;
	}
	{
	ClassB b(7,8,9,10);
	}
	return 0;
}

执行结果如下: 

ClassB的无参构造函数初始化正常,比较诡异的是在构造函数体中执行父类构造函数的有参构造函数,只能从反汇编找原因了。

2、初始化列表调用基类构造函数反汇编

main方法的汇编代码如下:

   lea    -0x10(%rbp),%rax
   mov    %rax,%rdi          将this指针传入构造函数
   callq  0x4009fa <ClassB::ClassB()>
   lea    -0x10(%rbp),%rax
   mov    %rax,%rdi          把this指针即对象地址传入析构函数中
   callq  0x400abc <ClassB::~ClassB()>
   lea    -0x20(%rbp),%rax
   mov    $0xa,%r8d
   mov    $0x9,%ecx
   mov    $0x8,%edx
   mov    $0x7,%esi
   mov    %rax,%rdi      同上,第一个参数是this指针,然后4个方法参数,7,8,9,10
   callq  0x400a50 <ClassB::ClassB(int, int, int, int)>
   lea    -0x20(%rbp),%rax
   mov    %rax,%rdi
   callq  0x400abc <ClassB::~ClassB()>

   重点看ClassB::ClassB(int, int, int, int)的汇编代码:

 mov    %rdi,-0x18(%rbp)
 mov    %esi,-0x1c(%rbp)
 mov    %edx,-0x20(%rbp)
 mov    %ecx,-0x24(%rbp)  
 mov    %r8d,-0x28(%rbp)  把寄存器中的参数拷贝到栈帧中
 mov    -0x18(%rbp),%rax
 mov    -0x24(%rbp),%ecx
 mov    -0x20(%rbp),%edx
 mov    -0x1c(%rbp),%esi   将ClassA构造函数的参数从栈帧拷贝到寄存器中
 mov    %rax,%rdi             将传入ClassB构造函数的this指针传入ClassA的构造函数中
 callq  0x40096a <ClassA::ClassA(int, int, int)>
 mov    -0x18(%rbp),%rax
 mov    -0x28(%rbp),%edx
 mov    %edx,0xc(%rax)   初始化ClassB的属性d

    ClassA::ClassA(int, int, int)的汇编代码:

   mov    %rdi,-0x18(%rbp)
   mov    %esi,-0x1c(%rbp)
   mov    %edx,-0x20(%rbp)
   mov    %ecx,-0x24(%rbp)   把寄存器中的参数拷贝到栈帧中
   mov    -0x18(%rbp),%rax
   mov    -0x20(%rbp),%edx
   mov    -0x1c(%rbp),%ecx
   mov    %ecx,%esi    将Base构造函数的参数从栈帧拷贝到寄存器中
   mov    %rax,%rdi     将传入ClassA构造函数的this指针传入Base的构造函数中
   callq  0x4008c0 <Base::Base(int, int)>
   mov    -0x18(%rbp),%rax
   mov    -0x24(%rbp),%edx
   mov    %edx,0x8(%rax)   初始化ClassA的属性c

     Base::Base(int, int)的汇编代码:

mov    %rdi,-0x8(%rbp)
mov    %esi,-0xc(%rbp)
mov    %edx,-0x10(%rbp)  把寄存器中的参数拷贝到栈帧中
mov    -0x8(%rbp),%rax
mov    -0xc(%rbp),%edx
mov    %edx,(%rax)   Base的属性a赋值
mov    -0x8(%rbp),%rax
mov    -0x10(%rbp),%edx
mov    %edx,0x4(%rax)  Base的属性b赋值

   综上可知对象实例b的内存地址作为this指针传入ClassB的构造函数,再依次传入到ClassA,Base的构造函数中,通过构造函数完成各自私有属性的初始化,因此子类在内存中的数据结构是基于父类的,子类中新增的属性都是在父类的数据结构上追加的,据此可以通过指针访问或者修改父类的私有数据,从而绕开编译器规定的子类直接不能访问父类私有属性的限定。

ClassB的内存数据结构如下图:

修改main方法的代码如下:

int main(){
	cout<<"Base size:"<< sizeof(Base) <<endl;
	cout<<"ClassA size:"<< sizeof(ClassA) <<endl;
	cout<<"ClassB size:"<< sizeof(ClassB) <<endl;

	ClassB a(7,8,9,10);
	int * pt=(int *)&a;
	cout<<"a=" << *pt <<endl;
    pt++;
    cout<<"b=" << *pt <<endl;
    pt++;
    cout<<"c=" << *pt <<endl;
    pt++;
    cout<<"d=" << *pt <<endl;

	return 0;
}

 执行结果如下:

因为int在测试机器上的字节数是4,所以Base,ClassA,ClassB的大小依次是8,12,16。因为ClassB在内存中数据结构是4个int数据,所以可以通过将对象实例b的地址强转成int指针获取这4个int属性值。

3、构造函数体中调用基类构造函数反汇编

main方法的汇编代码不变,重点看ClassB::ClassB(int, int, int, int)的汇编代码:

mov    %rdi,-0x28(%rbp)
mov    %esi,-0x2c(%rbp)
mov    %edx,-0x30(%rbp)
mov    %ecx,-0x34(%rbp)
mov    %r8d,-0x38(%rbp)  将构造函数的入参从寄存器拷贝到栈帧中
mov    -0x28(%rbp),%rax
mov    %rax,%rdi   将ClassB的this指针传入ClassA的无参构造函数中
callq  0x400914 <ClassA::ClassA()>
mov    -0x34(%rbp),%ecx
mov    -0x30(%rbp),%edx
mov    -0x2c(%rbp),%esi
lea    -0x20(%rbp),%rax  
mov    %rax,%rdi   将ClassA构造函数的入参拷贝至寄存器中,注意此时传入的不是ClassB的this指针,此时相当于创建了一个                                   ClassA类实例
callq  0x40096a <ClassA::ClassA(int, int, int)>
lea    -0x20(%rbp),%rax
mov    %rax,%rdi  将上一步ClassA(int, int, int)创建的实例的地址作为this指针传入ClassA析构函数中,因为代码中并未新增属性                                 保存该实例,所以构造方法退出时需要销毁该实例
callq  0x4009e8 <ClassA::~ClassA()>
mov    -0x28(%rbp),%rax
mov    -0x38(%rbp),%edx
mov    %edx,0xc(%rax) 初始化属性d

ClassA::ClassA()的执行过程跟上一节分析的基本一致,不再重复讨论,继续看ClassA::ClassA(int, int, int)的汇编代码:

mov    %rdi,-0x28(%rbp)
mov    %esi,-0x2c(%rbp)
mov    %edx,-0x30(%rbp)
mov    %ecx,-0x34(%rbp)
mov    -0x28(%rbp),%rax
mov    %rax,%rdi
callq  0x40088e <Base::Base()>   同上,调用Base的无参构造函数完成初始化
mov    -0x30(%rbp),%edx
mov    -0x2c(%rbp),%ecx
lea    -0x20(%rbp),%rax
mov    %ecx,%esi
mov    %rax,%rdi
callq  0x4008c0 <Base::Base(int, int)>
lea    -0x20(%rbp),%rax
mov    %rax,%rdi
callq  0x4008f6 <Base::~Base()>  同上,ClassA初始化完成调用Base(int, int)创建一个临时实例,然后调用析构函数销毁

Base::Base(int, int)不涉及构造函数体中调用父类构造函数,汇编代码跟上一节一样,不做讨论。

综上分析可以得出

  • 如果在初始化列表中未显示调用其他的父类构造函数,编译器会自动调用默认构造函数
  • C++类的构造是在构造函数进入函数体前完成的,函数体中执行的代码是为了对象实例属性的初始化,即父类非默认构造函数的调用只能通过初始化列表的方式
  • 跟Java在子类构造函数体中通过super调用父类构造函数完全不同,在C++子类构造函数体中调用父类构造函数并不是初始化子类“继承”自父类的属性,而一次普通的函数调用,返回一个单独的实例

为了验证上述汇编代码分析的结论,可以修改下测试代码,加上this指针的打印,如下:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	Base(int i, int j);
	~Base();
};

Base::Base() {
	a = 1;
	b = 2;
	cout << "Base(),this->"<<this <<endl;
}

Base::Base(int i, int j) {
	a = i;
	b = j;
	cout << "Base(int i,int j),this->"<<this <<endl;
}

Base::~Base() {
	cout << "~Base(),this->"<<this <<endl;
}


class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	ClassA(int i,int j,int k);
	~ClassA();
};

//默认调用基类的构造函数
ClassA::ClassA(){
	c=3;
	cout << "ClassA(),this->"<<this <<endl;
}

ClassA::ClassA(int i,int j,int k){
	Base(i,j);
	c=k;
	cout << "ClassA(int i,int j,int k),this->"<<this <<endl;
}

ClassA::~ClassA(){
	cout << "~ClassA(),this->"<<this <<endl;
}

class ClassB: public ClassA {
private:
	int d;
public:
	ClassB();
	ClassB(int i,int j,int k,int h);
	~ClassB();
};

//默认调用基类的构造函数
ClassB::ClassB(){
	d=3;
	cout << "ClassB(),this->"<<this <<endl;
}


ClassB::ClassB(int i,int j,int k,int h){
	ClassA(i,j,k);
	d=h;
	cout << "ClassB(int i,int j,int k,int h),this->"<<this <<endl;
}


ClassB::~ClassB(){
	cout << "~ClassB(),this->"<<this <<endl;
}


int main(){
	//加上大括号表示一个代码段,让析构函数提前执行
	{
	ClassB a;
	}
	{
	ClassB b(7,8,9,10);
	}
	return 0;
}

  执行结果说明如下:

Base(),this->0xffffcc10
ClassA(),this->0xffffcc10
ClassB(),this->0xffffcc10
~ClassB(),this->0xffffcc10
~ClassA(),this->0xffffcc10
~Base(),this->0xffffcc10    ClassB实例a所在代码块执行完成
Base(),this->0xffffcc00
ClassA(),this->0xffffcc00    ClassB实例b调用ClassA和Base的默认构造函数执行完成,即构造ClassB完成,进入函数体开始                                                    ClassA(i,j,k); 的执行
Base(),this->0xffffcba4       调用Base的无参构造函数完成,即ClassA(i,j,k)时构造ClassA完成
Base(int i,int j),this->0xffffcb58   
~Base(),this->0xffffcb58     进入ClassA(int i,int j,int k)构造函数的函数体执行Base(int i,int j)创建一个临时实例,然后调用析构                                                  函数销毁
ClassA(int i,int j,int k),this->0xffffcba4    至此ClassA(int i,int j,int k)构造函数执行完成
~ClassA(),this->0xffffcba4   
~Base(),this->0xffffcba4   至此ClassA(i,j,k);执行完成,销毁其创建的临时实例
ClassB(int i,int j,int k,int h),this->0xffffcc00    至此ClassB(int i,int j,int k,int h)构造函数执行完成
~ClassB(),this->0xffffcc00
~ClassA(),this->0xffffcc00
~Base(),this->0xffffcc00    销毁ClassB实例b

4、子类调用父类方法

     公有继承下子类会继承父类的public方法,将其视为自己的public方法,子类可以添加新的方法或者覆盖原来的方法定义。当子类对象实例调用方法时会判断该方法在子类中是否有新的定义,如果有则使用子类中新定义的方法,如果没有则从直接父类开始不断往上查找直到找到方法定义为止。

    子类不能使用非直接父类的构造函数,但是可以在方法实现中直接使用非直接父类的方法,加上对应父类的类名的限定即可,如果多个父类存在同名的方法定义,则通过类名限定可以快速匹配对应的方法实现。Java中必须将子类this强转成指定父类才能调用该父类的方法实现,C++的类名限定方式更加便捷和清晰。参考如下示例:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	Base(int i, int j);
	~Base();
	void show();
	void show2();
};

Base::Base() {
	a = 1;
	b = 2;
	cout << "Base()\n";
}

Base::Base(int i, int j) {
	a = i;
	b = j;
	cout << "Base(int i,int j)\n";
}

Base::~Base() {
	cout << "~Base()\n";
}

void Base::show() {
	cout << "Base::show(), a=" << a << ",b=" << b << endl;
}

void Base::show2() {
	cout << "Base::show2(), a=" << a << ",b=" << b << endl;
}


class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	ClassA(int i,int j,int k);
	~ClassA();
	//覆盖父类的show()方法
	void show();
	//增加新的成员方法
	void show3();
};

//默认调用基类的构造函数
ClassA::ClassA(){
	c=3;
	cout << "ClassA()\n";
}

ClassA::ClassA(int i,int j,int k):Base(i,j),c(k){
	cout << "ClassA(int i,int j,int k)\n";
}

ClassA::~ClassA(){
	cout << "~ClassA()\n";
}

void ClassA::show(){
	//调用父类方法,Base是类名限定
	Base::show();
	cout<<"ClassA::show(),c="<<c<<endl;
}

void ClassA::show3(){
	Base::show2();
	cout << "ClassA::show3(), c="<<c << endl;
}


class ClassB: public ClassA {
private:
	int d;
public:
	ClassB();
	ClassB(int i,int j,int k,int h);
	~ClassB();
	void show4();
};

ClassB::ClassB(){
	d=4;
	cout << "ClassB()\n";
}


ClassB::ClassB(int i,int j,int k,int h):ClassA(i,j,k),d(h){
	cout << "ClassB(int i,int j,int k,int h)\n";
}

ClassB::~ClassB(){
	cout << "~ClassB()\n";
}

void ClassB::show4(){
	//可以直接访问非直接父类的方法,Java中需要将this指针显示强转成指定父类才能访问该父类的特定方法实现
	Base::show();
	ClassA::show();
	cout <<"ClassB::show4(),d="<<d <<endl;
}

int main(){
	Base b;
	cout<<"--------------"<<endl;
	b.show();
	cout<<"--------------"<<endl;
	b.show2();
	cout<<"--------------"<<endl;
	ClassA a;
	cout<<"--------------"<<endl;
	a.show();
	cout<<"--------------"<<endl;
    a.show2();
    cout<<"--------------"<<endl;
    a.show3();
    cout<<"--------------"<<endl;
    ClassB c;
    cout<<"--------------"<<endl;
    c.show();
    cout<<"--------------"<<endl;
    c.show2();
    cout<<"--------------"<<endl;
    c.show3();
    cout<<"--------------"<<endl;
    c.show4();
    cout<<"--------------"<<endl;
	return 0;
}

5、子类方法重定义

      C++继承有一点比较特殊,当子类重定义了父类的同名方法时,父类的同名方法就会自动隐藏,即变成子类的protected方法,不能通过子类实例直接访问,所谓重定义是指方法同名但是参数特征标不同,Java中这种情形是子类重载继承自父类的同名方法,如下示例:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	void show();
	void show2();
};

Base::Base() {
	a = 1;
	b = 2;
}


void Base::show() {
	cout << "Base::show()" << endl;
}

void Base::show2() {
	cout << "Base::show2()" << endl;
}

class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	void show(int i);
};

ClassA::ClassA(){
	c=3;
}

void ClassA::show(int i){
	Base::show();
	cout << "ClassA::show(int i)" << endl;
}

class ClassB:public ClassA {
private:
	int d;
public:
	ClassB();
	void show(int i,int j);
};

ClassB::ClassB(){
	d=4;
}

void ClassB::show(int i,int j){
	Base::show();
	Base::show2();
	ClassA::show(i);
	cout << "ClassB::show(int i)" << endl;
}

int main(){
	ClassA a;
    //编译报错
    //a.show();
	a.show(4);
	cout << "------------------" << endl;
	ClassB b;
    //编译报错
    //b.show(1);
	b.show(1, 2);
	return 0;
}

6、基类引用/指针的兼容

      通常情况下C++不允许将类型为A的指针变量指向类型为B的实例,类型为A的引用变量引用类型为B的实例,只有一种情形例外,A是B的基类。

     基类引用可以在不显示类型转换的情况下引用子类实例,因此可以用子类实例直接初始化基类或者给基类赋值,这会调用默认的复制构造函数或者默认赋值运算符重载函数完成,Java中将子类实例赋值给基类实例只是对象引用复制并未创建一个新的基类对象,注意引用自子类实例的基类引用不能调用子类方法,因为编译器只会将其作为基类引用,不会额外记录实际引用的子类实例类型。因为将子类实例赋值给基类实际是创建了一个新的基类实例,因此将基类实例强转成子类实例是无意义的,基类实例中已经丢失了子类特有的数据了,另外C++不同于Java中的类型强转,如果赋值运算符两侧的变量类型不符,编译器是自动调用匹配的复制构造函数或者类型转换函数,除非有该函数定义,否则强转报错no matching function。因为引用必须通过实例初始化,因此将实际引用自子类的基类引用强转至子类引用时编译器会先调用以基类引用为入参的复制构造函数创建一个临时的子类实例,除非有该函数定义,否则强转报错no matching function。

    基类指针可以在不显示类型转换的情况下指向子类实例,注意此时基类指针不能调用子类实例的方法,同上编译器只将其作为基类指针。原本指向子类实例的基类指针可以通过显示强转变成子类指针,然后可以调用子类的特定方法或者实现。

基于上一节的示例,修改下main方法,测试代码如下:

int main(){
	//将子类赋值给基类,实际调用的是Base的复制构造函数,将ClassA()创建的临时变量作为入参Base引用
	Base b=ClassA();
	cout<<"--------------"<<endl;
	b.show();
	cout<<"--------------"<<endl;

	ClassB c;
	cout<<"--------------"<<endl;
	//基类的引用可以在不进行显示转换的情况下引用子类实例
	Base & b2=c;
	//基类的指针可以在不进行显示转换的情况下指向子类实例
	Base * b3=&c;
	b2.show();
	cout<<"--------------"<<endl;
	b3->show2();
	cout<<"--------------"<<endl;

    //不能将基类类实例或者类引用直接强转成子类,强转实际调用的是复制构造函数或者类型转换函数
	//如果没有对应的函数定义则转换报错
//	ClassA a=(ClassA)b;
//	a.show3();
//	cout<<"--------------"<<endl;

//	ClassB & c3=(ClassB)b2;
//	c3.show4();
//	cout<<"--------------"<<endl;

	//将实际指向子类实例的指针可以强转成子类指针,可以调用子类的对应方法
	ClassB* c2=(ClassB*)b3;
	c2->show4();
	cout<<"--------------"<<endl;

	return 0;
}

执行结果如下:

   基类引用或者指针可以在不显示类型转换的情况下引用或者指向子类实例,这种称为隐式向上类型转换。那么可以向上转换成基类的基类么,如上面示例中的Base?当然可以,因为子类保存了所有基类的数据。修改main方法代码:

int main(){
	ClassB b;
	ClassA & a=b;
	a.show(1);
	Base & a2=b;
	a2.show();
	Base * a3=&b;
	a3->show();
	return 0;
}

 执行结果如下:

7、protected 访问控制

     proteccted成员跟private不同,可以在子类代码中直接访问,等同与public成员,但是不同像public成员一样通过类对象实例直接访问,如下示例:

#include <iostream>

using namespace std;

class Base {
private:
	int a=1;
	int b=2;
protected:
	void show();
public:
	void show2();
};

void Base::show() {
	cout << "Base::show()" << endl;
}

void Base::show2() {
	cout << "Base::show2()" << endl;
}

class ClassA: public Base {
private:
	int c=3;
protected:
	void show2();
public:
	void show();
};

ClassA::ClassA(){
	c=3;
}

void ClassA::show(){
	Base::show();
	cout << "ClassA::show(int i)" << endl;
}

void ClassA::show2(){
	Base::show();
	cout << "ClassA::show2(int i)" << endl;
}

class ClassB:public ClassA {
private:
	int d=4;
public:
	void show3();
};

ClassB::ClassB(){
	d=4;
}

void ClassB::show3(){
	Base::show();
	Base::show2();
	ClassA::show();
	ClassA::show2();
	cout << "ClassB::show(int i)" << endl;
}

int main(){
	ClassA a;
	//重定义改变了基类同名函数的访问控制,show从protected变成public
	//show2从public变成protected
	a.show();
	//编译报错,protected成员只能在子类直接访问
//	a.show2();
	cout << "------------------" << endl;
	ClassB b;
	b.show();
	b.show3();
	return 0;
}

二、多态

1、多态定义与动态联编

    上一节中基类引用可以引用子类实例,基类指针可以指向子类实例,但是这种情况下只能调用基类的方法;如果子类覆盖了基类的方法定义,这时调用该基类方法只能使用基类的实现,如下示例:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	Base(int i, int j);
	~Base();
	void show();
};

Base::Base() {
	a = 1;
	b = 2;
}

Base::Base(int i, int j) {
	a = i;
	b = j;
}

Base::~Base() {
	cout << "~Base()\n";
}

void Base::show() {
	cout << "Base::show(), a=" << a << ",b=" << b << endl;
}

class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	ClassA(int i, int j, int k);
	~ClassA();
	void show();
};

//默认调用基类的构造函数
ClassA::ClassA() {
	c = 3;
}

ClassA::ClassA(int i, int j, int k) :
		Base(i, j), c(k) {
}

ClassA::~ClassA() {
	cout << "~ClassA()\n";
}

void ClassA::show() {
	cout << "ClassA::show(),c=" << c << endl;
}

class ClassB: public ClassA {
private:
	int d;
public:
	ClassB();
	ClassB(int i, int j, int k, int h);
	~ClassB();
	void show();
};

ClassB::ClassB() {
	d = 4;
}

ClassB::ClassB(int i, int j, int k, int h) :
		ClassA(i, j, k), d(h) {
}

ClassB::~ClassB() {
	cout << "~ClassB()\n";
}

void ClassB::show() {
	cout << "ClassB::show(),d=" << d << endl;
}

void test(Base & ba) {
	ba.show();
}

void testp(Base * ba) {
	ba->show();
}

int main() {

	Base bs = Base();
	ClassA a = ClassA();
	ClassB b = ClassB();
	Base * bs2 = &bs;
	Base * a2 = &a;
	Base * b2 = &b;
	cout << "--------------" << endl;
	bs2->show();
	a2->show();
	b2->show();
	cout << "--------------" << endl;
	testp(bs2);
	testp(a2);
	testp(b2);
	cout << "--------------" << endl;

	Base & bs3 = bs;
	Base & a3 = a;
	Base & b3 = b;
	bs3.show();
	a3.show();
	b3.show();
	cout << "--------------" << endl;
	test(bs3);
	test(a3);
	test(b3);
	cout << "--------------" << endl;

	return 0;
}

直接结果如下:

     

如上述示例的test(Base & ba)和testp(Base * ba)方法,传入方法的入参可能引用/指向 子类或者基类实例,最终调用的都是基类的方法实现,能否实现根据基类引用/指针实际引用/指向的实例类型调用该实例类型的方法实现呢?这种根据基类引用/指针实际引用/指向的实例类型调用该实例类型的方法实现的行为就称为多态。Java继承体系中是原生支持多态的,不需要任何特殊声明,C++中为了保证非多态方法执行效率,引入了virtual关键字,在类定义时用virtual修饰方法称为虚方法或者虚函数,编译器会对该虚方法做特殊处理以实现多态。

修改上述示例代码,Base类定义的show方法前加上virtual关键字,然后重新编译执行,结果如下:

将代码中函数调用如上述示例test(Base & ba)方法中的ba.show();解析成特定的函数代码块或者函数实现的执行,即确定被调用函数的指令地址的过程称为函数联编,普通方法在编译期就能确定实际调用的函数实现,这种称为静态联编或者早期联编,虚方法因为在编译期无法确定实际的对象类型,只能在运行时获取正确的函数实现,这种称为动态联编或者晚期联编。

使用虚函数需要注意如下事项:

  • 构造函数不能是虚函数,因为构造函数不能被子类继承,也不能被重写,所以声明成虚函数是无意义的
  • 通常应该给基类提供一个虚析构函数,即使该析构函数什么都不做,方便delete释放new创建的子类实例时能够正确调用子类和基类的析构函数
  • 友元不能是虚函数,因为虚函数必须是类成员,友元不是类成员

2、虚析构函数

     上市示例中析构函数并不是虚的,创建的三个对象实例也都正确的调用了析构函数,这是因为Base指针/引用指向/引用的对象实例的类型是正确的,当对象销毁时会根据类型信息正确调用对应的析构函数。有一种情形特殊,当Base指针指向通过new创建的子类实例的情形,如下示例:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	Base(int i, int j);
	~Base();
};

Base::Base() {
	a = 1;
	b = 2;
	cout << "Base(),this->"<<this<<endl;
}

Base::Base(int i, int j) {
	a = i;
	b = j;
}

Base::~Base() {
	cout << "~Base(),this->"<<this<<endl;
}


class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	ClassA(int i, int j, int k);
	~ClassA();
};

ClassA::ClassA() {
	c = 3;
	cout << "ClassA(),this->"<<this<<endl;
}

ClassA::ClassA(int i, int j, int k) :
		Base(i, j), c(k) {
}

ClassA::~ClassA() {
	cout << "~ClassA(),this->"<<this<<endl;
}


class ClassB: public ClassA {
private:
	int d;
public:
	ClassB();
	ClassB(int i, int j, int k, int h);
	~ClassB();
};

ClassB::ClassB() {
	d = 4;
	cout << "ClassB(),this->"<<this<<endl;
}

ClassB::ClassB(int i, int j, int k, int h) :
		ClassA(i, j, k), d(h) {
}

ClassB::~ClassB() {
	cout << "~ClassB(),this->"<<this<<endl;
}

int main(){
	{
	//编译报错不能把自动把非ClassB变量赋值给非const ClassB引用
//	ClassB & a=ClassB();
	const ClassB & a=ClassB();
	//编译报错 不能对临时变量取地址
//	ClassB * a2=&ClassB();
    ClassB * a3=new ClassB();
    delete a3;
	}
	cout << "--------------" << endl;
	{
    //可以正常调用析构函数,在代码执行过程中会创建一个正确类型的临时变量,然后让b引用该临时变量
	//当该临时变量销毁时会根据临时变量而非引用类型正确调用析构函数
    const Base & b=ClassB();
    Base * b2=new ClassB();
    //只调用了Base的析构函数,b2指向的对象实例的销毁由delete实现,delete只能根据指针类型调用对应的析构函数
    //无法获取指针实际指向的实例类型
    delete b2;
	}

	return 0;
}

  执行结果如下:

  

当delete b2 销毁b2指向的对象实例时只调用了Base的析构函数,未正确调用子类ClassB和ClassA的析构函数,这是因为delete销毁时只能根据b2的指针类型判断对象实例类型,无法获取实际指向的实例类型。为了保证这种情形下依然能够按照指针实际指向的类类型调用正确的析构函数,需要将Base的析构函数也声明成虚函数,修改后测试结果如下:

 

3、虚函数的继承

     上面两个示例中都是在Base中声明虚函数,子类的ClassA和ClassB并未声明虚函数,这时如果ClassA指针指向ClassB的实例会有多态效果么?

虚show()方法的示例main方法修改如下:

int main(){
	ClassB b=ClassB();
	ClassA & a=b;
	ClassA * a2=&b;
	a.show();
	a2->show();
	return 0;
}

执行结果如下:

虚析构函数的示例main方法修改如下:

int main(){
	ClassA * a=new ClassB();
	delete a;
	return 0;
}

 执行结果如下:

 综上可以得知虚函数的声明可以从基类继承,当基类声明了虚函数,子类在覆盖基类的虚函数定义时无需重新声明成虚函数,自动继承基类的虚函数声明。

4、虚函数的实现

     Java中实现多态是基于类型信息的,每个Java对象实例都保存了指向该对象实例的类型Class对象的引用,参考《Java程序员自我修养——内存模型》,Java可以根据对象的实际类型调用该类型的特定方法实现从而实现多态。C++的对象实例在内存中是没有任何类型信息的,引用变量或者指针变量在内存中就是一个内存地址而已也没有任何内存信息,那C++的多态是如何实现的呢?

      C++另辟蹊径,静态联编时通常是在符号解析和静态链接的过程中确定方法调用实际的指令地址,动态联编时是从虚函数表中获取方法调用的实际指令地址,虚函数表实际是一个指针数组,按照虚函数的声明顺序保存了当前对象实例所属的实际类型的所有虚函数的指令地址,如果未重写基类的虚方法,则为基类虚方法实现的指令地址,如果重写了则为子类虚方法实现的指令地址。对声明了虚函数的类,编译器会自动添加一个隐藏的指向该类的虚函数表的指针,当调用某个实例的虚方法时,会根据该指针找到该实例所属类的特定虚方法实现的指令地址,然后调用该方法。因为存在中间虚函数表的转换过程,所以虚方法执行效率相比静态联编的普通方法要差一点,所以C++中默认是静态联编,只有使用了virtual修饰的方法才会使用动态联编。那么虚函数表是什么时候创建,由谁创建和销毁了?从汇编找答案。

      测试代码如下:

#include <iostream>

using namespace std;

class Base {
private:
	int a;
	int b;
public:
	Base();
	Base(int i, int j);
	virtual ~Base();
	virtual void show();
	virtual void show2();
	virtual void show3();
	void show4();
};

Base::Base() {
	a = 1;
	b = 2;
}

Base::~Base() {

}


Base::Base(int i, int j) {
	a = i;
	b = j;
}

void Base::show() {
	cout << "Base::show(), a=" << a << ",b=" << b << endl;
}

void Base::show2() {
	cout << "Base::show2(), a=" << a << ",b=" << b << endl;
}

void Base::show3() {
	cout << "Base::show3(), a=" << a << ",b=" << b << endl;
}

void Base::show4() {
	cout << "Base::show4(), a=" << a << ",b=" << b << endl;
}

class ClassA: public Base {
private:
	int c;
public:
	ClassA();
	ClassA(int i, int j, int k);
	void show2();
};

//默认调用基类的构造函数
ClassA::ClassA() {
	c = 3;
}

ClassA::ClassA(int i, int j, int k) :
		Base(i, j), c(k) {
}



void ClassA::show2() {
	cout << "ClassA::show2(),c=" << c << endl;
}

class ClassB: public ClassA {
private:
	int d;
public:
	ClassB();
	ClassB(int i, int j, int k, int h);
	void show3();
};

ClassB::ClassB() {
	d = 4;
}

ClassB::ClassB(int i, int j, int k, int h) :
		ClassA(i, j, k), d(h) {
}

void ClassB::show3() {
	cout << "ClassB::show3(),d=" << d << endl;
}

int main(){
	Base * c=new ClassA();
	Base * c2=new ClassB();
	cout<<"int size:"<<sizeof(int) <<endl;
	cout<<"Base size:"<<sizeof(Base) <<"_Alignof size:" <<alignof(Base) <<endl;
	cout<<"ClassA size:"<<sizeof(ClassA) <<"_Alignof size:" <<alignof(ClassA)<<endl;
	cout<<"ClassB size:"<<sizeof(ClassB) <<"_Alignof size:" <<alignof(ClassB)<<endl;
	c->show2();
	c2->show3();

	c->show4();

	delete c;
	delete c2;

	return 0;
}

 执行结果如下:

 

 将上述示例中Base的析构函数,show(),show2(),show3()方法都加上virtual修饰后,执行结果如下:

 

64位系统下指针变量为8字节,类大小由原来4字节对齐变成8字节对齐,Base的大小8+8=16,符合对齐要求,ClassA的大小12+8=20,因为8字节对齐变成24,ClassB的大小由16变成24,符合对齐要求。

Base()反汇编代码如下:

mov    %rdi,-0x8(%rbp)   将this指针从rdi拷贝至栈帧
mov    -0x8(%rbp),%rax   
movq   $0x4012b0,(%rax) 将虚函数表的地址写入到this指针后8个字节中
mov    -0x8(%rbp),%rax
movl   $0x1,0x8(%rax)   初始化属性a
mov    -0x8(%rbp),%rax
movl   $0x2,0xc(%rax) 初始化属性b

ClassA()反汇编代码如下:

mov    %rdi,-0x8(%rbp) 将this指针从rdi拷贝至栈帧
mov    -0x8(%rbp),%rax
mov    %rax,%rdi    将入参的this指针传入Base构造函数
callq  0x400a6e <Base::Base()>
mov    -0x8(%rbp),%rax  
movq   $0x401270,(%rax)  覆写Base构造函数中已经初始化的虚函数表地址
mov    -0x8(%rbp),%rax
movl   $0x3,0x10(%rax)   初始化属性c

ClassB()反汇编代码如下:

 mov    %rdi,-0x8(%rbp)  将this指针从rdi拷贝至栈帧
 mov    -0x8(%rbp),%rax
 mov    %rax,%rdi    将入参的this指针传入ClassA构造函数
 callq  0x400c50 <ClassA::ClassA()>
 mov    -0x8(%rbp),%rax
 movq   $0x401230,(%rax)  覆写ClassA构造函数中已经初始化的虚函数表地址
 mov    -0x8(%rbp),%rax
 movl   $0x4,0x14(%rax)   初始化属性c

 至此可以得出结论使用虚函数后,编译器自动添加的指向虚函数表的指针变量是作为类的第一个成员属性保存的,虚函数表在编译阶段生成,同代码指令一样保存在不可修改的代码段中,虚函数表的地址在链接阶段确认,并写入构造函数的汇编指令中。

main方法中调用show方法和delete指令的汇编代码如下:

<+301>: mov    -0x18(%rbp),%rax  变量c的地址复制到rax中
<+305>: mov    (%rax),%rax    将rax中内存地址后8字节的内存数据即虚函数表的地址拷贝到rax中
<+308>: add    $0x18,%rax   rax中的地址自增24,即从虚函数表中获取虚方法show2所在的数组元素地址
<+312>: mov    (%rax),%rax  将show2方法的真实地址拷贝到rax中
<+315>: mov    -0x18(%rbp),%rdx
<+319>: mov    %rdx,%rdi   将变量c的地址即this指针放入rdi寄存器
<+322>: callq  *%rax    调用rax中内存地址指向的方法,即调用子类的show2方法
<+324>: mov    -0x20(%rbp),%rax  变量c2的地址复制到rax中
<+328>: mov    (%rax),%rax   
<+331>: add    $0x20,%rax    rax中的地址自增32,即从虚函数表中获取虚方法show3所在的数组元素地址
<+335>: mov    (%rax),%rax
<+338>: mov    -0x20(%rbp),%rdx
<+342>: mov    %rdx,%rdi
<+345>: callq  *%rax     调用子类的show3方法
<+347>: mov    -0x18(%rbp),%rax
<+351>: mov    %rax,%rdi
<+354>: callq  0x400c50 <Base::show4()>   子类没有重写show4方法,直接使用子类的实现
<+359>: cmpq   $0x0,-0x18(%rbp)   指针变量判空,如果是空指针则通过je指令跳转到389处的指令,否则继续je下一行指令
<+364>: je     0x400fa4 <main()+389>
<+366>: mov    -0x18(%rbp),%rax    变量c的地址复制到rax中
<+370>: mov    (%rax),%rax
<+373>: add    $0x8,%rax     rax中的地址自增8,即从虚函数表中获取虚方法析构函数所在的数组元素地址
<+377>: mov    (%rax),%rax
<+380>: mov    -0x18(%rbp),%rdx
<+384>: mov    %rdx,%rdi
<+387>: callq  *%rax     调用子类的析构函数的实现
<+389>: cmpq   $0x0,-0x20(%rbp)
<+394>: je     0x400fc2 <main()+419>
<+396>: mov    -0x20(%rbp),%rax   变量d的地址复制到rax中
<+400>: mov    (%rax),%rax
<+403>: add    $0x8,%rax    rax中的地址自增8,即从虚函数表中获取虚方法析构函数所在的数组元素地址
<+407>: mov    (%rax),%rax
<+410>: mov    -0x20(%rbp),%rdx  
<+414>: mov    %rdx,%rdi
<+417>: callq  *%rax   调用子类的析构函数的实现

上述汇编代码演示了C++中多态实现的底层逻辑,当基类指针调用虚方法时,首先去虚函数表中找到当前实例实际虚函数实现的指令地址,然后设置方法参数和this指针,最后调用该指令地址对应的方法。

上述示例修改main方法如下:

int main() {
	typedef void (*FUNC)();
	ClassB a;
	Base * b=&a;
	long *vp = (long *)(*(long*)&a);
	FUNC f = (FUNC) vp[0];
	FUNC f2 = (FUNC) vp[1];
	FUNC f3 = (FUNC) vp[2];
	FUNC f4 = (FUNC) vp[3];
	FUNC f5 = (FUNC) vp[4];

    cout<<"end\n";

}

在gdb下调试,先执行b 125给cout这行加上断点,125是行号,然后run,gdb停在断点出,然后用p命令打印f,f2,f3,f4,f5所指向的函数实现,如下图:

上述代码是模拟从虚函数表获取函数地址的逻辑来获取虚函数表的内容的,gdb的info vtbl提供了打印虚函数表的功能,如下图,因为代码改了,显示定义了析构函数所以打印的地址跟上图不一样。

因为ClassA、ClassB都没有覆写Base的show方法,所以虚函数表保存show()方法的地址的是Base的实现,ClassA只改写了show2()方法,ClassB只改写了show3()方法,所以虚函数表保存的show2()方法的地址是ClassA的实现,show3()方法的地址是ClassB的实现。那为啥这里会有两个析构函数的地址了?

修改main方法代码,如下,继续测试:

int main() {
	//当大括号退出时会自动销毁变量a
	{
	typedef void (*FUNC)();
	ClassB a;
	Base * b=&a;
	long *vp = (long *)(*(long*)&a);
	FUNC f = (FUNC) vp[0];
	FUNC f2 = (FUNC) vp[1];
    cout<<"end\n";
	}

	Base * b2=new ClassB();
	delete b2;

}

进入gdb调试,执行b 123 在cout处设置断点,执行b 127在delete处设置断点,执行set disassemble-next-line    on开启反汇编,然后run, gdb停在第一个断点出,用p f和p f2打印两个析构函数的地址,如下图:

然后执行ni命令,进入cout这行代码的汇编指令的执行,如下图:

cout在上一个callq指令调用完成,然后执行ClassB的析构函数销毁变量b,此时析构函数的地址是0x40101e,即f的地址

输入c,gdb停在下一个断点,继续ni直到最后一步callq,执行info registers rax,查看rax寄存器的数据,如下图:


rax中的值跟f2的地址是一样的,继续si,查看这个函数跟f指向的析构函数有啥区别,如下图

这个函数调用了ClassB的析构函数,最后释放该变量占用的堆内存,据此判断该函数应该是编译器自动添加的delete时调用用于释放资源的虚函数。

三、抽象基类

1、抽象基类定义

     抽象基类类似于Java中的抽象类,声明了纯虚函数的类称为抽象基类,抽象基类可以提供虚函数的实现也可不提供,注意抽象基类不能实例化,只能用作基类。子类必须提供参数特征标一致的纯虚函数的实现,否则编译报错。

#include <iostream>

using namespace std;

class Base {
private:
	int a=1;
	int b=2;
public:
	virtual ~Base();
	//纯虚函数必须用virtual修饰
	virtual void show()=0;
};

Base::~Base(){

}

//Base可以提供纯虚函数的实现,也可以不提供
void Base::show(){
	cout << "Base::show()" << endl;
}

class ClassA: public Base {
private:
	int c=3;
public:
	void show();
	void show(int i);
};

void ClassA::show(){
	Base::show();
	cout << "ClassA::show()" << endl;
}

void ClassA::show(int i){
	cout << "ClassA::show(int i)" << endl;
}



int main(){
	//如果类声明中包含纯虚函数则不能创建该类的实例,只能用作基类
//	Base b;
	//子类必须复写基类的纯虚函数,否则编译报错
    ClassA a;
    a.show();
    a.show(2);

	return 0;
}

2、接口 

C++中没有单独表示接口的关键字,只能用只包含纯虚函数的类代替,如下示例:

#include <iostream>

using namespace std;

class Base {
public:
	virtual ~Base();
	//纯虚函数必须用virtual修饰
	virtual void show()=0;
	virtual void show2()=0;
};

Base::~Base(){

}


class ClassA: public Base {
private:
	int c=3;
public:
	void show();
	void show2();
};

void ClassA::show(){
	cout << "ClassA::show()" << endl;
}

void ClassA::show2(){
	cout << "ClassA::show2()" << endl;
}

class ClassB: public Base {
private:
	int c=3;
public:
	void show();
	void show2();
};

void ClassB::show(){
	cout << "ClassB::show()" << endl;
}

void ClassB::show2(){
	cout << "ClassB::show2()" << endl;
}



int main(){
    ClassA a;
    a.show();
    a.show();
    ClassB b;
    b.show();
    b.show();

	return 0;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值