C++学习之包含对象的类、私有和保护继承

C++的一个主要目的是促进代码重用。公有继承是实现这种目标的方式之一。但不是唯一的方式。其他方法:使用本身就是另一个类的对象的类成员。例如Stock类中包含一个string类成员。这种方式成为包含(containment)、组合(composition)或层次化(layering)。以及使用私有或保护继承,可以实现has-a关系。模板(Template)也是实现代码重用的一个重要手段。

包含对象成员的类

Student类,计划用一个string类对象来表示姓名,一个valarray<double>类来表示成绩。

这里公有派生,从valarray中、或者string类中派生出学生类就不太适合了,因为学生和姓名,学生和成绩不是is-a关系。而是has-a关系——学生有姓名,学生有成绩。方法就是包含,即创建一个包含其他对象的类。

1
2
3
4
5
6
class Student
{
private:
string name;
valaaray<double> scores;
};

将上述成员声明为私有的,Student类成员函数可以使用string类和valarray类的公有方法来修改namescores对象。对于这种情况,描述为:Student类获得其成员对象的实现,但没有继承接口。举个例子说,就是虽然Student使用了string类对象用作表示姓名,但Student没有获得使用string::operator+=()的能力。(没有用加号来让两个学生对象的姓名相连的接口),如果要实现这种功能,做法是要在Student里,通过name对象调用string的方法。例如name.size(); 通过scores对象来调用valarray方法,例如scores.max();

has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。例如两个string对象可以使用+=来实现连接的功能。但对Student来说+=是没有意义的。

私有继承:Student类的新版本

C++实现has-a关系的另一种方法:私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着:基类方法将不会成为派生类对象的公有接口的一部分,但可以在派生类的成员函数中使用它们。

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
class Student :private std::string, private std::valarray<double>
{
private:
typedef std::valarray<double> ArrayDb;
std::ostream & arr_out(std::ostream & os)const;
public:
Student() :std::string("Null Student"), ArrayDb() {}
explicit Student(const std::string & s)
:std::string(s), ArrayDb() {}
explicit Student(int n) :std::string("Nully"), ArrayDb(n) {}
Student(const std::string & s, const ArrayDb & a)
:std::string(s), ArrayDb(a) {}
Student(const char * str, const double * pd, int n)
:std::string(str), ArrayDb(pd, n) {}
~Student() {}
double Average() const;
double & operator[](int i);
double operator[](int i)const;
const std::string & Name() const;

//friend
//input and output
friend std::istream & operator>>(std::istream & is, Student & stu);
friend std::istream & getline(std::istream & is, Student & stu);
friend std::ostream & operator<<(std::ostream & os, const Student & stu);

};

基类组件

因为隐式继承组件而不是成员对象将影响代码的编写。没有namescores来描述对象了。而必须使用用于公有继承的技术。对于构造函数,使用类名而不是成员名

1
Student() :std::string("Null Student"), ArrayDb() {}

访问基类方法

使用私有继承时,只能在派生类的方法中使用基类的方法。使用包含时,是用对象名和成员运算符”.”来调用方法,而使用私有继承时使用类名和作用域解析运算符。举下面这个方法为例:

1
2
3
4
5
6
7
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}

访问基类对象

使用作用域解析运算符可以访问基类的方法。但如果要使用基类对象本身,该如何做呢?例如,下面这段代码是使用包含*关系的Student类有Name()方法

1
2
3
4
const string & Student::Name()const
{
return name;
}

但使用私有继承时,该string对象没有名称,那么如何访问内部string对象?答案是使用强制类型转换。由于Student类是由string类派生的,因此可以通过强制类型转换来把Student类转换为string类对象。指针this来指向用来调用方法的对象,*this则为调用方法的对象本身。而且为了避免调用构造函数创建新的对象,所以创建一个引用,返回引用。

1
2
3
4
const string & Student::Name()const
{
return (const string &) *this;
}

私有继承的指针关系

这一行代码:

1
os << "Scores of" << (const string &) stu << ":\n";

这里用的是operator<<(ostream & ,const string &);原因:在私有继承中,未进行显示类型转换的派生类引用和指针,无法赋值给基类的引用和指针。

使用包含还是私有继承?

