什么是继承?
继承是指一种代码可以被复用的机制,在一个类的基础上进行扩展,产生的新类叫做派生类,被继承的类叫基类。(也可称为子类和父类)
继承的写法:
class B : 继承方式 A (类B以public/private/protected的方式继承类A)
当需要继承多个类时:
class C : 继承方式 A B (类C以public/private/protected的方式继承类A、B)
注意:谁在前就先继承谁,如上则是先继承A再继承B
继承究竟做了什么?
继承继承,顾名思义就是继承了父类的成员(准确的说是会继承除去析构、构造以外所有的成员)再加上子类的扩展形成新类。
如下图:(请先忽略构造函数部分)子类student继承了父类people的_name和_age,所以这个子类中也有_name和_age成员变量。
继承的特性
继承方式
(父类XX成员以XX继承后,在子类是XX成员)
类成员/继承方式 | public | protected | private |
---|---|---|---|
父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
父类的private成员 | 在子类中不可见 | 在子类中不可见 | 在子类中不可见 |
总结:在继承方式和原本的成员权限中选权限更小的,如在父类中为public以protected继承则继承后的权限为protected(权限大小:public<protected<private)
虽然继承方式有三种,但常用的却只有public。
切片
在继承中,往往可以进行用子类对象直接调用父类函数的情况。像前者这种情况往往发生在相似类型身上,因为相似所以可以转换。而父类有的,子类都有,所以程序执行时就会把子类中与父类相同的部分拿出来去调用函数,这种情况就叫切片。
例如:将Student的对象s拿去给People的对象p去调用拷贝构造就是使用了切片的特性
子类构造与析构
虽说父类有的子类都有,理论上说构造自己搞定进行,但规定上在构造子类对象时要先调用父类构造函数,再构造子类部分。(继承是一种复用代码的机制,这里也是复用的父类构造,减少了工作量)
这就是为什么Student的构造函数中的初始化列表要调用 People(string name,int age) 这个构造(这个过程同样会发生切片)
构造要手动调用,那析构也要吗?
手动调用时结果父类部分析构了两次。实际上在子类析构结束后会自动调用父类析构去释放子类中与父类一致的部分,不需要去手动调用。
隐藏(重定义)
当父类与子类出现同名的成员变量或成员函数时,子类成员将屏蔽对父类成员的直接访问,这时通过子类对象去访问这个同名成员时优先访问子类的。
class People
{
public:
void printf()
{
cout << _name << " " << _age << endl;
}
People(string name, int age)
:_name(name),
_age(age)
{}
People(People& p)
:_name(p._name),
_age(p._age)
{}
~People()
{
cout << "~People()" << endl;
}
string _name;
int _age;
};
class Student : public People
{
public:
void printf()
{
cout << _name << " " << _age << " "<< _Sno<<endl;
}
Student(string name, int age, int sno)
:People(name, age),
_Sno(sno)
{}
~Student()
{
cout << "~Student()" << endl;
}
//学号
int _Sno;
};
int main()
{
Student s("swi",20,34);
s.printf();
return 0;
}
之所以会出现隐藏这种特性与他的搜索逻辑有关,当我们通过子类对象访问成员时,会先去子类中寻找这个成员,如果没找到再去父类中寻找。
以上所讲都是继承的通用的特性,样例均为单继承
多继承
多继承顾名思义就是继承多个类。
class A
{
public:
void printf()
{
cout << _a << endl;
}
int _a = 1;
};
class B
{
public:
void printf()
{
cout << _b << endl;
}
int _b = 2;
};
//C同时继承了类A和类B(先继承的A,后继承的B)
class C:public A,B
{
public:
void printf()
{
cout << _c << endl;
}
int _c = 3;
};
int main()
{
C* c = new C;
return 0;
}
查看内存后我们发现,缺省值为1的_a在缺省值为2的_b 的前面,class C:public A,B的顺序能够决定谁先被继承。
A,B反过来后:
多继承的问题
假设他们的成员变量分别为_a,_b,_c,_d,那么他们在内存中的存储会如下图:
此时就会出现两个问题:
- 二义性:当我访问_a时,到底访问哪一个_a。
- 数据冗余:出现重复数据,占用不必要空间
解决方法
在继承方式前加上关键字virtual构成虚继承(注意:想要解决菱形继承的问题,放virtual关键字的类是出现重复部分的若干个子类,被virtual修饰的子类的父类称作虚基类)
如下图的菱形问题(菱形问题是这类问题的总称,并不一定是菱形):
不处理时:
使用virual处理后:
虚基类的成员将会在下方找到一片公共空间去存储,这样一来就解决了二义性和数据冗余。
但是原来两条红线存储的又是什么?
将这四行数字重新组合排序可以得到两个地址,去查找这个地址,发现他分别存储了一个十六进制的28,一个十六进制的18。实际上这个地址是虚基表的地址,而虚基表存储的是地址偏移量,该类的部分的起始地址加上偏移量就能找到虚基类的成员变量_a。
多态
对不同的对象会有不同的实现方法,即为多种形态。
多态的条件:
- 虚函数的重写(父子类虚函数需要三同,三同指函数名、参数、返回值)
- 父类的指针或引用去调用
但是有三种例外:
- 协变(基类与派生类的虚函数返回值不同)
- 析构函数的重写(看似不符合函数名相同的条件,实际上编译器对其进行了特殊处理,编译后析构函数的名字统一处理成destructor)
- 派生类虚函数重写可以不加virtual(但建议写上)
虚函数
virtual修饰的函数叫做虚函数,虚函数只能是类中非静态的成员函数。
虚函数的重写
子类和父类中的虚函数拥有相同的名字,返回值,参数列表,那么称子类中的虚函数重写了父类的虚函数,或者叫做覆盖。(虚函数重写,重写的是函数体)
//成人
class People
{
public:
virtual void fun()
{
cout << "全票" << endl;
}
};
//儿童
class Child : public People
{
public:
virtual void fun()
{
cout <<"半票" << endl;
}
};
void buyTicket(People& p)
{
p.fun();
}
int main()
{
People p;
Child c;
buyTicket(p);
buyTicket(c);
return 0;
}
多态的原理
为什么会访问到不同的虚函数?
使用父类的指针或引用去调用(用子类来调用就会产生切片),根据其指针或引用可以找到其对应的虚函数表,进而找到虚函数地址然后去调用不同的虚函数。