继承是什么?
继承是 C++ 面向对象编程中一个强大的特性,他指的是一个类在原有类的基础上进行更详细更具体的定义过程,它允许一个类从另一个类获取成员,包括除了构造函数、拷贝构造函数、析构函数、友元和静态成员以外所有的数据成员和成员函数。
继承的特性
继承具有传递性和不对称性,我们举个例子,交通工具是一个大类,在他的基础上我们可以派生出火车、汽车、飞机、轮船等类,而这些类又可以再派生出其他类,比如汽车可以分为轿车、面包车、卡车等,轿车是交通工具,这就说明了继承的传递性;所有的轿车都是交通工具,但是不是所有的交通工具都是轿车,这说明了继承的不对称性。
继承的作用
通过继承,可以实现代码的复用和扩展,避免重复编写相同或相似的代码。例如,如果有一个基类 Animal ,它包含了一些基本的属性和方法,如name、age和eat()方法。然后我们可以创建一个派生类Dog,它继承自Animal,那么他也拥有了name、age和eat()方法,并且还可以添加自己特有的属性和方法,比如bark() (吠叫)方法。
继承的分类
根据子类继承的父类数量可以将继承分为单继承和多继承:
单继承说的是一个子类只有一个直接父类,多继承是一个子类有多个直接父类。
子类就是新产生的类,也可以叫做派生类,父类就是原有的类,也可以叫做基类。
根据继承时的访问限定可以分为公有继承、私有继承和保护继承:
公有继承时,基类的公有成员在派生类中仍然是公有成员,可以被派生类对象以及派生类之外的代码访问。 基类的保护成员在派生类中仍然是保护成员,只能在派生类及其子类中访问。
保护继承时,基类的公有成员在派生类中变为保护成员。 基类的保护成员在派生类中仍然是保护成员。
私有继承时,基类的公有成员和保护成员在派生类中都变为私有成员,都只能在派生类内部访问。
无论哪种继承方式,基类的私有成员在派生类中都是不可直接访问的。如果想要在派生类内部来访问基类的私有成员,则基类需要提供相应的公共接口,比如公有或保护成员函数,使派生类可以间接操作基类的私有成员。
类型兼容性原则
说到公有继承,就可以联想到类型兼容性原则,就是说 任何需要基类对象的地方都可以用该基类的公有派生类代替。因为一个类的公有派生类继承了除构造析构拷贝构造,静态成员外的所有成员,同时基类成员的权限通过公有继承基本不变,这样就意味着可以用这个类的公有派生类对象来代替基类对象。
他涉及到五种使用场景:
① 用派生类对象给基类对象赋值
② 拿派生类对象初始化基类对象
③ 派生类对象可以拷贝构造基类对象
④ 基类指针可以指向派生类对象,要注意的是,在这种情况下,指针只能访问基类有的成员,而不能访问派生类独有的成员
⑤ 基类对象的引用可以引用派生类对象
调用顺序
在继承中,还涉及到构造函数和析构函数的调用顺序。当创建派生类的对象时,首先会调用基类的构造函数,对基类继承的成员进行内存分配和初始化,然后再调用派生类的构造函数如果是多继承的话,则按照继承时声明的顺序调用基类构造函数;在对象销毁时,析构函数的调用顺序则相反,先调用派生类的析构函数,然后再调用基类的析构函数。
#include <iostream>
using namespace std;
class A1
{
public:
A1() { cout << "A1 constructor" << endl; }
~A1() { cout << "A1 destructor" << endl; }
};
class A2
{
public:
A2() { cout << "A2 constructor" << endl; }
~A2() { cout << "A2 destructor" << endl; }
};
class A3
{
public:
A3() { cout << "A3 constructor" << endl; }
~A3() { cout << "A3 destructor" << endl; }
};
class Demo :public A1, public A3, public A2
{
public:
Demo() { cout << "Demo constructor" << endl; }
~Demo() { cout << "Demo destructor" << endl; }
};
int main()
{
Demo obj;
return 0;
}
如上述代码运行结果如下图:
派生类构造函数的写法
类名::构造函数名(形参列表):基类名1(参数列表),基类名2(参数列表),…基类名n(参数列表),成员1(参数列表),成员2(参数列表),…成员n(参数列表)
{
构造函数函数体;
}
关于继承中的同名问题:
① 成员函数的隐藏
派生类中含有和基类成员函数同名的函数时,在派生类对象调用时,默认只能看见派生类自己的同名函数。
规则:1)有继承关系
2)基类和派生类函数名同名
② 成员函数的重定义
在隐藏规则的前提下,派生类的成员函数的函数原型和基类的函数原型一致。
规则:1)有继承关系
2)基类和派生类函数名同名,返回值一致,参数一致
当子类的成员函数名与父类中成员函数同名且函数原型完全相同时,我们可以用virtual关键字来修饰父类中的同名成员函数,也就是说父类中的这个同名成员函数现在是一个虚函数,此时我们通过父类指针调用该函数时,实际上调用的就是子类中重写的函数了,这就构成了多态。
例如,假设有一个基类Shape,其中有一个虚函数draw() ,然后有派生类Circle 和 Rectangle(矩形)分别重写了 draw() 函数。此时我们创建一个Shape类型的指针ptr,让他等于new Circle(),就是开辟一块Circle大小的空间,把他存到指针ptr里,再让ptr指向draw()函数,我们就可以发现虽然指针类型是Shape类的,但是他调用的是Circle类的draw()函数。如果把Circle换成Rectangle也是一样的。
#include<iostream>
using namespace std;
class Shape
{
public:
virtual void draw()
{
cout << "画一个图形" << endl;
}
};
class Circle :public Shape
{
public:
void draw()
{
cout << "画一个圆形" << endl;
}
};
class Rectangle :public Shape
{
public:
void draw()
{
cout << "画一个矩形" << endl;
}
};
int main()
{
/*Shape* ptr = new Circle();
ptr->draw();
delete ptr;
ptr = nullptr;*/
Circle c;
Rectangle r;
Shape s;
Shape* p[] = { &c,&r,&s };
for (int i = 0; i < 3; i++)
{
p[i]->draw();
}
return 0;
}
运行结果如下图:
菱形继承与虚继承:
继承中还有一个特殊的继承,菱形继承
当出现一个派生类从多个基类继承,而这些基类又有共同的基类时,就可能出现菱形继承。
这可能导致数据冗余和二义性问题。为了解决这个问题,我们可以使用虚继承技术。所谓虚继承实际上是在派生类中多了一个存储结构,叫虚基表,里面存放的是基类的成员函数地址,派生类实例化对象时,不同基类中的重复成员在派生类对象内存中只有一份地址,这使得重复的成员在派生类中只存在一份实例,避免了重复。