继承的概念及其定义
概念
:C++的继承机制是面向对象程序设计
使代码可以复用
的最重要的手段,允许用户在保持原类的特性的基础上进行拓展,这样新产生的类叫做
派生类(父类)
,原类叫做
基类(子类)
。
定义:
class 派生类名 : 继承方式 基类名
举个例子:
class Person
{
protected:
std::string _name = "张三";
int _age = 18;
};
class Student : public Person
{
public:
int studentId;
};
class Teacher : public Person
{
public:
int teacherId;
};
int main()
{
Student s;
s.studentId = 2109122229;
Teacher t;
t.teacherId = 1422124;
return 0;
}
Person对于Student、Teacher就是基类,而Student、Teacher是Person的派生类。
派生类对象中天然的就有基类对象。
继承方式和访问限定符一样有三种:public、protected、private
继承方式和访问限定符共同决定了类成员的被访问权限!
public继承 | protected继承 | private继承 | |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
总结:
- 基类private成员不论以什么继承方式,在派生类中都是不可见的。(不可见的意思是:基类private成员仍会被继承到派生类中,但语法限制无论在类内外都无法访问该基类private成员);
- 基类private成员被继承下去后是无法在派生类内被访问的,但如果想能够在派生类外不被访问并且在派生类内能被访问,这时就用protected关键字修饰该成员(protected也就是基于该原因而出现的);
- 当派生类的关键字是class时默认为private继承方式,当派生类的关键字是struct时默认为public继承方式;
- 在实际应用中,public继承方式最为常见,protected和private继承方式非常少见。
派生类对象赋值给基类对象
派生类对象的大小总是比基类对象要大于等于的。
所以,
-
派生类对象
能够赋值给
基类对象/基类指针/基类引用
,方法就是将派生类对象中基类部分切下来赋值。 -
基类对象不能够赋值给派生类对象
-
基类的指针/引用可以通过强制类型转换赋值给派生类的指针/引用,但必须基类的指针是指向派生类对象时才是安全的。如果基类是多态类型,可以使用RTTI(Run-Time Type Information)的dynamicast进行识别后进行安全转换。
作用域与隐藏
派生类一般会在基类的基础上进行拓展,那么就可能会出现拓展的变量/函数名与基类一样,但这是合法的。
class Person
{
public:
void Func()
{
std::cout << "Person::Func()" << std::endl;
}
public:
std::string _name = "person";
};
class Student : public Person
{
public:
void Func()
{
std::cout << "Student::Func()" << std::endl;
}
public:
std::string _name = "student";
};
int main()
{
Student s;
s.Func();
std::cout << s._name << std::endl;
s.Person::Func();
std::cout << s.Person::_name << std::endl;
return 0;
}
-
派生类中有两个Func和两个_name,但不会导致函数重载等问题,因为派生类中拓展内容和基类内容是两个
不同的作用域
; - 访问派生类内容时,默认先访问拓展内容,若拓展内无该内容则去基类内容中访问(可添加基类作用域显式访问);
-
派生类和基类重名这一现象称为
隐藏
; - 如果是成员函数的隐藏,则只需要函数名相同即可隐藏。
派生类的默认成员函数
-
派生类中的基类成员会自动先调用基类的默认构造,若基类无默认构造,则必须在派生类的构造函数初始化列表显式调用构造函数;
class A { public: A(int a) :_a(a) {} public: int _a; }; class B : public A { public: B(int b) :A(10)//类A无默认构造,在B的构造初始化列表处显式调用类A的构造函数,像一个匿名对象一样 , _b(b) {} public: int _b; };
-
派生类的拷贝构造函数中必须调用基类的拷贝构造函数来完成基类成员的拷贝构造;
class A : public string { A(const A& a) :string(a)//这里用派生类对象a切片拷贝构造,使用方法仍然像一个匿名对象 :_a(a._a) {} public: int _a; };
-
派生类的赋值运算符重载中必须调用基类的赋值运算符重载来完成基类成员的赋值;
class A : public string { A& operator=(const A& a) { if(this != &a) { string::operator=(a); _a = a._a; } return *this; } public: int _a; };
-
派生类的析构函数完成之后会自动调用基类的析构函数;
-
构造基类成员先进行,拓展内容后进行;
-
析构拓展内容先进行,基类成员后进行;
-
基类和派生类的析构函数默认构成隐藏。
菱形继承&&菱形虚拟继承
C++也是支持多继承的,而多继承的出现就必然会出现菱形继承这种特殊情况。
菱形继承会导致两个问题:
- 代码冗余(上述例子中,D类中会有两份A类成员);
- 二义性(上述例子中,当访问D中的A成员时,会存在歧义,除非主动添加B/C类域);
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;
};
解决上述问题的方法就是菱形虚拟继承,在中间环节的继承中添加上virtual关键字
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;
};
菱形虚拟继承的原理
#include <iostream>
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._b = 2;
d.C::_a = 3;
d._c = 4;
d._d = 5;
return 0;
}
这是一般菱形继承的内存视角
#include <iostream>
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 = 1;
d._b = 2;
d._c = 3;
d._d = 4;
return 0;
}
这是菱形虚拟继承的内存视角
这里是通过了两个指针分别指向的一张表。这两个指针称为虚基表指针,这两个表称为虚基表,虚基表中存的共享的_d的偏移量地址。
继承和组合
- public继承是一种is-a的关系,每个派生类对象都是一个基类对象;
- 组合是一种has-a的关系,假设B组合了A,那么每个B对象都有一个A对象。
当某种场合下,使用继承和组合都合适,那么优先使用组合。
组合是另一种代码复用的手段,组合的耦合度相比继承较低,该复用风格称为黑箱复用(因为对象的内部细节不可见),反正继承的复用风格称为白箱复用(对象内部细节相对可见)。