前言
继承机制是面向对象程序设计可以使代码复用的的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
比如我们定义一个person类,再定义student和teacher类,student和teacher显然都是人,所以他们肯定都会复用person类里的信息
这样我们就减少了重复的工作了
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
int _stuid; // 学号
};
继承
被继承的类往往称为父类或者基类
继承的类往往称为子类或者派生类
继承方式
那么如何实现继承呢?格式:class 派生类:继承方式 基类 上面代码也有展示
关于继承方式我们只要了解pubilc继承(父类是什么类型的成员到了子类还是什么成员)就好,其他的基本上都用不到
这里注意用到了以前往往都用不到的protected
这里简单地以public继承来举例,如果你想让基类的成员在子类中能访问且类外不能访问就定义为protected
让基类的成员在子类中不可见,只是继承过去就定义为private
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式
基类和派生类的赋值操作
我们认为基类和派生类是is-a的关系,在将派生类赋给基类的对象 / 基类的指针 / 基类的引用
时候,不会产生临时对象,此时指向派生类中基类的部分
这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
注意:基类不能赋值给派生类
子类和父类中有同名成员,因为它们的作用域是独自的。子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,不管参数和返回值。
派生类的默认成员函数
派生类想要初始化基类的那一部分,在写构造函数的时候就要去调用父类的构造函数,然后在操作自己的那一部分成员,不能直接操作父类的成员变量
注释掉的是错误的写法,在调用父类的构造函数的时候可以传参
派生类的拷贝构造,赋值重载,初始化等都是先调用基类的,再调用子类的
但是析构函数却是先调用派生类后,自动调用基类的析构函数,基类的析构
原因:因为如果先调用基类的可能在派生类的析构函数中会对父类进行一些操作,这个时候存在越界访问的问题了
由于多态的原因,析构最后会统一处理成destructor,所以父子类的析构函数构成隐藏
继承和友元:友元不会被继承,基类的友元不能访问派生类的成员
继承和静态成员:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
复杂的菱形继承
c++是支持多继承(一个子类有两个或以上直接父类时称这个继承关系为多继承)
比如class A,class B ,class C public:A public:B
但是祖师爷可能没想到我们可以把多继承玩的太花了
在菱形继承的情况下就存在数据冗余的问题,Assistant中会存在两份Person的数据
这样我们访问Person中的一个成员的时候,就会存在访问的二义性问题,虽然可以通过显示指定访问来解决,但是却无法解决数据的冗余性的问题
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
那么怎么解决呢?c++引入了虚继承——在继承方式之前加上virtual
这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
多态
当我们在平常买票的时候,成人往往是全票,学生是半票,军人有特殊待遇
这种同一行为,由不同用户来做的结果导致的结果不同就叫做多态
多态的条件
1.虚函数重写
2.由父类的指针或者引用调用虚函数
虚函数重写的注意事项
1.三同——函数名,参数,返回值相同
2.三同的例外:协变——返回的对象必须为父子类关系的指针或者引用
3.子类的虚函数可以不加virtual,但是不规范,建议都加上
4.如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,因为析构函数最终都会被处理成destructor5.虚函数重写的是实现(假如传参有默认值需要注意!!!)
C++11——final和override
c++11新增了两个关键字,帮助用户检测虚函数是否完成重写
final:修饰虚函数,表示该虚函数不会被重写
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
重载,重定义(隐藏)和重写之间的关系
抽象类
在虚函数的定义后面加上=0,就代表这个函数为纯虚函数
只要含有一个纯虚函数的类都叫做抽象类,不能实例化,只能被继承
派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
抽象类某种程度上和override很像,强迫子类去重写
什么东西可以写成抽象类——就如字面意思一样,抽象的东西可以写为抽象类,如:人,动物
然后当我们创建学生,老虎这种类的时候就可以继承抽象类
多态的原理
c++是如何实现多态以完成不同的功能的呢?
我们可以先来看一下一个含有虚函数的类的内存大小,大家可以先做做看
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
正确的答案是8,为什么呢?我们可以调试来看看
调试完发现,b对象当中除了_b成员外,实际上还有一个_vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关)
这个_vfptr是虚表指针,指向虚函数表,而虚函数表则存储着加了virtual的虚函数的地址
虚函数表本质上就是一个函数指针数组,每个位置存储着一个指针指向虚函数
一个类的不同成员共用一张虚表
虚表实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中
我们来看看下面这段代码
#include <iostream>
using namespace std;
//父类
class Base
{
public:
//虚函数
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
//虚函数
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
private:
int _b = 1;
};
//子类
class Derive : public Base
{
public:
//重写虚函数Func1
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过调试窗口我们可以发现如下结构
具体过程:
父类b的func1和func2为虚函数,所以b对象中有一个vfptr,指向虚函数表,虚函数表存储着func1和func2,func3不是虚函数,不会被放进虚表中
子类d继承了父类,重写了func1函数,没有重写func2函数。所以在创建子类对象的时候,会将父类的虚表拷贝给子类的时候,将虚表重写的函数的地址给覆盖了,这也是重写为什么叫覆盖的原因
新增的func4也会被纳入虚表中
总结一下,派生类的虚表生成步骤如下:
先将基类中的虚表内容拷贝一份到派生类的虚表。
如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址。
派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
说了这么多,那么多态的原理到底是什么呢?
之前讲过多态的条件之一就是必须是父类的引用或者指针
用两个父类指针指向b和d:
Base*p1=b,Base*p2=d;
p1->func1();
p2->func1();
p2会发生切片,指向子类的父类部分,其中包含虚表指针
当他们调用func1的时候,从虚表中调用的就是不同的函数了
是不是很神奇,以为很高端的操作原理却是这么朴实无华
父类的引用也是一样的道理,但是不能是父类对象
Base p=d;
这时可以理解p也会将d切片,但是会调用父类的构造函数,所以p里的虚表存储的也就是父类的虚函数地址了,就不能实现多态了
动态绑定和静态绑定
静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。
动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
多继承中的虚函数表
//基类1
class Base1
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void func2() { cout << "Base1::func2()" << endl; }
private:
int _b1;
};
//基类2
class Base2
{
public:
virtual void func1() { cout << "Base2::func1()" << endl; }
virtual void func2() { cout << "Base2::func2()" << endl; }
private:
int _b2;
};
//多继承派生类
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; }
private:
int _d1;
};
两个Base类的对象的虚函数表是很简单的,但是派生类的可就不简单了
总结:多继承中派生类继承每个父类的虚函数表
有重写的就对继承的虚函数表进行覆盖
新加的虚函数表添加到第一个虚表中去
相关题目
class A
{
public:
A ():m_iVal(0){test();}
virtual void func() { std::cout<<m_iVal<<‘ ’;}
void test(){func();}
public:
int m_iVal;
};
class B : public A
{
public:
B(){test();}
virtual void func()
{
++m_iVal;
std::cout<<m_iVal<<‘ ’;
}
};
int main(int argc ,char* argv[])
{
A*p = new B;
p->test();
return 0;
}
思路:在创建一个b对象的时候,因为b是继承A的,所以先调用A的构造函数,由于此时还处于对象构造阶段,多态机制还没有生效,所以,此时执行的func函数为父类的func函数,打印0
A对象构造好了就要去调用B的构造函数了,A对象构造好了意味着其虚表也存在了,多态机制生效了,所以B的构造函数中调用的是B的func
最后通过p指针调用test,还是多态,调用的仍然是B的func
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
这里注意多态是实现继承,大概思路参考上面一题的后面
调用的时候虚表生成,多态机制成立,所以调用的是b的func,但是接口仍然是a的
所以输出B-》1
总结
继承和多态又是c++的一个难点,但学完之后能方便我们使用以及提高效率
希望本篇为文章能让大家对继承和多态有更深的认识,如果错误,请及时指出