1.初见继承
1.1引言
面向对象程序设计有4个主要特点:抽象、封装、继承和多态性。学会了类和对像就基本了解数据抽象与封装。继承性是面向对象程序设计最重要的特征,如果没有掌握继承性,就等于没有掌握类和对象的精华。
1.2继承相关概念
- 简单地说:A is a B;这就是继承关系。比如 :男性和女性相对于人类而言就是一种继承关系。
- 继承具有传递性,但不具有对称性。 比如:小男孩是男人也是人类;但小男孩不是女人
- B类继承A类(从类A派生类B),A成为基类(父类),B称为派生类(子类)
- 单继承:只从一个父类继承就是单继承,比如:小男孩类继承男人类就是单继承
- 多继承:从多个父类继承就是多继承,比如:”阴阳人儿“同时继承男人类和女人类就是多继承(只是方便理解而举例,没有任何伦理暗示)
1.3派生类的代码定义
class 派生类名:基类名表
{
数据成员和成员函数声明;
};
基类名表构成:访问控制 基类1名,访问控制 基类2名,....
访问控制表示派生类对基类的继承方式:
- public 公有继承
- private 私有继承
- protected 保护继承
class Parent
{
private:
int a;
int b;
protected:
public:
void print() {
cout << b << a << endl;
}
};
class Child :protected Parent
{
private:
protected:
public:
};
1.4继承有什么用?
知道了继承怎么写后我们看一下继承具体有什么作用:
-
子类拥有父类的所有成员变量和成员函数
-
子类可以拥有父类没有的方法和属性
-
子类就是一种特殊的父类
-
子类对象可以当作父类对象使用
2.派生类的访问控制
2.1三种继承方式
上面我们知道了继承有三种访问控制类别,那么为什么要设计这么多种访问级别呢?只有public和private难道不够嘛?
- public修饰的成员变量和方法,在类的内部、类的外部都能使用
- protected修饰的成员变量和方法,在类的内部、继承的子类中可使用
- private修饰的成员变量和方法,只能在类的内部使用
由此可以看出protected修饰符就是为了继承这个功能服务而诞生的,三种继承方式的具体影响如下:
- public继承:父类成员在子类中保持原有访问级别
- private继承:父类成员在子类中变为private成员
- protected继承:父类中public成员会变成protected、父类中protected成员仍然为protected、父类中private成员仍然为private
private成员在子类中依然存在,但是却无法访问到。不论种方式继承基类,派生类都不能直接使用基类的私有成员
class Parent
{
private:
int c;
protected:
int b;
public:
int a;
public:
void printP() {
cout << b << a << endl;
}
};
class Child1 :public Parent
{
private:
protected:
public:
void func() {
a = 0;//在当前类中是 公有
b = 0;//在当前类中是 保护
c = 0;//报错,无法访问父类私有成员
}
};
class Child2 :protected Parent
{
private:
protected:
public:
void func() {
a = 0;//在当前类中是 保护
b = 0;//在当前类中是 私有
c = 0;//报错,无法访问父类私有成员
}
};
class Child3 :private Parent
{
private:
protected:
public:
void func() {
a = 0;//在当前类中是 私有
b = 0;//在当前类中是 私有
c = 0;//报错,无法访问父类私有成员
}
};
2.2总结1:如何判断一个变量能否被访问
- 看调用语句,这句话写在子类的内部还是外部
- 看子类如何从父类继承
- 看它父类中的访问级别
2.3总结2:怎么合适的设计访问级别
- 需要被外界访问的成员直接设置为public
- 只能在当前类中访问的成员设置为private
- 只能在当前类和子类中访问的成员设置为protected
附加:
- protected关键字修饰的成员变量和成员函数,是为了在继承中使用!只要看到protected就要想到继承!
- 实际开发中,一般情况下都用Public继承
3.继承中的构造和析构
3.1类型兼容性原则(赋值兼容性原则)
类型兼容性原则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题公有派生类都可以解决。类型兼容性原则中所指的替代包括以下情况:
- 子类对象可以当作父类使用
- 子类对象可以直接赋值给父类对象
- 子类对象可以直接初始化父类对象
- 父类指针可以直接指向子类对象
- 父类引用可以直接引用子类对象
例:
class People
{
public:
void printP()
{
cout << "我是人类" << endl;
}
People(){}
People(const People &other) {
cout << "people的拷贝构造函数" << endl;
}
private:
int a;
};
class Man :public People
{
public:
void printM() {
cout << "我是男人" << endl;
}
private:
int b;
protected:
};
void test(People *ptr) {
ptr->printP();
}
void test(People& ptr) {
ptr.printP();
}
int main()
{
People p1;
p1.printP();
Man m1;
m1.printM();
//-1-基类指针指向子类对象
People *p = NULL;
p = &m1;
p->printP();//但只能访问继承到的父类的成员
//-2-父类指针做函数参数
test(&m1);
//-3-父类引用做函数参数
test(m1);
//-4-直接让子类对象去初始化父类对象 会调用拷贝构造函数
People p3 = m1;
}
3.2继承中的对象模型(内存模型)
在C++中,类的成员变量和成员函数在内存中是分开存储的(函数通过this指针来找到对应的类)
子类是由父类成员叠加子类新成员得到的。比如B继承A,C继承B时,各自的内存模型如下:
那么父类与子类的构造函数有什么关系呢?
- 在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初始化
- 在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清理
3.3继承中的构造析构调用顺序
总结1:子类与父类间的构造析构顺序
- 子类对象在创建时会首先调用父类的构造函数
- 父类构造函数执行结束后,执行子类的构造函数
- 当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
- 析构函数调用的先后顺序与构造函数相反
总结2:继承与聚合(复合)混合使用时的调用顺序
-
先构造父类,再构造成员变量、最后构造自己
-
先析构自己,在析构成员变量、最后析构父类
4.单继承中的其他重要知识
4.1继承中的同名成员变量
当子类成员变量与父类成员变量同名时:
- 子类依然从父类继承同名成员。
- 在子类中通过作用域分辨符::进行同名成员区分(子类中默认是子类的同名对象,所以使用父类的同名对象时要显示调用)
- 同名成员存储在内存中的不同位置。
4.2继承中的static
- static关键字遵守派生类的访问控制原则
- 静态变成记得要在类外初始化,从编译器的角度看,这个初始化语句不是简单的赋值,而是告诉编译器记得分配内存有子类会用!!
5.多继承(由于二义性在实际开发中常摒弃)
5.1概念
一个类有多个直接基类的继承关系称为多继承
class Base1
{
public:
Base1(int b1) {
this->b1 = b1;
}
private:
int b1;
};
class Base2
{
public:
Base2(int b2) {
this->b2 = b2;
}
private:
int b2;
};
class B :private Base1, private Base2
{
public:
B(int a,int b,int c):Base1(a),Base2(b)
{
this->c = c;
}
private:
int c;
};
5.2构造和访问顺序
- 多个基类的派生类构造函数可以用初始式调用基类构造函数初始化数据成员
- 执行顺序与单继承构造函数情况类似。多个直接基类构造函数执行顺序取决于定义派生类时指定的各个继承基类的顺序。
- 一个派生类对象拥有多个直接或间接基类的成员。不同名成员访问不会出现二义性。如果不同的基类有同名成员,派生类对象访问时应该加以识别。
5.3虚继承