C++中的代码重用

C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。例如使用这样的类成员:本身是另一个类的对象。这种方法称为包含(containment)组合(composition)或层次化(layering)。另一种方法是使用私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。

类模板:类模板使我们能够使用通用术语定义类,然后使用模板来创建针对特定类型定义的特殊类。例如,可以定义一个通用的栈模板,然后使用该模板创建一个用于表示int值栈的类和一个用于表示double值栈的类,甚至可以创建一个这样的类,即用于表示由栈组成的栈。

包含对象成员的类:

valarray类是由头文件valarray支持的。顾名思义,这个类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作。valarray被定义为一个模板类,以便能够处理不同的数据类型。

接口和实现:使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。

C++和约束:C++包含让程序员能够限制程序结构的特征——使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。

初始化顺序:当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。

使用被包含对象的接口:被包含对象的接口不是公有的,但可以在类方法中使用它。

私有继承

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

使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口:这是is-a关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。
使用私有继承,类将继承实现。例如,如果从String类派生出Student类,后者将有一个String类组件,可用于保存字符串。另外,Student方法可以使用String方法来访问String组件。
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象。因此私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。

使用多个基类的继承被称为多重继承(multiple inheritance,MI)。通常,MI尤其是公有MI将导致一些问题,必须使用额外的语法规则来解决它们。

访问基类的方法:使用私有继承时,只能在派生类的方法中使用基类的方法。然而,私有继承使得能够使用类名和作用域解析运算符来调用基类的方法。
总之,使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用于解析运算符来调用方法。

访问基类对象:使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身时,该如何做?例如,Student类的包含版本实现了Name()方法,它返回string对象成员name;但使用私有继承时,该string对象没有名称。那么,Student类的代码如何访问内部的string对象呢?
答案是使用强制类型转换。由于Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象;结果为继承而来的string对象。

访问基类的友元函数:用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确地函数。
在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。

#pragma once
#include<iostream>
#include<valarray>
#include<string>

class Student :
	private std::string, private std::valarray<double>
{
private:
	typedef std::valarray<double> ArrayDb;
	//private method for scores output
	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, int n) :std::string(s), 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
	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);
};

#include "Student.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;


//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);
}

ostream &Student::arr_out(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";
	return os;
}

istream &operator>>(istream &is, Student &stu) {
	is >> (string &)stu;
	return is;
}

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

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

使用包含还是私有继承:由于既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用那种方式呢?大多数C++程序员倾向于使用包含。首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。另外,包含能够包括多个同类的子对象,而继承则只能使用一个这样的对象。
然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。
另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。

提示:通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

保护继承:保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected。
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。

使用using重新定义访问权限:使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。例如,假设希望Student类能够使用valarray类的sum()方法,可以在Student类的声明中声明一个sum()方法。这样Student对象便能够调用Student::sum()。
另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明来指出派生类可以使用特定的基类成员,即使采用的是私有派生。

多重继承:MI描述的是有多个直接基类的类。与单继承一样,公有MI表示的也是is-a关系。请注意,必须使用关键字public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为私有派生:

class SingingWaiter : public Waiter, Singer {...};	//Singer is a private bese

私有MI和保护MI可以表示has-a关系。

MI可能会给程序员带来很多新问题。其中两个主要的问题是:从两个不同的基类继承同名方法:从两个或更多相关基类那里继承同一个类的多个实例。为解决这些问题,需要使用一些新规则和不同的语法。

虚基类:虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类(virtual和public的次序无关紧要):

class Singer : virtual public Worker{ ... };
class Waiter : public virtual Worker{ ... };

然后可以将SingerWaiter类定义为:

class SingingWaiter : public Waiter, public Singer {...};

现在SingingWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本。因为SingingWaiter现在只包含了一个Worker子对象,所以可以用多态。

新的构造函数规则:使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数即是基类构造函数。但这些构造函数可能需要将信息传递给其基类。
如果Worker是虚基类,则信息自动传递将不起作用。例如,对于下面的MI构造函数:

SingingWaiter(const Worker &wk, int p = 0, int v = Singer::other) : Waiter(wk, p), Singer(wk, v) {};

存在的问题是,自动传递信息时,将通过2条不同的途径(Waiter和Singer)将wk传递给Worker对象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。在上述的情况下,Waiter和Singer将初始化出Worker之外的成员,而Worker将使用默认构造函数。如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需地基类构造函数。因此,构造函数应该是这样:

SingingWaiter(const Worker &wk, int p = 0, int v = Singer::other) 
	: Worker(wk), Waiter(wk, p), singer(wk, v) {};

