摘要

介绍了C++中的派生与继承,包括单继承、多重继承、重复继承、虚基类机制,以及构造函数和析构函数的调用顺序,同时讲解了赋值兼容性规则和访问控制。

目录

[TOC]

继承

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。

当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类

继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。

img

代码如下:

1
2
3
4
5
6
7
8
9
10
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};

//派生类
class Dog : public Animal {
// bark() 函数
};

基类 & 派生类

一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。

派生类的对象不仅存放了在派生类中定义的非静态数据成员,而且也存放了从基类中继承下来的非静态数据成员。

类派生列表以一个或多个基类命名,形式如下:

1
class derived-class: access-specifier base-class

其中,访问修饰符 access-specifier 是 public、protectedprivate 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。

假设有一个基类 ShapeRectangle 是它的派生类,如下所示:

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

// 基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};

// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};

int main(void)
{
Rectangle Rect;

Rect.setWidth(5);
Rect.setHeight(7);

// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;

return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
Total area: 35

访问控制和继承

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

我们可以根据访问权限总结出不同的访问类型,如下所示:

访问 public protected private
同一个类 yes yes yes
派生类 yes yes no
外部的类 yes no no

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

继承类型

当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。

我们几乎不使用 protectedprivate 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

构造函数与析构函数的调用次序

构造函数的调用次序

  • 先调用其基类的构造函数
  • 再调用本类对象成员的构造函数
  • 最后才调用本类的构造函数

析构函数的调用次序

  • 先调用本类的析构函数
  • 再调用本类对象成员的析构函数
  • 最后才调用其基类的析构函数

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;
class C {
public: C() // 构造函数
{ cout << "Constructing C object.\n"; }
~C() // 析构函数
{ cout << "Destructing C object.\n"; }
};
class BASE {
public: BASE() // 构造函数Anon
{ cout << "Constructing base object.\n"; }
~BASE() // 析构函数
{ cout << "Destructing base object.\n"; }
}; 
class DERIVED: public BASE {
C mOBJ;
public: DERIVED() // 构造函数
{ cout << "Constructing derived object.\n"; }
~DERIVED() // 析构函数
{ cout << "Destructing derived object.\n"; }
}; 
int main()
{ DERIVED obj; // 声明一个派生类的对象
  return 0;
}

运行结果

1
2
3
4
5
6
7
运行结果:
Constructing base object.
Constructing C object.
Constructing derived object.
Destructing derived object.
Destructing C object.
Destructing base object.

向基类构造函数中传递实际参数

给基类构造函数传递实际参数是通过向派生类构造函数传递实际参数以及初始化列表来间接实现传递的。
带初始化列表的派生类构造函数的一般形式:

1
2
3
4
派生类名 ( 参数表 ) :  基类名 ( 调用基类构造函数参数表 )
{
//派生类构造函数体
}

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
using namespace std;
class Base
{
int private1, private2;
public:
Base(int p1, int p2)
{
private1 = p1;
private2 = p2;
}

int inc1() { return ++private1; }
int inc2() { return ++private2; }

void display()
{
cout<<"private1 = "<<private1
<<", privte2 = "<<private2<<"\n";
}
};

class Derived:private Base
{
int private3;
Base private4;
public:
Derived(int p1, int p2, int p3, int p4,int p5):Base(p1,p2),private4(p3,p4)
{
private3 = p5 ;
}
int inc1() { return Base::inc1();}
int inc3() { return ++private3 ; }
void display()
{
Base::display();
private4.display();
cout<<"private3 = "<<private3<<"\n";
}
};

int main()
{
Derived obj( 17, 18, 1, 2, -5);
obj.inc1();
obj.display();
}

输出结果

1
2
3
private1 = 18, private2 = 18
private1 = 1, private2 =2
private3 = -5

多重继承

概念

多继承即一个子类可以有多个父类,它继承了多个父类的特性。

C++ 类可以从多个类继承成员,语法如下:

1
2
3
4
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};

其中,访问修饰符继承方式是 public、protectedprivate 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔,如上所示。现在让我们一起看看下面的实例:

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>

using namespace std;

// 基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};

// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};

// 派生类
class Rectangle: public Shape, public PaintCost
{
public:
int getArea()
{
return (width * height);
}
};

