首先继承,继承方式和访问限定符,以及派生类的默认成员函数。然后在对菱形继承与虚继承进行详细的介绍。
继承
1. 定义
继承是面向对象复用的最要手段。通过继承定义一个类,继承是类型之间的关系建模,共享公有的东西,实现各自本质不同的东西。
2. 继承方式与访问限定符
访问限定符:public(公有)、protected(保护)、私有(private)
继承方式:public(公有继承)、protected(保护继承)、私有(private继承)
注: 1. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
2. 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承.
3. public --- “is - a”原则 :一个子类就是一个父类《Efftive C++》
3. public继承下的赋值兼容规则
//赋值兼容规则
class Person
{
public :
/*Person(const string& name)
: _name(name)
{}*/
void Display ()
{
cout<<_name<<endl;
}
public: :
string _name ; // 姓名
};
class Student : public Person
{
public: :
int _num ; // 学号
};
void Test1()
{
Person p;
Student s;
//1. 子类对象可以赋值给父类的对象(切割/切片)
p = s;
//2. 父类的指针/引用可以指向子类对象
Person* p1 = &s;
Person& r1 = s;
//3. 父类对象不能赋值给子类对象
//s = p;
//4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)(空间不重要,类型重要)
Student* p2 = (Student*)&p;
Student& r2 = (Student&)p;
//这里访问会崩溃
p2->_name = 10;
r2._num = 20;
}
4. 派生类的默认成员函数
在继承关系里面,在派生类中如果没有显示定义这六个成员函数,编译系统则会默认合成这六个默认成员函数。(子类中调用父类构造)
构造函数
父类含有默认构造/缺省构造,子类默认构造可直接调用父类默认构造,否则需要在子类中自己写构造函数,并且调用父类构造。
拷贝构造函数
Student(const Person& p)
: Person(s) //切片
, _name(s.num)
{}
析构函数
子类隐藏父类。(编译器会对析构函数名特殊处理变为同名函数)子类不需要显示的调用父类析构,编译器会自动的调用。调用完子类的析构会自动的调用父类析构。(先清理子类,在清理父类)(由于多态机制)
赋值运算符重载
赋值运算符,子类隐藏父类(父类需要显示调用)
注: 构造、拷贝构造、赋值运算符重载子类必须显示调父类(合成)
析构:子类不允许显示调用父类,子类析构完成后自动调用父类(自动调)
//派生类的默认成员函数
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 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;
}
private :
int _num ; //学号
};
菱形继承
什么是菱形继承呢?虚继承有是如何解决菱形继承的二义性和数据冗余呢?
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类两个或两个以上直接父类时,称这个继承关系为多继承
菱形继承:两个子类继承同一个父类,而又有一个子类同时继承这两个子类。
eg:
class Person
{
public :
string _name ; // 姓名
};
class Student : public Person
{
public :
int _num ; //学号
};
class Teacher : public Person
{
public :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
public :
string _majorCourse ; // 主修课程
};
分析:
由于student和Teacher都继承了Person,此时Assistant同时继承Student和Teacher,则Assistant中会存在两份Person成员(数据冗余) ,当定义Assistant对象a去访问a._name时,由于不明确访问Student中的_name还是Teacher中的_name,这就产生二义性,会出现访问不明确错误。
虚继承
C++使用虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。
虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
eg:
class Person
{
public :
string _name ; // 姓名
};
class Student : public virtual Person
{
public :
int _num ; //学号
};
class Teacher : public virtual Person
{
public :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
public :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "xxx";
a._num = 1;
a._id = 2;
a._majorCourse = "yyy";
}
分析:
引进虚基类之后,派生类(子类)的对象中只存在一个虚基类的子对象;当一个类拥有虚基类的时候,编译系统会为这个类的对象定义一个指针成员,并让它指向虚基类的子对象;该指针被称为虚基类指针; 这样的话,不同继承路径上的虚基类子对象在派生类中被合并成一个子对象了,这便是虚基类的作用,这样就可以消除合并之前出现的二义性问题。
注:
虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。