c++类——继承(二)

本文最后更新于:1 年前

(一)基类和派生类的默认成员函数:

默认的成员函数共有六个:

  • 构造函数

  • 析构函数

  • 拷贝构造函数

  • 重载的赋值操作符

  • 重载的取地址操作符

  • 重载的const修饰的取地址操作符。

在继承关系内,在派生类中如果没有显示定义这六个默认构造函数,编译器系统会默认合成这六个成员函数。

后面的两个很少需要我们自己实现,此处略。

1. 构造函数:

建立一个类层次后,通常创建某个派生类的对象,该对象也包括使用基类的数据和函数。

c++提供一种机制,在创建派生类对象时用指定参数调用基类的构造函数来初始化派生类继承基类的数据。

此时,派生类的构造函数声明为:

派生类构造函数(变元表):基类(变元表),对象成员1(变元表), ……. , 对象成员n(变元表) { 函数体}

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。

构造函数的执行顺序为基类->派生类。

2. 析构函数:

因为在派生类的构造函数调用了基类的构造函数,因此其析构函数也需要对派生类对象和基类对象都进行清理。该任务的完成由各自的析构函数完成。

析构函数的执行顺序为派生类->基类。

注意:不要在派生类中主动调用基类的析构函数,因为派生类的析构函数在被调用完成后会自动调用基类的析构函数清理基类成员。以此来保证派生类对象先清理派生类成员再清理基类成员。

3. 拷贝构造函数:

派生类的拷贝构造函数必须调用基类的拷贝构造函数完成对基类的拷贝初始化。

基类的拷贝构造函数会通过“切片”(见后面的内容)拿到基类需要的一部分值进行初始化。

4. 赋值操作符重载:

派生类的operator=必须要显式调用基类的operator=完成基类的复制。

因为基类和派生类的运算符,编译器默认给的是同一个名字,构成了隐藏,每次调用=这个赋值运算符都会一直调用子类,造成无限循环,所以赋值运算符要直接修饰限定的基类。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include<iostream>
using namespace std;
class Father
{
protected:
string name;
int age;
double weight;
double height;
public:
Father(string s="", int i=0, double w=0, double h=0) :name(s), age(i), weight(w), height(h)
{
cout << "调用基类的构造函数" << endl;
}
Father(const Father& f)
{
name = f.name;
age = f.age;
weight = f.weight;
height = f.height;
cout << "调用基类的拷贝构造函数" << endl;
}
Father& operator= (const Father& f)
{
name = f.name;
age = f.age;
weight = f.weight;
height = f.height;
cout << "调用基类的赋值运算符" << endl;
return *this;
}
~Father()
{
cout << "调用基类的析构函数" << endl;
}
};
class Son:public Father
{
protected:
int sex;
public:
Son(string s="", int i1=0, double w=0, double h=0, int i2=0):Father(s,i1,w,h),sex(i2)
{
cout << "调用派生类的构造函数" << endl;
}
Son(const Son& s):Father(s)
{
sex = s.sex;
cout << "调用派生类的拷贝构造函数" << endl;
}
Son& operator= (const Son& s)
{
Father::operator=(s);
sex = s.sex;
cout << "调用派生类的赋值运算符" << endl;
return *this;
}
~Son()
{
cout << "调用派生类的析构函数" << endl;
}
};

int main()
{
Son me("S", 20, 188, 190, 0);
cout << endl;
Son you("SS", 30, 199, 200, 1);
cout << endl;
Son he;
cout << endl;
he = me;
cout << endl;
Son she(you);
cout << endl;
return 0;
}
//运行结果
//调用基类的构造函数
//调用派生类的构造函数
//
//调用基类的构造函数
//调用派生类的构造函数
//
//调用基类的构造函数
//调用派生类的构造函数
//
//调用基类的赋值运算符
//调用派生类的赋值运算符
//
//调用基类的拷贝构造函数
//调用派生类的拷贝构造函数
//
//调用派生类的析构函数
//调用基类的析构函数
//调用派生类的析构函数
//调用基类的析构函数
//调用派生类的析构函数
//调用基类的析构函数
//调用派生类的析构函数
//调用基类的析构函数

(二)基类和派生类的赋值转换:

派生类对象可以赋值给:

  • 基类的对象:

    因为派生类中包括基类的所有数据成员,赋值时将这部分赋值给基类即可完成赋值转换。

  • 基类的指针:

    基类指针指向派生类的首元素地址,不会发生越界,而且只能在基类的相对应的数据成员范围上操作,不能越界操作派生类数据成员。

  • 基类的引用:

    基类引用只可以操作派生类中对应的基类的数据成员,同样不能越界操作。

