从零开始的C++学习生活 10:继承和多态
个人主页:Yupureki-CSDN博客
C++专栏:C++_Yupureki的博客-CSDN博客
目录
前言
1. 继承基础
1.1 继承的基本概念
1.2 继承的基本语法
1.3 基类和派生类间的转换
1.4 继承中的作用域
2. 继承使用
2.1 派生类的默认成员函数
2.2 不能被继承的类
2.3 继承与友元
2.4 继承与静态成员
2.5 多继承及其菱形继承
2.6 虚继承
3. 多态基础
3.1 多态的基本概念
3.2 多态的实现条件
3.3 虚函数与重写
3.4 override和final关键字
3.5 纯虚函数与抽象类
4. 多态的原理
4.1 虚函数表指针
4.2 多态是如何实现的
4.3 虚函数表
结语
上一篇:从零开始的C++学习生活 9:stack_queue的入门使用和模板进阶-CSDN博客
前言
在面向对象编程的三大特性(封装、继承、多态)中,继承和多态是构建复杂软件系统的核心支柱。继承让我们能够建立类之间的层次关系,实现代码的复用和扩展;而多态则赋予程序运行时灵活性,让相同的代码能够根据不同的对象表现出不同的行为。
理解继承和多态不仅是为了掌握C++语法,更是为了培养面向对象的设计思维。在实际开发中,合理的继承体系设计和多态运用能够显著提高代码的可维护性、可扩展性和可读性。
我将带你深入探讨C++中继承和多态的实现机制、使用场景和底层原理,帮助你从语法使用者成长为面向对象的设计者。
1. 继承基础
1.1 继承的基本概念
继承是面向对象编程中实现代码复用的重要机制。它允许我们在保持原有类特性的基础上进行扩展,增加新的成员变量和成员函数。
放眼现实生活中,如儿子继承父亲的特性,学生继承人的特性(听起来有点怪?),智能手机继承传统电话机(打电话)的特性。可见,继承的关键是继承前辈的特性,从而方便我们以此为基础构建新的对象
-
学生是人(Student is a Person)
-
老师是人(Teacher is a Person)
-
汽车是交通工具(Car is a Vehicle)
1.2 继承的基本语法
C++支持三种继承方式:
访问权限 | public继承 | protected继承 | private继承 |
---|---|---|---|
public → | public | protected | private |
protected → | protected | protected | private |
private → | 不可见 | 不可见 | 不可见 |
我们们可以简单理解为:public为公有的,无论类内还是类外,都可以访问,因此权限最大;proctected和private虽然都是类内可访问,类外不可访问,但关键是子类(即继承了父类的类)无法访问继承下来的private成员,但可以访问protected成员,因此proctected权限大于private
因此权限:public>proctected>private
访问权限和继承方式取二者的权限最小者
例如public继承proctected成员,即便public权限很大,也按照proctected的方式,因为protected权限小于public,取二者的较小者
在实际运用中⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用 protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实 际中扩展维护性不强。
class Person {//父类
protected:string _name;public:void identity() {cout << "身份验证: " << _name << endl;}
};class Student : public Person {//子类,public继承
protected:int _stuNum;public:void PersonPrint(){_name = "zhangsan";//_name和identity()为Person中的成员变量,但继承下来,Student也能使用identity();cout << _name << endl;}void study() {cout << _name << "正在学习" << endl;}
};
在上面代码中,Person(人)为父类,Student(学生是人awa)为子类,继承了Person,方式是public,Person中的string_name为proctected修饰,权限比public小,那么仍为proctected方式。
之所以是继承,那么就说明Student可以调用Person的成员,即子类调用父类的成员
1.3 基类和派生类间的转换
父类也叫基类,子类也叫派生类
public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切篇或者切 割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
Person p;
Student s("李四", 1002);p = s; // 切片:只复制Person部分,Student特有部分被丢弃
Person& rp = s; // 引用:不会切片,但只能访问Person部分
Person* pp = &s; // 指针:不会切片,但只能访问Person部分
因为派生类是继承了基类的,可以看作基类有的基本在派生类中也有,并且还加入了自己的东西,因此派生类内容一定是比基类更丰富的,所以如果用派生类的对象赋值给基类的指针或这引用,就可以生成一个基类,就因为派生类中含有基类的所有内容,可以切割下来给基类
上述Student(派生类)的对象s可以切割赋值给Person(基类)的指针或者引用
注意一定是指针或者引用,普通的直接赋值无法转换
1.4 继承中的作用域
我们知道C++中有多个作用域,类域也是个作用域
其中在在继承体系中基类和派生类都有独立的作用域:
- 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派生类成员函数中,可以使用基类::基类成员显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
class Person {
protected:string _name = "zhangsan";public:void identity() {cout << "身份验证: " << _name << endl;}
};class Student : public Person {
protected:int _stuNum;string _name = "lisi";//派生类含有基类相同的变量,就会隐藏基类的变量,使用派生类的public:void identity() {//相同的函数,会隐藏基类的函数cout << "身份验证: " << _name << endl;}void PersonPrint(){identity();//调用Student的函数,Person中的自动被隐藏Person::identity();//如果要调用Person中的函数,需用命名空间访问符cout << _name << endl;}void study() {cout << _name << "正在学习" << endl;}
};
派生类隐藏了基类中的成员,并非无法访问,可以加 基类:: 来强制访问基类的成员
但一般不推荐
2. 继承使用
2.1 派生类的默认成员函数
4个常见默认成员函数
默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成⼀个,那么在派生类中,这几个成员函数是如何生成的呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的 operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
可以简单理解为,派生类毕竟是继承了基类的,拥有基类的所有成员,那么你不仅要初始化基类的专有的变量,那肯定也要处理继承下来基类的变量。至于怎么处理,那肯定就是调用基类的函数
class Person {
public:Person(const string& name) :_name(name) {cout << "Person构造: " << _name << endl;}~Person() {cout << "Person析构: " << _name << endl;}
protected:string _name;
};class Student : public Person {
public:Student(const string& name, int num) : Person(name), //先调用基类的构造函数_stuNum(num) { cout << "Student构造,学号: " << _stuNum << endl;}~Student() {cout << "Student析构,学号: " << _stuNum << endl;// 自动调用基类析构(~Person)}protected:int _stuNum;
};// 使用示例
void demo() {Student s("张三", 1001);// 输出顺序:// Person构造: 张三// Student构造,学号: 1001// Student析构,学号: 1001 // Person析构: 张三
}
2.2 不能被继承的类
方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
class Bas{public:void func5() { cout << "Base::func5" << endl; }protected:int a = 1;private:Base()//默认构造函数在private中,派生类无法访问{}};
方法2:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。
class Base final
2.3 继承与友元
友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员
void friendfunc()
{cout << "friendfunc" << endl;
}class Person {friend void friendfunc();//.......
};class Student : public Person {
//........
};int main()
{Student s;s.friendfunc();//报错,无法访问基类的友元函数s.PersonPrint();
}
可以理解为,一个人是你父亲的朋友,但一定是你的朋友吗?
2.4 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有⼀个这样的成员。无论派生出多少个派生类,都只有⼀个static成员实例。
class Person {
public:static int id;//......
};int Person::id = 123456;class Student : public Person {//......
};int main()
{Student s;Person p;cout << s.id << endl;//无论是派生类还是基类都可以访问静态成员cout << p.id << endl;s.PersonPrint();
}
2.5 多继承及其菱形继承
单继承:⼀个派生类只有⼀个直接基类时称这个继承关系为单继承
class Student : public Person //单继承
多继承:⼀个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。
class Assistant : public Student, public Teacher....//多继承
菱形继承:菱形继承是多继承的⼀种特殊情况。
可以看见BB和CC都继承了AA,结果DD右都继承了BB和CC,相当于DD继承了两遍AA
因此菱形继承有数据冗余和二义性的问题
我们不推荐写出菱形继承,这种写法会出现数据庞大且很容易出现bug,因从很容易被开除awa
2.6 虚继承
虚继承可以解决菱形继承的问题,使得最终的派生类只保留一份基类的成员
class Person {
public:string _name;
};class Student : virtual public Person { // 虚继承
public:int _stuNum;
};class Teacher : virtual public Person { // 虚继承
public:int _teacherId;
};class Assistant : public Student, public Teacher {
public:string _majorCourse;
};void solution() {Assistant a;a._name = "张三"; // 现在只有一个_name,无二义性
}
3. 多态基础
3.1 多态的基本概念
多态是指相同的接口表现出不同的行为。C++支持两种多态:
-
编译时多态(静态多态): 函数重载、模板
-
运行时多态(动态多态): 虚函数机制
编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运行时归为动态。
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种 形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再比如,同样是动物叫的⼀个行为(函数),传猫对象过去,就是”(>^ω^<) 喵“,传狗对象过去,就是"汪汪"。
3.2 多态的实现条件
多态是⼀个继承关系的下的类对象,因此多态是继承的拓展
要实现运行时多态,必须满足两个条件:
-
基类的指针或引用调用虚函数
-
派生类对基类的虚函数进行重写
再次强调,多态一定是基类的指针或者引用!!!
因为如果我们有一个基类,和以该基类衍生的派生类,为了方便想一起调用这几个派生类,那么只有基类的指针或引用才能既指向基类对象又指向派生类对象
同时派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到
3.3 虚函数与重写
类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修 饰
class Person {
public:virtual void buyTicket() { // 虚函数cout << "全价票: 100元" << endl;}virtual ~Person() { // 虚析构函数cout << "Person析构" << endl;}
};class Student : public Person {
public:virtual void buyTicket() { // 重写虚函数cout << "学生票: 50元" << endl;}virtual ~Student() {cout << "Student析构" << endl;}
};class Soldier : public Person {
public:virtual void buyTicket() { // 重写虚函数cout << "军人优先票: 80元" << endl;}
};
再加上之前的,利用基类的类型赋值为派生类的对象的地址或者引用
void purchaseTicket(Person& person) {//基类(Person)的类型 此处&为引用,也可以使用指针person.buyTicket(); // 根据实际对象类型调用不同的函数
}void demo() {Person p;//基类Student s;//派生类Soldier soldier;//派生类purchaseTicket(p); // 输出: 全价票: 100元purchaseTicket(s); // 输出: 学生票: 50元 purchaseTicket(soldier); // 输出: 军人优先票: 80元
}
2.4 override和final关键字
3.4 override和final关键字
C++11引入了override和final来增强类型安全
override可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰
class Animal {
public:virtual void speak() const {cout << "动物叫声" << endl;}virtual void eat() final { // 禁止派生类重写cout << "动物进食" << endl;}
};class Dog : public Animal {
public:virtual void speak() const override { // 显式声明重写cout << "汪汪" << endl;}// virtual void eat() override { } // 错误:基类已声明为final
};class Cat : public Animal {
public:virtual void speak() const override {cout << "喵喵" << endl;}
};
3.5 纯虚函数与抽象类
在虚函数的后面写上=0,则这个函数为纯虚函数。纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
// 抽象类:包含纯虚函数的类
class Shape {
public:virtual double area() const = 0; // 纯虚函数virtual double perimeter() const = 0;};// 派生类必须实现纯虚函数
class Circle : public Shape {
private:double _radius;public:Circle(double r) : _radius(r) {}virtual double area() const override {return 3.14159 * _radius * _radius;}virtual double perimeter() const override {return 2 * 3.14159 * _radius;}};class Rectangle : public Shape {
private:double _width, _height;public:Rectangle(double w, double h) : _width(w), _height(h) {}virtual double area() const override {return _width * _height;}};void processShape(const Shape& shape) {shape.draw();cout << "面积: " << shape.area() << endl;cout << "周长: " << shape.perimeter() << endl;
}void demo() {Circle c(5.0);Rectangle r(4.0, 6.0);processShape(c);processShape(r);// Shape s; // 错误:抽象类不能实例化
}
4. 多态的原理
4.1 虚函数表指针
我们先看一个题目
下面编译为32位程序的运行结果是什么()
A. 编译报错 B.运行报错 C.8 D.12
class Base{public:virtual void Func1(){cout << "Func1()" << endl;}protected:int _b = 1;char _ch = 'x';};int main(){Base b;cout << sizeof(b) << endl;return 0;}
表面上看,Base中含有一个int和char类型变量,按照内存对齐,应该是8字节,但实际运行结果是12bytes
因为除了_b和_ch成员,还多⼀个_vfptr放在对象的前⾯
这个_vfptr是一个虚函数表指针,在32位平台下是4字节
⼀个含有虚函数的类中都至少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表
4.2 多态是如何实现的
class Person {public:virtual void BuyTicket() { cout << "买票全价" << endl; }
private: string _name;
;class Student : public Person {
public:virtual void BuyTicket() { cout << "买票打折" << endl; }
private: string _id;
};class Soldier: public Person {
public:virtual void BuyTicket() { cout << "买票优先" << endl; }
private: string _codename;
};void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。ptr->BuyTicket();
}// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后// 多态也会发⽣在多个派⽣类之间。
int main{Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}
我们可以看见,同样是调用BuyTicket(),Person类型(基类)应该是调用Person内部的BuyTicket()
但是Student或Soldier(子类)给Person赋值后,Person调用的却是对应类型(Student或Soldier)的BuyTicket()
因为满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数
指向派生类就调用派生类对应的虚函数。ptr指向的Person对象,调用的是Person的虚函数;ptr指向的Student对象,调用的是Student的虚函数。
4.3 虚函数表
- 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同⼀张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
- 派生类由两部分构成,继承下来的基类和自己的成员。继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针,但继承下来的基类部分虚函数表指针是自己独立生成的,不是基类的原指针
- 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
- 虚函数表本质是⼀个存虚函数指针的指针数组
- 虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的
结语
继承和多态是C++面向对象编程的核心,它们共同构建了灵活、可扩展的软件架构:
-
继承建立了类之间的层次关系,实现了代码的复用和扩展
-
多态提供了运行时的灵活性,让相同的接口能够表现出不同的行为
-
虚函数机制是多态的底层支撑,理解虚表有助于深入掌握C++
通过合理运用继承和多态,我们可以:
-
减少代码重复,提高复用性
-
增强程序的可扩展性和可维护性
-
建立清晰的类层次结构
-
实现面向接口编程,降低模块耦合度