继承的概念
在c++中,继承是面向对象编程的一个核心概念,同样也是使代码灵活复用的一种手段。它允许一个类(称为子类或者派生类)继承另一个类(称为基类或者父类)的属性和方法,并创建一个类的新版本,在不修改原始类的情况下添加或覆盖功能。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
来看一个简单的继承例子:
class Person { public: void Print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } protected: string _name = "peter"; // 姓名 int _age = 18; // 年龄 }; class Student :public Person { protected: int _stuid; }; class Teacher :public Person { protected: int _jobid; }; int main() { Student s; Teacher t; s.Print(); t.Print(); return 0; }
Student类与Teacher类是Person的子类,并且复用了父类的成员函数以及成员变量。复用后的成员的访问权限是什么呢?访问的权限取决于该成员在父类的访问限定符以及继承的方式。这一点下面会讲到。
继承的格式
根据上面继承的例子来解析继承的格式,观察下图:
派生类必须要通过使用派生类列表明确指出他是从哪个或者哪些基类继承过来的。派生类列表的形式如上图所示。首先是一个冒号
:
,后面紧跟以逗号分隔的基类列表,其中每个基类列表前面可以有指明继承方式。
其中,继承方式有三种:
- public:公有继承。父类的公有成员和保护成员在派生类中保持其原有的状态。这是最常用的继承类型。
- protected:保护继承。父类的公有成员和保护成员在派生类中都变成保护成员。
- private:私有继承。父类的公有成员和保护成员在派生类中都变成私有成员。
为什么要声明继承方式呢?
派生类需要将继承而来的成员函数的访问限定权限覆盖(有时候派生类不想完全复用基类成员的访问权限),从而起到控制派生类用户对于基类的访问权限。这只针对基类的保护成员和公有成员,对于基类的私有成员,派生类是无法重新覆盖的。
某个类对其继承过来的成员的访问权限受到两个因素影响:
- 在基类中该成员的访问限定符
- 继承方式,即派生列表中的访问限定符
下面我们来用代码探究继承过来的成员的访问权限
继承基类成员访问方式的变化
给出以下基类代码:
class person { public: void pub_mem() {//public成员 cout << "pub_mem" << endl; } protected://protected成员 void prot_mem() { cout << "prot_mem" << endl; } private://private成员 void priv_mem() { cout << "priv_mem" << endl; } };
1.给出一个student类继承person类,public继承 在student内部可以访问父类的public以及protected成员,但是不能访问基类的private成员
当student实例化对象之后,该对象同样不能访问基类的protected成员,只能访问基类的public成员
2.给出一个student类继承person类,protected继承
在student内部可以访问父类的public以及protected成员,但是不能访问基类的private成员
当student实例化对象之后,该对象不能访问所有的基类成员
3.给出一个student类继承person类,private继承 在student内部可以访问父类的public以及protected成员,但是不能访问基类的private成员
当student实例化对象之后,该对象不能访问所有的基类成员
上面的例子可以用一个表格概况
总结:
- 派生访问说明符(继承方式)对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响,在派生类的内部都能访问基类的protected、public成员,但不能访问private成员。
- 虽然基类的private成员无论如何都不能被派生类直接访问,但是还是会被继承过来。基类可以提供非private的接口给让派生类间接访问。
- 如果基类只想让成员在派生类内部访问,而不想被派生类实例化后的对象直接访问,可以将其的访问限定符设为protected。protected是因为继承才出现的。
- 基类的其他成员(除了private)在子类的访问方式 等于Min(成员在基类的访问限定符,继承方式),其中访问限定符的权限大小关系为:public > protected>private。
- 如果不在派生列表标明继承方式,默认就是private。struct则默认为public。
基类和派生类对象赋值转换
- 派生类拥有所有基类的成员,但是基类却不一定有派生类的成员。我们不能直接将基类赋值给一个派生类(向下转换),因为派生类可能包含基类没有的额外成员变量和方法,直接赋值可能会导致这部分信息缺失。
- 可以通过强制转换(如使用 dynamic_cast)在运行时尝试将基类指针或引用转换为派生类指针或引用。但这种转换只有在基类指针或引用实际指向一个派生类对象时才能成功。
- 而如果是派生类赋值给基类,由于派生类中有额外的成员,在赋值的时候只会把属于基类的成员赋值过去(向上转换),这个过程我们称为“切片”或者“切割”。
同时,派生类也可以直接赋值给基类的引用和指针。
下面演示基类与派生类赋值转换。观察以下代码:
class Person { protected : string _name; // 姓名 string _sex; // 性别 int _age; // 年龄 }; class Student : public Person { public : int _No ; // 学号 };
1.子类对象可以赋值给父类对象/指针/引用
Student sobj ; Person pobj = sobj ; Person* pp = &sobj; Person& rp = sobj;
2.基类对象不能赋值给派生类对象
Student sobj; Person pobj; sobj = pobj;//错误
3.基类的指针可以通过强制类型转换赋值给派生类的指针(需要满足情况)
Student sobj; Person *pp; pp = &sobj Student* ps1 = (Student*)pp; // 这种情况转换时可以的。 ps1->_No = 10;
切片过程:
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
独立的作用域这一点很容易理解。有了自己独立的作用域,也就意味着有自己的命名空间。所以派生类和基类可以有同名的成员,尽管子类成员会屏蔽父类的同名成员,但我们依旧建议尽量不同名。这是为了保证代码的可读性。
但是有的时候我们又需要派生类的成员与基类的成员同名。借用“屏蔽”,可以定义与基类成员同名但行为不同的成员。
用以下代码观察屏蔽现象:
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆 class Person { protected: string _name = "小李子"; // 姓名 int _num = 111; // 身份证号 }; class Student : public Person { public: void Print() { cout << " 姓名:" << _name << endl; cout << " 身份证号:" << Person::_num << endl; cout << " 学号:" << _num << endl; } protected: int _num = 999; // 学号 }; void Test() { Student s1; s1.Print(); }; int main() { Test(); return 0; }
值得注意的是,如果是基类的成员函数与派生类的成员函数同名,依旧构成的是隐藏而不是函数重载。
派生类的默认成员函数
既然是默认成员函数,也就意味着如果我们不写,编译器会帮我们写一个。类的六个默认成员函数的介绍在我之前的博客里有讲解:
类的六个默认成员函数
那么派生类的默认成员函数与基类有神魔关系呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。(把基类当成是派生类的一个成员类)
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
其实,基类非常像派生类的一个自定义类型的成员变量。只不过在构造和析构上的顺序有所差异。在一个派生类中,先是基类构造再构造派生类,先析构派生类再析构基类。我这里说的基类是指在派生类当中属于基类的那一部分成员集合。
用以下代码观察:
class Person { public: Person(string name, int age) :_name(name) ,_age(age) { cout << "person 构造" << endl; } ~Person() { cout << "person 析构" << endl; } private: string _name ; int _age; }; class Student : public Person { public: Student(string name="zhangsan", int age=18, int num=110) :Person(name, age) ,_num(num) { cout << "student 构造" << endl; } ~Student() { cout << "student 析构" << endl; } private: int _num; }; void Test() { Student s1; }; int main() { Test(); return 0; }
值得注意的是,如果我们在派生类中显式的析构基类,加上编译器自动析构基类,一共会析构两次基类成员
借用下面的图或许能更好理解这一过程
继承与友元
==友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员 ==
我们先回忆什么叫友元,以及友元的作用:友元是啥
如果A类是B类的友元类,那么A类成员函数可以直接访问B类的所有成员,包括私有成员。
如果一个类是基类的友元类,那么这个友元类并不能访问其派生类的私有或者保护成员。
继承与静态成员
类的静态成员不属于任何一个该类的实例化,只与该类本身有关。基类定义了static静态成员,则整个继承体系里面只有一个这样的成员 无论派生出多少个子类,都只有一个static成员实例 。 更准确的说,派生类和基类共享静态成员。这意味着,无论你通过基类还是派生类访问静态成员,其实访问的是同一个数据。