一些概念:
has-a关系:通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。
子对象(subobject)表示通过继承或包含添加的对象。
多重继承(multiple inheritance,MI)使用多个基类的继承。多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
虚基类:使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
C++促进代码重用的方法:
- 公有继承。用于实现is-a关系
- 使用这样的类成员:本身是另一个类的对象。这种方法称为包含(containment)、组合(composition)或层次化(layering)。
- 使用私有或保护继承.
包含和私有继承的区别:
- 包含将对象作为一个命名的成员对象添加到类中。私有继承将对象作为一个未被命名的继承对象添加到类中。
- 初始化基类组件:包含的构造函数成员初始化列表使用成员名。私有继承的构造函数成员初始化列表使用类名。
- 访问基类方法:包含使用成员对象名来调用基类的方法。私有继承使用类名和作用域解析运算符来调用基类的方法。
- 访问基类对象:包含使用成员对象名。私有继承使用强制类型转换。
- 访问基类的友元函数:包含使用成员对象名隐式调用基类的友元函数。私有继承可以通过显式地转换为基类来调用正确的函数。
- 包含能够包括多个同类的子对象。私有继承只能使用一个基类对象。
- 私有继承的派生类可以重新定义虚函数,但包含类不能。
- 通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
- 私有继承可使用using声明让基类的方法在派生类外面可用,但是using声明只适用于继承,而不适用于包含。
14.1 包含对象成员的类
对于一个学生各科考试分数的表示:
- 可以使用一个定长数组,这限制了数组的长度;
- 可以使用动态内存分配并提供大量的支持代码;
- 也可以设计一个使用动态内存分配的类来表示该数组;
- 还可以在标准C++库中查找一个能够表示这种数据的类。 (valarray类)
接口和实现:
使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提 供实现)。获得接口是is-a关系的组成部分。
使用组合(包含),类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
构造函数的成员初始化列表:
- 初始化内置类型的成员。
- 初始化派生对象的基类部分(对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数。对于成员对象,构造函数则使用成员名。)
如果不使用初始化列表语法,情况将如何呢?
C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。
valarray类
模板特性意味着声明对象时,必须指定具体的数据类型。因此,使用valarray类来声明一个对象时,需要在标识符valarray后面加上一对尖括号,并在其中包含所需的数据类型:
valarray类是由头文件valarray支持的。
- operator :访问各个元素。
- size( ):返回包含的元素数。
- sum( ):返回所有元素的总和。
- max( ):返回最大的元素。
- min( ):返回最小的元素。
#include <valarray>
double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3};
valarray<double> v1; // an array of double, size 0
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 of gpa
valarray<int> v5 = {20, 32, 17, 9}; // C++11
示例:一个包含对象成员的类
studentc.h
#ifndef PRIMERPLUS_STUDENTC_H
#define PRIMERPLUS_STUDENTC_H
#include <iostream>
#include <string>
#include <valarray>
using namespace std;
class Student
{
private:
typedef valarray<double> ArrayDb;
string name;
ArrayDb scores;
ostream & arr_out(ostream & os) const;
public:
Student() : name("Null"), scores() {}
explicit Student(const string & s) : name(s), scores() {}
explicit Student(int n) : name("Null"), scores(n) {}
Student(const string & s, int n) : name(s), scores(n) {}
Student(const 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 string & Name() const;
double & operator[](int i);
double operator[](int i) const;
friend istream & operator>>(istream & is, Student & stu);
friend istream & getline(istream & is, Student & stu);
friend ostream & operator<<(ostream & os, const Student & stu);
};
#endif //PRIMERPLUS_STUDENTC_H
studentc.cpp
#include "studentc.h"
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];
}
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;
}
}
else
os << "empty array.";
return os;
}
// friend method
// 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);
return os;
}
usestudentc.cpp
#include "studentc.h"
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";
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;
}
14.2 私有继承
- C++还有另一种实现has-a关系的途径——私有继承。
- 使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是is-a关系的一部分。
- 使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口;这是has-a关系的一部分。
- 私有基类的私有成员,只能通过私有基类的成员函数进行访问;
- 私有基类的公有成员和保护成员,只能通过私有派生类的成员函数进行访问;
私有继承访问基类的友元函数时为什么要进行强制类型转换?
- 在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。
- 如果不使用类型转换,有些(<<重载运算符)友元函数代码将与原型匹配,从而导致递归调用。
- 另一个原因是,由于这个类使用的是多重继承,编译器将无法确定应转换成哪个基类,如果两个基类都提供了函数operator<<( )。
示例:一个私有继承的示例
studenti.h
#ifndef PRIMERPLUS_STUDENTI_H
#define PRIMERPLUS_STUDENTI_H
#include <iostream>
#include <cstring>
#include <valarray>
using namespace std;
class Student : private string, private valarray<double>
{
private:
typedef valarray<double> ArrayDb;
ostream & arr_out(ostream & os) const;
public:
Student() : string("Null"), ArrayDb() {} // 调用相应基类的构造函数
explicit Student(const string & s) : string(s), ArrayDb() {}
explicit Student(int n) : string("Null"), ArrayDb(n) {}
Student(const string & s, int n) : string(s), ArrayDb(n) {}
Student(const string & s, const ArrayDb & a) : string(s), ArrayDb(a) {}
Student(const char * str, const double * pd, int n) : string(str), ArrayDb(pd, n) {}
~Student() {}
double Average() const;
double & operator[](int i);
double operator[](int i) const;
const string & Name() const;
friend istream & operator>>(istream & is, Student & stu);
friend istream & getline(istream & is, Student & stu);
friend ostream & operator<<(ostream & os, const Student & stu); // 这里注意加const
};
#endif //PRIMERPLUS_STUDENTI_H
studenti.cpp
#include "studenti.h"
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);
}
// private method
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;
}
}
else
os << "empty array.";
return os;
}
// friend method
// use string version of operator>>()
istream & operator>>(istream & is, Student & stu)
{
is >> (string &) stu;
return is;
}
// use string friend getline(ostream &, const string &)
istream & getline(istream & is, Student & stu)
{
getline(is, (string &) stu);
return is;
}
// use string version of operator<<()
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &) stu << ":\n";
stu.arr_out(os);
return os;
}
保护继承
- 保护继承是私有继承的变体。保护继承在列出基类时使用关键字 protected。
- 使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。
私有继承和保护继承之间的主要区别:(当从派生类派生出另一个类时)
- 使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;
- 使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
使基类的方法在派生类外面可用:
- 定义一个使用该基类方法的派生类方法。
- 将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。
using声明只使用成员名——没有圆括号、函数特征标和返回类型。
using声明只适用于继承,而不适用于包含。
// 定义一个使用该基类方法的派生类方法。
double Student::sum() const // public Student method
{
return std::valarray<double>::sum(); // use privately-inherited method
}
// 方法二
class Student : private std::string, private std::valarray<double>
{
...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
using std::valarray<double>::operator[];
...
};
14.3 多重继承
MI可能会给程序员带来很多新问题。其中两个主要的问题是:
- 从两个不同的基类继承同名方法;
- 从两个或更多相关基类那里继承同一个类的多个实例。
示例:一个多重继承的示例
例:先定义一个抽象基类Worker,并使用它派生出Waiter类和Singer类。然后,便可以使用MI从Waiter类和Singer类派生出SingingWaiter类。
假设首先从Singer和Waiter公有派生出SingingWaiter:因为Singer和Waiter都继承了一个Worker组件,因此SingingWaiter 将包含两个Worker组件。
把基类指针设置为派生对象中的基类对象的地址。将派生类对象的地址赋给基类指针。使用类型转换来指定对象
Worker * pw1 = (Waiter *) &ed; // the Worker in Waiter
Worker * pw2 = (Singer *) &ed; // the Worker in Singer
C++引入多重继承的同时,引入了一种新技术——虚基类(virtual base class),使MI成为可能。
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
通过在类声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类(virtual和public的次序无关紧要)
class Singer : virtual public Worker {...};
class Waiter : public virtual Worker {...};
class SingingWaiter: public Singer, public Waiter {...};
如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
C++在基类是虚的时,禁止信息通过中间类自动传递给基类。然而,编译器必须在构造派生对象之前构造基类对象组件;在上述情况下,编译器将使用Worker的默认构造函数。
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk,p), Singer(wk,v) {}
多重继承调用哪个基类的方法:可以采用模块化的方式,并使用作用域解析运算符来澄清编程者的意图。
void Worker::Data() const
{
cout << "Name: " << fullname << "\n";
cout << "Employee ID: " << id << "\n";
}
void Waiter::Data() const
{
cout << "Panache rating: " << panache << "\n";
}
void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << "\n";
}
void SingingWaiter::Data() const
{
Singer::Data();
Waiter::Data();
}
void SingingWaiter::Show() const
{
cout << "Category: singing waiter\n";
Worker::Data();
Data();
}
最后的例程:
workermi.h
#ifndef _PRIMERPLUS_WORKERMI_H
#define _PRIMERPLUS_WORKERMI_H
#include <cstring>
using namespace std;
class Worker // an abstract base class
{
private:
std::string fullname;
long id;
protected:
virtual void Data() const;
virtual void Get();
public:
Worker() : fullname("no one"), id(0L) {}
Worker(const std::string & s, long n)
: fullname(s), id(n) {}
virtual ~Worker() = 0; // pure virtual function
virtual void Set() = 0;
virtual void Show() const = 0;
};
class Waiter : virtual public Worker
{
private:
int panache;
protected:
void Data() const;
void Get();
public:
Waiter() : Worker(), panache(0) {}
Waiter(const std::string & s, long n, int p = 0)
: Worker(s, n), panache(p) {}
Waiter(const Worker & wk, int p = 0)
: Worker(wk), panache(p) {}
void Set();
void Show() const;
};
class Singer : virtual public Worker
{
protected:
enum {other, alto, contralto, soprano,
bass, baritone, tenor};
enum {Vtypes = 7};
void Data() const;
void Get();
private:
static char *pv[Vtypes]; // string equivs of voice types
int voice;
public:
Singer() : Worker(), voice(other) {}
Singer(const std::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;
};
// multiple inheritance
class SingingWaiter : public Singer, public Waiter
{
protected:
void Data() const;
void Get();
public:
SingingWaiter() {}
SingingWaiter(const std::string & s, long n, int p = 0,
int v = other)
: Worker(s,n), Waiter(s, n, p), Singer(s, n, v) {}
SingingWaiter(const Worker & wk, int p = 0, int v = other)
: Worker(wk), Waiter(wk,p), Singer(wk,v) {}
SingingWaiter(const Waiter & wt, int v = other)
: Worker(wt),Waiter(wt), Singer(wt,v) {}
SingingWaiter(const Singer & wt, int p = 0)
: Worker(wt),Waiter(wt,p), Singer(wt) {}
void Set();
void Show() const;
};
#endif
workermi.cpp
// workermi.cpp -- working class methods with MI
#include <iostream>
#include "workermi.h"
using namespace std;
// Worker methods
Worker::~Worker() { }
// protected methods
void Worker::Data() const
{
cout << "Name: " << fullname << endl;
cout << "Employee ID: " << id << endl;
}
void Worker::Get()
{
getline(cin, fullname);
cout << "Enter worker's ID: ";
cin >> id;
while (cin.get() != '\n')
continue;
}
// Waiter methods
void Waiter::Set()
{
cout << "Enter waiter's name: ";
Worker::Get();
Get();
}
void Waiter::Show() const
{
cout << "Category: waiter\n";
Worker::Data();
Data();
}
// protected methods
void Waiter::Data() const
{
cout << "Panache rating: " << panache << endl;
}
void Waiter::Get()
{
cout << "Enter waiter's panache rating: ";
cin >> panache;
while (cin.get() != '\n')
continue;
}
// Singer methods
char * Singer::pv[Singer::Vtypes] = {"other", "alto", "contralto",
"soprano", "bass", "baritone", "tenor"};
void Singer::Set()
{
cout << "Enter singer's name: ";
Worker::Get();
Get();
}
void Singer::Show() const
{
cout << "Category: singer\n";
Worker::Data();
Data();
}
// protected methods
void Singer::Data() const
{
cout << "Vocal range: " << pv[voice] << endl;
}
void Singer::Get()
{
cout << "Enter number for singer's vocal range:\n";
int i;
for (i = 0; i < Vtypes; i++)
{
cout << i << ": " << pv[i] << " ";
if ( i % 4 == 3)
cout << endl;
}
if (i % 4 != 0)
cout << '\n';
while (cin >> voice && (voice < 0 || voice >= Vtypes) )
cout << "Please enter a value >= 0 and < " << Vtypes << endl;
while (cin.get() != '\n')
continue;
}
// SingingWaiter methods
void SingingWaiter::Data() const
{
Singer::Data();
Waiter::Data();
}
void SingingWaiter::Get()
{
Waiter::Get();
Singer::Get();
}
void SingingWaiter::Set()
{
cout << "Enter singing waiter's name: ";
Worker::Get();
Get();
}
void SingingWaiter::Show() const
{
cout << "Category: singing waiter\n";
Worker::Data();
Data();
}
workmi.cpp
// workmi.cpp -- multiple inheritance
// compile with workermi.cpp
#include <iostream>
#include <cstring>
#include "workermi.h"
const int SIZE = 5;
int main()
{
using std::cin;
using std::cout;
using std::endl;
using std::strchr;
Worker * lolas[SIZE];
int ct;
for (ct = 0; ct < SIZE; ct++)
{
char choice;
cout << "Enter the employee category:\n"
<< "w: waiter s: singer "
<< "t: singing waiter q: quit\n";
cin >> choice;
while (strchr("wstq", choice) == NULL)
{
cout << "Please enter a w, s, t, or q: ";
cin >> choice;
}
if (choice == 'q')
break;
switch(choice)
{
case 'w': lolas[ct] = new Waiter;
break;
case 's': lolas[ct] = new Singer;
break;
case 't': lolas[ct] = new SingingWaiter;
break;
}
cin.get();
lolas[ct]->Set();
}
cout << "\nHere is your staff:\n";
int i;
for (i = 0; i < ct; i++)
{
cout << endl;
lolas[i]->Show();
}
for (i = 0; i < ct; i++)
delete lolas[i];
cout << "Bye.\n";
// cin.get();
// cin.get();
return 0;
}
混合使用虚基类和非虚基类:
- 如果基类是虚基类,派生类将包含基类的一个子对象;
- 如果基类不是虚基类,派生类将包含多个子对象。
使用虚基类将改变C++解析二义性的方式:
- 如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。
- 如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于(dominates)其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。
- 一个成员名如何优先于另一个成员名呢? 派生类中的名称优先于直接或间接祖先类中的相同名称。
MI小结
不使用虚基类的MI:
- 如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。
- 如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径 分别继承非虚基类的一个实例。
使用虚基类的MI:
- 当派生类使用关键字virtual来指示派生时,基类就成为虚基类。
- 从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。
- 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;
- 通过优先规则解决名称二义性。
14.4 类模板
- 每个函数头都将以相同的模板声明打头
template <class Type>
// type泛型标识符,称为类型参数 - 还需将类限定符从Stack::改为
Stack<Type>::
template <class Type> // or template <typename Type>
bool Stack<Type>::push(const Type & item)
{
...
}
// 类模板实例化
Stack<int> kernels; // create a stack of ints
Stack<string> colonels; // create a stack of string objects
- 类模板和成员函数模板不是类和成员函数定义,它们是C++编译器指令,说明了如何生成类和成员函数定义。
- 不能将模板成员函数放在独立的实现文件中。
- 由于模板不是函数,它们不能单独编译。 模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
- 仅在程序包含模板并不能生成模板类,而必须请求实例化。
- 需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。
示例:一个出栈入栈的类模板例程
stacktp.h
// stacktp.h -- a stack template
#ifndef PRIMERPLUS_SATCKTP_H
#define PRIMERPLUS_SATCKTP_H
template <class Type>
class Stack // 什么时候对类实例化,什么时候Type变成需要的
{
private:
enum {MAX = 10}; // constant specific to class
Type items[MAX]; // holds stack items
int top; // index for top stack item
public:
Stack();
bool isempty();
bool isfull();
bool push(const Type & item); // add item to stack
bool pop(Type & item); // pop top into 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;
}
#endif //PRIMERPLUS_STCKTP_H
stacktem.cpp
// stacktem.cpp -- testing the template stack class
#include <iostream>
#include <string>
#include <cctype>
#include "stacktp.h"
using std::cin;
using std::cout;
int main()
{
Stack<std::string> st; // create an empty stack
char ch;
std::string po;
cout << "Please enter A to add a purchase order,\n"
<< "P to process a PO, or Q to quit.\n";
while (cin >> ch && std::toupper(ch) != 'Q')
{
while (cin.get() != '\n')
continue;
if (!std::isalpha(ch))
{
cout << '\a';
continue;
}
switch(ch)
{
case 'A':
case 'a': cout << "Enter a PO number to add: ";
cin >> po;
if (st.isfull())
cout << "stack already full\n";
else
st.push(po);
break;
case 'P':
case 'p': if (st.isempty())
cout << "stack already empty\n";
else {
st.pop(po);
cout << "PO #" << po << " popped\n";
break;
}
}
cout << "Please enter A to add a purchase order,\n"
<< "P to process a PO, or Q to quit.\n";
}
cout << "Bye\n";
// cin.get();
// cin.get();
return 0;
}
示例:允许指定数组大小的简单数组模板
- 一种方法是在类中使用动态数组和构造函数参数来提供元素数目。
- 另一种方法是使用模板参数(表达式参数)来提供常规数组的大小,C++11新增的模板array就是这样做的。
- 表达式参数可以是整型、枚举、引用或指针。 如
double m
是不合法的 - 模板代码不能修改参数的值,也不能使用参数的地址。 如n++, &n
- 实例化模板时,用作表达式参数的值必须是常量表达式。
template <class T, int n> // T为类型参数,n为int类型(非类型/表达式参数)
class ArrayTP
{
...
};
// 定义一个名为ArrayTP<double, 12>的类,并创建一个类型为ArrayTP<double, 12>的eggweight对象。
ArrayTP<double, 12> eggweights;
构造函数方法和表达式参数方法的区别:
- 构造函数方法使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈。
- 表达式参数的优点:执行速度将更快,尤其是在使用了很多小型数组时。
- 表达式参数的缺点:每种数组大小都将生成自己的模板。
- 构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
// 生成两个独立的类声明
ArrayTP<double, 12> eggweights;
ArrayTP<double, 13> donuts;
// 只生成一个类声明,并将数组大小信息传递给类的构造函数
Stack<int> eggs(12);
Stack<int> dunkers(13);
arraytp.h
//arraytp.h -- Array Template
#ifndef PRIMERPLUS_ARRAYTP_H
#define PRIMERPLUS_ARRAYTP_H
#include <iostream>
#include <cstdlib>
template <class T, int n> // T为类型参数,n为int类型(非类型/表达式参数)
class ArrayTP
{
private:
T ar[n];
public:
ArrayTP() {};
explicit ArrayTP(const T & v);
virtual T & operator[](int i);
virtual T operator[](int i) const;
};
template <class T, int n>
ArrayTP<T,n>::ArrayTP(const T & v)
{
for (int i = 0; i < n; i++)
ar[i] = v;
}
template <class T, int n>
T & ArrayTP<T,n>::operator[](int i)
{
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i
<< " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
template <class T, int n>
T ArrayTP<T,n>::operator[](int i) const
{
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i
<< " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
#endif //PRIMERPLUS_ARRAYTP_H
模板多功能性
模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。
template <typename T> // or <class T>
class Array
{
private:
T entry;
...
};
template <typename Type>
class GrowArray : public Array<Type> {...}; // inheritance
template <typename Tp>
class Stack
{
Array<Tp> ar; // use an Array<> as a component
...
};
...
Array < Stack<int> > asi; // an array of stacks of int
// 上面一行代码C++98要求>>之间有空格,C++11不要求
1、递归使用模板
在模板语法中,维的顺序与等价的二维数组相反。
// 以下两条命令等价
ArrayTP< ArrayTP<int,5>, 10> twodee;
int twodee[10][5];
twod.cpp
// twod.cpp -- making a 2-d array
#include <iostream>
#include "arraytp.h"
int main(void)
{
using std::cout;
using std::endl;
ArrayTP<int, 10> sums;
ArrayTP<double, 10> aves;
ArrayTP< ArrayTP<int,5>, 10> twodee;
int i, j;
for (i = 0; i < 10; i++)
{
sums[i] = 0;
for (j = 0; j < 5; j++)
{
twodee[i][j] = (i + 1) * (j + 1);
sums[i] += twodee[i][j];
}
aves[i] = (double) sums[i] / 10;
}
for (i = 0; i < 10; i++)
{
for (j = 0; j < 5; j++)
{
cout.width(2);
cout << twodee[i][j] << ' ';
}
cout << ": sum = ";
cout.width(3);
cout << sums[i] << ", average = " << aves[i] << endl;
}
cout << "Done.\n";
// std::cin.get();
return 0;
}
out:
1 2 3 4 5 : sum = 15, average = 1.5
2 4 6 8 10 : sum = 30, average = 3
3 6 9 12 15 : sum = 45, average = 4.5
4 8 12 16 20 : sum = 60, average = 6
5 10 15 20 25 : sum = 75, average = 7.5
6 12 18 24 30 : sum = 90, average = 9
7 14 21 28 35 : sum = 105, average = 10.5
8 16 24 32 40 : sum = 120, average = 12
9 18 27 36 45 : sum = 135, average = 13.5
10 20 30 40 50 : sum = 150, average = 15
Done.
2、使用多个类型参数
模板可以包含多个参数类型。假设希望类可以保存两种值,则可以创建并使用Pair模板来保存两个不同的值(标准模板库提供了类似的模板,名为pair)。
pairs.cpp
// 方法first( ) const和second( ) const报告存储的值,
// 由于这两个方法返回Pair数据成员的引用,因此让您能够通过赋值重新设置存储的值。
// pairs.cpp -- defining and using a Pair template
#include <iostream>
#include <string>
template <class T1, class T2>
class Pair
{
private:
T1 a;
T2 b;
public:
T1 & first();
T2 & second();
T1 first() const { return a; }
T2 second() const { return b; }
Pair(const T1 & aval, const T2 & bval) : a(aval), b(bval) { }
Pair() {}
};
template<class T1, class T2>
T1 & Pair<T1,T2>::first()
{
return a;
}
template<class T1, class T2>
T2 & Pair<T1,T2>::second()
{
return b;
}
int main()
{
using std::cout;
using std::endl;
using std::string;
Pair<string, int> ratings[4] =
{
Pair<string, int>("The Purpled Duck", 5),
Pair<string, int>("Jaquie's Frisco Al Fresco", 4),
Pair<string, int>("Cafe Souffle", 5),
Pair<string, int>("Bertie's Eats", 3)
};
int joints = sizeof(ratings) / sizeof (Pair<string, int>);
cout << "Rating:\t Eatery\n";
for (int i = 0; i < joints; i++)
cout << ratings[i].second() << ":\t "
<< ratings[i].first() << endl;
cout << "Oops! Revised rating:\n";
ratings[3].first() = "Bertie's Fab Eats";
ratings[3].second() = 6;
cout << ratings[3].second() << ":\t "
<< ratings[3].first() << endl;
// std::cin.get();
return 0;
}
out:
Rating: Eatery
5: The Purpled Duck
4: Jaquie's Frisco Al Fresco
5: Cafe Souffle
3: Bertie's Eats
Oops! Revised rating:
6: Bertie's Fab Eats
3、默认类型模板参数
template <class T1, class T2 = int> class Topo {...};
Topo<double, double> m1; // T1 is double, T2 is double
Topo<double> m2; // T1 is double, T2 is int
虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
模板的具体化
模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
通俗说法:模板类并没有生成类的定义,只是说了以下这个类的架子。模板是用于生成类的方式。具体化是用特定的类型对模板进行填充。
1、隐式实例化(用的时候再准备)
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。
编译器在需要对象之前,不会生成类的隐式实例化:
ArrayTP<int, 100> stuff; // 隐式实例化
ArrayTP<double, 30> * pt; // 此时还没有使用创建对象,没有分配内存,只是创建了一个指针指向这样的对象
pt = new ArrayTP<double, 30>; // 该语句导致编译器生成类定义,并根据该定义创建一个对象。
2、显式实例化(没用的时候就准备)
template class ArrayTP<string, 100>; _// generate ArrayTP<string, 100> class_
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。
虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。
更快速,省去运行阶段创建这个类的时间。
3、显示具体化(全部特殊处理)
template <> class Classname<specialized-type-name> { ... };
已经有了类的定义,对其中的一些进行修改,重新进行类的实现。
有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。
4、部分具体化(部分特殊处理)
关键字template后面的<>声明的是没有被具体化的类型参数。
示例:一个模板具体化的例程
#include <iostream>
using namespace std;
template<class T1, class T2>
class A
{
public:
void show();
};
template<class T1, class T2>
void A<T1, T2>::show()
{
cout << "this is a general definition." << endl;
}
// 显式实例化,A<double, double>这个类在内存中已经存在
template class A<double, double>;
// 显示具体化,不再使用通用的方法,所有类型都指定为某一特定的类型
template<>
class A<int, int>
{
public:
void show();
};
// template<> 不用这一行语句
void A<int, int>::show()
{
cout << "explicit specialization." << endl;
}
// 部分具体化
template<class T1> // <>中放没有具体化的,T1泛型,T2指定为某种特殊的类型
class A<T1, int>
{
public:
void show();
};
template<class T1> // 注意这里
void A<T1, int>::show()
{
cout << "partial specialization." << endl;
}
int main(void)
{
A<char, char> a1; // 隐式实例化
a1.show();
A<int, int> a2; // 显示具体化过
a2.show();
A<double, int> a3; // 调用部分具体化
a3.show();
return 0;
}
示例:模板类 和 模板函数 做 成员变量
模板可用作结构、类或模板类的成员。要完全实现STL的设计,必须使用这项特性。
// 模板类和模板函数做成员变量
#include <iostream>
using namespace std;
template<class T>
class beta
{
private:
template<class V> // beta模板类里面的成员变量是一个hold模板类
class hold
{
private:
V val;
public:
hold(V v=0):val(v) {} // 内联函数,构造函数
void show() const {cout << val << endl;}
V Value() const {return val;}
};
hold<T> q;
hold<int> n; // 基于int类型的hold对象
public:
beta(T t, int i):q(t), n(i) {}
void Show() const { q.show(); n.show(); }
template<class U> // 定义模板函数
U blab(U u, T t) {return (q.Value() + n.Value()) * u / t;}
// 注意q.Value()后面加括号,因为调用的是一个函数。
};
int main(void)
{
beta<double> guy(1.2, 3);
guy.Show();
cout << guy.blab(10, 6.1) << endl; // (f+int)*int/f,返回int类型
cout << guy.blab(10.0, 6.1) << endl;// (f+int)*f/f,返回f类型
return 0;
}
示例:模板类做参数
模板可以包含类型参数(如typename T)和非类型参数(如int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL。
template <typename T>
class King {...};
template <template <typename T> class Thing>
class Crab{...};
Crab<King> legs;
// 模板类做参数
#include <iostream>
#include "stacktp.h"
using namespace std;
template <template <class T> class Thing> // 参数Thing是一个模板类型
class Crab
{
private:
Thing<int> s1; // Ting<int>类的对象
Thing<double> s2; // Ting<double>类的对象
public:
Crab() {}
bool push(int a, double x) {return s1.push(a) && s2.push(x);}
bool pop(int & a, double & x) {return s1.pop(a) && s2.pop(x);}
};
int main(void)
{
Crab<Stack> nebula; // 包含两个对象Stack<int>和Stack<double>,包含两个栈
int ni;
double nb;
cout << "Enter int double pairs, such as 4 3.5 (0 to end):\n";
while (cin >> ni && ni > 0 && cin >> nb && nb > 0)
{
if (!nebula.push(ni, nb))
break;
}
while (nebula.pop(ni, nb))
cout << ni << ", " << nb << endl;
cout << "Done.\n";
return 0;
}
非模板友元;将一个常规的函数声明为友元函数。
友元函数可以访问全局对象;可以使用全局指针访问非全局对象;可以创建自己的对象;可以访问独立于对象的模板类的静态数据成员。
// 模板类中使用非模板友元函数
template <class T>
class HasFriend
{
public:
friend void counts(); // friend to all HasFriend instantiations
friend void report(HasFriend<T> &); // 要提供模板类参数,必须指明具体化。
...
};
// 友元声明的格式如下:
// 带HasFriend<int>参数的report( )将成为HasFriend<int>类的友元。
class HasFriend<int>
{
friend void report(HasFriend<int> &); // bound template friend
...
};
// 友元函数的定义:
// report( )本身并不是模板函数,而只是使用一个模板作参数。这意味着必须为要使用的友元定义显式具体化:
void report(HasFriend<int> & hf) {...}; // explicit specialization for int
void report(HasFriend<double> & hf) {...};
// 模板类中使用非模板友元函数
#include <iostream>
using namespace std;
template <typename T>
class HasFriend
{
private:
T item;
static int ct; // HasFriend<int> a1, a2; a1和a2公用一个ct变量
public:
HasFriend(const T & i):item(i) {ct++;}
~HasFriend() {ct--;}
friend void counts(); // display ct
friend void reports(HasFriend<T> &); // display item
};
template <typename T> // 这里要注意
int HasFriend<T>::ct = 0;
void counts()
{
cout << "int count: " << HasFriend<int>::ct << ";";
cout << " double count: " << HasFriend<double>::ct << ".\n";
}
void reports(HasFriend<int> & hf)
{
cout << "HasFriend<int>: " << hf.item << endl;
}
void reports(HasFriend<double> & hf)
{
cout << "HasFriend<double>: " << hf.item << endl;
}
int main(void)
{
counts();
HasFriend<int> hfi1(10);
counts();
HasFriend<int> hfi2(20);
counts();
HasFriend<double> hfi3(30.5);
counts();
reports(hfi1);
reports(hfi2);
reports(hfi3);
return 0;
}
约束和非约束模板 友元函数
约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;
约束模板友元函数是在类外面声明的模板的具体化。
// 首先,在类定义的前面声明每个模板函数。
template <typename T> void counts();
template <typename T> void report(T &);
// 然后,在函数中再次将模板声明为友元。
template <typename TT>
class HasFriendT
{
...
friend void counts<TT>(); // counts( )函数没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化。
friend void report<>(HasFriendT<TT> &); // report<>可以为空,可以从函数参数推断出模板类型参数
};
// 然后,函数定义
template <typename T>
void counts() {...}
template <typename T>
void report(T & hf) {...}
// 最后,在main()中调用
counts<int>();
非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。
友元模板类型参数与模板类类型参数是不同的。
// 非约束模板友元
// manyfrnd.cpp -- unbound template friend to a template class
#include <iostream>
using std::cout;
using std::endl;
template <typename T>
class ManyFriend
{
private:
T item;
public:
ManyFriend(const T & i) : item(i) {}
template <typename C, typename D> friend void show2(C &, D &);
// 在类内部声明,非约束友元,友元模板类型参数与模板类类型参数是不同的
};
template <typename C, typename D> void show2(C & c, D & d)
{
cout << c.item << ", " << d.item << endl;
}
int main()
{
ManyFriend<int> hfi1(10);
ManyFriend<int> hfi2(20);
ManyFriend<double> hfdb(10.5);
cout << "hfi1, hfi2: ";
show2(hfi1, hfi2); // hfi1, hfi2: 10, 20
// 具体化是void show2<ManyFriend<int> &, ManyFriend<int> &>(ManyFriend<int> & c, ManyFriend<int> & d);
cout << "hfdb, hfi2: ";
show2(hfdb, hfi2); // hfdb, hfi2: 10.5, 20
// std::cin.get();
return 0;
}
模板别名(C++11)
// 可使用typedef为模板具体化指定别名:
typedef std::array<std::string, 12> arrst;
arrst months; // months is type std::array<std::string, 12>
// 使用模板提供一系列别名:
template<typename T>
using arrtype = std::array<T,12>; // template to create multiple aliases
arrtype<double> gallons; // gallons is type std::array<double, 12>
// arrtype<T>表示类型std::array<T, 12>
// C++11允许将语法using =用于非模板。用于非模板时,这种语法与常规typedef等价:
typedef const char * pc1; // typedef syntax
using pc2 = const char *; // using = syntax
typedef const int *(*pa1)[10]; // typedef syntax
using pa2 = const int *(*)[10]; // using = syntax
14.5 总结
- C++提供了几种重用代码的手段。第13章介绍的公有继承能够建立is-a关系,这样派生类可以重用基类的代码。私有继承和保护继承也使得能够重用基类的代码,但建立的是has-a关系。使用私有继承时,基类的公有成员和保护成员将成为派生类的私有成员;使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员。无论使用哪种继承,基类的公有接口都将成为派生类的内部接口。这有时候被称为继承实现,但并不继承接口,因为派生类对象不能显式地使用基类的接口。因此,不能将派生对象看作是一种基类对象。由于这个原因,在不进行显式类型转换的情况下,基类指针或引用将不能指向派生类对象。
- 还可以通过开发包含对象成员的类来重用类代码。这种方法被称为包含、层次化或组合,它建立的也是has-a关系。与私有继承和保护继承相比,包含更容易实现和使用,所以通常优先采用这种方式。然而,私有继承和保护继承比包含有一些不同的功能。例如,继承允许派生类访问基类的保护成员;还允许派生类重新定义从基类那里继承的虚函数。因为包含不是继承,所以通过包含来重用类代码时,不能使用这些功能。另一方面,如果需要使用某个类的几个对象,则用包含更适合。例如,State类可以包含一组County对象。
- 多重继承(MI)使得能够在类设计中重用多个类的代码。私有MI或保护MI建立has-a关系,而公有MI建立is-a关系。MI会带来一些问题,即多次定义同一个名称,继承多个基类对象。可以使用类限定符来解决名称二义性的问题,使用虚基类来避免继承多个基类对象的问题。但使用虚基类后,就需要为编写构造函数初始化列表以及解决二义性问题引入新的规则。
- 类模板使得能够创建通用的类设计,其中类型(通常是成员类型)由类型参数表示。典型的模板如下:
template <class T>
class Ic
{
T v;
...
public:
Ic(const T & val) : v(val) { }
...
};
其中,T是类型参数,用作以后将指定的实际类型的占位符(这个参数可以是任意有效的C++名称,但通常使用T和Type)。在这种环境下,也可以使用typename代替class:
template <typename T> // same as template <class T>
class Rev {...} ;
- 类定义(实例化)在声明类对象并指定特定类型时生成。例如,下面的声明导致编译器生成类声明,用声明中的实际类型short替换模板中的所有类型参数T:
class Ic<short> sic; _// implicit instantiation_
这里,类名为Ic,而不是Ic。Ic称为模板具体化。具体地说,这是一个隐式实例化。
使用关键字template声明类的特定具体化时,将发生显式实例化:
template class IC<int>; _// explicit instantiation_
在这种情况下,编译器将使用通用模板生成一个int具体化——Ic<int>
,虽然尚未请求这个类的对象。
可以提供显式具体化——覆盖模板定义的具体类声明。方法是以template<>打头,然后是模板类名称,再加上尖括号(其中包含要具体化的类型)。例如,为字符指针提供专用Ic类的代码如下:
template <> class Ic<char *>.
{
char * str;
...
public:
Ic(const char * s) : str(s) { }
...
};
这样,下面这样的声明将为chic使用专用定义,而不是通用模板:
class Ic<char *> chic;
- 类模板可以指定多个泛型,也可以有非类型参数:
template <class T, class TT, int n>
class Pals {...};
下面的声明将生成一个隐式实例化,用double代替T,用string代替TT,用6代替n:
Pals<double, string, 6> mix;
- 类模板还可以包含本身就是模板的参数:
template < template <typename T> class CL, typename U, int z>
class Trophy {...};
其中z是一个int值,U为类型名,CL为一个使用template<typename, T>声明的类模板。
- 类模板可以被部分具体化:
第一个声明为两个类型相同,且n的值为6的情况创建了一个具体化。
同样,第二个声明为n等于100的情况创建一个具体化;
第三个声明为第二个类型是指向第一个类型的指针的情况创建了一个具体化。
template <class T> Pals<T, T, 10> {...};
template <class T, class TT> Pals<T, TT, 100> {...};
template <class T, int n> Pals <T, T*, n> {...};
- 模板类可用作其他类、结构和模板的成员。
- 所有这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。