继承的概念以及定义
- 继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。继承呈现了面向程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
// 基类:定义人普遍特征的类
class Person{
public:
void Print(){
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";
int _age = 18;
};
//派生类
class Student : public Person{
protected:
int _stuid;
};
解释:继承后父类的Person的成员(成员函数+成员变量)都会变成子类Student的一部分。体现了Student复用了Person的成员。
- 继承定义
- 定义的格式
class Student : public Person
(派生类) (继承方式) (基类)
{};
- 继承关系和访问限定符
继承方式分为三种:public继承, protected继承, private继承
访问限定符分为三种:public访问, protected访问,private访问
- 继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
-
基类的private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指积累的私有成员还是被继承到派生类对象中,
但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。 -
基类private成员在派生类中不能被访问,如果类成员不想在类外面直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因为继承才出现的。
-
实际上面的表格我们总结一下就会发祥,基类的除了private其他成员在子类的访问方式 == min(成员在基类的访问限定符, 继承方式), 可以看出的权限范围大小依次为:public > protected > private
-
使用关键字
class
时默认的继承方式为private, 使用struct
时默认的继承方式是 public, 不过最好显示的写出继承方式。 -
在实际的运用中一般使用都是 public继承。
-
基类和派生类对象赋值转换
首先来看一段代码:
#include <iostream>
#include <string>
using namespace std;
// 基类:定义人普遍特征的类
class Person{
public:
void Print(){
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";
int _age = 18;
};
// Student 为派生类
class Student : public Person{
protected:
int _stuid;
};
int main(){
Person s;
Student s1;
s = s1; // 正确,可以将派生类赋值给基类
s1 = s; //错误,不可以将基类赋值给派生类
// 一下都是正确的
Person pobj = s1;
Person *pp = &s1;
Person &rp = s1;
// 基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &s1;
Student *ps1 = (Student*)pp;
return 0;
}
- 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法叫切片或者分割。就是说把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象
- 不存在从基类向派生类的隐式类型转换
- 从派生类向基类的类型转换只对指针或引用类型有效
- 和任何其他成员一样,派生类向基类的类型转换也可能由于访问受限而变得不可行。
- 继承中的作用域
来看首先一段代码:
#include <iostream>
#include <string>
using namespace std;
// 基类:定义人普遍特征的类
class Person{
public:
// 与派生类同名的成员函数
void Print(){
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";
int _age = 18; // 与派生类同名的成员变量
};
// Student 为派生类
class Student : public Person{
public:
// 与基类同名的成员函数
void Print(){
cout << "name:" << _name << endl;
cout << "_age:" << _age << endl;
}
protected:
int _age = 22; //具有和基类相同名字的成员变量
};
int main(){
Student s;
s.Print();
return 0;
}
从结果来看,我们发现无论是派生类中的成员变量还是成员函数都起作用了,反而是基类中的同名的变量和函数并没有作用。我们叫这种现象为隐藏。
// 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;
}
};
- 派生类的默认成员函数
默认的意思就是不写,编译器也会自动生成的意思。那么在派生类中成员函数是如何生成的呢?
(1)派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
(2)派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
(3)派生类的 operator= 必须调用积累的 operator= 完成基类的复制
(4)派生类的析构函数在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类的对象先清理派生类成员后再清理基类成员的顺序。
(5)派生类对象初始化先调用基类构造再调用派生类构造
(6)派生类对象析构清理先调用派生类析构再调用基类的析构。