第 14 章 C++中的代码重用
C++的主要目标:促进代码重用。通过公有继承来实现这种目标机制。
14.1. 包含对象成员的类
通过动态内存分配来表示数组,使得数组长度不被限制。
14.1.1 valarray类简介
valarrat类由头文件 valarray 支持。valarray被定义为一个模板类,用于处理数值(它支持->将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作)。
模板特性意味着声明对象时,必须指定具体的数据类型。所以。valarray类来声明一个对象时,需要在标识符valarray
后面加上一对尖括号
,并在其中包含所需的数据类型。
double gpa[5] = {3.1,3.5,3.8,2.9,3.3}
valarray<double> v1; // double类型数组,大小为0
valarray<int> v2(8); // 8个int类型的元素数组
valarray<int> v3(10,8); // 8个int类型的元素数组,每个值初始化为8
valarray<double> v4(gpa,4); // 4个元素数组,初始化
valarray<int> v5 = {20,32,17,9}; // 在C++11中,用来初始化列表
可以创建长度为0的数组、指定长度的空数组、所有元素被初始化为指定值的数组、用常规数组中的值来进行初始化的数组。
valarray类的一些方法:
operator[]()
:访问各个元素size()
:返回包含的元素个数sum()
:返回所有元素的总和max()
:返回最大的元素min()
:返回最小的元素
14.1.2 Student类的设计
学生和其它类之间不是 is-a关系而是 has-a关系。学生有姓名和一组考试分数。
has-a 关系的C++技术是组合(包含),即创建一个包含其他类对象的类。
class Student
{
// 声明私有数据成员
private:
string name; // 使用string对象
valarray<double> scores; // 使用 valarray<double> 对象
...
// 成员函数可以使用 公有接口 来修改name和scores对象。但在类的外部不可行(通过Student类的公有接口来访问)
}
14.1.3 接口和实现
- 使用公有继承时,类可以继承接口。还会有实现(基类的纯虚函数提供接口,但不提供实现)
获得接口
是is-a
关系的组成部分。
- 使用
组合
,类可以获得实现,但不能获得接口。不继承接口
是has-a
关系的组成部分has-a
关系上,类对象不能自动获得被包含对象的接口。
- 对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。例如,string类将+运算符重载为将两个字符串连接起来;但从概念上说,将两个Student对象串接起来是没有意义的。
14.1.4 Student类示例
1、头文件
#ifndef TEST_STUDENT_C_H
#define TEST_STUDENT_C_H
#include <iostream>
#include <string>
#include <valarray>
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);
friend std::istream &getline(std::istream &is,Student &stu);
//output
friend std::ostream &operator<<(std::ostream &os,const Student &stu);
};
#endif //TEST_STUDENT_C_H
注意关键字explicit的用法:
explicit Student(const std::string &s):name(s),scores(){}
explicit Student(int n):name("Nully"),scores(n){}
如果没有关键字,上述声明均为一个参数的构造函数,前面所讲,它们会成为隐式转换函数;对于上述的第二个构造函数,第一个参数表示的是数组元素的个数,而不是数组的值,因此将一个构造函数用作int到Student的转换函数是没有意义的。
Student doh("Homer",10); //store "Homer",create array of 10 elements
doh = 5;//reset name to "Nully",reset to empty array of 5 elements
以上的例子就是通过使用关键字explicit来避免一些“隐形”错误。
14.1.5 C++和约束
C++包含让程序员限制程序结构的特性 ---- 使用 explicit 防止单参数构造函数的隐式转换。使用 const限制方法修改数据等。
原因:在编译阶段出现错误优于在运行阶段出现错误。
用成员初始化列表语法初始化内置数据类型的成员:
Queue::Queue(int qs):qsize(qs){...} //initialize qsize to qs
还可以初始化派生对象的基类部分:(对于继承的对象,构造函数在成员初始化列表汇总使用类名来调用特定的基类构造函数)
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)。
2、源文件
#include "student_C.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];
}
double Student::operator[](int i) const{ //这个是写入,确保不更改对象内容
return scores[i];
}
//private method
ostream &Student::arr_out(std::ostream &os) const {
int i = 0;
int lim = scores.size();
if (lim > 0){
for (int j = 0; j < lim; ++j) {
os << scores[j] << " ";
if (j % 5 == 4){
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;
}
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;
}
3、执行文件
下面测试上面新写的Student类。该程序使用一个只包含Student对象的数组,其中每个对象保存5个考试成绩。
#include <iostream>
#include "student_C.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 (int j = 0; j < pupils; ++j) {
set(ada[j],quizzes);
}
cout << "\nStudent List:\n";
for (int j = 0; j < pupils; ++j) {
cout << ada[j].Name() << endl;
}
cout << "\nResults:";
for (int j = 0; j < pupils; ++j) {
cout << endl << ada[j];
cout << "average:" << ada[j].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;
}
}
输出:
Please enter the student's name:Gil Bayts
Please enter 5 quiz scores:
92 94 96 93 95
Please enter the student's name:Pat Roone
Please enter 5 quiz scores:
83 89 72 78 95
Please enter the student's name:Fleur O'Day
Please enter 5 quiz scores:
92 89 96 74 64
Student List:
Gil Bayts
Pat Roone
Fleur O'Day
Results:
Scores forGil Bayts:
92 94 96 93 95
average:94
Scores forPat Roone:
83 89 72 78 95
average:83.4
Scores forFleur O'Day:
92 89 96 74 64
average:83
Done.
14.2 私有继承
实现 has-a
关系的另外一个途径 ---- 私有继承
。
使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。意味着基类方法将不会成为派生对象公有接口的一部分,但可在派生类的成员函数中使用。
派生类不基础基类的接口。
14.2.1 私有继承和组合的异同
- 相同点
- 私有继承
- 获得实现,但不获得接口。
- 将对象作为一个未被命名的继承对象添加到类中。
- 组合
- 将对象作为一个命名的成员对象添加到类中。
- 私有继承
- 不同点
- 类将继承实现
- 例如:如果从String类派生出Student类,后者将有一个String类组件,可用于保存字符串。另外,Student方法可以使用String方法来访问String组件。
- 组合提供两个被显式命名的对象成员。
- 私有继承提供两个无名称的子对象成员。
- 组合使用对象名来调用方法
- 私有继承使用类名 和 作用域解析运算符来调用方法。
- 类将继承实现
要进行私有继承,使用关键字 private
而不是 public
来定义类。
14.2.2 Student类的示例(新版本)
要进行私有继承,要使用关键字private而不是public来定义类(实际上,private是默认值,因此可以省略)。Student类应从两个类派生而来,因此语句如下:
class Student:private std::string,private std::valarray<double>{
public:
....
};
新的Student类不需要私有数据,因为两个基类已经提供了所需要的数据成员。
隐式继承组件而不是成员对象,会影响代码的编写。所以使用公有继承。
// 使用组合的构造函数,直接使用成员名来标识构造函数
Student(const char * str, const double * pd , int n)
: name(str),scores(pd,n) {}
// 使用继承类,使用类名而不是成员名来标识构造函数
Student(const char * str, const double *pd, int n)
: std::string(str), ArrayDb(pd,n) {} // ArrayDb 是 std::valarray<double> 的别名
下面列出了新的类定义,唯一不同的地方是,省略了显式对象名称,在内联构造函数使用了类名。
#ifndef TEST_STUDENT_C_H
#define TEST_STUDENT_C_H
#include <iostream>
#include <string>
#include <valarray>
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():name("Null Student"),scores(){}
explicit Student(const std::string &s):name(s),ArrayDb(){}
explicit Student(int n):name("Nully"),ArrayDb(n){}
Student(const std::string &s,int n):name(s),ArrayDb(n){}
Student(const std::string &s,const ArrayDb &a):name(s),ArrayDb(a){}
Student(const char *str,const double *pd,int n):name(str),ArrayDb(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);
friend std::istream &getline(std::istream &is,Student &stu);
//output
friend std::ostream &operator<<(std::ostream &os,const Student &stu);
};
#endif //TEST_STUDENT_C_H
14.2.3 访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类的方法。
- 在私有继承使得使用
类名
和作用域解析运算符
来调用基类的方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gI9cDVhq-1665738289865)(/Users/mac/Library/Application Support/typora-user-images/image-20220926214136587.png)]
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}
- 组合是直接使用对象名来调用方法。
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}
14.2.4 访问基类对象
Student类的包含版本实现了Name方法,返回string对象成员:
const string &Student::Name() const {
return name;
}
由于Student类是从string类(基类)派生而来,因此,通过强制类型转换
。将Student对象转换为string对象。
指针this
指向用来调用方法的对象,因此 *this
为用来调用方法的对象。
const string & Student::Name() const
{
return (const string &) *this; // 返回一个引用,引用指向用于调用方法的Student对象中的继承而来的string对象。
}
14.2.5 访问基类的友元函数
用类名显式地限定函数名不适合友元函数。 友元不属于类,但是可以通过显式转换为基类来调用正确的函数。
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &) stu << " : \n";
...
}
修改后的源文件
#include "student_C.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);
}
//private method
ostream &Student::arr_out(std::ostream &os) const {
int i = 0;
int lim = ArrayDb::size();
if (lim > 0){
for (int j = 0; j < lim; ++j) {
os << ArrayDb::operator[](j) << " ";
if (j % 5 == 4){
os << endl;
}
}
}else {
os << " empty array";
}
return os;
}
//friends
//use string version of operator>>()
istream &operator>>(istream &is,Student &stu){
is >> (string &)stu;
return is;
}
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;
}
14.2.6 使用组合还是私有继承
多数选择程序员选择使用 组合
。原因:
- 易于理解。
- 类声明中组合表示被包含类的显式命名对象。代码可以通过名称引用对象
- 继承会使得关系更抽象难懂。
- 继承会引起很多问题
- 组合可以包含多个同类的子对象。
- 继承只能使用一个对象(当对象都没有名称时,会难以区分)。
通常情况下:
- 应使用组合来建立
has-a
关系 - 如果新类需要访问原有类的
保护成员
,或者需要重新定义虚函数
,则应使用私有继承
。重新定义的函数将只能在类中使用,而不是公有的。
14.2.7. 保护继承
保护继承是私有继承的变体。
在基类中使用 关键字protected
来进行声明。
class Student : protected std::string, protected std::valarray<double>{
...
};
使用保护继承时,基类中的公有成员和保护成员都会变成派生类的保护成员。
- 使用私有继承`时,第三代类将不能使用基类的接口。
- 基类的公有方法在派生类中将变成私有方法。
- 使用
保护继承
时,基类的公有方法在第二代中将变成受保护的,所以第三代派生类可以使用。
14.2.8 使用using重新定义访问权限
使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。
让基类的方法在派生类外可用的两种方式:
-
定义一个使用该基类方法的派生类方法
-
double Student::sum()const{ //public student method return std::valarray<double>::sum(); //use privately-inherited method++aw }
-
-
将函数调用包封装在另一个函数调用中。通过
using声明
来指出派生类可以使用特定的基类成员。(类似 using namespace std; 名次空间的方式)
using
声明只使用成员名
---- 没有圆括号、函数特征标和返回类型
。
class Student : private std::string, private std::valarray<double>
{
...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
...
};
using声明只适合继承,不适合包含
14.3. 多重继承(MI)
使用多个基类的继承被称为 多重继承(multiple inheritance,MI)
。
MI描述的是is-a
关系。
MI必须使用 关键字public
来限定每一个基类。原因:非特别指出,编译会认为是私有派生。
// 从Singer 和 Waiter 公有派生出SingerWaiter
class SingerWaiter : public Waiter, public Singer {...};
// Singer 和 Waiter 都继承了一个 Worker组件,所以SingerWaiter则组合两个Woker组件
MI可能会给程序员带来很多新的问题:
- 从两个不同的基类基础同名方法
- 从两个或者更多基类那里继承了同一个类的多个实例
14.3.1 程序worker的分析
下面来看一个例子:首先定义一个抽象基类Worker,并使用它派生出Waiter类和Singer类。然后,便可以使用MI从Waiter类和Singer类派生出SingingWaiter类。
1、头文件
//
// Created by e on 2022/10/9.
//
#ifndef TEST_WORKER0_H
#define TEST_WORKER0_H
#include <string>
class Worker{
private:
std::string fullname;
long id;
public:
Worker():fullname("no one"),id(0L){}
Worker(const std::string &s,long n):fullname(s),id(n){}
virtual ~Worker() = 0;
virtual void Set();
virtual void Show()const;
};
class Waiter: public Worker{
private:
int panache;
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: public Worker{
protected:
enum {other , alto , contralto, soprano,bass ,baritone,tenor};
enum {Vtypes = 7};
private:
static char *pv[Vtypes]; // 相当于声音类型的字符串(二维数组)
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;
};
#endif //TEST_WORKER0_H
2、源文件
//
// Created by e on 2022/10/9.
//
#include "Worker0.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
// Worker methods
//must implement virtual destructor,even if pure
Worker::~Worker(){ }
void Worker::Set() {
cout << "Enter worker's name:";
getline(cin,fullname);
cout << "Enter worker's id:";
cin >> id;
while (cin.get() != '\n'){
continue;
}
}
void Worker::Show() const {
cout << "Name:" << fullname << endl;
cout << "Employee id: " << id << endl;
}
//Waiter method
void Waiter::Set() {
Worker::Set();
cout << "Enter waiter's panache rating:";
cin >> panache;
while (cin.get() != '\n'){
continue;
}
}
void Waiter::Show() const {
cout << "Category:waiter\n";
Worker::Show();
cout << "Panache rating: " << panache << endl;
}
//Singer methods
char *Singer::pv[] = {"other","alto","contralto","soprano","bass","baritone","tenor"};
void Singer::Set() {
int i ;
Worker::Set();
cout << "Enter number for singer's vocal range:" << endl;
for(i = 0; i < Vtypes;i++ ){
cout << i << ":" << pv[i] << "\t";
if (i % 4 == 3){
cout << endl;
}
}
if(i % 4 != 0){
cout << endl;
}
while(cin >> voice && (voice < 0 || voice >= Vtypes)){ //规定输入界限
cout << "Please enter a value >= 0 && value < " << Vtypes << endl;
}
while(cin.get() != '\n'){ //把可能留在缓冲队列中的换行字符给消去
continue;
}
}
void Singer::Show() const {
cout << "Category:Singer\n";
Worker::Show();
cout << "Vocal range :" << pv[voice] << endl;
}
3、执行文件
#include <iostream>
#include "Worker0.h"
const int LIM = 4;
int main(){
Waiter bob("Bob Apple",314L,5);
Singer bec("Beverly Hills",522L,3);
Waiter w_temp;
Singer s_temp;
Worker *pw[LIM] = {&bob,&bec,&w_temp,&s_temp};
int i = 0;
for (int j = 2; j < LIM; ++j) {
pw[j]->Set();
}
for (int j = 0; j < LIM; ++j) {
pw[j]->Show();
std::cout << std::endl;
}
return 0;
}
输出:
Enter worker's name:Waldo Dropmaster
Enter worker's id:442
Enter waiter's panache rating:3
Enter worker's name:Sylvie Sirenne
Enter worker's id:555
Enter number for singer's vocal range:
0:other 1:alto 2:contralto 3:soprano
4:bass 5:baritone 6:tenor
3
Category:waiter
Name:Bob Apple
Employee id: 314
Panache rating: 5
Category:Singer
Name:Beverly Hills
Employee id: 522
Vocal range :soprano
Category:waiter
Name:Waldo Dropmaster
Employee id: 442
Panache rating: 3
Category:Singer
Name:Sylvie Sirenne
Employee id: 555
Vocal range :soprano
如果添加一个从Singer和Waiter类派生出的SingingWaiter类后,将出现下面的问题:
- 有多少个Worker?
- 哪个方法?
14.3.1 有多少个Worker
假设首先从Singer和Waiter公有派生出SinginWaiter:
class SingingWaiter:public Singer,public Waiter{...};
因为Singer和Waiter都继承了一个Worker组件,所以SingingWaiter在这里有两个Worker组件,在将派生类对象地址赋给基类指针的时候会出现二义性:
SingingWaiter ed;
Worker *pw = &ed; //ambiguous
把基类指针设置为派生对象中的基类对象的地址,有两种地址可选择,应使用类型转换来指定对象。
Worker *pw1 = (Waiter *) &ed;
Worker *pw2 = (Singer *) &ed;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSowCU9z-1665738289866)(/Users/mac/Library/Application Support/typora-user-images/image-20221009201847610.png)]
-
虚基类
-
虚基类使得从多个类(基类相同)派生出的对象只继承一个基类对象。在类声明中使用关键字
virtual
来指示派生时,基类则成为虚基类
。-
class Singer:virtual public Worker{...}; class Waiter:public virtual Worker{...};
然后,可以将SingingWaiter类定义为:
class SingingWaiter:public Singer,public Waiter{...};
此时,SingingWaiter对象只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IczQO72P-1665738289866)(/Users/mac/Library/Application Support/typora-user-images/image-20221009203621327.png)]
-
-
-
新的构造函数规则
-
使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。
-
//example #1 class A{ int a; public: A(int n = 0):a(n){} ... }; class B:public A{ int b; public: B(int m = 0,int n = 0):A(n),b(m){} ... }; class C:public B{ int c; public: C(int q = 0,int m = 0,int n = 0):B(m,n),c(q){} ... }; //example #2(以下都是单继承) void Worker::Show()const{ cout << "Name:" << fullname << "\n"; cout << "Employee ID:" << id << "\n"; } void Waiter::Show()const{ Worker::Show(); cout << "Panache rating:" << panache << "\n"; } void HeadWaiter::Show()const{ Waiter::Show(); cout << "Presence rating :" << presence << "\n"; }
C类的构造函数只能调用B类的构造函数,而B类的构造函数只能调用A类的构造函数。这里,C类的构造函数使用值q,并将值m和n传递给B类的构造函数;而B类的构造函数使用值m,并将值n传递给A类的构造函数。
-
-
C++在基类是虚的时,禁止信息通过中间类自动传递给基类。
-
例如:
-
SingingWaiter(const Worker &wk,int p = 0,int v = Singer::other):Waiter(wk,p) , Singer(wk,v){} //flawed
存在问题是,自动传递信息的时候,将通过2条不同的途径(Waiter和Singer)将wk传递给Worker对象,为避免这种冲突,就…上面的构造函数只会初始化成员panache和voice。
-
-
-
如果不希望
默认构造函数
来构造虚基类对象
,则需要显式地调用所需的基类构造函数。-
SingingWaiter(const Worker &wk , int p = 0,int v = Singer::other):Worker(wk),Waiter(wk,p),Singer(wk,v){}
上述代码将显式地调用构造函数worker(const Worker&)
-
-
如果类有间接虚基类,则除非只需要使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
-
14.3.2 哪个方法
在多重继承中,每个直接祖先都有一个相同的函数
(如Show()),使得派生类调用此函数时存在二义性
。此时需要使用作用域解析运算
符来澄清编程者的意图。
SingerWaiter newhire("Elise Hawks",2005,6,soprano)
newhire.Singer::Show();
更好的方法是在SingingWaiter中重新定义Show(),并指出要使用哪个Show()。例如,如果希望SingingWaiter对象使用Singer版本的Show()函数
void SingingWaiter::Show(){
Singer::Show();
}
在祖先相同的时候,使用MI必须引入虚基类,并修改函数初始化列表的规则。
下面对上面的程序进行修改,以便演示虚基类的使用。
1、头文件
//
// Created by e on 2022/10/9.
//
#ifndef TEST_WORKERMI_H
#define TEST_WORKERMI_H
#include <string>
class Worker{
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;
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 std::string pv[Vtypes]; // 相当于声音类型的字符串(二维数组)
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 //TEST_WORKERMI_H
2、源文件
//
// Created by e on 2022/10/9.
//
#include "workermi.h"
#include "workermi.h"
#include <iostream>
using std::cout;
using std::endl;
using std::cin;
//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();
cout << endl;
}
//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
std::string Singer::pv[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();
cout << endl;
}
//protected methods
void Singer::Data() const {
cout << "Vocal range: " << pv[voice] << endl;
}
void Singer::Get() {
int i ;
// Worker::Get();
cout << "Enter number for singer's vocal range:" << endl;
for(i = 0; i < Vtypes;i++ ){
cout << i << ":" << pv[i] << "\t";
if (i % 4 == 3){
cout << endl;
}
}
if(i % 4 != 0){
cout << endl;
}
while(cin >> voice && (voice < 0 || voice >= Vtypes)){ //规定输入界限
cout << "Please enter a value >= 0 && value < " << 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();
cout << endl;
}
3、执行文件
#include <iostream>
#include <cstring>
#include "workermi.h"
const int SIZE = 5;
int main() {
using std::cin;
using std::cout;
using std::strchr;
using std::endl;
Worker *loloas[SIZE];
int ct;
for (int 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;
//strchr返回参数choice指定的字符串“wstq”中第一次出现的地址,如果没有这样的字符,则返回NULL指针
while (strchr("wstq", choice) == NULL) {
cout << "Please enter a w, s, t, or q: ";
cin >> choice;
}
if (choice == 'q') {
break;
}
switch (choice) {
case 'w':
loloas[ct] = new Waiter;
break;
case 's':
loloas[ct] = new Singer;
break;
case 't':
loloas[ct] = new SingingWaiter;
break;
}
cin.get();
loloas[ct]->Set();
}
cout << "Here is your staff: \n";
for (int i = 0; i < ct; ++i) {
loloas[i]->Show();
}
//开辟内存就需要相对应地释放内存
for (int i = 0; i < ct; ++i) {
delete loloas[i];
}
cout << "Bye." << endl;
return 0;
}
分析程序:
假若上面程序以下面模样写,会出现两次姓名和I D ,因为两个Show都调用Worker::Show();
void SingingWaiter::Show(){
Singer::Show();
Waiter::Show();
}
解决的方法是:使用模版化的方式,提供只显示Worker组件的方法和一个只显示Waiter组件或者Singer组件的方法。然后,在有多个祖先的派生类中组合起来,上面的程序SingingWaiter就是这么做的。
为什么组件都设置为protected?
因为,如同上述Data()方法,假若将其设置私有的话,那么它只能在类内部使用,这将会组织Waiter类或者Singer类使用Worker::Data()。如果Data()方法是protected的话,在继承层次结构中的类中都可以使用它,其他外界的类不能使用它,这很契合实现模版化方式。
下面介绍其他一些有关mi的问题:
1、混合使用虚基类和非虚基类
- 如果基类是虚基类,派生类将包含基类的一个子对象。
- 如果基类不是虚基类,派生类将包含多个子对象。
- 当虚基类和非虚基类混合时候,该类将包含一个表示所有虚途径的基类子对象,和分别表示各条非虚途径的多个基类子对象
2、虚基类和支配
- 使用非虚基类时候,如果从不同的类那里继承了两个或者多个同名成员(数据或者方法),则直接使用该成员名,会导致二义性
- 如果使用虚基类,不一定会导致二义性。因为,当某个名称优先于其他所有名称**(派生类的名称优先于直接或者间接祖先类中的相同名称)**,直接使用也不会导致二义性。下面看个例子:
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{ ... };
解释:
- 类C中的q()定义优先于类B中的q()定义,因为类C是从类B派生而来的。
- 任何一个omg()定义都不优先于其他omg()定义,因为C和E都不是对方的基类
虚二义性规则和访问规则无关:
- 即使E::omg()是私有的,不能在F类中直接访问,但使用omg()仍将导致二义性。
- 即使C::q()是私有的,它将优先于D::q()(这里的D::q()由前提看来是从基类B继承而来)。在这种情况下,可以在类F中调用B::q(),如果不限定q(),则意味着要调用不可访问的C::q()
14.4 类模板
C++模板提供参数化(parameterised)类型:能够让类型名作为参数传递给接收方来建立类或函数。
14.4.1 定义类模板
以第十章的Stack类为基础来建立模版:
typedef unsigned long Item;
class stack {
private:
enum {
Max = 10
};
Item items[Max];
int top;
public:
stack();
bool isempty() const;
bool isfull() const;
bool push(const Item &item);
bool pop(Item &item);
void show_stack();
};
模板类的代码定义:
template <typename Type> // 模板类型
template <class Type> // 模板类
-
template
告诉编译器,将要定义一个模板,尖括号中的内容相当于函数的参数列表。 -
关键字
class
看作变量的类型名,变量接受类型作为其值,把Type看作是该变量的名称。 -
Type 表示一个通用的类型说明符。在使用模板时,使用实际的类型替换它。
-
对于Stack类来说,这意味着应将声明中所有的typedef标识符替换为Type,例如
-
Item items[MAX]; #改为 Type items[MAX];
-
使用模版成员函数替换原有类的方法。每个函数头都将以相同的模版声明打头:
-
bool Stock::push(const Item &item){ ... } #改为 template <class Type> bool Stack<Type>::push(const Type &item){ ... }
-
如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
模板类不是函数,不能单独编译。
模板必须和特定的模板实例化
请求一起使用。一般是使将所有模板信息放在一个头文件
中。
下面程序列出类模版和成员函数模版:
//
// Created by e on 2022/10/12.
//
#ifndef TEST_STACKTP_H
#define TEST_STACKTP_H
template<class Type>
class Stack{
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;
}
}
// 1 2 3 4 5 6 7 (指针指到7的下一个位置)
template<class Type>
bool Stack<Type>::pop(Type &item) {
if (top > 0){
item = items[--top];//--top是为了回到栈顶元素的位置,并且控制top的大小模拟栈入栈出
return true;
}else{
return false;
}
}
#endif //TEST_STACKTP_H
14.4.2 使用模板类
声明模板类的对象
,必须显式地提供所需的类型
。
Stack<int> kernels; // 存储int类型的栈类
Stack<string> colonels; //存储string类型的栈类
⚠️:使用Stack类模版,使用的算法必须与数据类型相一致。举例子:Stack类假设可以将一个项目赋值给另一个项目,这种假设对于基本类型、结构和类来说是成立的,但是对于数组则不成立。
⚠️:模版类的类型参数(type parameter)必须显式声明。这就是与常规的函数模版有所不同的地方,常规的函数模版由编译器根据函数的参数类型来确定要生成哪种函数:
template <class T>
void simple(T t){cout << t << '\n';}
...
simple(2); //generate void simple(int)
simple("two"); //generate void simple(const char *)
2、执行文件
#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";
return 0;
}
输出:
Please enter A to add a purchase order.
P to process a PO, or Q to quit.
A
Enter a PO number to add: red911porsche
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
A
Enter a PO number to add: blueR8audi
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
A
Enter a PO number to add: silver747boeing
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
P
PO # silver747boeing popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
P
PO # blueR8audi popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
P
PO # red911porsche popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
P
stack already empty
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
Q
Bye
14.4.3 深入探讨模版类
1、不正确地使用指针栈
Stack<char*>st; // create a stack for pointers-to-char
修改版本1⃣️:
string po;
#替换为
char *po;
warning⚠️:单纯这样改是错误的,因为只创建指针,而没有创建保存字符串的空间
修改版本2⃣️:
string po;
#替换为
char po[40];
warning⚠️:这次修改可以将po放进栈中,但数组完全与pop()方法假设相冲突!!
template <class Type>
bool Stack<Type>::pop(Type &item){
if(top > 0){
item = items[--top];
return true;
}
....
}
假如type为char*,则引用变量item的类型为:(char*****)&item,这样item就变成了数组名,这样line4就不可能执行得了。加上引用变量item必须引用某种类型的左值,而不是数组名。
所以很遗憾地说,这种改法还是错了。
修改版本3⃣️:
string po;
#替换为
char *po = new char[40];
这次很高兴地告诉大家,po是变量,因此与pop()的代码兼容。但又不幸的是,每次po读取新字符串的时,由于没有释放原有的内存,内存的内容尽管每次都发生改变,但每次执行压入操作时,加入到栈中的地址都是相同的。对栈执行弹出操作的时候,它总是指向读入的最后一个字符串()。
2、正确使用指针栈
使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。把这些指针放在栈中是有意义的,因为每个指针都将指向不同的字符串。
下面程序重新定义了Stack 类,是Stack类的构造函数能够接受一个可变的大小参数,这里面也包含了动态指针数组。
1、头文件
//
// Created by e on 2022/10/12.
//
#ifndef TEST_STACKTP01_H
#define TEST_STACKTP01_H
template<class Type>
class Stack{
private:
enum {SIZE = 10}; //默认值
int stackSize;
int top;
Type *items; //以指针数组保证每个被压入字符串的地址各不相同
public:
explicit Stack(int ss = SIZE);
Stack(const Stack &st);
bool isFull(){return top == stackSize;}
bool isEmpty(){return top == 0;}
bool push(const Type &item);
bool pop(Type &item);
~Stack(){delete []items;};
Stack &operator=(const Stack &st);
};
template<class Type>
Stack<Type>::Stack(int ss):stackSize(ss),top(0){
items = new Type[stackSize];
}
template<class Type>
bool Stack<Type>::push(const Type &item) {
if(!isFull()){
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;
}
}
template<class Type>
Stack<Type>::Stack(const Stack &st){//拷贝构造函数
stackSize = st.starkSize;
top = st.top;
items = new Type[stackSize];
for (int i = 0; i < top; ++i) {
items[i] = st.items[i];
}
}
template<class Type>
Stack<Type>& Stack<Type>::operator=(const Stack<Type> &st) {
if(this == &st){ //要避免自我赋值
return *this;
}
delete []items; //释放原来指向的字符串内存,避免内存泄漏
stackSize = st.stackSize;
top = st.top;
items = new Type[stackSize];
for (int i = 0; i < top; ++i) {
items[i] = st.items[i];
}
}
#endif //TEST_STACKTP01_H
warning⚠️:为什么类外定义中使用作用域解析符,或者返回具体的类型,不能使用类名,而要使用Stack<int>
✅:实际模版类中,可以在模版声明或者模版函数定义内使用Stack,但在类的外面,即指定返回类型或者使用作用域解析符时,必须使用完整的Stack<int>.
2、执行文件
#include <iostream>
#include <cstdlib> //for rand(),srand()
#include <ctime> //for time()
#include "stacktp01.h"
const int Num = 10;
int main(){
std::srand(std::time(0)); //randomize rand()
std::cout << "Please enter stack size: ";
int stackSize;
std::cin >> stackSize;
//create an empty stack with stackSize slots
Stack<const char *>st(stackSize);
//in basket
const char *in[Num] = {" 1: Hank Gilgamesh"," 2: Kiki Ishtar",
" 3: Betty Rocker"," 4: Ian Flagranti",
" 5: Wolfgang Kibble"," 6: Protia Koop",
" 7: Joy Almondo", " 8: Xaverie Paprika",
" 9: Juan Moore","10: Misha Mache"};
//out basket
const char *out[Num];
int processed = 0;
int nextIn = 0;
while (processed < Num){
if (st.isEmpty()){
st.push(in[nextIn++]);
} else if(st.isFull()){
st.pop(out[processed++]);
} else if(std::rand() % 2 && nextIn < Num){
st.push(in[nextIn++]);
} else {
st.pop(out[processed++]);
}
}
for (int i = 0; i < Num; ++i) {
std::cout << out[i] << std::endl;
}
std::cout << "Bye\n";
return 0;
}
输出:
1: Hank Gilgamesh
2: Kiki Ishtar
5: Wolfgang Kibble
4: Ian Flagranti
7: Joy Almondo
6: Protia Koop
3: Betty Rocker
8: Xaverie Paprika
10: Misha Mache
9: Juan Moore
Bye
14.4.3 数组模板和非类型参数
为了探讨一些非类型(或表达式)参数以及如何使用数组来处理继承族
下面介绍一个允许指定数组大小的简单数组模版。
- 方法1⃣️,使用构造函数的参数和动态数组提供数组的元素数目
- 方法2⃣️:使用模版参数来提供常规数组的大小(c++11,模版array就是这么干的)
1、头文件
#ifndef TEST_ARRAYTP_H
#define TEST_ARRAYTP_H
#include <iostream>
#include <cstdlib>
template<class T,int n>
class Array{
private:
T ar[n];
public:
Array(){}
explicit Array(const T &v);
virtual T &operator[](int i);
virtual T operator[](int i) const;
};
template<class T,int n>
Array<T,n>::Array(const T &v){
for (int i = 0; i < n; ++i) {
ar[i] = v;
}
}
template<class T,int n>
T &Array<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 Array<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 //TEST_ARRAYTP_H
注意模版头
template <class T, int n>
- class 指出T为
类型参数
,int指出n的类型。这种参数称为非类型(non-type)
或表达式参数
。 - 表达式参数可以是
整型
、枚举
、引用
或指针
。(double m是不合法的,因为他是浮点型)
模板代码不能修改参数的值,也不能使用参数的地址。
形如n++,&n这类语句是不合法的
实例化模板时,用做表达式参数的值必须是常量表达式
(#define
或者const
定义的常数)。
-
表达式参数的优缺点
-
优点
- 构造函数使用new和delete来管理堆内存,但它为自动变量维护的内存栈,使得执行速度更快。
-
缺点
-
每种数组大小都会生成自己的模板。
Array<double,12> eggweights; Array<double,13> donuts;
-
-
-
构造函数的优缺点
-
优点
-
利用new和delete来管理堆内存,可扩充性比较好
-
多个声明只会生成一个类声明
Stack<int> eggs(12); Stack<int> dunkers(13);
-
这种方法更为通用:数组大小是作为类成员存储在定义中的。这可以将一种尺寸的数组赋值给另一种尺寸的数组,也可以创建数组尺寸可变的类。
-
-
缺点
- 构造函数使用new和delete来管理堆内存,执行速度慢
-
14.4.4 模板多功能性
模板类的四个功能:
-
用作基类
template <typename Type> class GrowArray : public Array <Type> {...};
-
用作组件类
-
模板可以包含多个类型参数。
template <class T1, class T2> { private: T1 a; T2 b; public: T1 & first(); T2 & second(); ... };
-
-
默认类型模板参数
-
可以为类型参数提供默认值
template <class T1, class T2 = int> class Topo {...}; // 如果省略了T2的值,编译器会默认使用 int
-
不能为
函数模板参数
提供默认值。但可为非类型参数
提供默认值。
-
1、递归使用模版
-
Array< Array<int,5>,10 > twodee;
这个twodee是一个包含10个元素的数组,每个元素又是长度为5的数组,与之相等的是:
int twodee[10][5];
warning⚠️:维的顺序与等价的二维数组相反
下面程序通过使用Array模版创建了一维数组和二维数组,分别保存这10个组的总数和平均值。方法调用cout.width(2)以两个字符的宽度显示下一个条目。
#include <iostream>
#include "arraytp.h"
using std::cout;
using std::endl;
int main() {
Array<int, 10> sums;
Array<double, 10> avgs;
Array<Array<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];
}
avgs[i] = sums[i] / 5;
}
for (i = 0; i < 10;i++){
for (j = 0;j < 5;j++){
cout.width(2);
cout << twodee[i][j] << ' ';
}
cout.width(3);
cout << "sum = " << sums[i] << " avg = " << avgs[i] << endl;
}
cout << "Done\n";
return 0;
}
输出:
1 2 3 4 5 sum = 15 avg = 3
2 4 6 8 10 sum = 30 avg = 6
3 6 9 12 15 sum = 45 avg = 9
4 8 12 16 20 sum = 60 avg = 12
5 10 15 20 25 sum = 75 avg = 15
6 12 18 24 30 sum = 90 avg = 18
7 14 21 28 35 sum = 105 avg = 21
8 16 24 32 40 sum = 120 avg = 24
9 18 27 36 45 sum = 135 avg = 27
10 20 30 40 50 sum = 150 avg = 30
Done
2、使用多个类型的参数
模版可以包含多个类型的 参数。若希望类可以保存两种类型的值,则可以创建并使用Pair模版来保存两个不同的值(标准模版库提供了类似的模版,名为pair)
下面程序便如上述描述的一样。其中first() const 和second() const报告存储的值,由于这两个方法返回Pair数据成员的引用,因此我们可以通过赋值重新设置存储的值。
#include <iostream>
#include <cstring>
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 &t1,const T2 &t2):a(t1),b(t2){}
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 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;
return 0;
}
3、默认类型模版参数
类模版的新特性为:可以为类型参数提供默认值;
template <class T1,class T2 = int> class Topo{....};
如果省略T2值,那么效果如下:
Topo<double,double> m1; //T1 is double,T2 is double
Topo<double> m2; // T1 is double,T2 is int
14.4.5 模板的具体化
隐式实例化
、显式实例化
和显式具体化
,统称为 具体化
。
模板
使用 泛型
的方式描述类
,具体化
是使用具体化的类型
生成类声明
。
-
隐式实例化
-
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。
ArrayTP <int, 100> stuff;
编译器在需要对象之前,不会生成类的隐式实例化,好比如:
Array<double,30> *pt; // a pointer,no object needed yet pt = new Array<double,30>; // now an object is needed
-
-
显式实例化
-
当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化。
-
声明必须位于模板定义所在的名称空间中。
template class ArrayTP<string, 100>; // 虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)
-
-
显式具体化
-
显式具体化是特定类型(用于替换模版中的泛型)的定义。有些时候,我们编写的模版并不总是会适用于所有类型
template<typename T> int compare(const T&v1,const T&v2) { if(v1<v2) return -1; if(v1>v2) return 1; return 0; }
当T为数字类型好办,但是T如果是const char*表示的字符串,这不管用,尽管它能正常工作,但字符串将会按照地址来比较,而不是按照字符串大小来比较。所以这时候我们需要使用具体化版本。
-
格式如下:
template <> class Classname <specialized-type-name> {...}; // 显式具体化的例子 template <> class Pair<int,int> {...}; template<> int compare<const char *>(const char *const &v1,const char *const &v2) { return strcmp(v1,v2); }
-
-
部分具体化
-
部分限制模板的通用性。
// 通用模板 template <class T1, class T2> class Pair {...}; // template <class T1> class Pair<T1,int> {...}; // T1 保持不变,但是T2 被具体化为 int
-
关键字
template
后面的<>声明
的是没有被具体化的类型参数,如果指定了所有的类型,则<>内将为空,这将导致显式具体化。 -
如果多个模板可供选择,编译器将优先使用具体化程度最高的模板。
Pair<double,double> p1; // use general Pair template Pair<double,int> p2; //use Pair<T1,int> partial specialization Pair<int,int> p3; // use Pair<int,int> explicit specialization
-
如果提供的类型不是指针,则编译器将使用通用版本。如果提供的是指针,则编译器将使用指针具体化版本
template<class T> //general version class Feeb{....}; template<class T*> //pointer partial specialization class Feeb{....}; Feeb<char> fb1; // use general Feeb template,T is char Feeb<char *>fb1; // use Feeb T* specialization , T is char
-
如果上述Feeb类没有进行部分具体化,则第二个声明将使用通用版本,将T转化为char*。如果上述Feeb类有进行部分具体化,则第二个声明将使用具体化模版。
-
14.4.6 成员模板
模板可用作结构、类或模板类的成员。要完全实现STL设计,必须使用这项特性。
下面是一个简短的模版类示例,该模版类将另一个模版类和模版函数作为其成员。
// template member
#include <iostream>
using std::cout;
using std::endl;
template<class T>
class beta {
private:
template<class V>
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; //template object
hold<int> n; //template object
public:
//借助私有模版成员q、n下的默认构造函数,beta通过初始化列表对val进行赋值
beta(T t, int i) : q(t), n(i) { }
template<class U>
//返回值的类型由参数u的数据类型U看出,因为u与blab()的数据类型是一样的。
U blab(U u, T t) { return n.Value() + q.Value() * u / t; }
void Show() const {
//组件
q.Show();
n.Show();
}
};
int main(){
beta<double>guy(3.5,3);
cout << "T was set to double" << endl;
guy.Show();
//q(3.5) --> hold(double v = 3.5):val(v){}
//q(3) --> hold(int v = 3):val(v){}
cout << "V was set to T,which is double,then was set to int" << endl;
cout << guy.blab(10,2.3) << endl;
cout << "U was set to int" << endl;
cout << guy.blab(10.0,2.3) << endl;
cout << "U was set to double" << endl;
cout << "Done!" << endl;
return 0;
}
输出:
T was set to double
3.5
3
V was set to T,which is double,then was set to int
18
U was set to int
18.2174
U was set to double
Done!
我们也可以搞点骚操作:模版嵌套
对上面程序进行改写:可以在beta模版中声明hold类和blah方法,并在beta模版的外面定义它们。
template<class T>
class beta {
private:
template<class V>
class hold;
hold<T> q; //template object
hold<int> n; //template object
public:
//借助私有模版成员q、n下的默认构造函数,beta通过初始化列表对val进行赋值
beta(T t, int i) : q(t), n(i) { }
template<class U>
//返回值的类型由参数u的数据类型U看出,因为u与blab()的数据类型是一样的。
U blab(U u, T t);
void Show() const {
//组件
q.Show();
n.Show();
}
};
//member definition
template <class T>
template <class V>
class beta<T>::hold{
private:
V val;
public:
hold(V v = 0):val(v){}
void Show() const{cout << val << endl;}
V value() const{return val;}
}
//member definition
template <class T>
template <class U>
U beta<T>::blab(U u,T t){
return (n.Value() + q.Value()) * u / t;
}
warning⚠️,模版是嵌套的,不能使用下面的语法:
template <typename T, typename V>
⚠️:定义还必须指出hold和blab是beta< T >类的成员,通过作用域解析运算符来完成。
14.4.7 将模版用作参数
模版可以包含类型参数(如typename T)和非类型参数(如 int n)。模版还可以包含本身就是模版的参数,这种参数是模版新增的特性,用于实现STL。
例如:有如下语句
template <template< typename T >class Thing>
class Crab
模版参数是template< typename T >class Thing,其中template< typename T >class是类型,Thing是参数。这意味着->假设有如下声明:
Crab<King> legs;
为了使上述模版被接受,模版参数King必须是一个模版类,其声明与模版参数Thing的声明匹配:
template <typename T>
class King{...};
总而言之:模版参数Thing将被替换为声明Crab对象时被用作模版参数的模版类型
下面程序演示上面的用法:
1、头文件
#ifndef TEST_STACKTP_H
#define TEST_STACKTP_H
template<class Type>
class Stack{
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;
}
}
// 1 2 3 4 5 6 7 (指针指到7的下一个位置)
template<class Type>
bool Stack<Type>::pop(Type &item) {
if (top > 0){
item = items[--top];//--top是为了回到栈顶元素的位置,并且控制top的大小模拟栈入栈出
return true;
}else{
return false;
}
}
#endif //TEST_STACKTP_H
2、执行文件
#include <iostream>
#include "stacktp.h"
template<template<class T> class Thing>
class Crab {
private:
Thing<int> s1;
Thing<double> s2;
public:
Crab(){}
// assumes the thing class has push() and pop() member
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(){
using std::cout;
using std::endl;
using std::cin;
Crab<Stack> nebula;
//Stack must match template <typename T> class thing
int ni;
double nb;
cout << "Enter int double pairs,such as 4 3.5 (0 0 to end):\n";
while (cin >> ni >> nb && ni > 0 && nb > 0){
if (!nebula.push(ni,nb)){
break;
}
}
while (nebula.pop(ni,nb)){
cout << ni << " , " << nb << endl;
}
cout << "Done.\n";
return 0;
}
输出:
Enter int double pairs,such as 4 3.5 (0 0 to end):
50 22.48
25 33.87
60 19.12
0 0
60 , 19.12
25 , 33.87
50 , 22.48
Done.
可以混合使用模版参数和常规参数,例如,Crab类的声明可以像下面这样打头:
template <template <typename T> class Thing ,typename U,typename V>
class Crab{
private:
Thing<U> s1;
Thing<V> s2;
....
}
14.4.8 模板类和友元
模板类声明也可以友元。模板的友元分为3类:
-
非模板友元
-
在模板类中声明的一个常规友元函数,称为模板所有实例化的友元。
template<class T> class HasFriend { public: friend void counts(); ... };
-
当为友元函数提供模板类参数,必须指明具体化。
template <class T> class HasFriend { friend void report(HasFriend<T> &);// 具体化之后,就会变成约束模板友元 };
-
下面程序说明以上几点,HasFriend模版有一个静态成员ct。这意味着这个类的每一个特定的具体化都将有自己的静态成员。count()方法是所有HasFriend具体化的友元,它报告两个特定的具体化(HasFriend< int >和HasFriend< double >)的ct的值。该程序还提供两个report()函数,它们分别是某个特定HasFriend具体化的友元。
#include <iostream> using std::cout; using std::endl; template <typename T> class HasFriend{ private: T item; static int ct; public: HasFriend(const T & i):item(i){ct++;} ~HasFriend(){ct--;} friend void counts(); friend void reports(HasFriend<T> &); // template parameter }; // each specialization has its own static data member template<typename T> int HasFriend<T>::ct = 0; // non-template friend to all HasFriend<T> classes void counts(){ cout << "int count: " << HasFriend<int>::ct << "; "; cout << "double count: " << HasFriend<double>::ct << "; " << endl; } // non-template friend to the HasFriend<int> class void reports(HasFriend<int> &hf){ cout << "HasFriend<int>: " << hf.item << endl; } // non-template friend to the HasFriend<double> class void reports(HasFriend<double> &hf){ cout << "HasFriend<double>: " << hf.item << endl; } int main(){ cout << "No objects declared: "; counts(); HasFriend<int>hfi1(10); cout << "After hfi1 declared: " ; counts(); HasFriend<int> hfi2(20); cout << "After hfi2 declared: "; counts(); HasFriend<double> hfd1(10.5); cout << "After hfd1 declared: "; counts(); HasFriend<double> hfd2(20.5); cout << "After hfd2 declared: "; counts(); reports(hfi1); reports(hfi2); reports(hfd1); reports(hfd2); return 0; } 输出: No objects declared: int count: 0; double count: 0; After hfi1 declared: int count: 1; double count: 0; After hfi2 declared: int count: 2; double count: 0; After hfd1 declared: int count: 2; double count: 1; After hfd2 declared: int count: 2; double count: 2; HasFriend<int>: 10 HasFriend<int>: 20 HasFriend<double>: 10.5 HasFriend<double>: 20.5
warning⚠️:report()本身并不是模版函数,而只是使用一个模版作参数,这意味着必须**为要使用的友元定义显式具体化。**这样report()将重载两个版本,一个是HasFriend< int >版本的友元,另一个是HasFriend< double >版本的友元
-
-
约束模板友元:友元的类型取决于类被实例化时的类型。
-
3个步骤:
-
在类定义的前面声明每个模板函数
template <typename T> void counts(); template <typename T> void reports(T &);
-
在函数中再次将模板声明为友元。根据类模板参数的类型声明具体化。
template <typename TT> class HasFriendT { friend void counts<TT>(); friend void report<> (HasFriendT<TT> &); ... };
-
为友元提供模版定义
-
下面程序将演示以上的步骤来实现约束模版友元
#include <iostream> using std::cout; using std::endl; //template prototypes template <typename T> void counts(); template <typename T> void report(T &); //template class template <typename TT> class HasFriendT{ private: TT item; static int ct; public: HasFriendT(const TT &i) : item(i){ct++;} ~HasFriendT(){ct--;} friend void counts<TT>(); friend void report<>(HasFriendT<TT> &); }; template <typename T> int HasFriendT<T>::ct = 0; //template friend functions definitions template <typename T> void counts(){ cout << "template size: " << sizeof(HasFriendT<T>) << "; "; cout << "template counts(): " << HasFriendT<T>::ct << endl; } template <typename T> void report(T &hf){ cout << hf.item << endl; } int main(){ counts<int>(); HasFriendT<int> hfi1(10); HasFriendT<int> hfi2(20); HasFriendT<double> hfdb(10.5); report(hfi1); //generate report(HasFriend<int> &) report(hfi2); //generate report(HasFriend<int> &) report(hfdb); //generate report(HasFriend<double> &) cout << "counts<int>() output:\n"; counts<int>(); cout << "counts<double> output:\n"; counts<double>(); return 0; } 输出: template size: 4; template counts(): 0 10 20 10.5 counts<int>() output: template size: 4; template counts(): 2 counts<double> output: template size: 8; template counts(): 1
对于下面语句:
friend void report<>(HasFriendT<TT> &);
声明中的<>指出这是模版具体化。对于report(),<>可以为空,因为可以从函数参数推断出模版类型参数。例如,main()函数里面report(hfi1),函数参数为hfi1,其类型为HasFriendT< int >,故可知模版类型参数为HasFriendT< int >
但counts()函数没有参数,因此必须使用模版参数语法(< TT >)来指明其具体化
-
-
-
非约束模板友元:每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模版类型参数与模版类类型参数是不同的
-
在类内部声明模板,可以创建非约束友元函数。
template <typename T> class ManyFriend { template <typename C,Typename D> friend void show2(C &, D &) ... }; // 实现使用方式 template <typename C, typename D> void show2(C & c, D & d) { ... }
-
下面程序是一个使用非约束友元的例子
#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); cout << "hfdb ,hfi2: "; show2(hfdb,hfi2); return 0; } 输出: hfi1 ,hfi2: 10, 20 hfdb ,hfi2: 10.5, 20
-
14.4.9 模板别名(C++11)
-
使用
typedef
为模板具体化指定别名typedef std::array<double,12> arrd; arrd gallons; // gallon type : std::array<double,12>
-
使用
using name = type
于非模板,此时与常规的typedef等价。template <typename T> using arrtype = std::array<T,12>; // arrtype定义为一个模板别名
-
C++11 允许将语法using = 用于非模版。用于非模版时,这种语法与常规typedef等价;
typedef const char *pc1; //typedef syntax using pc2 = const char*; //using = syntax