一、继承的概念和定义
1、继承的概念
继承是实现代码可复用性的重要方法,在保留原类的特征基础上做一些扩展,形成新的类,叫做派生类。
- 一个例子:
父亲有一套房子,儿子长大了,父子两人共同拥有这套房。但是经过儿子的奋斗,自己又买了一套房,此时儿子可以说是拥有两套房,一套与父亲共享,一套完全属于自己。就像派生类一样,既有父类的东西,又有自己的东西。
2、继承的定义
(1)定义格式
class Student : public Person
{
public:
int _stuid;
int _major;
}
其中Person是父类,Student是子类
(2)访问限定符
(一般情况下会选择用public方式继承,很少用private和protected)
二、基类和派生类对象的赋值转换
1、派生类对象可以赋值给基类对象、基类指针、基类引用。(因为可以将派生类的基类部分切割进行赋值)
2、基类对象不能赋值给派生类(派生类的东西比基类的东西多了一大截)
3、基类指针可以通过强制类型转换赋值给派生类指针。(只有基类指针是指向派生类对象的时候,才是安全的)
三、继承中的作用域
1、基类和派生类都有自己独立的作用域
2、若子类和父类有同名成员,**子类会将父类的同名成员隐藏(重定义)**掉。(可通过基类::基类成员 进行显式访问)
3、只要同名就会构成隐藏
四、派生类默认成员函数
六个默认成员函数:
1、构造函数
2、析构函数
3、拷贝构造
4、赋值运算符重载
5、&取地址运算符重载
6、const &取地址运算符重载
注意:
(1)派生类的构造函数必须调用基类的构造函数初始化基类的部分成员,如果基类没有默认构造函数,必须在派生类构造函数的初始化列表进行显式调用。
(2)拷贝构造和赋值运算符重载都要先调基类对应拷贝构造和运算符重载。
(3)析构时,是先析构派生类,再析构基类
好事先爸爸,坏事先儿子
#include<iostream>
using namespace std;
class Person
{
public:
Person(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(char* name, int num)
:Person(name)//注意这里是Person
, _stuid(num)
{
cout << "Student()" << endl;
}
Student(Student& s)
:Person(s)//这里直接用s构造Person
, _stuid(s._stuid)
{
cout << "Student(Student& s)" << endl;
}
Student& operator=(const Student& stu)
{
if (this != &stu)
{
Person::operator=(stu);//先调person的=重载
_stuid = stu._stuid;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;//本质上先析构子类在析构父类
}
protected:
int _stuid;
};
五、继承与友元
友元关系不能继承,即基类友元不能访问子类的私有和保护成员。
六、继承与静态成员
基类定义了static成员,则整个继承体系中只有一个这样的成员。
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 _Course ; // 科目
};
void TestPerson()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout <<" 人数 :"<< Person ::_count << endl;
Student ::_count = 0;
cout <<" 人数 :"<< Person ::_count << endl;
}
七、菱形继承
1、单继承
一个子类只有一个直接父类称其为单继承
2、多继承
一个子类有两个或两个以上父类时称该继承关系为多继承
3、菱形继承(多继承的一种特殊情况)
存在问题:
菱形继承会出现冗余性和二义性的问题
举例:
解决二义性可以显式定义访问哪个父类的成员
Asisitant a; a.Student::_name="lalalal";
但是冗余问题好像无法解决
- 虚拟继承可以解决上述两个问题
即在Student和Teacher继承Person时用虚拟继承就可以解决问题。
举例:
#include<iostream>
using namespace std;
//菱形继承用虚继承解决冗余和二义性两个问题
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl;//8+8+4=20 菱形继承情况下为20 虚继承下为24
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
system("pause");
return 0;
}
//内存对象模型(对象在内存中是怎么存的)
(1)普通菱形继承的内存对象模型分析
此时sizeof(d)结果为8+8+4=20;
(2)使用虚继承后的内存对象模型分析
这里sizeof(d)=24 因为B和C里面不仅存有各自本来的值,还存有各自指向A的指针,所以最终为24个字节。(当A中成员较多时,可以明显观察到其对于空间的节省)-解决原理:
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A
虚基表是用来找虚基类的
4、总结
(1)一般情况下不要设计菱形继承的关系
(2)多继承可以看作c++的缺陷之一
(3)继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
(4)对象组合是类继承之外的另一种复用选择,组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。