第14章 C++中的代码重用
1. 包含对象成员的类
包含指的是类声明中有其他类对象作为其数据成员,包含是一种 “ has-a ” 的关系,与公有继承的 “ is-a” 不同,包含只获得了该类对象的实现,并没有继承该类对象的接口,即可以通过公有方法访问类对象,在公有方法中调用类对象的公有方法,在类外不能。
class Student
{
private:
typedef valarray<double> ArrayDb;
std::string name;
ArrayDb scores;
public:
Student() : name("null student"), scores() {}
可以在类内通过 typedef 的方式起别名。包含的形式下,构造函数的成员初始化列表采用类对象数据成员直接初始化的形式,不像公有继承那样,通过直接调用基类的构造函数初始化基类部分的数据成员:
hasDma::hasDma(const hasDma & hs) : baseDma(hs) { ... } // 公有继承直接调用基类构造函数
如果不采用成员初始化列表的方式,由于 C++ 要求在类对象创建之前,先构建继承对象的所有成员对象,所以这种情况下,会调用包含的类对象的默认构造函数,这里也就是 string 和 valarray 的默认构造函数。初始化的顺序与成员初始化列表的顺序无关,与声明时的顺序有关,即 Student 类中,只会先初始化 name。
2. 私有继承
私有继承将基类的私有、保护、公有成员全部变成派生类的私有成员,派生类只能在公有方法中使用基类的方法或者访问基类的数据成员。因此,私有继承与包含一样,也是实现 has-a 关系的一种方式,只继承基类的方法,并没有继承基类的实现。可以认为包含是将命名的类对象添加到派生类中,私有继承是将未命名的类添加到派生类中。
私有继承使用关键字 private :
clsaa Student : private std::string, private std::valarray<double>
{
private:
typedef std::valarray<double> ArrayDb;
public:
Student() : std::string("Null Student"), ArrayDb() {}
double Average() const;
double & operator[](int i);
double & Student::operator[](int i) const;
const std::string & Name();
const std::string & Name() const;
friend std::ostream & operator<<(std::ostream & os, cosnt Student &st);
}
这种情况下,string 和 valarray 类为 Student 提供了两个未命名的对象成员。
私有继承与包含类似,只是在派生类成员初始化时,通过类名代替包含中命名的类对象,如:std::string("Null Student")
要调用基类方法时,通过类 + 作用域解析运算符(::)的方式。
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}
double & Student::operator[](int i)
{
return ArrayDb::operator[](i);
}
double & Student::operator[](int i) const
{
return ArrayDb::operator[](i);
}
要是派生类方法要访问基类对象本身,这种情况下采用强制类型转换 (const string &) *this:
const std::string & Student::Name() const
{
return (const std::string &) *this;
}
派生类的友元函数无法通过类加作用域解析运算符的方式指明使用哪个基类的友元,因为友元函数不是成员函数,这种情况下,同样使用显示类型转换的方式,私有继承无法使用隐式转换,因为不是 is-a 的关系,基类无法指向派生类,而且在多个基类的情况下,隐式转换无法确定使用哪个基类的友元:
std::ostream &(std::ostream & os, const Student& st)
{
os << (const std::string &) st << std::endl;
...
} // os << (const std::string &) 调用 string 的 << 运算符重载
对于采用包含还是私有继承的问题,二者各有优势。私有继承相比于包含更过于抽象,使用起来更复杂一些,而且当派生类有多个基类成员时,如有三个 string 类型的成员,通过包含可以很容易做到,而私有继承由于未命名,难以区分,因此只能使用一个对象,这种情况私有继承是完成不了的。但是私有继承提供的特性更多,像基类有保护类型的成员,在派生类中这样的成员是可用的,但是通过包含基类得到的类,类内也是无法访问这些保护成员的;再一个就是通过继承可以重新定义虚函数,包含的不能,私有继承重新定义的虚函数只能在类内使用,因为不是公有的。
通常没有特别的要求,使用包含实现 has-a 关系;如要访问原有类的保护成员或者需要重新定义虚函数,使用私有继承。
3. 保护继承
保护继承通过关键字 protected 实现,它将基类的公有成员和保护成员变成派生类的保护成员:
class Student : protected std::string, protected std::valarray<double>
{
...
};
同私有继承一样,基类的接口在派生类内也是可用的,在类外不可用。与私有继承的主要区别是在再次派生时,新的派生类在类内依然可以访问最早的基类的方法,而私有继承则不能。
特征 | 公有继承 | 保护继承 | 私有继承 |
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变成 | 只能通过基类的接口访问 | 只能通过基类的接口访问 | 只能通过基类的接口访问 |
能否隐式向上转换 | 能 | 能(只能在派生类中) | 否 |
要想在保护或私有继承中,派生类外使用基类的方法,一种方法是在派生类中定义一个基类的相同方法,在实现中调用基类的方法:
double Student::sum() const
{
return ArrayDb::size();
]
另一种方法是在派生类的公有中使用 using 声明:
class Student : private std::string, private std::valarrary<double>
{
public:
using std::valarrary<double>::min; // 不是 min() 是 min
using std::valarrary<double>::max;
}
这里的 using 表示 valarrary 的 min() max() 方法是可用的,即把这两个方法变成了 Student 类的公有方法:
Student ada;
cout << dad.min() << endl;
注意 using 声明后面没有圆括号,没有函数参数,也没有返回值。
using std::valarray<double> operator[]
它表明原派生类的两个 [ ] 重载都可用。using 只适用于继承,不用于包含。
4. 多重继承
主要介绍的是公有 MI(multiple inheritance),形式上与前面的说的私有、保护继承类似,通过关键字 public 实现:
class SingingWaiter : public Waiter, public Singer
{
...
};
public 不能省略,编译器默认是 private。
公有 MI 也是实现 is-a 的关系,不过在继承多次后可能会出现问题,以菱形继承为例:
Worker 派生出 Singer 和 Waiter 两个类,两个类共同派生出 Singerwaiter 类。Singerwaiter 类会出现一些问题,它会有两个 Worker 对象,而且如果不重新定义虚函数,那么它无法确定使用的是 Singer 的方法还是 Waiter 的方法。
Singerwaiter ed;
Worker * pw = & ed; // 二义性
这是因为 ed 中有两个 Worker 对象,pw 不知道该指向哪一个。可以通过类型转换来指定:
Singerwaiter ed;
Worker * pw = (Waiter *) &ed; // Waiter 中的 Worker 对象
Worker * pw2 = (Singer *) &ed; // Singer 中的 Worker 对象
虚基类可以解决多个基类对象的问题。虚基类使得从多个有共同基类的类派生出的类,只有一个该共同基类的对象,通过关键字 virtual 实现虚基类:
class Worker
{
private:
string name;
long id;
public:
Worker() : name("no one"), id(0L) {}
Worker(const string & s, long n) : name(s), id(n) {}
virtual void Set();
virtual void show() const;
}
class Singer : virtual public Worker
{
protect:
enum {other, alto, contralto, soprano,
bass, baritone, tenor};
enum { Vtypes = 7 };
private:
int voice;
public:
Singer() : Worker(), voice(other) {}
Singer(const string & s, long n, int v = other)
: Worker(s, n), voice(v) {}
Singer(const Worker & wk, int v = other)
: Worker(wk), voice(v) {}
void Set();
Void Show() const;
};
class Waiter : public virtual Worker
{
private:
int pan;
public:
Waiter() : Worker(), pan(0) {}
Waiter(const string & s, long n, int p = 0)
: Worker(s, n), pan(p) {}
Waiter(const Worker & wk, int p = 0)
: Worker(wk), pan(p) {}
void Set();
void Show() const;
};
public 和 virtual 的顺序不影响。这样使得 Worker 称为 Singer 和 Waiter 的虚基类,两个派生类再共同派生 Singerwaiter 类时,Singerwaiter 只有一个 Worker 类对象。
class Singerwaiter : public Singer, public Waiter
{
public:
Singerwaiter() {}
Singerwaiter(const string & s, long n, int p = 0,
int v = other)
: Worker(s, n), Waiter(s, n, p), Singer(s, n ,v) {}
Singerwaiter(const Worker & wk, int p = 0, int v = other)
: Worker(wk), Waiter(wk, p), Singer(wk, v) {}
Singerwaiter(const Worker & wk, int p = 0)
: Worker(wk), Waiter(wk, p), Singer(wk) {}
Singerwaiter(const Worker& wk, int v = other)
: Worker(wk), Singer(wk, v), Waiter(wk) {}
void Set();
void Show() const;
}; // 只有一个 Worker 类对象
在不使用虚基类时,二次派生的类的构造函数可以这样定义:
class A
{
int a;
public:
A(int n = 0) : a(n) {}
};
class B : public A
{
int b;
public:
B(int m = 0, n = 0) : A(m), b(n) {}
};
class C : public B
{
int c;
public:
C(int x = 0, int y = 0, int z = 0) : B(x, y), c(z) {}
};
二次派生类 C 的构造函数只能调用 B 类的构造函数,而 B 类的构造函数只能调用 A 类的构造函数,C 不允许成员初始化列表时调用 A 类的构造函数,会报错,不允许使用间接的构造函数。
而当初始基类是虚基类时,二次派生类的构造函数需要新的规则:
Singerwaiter(const Worker & wk, int p = 0, int v = Singer::other)
: Waiter(wk, p), Singer(wk, v) {} // 错误
Singerwaiter(const Worker & wk, int p = 0, int v = Singer::other)
: Worker(wk), Waiter(wk, p), Singer(wk, v) {} // 正确
第一种错误的原因是,wk 的值会通过 Singer 和 Waiter 的构造函数,这两种途径传递给 worker 对象,但是通过虚基类,实际只有一个 worker 对象,这会产生冲突。因此在第一种错误的构造函数下,编译器会调用 Worker 的默认构造函数,为 Singerwaiter 类的 worker 对象赋值。
如果不想使用虚基类的默认构造函数,二次派生类的构造函数在初始化成员列表中,需要显示的调用虚基类的某个构造函数。
二次派生类除了构造函数需要新的规则外,还要注意继承来的方法。如果没有重新定义 set() 或者 Show() 方法,那么 Singerwaiter 调用 set 时,会出现二义性,如果 Singerwaiter 是单继承,那么这个 set 会是 Singer 或者 Waiter 这个离它最近的祖先的 set()。
可以通过作用域解析运算符来显示调用未重新定义的 set():
Singerwaiter new;
new.Singer::set();
如果重新定义 set() 方法,需要在方法定义里显示指出用哪个类的 set():
void Singerwaiter::set()
{
Singer::set();
}
但是上述的方法忽略了 Singerwaiter 的 Waiter 成员 pan 的赋值,可以在 Singerwaiter 的 set 方法中再添加 Waiter 的 set 方法来补救,但是这样会重复两次设置 Worker 组件。
解决这种问题的方式是将各个基类的自身组件进行拆分,在 protect 下定义一些只处理自己组件的方法,这样在 Singerwaiter 中调用这些只处理自己组件的基类方法,可以避免多次重复的问题。
虚基类多次派生后方法的优先级问题:
class B
{
public:
short q();
};
class C : virtual public B
{
public:
long q();
int omg();
}
class D : public C
{
...
};
class E : virtual public B
{
private:
int omg()
};
class F : public D, public E
{
...
};
在类 F 中,F.q() 就是类 C 中的而不是 B 的,因为 C 是 B 派生来的,所以 C 比 B 离F更近;而 omg() 就会出现二义性,因为 C 和 E 不存在继承关系,类的访问规则,即公有和私有并不影响优先级,尽管 E 的 omg() 是私有的,C 的 omg() 是公有的,也不优先于 E。这意味着可以调用 F.q(),这使用的是 C 的 q() 方法,如果 C 的 q() 是私有的,它的优先级也高于 B 的 q(),这时 F.q() 依然是 C,但是这意味调用不可访问的 C::q()。
5. 类模板
类模板定义形式与函数模板类似,注意,不能将模板成员函数的定义放在单独的实现文件中。一般都是将模板的声明和定义放在一个 .h 文件中:
// stacktp.h
template <class T> // class 可以用 typename 替换
// T 可以所以命名 不是必须 T
class Stack
{
private:
enum { MAX = 10 };
T items[MAX];
int top;
public:
Stack();
bool isempty() const;
bool isfull() { return top == MAX; }
};
template <class T>
bool Stack<T>::isempty() const
{
...
}
类方法定义时也要加上 template 关键字,如果是在类声明中直接定义方法(内联定义),则可以省略模板前缀和类限定符。模板不是类和成员函数的定义,而只是编译器指令,告诉编译器如何生成函数定义。
在使用模板类时,必须显式声明一个具体的类型作为模板类对象:
Stack<int> k;
Stack<string> s;
这点不像模板函数,模板函数可以根据参数类型生成对应的函数定义,类模板不行。类模板的泛型标识符(这里的 T)称为类型参数,它只能是类型不能是数字等其他东西。
在类内声明或者定义可以直接使用类名,省略类型参数,类外不行:
template <class T>
class Stack
{
public:
Stack & operator=(const Stack & st);
};
可以创建带有非类型参数的类模板:
template <class T, int n>
class ArrayTP
{
private:
T ar[n];
public:
virtual T & operator[](int i);
...
};
T 是类型参数,int 指出 n 的类型,n这种参数称为非类型参数,也叫表达式参数,使用这种模板类
ArrayTP<double, 12> eggs;
编译器会创建生成一个类,T 替换为 double, n 替换为12。
表达式参数有限制,它可以是整型、枚举、引用或指针,因此 double m 是不合法的(m 是浮点数),但是可以是
double * m(指针可以),而且不能修改参数表达式的值和地址,模板中,n++ 或 &n 等表达式是不能使用的。
另一个缺陷是,每个不同的表达式都要生成一个类模板:
ArrayTP<double, 12> m;
ArrayTP<double, 13> n;
这会生成两个两个独立的类声明。
常规类的技术,模板类也可以使用,如模板类作为基类、作为组件、作为模板类型等。
可以创建对类型的模板类:
template <class T1, class T2>
class Pair
{
private:
T1 a;
T2 b;
public:
T1 & first();
T2 & second();
...
};
也可以为模板类的模板类型参数提供默认值,但不能为模板函数的类型参数提供默认值。二者都可以为非类型参数提供默认值:
template <class T1, class T2 = int> // 函数模板不能有默认类型参数
class Topo
{
...
};
Topo<double, double> m;
Topo<double> n; // T2 为 int
与模板函数一样,类模板也有隐式实例化、显示实例化和显示具体化。
声明一个或多个对象,指出所需类型,编译器通过模板生成具体定义的方式是隐式实例化,在需要对象之前,不会隐式实例化:
ArrayTP<double, 30> * pt; // 只是一个指针,没有生成对象,不会隐式实例化
pt = new ArrayTP<double, 30> // 隐式实例化
使用关键字 template 并指出所需类型来声明类时,编译器会生成类的定义,这是显式实例化。这个声明必须位于模板声明定义所在的名称空间中,或者在模板类声明定义的头文件里:
// 在 ArrayTP 模板类定义的文件中
template class ArrayTP<string, 100>; // 显式实例化一个类
同函数模板一样,类模板的显式具体化也是让编译器直接生成一个特定类型的定义。显式具体化的定义格式为:
template <> class 类名<特定的类型> { ... };
template <> class Stored<const char *> { ... };
C++ 还允许部分实例化,如,可以给类型参数之一指定具体类型:
template <class T1> class Pair<T1, int> { ... }; // 生成一个 T1,int的模板类定义
template <> class Pair<int, int> { ... }; // 变成了显式具体化
如果有多个模板类定义可供选择,将根据类对象的类型选择最符合的:
Pair<double,double> p1; // 默认的模板
Pair<double, int> p2; // 部分具体化的版本
Pair<int, int> p3; // 显式具体化的版本
部分具体化也可以做其他限制:
template <class T1, class T2, class T3> class Trio { ... }; // 1
template <class T1, class T2> class Trio<T1, T2, T2> { ... }; // 部分具体化 2
template <class T1> class Trio<T1, * T1, *T1> { ... }; // 部分具体化 3
Trio<int, short, char*> t1; // 默认类模板定义
Trio<int, short> t2; // 第二个定义
Trio<char, char *, char *> t3; //第三个
模板也可以用作结构、类或模板类的成员:
template <typename T>
class beta
{
private:
template <typename V>
class hold
{
private:
...
public:
...
};
hold<T> q;
public:
template<typename U>
U blab(U u, T t) { return u / T; }
...
};
blab() 函数模板的参数类型根据调用函数时的参数确定,可以在beta内部声明模板,在外部定义模板:
template <typename T>
class beta
{
private:
template <typename V>
class hold
hold<T> q;
public:
template<typename U>
U blab(U u, T t) ;
...
};
template <typename T>
template <typename V>
class hold
{
private:
...
public:
...
};
template <typename T>
template <typename U>
U blab(U u, T t)
{ ... }
因为模板是嵌套的,所以必须有两层 template,而不能是 template <typename T, typename V>。
模板也可以包含本身就是模板的参数:
template <template <typename T> class Thing> // template <typename T> class是类型
// Thing 是参数名,同 T
class Crab
{
private:
Thing<int> a;
Thing<double> b;
...
};
这里 Thing 类必须是符合 template <typename T> 这种类型的模板类,创建Crab 对象时:
Crab<Stack> n1;
模板类也可以有友元,模板的友元分三类:非模板友元、约束模板友元。非约束模板友元。
非模板友元:
template <class T>
class Hasf
{
public:
friend void counts();
};
count() 是 Hasf 所有实例化的友元。
如果友元函数的参数是模板类,则参数必须具体化:
friend void report(Hasf &); // 不合法
friend void report(Hasf<T> &); // 合法
由于 report() 本身并不是模板,因此如果程序需要用到 Hasf<int> 和 Hasf<double> 时,需要显示定义这两种参数的 report() 函数:
void report(Hasf<int> &)
{
...
}
void report(Hasf<double> &)
{
...
}
约束模板友元:
需要三步,首先在模板类定义前声明每个友元模板函数,然后再在类内将模板函数声明为友元,最后提供模板函数定义,约束模板友元函数实际的约束是,友元的类型参数与类类型参数相同:
template <typename T> void counts();
template <typename T> void report(T &);
template <typename TT>
class HT
{
public:
friend void counts<TT>();
friend void report<>(HT<TT> &); // <>具体化,report可以通过自身函数的参数判断模板类型
... // 也可以 report<HT<TT> >(HT<TT> &)
};
template <typename T>
void counts()
{ ... }
template <typename T>
void report(T &)
{ ... }
非约束模板友元函数:
就是友元函数的类型参数与类的类型参数不同,实现非约束友元模板函数只需要在模板类内声明就行:
template <class T>
class MF
{
private:
T item;
public:
template <typename C, typename D>
friend void show(C &, D &);
};
template <typename C, typename D>
friend void show(C &, D &)
{ ... }
可以通过关键字 typedef 为模板具体化起别名:
typedef array<double, 12> arrd;
typedef array<int, 12> arri;
arri ai;
arrd ad;
也可以通过 using 别名 = 的方式起别名:
template<typename T>
using arrt = array<T,12>;
arrt<double> a1;
arrt<int> days;
arrt<string> months;
C++ 11 允许 using 别名 = 的方式为非模板起别名:
typedef const char * pc1;
using pc2 = const char*;