哪个方法:对于单继承,如果没有重新定义一个函数,则将使用最近祖先中的定义。而在多重继承中,每个祖先都有一个相同名字的函数,这使得函数调用是二义性的。

警告:多重继承可能导致函数调用的二义性。

可以使用作用域解析运算符来澄清编程者的意图,然而,更好的方法是在SingingWaiter中重新定义Show(),并指出要使用哪个Show()。对于单继承来说,让派生方法调用基类的方法是可以的。

MI小结:从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足其他要求:

  • 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于简介非虚基类来说是非法的;
  • 通过优先规则解决名称的二义性。

类模板:

继承(公有、私有或保护)和包含并不总是能够满足重用代码的需要。例如,Stack类和Queue类都是容器类(container class),容器类设计用来存储其他对象或数据类型。St可以定义专门用于double值或string对象的Stack类,除了保存的对象类型不同外,这两种Stack类的代码是相同的。然而,与其编写新的类声明,不如编写一个泛型栈,然后将具体的类型作为参数传递给这个类。这样就可以使用通用的代码生成存储不同类型值的栈。

C++的类模板为生成通用的类声明提供了以各种更好的方法。模板提供参数化(parameterized)类型,即能够将类型名作为参数传递给接收方来建立类或函数。例如,将类型名int传递给Queue模板,可以让编译器构造一个对int进行排队的Queue类。

采用模板时,将使用模板定义替换Stack声明,使用模板成员函数替换Stack的成员函数。和模板函数一样,模板类以下面这样的代码开头:

template <class Type>

关键字template告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。
这里使用class并不意味着Type必须是一个类;而只是表明Type是一个通用的类型说明符,在使用模板时,将使用实际的类型替换它。较新的C++实现允许在这种情况下使用不太容易混淆的关键字typename代替class:

template <typename Type>

可以使用自己的泛型名代替Type,其命名规则与其他标识符相同。
由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件,并在要使用这些模板的文件中包含该头文件。

#pragma once
template <class Type>
class Stack
{
private:
	enum { MAX = 10 };
	Type items[MAX];
	int top;
public:
	Stack();
	bool isempty();
	bool isfull();
	bool push(const Type & item);
	bool pop(Type & item);
};

template<class Type>
Stack<Type>::Stack() {
	top = 0;
}

template<class Type>
bool Stack<Type>::isempty() {
	return top == 0;
}

template<class Type>
bool Stack<Type>::isfull() {
	return top == MAX;
}

template<class Type>
bool Stack<Type>::push(const Type & item) {
	if (top < MAX) {
		items[top++] = item;
		return true;
	}
	else return false;
}

template<class Type>
bool Stack<Type>::pop(Type & item) {
	if (top > 0) {
		item = items[--top];
		return true;
	}
	else return false;
}

可以将内置类型或类对象用作类模板Stack< Type >的类型。

模板多功能性:

  1. 递归使用模板
    对于数组模板定义,可以这样用它:

    ArrayTP<ArrayTP<int, 5>, 10>twodee;
    

    这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组。与之等价的常规数组声明如下:

    int twodee[10][5];
    

    在模板语法中,维的顺序与等价的二维数组相反。

  2. 使用多个类型参数
    模板可以包含多个类型参数。

  3. 默认类型模板参数
    类模板的另一项新特性是,可以为类型参数提供默认值:

    template<class T1, class T2 = int>class Topo { ... };
    

    虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。

模板的具体化:
类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化(specialization)。模板以泛型的方法描述类,而具体化是使用具体的类型生成类声明。

  1. 隐式实例化:声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义,编译器再需要对象之前,不会生成类的隐式实例化。

  2. 显式实例化:当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化(explicit instantiation)。声明必须位于模板定义所在的名称空间中。例如:

    template class ArrayTP<string ,100>;
    

    在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来具体化。

  3. 显式具体化:显示具体化(exolicit specialization)是特定类型的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。

  4. 部分具体化

模板别名(C++11):如果能为类型指定别名,将很方便,在模板设计中尤其如此。可使用typedef为模板具体化指定别名:

typedef std::array<double, l2>arrd;
typedef std::array<int, l2>arri;
typedef std::array<std::string, l2>arrst;
arrd gallons;
arri days;
arrst months;

但C++11新增了一项功能——使用模板提供一系列别名:

template<typename T>
using arrtype = std::array<T, l2>;

这将arrtype定义为一个模板别名,可使用它来指定类型,如下:

arrtype<double>gallons;
arrtype<int>days;
arrtype<std::string>months;
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值