继承(多继承,默认成员函数)
多继承,默认成员函数
- 继承的概念
- 继承的定义和格式
- 1.定义
- 2.格式
- 3.继承基类成员访问方式的变化
- 继承类模板
- 基类和派生类的转换
- 继承的作用域
- 隐藏规则
- 四个常见的默认成员函数
- 派生类的构造。
- 派生类的拷贝
- 赋值重载
- 析构函数
- 继承与友元
- 继承与static成员
- 多继承及菱形继承问题
- 菱形继承的问题
- 虚继承面临的问题。
- 继承和组合
- 作业:
继承的概念
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有
类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承
呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的
复⽤,继承是类设计层次的复⽤。
定义就是:每个人都有自己的姓名,年龄,电话,地址,但是如果类封装的时候,对于老师和学生都会里面都有同样的成员,太冗余,继承就解决了这个问题
继承的定义和格式
1.定义
//共同的类
class Person
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){std::cout << "void identity()" << _name << std::endl;}
protected:std::string _name = "张三"; // 姓名std::string _address; // 地址std::string _tel; // 电话int _age = 18; // 年龄
};//studen为子类,person为父类
class Student : public Person
{
public:// 学习void study(){// ...}protected:int _stuid; // 学号
};//teacher为子类
class Teacher : public Person
{
public:// 授课void teaching(){//...}
protected:std::string title; // 职称
};int main() {Student d1;d1.identity();//可以访问继承基类的函数,这里public继承,identity()也是public 限制访问符所以可以直接调用Teacher s1;s1.identity();}
2.格式

3.继承基类成员访问方式的变化
- 继承方式有三种:public,protected,private.
- 访问限制符也有三种:public ,protected,private.
所以继承基类成员的访问方式有9 种

看着这么麻烦,其实你只需要记住几个规则:
-
基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问 它。

这里基类的private成员,不管什么方式继承,都是不可以访问的。
解决:就是你自己在基类定义一个不是private的函数,里面访问age,然后这样就可以了 -
基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
可以对着表格看,只要是protected基类,在派生类里面最大都是protected,所以类外不能直接访问
- 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),public > protected >
private。
就是访问限制符和继承方式两个里面最小的那个,比如访问为public,限制符为private,然后继承下来的成员就为private.
- 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显
⽰的写出继承⽅式。


