4继承和多态
继承和多态
继承的基本意义:
继承的内存结构:
class A
{
int a;
int b;
int c;
};
class B:public A
{
int d;
int e;
int f;
};
int main() {
//std::cout << "Hello, World!" << std::endl;
A a;
B b;
std::cout<<sizeof(a)<<" "<<sizeof(b)<<std::endl;
return 0;
}
输出:12 24
派生类和基类的成员名字可以重复,因为作用域不同比如a 可以写为A::a
访问限定问题
继承方式 基类的访问限定 派生类的访问限定 (main)外部的访问限定
public
public public Y
protected protected N
private 不可见的 N
protected(基类的成员的访问限定,在派生类里面是不可能超过继承方式的)
public protected N
protected protected N
private 不可见的 N
private
public private N
protected private N
private 不可见的 N
总结:
- 外部只能访问public的成员,procted和private的成员无法直接访问
- 在继承结构中,派生类从基类可以继承过来private的成员但是派生类却不可以直接访问
- 如果没有继承结构protected和private一样。存在继承结构,基类的成员想被派生类访问,但不想被外部访问,那么将权限设为protected
默认继承方式
如果是class定义的类那么默认就是以private继承
如果是struck定义的类那么默认就是以public继承
派生类的构造过程
派生类怎么初始化从基类继承而来的成员变量呢?
-
派生类可以继承来自基类的所有成员(变量和方法),除构造函数和析构函数
-
初始化基类继承而来的部分,只能调用基类的构造函数
派生类对象的构造和析构过程是:
- 派生类调用基类的构造函数,初始化派生类对象中属于积累的那一部分
- 派生类调用自己的构造函数,初始化自己的那一部分
- 派生类调用自己的析构函数
- 派生类调用积累的析构函数
class A
{
public:
A()
{
cout<<"A()"<<this<<endl;
}
~A()
{
cout<<"~A()"<<this<<endl;
}
int a;
int b;
int c;
};
class B: public A
{
public:
B()
{cout<<"B()"<<this<<endl;}
~B()
{cout<<"~B()"<<this<<endl;}
int d;
int e;
int f;
};
int main() {
//std::cout << "Hello, World!" << std::endl;
B b;
cout<<&(b.a)<<" "<<&(b.d)<<endl;
return 0;
}
输出;
A()0x2aa4dff8f0
B()0x2aa4dff8f0
0x2aa4dff8f0 0x2aa4dff8fc
~B()0x2aa4dff8f0
~A()0x2aa4dff8f0
基类部分位于低地址(对象的头部),派生类部分位于高地址
重载 隐藏 覆盖
class Base
{
public:
Base(int data = 10):ma(data){}
void show(){cout<<"Base::show()"<<endl;}
void show(int){cout<<"Base::show(int)"<<endl;}
private:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20,int b=0)
: Base(data),mb(b)
{
}
void show(){cout<<"Derive::show"<<endl;}
// void show(int){cout<<"Derive::show(int)"<<endl;}
private:
int mb;
};
int main()
{
Derive d;
d.show(); //派生类中含有与基类同名的变量或方法,就会将基类中同名成员或方法的隐藏掉
d.show(10); //Base::show(10);
}
这段代码会报错Too many arguments to function call, expected 0, have 1; did you mean 'Base::show'?
因为编译器在Derive的作用域中看到了有show方法,想要调用基类中的show(int)函数必须加作用域Base::
基类 = 派生类 可行 || 派生类 = 基类 不可行
class Base
{
public:
Base(int data = 10):ma(data){}
void show(){cout<<"Base::show()"<<endl;}
void show(int){cout<<"Base::show(int)"<<endl;}
private:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20,int b=0)
: Base(data),mb(b)
{
}
void show(){cout<<"Derive::show"<<endl;}
// void show(int){cout<<"Derive::show(int)"<<endl;}
private:
int mb;
};
int main()
{
Base b(10);
Derive d(20);
b = d; //Base = Derive
d = b;
}
基类的指针指向派生类对象可行 || 派生类指针指向基类对象不可行
class Base
{
public:
Base(int data = 10):ma(data){}
void show(){cout<<"Base::show()"<<endl;}
void show(int){cout<<"Base::show(int)"<<endl;}
private:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20,int b=0)
: Base(data),mb(b)
{
}
void show(){cout<<"Derive::show"<<endl;}
// void show(int){cout<<"Derive::show(int)"<<endl;}
private:
int mb;
};
int main()
{
Base b(10);
Derive d(20);
Base* pb = &d; //基类指针指向派生类对象||可行
// Derive* pd = &b; //派生类指针指向基类对象不可行
}
基类指针可以指向派生类成员,但是只能访问派生类成员的基类部分(不强转为基类对象时)
派生类指针不可以指向基类成员,因为派生类内存空间大于基类空间。若用派生类的指针指向基类成员可能设计内存的非法访问
虚函数、静态绑定、动态绑定
什么是静态绑定:
class Base
{
public:
Base(int data = 10):ma(data){}
void show(){cout<<"Base::show()"<<endl;}
void show(int){cout<<"Base::show(int)"<<endl;}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20,int b=0)
: Base(data),mb(b)
{
}
void show(){cout<<"Derive::show"<<endl;}
void show(int){cout<<"Derive::show(int)"<<endl;}
private:
int mb;
};
int main()
{
Derive d(50);
Base* p = &d;
p->show();
p->show(10);
cout<<sizeof(int)<<" "<<sizeof(void*)<<endl;
cout<<sizeof(Base)<<" "<<sizeof(Derive)<<endl;
cout<<typeid(p).name()<<" || "<<typeid(*p).name()<<endl;
}
运行结果:
Base::show()
Base::show(int)
4 8
4 8
class Base * __ptr64 || class Base
比如这个show方法的调用就是提前在编译器编译时就确定的,这就是静态的
当将基类中的函数变为虚函数时
class Base
{
public:
Base(int data = 10):ma(data){}
virtual void show(){cout<<"Base::show()"<<endl;}
virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20,int b=0)
: Base(data),mb(b)
{
}
void show(){cout<<"Derive::show"<<endl;}
void show(int){cout<<"Derive::show(int)"<<endl;}
private:
int mb;
};
int main()
{
Derive d(50);
Base* p = &d;
p->show();
p->show(10);
cout<<sizeof(int)<<" "<<sizeof(void*)<<endl;
cout<<sizeof(Base)<<" "<<sizeof(Derive)<<endl;
cout<<typeid(p).name()<<" || "<<typeid(*p).name()<<endl;
}
运行结果:
Derive::show
Derive::show(int)
4 8
16 24
class Base * __ptr64 || class Derive
当类中基类中定义了虚函数结果就完全不一样了,这其中到底发生了什么事情?
一个类中添加了虚函数对这个类有什么影响?
-
如果类中定义了虚函数,那么在编译阶段,编译器会给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTL指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的 .rodata区
-
一个类里面定义了虚函数,那么这个类定义的对象其运行时,内存中开始部分多储存了一个名为vfptr的指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表
-
一个类里面虚函数的个数不影响对象内存的大小,影响的是虚函数表的大小
-
如果派生类中的方法和基类继承来的某个方法 返回值,函数名,参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法自动处理为虚函数
RTTI: run_time_type_information 运行时的类型信息
图片中的偏移量指的是 vfptr指针 在对象中距离对象内存的偏移量,但是由于,vfptr常位于对象内存的开始位置,所以这个偏移量一般为0(在没有虚继承的情况下)
class Base
{
public:
Base(int data = 10):ma(data){}
virtual void show(){cout<<"Base::show()"<<endl;}
virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20,int b=0)
: Base(data),mb(b)
{
}
void show(){cout<<"Derive::show"<<endl;}
private:
int mb;
};
int main()
{
Derive d(50);
Base* p = &d;
//p是Base类型的指针,编译器就在Base::的作用域中找show
//若发现show是普通函数,就进行静态绑定 call Base::show()
//若发现show是virtual函数,就进行动态绑定,
/*
mov eax,dword ptr[p] 将指针pd所指对象的前四个字节放入eax
mov ecx,dword ptr[eax]
call ecx (虚函数的地址)
*/
p->show(); //(*p).show() *p时Derive类型,所以调用
p->show(10);
cout<<sizeof(int)<<" "<<sizeof(void*)<<endl;
cout<<sizeof(Base)<<" "<<sizeof(Derive)<<endl;
cout<<typeid(p).name()<<" "<<typeid(*p).name()<<endl;
/*
如果Base中没有虚函数,*p识别就是编译时类型 *p 就是 Base类型
如果Base中含有虚函数,*p识别的就是运行时类型 *p就是 Derive类型
*/
}
运行得到:
Derive::show
Base::show(int)
4 8
16 24
class Base * __ptr64 class Derive
覆盖:基类和派生类的方法同名同参同返回,而且基类方法是虚函数,那么派生类的方法就会自动处理成虚函数,他们之间就是覆盖关系
虚析构函数
那些函数不能实现成虚函数
虚函数依赖:
- 虚函数能产生地址,存储在vftabel中
- 对象必须存在 (vfptr -> vftable -> 虚函数地址),若对象不存在则vfptr就不存在
构造函数 :
- virtual + 构造函数 : 不可以,对象在构造函数之后再回产生,没有对象就没有vfptr
- 在类的构造函数当中,调用虚函数,也是静态绑定(构造函数中调用其它函数(虚),不会发生动态绑定的)
static静态成员方法(virtual static NO!!!) 。 static方法调用的本质是通过类调用的而不是通过对象,所以没有 vfptr 指针,也就不可以实现为虚函数
虚析构函数
class Base
{
public:
Base(){cout<<"Base()"<<endl;}
~Base(){cout<<"~Base"<<endl;}
Base(int data):ma(data){}
virtual void show(){cout<<"Base::show()"<<endl;}
virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(){cout<<"Derive()"<<endl;}
~Derive(){cout<<"~Derive"<<endl;}
Derive(int data,int b)
: Base(data),mb(b)
{
}
void show(){cout<<"Derive::show"<<endl;}
// void show(int){cout<<"Derive::show(int)"<<endl;}
private:
int mb;
};
int main()
{
Base* b1 = new Derive();
b1->show();
delete b1;
//编译器先看b1的类型是Base* 所以就在Base的作用域中找析构函数,发现析构函数是一个普通函数,就进行静态绑定
}
输出:
Base()
Derive()
Derive::show
~Base //这里出现了问题,派生类的析构函数没有被调用!!!
编译器看到 b1 是Base* 类型,就在Base::的作用域下寻找delete要调用的析构函数,发现析构函数是一个普通函数,就进行静态绑定call Base::~Base()
,这也就导致了Derive的析构函数没有被调用。
但是,如果将基类中的析构函数写为虚函数,就会进行动态绑定,优先调用指针实际指向对象的析构函数,之后编译器会自动调用共基类的析构函数(调试发现)
什么时候把基类的析构函数必须写为虚函数?
基类的指针(引用)指向堆上new出来的对象的时候,delete 基类指针,调用析构函数的时候,必须发生动态绑定,否则导致积累的析构函数无法被调用共
再谈动态绑定
对象调用虚函数
class Base
{
public:
Base(){cout<<"Base()"<<endl;}
virtual ~Base(){cout<<"~Base"<<endl;}
Base(int data):ma(data){}
virtual void show(){cout<<"Base::show()"<<endl;}
virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(){cout<<"Derive()"<<endl;}
~Derive(){cout<<"~Derive"<<endl;}
Derive(int data,int b)
: Base(data),mb(b)
{
}
void show(){cout<<"Derive::show"<<endl;}
// void show(int){cout<<"Derive::show(int)"<<endl;}
protected:
int mb;
};
int main()
{
Base b;
Derive d;
b.show(); //callq 0x7ff7521c1145 ; Base::show(void) __ptr64
d.show(); //callq 0x7ff7521c1145 ; Base::show(void) __ptr64
cout<<"==========="<<endl;
}
Base()
Base()
Derive()
Base::show()
Derive::show
===========
~Derive
~Base
~Base
通过反汇编可以看到。对象调用虚函数是通过静态绑定的方式调用。
那么为什么要动态绑定而不是静态绑定?
因为:假设是动态绑定,调用的是各自的虚函数表中记录的虚函数,结果是一样的。但是这一过程较为麻烦所以编译器就直接编译为了静态绑定
int main()
{
Base b;
Derive d;
Base* pb1 = &b;
pb1->show(); //动态绑定
/*
0x7ff694695464 <+68>: leaq 0x28(%rsp), %rax
0x7ff694695469 <+73>: movq %rax, 0x78(%rsp)
0x7ff69469546e <+78>: movq 0x78(%rsp), %rax
0x7ff694695473 <+83>: movq (%rax), %rax
0x7ff694695476 <+86>: movq 0x78(%rsp), %rcx
0x7ff69469547b <+91>: callq *0x10(%rax)
0x7ff69469547e <+94>: leaq 0x58(%rsp), %rax
* */
Base* pb2 = &d;
pb2->show(); //动态绑定
/*
0x7ff694695483 <+99>: movq %rax, 0x80(%rsp)
0x7ff69469548b <+107>: movq 0x80(%rsp), %rax
0x7ff694695493 <+115>: movq (%rax), %rax
0x7ff694695496 <+118>: movq 0x80(%rsp), %rcx
0x7ff69469549e <+126>: callq *0x10(%rax)
* */
cout<<"==========="<<endl;
}
int main()
{
Base b;
Derive d;
Derive* p3 = (Derive*)&b; //将基类指针强转为派生类指针
p3->show(); //调用了基类中的虚函数
}
输出:
Base()
Base()
Derive()
Base::show() //调用基类中的show函数
~Derive
~Base
~Base
因为,这里用的指针调用虚函数 ,所以要调用动态绑定。就会进入对象b内,取对象b内的vfptr,由于b对象的本质是Base类对象,Base类中又存在虚函数show,所以会调用基类中的virtual show()
动态绑定:虚函数通过指针或者引用调用,才发生动态绑定 || 如果不是通过指针或引用来调用虚函数,那就是静态绑定
理解多态
如何解释多态:
静态(编译期)的多态:函数重载。模板(函数模板,类模板)
动态(运行期)的多态:在继承结构中基类的指针或者引用指向派生类的对象,通过指针(引用)调用同名覆盖方法(虚函数)。基类指针指向那个派生类对象就会调用那个派生类对象的覆盖方法(本质就是通过动态绑定实现)
#include <iostream>
using namespace std;
class Animal
{
public:
Animal(string name) :_name(name) {}
virtual void bark() = 0; //纯虚函数
protected:
string _name;
};
// 以下是动物实体类
class Cat : public Animal
{
public:
Cat(string name) :Animal(name) {}
void bark() { cout << _name << " bark: miao miao!" << endl; }
};
class Dog : public Animal
{
public:
Dog(string name) :Animal(name) {}
void bark() { cout << _name << " bark: wang wang!" << endl; }
};
class Pig : public Animal
{
public:
Pig(string name) :Animal(name) {}
void bark() { cout << _name << " bark: heng heng!" << endl; }
};
/*
下面的一组bark API接口无法做到我们软件涉及要求的“开-闭“原则
软件设计由六大原则 “开-闭“原则 对修改关闭,对扩展开放
*/
void bark(Animal *p)
{
p->bark(); // Animal::bark虚函数,动态绑定了
/*
p->cat Cat vftable &Cat::bark
p->dog Dog vftable &Dog::bark
p->pig Pig vftable &Pig::bark
*/
}
int main()
{
Cat cat("猫咪");
Dog dog("二哈");
Pig pig("佩奇");
bark(&cat);
bark(&dog);
bark(&pig);
return 0;
}
当在一个类中定义了一个纯虚函数,那么这个类就成为了抽象类。
抽象类不能再实例化Animal anm
这是不被允许的·,但是可以定义指针和引用变量
面经
class Base
{
public:
virtual void show(int i=10){cout<<"Base::show i:"<<i<<endl;}
};
class Derive : public Base
{
public:
virtual void show(int i=20){cout<<"Derive::show i:"<<i<<endl;}
};
int main()
{
Base* pb = new Derive;
pb->show();
//输出:Derive::show i:10
//动态绑定调用派生类的 show() 的show方法
//但是,对于默认参数却调用的是基类show方法的默认参数
return 0;
}
输出:
Derive::show i:10
要理解这个问题,需要用到函数调用堆栈的过程。在调用函数时编译器会先将函数的参数压入栈中,再去执行call指令。由于这是动态绑定,所以编译器只能看到Base作用域,而不能去加载.rodata段的vfpttr,由于在调用函数时没有传入参数,那么压栈用的参数是 Base类中show的默认参数
class Base
{
public:
virtual void show(){cout<<"Base::show i:"<<endl;}
};
class Derive : public Base
{
private:
virtual void show(){cout<<"Derive::show i:"<<endl;}
};
int main()
{
Base* p = new Derive();
p->show();
//最终可以调用到Derive::show(),是在运行时期才确定的
//方法的访问权限是在编译阶段就确定的,在编译阶段,编译器只能看到是Base::p调用
//p是Base类型的指针,编译器会在Base类中找show方法,并查看权限
//看到权限是public,在类外不可以访问,通过编译
delete p;
return 0;
}
输出:
cout<<"Derive::show i:"<<endl;
class Base
{
public:
Base()
{
cout<<"Base"<<endl;
clear();
}
void clear()
{
memset(this,0,sizeof(*this));
}
virtual void show(){cout<<"Base::show :"<<endl;}
};
class Derive : public Base
{
public:
Derive(){cout<<"Derive"<<endl;}
virtual void show(){cout<<"Derive::show :"<<endl;}
};
int main()
{
/* Base* pb1 = new Base; //调用构造器,在栈帧开辟完成时,就会创建vfptr指针
pb1->show(); //动态绑定,访问Base类中的vfptr,访问了空指针
delete pb1;*/
Base* pb2 = new Derive;
pb2->show(); //动态绑定,访问Derive类中的vfptr正常
delete pb2;
return 0;
}