C++ has-a关系之包含
一、前提简述
实现C++代码重用的方式不只有公有继承(is-a关系),还有其它方式,其中一种方式是has-a关系,而其中的包含、私有继承、保护继承就是用于实现has-a关系的。本次先记录下包含方式实现has-a关系。
包含是什么呢?
答:变量本身是另一个类的对象,即类中包含另一个类的对象,这种方法称为包含(containment)、组合(composition)、层次化(layering)。
二、包含对象成员的类
对于包含对象成员的类的说明,我们通过类设计示例说明。
2.1 主类设计说明
假设需要对学生考试分数进行管理,在需要知道每个学生的考试分数后,我们还需要使用学生姓名进行匹配,否则不能清晰的知晓每个学生的考试成绩。虽然可以使用公有继承方式实现,但是放在这里是不合适的,因为学生并不是姓名或分数,因此不是is-a关系,而是has-a关系,此处更适合使用包含的方式实现主类设计。接下来,通过这个类设计讲解下包含方式实现的has-a关系。
使用std::string类的对象来表示姓名,使用valarray类模板来实现对分数的管理。
下面是简单的包含类设计:
#include <string>
#include <valarray>
class Student
{
// 1、将成员变量声明为私有的,只允许外部通过Student类的公有成员函数访问
// 2、但是Student类的成员函数可以直接访问name与scores,或通过name或scores的公有接口修改
// 3、对于这种现象,通常描述为Student类获得了其成员对象的实现,但没有继承接口
private:
string name;
valarray<double> scores;
};
2.1.1 接口和实现
(1)公有继承:类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现),获得接口是is-a关系的组成部分。
(2)包含(组合):类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
2.1.2 示例类的详细声明
下面是示例类的详细声明,我们可以根据这个示例类来详细了解 包含 方式实现的has-a关系。
class Student
{
// 1、为简化表示,Student类的定义中包含typedef定义
// 2、在以后的代码中便可以使用typedef定义的ArrayDB类型,而不是std::valarray<double>, 因此,类方法和友元函数可以使用ArrayDb类型
// 3、将typedef定义放在类的私有部分意味着可以在类的函数实现中使用它,但在类外不能使用
private:
typedef std::valarray<double> ArrayDb;
std::string name;
ArrayDb scores;
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() {}
public:
double Average() const;
const std::string& Name() const;
double& operator[](int i);
double operator[](int i) const;
// 输入
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);
};
注意关键字explicit的使用
(1)前面介绍过,用一个参数调用的构造函数将用作从参数类型到类类型的隐式转换函数。
(2)在下述第二个构造函数中,int n参数表示的是数组的元素个数,而不是数组的值,因此将一个构造函数用作int到Student的转换函数是没有意义的,所以使用explicit关键字关闭隐式转换。
explicit Student(const std::string& s)
: name(s)
, scores()
{
}
explicit Student(int n)
: name("Nully")
, scores(n)
{
}
(3)如果省略explicit关键字,则可能会编写出如下的代码:
// 调用Student(const std::string& s, int n)构造函数,name存储为"Job",创建10个元素的数组
Student doh("Job", 10);
// 调用Student(int n)构造函数,name存储为"Nully",将数组重置为5个元素的数组;赋值操作又将 使用临时对象替换原来的doh值
doh = 5;
C++和约束
C++包含让程序员能够限制程序结构的特性:使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据。
这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
2.2 初始化被包含的对象
设计的Student类中使用成员初始化列表语法来初始化name和scores成员对象。使用成员对象的名称来初始化,初始化列表中的每一个成员名称初始化时都调用与之匹配的构造函数,如下所示:
// 1、成员初始化列表语法使用成员变量名称来初始化成员变量
// 2、name(str)调用string(const char*)构造函数
// 3、scores(pd, n)调用构造函数ArrayDb(const double*, int)
Student(const char* str, const double* pd, int n)
: name(str)
, scores(pd, n)
{
}
前面记录过构造函数使用成员初始化列表语法初始化派生类对象的基类部分,如下所示:
// 派生类使用成员初始化语法使用类名调用特定的基类构造函数初始化基类部分
DeriveDMA::DeriveDMA(const DeriveDMA& dv)
: BaseDMA(dv)
{
}
2.2.1 如果不使用初始化列表语法,情况将如何呢?
C++要求在构建对象的其它部分之前,先构建继承对象的所有成员对象。如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。
2.2.2 初始化顺序
当初始化列表包含多个成员变量初始化时,这些成员变量被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。如下所示:
// 1、name成员变量仍将首先被初始化,因为在类定义中它首先被声明
// 2、本示例中初始化顺序并不重要,但是如果代码使用一个成员的值作为另一个成员的初始化表达式的 一部分时,初始化顺序就显得非常重要
Student(const char* str, const double* pd, int n)
: scores(pd, n)
, name(str)
{
}
2.3 使用被包含对象的接口
类中声明(包含)的成员变量是私有的,所以不能直接通过成员变量调用成员变量的接口,但是可以在类中声明公有的调用成员变量接口的接口,如下面代码所示定义一个返回平均分数的函数:
// 1、此函数在Student中声明为公有的,在此定义了一个可有Student对象调用的函数
// 2、该函数内部调用scores(valarray类型)对象的size()和sum()函数
// 3、总之,Student对象调用Student的函数,而Student方法通过被包含的valarray对象调用 valarray类的函数
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}
// 1、定义一个使用string版本的<<运算符的友元函数
// 2、由于stu.name是string类型的变量,所以它将调用string类的函数operator<<(ostream&,
// const string&),
// 3、ostream& operator<<(ostream& os, const Student& stu)必须是Student类的友元函数,这
// 样才能访问name成员变量
// 4、还有一种方式是不定义这样的友元函数,直接定义获取name的Name()公有函数
ostream& operator<<(ostream& os, const Student& stu)
{
os << "Scores for " << stu.name << ":\n";
stu.arr_out(os);
return os;
}
// 1、定义一个辅助函数来输出分数,因为valarray类没有<<运算符实现
// 2、在Student类中声明私有的用于输出scores的辅助函数
// 3、通过这种方式可以将凌乱的细节单独处理,避免编码混乱
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 << std::endl;
}
}
if (i % 5 != 0) {
os << std::endl;
}
} else {
os << "empty array ";
}
return os;
}