int main(void)
{
Rectangle Rect;
int area;

Rect.setWidth(5);
Rect.setHeight(7);

area = Rect.getArea();

// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;

// 输出总花费
cout << "Total paint cost: $" << Rect.getCost(area) << endl;

return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
Total area: 35
Total paint cost: $2450

构造和析构函数

多个基类构造函数的调用次序

1)按基类在被继承时所声明的次序、从左到右依次调用;
2)与出现在派生类构造函数实现初始化列表中的次序无关。

1
2
3
4
5
6
7
8
9
10
class DERIVED: public BASE2, public BASE1 {
public:
DERIVED(int x, int y): BASE1(x), BASE2(y)
{ cout << "Constructing derived object.\n";
}
……
};
……
DERIVED obj(3,4);
// obj在创建时,先调用BASE2的构造函数,然后调用BASE1的构造函数,最后才执行自己DERIVED的构造函数

重复继承

定义

重复继承指一个派生类多次继承同一个基类

C++中关于继承的限制

  • 不允许直接或间接让一个类继承自己
  • 不允许一个派生类直接继承同一个基类两次以上
  • 不允许一个基类即是直接基类又是间接基类

重复继承的两种类型

  • 复制继承
    被多次重复继承的基类有多个实体副本
  • 共享继承
    被多次重复继承的基类只有一个实体副本

二义性

问题

若在继承时没有作特殊声明,此时采用的是复制继承,会导致重复继承的二义性问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class BASE {public:  int i;}; 
class BASE1: public BASE {
public: int j;
};
class BASE2: public BASE {
public: int k;
}; 
class DERIVED: public BASE1, public BASE2 {
public: int sum;
}; 

void main()
{
DERIVED obj; // 声明一个派生类对象 
obj.i = 3; //错误,编译程序无法确定使用i的哪一份副本
obj.j = 5; // 正确的,使用从BASE1继承下来的j
obj.k = 7;// 正确的,使用从BASE2继承下来的k
}

解决办法

1) 如不改变重复继承的方法(还是复制继承),则采用作用域运算符::明确指明采用哪个副本

2) 改用共享方式继承
用虚基类机制保证任何派生类中只提供一个基类的副本

虚基类的构造函数与析构函数

对虚基类构造函数的调用总是先于普通基类的构造函数。
虚基类的唯一副本只被初始化一次

C++中构造函数的调用次序

  • 最先调用虚基类的构造函数。
  • 其次调用普通基类的构造函数,多个基类则按派生类声明时列出的次序、从左到右调用,而不是初始化列表中的次序。
  • 再次调用对象成员的构造函数,按类声明中对象成员出现的次序调用,而不是初始化列表中的次序。
  • 最后执行派生类的构造函数。
1
2
3
4
5
int main()
{ DERIVED obj;
obj.BASE1::i = 3;
……
}

2) 改用共享方式继承
用虚基类机制保证任何派生类中只提供一个基类的副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class baseA{
public:
baseA(){cout<<"This is baseA class.\n";}
baseA(int i){cout<<"This is baseA class: int "<<i<<" \n";}
};

class baseB{
public:
baseB(){cout<<"This is baseB class.\n";}
baseB(int i ){cout<<"This is baseB class: int "<<i<<" \n";}
};

class derivedA:public baseB, virtual public baseA{
public:
derivedA():baseB(1){cout<<"This is derivedA class.\n";}
};

class derivedB:public baseB, virtual public baseA{
public:
derivedB():baseA(2){cout<<"This is derivedB class.\n";}
};

class derived:public derivedA, virtual public derivedB{
public:
derived(){cout<<"This is derived class.\n";}
};

int main(){
derived obj;
return 1;
}

运行结果

1
2
3
4
5
6
7
This is baseA class.
This is baseB class.
This is derivedB class.
This is baseB class: int 1
This is derivedA class.
This is derived class.
Press any key to continue

赋值运算的类型兼容

类型的赋值兼容性规则:

  1. 允许将后代类的对象赋值给祖先类,但反之不成立

    例:

    1
    2
    3
    4
    BASE  obj1;		//基类
    Y1 obj2; //继承基类
    obj1 = obj2 ; // 把obj2中基类部分的内容赋给obj1,对
    obj2 = obj1 ; //错误

    但此规则只适用于公有派生,只有公有派生类才能兼容基类类型

  2. 指向基类对象的指针也可指向公有派生类对象

1
2
3
4
5
6
7
8
BASE *p ;			
p = &obj1; //正确
p = &obj2; //正确

Y1 *p1;
p1 = &obj1; //错误,指向派生类的指针不能直接赋值指向基类
p1 = &obj2; //正确
p = p1 ; //正确