继承的基本意义
1.继承的本质和原理
继承的本质:代码的复用
类和类之间的关系:组合,继承
继承示例代码:
class person
{
public:
int ma;
protected:
int mb;
private:
int mc;
};
class student : public person
{
public:
int md;
protected:
int me;
private:
int mf;
};

继承方式 基类的访问限定 派生类的访问限定 外部的访问限定
| 继承方式 | 基类的访问限定 | 派生类的访问限定 | 外部的访问限定 |
|---|---|---|---|
| public | public | public | Y |
| public | protected | protected | N |
| public | private | 不可见 | N |
| protected | public | protected | N |
| protected | protected | protected | N |
| protected | private | 不可见 | N |
| private | public | private | N |
| private | protected | private | N |
| private | private | 不可见 | N |
总结:
1-外部只能访问对象public的成员,protected和private的成员无法直接访问。
2-在继承结构中,派生类从基类可以继承过来private的成员,但派生类却无法直接访问。
3-protected和private的区别?在基类中定义的成员,像被派生类访问,但是不想被外部访问,那么在基类中把相关成员定义为protected;如果派生类和外部都不打算直接访问,那么在基类中就定义为private。
默认的继承方式是?
要看派生类是用class定义的还是struct定义的
如果是class定义,默认定义是private
如果是struct定义,默认就是public
派生类的构造过程
派生类怎么初始化从基类继承而来的成员变量?
1.派生类从基类可以继承来所有成员(变量和方法),除了构造函数和析构函数
那么派生类怎么初始化从基类继承而来的成员变量呢?
答:通过调用基类的构造函数
派生类的构造函数和析构函数,负责初始化和清理派生类部分
派生类从基类继承来的成员的初始化和清理 — 由基类的构造和析构来负责
派生类构造具体过程
派生类调用基类构造函数 =》 初始化继承的基类成员 =》 调用派生类自己的构造函数 =》 初始化派生类自己特有的成员
重载、隐藏、覆盖
-
重载:
一组函数要重载,必须要在同一作用域下:而且函数名相同,参数列表不同 -
隐藏(作用域的隐藏)的关系:
在继承结构中,派生类的同名成员,把基类的同名成员隐藏了
虚函数、静态绑定和动态绑定
示例代码:
#include<iostream>
using namespace std;
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):Base(data),mb(data){}
void show() { cout << "Derive::show()" << endl; }
private:
int mb;
};
int main()
{
Derive d(50);
Base* pb = &d;
pb->show(); //静态(编译时期)绑定(函数)Base::show()
pb->show(10);//Base::show()
cout << sizeof(Base) << endl;//4
cout << sizeof(Derive) << endl;//8
cout << typeid(pb).name() << endl;//class Base*
cout << typeid(*pb).name() << endl;//class Base
return 0;
}
发现并不会调用派生类的方法,因为在编译时期,已经绑定了基类Base的方法
将方法定义成虚函数
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;
};
//Derive::show()
//Base::show(int)
就会使用动态绑定
如果类里面定义了虚函数,那么在编译阶段,编译器给这个类类型产生一个唯一的vftable函数表,虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区。
一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始的部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。
一个类型定义的n个对象,他们的vfpt指向的都是同一张虚函数表
一个类里面,虚函数的个数不影响对象内存大小(vfpt),影响的是虚函数表的大小。
如果派生类中的方法,和基类继承来的方法,返回值,函数名,参数列表都相同,而基类的方法是虚函数,那么这个派生类的方法,自动处理成虚函数。(覆盖关系)
虚析构函数
问题一:哪些函数不能实现成虚函数?
虚函数:
1.虚函数能产生函数地址,存储在vftable当中
2.需要依赖对象,对象必须存在(vfptr -》vftable 虚函数地址)
构造函数中调用的任何函数都是静态绑定。
vitrual + static !不可能
问题二:析构函数调用的时候,对象是存在的,所以可以使用虚函数
虚析构作用:
如果析构函数不使用虚函数(动态绑定),那么使用基类指针指向派生类时,析构函数会静态绑定成基类的析构函数。此时如果派生类有自己开辟的内存时,这块内存由于没有调用派生类的析构函数而发生内存泄漏。
什么时候把基类的析构函数必须实现成虚函数?
基类指针指向堆上new出来的派生类对象时,delete pb基类的指针,它调用析构函数的时候,必须发生动态绑定。
再谈动态绑定
是不是虚函数的调用一定就是动态绑定?
不是的,在构造函数中,就算使用虚函数,也是静态绑定
其次,如果表示通过指针或者引用变量来调用虚函数,那么就是静态绑定
class Base
{
public:
Base(int data = 10)
:ma(data){}
virtual void show() { cout << "Base::show()" << endl; }
~Base() { cout << "~Base()" << endl; }
virtual void show(int) { cout << "Base::show(int)" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20):Base(data),mb(data){}
virtual void show() { cout << "Derive::show()" << endl; }
private:
int mb;
};
int main()
{
Base b;
Derive d;
b.show();//虚函数 call Base::show
d.show();//虚函数 call Derive::show
//都是静态绑定
//使用对象本身调用虚函数,是静态绑定
//动态绑定
Base* pb1 = &b;
pb1->show();
Base* pb2 = &d;
pb2->show();
Base& rb1 = b;
rb1.show();
Base& rb2 = d;
rb2.show();
return 0;
}
理解多态到底是什么
静态(编译)多态:
函数重载,模板(函数模板,类模板)
动态(运行)多态:
在继承结构在,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数)。
基类指针指向哪个派生类对象,就会调用哪个派生类对象的覆盖方法,称为多态。
phase=》show()
底层是通过动态绑定来实现的。phase=》访问谁的vfptr=》继续访问谁的vftable=》当然是对应的派生类对象的方法了
理解抽象类
抽象类和普通类有什么区别?
拥有纯虚函数的类叫做抽象类
抽象类不能再实例化对象了,但是可以定义指针和引用变量
一般把什么设计成抽象类?
一般把基类设计为抽象类
继承多态笔试题实战分析
案例一:分析结果
#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<<"Cat"<<endl; }
};
class Dog : public Animal
{
public:
Dog(string name) :Animal(name) {}
void bark() { cout<<"Dog"<<endl; }
};
int main()
{
Animal* p1 = new Cat("加菲猫"); //vfptr -> dog vftable
Animal* p2 = new Dog("二哈");
int* p11 = (int*)p1;//p11[0],访问是Cat的前4字节 ---- vfptr
int* p22 = (int*)p2;//p22[0],访问是Dog的前4字节 ---- vfptr
int tmp = p11[0];
p11[0] = p22[0];
p22[0] = tmp;
//相当于把指向虚函数表的指针替换了
p1->bark();
p2->bark();
delete p1;
delete p2;
return 0;
}
案例二:
class Base
{
public:
virtual void show(int i = 10)
{
cout << "call Base::show i :: " << i << endl;
}
};
class Derive :public Base
{
public:
void show(int i = 20)
{
cout << "call Derive::show i::" << i << endl;
}
};
int main()
{
Base* p1 = new Derive();
/*
* 函数调用,参数压栈是在编译时期就确定好了的
push 0Ah
mov eax, dword ptr[p1]
mov ecx, dword ptr[eax]
call ecx
运行时绑定,已经定下来了是Base类型的指针
原来的Derive的形参默认值永远是没用的
只有用Derive的指针才有用
*/
p1->show();
//call Derive::show i:: 10 ----- ? ? ?
delete p1;
return 0;
}
案例三:将派生类的方法写成私有的,结果发现还是可以调用
class Base
{
public:
virtual void show(int i = 10)
{
cout << "call Base::show i :: " << endl;
}
};
class Derive :public Base
{
private:
void show(int i = 20)
{
cout << "call Derive::show i::" << i << endl;
}
};
int main()
{
Base* p1 = new Derive();
p1->show();
delete p1;
return 0;
}
//call Derive::show i::
为什么?
答:成员方法能不能调用,就是说方法的访问权限是不是public,是在编译阶段就需要确定的。
而p1->show()是在运行阶段才能确定的
在编译阶段看,p1是Base类指针,show()方法在Base类中是public,完全没有问题!
案例四:
class Base
{
public:
Base() {
cout << "call Base()" << endl;
clear();
}
void clear() {
memset(this, 0, sizeof(*this));
//从this指针开始,大小为sizeof(*this)的内存都赋值为0(就是Base的大小)
}
virtual void show()
{
cout << "call Base::show()" << endl;
}
};
class Derive :public Base
{
public:
Derive()
{
cout << "call Derive()" << endl;
}
void show()
{
cout << "call Derive::show()" << endl;
}
};
int main()
{
//都是动态绑定
Base* pb1 = new Base();
pb1->show();
delete pb1;
Base* pb2 = new Derive();
pb2->show();
delete pb2;
return 0;
}
结果编译出错
上半部分pb1部分出错
下半部分pb2没问题
原因:
当new Base()的时候,Base()的构造函数调用了clear(),把new的Base对象的内容全部归零了,包括vfptr,而在运行的时候,找不到vftable了,就出错了。

而下半部分new的是一个派生类Derive:
先调用基类Base()的构造,会把Base的vftable的地址写入vfptr
然后把Base()部分clear了
然后再调用派生类Derive的构造,会把Derive的vftable的地址写入vfptr
这样就又有了正确的虚函数表的地址了

这个问题的重点就是,vftable的地址是什么时候写入vfptr的!
3027



被折叠的 条评论
为什么被折叠?