这个过程也被称为切片/切割,意即把派生类中基类的部分切出赋值过去。

基类对象一般情况下不能赋值给派生类对象。但可以把基类的指针强制类型转换赋值给派生类的指针,此指针还要求指向的是派生类对象,否则可能会越界访问。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include<iostream>
using namespace std;
class A
{
private:
int a;
protected:
double aa;
public:
A(int i,double d):a(i),aa(d){}
void printA()
{
cout << "a:" << a << endl;
cout << "aa:" << aa << endl;
return;
}
void changeaa()
{
aa++;
return;
}
};
class B:public A
{
private:
int b;
protected:
double bb;
public:
B(int i1, int i2, double d1, double d2) :b(i1), bb(d1), A(i2, d2) {}
void printB()
{
A::printA();
cout << "b:" << b << endl;
cout << "bb:" << bb << endl;
return;
}
void changebb()
{
aa--;
bb++;
return;
}
};

int main()
{
B b1(1, 10, 1.5, 2.5);
B b2(1, 10, 1.5, 2.5);
A a1 = b1;
A* pa1 = &b1;
A& a2 = b2;

a1.printA();
pa1->printA();
a2.printA();
cout << endl;
b1.changeaa();
a1.printA();
pa1->printA();
cout << endl;
b2.changebb();
a2.printA();
cout << endl;
B *b3 = (B*)(pa1);
b1.printB();
b3->printB();
return 0;
}
//运行结果:
//a:10
//aa : 2.5
//a : 10
//aa : 2.5
//a : 10
//aa : 2.5
//
//a : 10
//aa : 2.5
//a : 10
//aa : 3.5
//
//a : 10
//aa : 1.5
//
//a : 10
//aa : 3.5
//b : 1
//bb : 1.5
//a : 10
//aa : 3.5
//b : 1
//bb : 1.5

(三)单继承和多继承:

1. 基本内容

单继承是指派生类只有一个直接基类,多继承是指一个派生类拥有两个或多个直接基类。

继承的格式详见一。

多继承完成了更多的代码复用,并使得复用的程度更深。

多继承时,派生类构造函数调用多个基类的构造函数,析构函数同理,构造顺序按照声明继承的顺序进行,析构则相反顺序。

若不同继承基类有同名成员,直接访问会造成命名冲突,此时需要在名字前加上类名和域解析符::,显式指定使用哪个类的成员,消除二义性。

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
60
61
62
63
64
65
66
67
#include<iostream>
using namespace std;
class A
{
protected:
int x;
public:
A(int i):x(i)
{
cout << "调用A构造函数" << endl;
}
~A()
{
cout << "调用A析构函数" << endl;
}
};
class B
{
protected:
int x;
public:
B(int i) :x(i)
{
cout << "调用B构造函数" << endl;
}
~B()
{
cout << "调用B析构函数" << endl;
}
};
class C:public A, public B
{
protected:
int x;
public:
C(int i1,int i2,int i3) :B(i1),A(i2),x(i3)
{
cout << "调用C构造函数" << endl;
}
void printx()
{
cout << "A.x:" << A::x << endl;
cout << "B.x:" << B::x << endl;
cout << "x:" << x << endl;
return;
}
~C()
{
cout << "调用C析构函数" << endl;
}
};
int main()
{
C c(1, 2, 3);
c.printx();
return 0;
}
//运行结果:
//调用A构造函数
//调用B构造函数
//调用C构造函数
// A.x:2
//B.x:1
//x : 3
//调用C析构函数
//调用B析构函数
//调用A析构函数

2. 菱形继承

多继承内存在着许多问题,例如二义性和代码冗余,下面通过菱形继承说明。

菱形继承是多继承的一种特殊情况。

20_C++菱形继承

如图,类B和类C分别继承类A,类D对类BC进行多继承,此时,创建一个D对象,会导致基类A构造两次并析构两次——派生类中产生了重复的基类数据,并且重复的构造和析构同一个基类对象,造成了代码冗余,浪费了很大的空间。

同时,因为BC都继承了A的成员,D在继承BC时会存在同名的数据成员,无法直接通过变量名读取相应的值,产生了二义性,此时需要通过域解析符::进行区分,非常麻烦。

