阿尼亚全程陪伴大家学习~
前言
每个程序员在开发新系统时,都希望能够利用已有的软件资源,以缩短开发周期,提高开发效率。 为了提高软件的可重用性(reusability),C++提供了类的继承机制。
1.继承的概念
继承: 指在现有类的基础上建立一个新的类。现有类称为基类或父类,新建类称为派生类或子类。对于父类与子类,人们也常说子类继承了父类,或者父类派生了子类。
语法:class 子类(派生类):继承方式 父类(基类)
现在我们一起来看一下具体的实现
#include<iostream>
using namespace std;
//基础界面
class BasePage
{void left(){cout << "Java,C++,Python,C...." << endl;}void right(){cout << "右界面" << endl;}void head(){cout << "头部界面" << endl;}void bottom(){cout << "底部界面" << endl;}
};
//C语言界面
class C
{
public:void left(){cout << "Java,C++,Python,C...." << endl;}void right(){cout << "右界面" << endl;}void head(){cout << "头部界面" << endl;}void bottom(){cout << "底部界面" << endl;}void Linux(){cout << "Linux" << endl;}
};
//C语言界面
class C :public BasePage
{
public:void Linux(){cout << "Linux" << endl;}
};
//Java界面
class Java :public BasePage
{
public:void JavaSE(){cout << "JavaSE" << endl;}
};
这可以看做一个编程语言学习的主界面,左界面是各种不同语言的分类,这些不同编程语言界面都有主界面的部分,但是他们也有自己独特的部分,主界面是基类,而具体的编程语言界面则是派生类,派生类都继承了父类(基类)所有的成员函数。
2.继承的三种方式
公共(public)继承、保护(protected)继承、私有(private)继承
下面我们一一介绍
2.1公共继承
#include<iostream>
using namespace std;
class A
{
private:int _a = 1;
public:int _A = 10;void A_print(){cout << "_a=" << _a << endl;cout << "_A=" << _A << endl;}
};
class B:public A
{
private:int _b = 2;
public:int _B = 20;void B_print(){cout << "_b=" << _b << endl;}
};
int main()
{B b;b.B_print();b.A_print();return 0;
}
我们发现通过公共继承的方式来继承A类,A类中的打印函数也被继承了,所以B类的对象可以调用A类的成员函数,接下来我们修改一下代码,在B类中访问A类的成员变量
编译器告诉我们_a不可以被访问因为他是A类中的私有成员,而_A可以被访问因为他是A类中的公有成员,那有没有什么方法能让_a也能被访问呢?
有。一种是把_a设置为public成员,另外一种是把_a设置为protected成员(最好的做法),这三种又有什么区别呢?(重要!!!)
*公有(public)成员:
公有成员可以从任何地方被访问,包括类的内部、类的派生类以及类的外部。
把_a设置为公有成员意味着任何地方的代码都可以直接访问它,这通常不是一个好的做法,因为它破坏了封装性(封装性意味着隐藏对象的内部状态以防止它们被外部代码直接访问)。*私有(private)成员:
私有成员只能在类的内部被访问。
编译器告诉我们_a不可以被访问,因为它被声明为私有成员。这意味着你不能从类的外部或派生类中直接访问它。*保护(protected)成员:
保护成员可以在类的内部和派生类中被访问,但不能在类的外部被访问。
把_a设置为保护成员意味着你可以在其派生类中访问它,但不能在类的外部直接访问它。这提供了一种在派生类中重用和扩展基类功能的方式,同时保持对外部世界的封装性。
能明白三者的差异,也就很容易理解三种继承方式的差别了
#include<iostream>
using namespace std;
class A
{
private:int _a = 1;
protected:int a = 100;
public:int _A = 10;void A_print()//A类打印函数{cout << "_a=" << _a << endl;cout << "_A=" << _A << endl;cout << "a=" << a << endl;}
};
class B:public A
{
private:int _b = 2;
public:void B_print()//B类打印函数{cout << "_b=" << _b << endl;//cout << "_a=" << _a << endl;//A类私有成员不能被访问cout << "_A=" << _A << endl;//A类公有成员可以被访问cout << "a=" << a << endl;//A类保护成员可以被访问A_print();//A类公有成员可以被访问}
};
int main()
{B b;b.B_print();cout << endl;cout << b._A << endl;//public继承方式,在类的外部能访问类public成员b.A_print();//public继承方式,在类的外部能访问类public成员//cout << b.a << endl;//在类的外部不能访问类protected成员return 0;
}
2.2保护继承
#include<iostream>
using namespace std;
class A
{
private:int _a = 1;
protected:int a = 100;
public:int _A = 10;void A_print()//A类打印函数{cout << "_a=" << _a << endl;cout << "_A=" << _A << endl;cout << "a=" << a << endl;}
};
class B:protected A
{
private:int _b = 2;
public:void B_print()//B类打印函数{cout << "_b=" << _b << endl;//cout << "_a=" << _a << endl;//A类私有成员不能被访问cout << "_A=" << _A << endl;//A类公有成员可以被访问cout << "a=" << a << endl;//A类保护成员可以被访问A_print();//A类公有成员可以被访问}
};
int main()
{B b;b.B_print();//b.A_print();//protected继承方式,在类的外部不能访问类成员return 0;
}
2.3私有继承
#include<iostream>
using namespace std;
class A
{
private:int _a = 1;
protected:int a = 100;
public:int _A = 10;void A_print()//A类打印函数{cout << "_a=" << _a << endl;cout << "_A=" << _A << endl;cout << "a=" << a << endl;}
};
class B:private A
{
private:int _b = 2;
public:void B_print()//B类打印函数{cout << "_b=" << _b << endl;//cout << "_a=" << _a << endl;//A类私有成员不能被访问cout << "_A=" << _A << endl;//A类公有成员可以被访问cout << "a=" << a << endl;//A类保护成员可以被访问//A_print();//A类公有成员可以被访问}
};
int main()
{B b;b.B_print();cout << endl;//cout << b._A << endl;//private继承方式,在类的外部不能访问类public成员//cout << b.a << endl;private继承方式,在类的外部不能访问类protected成员//b.A_print();//private继承方式,在类的外部不能访问类public成员return 0;
}
总结
在类的内部,派生类无论是以哪种方式继承基类,都不能访问基类的private成员,而基类的public成员、protected成员可以被派生类访问
在类的外部,首先需要明确的是无论是基类还是派生类的private成员和protected成员都是不能直接被访问的。而public继承的方式可以通过派生类的对象访问基类的public成员,而private继承的方式和protected继承的方式中,却不可以通过派生类的对象访问基类的public成员(可以这么理解此时基类的public成员分别成了派生类的私有成员和保护成员)。
下图辅助理解
3.继承的对象模型
我们发现B类的大小是16个字节,但是我们之前继承方式当中不是说基类的私有成员派生类不能访问吗?
其实父类中所有非静态成员都会被子类继承,父类中的私有成员属性其实是被编译器隐藏了,因此访问不到,但是确实被继承了下来,下面我们来验证一下
首先我们先打开这个工具(开发人员命令提示符)
指令输入步骤
最终呈现结果
经过验证是不是更可靠了呢
4.派生类的构造函数和析构函数
4.1构造函数
派生类不继承基类的构造函数,在声明派生类时一般应定义自己的构造函数
注意
派生类构造函数的总参数表中的参数,应当包括调用基类构造函数所需的参数 派生类构造函数的执行过程是,先调用基类构造函数初始化基类成员,然后对新增成员初始化
#include<iostream>
#include<string>
using namespace std;class Person//基类
{
protected:string _name;//姓名char _sex;//性别int _age;//年龄
public://Person构造函数Person(string name,char sex,int age):_name(name),_sex(sex),_age(age){}void PersonPrint(){cout << "name:" << _name << endl;cout << "sex:" << _sex << endl;cout << "age:" << _age << endl;}
};
//派生类
class Student :public Person
{
private:int _num;//学号
public://Student构造函数Student(string name,char sex,int age,int num):Person(name,sex,age),_num(num){}void StudentPrint(){PersonPrint();cout << "num:" << _num << endl;}
};
int main()
{Student s("liming", 'M', 18, 1001);s.StudentPrint();return 0;
}
4.1.1有子对象的派生类的构造函数
在学习结构体时,我们讲到一个结构体的成员还可以是个结构体变量。 派生类也可以有子对象(类类型的成员变量)
#include<iostream>
#include<string>
using namespace std;class Person//基类
{
protected:string _name;//姓名char _sex;//性别int _age;//年龄
public://Person构造函数Person(string name,char sex,int age):_name(name),_sex(sex),_age(age){}void PersonPrint(){cout << "name:" << _name << endl;cout << "sex:" << _sex << endl;cout << "age:" << _age << endl;}
};
//派生类
class Student :public Person
{
private:Person _teacher;//班主任(子对象)int _num;//学号
public://Student构造函数Student(string name,char sex,int age,int num,const Person& teacher):Person(name,sex,age),_num(num),_teacher(teacher){}void StudentPrint(){PersonPrint();//打印学生的信息cout << "num:" << _num << endl;_teacher.PersonPrint();//打印老师的信息}
};
int main()
{Person teacher("zhaoli", 'F', 38);//班主任Student s("liming", 'M', 18, 1001, teacher);//学生s.StudentPrint();return 0;
}
基类构造函数和子对象的书写顺序可以任意
这里有子对象的派生类的构造函数还有另外两种写法
写法一
这种不如引用传参更安全,效率高
写法二
这种写法相对比较麻烦,写的形参更多了
4.1.2派生类构造函数的执行顺序
结论:
先调用基类构造函数,对基类数据成员初始化
再调用子对象类的构造函数,对子对象的数据成员初始化
最后执行派生类构造函数体中的语句,对派生类新增数据成员初始化
验证
4.2析构函数
1.派生类不继承基类的析构函数,在声明派生类时,应当定义自己的析构函数
2.派生类的析构函数只对新增成员进行清理工作,基类、子对象的清理工作仍由它们各自的析构函数负责。
3.在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,分别对基类和子对象进行清理
4.析构函数的执行顺序与构造函数正好相反 先执行派生类自己的析构函数,对派生类新增成员进行清理; 然后调用子对象的析构函数,对子对象进行清理; 最后调用基类的析构函数,对基类进行清理。
析构顺序验证
我们发现Person类只构造了两次,居然析构了三次,这是为什么呢?
其实是因为默认的Person类拷贝构造函数,现在我们显示写一下他的拷贝构造函数来验证一下
#include<iostream>
#include<string>
using namespace std;
class Person//基类
{
protected:string _name;//姓名char _sex;//性别int _age;//年龄
public:void PersonPrint(){cout << "name:" << _name << endl;cout << "sex:" << _sex << endl;cout << "age:" << _age << endl;}//Person构造函数Person(string name,char sex,int age) :_name(name), _sex(sex), _age(age){cout << "Person构造" << endl;}//Person拷贝构造Person(const Person& p){_name = p._name;_sex = p._sex;_age = p._age;cout << "Person拷贝构造" << endl;}~Person(){cout << "~Person析构" << endl;}
};
class Student :public Person
{
private:Person _teacher;//班主任(子对象)int _num;//学号
public://Student构造函数Student(string name, char sex, int age, int num, const Person& teacher):Person(name, sex, age), _num(num), _teacher(teacher){cout << "Student构造" << endl;}void StudentPrint(){PersonPrint();//打印学生的信息cout << "num:" << _num << endl;_teacher.PersonPrint();//打印老师的信息}~Student(){cout << "~Student析构" << endl;}
};
int main()
{Person teacher("zhaoli", 'F', 38);//班主任cout << "*******************************" << endl;Student s("liming", 'M', 18, 1001, teacher);//学生//s.StudentPrint();return 0;
}
那么谁是拷贝构造的呢?
第一个构造的是Person类的班主任对象,第二个则是调用基类的构造,第三个构造的是子对象是拷贝构造
5.同名成员的处理
假如在A类(基类)和B类(派生类)中有同名数据成员m,同名函数print(),那在类外面访问他们的时候会如果我们想调用A类中的函数print()和访问A类数据成员m,可以通过创建一个A类的对象,通过对象来访问。
但是既然A类被B类通过公共继承的方式继承了,那么A类的公有数据成员m和函数print(),也被继承了,但是B类(派生类)中有同名数据成员m,同名函数print()。此时如果我们建一个B类的对象,访问数据成员m和调用函数print(),会产生二义性吗(冲突)
一起来看一下
证明了同名成员也被继承
我们发现B类的对象访问的都是B类中的数据成员m、函数print(),A类的数据成员m、函数print()并没有被访问,也没有产生二义性,其实派生类会隐藏基类的同名成员,那我们怎么样才能通过派生类的对象,访问基类成员和成员函数呢?
其实很简单只需要加类名::即可
总结:
1.子类对象可以直接访问到子类中同名成员
2.子类对象加作用域可以访问到父类同名成员
3.当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
补充
继承同名静态成员处理方式
问:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
·访问子类同名成员直接访问即可
·访问父类同名成员需要加作用域
6.多继承
6.1语法
C++允许一个类继承多个类
语法:class子类:继承方式 父类1,继承方式 父类2......
多继承可能会引发父类中有同名成员出现,需要加作用域区分
6.2多继承派生类的构造函数
注意:
⑴初始化表中基类构造函数的排列顺序任意;
⑵派生类D的构造函数的执行顺序是先调用基类的构造函数,再执行派生类构造函数的函数体;
⑶调用基类的构造函数的顺序是按照声明派生类时基类出现的顺序。
示例:
7.菱形继承
如图,先声明A类,然后由它派生出B1类、B2类,D类同时继承了B1类、B2类
当菱形继承,两个父类拥有相同数据,需要加以作用域区分
A类中的数据成员a这份数据我们知道只有有一份就可以,菱形继承导致数据有两份,造成了资源浪费,该如何解决呢?
利用虚继承可以解决,在继承方式之前 加上关键字virtual即可,此时的基类称为虚基类