5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。
继承类模板
与普通类不同的是,类模板在只有在使用函数的时候才会实列化,开始不会实列化,构造函数在创建对象会实列化和成员变量也是声明不会立即检查错误。就是里面的函数还不会编译,检查。普通类会一开始就全部实列化。一开始就全部编译。
namespace jcm {template<class T>class stack:public std::vector<T> {public:void push( const T x) {//这里传值引用必须加const,因为3是个临时变量.std::vector<T>::push_back(x);}};}int main() {//Student d1;//d1.identity();//可以访问继承基类的函数,这里public继承,identity()也是public 限制访问符所以可以直接调用//Teacher s1;//s1.identity();jcm::stack<int> d1;d1.push(3);}
我的错误:对于3,临时变量,引用没有加const ,
如果不显示实列化,会找不到push_back,因为没有实列化,就没有push_back;基类::push_back();
这里的逻辑是调用d1.push才实列化push,知道模板是int,然后加个类域实列化push_back.
基类和派生类的转换
int a = 2.3;const double& s = a;std::string s1 = "1111";const std::string e = s1;//但是派生类和基类的转换不需要const;Student a1;Person p1 = a1;Person& p2 = a1;Person* p3 = &a1;
对于派生类和基类的转换,不需要const,为什么,派生转换给基类,相当于切片,把继承下来的成员又切回去。
- 上面是派生类转换给基类,但是反过来基类对象转换给派生类对象不行。
- 基类的指针或者引用可以强制类型转换为派生类的指针或者引用,前提基类的指针或者引用都需要指向派生类
继承的作用域
隐藏规则
- 基类和派生类有同名的成员,如果访问这个成员,只能访问派生类里面的,但是可以通过类名::来访问
class Person
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){std::cout << "void identity()" << _name << std::endl;}
protected:std::string _name = "张三"; // 姓名std::string _address; // 地址std::string _tel; // 电话int _age = 18; // 年龄
};//studen为子类,person为父类
class Student :public Person
{
public:// 学习void study(){std::cout << _age;}protected:double _age = 90;int _stuid; // 学号
};

即使类型不同,只要同名访问,只能访问派生自己的
解决方案:

- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
class A
{
public:void fun(){std::cout << "func()" << std::endl;}
};
class B : public A
{
public:void fun(int i){std::cout << "func(int i)" << i <<std:: endl;}
};
int main()
{B b;b.fun(10);b.fun();return 0;
};

上面就是典型的隐藏规则,调用fun的时候,在派生类里面找到了,但是参数不对就编译报错,所以选B,A。
四个常见的默认成员函数
派生类的构造。
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 {protected:int _age;string num;//学号;
};int main() {Student d1;}
基类有默认构造的时候,我们派生类也可以不写,因为内置成员都调自己的默认构造,自定义调他自己的默认构造,而基类也可以调用自己的默认构造(也包括自己不实现编译器实现的默认构造)。
- 如果基类没有默认构造,怎么办,那我们需要在派生类里面自主调用基类的构造
//没有的情况
class Person
{
public:Person(const char* name): _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 _age = 19, string num = "1111"):Person(name)//这种的形式., _age(_age),num(num){}protected:int _age;string num;//学号;
};int main() {Student d1;}
派生类的拷贝
对于拷贝,一般不需要自己在派生类里面实现,它会自动调用自己的默认拷贝构造和基类的拷贝构造或者基类的默认拷贝构造。但是默认拷贝是浅拷贝,如果内置成员需要深拷贝,要自主实现拷贝构造的时候,我们要显示调用基类的拷贝构造,不然它会调用person(),而没有默认构造函数,发生错误。
class Person
{
public:Person(const char* name): _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 _age = 19, string num = "1111"):Person(name), _age(_age),num(num){}Student(Student& d1):Person(d1), _age(d1._age),num(d1.num){}
protected:int _age;string num;//学号;
};int main() {Student d1;Student d2(d1);}
可以自己不用实现拷贝构造,如果有深拷贝,自己实现,一定要显示调用拷贝构造
class Person
{
public:Person(const char* name): _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; // 姓名
};
1.虽然没有自己实现基类拷贝构造,但是显示调用也会调用默认的拷贝构造。
2.派生类没有实现拷贝,基类实现了拷贝。编译器也会自动调用实现的拷贝.
这个要注意

赋值重载

如果这里不显示调用的话,不指定类域,会触发无限递归,因为隐藏的规则,调用operator会先先从派生类找,如果找到operator,就会一直调用这个operator.
正确要显示实列化
class Person
{
public:Person(const char* name): _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 _age = 19, string num = "1111"):Person(name), _age(_age),num(num){}Student(Student& d1):Person(d1), _age(d1._age),num(d1.num){}Student& operator=(const Student&x) {if (this != &x) {//只有比较地址,不能比较对象,因为没有重载!=Person::operator=(x);_age = x._age;num = x.num;}return *this;
}protected:int _age;string num;//学号;
};int main() {Student d1("小米");Student d2;d1 = d2;}
析构函数
如果我们要自己写析构,调用基类的析构必须加类名
~Student() {Person::~Person();}
但是两个对象会析构四次为什么?

在析构,即使我们显示写了,它也还会调用基类的析构,所以我们一般不显示写,如果没有资源释放,我们一般不写析构
继承与友元
直接给说一个概念:B是A的友元,如果C继承A,那B会是C的友元吗,答案是不会,友元不会继承。
class student;
class person {
public:friend void Display(const person& x, const student& y);protected:int _age;};class student:public person {
public ://friend void Display(const person& x, const student& y);protected:int _num;};void Display(const person& x, const student& y) {std::cout << x._age<<std::endl;cout << y._num;
}
int main() {}
如果没有这个派生类的友元就会报错,