一般情况下,不建议设计多继承和菱形继承,因为情况比较复杂。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include<iostream>
using namespace std;
class A
{
private:
int a;
protected:
int number;
public:
A(int i1,int i2):a(i1),number(i2)
{
cout << "调用A构造函数" << endl;
}
~A()
{
cout << "调用A析构函数" << endl;
}
};
class B:public A
{
private:
int b;
public:
B(int i1,int i2,int i3) :A(i1,i2),b(i3)
{
cout << "调用B构造函数" << endl;
}
~B()
{
cout << "调用B析构函数" << endl;
}
};
class C:public A
{
private:
int c;
public:
C(int i1, int i2, int i3) :A(i1, i2), c(i3)
{
cout << "调用C构造函数" << endl;
}
~C()
{
cout << "调用C析构函数" << endl;
}
};
class D:public B,public C
{
private:
int d;
public:
D(int i1, int i2, int i3,int i4,int i5) :B(i1,i2,i3),C(i1,i2,i4),d(i5)
{
cout << "调用C构造函数" << endl;
}
void printD() {
cout << "sizeof(A):" << sizeof(A) << endl;
cout << "sizeof(B):" << sizeof(B) << endl;
cout << "sizeof(C):" << sizeof(C) << endl;
cout << "sizeof(D):" << sizeof(D) << endl;
cout << "d:" << d << endl;
}
};
int main()
{
D d(1, 2, 3, 4, 5);
d.printD();
return 0;
}
//运行结果:
//调用A构造函数
//调用B构造函数
//调用A构造函数
//调用C构造函数
//调用C构造函数
//sizeof(A) : 8
//sizeof(B) : 12
//sizeof(C) : 12
//sizeof(D) : 28
//d : 5
//调用C析构函数
//调用A析构函数
//调用B析构函数
//调用A析构函数

3. 菱形继承的解决:虚继承

为了避免以上的情况出现,我们需要通过虚继承的方式解决问题——需要虚基类。

虚基类不是在声明基类时声明的,而是声明派生类时指定继承方式声明的。

虚继承是指在继承的基类前添加关键字virtual,格式为:

class 派生类名: virtual 继承方式 基类名

此时的基类被称为虚基类,也就是被虚继承的基类(不是抽象类!!)

在菱形继承中,当BC对A都进行虚继承后,D对BC进行多继承时,重复的变量将只会保留一份。这是因为,D不仅对直接基类进行初始化,也会对虚基类初始化,且只有一次——c++编译器只调用最后的派生类对基类的构造函数调用,忽略其他派生类对虚基类的构造函数调用,避免了对基类数据成员的重复初始化。

虚继承后,访问同名变量不需要使用域成员运算符,因为只有一个基类对象。

此处暂不介绍虚继承的内存模型,以后有需要单开一篇进行介绍。

以下是虚继承之后的情况:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include<iostream>
using namespace std;
class A
{
private:
int a;
protected:
int number;
public:
A(int i1,int i2):a(i1),number(i2)
{
cout << "调用A构造函数" << endl;
}
~A()
{
cout << "调用A析构函数" << endl;
}
};
class B:virtual public A
{
private:
int b;
public:
B(int i1,int i2,int i3) :A(i1,i2),b(i3)
{
cout << "调用B构造函数" << endl;
}
~B()
{
cout << "调用B析构函数" << endl;
}
};
class C:virtual public A
{
private:
int c;
public:
C(int i1, int i2, int i3) :A(i1, i2), c(i3)
{
cout << "调用C构造函数" << endl;
}
~C()
{
cout << "调用C析构函数" << endl;
}
};
class D:public B,public C
{
private:
int d;
public:
D(int i1, int i2, int i3,int i4,int i5) :A(i1,i2),B(i1,i2,i3),C(i1,i2,i4),d(i5)
{
cout << "调用C构造函数" << endl;
}
void printD() {
cout << "sizeof(A):" << sizeof(A) << endl;
cout << "sizeof(B):" << sizeof(B) << endl;
cout << "sizeof(C):" << sizeof(C) << endl;
cout << "sizeof(D):" << sizeof(D) << endl;
cout << "anumberaddress:" << &(A::number) << endl;
cout << "bnumberaddress:" << &(B::number) << endl;
cout << "cnumberaddress:" << &(C::number) << endl;
cout << "dnumberaddress:" << &number << endl;
cout << "d:" << d << endl;
}
};
int main()
{
D d(1, 2, 3, 4, 5);
d.printD();
return 0;
}
//运行结果:
//调用A构造函数
//调用B构造函数
//调用C构造函数
//调用C构造函数
//sizeof(A) : 8
//sizeof(B) : 24
//sizeof(C) : 24
//sizeof(D) : 48
//anumberaddress : 000000C76B8FF5C4
//bnumberaddress : 000000C76B8FF5C4
//cnumberaddress : 000000C76B8FF5C4
//dnumberaddress : 000000C76B8FF5C4
//d : 5
//调用C析构函数
//调用B析构函数
//调用A析构函数

c++类——继承(二)
https://github.com/xiaohei07/xiaohei07.github.io/2023/04/03/c++类——继承(二)/
作者
07xiaohei
发布于
2023年4月3日
许可协议