大多数C++程序倾向于使用包含。首先,包含易于理解。其次,继承会引起很多问题,尤其是多重继承。例如,如果某个类需要3个string类对象,那么可以包含3个独立的string成员,然而使用继承的话很麻烦,只能使用一个这样的对象(对象没有名称,难以区分)

然而,私有继承的特性比包含多。例如:派生类可以重新定义虚函数,但包含不能。例如 派生类可以访问保护成员,包含不能。

下面是上述用私有继承实现的Student类的完整代码:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#ifndef STUDENTC_H_
#define STUDENTC_H_

#include<iostream>
#include<string>
#include<valarray>

class Student :private std::string, private std::valarray<double>
{
private:
typedef std::valarray<double> ArrayDb;
std::ostream & arr_out(std::ostream & os)const;
public:
Student() :std::string("Null Student"), ArrayDb() {}
explicit Student(const std::string & s)
:std::string(s), ArrayDb() {}
explicit Student(int n) :std::string("Nully"), ArrayDb(n) {}
Student(const std::string & s, const ArrayDb & a)
:std::string(s), ArrayDb(a) {}
Student(const char * str, const double * pd, int n)
:std::string(str), ArrayDb(pd, n) {}
~Student() {}
double Average() const;
double & operator[](int i);
double operator[](int i)const;
const std::string & Name() const;

//friend
//input and output
friend std::istream & operator>>(std::istream & is, Student & stu);
friend std::istream & getline(std::istream & is, Student & stu);
friend std::ostream & operator<<(std::ostream & os, const Student & stu);

};

#endif


using std::ostream;
using std::endl;
using std::istream;
using std::string;

//private method
std::ostream & Student::arr_out(std::ostream & os)const
{
int i;
int lim = ArrayDb::size();
if (lim > 0)
{
for (i = 0; i < lim; i++)
{
os << ArrayDb::operator[](i) << " ";
if (i % 5 == 4)
os << endl;
}
if (i % 5 != 0)
os << endl;
}
else
os << "empty array" << endl;
return os;
}

//public methods
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}

const string & Student::Name()const
{
return (const string &) *this;
}

double & Student::operator[](int i)
{
return ArrayDb::operator[](i);
}

double Student::operator[](int i)const
{
return ArrayDb::operator[](i);
}

//friend
std::istream & operator>>(std::istream & is, Student & stu)
{
is >> (string &)stu;
return is;
}

std::istream & getline(std::istream & is, Student & stu)
{
getline(is, (string &)stu);
return is;
}

std::ostream & operator<<(std::ostream & os, const Student & stu)
{
os << "Scores of" << (const string &) stu << ":\n";
stu.arr_out(os);
return os;
}

using std::cin;
using std::cout;

void set(Student & sa, int n);
const int pupils = 3;
const int quizzes = 5;

int main()
{
Student ada[pupils] = { Student(quizzes),Student(quizzes),Student(quizzes) };

int i;
for (i = 0; i < pupils; i++)
set(ada[i], quizzes);
cout << "\nStudent List:\n";
for (i = 0; i < pupils; i++)
cout << ada[i].Name() << endl;
cout << "\nResults:";
for (i = 0; i < pupils; i++)
{
cout << endl << ada[i];
cout << "average: " << ada[i].Average() << endl;
}
cout << "Done." << endl;
return 0;
}

void set(Student & sa, int n)
{
cout << "Please enter the student's name: ";
getline(cin, sa);
cout << "Please enter " << n << " quiz scores:\n";
for (int i = 0; i < n; i++)
cin >> sa[i];
while (cin.get() != '\n')
continue;
}

保护继承

保护继承是私有继承的辩题。使用关键字protected

1
2
3
4
class Student: protected std::string, protected std::valarray<double>
{
……
};

使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。

当从派生类派生出另一个类时,私有继承和保护继承之间的主要差别就出来了。使用私有继承时,第三代类就不能使用基类的接口,因为基类的公有方法在派生类中是私有方法。使用保护继承时,基类的公有方法在第二代类中将变成保护的,因此在第三代类中可以使用。

各种继承小总结

特征 公有继承 保护继承 私有继承
公有成员变成 派生类的公有成员 派生类的保护成员 派生类的私有成员
保护成员变成 派生类的保护成员 派生类的保护成员 派生类的私有成员
私有成员变成 只能通过基类的接口访问 只能通过基类的接口访问 只能通过基类接口访问
能否隐式向上转换? 能(但只能在派生类中) 不能