目录
接下来的日子会顺顺利利,万事胜意,生活明朗-----------林辞忧
衔接上篇的内容继续介绍
一:子类的六大默认成员函数
1.默认的意思就是我们不写,编译器会自动生成一个,在子类中的默认成员函数要把父类单独看成一个独立的对象来完成对应操作
父类的默认函数
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 id)
:Person(name)//作为一个单独对象构造
,_id(id)
{
cout << "Student(const char* name, int id)" << endl;
}
// 子类默认生成的拷贝构造 赋值重载跟拷贝构造类似
// 父类成员(整体) -- 调用父类的拷贝构造
// 子类自己的内置成员 -- 值拷贝
// 子类自己的自定义成员 -- 调用他的拷贝构造
// 一般就不需要自己写了,子类成员涉及深拷贝,就必须自己实现
Student(Student& s)
:Person(s)//父子类的赋值规则
, _id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(Student& s)
{
if (this != &s)
{
_id = s._id;
Person::operator = (s);//父子类的赋值规则
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
~Student()
{
//Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _id;
};
特殊的对于子类此处的析构函数,不能显示调用父类的析构,原因为
1.由于多态的原因,析构函数统一会被处理成destructor
2.父子类的析构函数构成隐藏
3.为了保证析构安全,先子后父(如果先父后子的话,在父类析构后,在子类还可以访问父类的成员,此时就会导致访问已经释放的空间,导致越界访问),构造为先父后子,在走初始化初始化时,走的是声明的顺序,一定能保证先构造父类,再子类的
4.父类析构函数不需要显示调用,子类析构函数结束时会自动调用父类析构,保证先子后父
练习:如何实现一个不能被继承的类
方法为:可以将父类的成员函数或者整个成员访问方式设置为private,这样继承的时候无论继承方式为那种,都是不可见的
在c++11中新添加了final关键字,使用的话意味着该类不能被继承
二:继承与友元
友元关系不能继承,基类友元不能访问子类私有和保护成员
如要访问子类的私有和保护成员的话,在子类中也声明友元关系(就是基类的友元声明在派生类声明一下)
三:继承与静态成员
基类中定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例
练习:计算创建多少个类对象--子类初始化时不管我们写不写都会走初始化列表,也一定会走父类的构造
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
Student func()
{
Student st;
return st;
}
int main()
{
Person p;
Student s;
func();
cout << Student::_count << endl;
return 0;
}
四:复杂的继承关系+菱形继承+菱形虚拟继承
1.单继承
一个子类只有一个直接父类
2.多继承
一个子类有两个或以上直接父类
3.菱形继承;一种特殊的多继承
从这里就可以看出,在Assistant的对象中就有两份Person成员,这就导致数据冗余和二义性的问题,这样在访问数据的时候就不知道具体访问哪一个
4.菱形虚拟继承
于是提出了使用虚继承即添加关键字virtual来解决,这是就只有一份Person成员,只初始化一次
如:
class Person
{
public:
string _name; // 姓名
int _age;
int _tel;
// ...
};
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; // 主修课程
};
void Test()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a.Student::_name = "小张";
a.Teacher::_name = "老张";
a._name = "张三";
}
int main()
{
Test();
return 0;
}
5.虚拟继承解决数据冗余和二义性的原理
不使用虚拟继承前数据冗余的情况
发现在B子类 和C子类中都有一份父类A的数据,造成数据冗余
使用虚拟继承数据的情况
这时父类的数据只有一份,子类访问的都是同一父类的数据
这时就会看到在 添加virtual的子类中会多存储一个地址,这个地址其实存储的是距离A的偏移量
此时A同属于B和C,这样B和C该如何找到公共的A呢,这里是通过了B和C的两个指针,指向的一张表,这两个指针叫虚基表指针,这两个表叫虚基表,虚基表中存的是偏移量,通过偏移量找到A
这个偏移量就是从该位置向后跳跃的字节数,然后找到并访问父类的成员数据
经典例题
对于p1和p3指向的位置相同,但访问权限不同
打印顺序为什么?
对于任何的派生类的构造都要 调用基类的构造
注意:初始化列表出现的顺序是按声明顺序来执行的,不是执行顺序
所以结果为 class A,class B ,class C,class D
一般不建议设计出菱形继承
五:继承和组合
1.public继承是一种is-a的关系,就是说每个派生类对象都是一个基类对象
2.组合是一种has-a的关系,假设B组合了A,每个B对象中都有一个A对象
对于这两个的大小都是一样大的,
但对于继承父类中的protected 成员可以在子类中访问
对于组合的话,一个类中的protected成员,另一个类不能使用,不能直接调用另一个类的public成员函数
3.
4.实际尽量多去使用组合,组合的耦合度低,代码的可维护性好。对于有些关系就适合继承就使用继承,对于多态,也必须实现继承,对于既能用组合又能继承的,就使用组合