一、继承的概念及定义
1、继承的概念
继承 (inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
// 基类 class Person { protected: void print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } protected: string _name = "张三"; // 姓名 int _age = 18; // 年龄 }; // 派生类 class Student : public Person { protected: int _stuid; // 学号 }; // 派生类 class Teacher : public Person { protected: int _jobid; // 工号 }; int main() { Person p; p.Print(); Student s; s.print(); Teacher t; t.print(); return 0; }
继承后父类 Person 的成员(成员函数 + 成员变量)都会变成子类的一部分。
这里体现出了 Student 和 Teacher 复用了 Person 的成员。
2、继承定义
(1)定义格式
下面我们看到 Person 是父类,也称作基类。Student 是子类,也称作派生类。
(2)继承关系和访问限定符
继承后成员的访问权限是同时受父类的访问限定符和子类继承父类的方式影响的,也就是说互相组合一共有 9 种继承方式。
(3)继承基类成员访问方式的变化
下面这个表格不需要记忆,通过理解加深印象即可。
(红色框住的是常用的)
【总结】
- 基类 private 成员在派生类中无论以什么方式继承都是不可见的。【这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是在语法上限制派生类对象不管是在类里面还是在类外面都不能去访问它。】
- 基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的。
- 实际从上面的表格种,我们总结一下可以发现,基类的私有成员在子类都是不可见的。基类的其他成员在子类的访问方式 == Min(访问方式,继承方式),public > protected > private。
- 使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是 public(公有) 继承,几乎很少使用 protetced(保护)/ private(私有) 继承,也不提倡使用 protetced / private 继承,因为 protetced / private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
实例演示三种继承关系下基类成员的各类型成员访问关系的变化:
// 基类 class Person { public : void Print () { cout << _name << endl; } protected : string _name ; // 姓名 private : int _age ; // 年龄 }; // 派生类 //class Student : protected Person //class Student : private Person class Student : public Person { protected : int _stunum ; // 学号 };
⚪【补充】
如何给基类成员设置合适的访问限定符?
- 基类成员想让它被所有人访问,就设置成 public【公有】。
- 基类成员不想让它在类外被直接访问,但需要在派生类中被访问,就设置成 protected【保护】。
- 基类成员不想让它在类外被直接访问,也不想让它在派生类中被访问,就设置成 private【私有】。
二、基类和派生类对象赋值转换
- 派生类对象可以赋值给 ① 基类的对象 / ② 基类的指针 / ③ 基类的引用。(前提是公有)这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run-Time Type Information)的 dynamic_cast 来进行识别后进行安全转换。(了解)
// 基类 class Person { protected : string _name; // 姓名 string _sex; // 性别 int _age; // 年龄 }; // 派生类 class Student : public Person { public : int _id; // 学号 }; void Test () { Student sobj ; // 1.子类对象可以赋值给父类对象/指针/引用 Person pobj = sobj ; Person* pp = &sobj; Person& rp = sobj; // 2.基类对象不能赋值给派生类对象 sobj = pobj; // 3.基类的指针可以通过强制类型转换赋值给派生类的指针 pp = &sobj Student* ps1 = (Student*)pp; // 这种情况转换是可以的 ps1->_No = 10; pp = &pobj; Student* ps2 = (Student*)pp; // 这种情况转换虽然可以,但是会存在越界访问的问题 ps2->_No = 10; }
三、继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,虽然这样的代码虽然能跑,但是非常容易混淆 // 基类 class Person { protected : string _name = "李四"; // 姓名 int _num = 333444; // 身份证号 }; // 派生类 class Student : public Person { public: void Print() { cout << "姓名:" << _name << endl; cout << "身份证号:" << Person::_num << endl; // 类名::成员显示访问 cout << "学号:" << _num << endl; // 打印的是派生类的学号 } protected: int _num = 111222; // 学号 }; int main() { Student s1; s1.Print(); return 0; };
class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { A::fun(); // 类名::成员 还是可以访问的 cout << "func(int i)->" << i << endl; } }; int main() { B b; b.fun(10); return 0; };
注意:这里要重点区分一下函数重载,首先重载是对同一作用域中才存在的概念,这里肯定不构成重载。其次隐藏的要求也和重载不同,只需要和基类成员函数同名就能构造隐藏。
B 中的 fun 和 A 中的 fun 不是构成重载,因为不在同一作用域。
B 中的 fun 和 A 中的 fun 构成隐藏,成员函数满足函数名相同就构成隐藏。
四、派生类的默认成员函数
6 个默认成员函数,“默认” 的意思就是指我们不写,编译器会给我们自动生成一个,那么 在派生类中,这几个成员函数是如何生成的呢?
1、派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。
如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 对于继承的基类成员 ——> 把它当作一个整体,先自动调用基类的默认构造函数初始化。
- 对于类中的内置类型成员 ——> 不任何处理(除非声明时给了缺省值)。
- 对于类中的自定义类型成员 ——> 调用它的默认构造函数(不需要参数就可以调用的,比如无参构造函数或全缺省构造函数)。
2、派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 对于继承的基类成员 ——> 把它作为一个整体,先自动调用基类的拷贝构造函数来完成拷贝初始化。
- 对于类中的内置类型成员 ——> 完成值拷贝。
- 对于类中的自定义类型成员 ——> 自动调用它的拷贝构造函数来完成拷贝初始化。
3、派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。设计子类析构时只要保证自己的资源正确释放即可。
- 对于继承的基类成员 ——> 把它作为一个整体,调用基类的拷贝赋值函数来完成赋值初始化。
- 对于类中的内置类型成员 ——> 完成值拷贝。
- 对于类中的自定义类型成员 ——> 调用它的赋值重载函数来完成赋值初始化。
4、派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。(因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。(要符合先定义的先析构))
- 对于继承的基类成员 ——> 把它作为一个整体,派生类的析构函数调用完成后,会自动调用基类的析构函数完成清理工作。
- 对于类中的内置类型成员 ——> 不处理。
- 对于类中的自定义类型成员 ——> 调用它的析构函数完成清理工作。
5、派生类对象初始化先调用基类构造再调派生类构造。
6、派生类对象析构清理先调用派生类析构再调基类的析构。
在一些场景下,析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成 destrutor(),所以父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。
- 子类的析构函数在执行结束后会自动调用父类的析构函数。
因为创建派生类对象时,先创建初始化了基类成员,再创建初始化了派生类成员。所以派生类对象析构清理先调用派生类析构函数清理派生类成员后,再调用基类析构函数清理基类成员。
// 基类 class Person { public : // 默认构造函数 Person(const char* name = "Peter") : _name(name ) { cout << "Person()" << endl; } // 拷贝构造函数 Person(const Person& p) : _name(p._name) { cout << "Person(const Person& p)" << endl; } // 赋值重载函数 Person& operator=(const Person& p ) { cout << "Person operator=(const Person& p)" << endl; if (this != &p) _name = p ._name; return *this ; } // 析构函数 ~Person() { cout << "~Person()" << endl; } protected : string _name ; // 姓名 }; //派生类 class Student : public Person { public : // 自己实现派生类的构造函数 // 注意:继承的基类成员是作为一个整体,调用基类的构造函数进行初始化 Student(const char* name, int num) : Person(name) // 显示调用基类的构造函数 , _num(num) { cout << "Student()" << endl; } // 如果我们要自己实现派生类的拷贝构造,如下所示: // 但一般用编译器默认生成的即可,如果派生类中存在深拷贝,才需要自己实现 Student(const Student& s) : Person(s) // 必须显示调用基类的拷贝构造(这里会发生切片) , _num(s ._num) { cout << "Student(const Student& s)" << endl ; } // 自己实现派生类的赋值重载函数 Student& operator = (const Student& s ) { cout << "Student& operator= (const Student& s)" << endl; if (this != &s) { Person::operator =(s); // 必须显示调用基类的赋值重载(这里会发生切片) _num = s ._num; } return *this ; } ~Student() // 先清理自己的资源 { cout << "~Student()" << endl; } // 结束后会自动调用父类的析构函数 protected : int _num ; //学号 }; int main() { Student s1 ("Jack", 18);// 调用构造函数 Student s2 (s1); // 调用拷贝构造函数 Student s3 ("Rose", 17); s1 = s3 ; // 调用重载赋值函数 return 0; }
【C++】继承 -- 详解(下)https://developer.aliyun.com/article/1514708?spm=a2c6h.13148508.setting.18.4b904f0ejdbHoA