前言:
上篇文章我们一起初步了解了继承的概念和使用,本章我们回家新一步深入探讨继承更深层次的内容。
前文回顾——>继承---上
目录
(一)派生类的默认成员函数
(1)6个默认成员函数
(2)派生类的默认成员函数使用规则
(3)实例化详解
(4)应用:如何设计一个不能继承的类
(二) 继承与友元
(三)继承与静态成员
(四)多继承和菱形继承
(1)菱形继承的问题
(2)解决方法之虚拟继承
(3)虚拟继承的底层原理
在之前类和对象的学习中,我们详细学习了基类的默认成员函数
类和对象默认成员函数复习——>类和对象的默认成员函数
那么派生类的默认成员函数的使用规则是什么样的呢?
我们浅浅回顾一下~
我们6个成员函数对于一个普通的类适用,那么对于基类也适用
下面是我们6大默认成员函数
顾名思义,就是对于一些默认类型,我们自己不需要给出具体的成员函数,编译器可以自动生成。我们前面也详解过,对于自定义类型还是需要我们手动实现深拷贝,编译器自动生成的都是浅拷贝等等(遗忘的童鞋可以回顾之前的博文)
我们这里引用一个Person父类和Student的子类来一一验证。
Person类:
#include
using namespace std;
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; // 姓名
};
Student类:
class Student : public Person
{
public:
Student(const char* name, int num)
: Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
我们下面开始测试:
父类成员构造、拷贝构造、赋值、析构:
测试代码:
运行结果:
此时父类和普通类的默认成员函数使用规则一样。
派生类成员构造、拷贝构造、赋值、析构 :
这里我们可以清楚观察到对于派生类对象的构造函数、拷贝构造函数和赋值重载:
继承的派生类对象都必须先调用基类的构造(拷贝构造、赋值重载)然后再完成自己这部分的构造(拷贝构造、赋值重载)
对于析构函数:
派生类对象析构清理先调用派生类析构再调基类的析构。
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
开门见山:
方法:把构造函数私有化。
class A
{
private:
A()
{}
};
class B : public A
{
};
int main()
{
B b;
return 0;
}
但是问题来了:
这时候就还有一个问题A类想单独构造对象也不行了
解决办法:(单例设计模式)
这时又有一个问题 —— 先有鸡还是先有蛋的问题:
解决办法:
用一个静态成员函数就能很好的解决问题
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
报错:
问题:
比如说父类有一个静态成员,那子类继承之后,子类会增加一个静态成员还是和父类共享一个静态成员呢?
验证代码:
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
int main()
{
TestPerson();
return 0;
}
这里我们输出的会是3还是4呢?
通过输出我们可以发现:
多继承:
菱形继承:
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void Test()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";
}
报错:
我们可以指定作用域来解决二义性问题:
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
但是数据冗余的问题无法解决:
菱形虚拟继承解决了数据冗余和二义性的问题。
先看代码:
class Person
{
public:
string _name; // 姓名
};
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._name = "peter";
cout << a._name << endl;
}
int main()
{
Test();
}
样例代码:
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d._a = 0;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
运行调试我们发现虚继承之后无论有没有指定作用域,各个作用域_a的值是一样的:
我们再调用内存管理来更深入的探究:
我们&d后找到d的内存,大致能看出 _a,_b,_c,_d的存放位置,按照常理来说,_a按照顺序应该是在前面存放,但事实上确是在最后,那第一个位置存放的地址中到底是什么呢?
我们访问第一个位置存放的地址:
我们发现存放了00 00 00 00 然后有一个14 00 00 00,14是十六进制换算十进制就是16+4=20,20个字节正好够存放五个地址 ,我们惊奇的发现02 00 00 00和这个地址的偏移量就恰好是20个字节(5个地址),那么我们可以假设第一个地址中存放的是_a的偏移量。
而_c上面的地址54 7b b1 00 我们同样方法访问可以得到:
0c转换成十六进制就是12,也就是12个字节,而_a正好也和它相差3个地址的偏移量,我们更可以确信这是存放偏移量的地址。
模型总结:
菱形虚拟继承调整了对象的模型。
而且B和C虚继承后第一个地址经过我们分析存放的是偏移量,用来找到存储_a的地址。
为什么要搞这个偏移量呢?
我们发现找偏移量的时候,第一个位置存放的总是00 00 00 00。
为什么偏移量存储在第二个位置,而不是存在第一个位置:
菱形虚拟继承的缺点:
模型的优点:
因为不同的编译的设计的不同,A对象存储的位置也会不一样,但是只要有指针去找偏移量,再通过偏移量去找A就能找到,这是通用的方法,统一模型。
补充:
图解:
一道练习题:
class A
{
public:
A(const char* s)
{
cout << s << endl;
}
~A()
{}
};
class B : virtual public A
{
public:
B(const char* s1, const char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
class C : virtual public A
{
public:
C(const char* s1, const char* s2)
:A(s1)
{
cout << s2 << endl;
}
};
class D : public B, public C
{
public:
D(const char* s1, const char* s2, const char* s3, const char* s4)
:B(s1, s2)
,C(s1, s3)
,A(s1)
{
cout << s4 << endl;
}
};
int main()
{
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
输出是什么?
原因:
总结
感谢您的阅读,祝您学业有成!