C++中的继承
1 什么是继承?
继承:继承机制是面向对象程序设计使代码可以复用的最重要的手段。它允许程序员在保持原有类特性的基础上进行扩展、增加功能,这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
1.1为什么需要继承?
代码复用和实现多态。假设在写代码的过程中我们定义看一个Person类,其中这个类包含了一些人的行为,我们定义为共有的例如:Eat(),Sleep()等这些函数,定义_name,_gender这两个为私有的成员变量。当我们需要写Student类的时候,没有继承这个概念的话,可能会重新写一份Student类,假如还有Teacher类呢?还需要再写一份,这听起来很麻烦。但是Person类中包含了Eat(),Sleep()等这些函数,_name,_gender这两个成员变量,因此用继承机制的话可以避免重复劳动,Student类可以继承Person类,然后在自己继承后的函数中增加自己新增加的成员函数或者是成员变量,形成一份自己独特的类,同理Teacher类也可以,这就是继承。多态在后续的博客中讲解,目前知道多态这个概念即可。
//基类
class Person
{
public:
void Eat()
{
cout << "biajibiaji" << endl;
}
void Sleep()
{
cout << "huluhulu" << endl;
}
private:
string _name;
string _gender;
int _age;
};
// 派生类/子类
class Student : public Person
{
public:
void Study()
{
cout << "念书" << endl;
}
private:
int _stuid;
};
int main()
{
Student s;
s.Eat();
s.Sleep();
cout << sizeof(Student) << endl;
return 0;
}
继承后父类的Person的包括成员函数和成员变量都会变成子类的一部分,也称之为student类复用了person类的成员函数和成员变量。
1.2 继承的格式
// 派生类/子类
class Student : public Person
{
public:
void Study()
{
cout << "念书" << endl;
}
private:
int _stuid;
};
上面看到的是子类也就是派生类,其中Student称为派生类,public是继承的权限,Person是基类名称,三部分缺一不可。
继承结果示意图:
在子类继承基类中继承方式分为3种:public继承、protected继承、private继承。
基类或者子类的访问限定符也分为3种:public访问、protected访问、private访问。
以下是子类继承基类时,基类不同的访问限定符、子类不同的访问限定符、基类继承权限不同时的汇总。
1.3共有继承
采用公用继承方式时,基类的公用成员和保护成员在派生类中仍然保持其共有成员和保护成员的属性,而基类的私有成员在派生类中比呢没有成为派生类的私有成员,它仍然是基类的私有成员,只有基类的成员函数可以引用它,而不能被派生类的成员函数引用,因此就成为派生类中不可访问的成员。
基类的成员在派生类中的访问属性:
在基类的访问属性 | 继承方式 | 在派生类的访问属性 |
---|---|---|
private | public | 不可访问 |
public | public | public |
protected | public | protected |
1.4 私有继承
私有基类的共有成员和保护成员在派生类中的访问属性相当于派生类的私有成员,即派生类的成员函数能访问,而在派生类外不能访问他们,私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用他们。一个基类成员在基类中的访问属性和在派生类中的访问属性可能是不同的。私有基类的成员可以被基类的成员函数访问,但不能被派生类的成员函数访问。私有基类的成员在私有派生类中的访问属性如下表:
基类的成员在派生类中的访问属性:
在基类的访问属性 | 继承方式 | 在派生类的访问属性 |
---|---|---|
private | private | 不可访问 |
public | private | private |
protected | private | private |
1.5 保护继承
保护基类的共有成员和保护成员在派生类中都成了保护成员,其私有成员仍为基类私有,也就是把基类原有的共有成员也保护了起来,不让类外任意访问。
基类的成员在派生类中的访问属性:
在基类的访问属性 | 继承方式 | 在派生类的访问属性 |
---|---|---|
private | protected | 不可访问 |
public | protected | protected |
protected | protected | protected |
2 基类和派生类对象赋值转换
2.1 赋值规则
- 派生类对象可以赋值给基类的对象/基类的指针/基类的引用,反之不行。
- 可以使用基类指针或者引用指向子类的对象,反之则不行。除非进行强制类型转换,但是必须是基类的指针或者是应用指向派生类的对象时才是安全的。
2.2 对象模型
派生类给基类赋值:
基类给派生类赋值:
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
void Test()
{
Student sobj;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_No = 10;
}
3 派生类的构造和析构函数
3.1 派生类继承模板
class Base
{
public:
Base(int b)
: _b(b)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
protected:
int _b;
};
class Derived : public Base
{
public:
Derived(int b, int d)
: Base(b) // 初始化基类部分继承下来的成员
, _d(d)
{
cout << "Derived()" << endl;
}
~Derived()
{
cout << "~Derived()" << endl;
// call ~Base();
}
protected:
int _d;
};
说明:
- 创建哪个类的对象,编译器就会调用哪个类的构造函数;洗头哪个类的对象,编译器就会调用哪个类的析构函数。
- 编译器在子类析构完成后,会调用call,调用基类析构函数();
- 如果基类的构造函数是无参的默认构造函数或者是全缺省的构造函数,用户在子类构造函数初始化列表位置是否显示调用都可以,因为如果用户没有显示调用,则编译器会自己增加。
- 如果基类具有带有参数的非全缺省的构造函数,用户必须要在子类构造函数初始化列表的位置显示调用基类的构造函数。
- 如果基类的构造函数是默认的构造函数(无参/全缺省),则子类构造函数是否定义都可以,则需要定义,如果不需要刻意不用定义,因为编译器会给子类生成默认的构造函数,如果基类具有带有参数的非缺省的构造函数,则子类构造函数必须显示提供。
4 继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
B中的fun和A中的fun不是构成重载,因为不是在同一作用域。
B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
5 派生类的默认成员函数
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。