目录
0 引言
1. 继承的概念
1.1 继承的本质
1.2 继承的作用
2. 继承的定义
2.1 继承的格式
2.2 继承的权限
2.3 默认继承
2.5 继承权限的使用
3. 继承的作用域
3.1 隐藏
4. 基类与派生类对象的赋值转换
4.1 切片
5. 派生类的默认成员函数
5.1 隐式调用
5.2 显示调用
0 引言
从前面我们知道,继承是面向对象的三大特性之一(封装 继承 多态)。今天我们主要一起学习什么是继承。即如何在父类的基础之上去构建更加丰富的子类。在此举一个不完全恰当的例子,例如柑橘类水果的不同品种,实际上继承了其父类品种的某些特性。
1. 继承的概念
那什么是继承呢?是继承遗产还是某些东西呢?答案都不是。
实际上,官方说 继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有基类(父类)特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类(子类)。
因此:被继承的对象:父类/基类
继承一方:子类/派生类
1.1 继承的本质
继承的本质实际上就是为了复用代码。
例如:现在需要完成一个学校教务系统代码的编写,单从角色划分上来说,可以简单分为:教职工和学生 这两大类,但如果继续划分的话,还可以分出:校领导、各级院长、辅导员、后勤人员、大一/大二/大三/大四学生等,假设为每个角色都设计一个结构,那么这个工程量也未免太大了,且存在冗余。
所以,为了提高开发效率,我们就可以利用继承的概念。可以从各种角色中选出共同点,组成基类,比如每个人都有姓名、年龄、性别、联系方式等基本信息
而 教职工与学生的区别就在于 管理与被管理,因此可以在基类的基础上加一些特殊信息如教职工号表示教职工,加上学号表示学生,其他细分角色设计也是如此,产生多种子类。
因此,我们通过继承的方式,复用基类的代码,进而划分出各种子类。
1.2 继承的作用
子类基础父类后,可以享有父类中的所有的 公开 / 保护 属性,也就是说,除了 私有 内容外,父类有的,子类全都有 。
举一个例子, 在父类 - 房子 的基础上,派生出 小平层和别墅 这两个子类。
//父类 - 房子
class house
{
public:
house(int area = int())
:_area(area)
{}
int getarea()
{
return _area;
}
private:
int _area;
};
//子类 - 平层
class Flatbed : public house
{
public:
Flatbed()
:house(90)
{
cout << "小平层,面积是:" << getarea() << "平方米" << endl;
}
};
//子类 - 别墅
class villa : public house
{
public:
villa()
:house(500)
{
cout << "别墅,面积是:" << getarea() << "平方米" << endl;
}
};
int main()
{
Flatbed f;
villa v;
return 0;
}
可以看到,两个子类都能具备父类中的 公有和保护 属性,并且互不干扰。
2. 继承的定义
2.1 继承的格式
继承的格式:子类 :继承方式 父类,比如 class A :public B 就表示 A 继承 B, 且为公有继承。
2.2 继承的权限
继承的权限可分为 公有继承(public)、保护继承(protected)、私有继承(private)。
权限大小:public > protected > private。
public | 公开的,任何人都可以访问 |
protected | 保护的,只有当前类和子类可以访问 |
private | 私有的,只允许当前类进行访问 |
保护 protected 比较特殊,只有在 继承 中才能体现它的价值,否则与 私有 作用一样。
因此存在多种 父类成员权限 与 继承权限 的搭配方案。(所谓的外部其实就是子类对象)
public 继承 | protected 继承 | private 继承 | |
父类:public 成员 | 外部可见,子类中可见 | 外部不可见,子类中可见 | 外部不可见,子类中可见 |
父类:protected 成员 | 外部不可见,子类中可见 | 外部不可见,子类中可见 | 外部不可见,子类中可见 |
父类:private 成员 | 都不可见 | 都不可见 | 都不可见 |
总结:无论是哪种继承方式,父类中的 private 成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员,依据 min(父类成员权限,子类继承权限),只有最终权限为 public 时,外部才能访问。
下面我们来看示例:
2.3 默认继承
如果不注明继承权限, class 默认为 private , struct 默认 public ,因此最好是注明继承权限。
那么如何访问父类的私有成员?我们只需在父类中设计相应的函数,间接访问私有成员。
2.5 继承权限的使用
那么我们如何优雅的使用好权限呢?我们只需要记住以下的点:
对于只想自己类中查看的成员,设为 private ,对于想共享给子类使用的成员,设为 protected, 其他成员都可以设为 public 。
例如在 小明 家中,房子面积可以设置为公开,家庭存款只让家庭成员知道,而隐私则可以设置为私有。
class Home
{
public:
int area = 120; //房屋面积
};
class Father : public Home
{
protected:
int money = 100000; //存款
private:
int privateMoney = 100; //私房钱
};
class xiaoming : public Father
{
public:
xiaoming()
{
cout << "我是小明" << endl;
cout << "我知道我家房子有 " << area << " 平方米" << endl;
cout << "我也知道我家存款有 " << money << endl;
cout << "但我不知道我爸爸的私房钱有多少" << endl;
}
};
class zhangsan
{
public:
zhangsan()
{
cout << "我是张三" << endl;
cout << "我只知道张三家房子有 " << Home().area << " 平方米" << endl;
cout << "其他情况不知道" << endl;
}
};
int main()
{
xiaoming x;
cout << "================" << endl;
zhangsan z;
return 0;
}
因此我们可以看到,权限可以很好的保护成员。
那么我们如何设计一个不能被继承的类?
我们只需要将父类的构造和析构函数设为私有,这样子类就无法创建父类对象,同时也就无法继承
3. 继承的作用域
子类虽然继承自父类,但两者的作用域是不相同的,假设出现同名函数时,默认会将父类的同名函数隐藏调,进而执行子类的同名函数。
隐藏 也叫 重定义,与它类似的概念还有:重写(覆盖)、重载。
3.1 隐藏
子类中出现父类的 同名 方法或者成员。
//父类
class Base
{
public:
void func() { cout << "Base val: " << val << endl; }
protected:
int val = 111;
};
//子类
class Derived : public Base
{
public:
int func()
{
cout << "Derived val: " << val << endl;
return 0;
}
private:
int val = 222;
};
int main()
{
Derived d;
d.func();
return 0;
}
我们发现,父子类中的方法和成员均被隐藏,执行的是 子类方法,输出的是子类成员。
现在我们将子类中函数修改为 funA
int funA()
{
cout << "Derived val: " << val << endl;
return 0;
}
发现此时 隐藏 消失,并且结果的是 父类方法 + 父类成员。
接着,我们修改子类标识符 val
int num = 222;
此时 隐藏 也消失,执行结果 子类方法 + 父类成员。
因此, 当子类中的方法出现 隐藏 行为时,优先执行 子类 中的方法;当子类中的成员出现 隐藏 行为时,优先选择当前作用域中的成员(局部优先)。
那么,我们如何显示的调用父类的方法或者成员?
我们利用域作用限定符 : : 即可。
总结:
- 只要是命名相同,都构成 隐藏 ,与 返回值、参数 无关
- 隐藏会干扰调用者的意图,因此在继承中,要尽量避免同名函数的出现
4. 基类与派生类对象的赋值转换
首先提出的是,在继承中,允许将子类对象直接赋值给父类,但不允许父类对象赋值给子类。
并且这种 赋值 是非常自然的,编译器直接处理,不需要调用 赋值重载 等函数。
//父类
class Base
{
protected:
int val = 111;
};
//子类
class Derived : public Base
{
private:
int num = 222;
};
int main()
{
Base b;
Derived d;
b = d;
//d = b; //非法,只允许 子->父
return 0;
}
子类对象 在 赋值 给 父类对象 时,触发 切片 机制,丝滑的完成 赋值。
4.1 切片
下图展示了柠檬切片:
在基类与派生类的赋值转换中,切片将父类对象看作一个结构体,子类对象 看作 结构体Plu 版,将子类对象中多余的部分去除,留下父类对象可接收的成员,最后再将对象的指向进行改变就完成了切片。
因为整个切片过程是由编译器自己完成的,所以效率很高,并且不会发生借助临时对象构造再赋值的情况。由于父类无法满足子类的需求,切片只在 子类->父类 时发生。
5. 派生类的默认成员函数
派生类(子类)也是 类,同样会生成 六个默认成员函数(用户未定义的情况下)
不同于单一的 类,子类 是在 父类 的基础之上创建的,因此它在进行相关操作时,需要为父类进行考虑。
5.1 隐式调用
子类在继承父类后,构建子类对象时 会自动调用父类的 默认构造函数,子类对象销毁前,还会自动调用父类的 析构函数。
class Person
{
public:
Person() { cout << "Person()" << endl; }
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
Student() { cout << "Student()" << endl; }
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Student s;
return 0;
}
此时,自动调用是由编译器完成的,前提是父类存在对应的默认成员函数;如果不存在,会报错
5.2 显示调用
因为存在 隐藏 的现象,当父子类中的函数重名时,子类无法再自动调用父类的默认成员函数,此时会引发 浅拷贝 相关问题。
class Person
{
public:
Person() { cout << "Person()" << endl; }
void operator=(const Person& P) { cout << "Person::operator=()" << endl; }
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
Student() { cout << "Student()" << endl; }
void operator=(const Student&) { cout << "Student::operator=()" << endl; }
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Student s1;
cout << "================" << endl;
Student s2;
s1 = s2;
return 0;
}
此时可用通过 域作用限定符 ::
显式调用父类中的函数。
总的来说,子类中的默认成员函数调用规则可以概况为以下几点:
- 子类的构造函数必须调用父类的构造函数,初始化属于父类的那一部分内容;如果没有默认构造函数,则需要显式调用
- 子类的拷贝构造、赋值重载函数必须要显式调用父类的,否则会造成重复析构问题
- 父类的析构函数在子类对象销毁后,会自动调用,然后销毁父类的那一部分
注意:
- 子类对象初始化前,必须先初始化父类那一部分
- *子类对象销毁后,必须销毁父类那一部分
- 不能显式的调用父类的析构函数(因为这不符合栈区的规则),父子类析构函数为同名函数destruct ,构成隐藏,如果想要满足我们的析构需求,就需要将其变为虚函数,构成重写。