C++教程正在更新中,具体请查看教程目录
C++的继承
在 C++ 中继承是一个很重要的概念,同样继承也是面向对象的语言的一个非常重要的概念,在维基百科中对于继承的定义如下:
继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类的行为在编译期就已经决定,无法在运行期扩展。
有些编程语言支持多重继承,即一个子类可以同时有多个父类别,比如C++编程语言;而在有些编程语言中,一个子类只能继承自一个父类别,比如Java编程语言,这时可以透过实现接口来实现与多重继承相似的效果。
现今面向对象程序设计技巧中,继承并非以继承类别的“行为”为主,而是继承类别的“类型”,使得组件的类型一致。另外在设计模式中提到一个守则,“多用合成,少用继承”,此守则也是用来处理继承无法在运行期动态扩展行为的遗憾。
在之前讲解面向对象程序设计的时候说到面向对象程序设计本身是一种模仿自然界来设计城西的过程,那么同样也对继承进行了模仿,我们将类分为父类和子类,子类可以继承父类的大多数性质和函数,同样子类也可以衍生出一些只有自己独有的性质和函数。
在程序设计的过程中,继承允许我们根据一个类来定义另一个类,这样的程序设计可以可以使得创建和维护一个应用程序变得更容易,这样做,可以达到重用代码功能和提高程序执行效率的效果。当我们创建一个类的时候,我们不需要重新编写新的数据成员和数据函数,只需要将新建的类继承一个已有的类的成员即可,被继承的类被称为基类,而新建的类称为派生类。
如果用英语来表示继承的关系,可以使用 is a 来表示继承的关系,比如:哺乳动物是动物,狗是哺乳动物,那么狗是动物。
1.继承的特点
接下来我们先来了解继承的特点:
1.继承的子类拥有了父类的所有属性和方法(构造函数和析构函数除外)
2.子类可以拥有父类没有的属性和方法
3.子类是一种特殊的父类,可以在一定程度上用子类类代替父类
4.子类的对象可以当作父类的对象来使用
2.继承的语法
一个父类可以派生出多个子类,这就意味着,一个子类可以从多个父类继承数据和哈桑农户,我们定义一个子类,需要使用一个类派生列表来指定基类,类派生列表以一个或者多个基类来命名,形式如下:
class derived-class : access-specifier base-class
其中访问修饰符 access-specifier 是 public,protected,private 中的一个,base-class 是之前定义过的某个类的名称,如果为使用访问修饰符,则默认为 private,同样结构体也可以进行继承,语法如下:
struct derived-class : access-specifier base-struct
这里结构体和类类似,只不过当没有访问修饰符的时候,结构体默认为 public 继承
假如我们需要求一个盒子的体积,代码如下:
class box{
protected:
int length, breadth, height;
public:
void set_number(int l, int b, int h){
length = l, breadth = b, height = h;
}
};
class box_volume : public box{
private:
int volume;
public:
int get_volume(){
volume = length * breadth * height;
return volume;
}
};
int main(){
int length, breadth, height;
scanf("%d%d%d", &length, &breadth, &height);
box_volume my_box;
my_box.set_number(length, breadth, height);
printf("%d", my_box.get_volume());
return 0;
}
由于构造函数和析构函数不同用,所以这里使用了 set_number 函数来充当构造函数的作用,上面的程序,我们使用了继承来实现求解盒子体积的问题。
3.继承中的访问控制
我们在一个类中可以对成员变量和成员函数进行访问控制,通过 C++ 提供的三种权限修饰符来进行实现,在子类继承父类的时候,C++提供了三种权限修饰符在继承的时候进行使用,上面三种不同的继承方式会改变子类对父类属性和方法的访问限制。
在之前讲解类和对象的时候有讲到过访问控制,如下:
数据封装是面向对象编程的一个重要的特点,将数据封装可以防止函数直接访问类内部成员,而确定类的访问限制主要是通过在类的主体中运用访问修饰符: public,private,protected 来确定的。一个类可以有很多个访问修饰符来标记区域,可以作用的区域是到下一个访问修饰符出现之前或者遇到这个类结束的右括号之前。
如果类中的成员没有声明访问权限,那么默认访问权限是 private 的。
public成员
public 的成员是在类的外部仍可进行访问的,在类的外部可以不使用任何成员函数来给成员赋值和或者变量的值。如下所示
class box{
public:
int height, lenth, breadth;
void set_lenth(int len){
lenth = len;
}
void set_breadth(int bre){
breadth = bre;
}
void set_height(int he){
height = he;
}
};
int main(){
box my_box;
my_box.set_lenth(1), my_box.set_breadth(2), my_box.set_height(3);
printf("%d%d%d", my_box.lenth, my_box.breadth, my_box.height);
return 0;
}
通过上面的代码由于变量是 public 的,我们可以直接在类的外部访问成员变量的值,同样在类的外部也可以直接设置成员变量的值。
private成员
private的成员变量或函数在类外面的是无法访问的,甚至无法查看,只有类和友元可以访问私有成员。在默认的情况下,类中定义的所有成员都是私有的,如果需要对私有的成员变量或函数进行访问,需要运用类中的成员函数进行访问。如下所示:
class box{
int volume;
private:
int height, lenth, breadth;
public:
int get_volume();
void set_lenth(int len){
lenth = len;
}
void set_breadth(int bre){
breadth = bre;
}
void set_height(int he){
height = he;
}
};
inline int box::get_volume(){
volume = height * lenth * breadth;
return volume;
}
int main(){
box my_box;
my_box.set_lenth(1), my_box.set_breadth(2), my_box.set_height(3);
printf("%d", my_box.get_volume());
return 0;
}
通过上面的函数我们同样能获得盒子的体积,由于类中的成员变量没有指定访问权限,所以C++默认变量 volume 的访问权限是 private 的。
protected成员
protected 的成员变量和函数和 private 的成员类似,但是 protected 的成员在派生类 / 子类中是可以访问的。至于派生和继承在之后将会提到,在下面的代码中,我们从父类 box 中派生出了子类 small_box 如下所示:
class box{
protected:
int volume;
int height, lenth, breadth;
public:
void set_lenth(int len){
lenth = len;
}
void set_breadth(int bre){
breadth = bre;
}
void set_height(int he){
height = he;
}
};
class small_box : public box{
public:
int get_volume();
};
inline int small_box::get_volume(){
volume = height * lenth * breadth;
return volume;
}
int main(){
small_box my_box;
my_box.box::set_lenth(1), my_box.box::set_breadth(2), my_box.box::set_height(3);
printf("%d", my_box.get_volume());
return 0;
}
我们定义了基类 box 和派生类 small_box 通过派生类我们访问了被保护的成员变量并将这个成员变量进行输出。
继承中的特点
在类的继承中,有 public,protected,private 三种继承方式,他们相应的改变了基类成员的访问属性。
继承方式 | 基类的public成员 | 基类的protected成员 | 基类的private成员 | 继承引起的访问控制关系变化概括 |
---|---|---|---|---|
public继承 | 仍为public成员 | 仍为protected成员 | 不可见 | 基类的非私有成员在子类的访问属性不变 |
protected继承 | 变为protected成员 | 变为protected成员 | 不可见 | 基类的非私有成员都为子类的保护成员 |
private继承 | 变为private成员 | 变为private成员 | 不可见 | 基类中非私有成员都成为子类的私有成员 |
但是无论上述的哪种继承方式下面的两点都不变:
1.private 成员只能被本类(类内)成员和友元访问,不能被派生类访问
2.protected 成员只能被派生类访问
public继承
class A{
public:
int a1;
protected:
int a2;
private:
int a3;
public:
int a;
A(){
a1 = 1, a2 = 2, a3 = 3, a = 4;
}
void function(){
printf("%d %d %d %d\n", a1, a2, a3, a);
}
};
class B : public A{
public:
int a;
B(int i){
A();
a = i;
}
void function(){
cout << a << " "; //正确,public成员
cout << a1 << " ";//正确,基类的public成员,在派生类中仍是public
cout << a2 << " ";//正确,基类的protected成员在派生类中可以被访问
cout << a3 << " ";//错误,基类的private成员在派生类中无法访问
}
};
int main(){
B b(10);
cout << b.a << " ";//正确,派生类中的public成员
cout << b.a1 << " ";//正确,基类的public成员
b.function();//正确,派生类的pbulic成员函数
b.A::function();//正确,基类的pbulic成员
cout << b.a2 << " ";//错误, protected成员不能在类外被访问
cout << b.a3 << " ";//错误, private成员不能在类外被访问
return 0;
}
protected继承
class A{
public:
int a1;
protected:
int a2;
private:
int a3;
public:
int a;
A(){
a1 = 1, a2 = 2, a3 = 3, a = 4;
}
void function(){
printf("%d %d %d %d\n", a1, a2, a3, a);
}
};
class B : protected A{
public:
int a;
B(int i){
A();
a = i;
}
void function(){
cout << a << " "; //正确,public成员
cout << a1 << " ";//正确,基类的public成员,变为protected可以被派生类访问
cout << a2 << " ";//正确,基类的protected成员在派生类中仍然是protected成员可以被访问
cout << a3 << " ";//错误,基类的private成员在派生类中无法访问
}
};
int main(){
B b(10);
cout << b.a << " ";//正确,派生类中的public成员
cout << b.a1 << " ";//错误,基类的public成员变为protected成员在类外无法被访问
b.function();//正确,派生类的pbulic成员函数
b.A::function();//错误,基类的pbulic成员变为protected成员无法在类外访问
cout << b.a2 << " ";//错误, protected成员不能在类外被访问
cout << b.a3 << " ";//错误, private成员不能在类外被访问
return 0;
}
private继承
class A{
public:
int a1;
protected:
int a2;
private:
int a3;
public:
int a;
A(){
a1 = 1, a2 = 2, a3 = 3, a = 4;
}
void function(){
printf("%d %d %d %d\n", a1, a2, a3, a);
}
};
class B : private A{
public:
int a;
B(int i){
A();
a = i;
}
void function(){
cout << a << " "; //正确,public成员
cout << a1 << " ";//正确,基类的public成员,变为private成员可以被派生类访问
cout << a2 << " ";//正确,基类的protected成员在派生类中变为private成员可以被访问
cout << a3 << " ";//错误,基类的private成员在派生类中无法访问
}
};
int main(){
B b(10);
cout << b.a << " ";//正确,派生类中的public成员
cout << b.a1 << " ";//错误,基类的public成员变为private成员在类外无法被访问
b.function();//正确,派生类的pbulic成员函数
b.A::function();//错误,基类的pbulic成员变为private成员无法在类外访问
cout << b.a2 << " ";//错误, private成员不能在类外被访问
cout << b.a3 << " ";//错误, private成员不能在类外被访问
return 0;
}
4.子类对父类的扩展
通过继承,子类复制了父类的数据成员和成员函数,使我们不需要编程就可以具备了父类的功能,即子类可以通过增加新的数据成员和成员函数重载从父类中继承得到的成员函数,重定义从父类继承的成员函数,并且可以改变父类成员在子类中的访问属性。
成员函数的重定义和名字隐藏
子类不仅可以在父类的基础上添加父类没有的新成员,还可以对从父类继承得到的成员函数进行重定义,即子类可以定义与基类成员函数原型相同的成员函数,即可以有相同的返回类型,函数名和参数,而重载成员函数需要函数具有不同的函数原型
父类成员函数的访问
如果需要对父类成员函数进行访问,需要使用如下格式的语句:
(对象.)父类 :: 父类中的函数名
友元和继承,静态成员和继承
如果一个类继承了其他类,那么这个类声明的友元可以访问自己的全体成员和从父类继承得到的成员,但是无法访问私有成员
友元函数不能继承,因为友元函数不属于类,但是静态成员可以被继承,以为i整个继承体系中只有一个静态成员变量, 这份静态成员变量使所有的类和对象所共有的。
继承中的静态成员也同样遵循三种继承的基本访问控制原则,同样,静态成员可以通过类名和作用域操作符在进行访问,也可以通过对象点式来进行访问。
5.构造函数和析构函数
在子类中子类不仅继承了父类的数据成员而且可能定义了新的数据成员,这些数据成员都需要构造函数进行初始化,同样不论是父类还是子类都需要构造函数,析构函数,拷贝构造函数。如果一个类没有定义如上的函数,那么编译器会自动生成相应的函数。
子类构造函数的建立规则
子类只能在构造函数初始化列表中为父类或者对象成员进行初始化,形式如下:
子类构造函数名(参数) : 父类构造函数名(参数), ...{
}
当父类没有定义构造函数或者父类有默认构造函数的情况下子类可以不定义构造函数。
如果子类的父类同样是另一个类的子类,那么这个子类只负责它的直接的父类的构造函数的调用。
继承中构造函数和析构的函数的调用执行顺序
1.子类对象在创建的时候会优先调用父类的构造函数,如果父类仍是另一个父类的子类,那么优先调用父类的父类的构造函数,同理,依次向上类推
2.父类的构造函数执行完毕后再去执行子类的构造函数
3.当父类的构造函数不是默认的构造函数(即编译器自动生成的),那么需要在子类的每一个构造函数上使用初始化列表来调用父类的构造函数
4.析构函数和构造函数的调用顺序恰好相反
继承中构造函数和析构函数的示例
代码如下:
class test_1{
protected:
int number_1, number_2;
public:
test_1(){
cout << "父类构造函数\n";
}
~test_1(){
cout << "父类析构函数\n";
}
};
class test_2 : public test_1{
public:
test_2(){
cout << "子类构造函数\n";
}
~test_2(){
cout << "子类析构函数\n";
}
};
int main(){
test_2 test;
return 0;
}
输出结果如下:
父类构造函数
子类构造函数
子类析构函数
父类析构函数
从上面的代码和输出可以体会到构造函数的析构函数的调用顺序以及子类构造函数和析构函数的使用方法
继承和组合情况混搭下的构造函数的调用顺序
构造函数:先调用父类的构造函数在调用成员变量的构造函数最后调用本身的构造函数。
析构函数:先调用自己的析构函数再调用成员变量的析构函数最后调用父类的析构函数。
6.多继承
如果我们需要从多个地方继承成员变量和成员函数,这就称为多继承,多继承的写法如下所示:
class <子类名> : <继承方式1> <父类名1>, <继承方式2> <父类名2> ...{
}
多继承并不被推荐,因为多继承增加了代码的负责都,而且一个可以通过多继承解决的问题都可以通过单继承的方式进行解决
多继承中的构造函数和析构函数
当一个子类拥有多个父类的时候,子类必须要为每个父类的构造函数提供初始化参数,其原则和方法和单继承类似,调用次序按照继承时的父类顺序,同理析构函数相反。
多继承代码举例
通过求解盒子的体积问题来了解多继承的问题,代码如下:
class length{
protected:
int box_length;
public:
length(int l){
box_length = l;
}
};
class breadth{
protected:
int box_breadth;
public:
breadth(int b){
box_breadth = b;
}
};
class height{
protected:
int box_height;
public:
height(int h){
box_height = h;
}
};
class volume : public length, public breadth, public height{
private:
int box_volume;
public:
volume(int l, int b, int h) : length(l), breadth(b), height(h){
box_volume = box_length * box_breadth * box_height;
}
int get_volume(){
return box_volume;
}
};
int main(){
volume box(1, 2, 3);
printf("%d", box.get_volume());
return 0;
}
7.继承中的类型兼容性原则
继承中的同名成员
当在继承中,如果父类的成员和子类的成员的名称相同,我们可以通过作用域操作符 : 来显式使用父类的成员,如果我们不使用作用域操作符,那么程序默认使用子类的成员。
类型兼容性原则
类型兼容性原则是指在需要父类的对象的所有地方,都可以通过公有继承的子类的对象来进行替代,通过共有继承,子类获得了父类除了构造函数和析构函数的所有属性和方法,这样子类就有了父类的所有功能,因此,凡是父类可以解决的问题,子类一定可以解决。
类型兼容性原则可以替代的情况
子类对象可以当作父类的对象来使用
子类对象可以直接赋值给父类对象
子类对象可以直接初始化父类对象
父类指针可以直接指向子类对象
父类引用可以直接引用子类对象
8.虚拟继承/虚继承(环状继承)
如果我们需要进行如下继承方式:
class D{
...
}
class B : public D{
...
}
class A : public D{
...
}
class C : public A, public B{
...
}
这样的继承方式又被称之为菱形继承,菱形继承通常由二义性的问题。
菱形继承所占内存:内存大小 = 父类1成员的大小 + 父类2成员的大小 + 子类特有的从成员的大小。
我们来看下面的代码:
class Animal{
public:
void eat(){
}
};
class Mammal : public Animal{
public:
void breadth(){
}
};
class WingedAnimal : public Animal{
public:
void flap(){
}
};
class Bat : public Mammal, public WingedAnima{
};
int main(){
Bat bats;
bats.eat();
return 0;
}
上面的代码模仿了动物界,蝙蝠既是哺乳动物又是可以飞行的动物,同样哺乳动物和可飞行的动物都是动物,因此我们可以断定蝙蝠会吃。
但是上面的代码会法伤错误,对于函数:bats.eat() 是有歧义的,因为在 Bat 中有两个 Animal 父类(分别从 WingedAnimal 和 Mammal 中间接而来),所以对于所有的 Bat 的对象都有两个不同的 Animal 父类的子对象,因此,尝试直接引用会发生错误。用一个简单的例子来说明,如果我要从 Bat 函数到达 Animal 函数并找到其中的 eat 函数,需要经过一个岔路口,但是程序在执行的同时,程序并不知道该从哪个岔路口经过,因此程序会发生错误。
通过使用关键字 virtual 可以解决这一问题,将关键字写在过程类前即可,如下所示:
class Animal{
public:
void eat(){
}
};
class Mammal : virtual public Animal{
public:
virtual void breadth(){
}
};
class WingedAnimal : virtual public Animal{
public:
virtual void flap(){
}
};
class Bat : public Mammal, public WingedAnimal{
};
int main(){
Bat bats;
bats.Animal::eat();
return 0;
}
同样,可以通过求解盒子题解的方式来联系多继承和菱形继承的问题,代码如下:
class Box{
protected:
int length, breadth, height;
public:
void set_number(int l, int b, int h){
length = l, breadth = b, height = h;
}
};
class Box_area : virtual public Box{
protected:
int area;
public:
void get_area(){
area = length * breadth;
}
};
class Box_height : virtual public Box{
protected:
int height;
public:
void get_height(){
height = Box :: height;
}
};
class Box_volume : public Box_area, public Box_height{
private:
int volume;
public:
int get_volume(){
volume = area * Box_height :: height;
return volume;
}
};
int main(){
Box_volume my_box;
int length, breadth, height;
scanf("%d%d%d", &length, &breadth, &height);
my_box.set_number(length, breadth, height);
my_box.get_area();
my_box.get_height();
printf("%d", my_box.get_volume());
return 0;
}
查看上一篇:C++类和对象,一篇文章两小时带你理解清C++的类和对象
查看下一篇:C++的多态
查看目录:C++教程目录