目录
1.派生类和基类的构造析构关系
【1】派生类并不继承基类的构造和析构函数,只继承成员变量和普通成员方法
- 不继承,意思是派生类中确实没有,不包含基类的构造和析构函数
- 派生类自己有自己的构造和析构,且构造和析构的规则和之前讲过的完全一样
- 研究构造和析构函数时,一定要注意默认规则
【2】派生类的构造函数一定会调用基类的构造函数,析构也一样
- 代码验证:在基类和派生类中都显示提供“默认构造”并添加打印信息,通过执行结果来验证
/***********person.hpp***************/
#ifndef __PERSON_HPP__
#define __PERSON_HPP__
#include <string>
#include <iostream>
using namespace std;
class person
{
public:
//成员变量
int age;
string name;
//构造函数
person()
{
cout << "person()" << endl;
}
//析构函数
~person()
{
cout << "~person()" << endl;
}
//成员方法
void speak(void);
void print(void);
private:
bool male;
};
#endif
/***********person.cpp***************/
#include "person.hpp"
void person::speak(void)
{
}
void person::print(void)
{
}
/***********man.hpp***************/
#ifndef __MAN_HPP__
#define __MAN_HPP__
#include "person.hpp"
class man:public person
{
public:
//构造函数
man()
{
cout << "man()" << endl;
}
//析构函数
~man()
{
cout << "~man()" << endl;
}
void work(void);
private:
};
#endif
/***********man.cpp***************/
#include "man.hpp"
void man::work(void)
{
}
/***********main.cpp***************/
#include "man.hpp"
#include "person.hpp"
int main()
{
man man1;
return 0;
}
实验结果:
- 通过代码执行结果看到的现象总结:派生类的构造函数执行之前,会先调用基类的构造函数,然后再调用自己的构造函数。而在派生类的析构函数之后,会先执行自己的析构函数,再执行基类的析构函数。
- 代码验证:派生类的任意构造函数,可以显式指定调用基类的任意一个构造函数,通过参数匹配的方式(类似于函数重载)
/***********man.hpp***************/
#ifndef __MAN_HPP__
#define __MAN_HPP__
#include "person.hpp"
class man:public person
{
public:
int length;
//构造函数
// man() 简写 man:person()才是默认构造函数的完整写法
man():person("aston")
{
cout << "man()" << endl;
}
man(int mylen):person()
{
this->length = mylen;
cout << "man(int mylen)" << endl;
}
//析构函数
//~man():~person() 显示写了后面的基类析构就会编译报错
~man()
{
cout << "~man()" << endl;
}
void work(void);
private:
};
#endif
/***********person.hpp***************/
#ifndef __PERSON_HPP__
#define __PERSON_HPP__
#include <string>
#include <iostream>
using namespace std;
class person
{
public:
//成员变量
int age;
string name;
//构造函数
person()
{
cout << "person()" << endl;
}
//自定义构造函数
person(string myname)
{
this->name = myname;
cout << "person(string myname)" << endl;
}
//析构函数
~person()
{
cout << "~person()" << endl;
}
//成员方法
void speak(void);
void print(void);
private:
bool male;
};
#endif
- 思考:为什么派生类的构造函数必须调用基类的1个构造函数,这样设计有什么合理性?
【3】为什么派生类的构造(析构)必须调用基类的某个构造(析构)
- 牢记构造函数的2大作用:初始化成员,分配动态内存
- 派生类和基类各自有各自的构造函数和析构函数,所以是各自管理各自的成员初始化,各自分配和释放各自所需的动态内存
- 继承的语言特性,允许派生类调用基类的构造和析构函数,以管理派生类从基类继承而来的那些成员。
- 明确:派生类的构造和析构处理的永远是派生类自己的对象,只是派生类对象模板中有一部分是从基类继承而来的而已。
【4】其他几个细节
- 派生类构造函数可以直接全部写在派生类声明的class中,也可以只在clas中声明时只写派生类构造函数名和自己的参数列表,不写继承基类的构造函数名和参数列表,而在派生类的cpp文件中再写满整个继承列表,这就是语法要求
// xx.hpp
man(int mylen);
// xx.cpp
man::man(int mylen):person()
{
this->length = mylen;
cout << "man(int mylen)" << endl;
}
- 派生类析构函数则不用显式调用,直接写即可直接调用基类析构函数。猜测是因为参数列表问题。
//~man():~person() 显示写了后面的基类析构就会编译报错
~man()
{
cout << "~man()" << endl;
}
- 构造函数的调用顺序是先基类再派生类,而析构函数是先派生类再基类,遵循栈规则。
- 派生类的构造函数可以在调用基类构造函数同时,用逗号间隔同时调用初始化式来初始化派生类自己的成员
/* :继承基类,初始化式 */
man(int mylen):person(),length(mylen)
{
//this->length = mylen; 上面用初始化式初始化了
cout << "man(int mylen)" << endl;
}
【5】派生类做的三件事
- 吸收基类成员:除过构造和析构函数以外的所有成员全部吸收进入派生类中
- 更改继承的成员。1是更改访问控制权限(根据继承类型还有成员在基类中的访问类型决定) 2是同名覆盖(派生类中同名成员覆盖掉基类中)
- 添加派生类独有的成员。
2.派生类和基类的同名成员问题
【1】派生类中再实现一个基类中的方法会怎样
- 代码实验:派生类和基类中各自实现一个内容不同但函数原型完全相同的方法,会怎么样
- 结论:基类对象调用的是基类的方法,派生类对象调用执行的是派生类中重新提供的方法
- 这种派生类中同名同参方法替代掉基类方法的现象,叫做:重定义(redefining),也有人叫做隐藏。
- 隐藏特性生效时派生类中实际同时存在2份同名同参(但在不同类域名中)的方法,同时都存在,只是一个隐藏了另一个
【2】派生类中如何访问被隐藏的基类方法
- 派生类对象直接调用时,隐藏规则生效,直接调用的肯定是派生类中重新实现的那一个
- 将派生类强制类型转换成基类的类型,再去调用则这时编译器认为是基类在调用,则调用的是基类那一个,隐藏规则被绕过了,一般不推荐
- 在派生类内部,使用
父类::
方法()的方式,可以强制绕过隐藏规则,调用父类实现的那一个
【3】注意和总结
- 其实不止成员方法,成员变量也遵循隐藏规则。
- 隐藏规则本质上是大小作用域内同名变量的认领规则问题,实际上2个同名成员都存在当前派生类的对象内存中的
- 隐藏(重定义,redefining),与重载(overload)、重写(override,又叫覆盖),这三个概念一定要区分清楚。
3.子类和父类的类型兼容规则
【1】何为类型兼容规则
- C和C++都是强类型语言,任何变量和对象,指针,引用等都有类型,编译器根据类型来确定很多事
- 派生类是基类的超集,基类有的派生类都有,派生类有的基类不一定有,所以这2个类型间有关联
- 派生类对象可以cast后当作基类对象,而基类对象不能放大成派生类对象,否则就可能会出错
- 考虑到指针和引用与对象指向后,派生类和基类对象的访问规则就是所谓类型兼容规则。
【2】类型兼容规则的常见情况
int main()
{
person pn1;
man mn1;
person pn2 = mn1; //子类对象可以直接初始化或直接赋值给父类对象
person* p = &mn1; //类型兼容
p->speak(); //执行的是person类里面的speak()
mn*p = &person; //编译报错,派生类是基类的超集
person& rp = mn1; //类型兼容
rp.speak(); //执行的是person类里面的speak()
mn& rp = pn1; //编译报错,派生类是基类的超集
return 0;
}
- 子类对象可以当作父类对象使用,也就是说子类对象可以无条件隐式类型转换为一个父类对象
- 子类对象可以直接初始化或直接赋值给父类对象
- 父类指针可以直接指向子类对象
- 父类引用可以直接引用子类对象
【3】总结
- 派生类对象可以作为基类的对象使用,但是只能使用从基类继承的成员。
- 类型兼容规则是多态性的重要基础之一。
- 子类就是特殊的父类 (base *p = &child;)
4.继承的优势与不良继承
【1】何为不良继承
- 鸵鸟不是鸟问题。因为鸵鸟从鸟继承了fly方法但是鸵鸟不会飞
- 圆不是椭圆问题。因为圆从椭圆继承了长短轴属性然而圆没有长短轴属性
- 不良继承是天然的,是现实世界和编程的继承特性之间的不完美契合
【2】如何解决不良继承
- 修改继承关系设计。既然圆继承椭圆是一种不良类设计就应该杜绝。去掉继承关系,两个类可以继承自同一个共同的父类,不过该类不能执行不对称的setSize计算,然后在圆和椭圆这2个子类中分别再设计以区分。
- 所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。
5.组合介绍以及与继承对比
【1】什么是组合
- composition,组合,就是在一个class内使用其他多个class的对象作为成员
- 用class tree做案例讲解
class shugen
{
}
class shugan
{
}
class shuzhi
{
}
class shuye
{
}
class tree
{
shugen gen;
shugan gan;
shuzhi zhi;
shuye ye;
}
- 组合也是一种代码复用方法,本质也是结构体包含
【2】继承与组合的特点对比
- 继承是a kind of(is a)关系,具有传递性,不具有对称性。
- 组合是a part of(has a)的关系,
- 继承是白盒复用。因为类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的。
- 继承的白盒复用特点,一定程度上破坏了类的封装特性,因为这会将父类的实现细节暴露给子类
- 组合属于黑盒复用。被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小
- 组合中被包含类会随着包含类创建而创建,消亡而消亡。组合属于黑盒复用,并且可以通过获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合。而缺点就是致使系统中的对象过多。
- OO设计原则是优先组合,而后继承
6.多继承及其二义性问题
【1】多继承
- 多继承就是一个子类有多个父类
【2】多继承的二义性问题1
- 场景:C多继承自A和B,则C中调用A和B的同名成员时会有二义性
/************test.hpp*************/
#ifndef __TEST_HPP__
#define __TEST_HPP__
class A
{
public:
void set(int a); //设置a的值
void printA(void);
private:
int val1;
};
class B
{
public:
void set(int a); //设置a的值
void printB(void);
private:
int val2;
};
class C:public A,public B
{
};
#endif
/************test.cpp*************/
#include "test.hpp"
#include <iostream>
using namespace std;
void A::set(int a)
{
this->val1 = a;
}
void A::printA(void)
{
cout << "val1 = " << this->val1 << endl;
}
void B::set(int a)
{
this->val2 = a;
}
void B::printB(void)
{
cout << "val2 = " << this->val2 << endl;
}
int main(void)
{
C a;
a.set(1); //这句有二义性,因为编译器无法确定到底是想调用A类还是B类里面的set()函数
a.A::set(1); //没有二义性
return 0;
}
- 原因:C从A和B各自继承了一个同名(不同namespace域)成员,所以用C的对象来调用时编译器无法确定我们想调用的是哪一个
- 解决办法1:避免出现,让A和B的public成员命名不要重复冲突。但这个有时不可控。
- 解决办法2:编码时明确指定要调用哪一个,用c.A::func()明确指定调用的是class A的func而不是class B的
- 解决办法3:在C中重定义func,则调用时会调用C中的func,A和B中的都被隐藏了
- 总结:能解决,但是都没有很好的解决。
【3】多继承的二义性问题2
- 场景:菱形继承问题。即A为祖类,B1:A, B2:A, C:B1,B2,此时用C的对象调用A中的某个方法时会有二义性
/************test.hpp*************/
#ifndef __TEST_HPP__
#define __TEST_HPP__
class A
{
public:
void set(int a); //设置a的值
void print(void);
private:
int val1;
};
class B1:public A
{
public:
void set(int a);
void print(void);
private:
int valB1;
};
class B2:public A
{
};
class C:public B1,public B2
{
};
#endif
/************test.cpp*************/
#include "test.hpp"
#include <iostream>
using namespace std;
void A::set(int a)
{
this->val1 = a;
}
void A::print(void)
{
cout << "val1 = " << this->val1 << endl;
}
void B1::set(int a)
{
this->valB1 = a;
}
void B1::print(void)
{
cout << "valB1 = " << this->valB1 << endl;
}
int main(void)
{
C c;
//c.set(); //有歧义
c.B1::set(1); //没有歧义
c.B1::print();
c.A::set(2); //有歧义
c.A::print();
return 0;
}
- 分析:c.func()有二义性,c.A::func()也有二义性,但是c.B1::func()和c.B2::func()却没有二义性
- 解决办法:和问题1中的一样,但是问题2更隐蔽,也更难以避免
【4】总结
- 二义性就是歧义,好的情况表现为编译错误,不好的情况表现为运行时错误,最惨的情况表现为运行时莫名其妙
- 随着系统的变大和变复杂,难免出现二义性,这不是程序员用不用心的问题,是系统自身带来的
- 解决二义性问题不能靠程序员个人的细心和调试能力,而要靠机制,也就是编程语言的更高级语法特性
- 虚函数、虚继承、纯虚函数、抽象类、override(重写,覆盖)、多态等概念就是干这些事的
7.虚继承解决菱形继承的二义性问题
【1】虚继承怎么用
- 场景:菱形继承导致二义性问题,本质上是在孙子类C中有B1和B2中包含的2份A对象,所以有了二义性。
- 虚继承解决方案:让B1和B2虚继承A,C再正常多继承B1和B2即可
class B1:virtual public A
{
public:
void set(int a);
void print(void);
private:
int valB1;
};
class B2:virtual public A
{
};
C c;
c.A::set(2); //没有歧义
c.A::print();
- 虚继承就这么简单,就是为了解决菱形继承的二义性问题而生,和虚函数(为了实现多态特性)并没有直接关系
【2】虚继承的实现原理
- 虚继承的原理是:虚基类表指针vbptr和虚基类表virtual table
- 参考博客:虚继承的原理