继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
对比下面二份代码
发现,上面二分代码存在大量的重复,需要减少重复代码的书写
下面为上面二份代码的基类
1.2.1定义格式
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类
要实现继承方式,一定要有一个基类,基类就是对各种对象进行高度抽象
下面代码展示了继承
#include
using namespace std;
#include class Pet
{
public:void Eat(){cout << _name << "在吃饭" << endl;}void Sleep(){cout << _name << "在睡觉" << endl;}string _name;string _gender;
};class Dog:public Pet
{
public:void Bark(){cout << _name << "旺旺~~~" << endl;}string _color;
};class Cat :public Pet
{
public:void mew(){cout << _name << "喵喵~~~" << endl;}string _temper;
};int main()
{Dog dog;dog._name = "旺财";dog._gender = "公";dog._color = "金色";dog.Bark();dog.Sleep();dog.Bark();Cat cat;cat._name = "苹果";cat._gender = "母";cat._temper = "白色";cat.Eat();cat.Sleep();cat.mew();return 0;
}
1.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
2.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
3. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
class Fu
{
public:void SetFu(int pub, int pro, int pri){_pub = pub;_pro = pro;_pri = pri;}void Print(){cout << _pub << " " << _pro << " " << _pri << endl;}
public:int _pub;protected:int _pro;private:int _pri;
};// 基类中不同访问权限的成员都被子类继承了
class Zi : public Fu
{};int main()
{Zi zi;zi.SetFu(1,2,3);zi.Print();cout << sizeof(zi) << endl;return 0;
}
大前提:在public的继承方式下
子类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
基类对象不能赋值给派生类对象
如果一定要让子类的指针指向基类的对象,只能强转
测试代码如下
class B
{
public:void SetB(int b){_b = b;};void PrintB(){cout << _b << endl;};
protected:int _b;
};//赋值兼容规则的大前提,public继承方式
class D :public B
{
public:void SetD(int b, int d){SetB(b);_d = d;};void PrintD(){PrintB();cout << _d << endl;};
protected:int _d;
};int main()
{B b;b.SetB(1);D d;d.SetD(2, 3);//1.可以使用子类对象给基类赋值,反之则不行b = d;//d = b; 这样就会报错,编译失败//2.可以让基类的引用引用子类的对象,反之则不行B& rb = d;//D& rd =b; 报错//可以让基类的指针指向子类的对象,反之则不行B* pb = &d;//D* pd=&b;报错return 0;
}
举例说明
对象模型角度再解释上述代码:
可以使用子类对象给基类赋值,反之则不行
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
1. 在继承的体系中,子类和基类存在相同名称的成员(成员变量 || 成员函数)
2. 如果通过子类对象直接调用相同名称的成员时,优先调用的是子类自己的成员,基类同名的成员无法被子类对象直接访问到*
3. 如果子类对象要直接访问基类的同名成员,必须在同名成员前添加 基类名称以及:: 即明确告诉编译器现在要访问的是基类的同名成员
4. 建议:在继承体系中,基类和子类尽量避免定义相同名称的成员
class B
{
public:void func(){cout << "B::func()" << endl;}void SetB(char b){_b = b;}char _b;int _c;
};class D : public B
{
public:void func(int b){cout << "D::func(int)" << endl;}int _b;int _d;
};int main()
{D d;d._b = 'A'; // 直接访问优先访问到的是自己的d.B::_b = 'B'; // 明确告诉编译器 现在要访问的是从基类继承下来的_bd.SetB('C');/d.func(10);d.B::func();return 0;
}
在继承体系中:构造、析构、拷贝构造、赋值运算符重载?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
下面用一系列代码进行演示学习
1. 如果基类没有显式定义任何构造方法,则子类可以根据自己是否需要选择性实现
//继承体系中子类构造方法的实现:
#if 0
class B
{
public:void SetB(int b){_b = b;}
protected:int _b;
};class D : public B
{
public:D(int a){cout << "D()" << endl;}void SetD(int b, int d){SetB(b);_d = d;}int _d;
};
int main()
{D d(10);d.SetD(1,2);return 0;
}
2. 如果基类定义了无参或者全缺省的构造方法,则子类可以根据自己是否需要选择性实现
class B
{
public:B()//基类定义了无参或者全缺省的构造方法 //全缺省int b = 10;{cout << "B::B()" << endl;}void SetB(int b){_b = b;}
protected:int _b;
};class D : public B
{
public:D(int a = 10) //此处看需要自己决定: B(10){cout << "D()" << endl;}void SetD(int b, int d){SetB(b);_d = d;}int _d;
};
3. 如果基类定义了带有参数的非全缺省的构造方法,则子类必须实现自己的构造方法,并且必须在其初始化列表的位置显式调用基类的构造方法,从基类继承下来的成员变量进行初始化
class B
{
public:B(int b){cout << "B::B()" << endl;}void SetB(int b){_b = b;}
protected:int _b;
};class D : public B
{
public:D(int b, int d): B(b) //其初始化列表的位置显式调用基类的构造方法, _d(d){cout << "D()" << endl;}void SetD(int b, int d){SetB(b);_d = d;}int _d;
};
4.基类拷贝构造如果没有实现,则子类的拷贝构造可以实现也可以不用实现
5. 如果基类和子类的拷贝构造都定义了,子类的构造方法必须在其初始化列表的位置显式基类的拷贝构造
class B
{
public:B(int b): _b(b){cout << "B::B()" << endl;}B(const B& b): _b(b._b){cout << "B(const B& b)" << endl;}
protected:int _b;
};class D : public B
{
public:D(int b, int d): B(b), _d(d){cout << "D()" << endl;}// 子类拷贝构造函数的书写方式D(const D& d): B(d), _d(d._d){cout << "D(const D& d)" << endl;}int _d;
};int main()
{D d1(1,2);D d2(d1);return 0;
}
6.基类的赋值运算符重载如果没有写,则子类也可以不用写
7.如果子类中涉及到资源的管理时,子类必须要实现自己的赋值运算符重载
class B
{
public:B(int b): _b(b){cout << "B::B()" << endl;}B(const B& b): _b(b._b){cout << "B(const B& b)" << endl;}B& operator=(const B& b){if (this != &b){_b = b._b;}return *this;}
protected:int _b;
};// 基类的赋值运算符重载如果没有写,则子类也可以不用写
class D : public B
{
public:D(int b, int d): B(b), _d(d){cout << "D()" << endl;}// 子类拷贝构造函数的书写方式D(const D& d): B(d), _d(d._d){cout << "D(const D& d)" << endl;}D& operator=(const D& d){if (this != &d){// 1. 先调用基类的赋值运算符重载完成从基类继承下来的成员的赋值B::operator=(d);// 2. 再给子类自己的成员赋值_d = d._d;}return *this;}int _d;
};int main()
{D d1(1,2);D d2(d1);d1 = d2;return 0;
}
8.如果子类中未涉及到资源管理时,子类的析构函数可以不用定义
class B
{
public:B(){_p = new int[10];//申请资源}~B()//析构销毁{delete[] _p;_p = nullptr;cout << "~B()" << endl;}
protected:int* _p;
};class D : public B
{
public:D(int d): B(), _d(d){}// 在该场景中,如果子类的析构方法没有定义// 编译器给D类生成了一个默认的析构函数/** ~D()* {* ~B();* }*/int _d;
};void TestFun()
{D d(10);return 0;
}
9.如果子类设计到资源管理时,则子类析构方法一定要定义,否则就会处在资源泄漏
class B
{
public:B(){_p = new int[10];}~B(){delete[] _p;_p = nullptr;cout << "~B()" << endl;}
protected:int* _p;
};class D : public B
{
public:D(int d): B(), _d(d){_pd = new int[10];}/** ~D()* {* ~B();* }*/~D(){delete[] _pd;_pd = nullptr;// 编译器在编译时候,在子类析构函数中最后一条代码之后添加自动调用基类析构方法的语句// call ~B();}int _d;int* _pd;
};void TestFunc()
{D d(10);
}int main()
{TestFunc();_CrtDumpMemoryLeaks();return 0;
}
展示如下
class B
{
public:B(int b): _b(b){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}protected:int _b;
};class D : public B
{
public:D(int b, int d): B(b), _d(_d){cout << "D(int,int)" << endl;}~D(){cout << "~D()" << endl;// 编译器会在子类析构方法最后一条语句之后添加自动调用基类析构方法的语句// ~B();}
protected:int _d;
};