1、继承的概念
继承也就是:类设计层次复用
继承 (inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法 (成员函数) 和属性 (成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类 Student 和 Teacher,Student 和 Teacher 都有姓名 / 地址 / 电话 / 年龄等成员变量,都有 identity 身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。
class Person { public: // 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 void identity() { cout << "void identity()" <<_name<< endl; } protected: string _name = "张三"; // 姓名 string _address; // 地址 string _tel; // 电话 int _age = 18; // 年龄 }; class Student : public Person { public: // 学习 void study() { // ... } protected: int _stuid; // 学号 }; class Teacher : public Person { public: // 授课 void teaching() { //... } protected: string title; // 职称 }; int main() { Student s; Teacher t; s.identity(); t.identity(); return 0; }这里的运行结果是:
Person(基类)
/ \
Student Teacher (派生类)
student中继承到的内容是:
class Student : public Person {
public:
void study() { ... }
protected:
int _stuid; // 学号
};
在teacher中继承到的是:
class Teacher : public Person {
public:
void teaching() { ... }
protected:
string title; // 职称
};
protected成员不能再类外访问,
继承的定义:
既叫基类/派生类,也叫父类/子类
继承基类成员访问方式的变化
- 基类
private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。 - 基类
private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。 - 实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min (成员在基类的访问限定符,继承方式),
public > protected > private。 - 使用关键字
class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显式的写出继承方式。 - 在实际运用中一般使用都是
public继承,几乎很少使用protected/private继承,也不提倡使用protected/private继承,因为protected/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
- public(公有)类内、类外、派生类都可以直接访问。
- protected(保护)✅ 类内可以访问 ✅派生类内部可以访问❌ 类外部(对象.)不能访问
- private(私有)✅ 只有本类内部能访问 ❌ 派生类、类外都不能直接访问
2、基类和派生类之间的转化
- 通常情况下我们把一个类型的对象赋值给另一个类型的指针或者引用时,存在类型转换,中间会产生临时对象,所以需要加 const,如:
int a = 1; const double& d = a;public 继承中,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针 / 基类的引用,而不需要加 const,这里的指针和引用绑定是派生类对象中的基类部分,如下图所示。也就意味着一个基类的指针或者引用,可能指向基类对象,也可能指向派生类对象。 - 派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成的 (这两个函数的细节后面小节会细讲),这个过程就像派生类自定义部分成员切掉了一样,所以也被叫做切割或者切片,如下图中所示。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI (Run-Time Type Information) 的
dynamic_cast来进行识别后进行安全转换。
考点:
- public 继承:派生类对象可以直接赋值给基类指针 / 引用,不用 const,指针引用绑定派生对象里的基类部分。
- 派生类对象赋值给基类对象 →切片(切割):只拷贝基类那部分,派生独有成员丢掉。
- 普通情况:基类对象不能直接赋值给派生类对象。
- 基类指针转派生指针:要强制转换;只有基指针原本就指向派生对象才安全;多态可用
dynamic_cast安全转换。
3、继承中的作用域(很重要triple star)
(1) 隐藏规则:
1. 在继承体系中基类和派生类都有独立的作用域。
2.派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派生类成员函数中,可以使用基类::基类成员显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
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; // 学号 }; int main() { Student s1; s1.Print(); return 0; };运行结果是:
姓名:小李子
身份证号:111(在这里,在派生类成员里使用“基类:基类成员 ”来访问)
学号:999
图片中的文字提取如下:
4、 4个常见默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
class Person { protected: string _name; public: // 父类没有默认构造(因为写了带参的,编译器就不给了) Person(const string& name) : _name(name) { cout << "Person 构造" << endl; } }; class Student : public Person { protected: int _id; public: // 【重点】子类必须在初始化列表显式调用父类构造 Student(const string& name, int id) : Person(name), // 必须写!不写就报错 _id(id) { cout << "Student 构造" << endl; } };
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
class Student : public Person { public: // 默认生成的拷贝构造大概长这样: Student(const Student& s) : Person(s), // 自动调用父类拷贝构造,把 _name 也拷过去 _id(s._id) // 拷贝自己的成员 {} };
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域
class Person { protected: string _name; public: Person& operator=(const Person& p) { cout << "Person operator=" << endl; _name = p._name; return *this; } }; class Student : public Person { protected: int _id; public: // 如果子类自己写了 operator=,就会【隐藏】父类的 operator= Student& operator=(const Student& s) { // 必须显式指定作用域调用父类的赋值,否则只会拷贝自己的 _id Person::operator=(s); _id = s._id; return *this; } };
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5.派生类对象初始化先调用基类构造再调派生类构造。
#include <iostream> using namespace std; class Person { public: Person() { cout << "1. 执行 Person 的构造函数 (基类)" << endl; } }; class Student : public Person { public: Student() { // 就算你这里什么都不写,编译器也会自动帮你调用 Person() cout << "2. 执行 Student 的构造函数 (派生类)" << endl; } }; int main() { cout << "--- 准备创建 Student 对象 ---" << endl; Student s; // 这里触发构造 cout << "--- Student 对象创建完毕 ---" << endl; return 0; }
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。
(4、6、7点)
class Person { public: // 注意:这里没加 virtual ~Person() { cout << "~Person 析构" << endl; } }; class Student : public Person { public: ~Student() { cout << "~Student 析构" << endl; } }; int main() { Student s; // 当 main 结束时,s 的析构过程: // 1. 先调用 ~Student() (清理子类成员) // 2. 编译器自动调用 ~Person() (清理父类成员) }
析构函数名字因为后续多态(重写)章节的原因,会被处理成destructor,所以派生类和基类析构,构成隐藏关系。
派生类析构调用后,会自动调用父类析构,所以自己实现析构时不需要显示调用
析构初始化,先父类后子类。析构清理资源,先子后父。
5、继承和友元
友元不能被继承,也就是说基类友元不能访问派生类私有和保护成员。友元关系不能传递
#include <iostream> using namespace std; // 提前声明一下 Student 类 class Student; // 这是一个外部类,想要成为 Person 和 Student 的“朋友” class Teacher { public: void visitPerson(Person& p); void visitStudent(Student& s); }; // 基类:Person class Person { // 给 Teacher 发了一张通行证 friend class Teacher; private: string _name = "张三"; // 私有成员 }; // 派生类:Student class Student : public Person { private: int _id = 1001; // 派生类自己的私有成员 }; // Teacher 的实现 void Teacher::visitPerson(Person& p) { // 【成功】Teacher 是 Person 的友元,可以访问 Person 的 private cout << "访问父类私有成员: " << p._name << endl; } void Teacher::visitStudent(Student& s) { // 【失败!编译报错】友元不能继承 // 虽然 Teacher 是 Person 的友元,但 Student 并不认账 // cout << s._id << endl; // 报错:_id 是 private // 【成功】但 Teacher 依然可以访问 Student 从父类继承来的 _name // 因为:访问 s._name 相当于访问 (Person)s._name,而 Teacher 是 Person 的友元 cout << "访问子类继承自父类的私有成员: " << s._name << endl; } int main() { Person p; Student s; Teacher t; t.visitPerson(p); t.visitStudent(s); // 注意:这里只能打印出父类的 _name,打印不了子类的 _id return 0; }