本章內容包括:
C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。
本章将介绍其他方法,其中之一是使用这样的类成员:本身是另一个类的对象。
这种方法称为包含( containment)、组合( composition)或层次化( layering)。另一种方法是使用私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。例如, HomeTheater类可能包含一个 BluRayplayer对象。多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
第10章介绍了函数模板,本章将介绍类模板一一另一种重用代码的方法。类模板使我们能够使用通用
术语定义类,然后使用模板来创建针对特定类型定义的特殊类。例如,可以定义一个通用的栈模板,然后
使用该模板创建一个用于表示int值栈的类和一个用于表示 double值機的类,甚至可以创建一个这样的类,用于表示由栈组成的栈。
首先介绍包含对象成员的类。有一些类(如 string类和第16章将介绍的标准C+类模板)为表示类中的组件提供了方便的途径。下面来看一个具体的例子。
学生是什么?入学者?参加研究的人?残酷现实社会的避难者?有姓名和一系列考试分数的人?显然,最后一个定义完全没有表示出人的特征,但非常适合于简单的计算机表示。因此,让我们根据该定义来开发 Student类 将学生简化成姓名和一组考试分数后,可以使用一个包含两个成员的类来表示它:一个成员用于表示姓名,另一个成员用于表示分数。对于姓名,可以使用字符数组来表示,但这将限制姓名的长度。
当然,也可以使用char指针和动态内存分配,但正如第12章指出的,这将要求提供大量的支持代码。一种更好的方法是,使用一个由他人开发好的类的对象来表示。例如,可以使用一个 String类(参见第12章)或标准C++ string类的对象来表示姓名。
较简单的选择是使用 string类,因为C++库提供了这个类的所有实现代码,且其实现更完美。
对于考试分数,存在类似的选择。可以使用一个定长数组,这限制了数组的长度:可以使用动态内存分配并提供大量的支持代码:也可以设计一个使用动态内存分配的类来表示该数组:还可以在标准C++库中査找一个能够表示这种数据的类。
自己开发这样的类一点问题也没有。开发简单的版本并不那么难,因为 double t数组与char数组有很多相似之处,因此可以根据 String类来设计表示 double数组的类。事实上,本书以前的版本就这样做过。
当然,如果C++库提供了合适的类,实现起来将更简单。C++库确实提供了一个这样的类,它就是valarray
valarray类是由头文件 valarray支持的。
顾名思义,这个类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作。
valarray被定义为一个模板类,以便能够处理不同的数据类型。
本章后面将介绍如何定义模板类,但就现在而言,您只需知道如何使用模板类即可。
模板特性意味着声明对象时,必须指定具体的数据类型。因此,使用 valarray类来声明一个对象时,
需要在标识符 valarray后面加上一对尖括号,并在其中包含所需的数据类型:
valarray<int> q_values // an array of int
valarray<double> weights; // an array of double
类特性意味着要使用 valarray对象,需要了解这个类的构造函数和其他类方法。下面是几个使用其构造函数的例子
double gpa[5] = { 3.1, 3.5, 3.8, 2.9, 3.3 };
valarray<double> v1; // an array of double, size
valarray<int> v2(8); // an array of 8 int elements
valarray<int> v3(10, 8) // an array of 8 int elements , each set to 10
valarray<double> v4(gpa, 4); //an array of 4 elements , initialized to the first 4 elements
从中可知,可以创建长度为零的空数组、指定长度的空数组、所有元素度被初始化为指定值的数组、
用常规数组中的值进行初始化的数组。在C++11中,也可使用初始化列表:
valarray<int> v5 = {20,32,17,9};//C++11
下面是这个类的一些方法。
至此,已经确定了 Student类的设计计划!:使用一个 string对象来表示姓名,使用一个 valarray< double>
来表示考试分数。那么如何设计呢?
您可能想以公有的方式从这两个类派生出 Student类,这将是多重公有继承,C++允许这样做,但在这里并不合适,因为学生与这些类之间的关系不是is-a模型。学生不是姓名,也不是一组考试成绩。这里的关系是has-a,学生有姓名,也有一组考试分数。
通常,用于建立 has-a关系的C++技术是组合(包含),即创建一个包含其他类对象的类。例如,可以将 Student类声明为 如下所示:
class Student
{
private:
string name; // use a string object for name
valarray<double> scores; //use a valarray object for scores
}
同样,上述类将数据成员声明为私有的。这意味着 Student类的成员函数可以使用 string和valarray
类的公有接口来访问和修改name和 scores对象,但在类的外面不能这样做,而只能通过Student类的公有接口访问name和 score(请参见图14)。
对于这种情况,通常被描述为 Student类获得了其成员对象的实现,但没有继承接口。
例如, Student对象使用 string的实现,而不是char* name
或 char name[26]
实现来保存姓名。但 Student 对象并不是天生就有使用函数 string operator+=()
的能力。
使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。
获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。
不继承接口是has-a关系的组成部分。
对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。例如, string类将+运算符重载为将两个字符串连接起来;但从概念上说,将两个 Student对象串接起来是没有意义的。
这也是这里不使用公有继承的原因之一。另一方面,被包含的类的接口部分对新类来说可能是有意义的。
例如,可能希望使用 string接口中的 operators<()方法将 Student对象按姓名进行排序,为此可以定义 Student::Operator<()成员函数,它在内部使用函 string::Operator<()。下面介绍一些细节。
现在需要提供 Student类的定义,当然它应包含构造函数以及一些用作Student类接口的方法。
程序清单14.1是 Student类的定义,其中所有构造函数都被定义为内联的;它还提供了一些用于输入和输出的友元函数
程序清单14.1 studentc.h
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// studentc.h -- defining a Student class using containment
#ifndef STUDENTC_H_
#define STUDENTC_H_
#include
#include
#include
class Student
{
private:
typedef std::valarray<double> ArrayDb;
std::string name; // contained object
ArrayDb scores; // contained object
// private method for scores output
std::ostream & arr_out(std::ostream & os) const;
public:
Student() : name("Null Student"), scores() {}
explicit Student(const std::string & s)
: name(s), scores() {}
explicit Student(int n) : name("Nully"), scores(n) {}
Student(const std::string & s, int n)
: name(s), scores(n) {}
Student(const std::string & s, const ArrayDb & a)
: name(s), scores(a) {}
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {}
~Student() {}
double Average() const;
const std::string & Name() const;
double & operator[](int i);
double operator[](int i) const;
// friends
// input
friend std::istream & operator>>(std::istream & is,
Student & stu); // 1 word
friend std::istream & getline(std::istream & is,
Student & stu); // 1 line
// output
friend std::ostream & operator<<(std::ostream & os,
const Student & stu);
};
#endif
为简化表示, Student类的定义中包含下述 typedef:
typedef std::valarray<double> ArrayDb;
这样,在以后的代码中便可以使用表示 ArrayDb,而不是std::valarray- double>,因此类方法和友元函数可以使用 ArrayDb类型。
将该 typedef放在类定义的私有部分意味着可以在 Student类的实现中使用它,
但在 Student类外面不能使用。
请注意关键字 explicit的用法:
explicit Student(const std::string & s) : name(s), scores() {}
explicit Student(int n) : name("Nully"), scores(n) {}
本书前面说过,可以用一个参数调用的构造函数将用作从参数类型到类类型的隐式转换函数;但这通常不是好主意。
在上述第二个构造函数中,第一个参数表示数组的元素个数,而不是数组中的值,因此将一个构造函数用作int到 Student的转换函数是没有意义的,所以使用 explicit关闭隐式转换。
如果省略该关键字,则可以编写如下所示的代码:
Student doh("Homer", 10); //store "Homer",create array of 10 elements
doh = 5; //reset name to "Nully", reset to empty array of 5 elements
在这里,马虎的程序员键入了doh而不是doh[0]。
如果构造函数省略了 explicit, 则将使用构造函数调用 Student(5)将5转换为一个临时 Student对象,并使用 "Nully"
来设置成员name的值。
因此赋值操作将使用临时对象替换原来的doh值。使用了 explicit后,编译器将认为上述赋值运算符是错误的。
C++包含让程序员能够限制程序结构的特性一一使用 explicit防止单参数构造函数的隐式转换,使用 const限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
构造函数全都使用您熟悉的成员初始化列表语法来初始化name和 score成员对象。
在前面的一些例子中,构造函数用这种语法来初始化内置类型的成员:
Queue::Queue(int qs): qsize(qs)..// initialize qsize to qs
上述代码在成员初始化列表中使用的是数据成员的名称( size)。另外,前面介绍的示例中的构造函 数还使用成员初始化列表初始化派生对象的基类部分
hasDMA::hasDMA(const hasDMA &hs): baseDMA(hs) {}
对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数。
对于对象本身,构造函数则使用成员名。例如,请看下面这个构造函数
Student(const char *str, const double * pd, int n) :name(str), scores(pd, n) {}
因为该构造函数初始化的是成员对象,而不是继承的对象,所以在初始化列表中使用的是成员名,而不是类名。
初始化列表中的每一项都调用与之匹配的构造函数,即name(str)调用构造函数 string( const char *)
, scores(pd,n)调用构造函数 ArrayDb( const double*,int)
如果不使用初始化列表语法,情况将如何呢? C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。
因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。
例如,假设 Student构造函数如下:
Student(const char * str, const double *pd, int n) : scores(pd, n), name(str) {}
则name成员仍将首先被初始化,因为在类定义中它首先被声明。
对于这个例子来说,初始化顺序并不重要,但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。
被包含对象的接口(如string,valarray等类)不是公有的,但可以在类方法中使用它。例如,下面的代码说明了如何定义一个返回学生平均分数的函数:
double Student::Average () const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}
上述代码定义了可由 Student对象调用的方法,该方法内部使用了 valarray的方法size()和sum()。这是因为 scores是一个 valarray对象,所以它可以调用 valarray类的成员函数。
总之, Student对象调用 Student的方法,而后者使用被包含的 valarray对象来调用 valarray类的方法.
同样,可以定义一个使用 string版本的<<运算符的友元函数:
// use string version of operator<<()
ostream & operator<<(ostream &os, const Student &stu)
{
os <<" Scores for " << stu.name < ":\n";
}
因为 stu.name是一个 string对象,所以它将调用函数 operators<<( ostream &, const string &),
该函数位于 string类中。注意, operator<<( ostream &os, const Student &stu)
必须是 Student类的友元函数,这样才能访问name成员。
另一种方法是,在该函数中使用公有方法Name(),而不是私有数据成员name。
同样,该函数也可以使用 valarray的<<实现来进行输出,不幸的是没有这样的实现;因此, Student类定义了一个私有辅助方法来处理这种任务:
// private method
ostream & Student::arr_out(ostream & os) const
{
int i;
int lim = scores.size();
if (lim > 0)
{
for (i = 0; i < lim; i++)
{
os << scores[i] << " ";
if (i % 5 == 4)
os << endl;
}
if (i % 5 != 0)
os << endl;
}
else
os << " empty array ";
return os;
}
通过使用这样的辅助方法,可以将零乱的细节放在一个地方,使得友元函数的编码更为整洁:
// use string version of operator<<()
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << stu.name << ":\n";
stu.arr_out(os); // use private method for scores
return os;
}
辅助函数也可用作其他用户级输出函数的构建块——如果您选择提供这样的函数的话。
程序清单14.2是 Student类的类方法文件,其中包含了让您能够使用[]
运算符来访问 Student对象中各项成绩的方法。
程序清单14.2 student.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// studentc.cpp -- Student class using containment
#include "studentc.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;
//public methods
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum()/scores.size();
else
return 0;
}
const string & Student::Name() const
{
return name;
}
double & Student::operator[](int i)
{
return scores[i]; // use valarray::operator[]()
}
double Student::operator[](int i) const
{
return scores[i];
}
// private method
ostream & Student::arr_out(ostream & os) const
{
int i;
int lim = scores.size();
if (lim > 0)
{
for (i = 0; i < lim; i++)
{
os << scores[i] << " ";
if (i % 5 == 4)
os << endl;
}
if (i % 5 != 0)
os << endl;
}
else
os << " empty array ";
return os;
}
// friends
// use string version of operator>>()
istream & operator>>(istream & is, Student & stu)
{
is >> stu.name;
return is;
}
// use string friend getline(ostream &, const string &)
istream & getline(istream & is, Student & stu)
{
getline(is, stu.name);
return is;
}
// use string version of operator<<()
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << stu.name << ":\n";
stu.arr_out(os); // use private method for scores
return os;
}
除私有辅助方法外,程序清单14.2并没有新增多少代码。使用包含让您能够充分利用已有的代码。
下面编写一个小程序来测试这个新的 Student类。出于简化的目的,该程序将使用一个只包含3个 Student对象的数组,其中每个对象保存5个考试成绩。另外还将使用一个不复杂的输入循环,该循环不验 证输入,也不让用户中途退出。程序清单14.3列出了该测试程序
程序清单14.3use_stuc.cpp
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
// use_stuc.cpp -- using a composite class
// compile with studentc.cpp
#include
#include "studentc.h"
using std::cin;
using std::cout;
using std::endl;
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.\n";
// cin.get();
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;
}
程序的运行结果:
meng-yue@ubuntu:~/MengYue/c++/code_reuse/01$ g++ -o use_stuc use_stuc.cpp studentc.cpp
meng-yue@ubuntu:~/MengYue/c++/code_reuse/01$ ./use_stuc
Please enter the student's name: meng
Please enter 5 quiz scores:
12 13 14 15 16
Please enter the student's name: yue
Please enter 5 quiz scores:
1 2 3 4 5
Please enter the student's name: foundation
Please enter 5 quiz scores:
9 9 9 9 9
Student List:
meng
yue
foundation
Results:
Scores for meng:
12 13 14 15 16
average: 14
Scores for yue:
1 2 3 4 5
average: 3
Scores for foundation:
9 9 9 9 9
average: 9
Done.
meng-yue@ubuntu:~/MengYue/c++/code_reuse/01$