一些结论,基类和派生类,派生类改变继承下来的成员值,不会改变基类原来的值。友元一般放在什么访问限制符都不会影响他访问类的私有成员。一般推荐放在public
继承与static成员
首先我们要了解static 成员:它属于类本身,但类的特定实列化成员,属于每个类。
////static成员的继承
class Person
{
public:string _name;static int _count;
};
int Person::_count = 0;//在类外进行初始化
class Student : public Person
{
protected:int _stuNum;
};
int main()
{Person p;Student s;// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份cout << &p._name << endl;cout << &s._name << endl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的// 说明派⽣类和基类共⽤同⼀份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员cout << Person::_count << endl;cout << Student::_count << endl;return 0;}

多继承及菱形继承问题
单继承就是只继承一个类,多继承就是一下继承多个类。
- 单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
- 多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型
是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。这个标记的就可以了解在构造派生类时,初始化列表初始化时,先继承的先初始化,自己的成员最后初始化.
多继承的格式,中间用逗号隔开

菱形继承的问题

这就是菱形继承
class Student : public Person
{
protected:int _num; //学号
};
//
class Teacher : public Person
{
protected:int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{// 使⽤虚继承,可以解决数据冗余和⼆义性Assistant a;a._name = "peter";//继承了两个name,编译器也不知道调哪个?}
这里会报错,因为Assiant继承了两个name,编译器不知道调哪个,造成二义性和冗余。如图:

解决:
- 第一可以添加类名,指出哪个域,但是如果person有多个数据,什么age,等,那不是这些对于都麻烦了。
- 虚继承,在student,teacher继承那里加个virtual.这样改变了内存存储的结构,导致没有冗余和二义性.person对于单纯分出来了

virtual的代码
class Person
{
public:string _name; // 姓名/*int _tel;int _age;string _gender;string _address;*/// ...
};
//
class Student :virtual public Person
{
protected:int _num; //学号
};
//
class Teacher :virtual public Person
{
protected:int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{// 使⽤虚继承,可以解决数据冗余和⼆义性Assistant a;a._name = "peter";//继承了两个name,编译器也不知道调哪个?}
虚继承面临的问题。
- 对于assiant的构造面临的问题,构造的时候太麻烦,容易不理解
class Person
{
public:Person(const string &x) {_name = x;}string _name; // 姓名/*int _tel;int _age;string _gender;string _address;*/// ...
};
// 虚继承
class Student :virtual public Person
{
public:Student(string&name,int num):Person(name),_num(num){}protected:int _num; //学号
};
//
class Teacher :virtual public Person
{
public:Teacher(string&name,int id) :Person(name),_id(id){}
protected:int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{
public:Assistant(string name1,string name2,string name3):Person(name1)//这个person还要单独拿出来构造,Student(name2,1),Teacher(name3,2){}protected:string _majorCourse; // 主修课程
};
int main()
{Assistant a1("张三", "王五", "李四");
最后这个a1的name是张三,这个构造的顺序是先person,student,teacher,.但为什么还是张三,因为在调用student的时候,里面没有person,person已经被单独拿出来了,所以不会在student里面进行person的再次构造,
- 使用虚继承会降低运行效率。
库里面的输入输出都使用了多继承,使用了虚继承。,还有这个virtual虽然把person分离出去了,这个虚继承的分离只是在多继承的菱形里面有效。但是再实列化student的对象的时候,还是里面还是会有person继承下来了,person还是student的内部。
继承和组合
继承可以看成白盒,对于派生类可以看到基类里面的所有成员,函数。组合,列如适配器和在一个类定义另一个类的对象当作成员,相当于黑盒,里面的保护和私有成员不能访问。降低了类之间的关联度
- public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
- 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
- 继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤ (white-box
reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可 ⻅。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依 赖关系很强,耦合度⾼。 - 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),
因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关
系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。 - 优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太
那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的
关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合

作业:
1

这个A选项,final的类就不能被继承。
B选项,使用组合
c选项还有组合方式的复用
所以答案是D
2
A.选项,只要函数相同,参数不同也可以,这是函数重载.
B选项,基类和子类的同名的函数他们是隐藏的关系
C选项,对于继承,有隐藏关系,可以相同
D在同一个类里面。不能有同名的变量
3

这里A有默认构造函数,所以B中不用显示调用构造,先构造A,再构造B,然后先构造后析构
所以选C








