目录
继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
例如学校管理系统,学生、老师具有相同的属性,例如姓名,性别,年龄等属性。各自也具有不同的属性,例如学号、工号。
class Person {
public:
void Print()
{
cout << "name: " << _name << endl;
cout << "_age: " << _age << endl;
}
protected:
//共同属性,基类
string _name = "Fancy";
int _age = 18;
};
class Student :public Person {//继承
protected:
//独特的属性,派生类
string _stuid = "B06040217";
};
class Teacher :public Person {
protected:
string _jobid = "";
};
int main()
{
Student s;
return 0;
}
继承方式
三种继承方式:
public继承、protected继承、private继承。
总结一下:
protected限定符是为了继承才产生的,当不在继承环境下使用时,效果和private一样,都是防止外部访问。
访问权限public > protected > private,基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)(private成员除外)
基类的private成员,不管是哪种继承方式,在派生类中都不可见,即派生类无法访问这些成员。
基类与派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
派生类赋值给基类时,只赋值基类中拥有的成员。
注意:
基类对象不能赋值给派生类对象。
但是,基类的指针可以通过强制类型转换赋值给派生类的指针。
继承中的作用域
class Person {
public:
void Print()
{
cout << "name: " << _name << endl;
cout << "_age: " << _age << endl;
}
protected:
string _name = "Fancy";
int _age = 18;
};
class Student :public Person {
public:
void test()
{
_age = 10;
}
protected:
string _stuid = "B06040217";
};
int main()
{
Student s;
s.Print();
s.test();
s.Print();
return 0;
}
输出结果:
因为是继承过来的,相当于是自己的成员变量,所以可以修改。
如果在派生类中存在与基类同名的成员,则派生类将屏蔽基类同名的成员。这种情况叫做隐藏,也叫作重定义。
也就是说,基类与派生类具有独立的作用域,访问过程就进原则
class Student :public Person {
public:
void test()
{
_age = 10;
}
protected:
int _age;
string _stuid = "B06040217";
};
修改之前的代码之后运行结果如下图所示
此时在修改_age的值就不会发生变化,此时调用基类Print会打印出来的是基类的_age。
派生类的默认成员函数
1、派生类构造函数会调用基类的构造函数来初始化基类的成员
注意:派生类无法自行初始化基类成员
class Person {
public:
Person(const char* name = "Fancy")//构造函数
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)//拷贝构造
:_name(p._name)
{
cout << "Person(const Person& )" << endl;
}
Person& operator=(const Person& p)//operator =
{
cout << "Person& operator=(const Person& )" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()//析构函数
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student :public Person {
public:
protected:
string _stuid = "B06040217";
};
int main()
{
Student s;
return 0;
}
可以看到派生类自动调用基类的构造函数与析构函数
如果尝试手动初始化基类的成员变量,会出错。
只能通过调用基类的构造函数才能初始化
同样的,拷贝构造函数是一样的道理
也需要手动的调用一下基类的拷贝构造函数,基类只截取他自己需要的参数。
operator = 也得显示调用,不然未完全拷贝,很危险。
注意:析构函数与前面不太一样,不需要显示的调用,他会自动的去调用,显示调用会析构多次。
基类的静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。这个很好理解。
菱形继承与虚拟继承
继承又分为,单继承、多继承、菱形继承。
单继承
多继承
菱形继承
菱形继承的问题:
二义性和数据冗余
用上面的图来说,二义性的表现在Student和Teacher中都有Person的成员,当Assistant 对象访问该成员时,不知道是Student的成员还是Person的成员。
对于这种问题,就只能交代清楚是谁中的成员。
例如:
Assistant a;
a.Student::_name = "who";
数据冗余表现在
Assistant 中有两份Person 中的成员,这并不一定是我们想要的。
针对这种问题,我们可以采用虚拟继承的方法
virtual关键字。
如果不采用虚拟继承,Assistant 中有两份Person 中的成员
class A {
public:
int _a;
};
//不采用虚拟继承
class B : public A{
public:
int _b;
};
class C : public A {
public:
int _c;
};
class D : public B, public C {
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
可以看到其中有两份A的信息,且其中的_a值不相同。
如果采用虚继承,Assistant 类只会继承一份Person类的成员信息
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;
};
可以看到所有的_a值都是一样的
虚继承的原理:虚基表
虚基表中存的偏移量,来计算虚基类对象的位置。
注意:多继承是C++语言上的缺陷。
继承与组合
继承:
class A{……};
class B :public A{……};
组合:
class A{……};
class B
{A a;};
继承和组合都完成了类的复用,前者是 is a的逻辑(是一个什么),后者是has a的逻辑(有一个什么)
继承一定程度上破坏了基类的封装,基类一旦改变,对派生类有很大的影响。所以耦合度高。
组合类似于黑箱复用,耦合度低,组合类之间没有强的依赖关系.