C++——代码重用
一、包含对象成员的类
接口和实现:使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
1、初始化被包含的对象
对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数。对于成员对象,构造函数则使用成员名。
C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。
初始化顺序:当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是他们在初始化列表中的顺序。但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。
2、使用被包含对象的接口
被包含对象的接口不是公有的,但可以在类方法中使用它。
Student.h
1 #include <iostream> 2 #include <string> 3 #include <valarray> 4 5 class Student{ 6 private: 7 typedef std::valarray<double> ArrayDb; 8 std::string name_; 9 ArrayDb scores_; 10 std::ostream & arr_out(std::ostream & os)const; 11 public: 12 Student():name_("Null Student"),scores_(){}; 13 explicit Student(const std::string & s):name_(s),scores_(){}; 14 explicit Student(int n):name_("Nully"),scores_(n){}; 15 Student(const std::string & s, int n):name_(s),scores_(n){}; 16 Student(const std::string & s, const ArrayDb & a):name_(s),scores_(a){}; 17 Student(const std::string & s, double * pd, int n):name_(s), scores_(pd,n){}; 18 ~Student(){}; 19 double Average()const; 20 const std::string & Name()const; 21 double & operator[](int i); 22 double operator[](int i)const; 23 friend std::istream & operator>>(std::istream & is, Student & stu); 24 friend std::istream & getline(std::istream & is, Student & stu); 25 friend std::ostream & operator<<(std::ostream & os, const Student & stu); 26 };
Student.cpp
1 #include "Student.h" 2 double Student:: Average()const{ 3 if (scores_.size() > 0) { 4 return scores_.sum()/scores_.size(); 5 } 6 return 0; 7 } 8 const std::string & Student::Name()const{ 9 return name_; 10 } 11 double & Student::operator[](int i){ 12 return scores_[i]; 13 } 14 double Student::operator[](int i)const{ 15 return scores_[i]; 16 } 17 std::istream & operator>>(std::istream & is, Student & stu){ 18 is >> stu.name_; 19 return is; 20 } 21 std::istream & getline(std::istream & is, Student & stu){ 22 getline(is, stu.name_); 23 return is; 24 } 25 std::ostream & operator<<(std::ostream & os, const Student & stu){ 26 os<< "Scores for " << stu.name_ << ":\n"; 27 stu.arr_out(os); 28 return os; 29 } 30 std::ostream & Student::arr_out(std::ostream &os)const{ 31 int i; 32 size_t lim = scores_.size(); 33 if (lim > 0) { 34 for (i = 0; i < lim; i ++) { 35 os << scores_[i] << ","; 36 if (i%5 == 4) 37 os << std::endl; 38 } 39 if (i % 5 != 0) 40 os << std::endl; 41 } 42 else 43 os << "empty array "; 44 return os; 45 }
main.cpp
#include <iostream> #include "Student.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(int argc, const char * argv[]) { Student ada[pupils] = {Student(quizzes), Student(quizzes), Student(quizzes)}; int i; for (i = 0; i < pupils; i ++) { set(ada[i], quizzes); } cout << "学生名单:\n"; for (i = 0; i < pupils; i ++ ) { cout << ada[i].Name() << endl; } cout << "\n测试结果:"; for (i = 0; i < pupils; i++) { cout << endl << ada[i]; cout << "平均成绩:" << ada[i].Average()<< endl; } cout << "完成\n"; return 0; } void set(Student & sa, int n){ cout << "请输入学生的名字:\n"; getline(cin, sa); cout << "请输入"<< n << "测试成绩:\n"; for (int i = 0; i < n ; i++) cin >> sa[i]; while (cin.get() != '\n') continue; } 输出结果: 请输入学生的名字: xiaohong 请输入5测试成绩: 87.4 90 89 78 94 请输入学生的名字: xiaolan 请输入5测试成绩: 78 45 91 23 46 请输入学生的名字: huyu 请输入5测试成绩: 90 98 99 97 100 学生名单: xiaohong xiaolan huyu 测试结果: Scores for xiaohong: 87.4,90,89,78,94, 平均成绩:87.68 Scores for xiaolan: 78,45,91,23,46, 平均成绩:56.6 Scores for huyu: 90,98,99,97,100, 平均成绩:96.8 完成
二、私有继承
C++还有另外一种实现has-a关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生类对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是is-a关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口;这种不完全继承是has-a关系的一部分。
使用私有继承,类将继承实现。
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们使用术语子对象(subobject)来表示通过继承或包含添加的对象。
因此私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。
1、Student类示例(新版本)
要进行私有继承,请使用关键字private而不是public来定义类(实际上,private是默认值,因此省略访问限定符也将导致私有继承)。
Student类应从两个类派生而来,因此声明将列出这两个类:
class Student:private std::string, private std::valarray<double>{
public:
...
};
使用多个基类的继承被称为多重继承(multiple inheritance,MI)。
下面我们对Student类进行重新设计,新版本的名字叫做Student1,他使用私有继承来实现包含关系。
(1)初始化基类组件
对于私有继承类,构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标示构造函数:
String1(const char * s, const double * pd, int n):std::string(s), ArrayDb(pd, n){}
私有继承与包含唯一不同的地方在于:私有继承省略了显式对象名称,并在內联构造函数中使用了类名,而不是成员名。
Student1.h
1 #include <iostream> 2 #include <string> 3 #include <valarray> 4 5 class Student1:private std::string, private std::valarray<double>{ 6 private: 7 typedef std::valarray<double> ArrayDb; 8 std::ostream & arr_out(std::ostream & os) const; 9 public: 10 Student1():std::string("Null Student"),ArrayDb(){}; 11 explicit Student1(const std::string & s):std::string(s),ArrayDb(){}; 12 explicit Student1(int n):std::string("Nully"),ArrayDb(n){}; 13 Student1(const std::string & s, int n):std::string(s), ArrayDb(n){}; 14 Student1(const std::string & s, const ArrayDb & a):std::string(s), ArrayDb(a){}; 15 Student1(const char * s, const double * pd, int n):std::string(s), ArrayDb(pd, n){}; 16 ~Student1(){}; 17 double Average()const; 18 double & operator[](int n); 19 double operator[](int n)const; 20 const std::string & Name()const; 21 friend std::istream & operator>>(std::istream & is, Student1 & stu); 22 friend std::istream & getline(std::istream & is, Student1 & stu); 23 friend std::ostream & operator<<(std::ostream & os, const Student1 & stu); 24 };
(2)访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类的方法。私有继承能够使用类名和作用域解析运算符来调用基类的方法。
总之,使用包含时将使用对象名来调用方法,而实用私有继承的时候将使用类名和作用域解析运算符来调用方法。
(3)访问基类对象
使用作用域解析运算符可以访问基类方法,但如果要使用基类对象本身需要进行强制类型转换,即将派生类强制转换为基类对象,结果将为继承而来的基类对象。
(4)访问基类的友元函数
与公有继承一样,要想访问基类的友元函数,需要对派生类进行强制类型转换,才能正确匹配基类友元函数的原型。
有一点需要注意,在私有继承中,未进行显式转换的派生类引用或指针,无法赋给基类的引用或指针。
Student1.cpp
1 #include "String1.h" 2 double Student1::Average()const{ 3 if (ArrayDb::size() > 0) { 4 return ArrayDb::sum()/ArrayDb::size(); 5 } 6 return 0; 7 } 8 double & Student1::operator[](int n){ 9 return ArrayDb::operator[](n); 10 } 11 double Student1::operator[](int n)const{ 12 return ArrayDb::operator[](n); 13 } 14 const std::string & Student1::Name()const{ 15 return (const std::string &) *this;//因为Student1是string的派生类,因此可以使用向上强制类型转换来访问string数据部分 16 } 17 std::istream & operator>>(std::istream & is, Student1 & stu){ 18 is >> (std::string &)stu; 19 return is; 20 } 21 std::istream & getline(std::istream & is, Student1 & stu){ 22 getline(is, (std::string & )stu); 23 return is; 24 } 25 std::ostream & operator<<(std::ostream & os, const Student1 & stu){ 26 os << "Scores for " << (const std::string &)stu << ": \n"; 27 stu.arr_out(os); 28 return os; 29 } 30 std::ostream & Student1::arr_out(std::ostream &os)const{ 31 int i; 32 size_t lim = ArrayDb::size(); 33 if (lim > 0) { 34 for (i = 0; i < lim; i++) { 35 os<<ArrayDb::operator[](i) << ","; 36 if (i % 5 == 4) { 37 os << std::endl; 38 } 39 } 40 if (i % 5 != 0) { 41 os << std::endl; 42 } 43 } 44 else 45 os << "empty array."; 46 return os; 47 }
2、使用包含还是私有继承
大多数C++程序员倾向于使用包含。首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。总之,实用包含不太可能遇到这样的麻烦。另外,包含能够包括多个同类的子对象,而继承只能使用一个这样的对象。
然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。
另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
提示:通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
3、保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键protected:
class Student:protected std::string, protected std::valarray<double>{...};
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法:使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
下面的表总结了公有、私有和保护继承。隐式向上转换意味着无需进行显式类型转换,就可以将基类的指针或引用指向派生类对象。
特征 | 公有继承 | 保护继承 | 私有继承 |
公有成员变量 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变量 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变量 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
4、使用using重新定义访问权限
使用保护派生和私有派生时,基类的公有成员将会成为保护成员或私有成员。如果要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。
另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如,假设希望通过Student类能够使用valarray的方法min()和max(),可以在Student的声明的公有部分加入如下using声明:
class Student:private std::string, private std::valarray<double>{
....
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
...
};
上述using声明使得valarray<double>::min()和valarray<double>::max()可用,就像他们是Student的公有方法一样。
注意,using声明只使用成员名——没有圆括号、函数特征标和返回类型。例如,为使Student类可以使用valarray的operator[]()方法,只需要在Student类声明的公有部分包含下面的using声明:
using std::valarray<double>::operator[]();
这将使两个版本(const和非const)都可用。这样,便可以删除Student::operator[]()的原型和定义。
注意,using声明只适用于继承,而不适用于包含。
三、多重继承
MI描述的是有多个直接基类的类。与单继承一样,公有MI表示的也是is-a关系。注意,私有MI和保护MI可以表示has-a关系。
MI可能会带来很多新问题。其中两个主要的问题是:从两个不同的基类继承同名方法;从两个或更多相关基类那里继承同一个类的多个实例。
现在有下面的继承关系:
上面的关系,表示成代码如下:
1 #include <iostream> 2 3 class A{ 4 int a; 5 public: 6 A(int n = 0):a(n) {}; 7 virtual~A(){}; 8 virtual void show(){std::cout << "a:" << a;}; 9 }; 10 class B:public A{ 11 int b; 12 public: 13 B(int a = 0, int n = 0):A(a),b(n){}; 14 virtual ~B(){}; 15 virtual void show(){A::show();std::cout << ", b:" << b;}; 16 }; 17 class C:public A{ 18 int c; 19 public: 20 C(int a = 0, int n = 0):A(a),c(n){}; 21 virtual ~C(){}; 22 virtual void show(){A::show();std::cout << ", c:" << c;}; 23 }; 24 class D:public B, public C{ 25 int d; 26 public: 27 D(int b1 = 0, int b2 = 0,int c1 = 0, int c2 = 0, int n = 0):B(b1,b2),C(c1,c2),d(n){}; 28 void show(){B::show();std::cout << ", ";C::show();std::cout << ", ";std::cout << " d:" << d ; }; 29 };
1、在D中有多少个A?
因为B和C都继承了一个A组件,因此D将包含两个A组件。
` 正如预期的,这将会引起问题。通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:
D d;
A * pa = &d;//发生二义性
通常,这种赋值将把基类指针设置为派生类对象中基类对象的地址,但是d中包含两个A对象,有两个地址可供选择,所有应使用类型转换来指定对象:
A * pa = (B *)&d;
A * pa2 = (C *)&d;
这将使得使用基类指针来引用不同的对象(多态性)复杂化。
包含两个A对象拷贝还会导致其他的问题。然而,真正的问题是:为什么需要A对象的两个拷贝?因为D公有继承自B和C,而B和C又都公有继承自A,即D既是B也是C,同时B、C、D又都是A,他们之间是is-a关系;那么D对象中包含两个A子对象将很奇怪,D对象中应该只有一个A子对象而不是两个或多个。C++引入多继承的同时,引入了一种新技术——虚基类(virtual base class),使MI成为可能。
(1)虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象,这种技术是通过在类声明中加入关键字virtual来实现的(virtual和public的顺序无关紧要):
class B:public virtual A{....};
class C:virtual public A{....};
然后,可以将D类定义为:
class D:public B, public C{....};
现在D对象中将只包含A对象的一个副本。从本质上说,继承的B和C对象共享一个A对象,而不是各自引入自己的A对象副本。
(2)新的构造函数
使用虚基类时,需要对类的构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给基类。例如,可能有下面的构造函数:
class A{
int a;
public:
A(int n = 0):a(n){};
.....
};
class B:public A{
int b;
public:
B(int a = 0, int n = 0):A(a), b(n){};
...
};
class E: public B{
int e;
public:
E(int a = 0, int b = 0, int n = 0):B(a,b), e(n){};
.....
};
在这里,E类的构造函数只能调用B类的构造函数,而B类的构造函数只能调用A类的构造函数。这里,E类的构造函数将使用n值,并将a和b传递给B类的构造函数;而B类的构造函数使用值b,并将a传递给A类的构造函数。
但是,如果A是虚基类:
class A{
int a;
public:
A(int n = 0):a(n){};
...
};
class F:virtual public A{
int f;
public:
F(int a = 0, int n = 0):A(a),f(n){};
....
};
class G:public virtual A{
int g;
public:
G(int a = 0, int n = 0):A(a), f(n){};
....
};
则这种信息自动传递将不起作用。例如:
class H:public F, public G{
int h;
public:
H(int a = 0, int f = 0, int g = 0, int n = 0):F(a,f),G(a,g),h(n){};
.....
};
存在的问题是,自动传递信息时,将通过2条不同的途径(F和G)将a传递给A对象。未避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上面的构造函数将初始化成员f和g,但a参数中的信息将不会传递给子对象A。然而,编译器必须在构造派生对象前构造基类对象组件:在上述情况下,编译器将使用A的默认构造函数。
如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函数应该这样:
class H:public F, public G{
int h;
public:
H(int a = 0, int f = 0, int g = 0, int n = 0):A(a),F(a,f),G(a,g),h(n){};
...
};
上述代码显式调用构造函数A(int )。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。
警告:如果类有间接虚基类,则除非只需要使用该虚基类的默认构造函数,否则必须显式调用该虚基类的某个构造函数。
2、哪个方法
假设有如下几个类A、B、C、D,它们之间的关系如下:
1 #include <iostream> 2 3 class A{ 4 int a; 5 public: 6 A(int n = 0):a(n) {}; 7 virtual~A(){}; 8 virtual void show(){std::cout << "a:" << a;}; 9 }; 10 class B:virtual public A{ 11 int b; 12 public: 13 B(int a = 0, int n = 0):A(a),b(n){}; 14 virtual ~B(){}; 15 virtual void show(){A::show();std::cout << ", b:" << b;}; 16 }; 17 class C:virtual public A{ 18 int c; 19 public: 20 C(int a = 0, int n = 0):A(a),c(n){}; 21 virtual ~C(){}; 22 virtual void show(){A::show();std::cout << ", c:" << c;}; 23 }; 24 class D:public B, public C{ 25 int d; 26 public: 27 D(int a = 0, int b = 0,int c = 0, int n = 0):A(a),B(a,b),C(a,c),d(n){};28 };
现在有如下问题,我们试图用D对象调用继承的show()方法:
D d(12,33,45,14,53);
d.show();
对于单继承,如果没有重新定义show(),则将使用最近祖先中的定义。而在多继承中,每个直接祖先都有一个show()函数,这使得上述调用是二义性的。
警告:多重继承可能导致函数调用的二义性。
可以使用作用域解析运算符来澄清意图:
D d1(90,123,435,223,55);
d1.B::show();
d1.C::show();
然而,更好的方法是在D中重新定义show()方法,并指出要使用哪个show()。例如,如果希望使用D对象使用B版本的show(),则可以这样做:
void D::show(){
B::show();
}
对于单继承,可以让派生类方法调用基类方法显示基类信息,并添加自己的信息,即递增的方式来显示派生类对象的信息。但是这种方法对于多继承来说是有缺陷的。例如,在D类的show()方法中同时调用基类B和基类C的show()方法,并加上自己的信息:
void D::show(){
B::show();
C::show();
std::cout << d;
}
然而,像上面的方法将会显示虚基类对象的信息两次,即会显示两次a的信息。因为B::show()和C::show()都调用了A::show()。
解决这种问题的一种方法是使用模块化,而不是递增方式,即提供一个只显示A组件的方法和一个只显示B组件或C组件(而不是B和C组件)的方法。然后,在D::show()方法中将组件合起来。例如,可以像下面这样做:
void A::Data()const{
std::cout << "a:" << a;
}
void B::Data()const{
std::cout << "b:" << b;
}
void C::Data()const{
std::cout << "c:" << c;
}
void D::Data()const{
B::Data();
std::cout << ", ";
C::Data();
}
void D::show()const{
A::Data();
std::cout << ", ";
Data();
}
与此相似,其他show()方法可以组合适当的Data()组件。
采用这种方法,对象仍可使用show()方法。而Data()方法只在类内部可用,作为协助公有接口的辅助方法。然而,使Data()方法成为私有的将阻止B中的代码使用C::Data(),这正是保护访问类的用武之地。如果Data()方法是保护的,则只能在继承层次结构中的类使用它,在其他地方则不能使用。
另一种方法是将所有的数据组件都设置为保护的,而不是私有的,不过使用保护方法(而不是保护数据)将可以更严格地控制对数据的访问。
总之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写他们。
下面是对A、B、C、D的完整代码:
1 #include <iostream>
2 3 class A{ 4 private: 5 int a; 6 protected: 7 void Data()const{ 8 std::cout << "a:" << a; 9 } 10 public: 11 A(int n = 0):a(n) {}; 12 virtual~A(){}; 13 virtual void show(){ 14 Data(); 15 } 16 }; 17 class B:virtual public A{ 18 private: 19 int b; 20 protected: 21 void Data()const{ 22 std::cout << "b:" << b; 23 }; 24 public: 25 B(int a = 0, int n = 0):A(a),b(n){}; 26 virtual ~B(){}; 27 virtual void show(){ 28 A::Data(); 29 std::cout << ", "; 30 Data(); 31 } 32 }; 33 class C:virtual public A{ 34 private: 35 int c; 36 protected: 37 void Data()const{ 38 std::cout << "c:" << c; 39 } 40 public: 41 C(int a = 0, int n = 0):A(a),c(n){}; 42 virtual ~C(){}; 43 virtual void show(){ 44 A::Data(); 45 std::cout << ", "; 46 Data(); 47 } 48 }; 49 class D:public B, public C{ 50 private: 51 int d; 52 protected: 53 void Data()const{ 54 std::cout << "d:" << d; 55 } 56 public: 57 D(int a = 0, int b = 0,int c = 0, int n = 0):A(a).B(a,b),C(a,c),d(n){}; 58 void show(){ 59 A::Data(); 60 std::cout << ", "; 61 B::Data(); 62 std::cout << ", "; 63 C::Data(); 64 std::cout << ", "; 65 Data(); 66 } 67 };
main.cpp
1 #include <iostream> 2 #include "ABCD.h" 3 4 using namespace std; 5 6 int main(int argc, const char * argv[]) { 7 D d{12,33,45,22}; 8 d.show(); 9 10 return 0; 11 }
输出结构:
a:12, b:33, c:45, d:22
(1)混合使用虚基类和非虚基类
通过多种途径继承一个基类的派生类的情况:
*如果基类是虚基类,派生类将包含基类的一个子对象;
*如果基类不是虚基类,派生类将包含多个子对象;
*当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
(2)虚基类和支配
使用虚基类将改变C++解析二义性的方式。
使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有使用类名将导致二义性。
但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于其他所有名称,则使用它,即便不使用限定符,也不会导致二义性。
派生类中的名称优先于直接或间接祖先类中的相同名称。
3、MI小结
(1)不使用虚基类的MI
*这种形式的MI不会引入新的规则。然而,如果一个类从两个不同类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。
*如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。在某些情况下,这可能正是需要的,但通常情况下,多个基类实例都是问题。
(2)使用虚基类的MI
当派生类使用关键字virtual来指示派生时,基类就称为虚基类。主要变化(同时也是使用虚基类的原因)是,从虚基类的一个或多个实例派生而来的类将只继承一个基类对象。为实现这种特性,必须满足其他要求:
*有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的。
*通过优先规则解决名称二义性。
MI会增加编程的复杂度。然而,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的。避免这种情况后,唯一需要注意的是,在必要时对继承的名称进行限定。
四、模板类
1、定义类模板
模板提供参数化类型,即能够将类型名作为参数传递给接收方来建立类或函数。
和模板函数一样,模板类以下面这样的代码开头:
template <class Type>
关键字template告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。
这里使用class并不意味着Type必须是一个类;而只是表明Type是一个通用的类型说明符,在使用模板时,将使用实际的类型替换他。较新的C++实现允许在这种情况下使用不太容易混淆的关键字typename来代替class:
template<typename Type>//新选择
可以使用自己的范型名代替Type,其命名规则与其他标识符相同。当前流行的选项包括T和Type。当模板被调用时,Type将被具体的类型值取代。
下面,我们对前面的栈Stack用模板类来实现:
Stack.h
1 #include <iostream> 2 3 template <class Type> 4 class Stack{ 5 private: 6 enum {MAX = 10}; 7 Type items[MAX]; 8 int top; 9 public: 10 Stack(); 11 bool isempty(); 12 bool isfull(); 13 bool push(const Type & item); 14 bool pop(Type & item); 15 }; 16 template<class Type> 17 Stack<Type>::Stack(){ 18 top = 0; 19 } 20 template<class Type> 21 bool Stack<Type>::isempty(){ 22 return top == 0; 23 } 24 template<class Type> 25 bool Stack<Type>::isfull(){ 26 return top == MAX; 27 } 28 template<class Type> 29 bool Stack<Type>::push(const Type &item){ 30 if (top < MAX) { 31 items[top ++] = item; 32 return true; 33 } 34 return false; 35 } 36 template<class Type> 37 bool Stack<Type>::pop(Type &item){ 38 if (top > 0) { 39 item = items[--top]; 40 return true; 41 } 42 return false; 43 }
注意,类模板和成员函数模板不是类和成员函数的定义。它们是C++编译器指令,说明了如何生成类和成员函数定义。模板的具体实现被称为实例化或具体化。不能将模板成员函数放在独立的实现文件中。由于模板不是函数,他不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
2、使用模板
仅仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换范型名。例如,下面的代码创建两个栈,一个用于存储int,另一个用于存储string对象:
Stack<int> stack_int; Stack<string> stack_str;
范型标识符——例如这里的Type——称为类型参数,这意味着他们类似于变量,但赋给他们的不能是数字,而只能是类型。注意,在使用类模板的时候必须现实提供所需的类型。
1 #include <iostream> 2 #include "Stack.h" 3 4 using namespace std; 5 6 int main(int argc, const char * argv[]) { 7 Stack<int> stack_int; 8 int num = 5; 9 while (!stack_int.isfull()) { 10 cout << num << ","; 11 stack_int.push(num); 12 num +=20; 13 } 14 cout << endl; 15 while (!stack_int.isempty()) { 16 stack_int.pop(num); 17 cout << num << ","; 18 } 19 return 0; 20 } 21 22 输出结果: 23 5,25,45,65,85,105,125,145,165,185, 24 185,165,145,125,105,85,65,45,25,5,
3、深入讨论模板类
对于上面的Stack模板类的功能不是很完善,比如不能制定模板大小等等。下面我们对Stack模板进行重新设计,使其能根据实际需要设定栈长度,那我们将会采用动态内存分配,因此,还需要显式定义复制构造函数和赋值运算符。
#include <iostream> template <class Type> class Stack{ private: enum {MAX = 10}; int stacksize; Type *items; int top; public: explicit Stack(int size = MAX); Stack(const Stack &); bool isempty(); bool isfull(); bool push(const Type & item); bool pop(Type & item); Stack & operator=(const Stack & st); }; template<class Type> Stack<Type>::Stack(int size){ stacksize = size; items = new Type[stacksize]; top = 0; } template<class Type> Stack<Type>::Stack(const Stack & st){ stacksize = st.stacksize; top = st.top; delete [] items; items = new Type[stacksize]; for (int i = 0; i < stacksize; i++) { items[i] = st.items[i]; } } 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; } return false; } template<class Type> bool Stack<Type>::pop(Type &item){ if (top > 0) { item = items[--top]; return true; } return false; } 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 < stacksize; i ++ ) { items[i] = st.items[i]; } return *this; }
原型将赋值运算符的返回值声明为Stack引用,而实际的模板函数定义需要将类型定义为Stack<Type>。前者是后者的缩写,但只能在类中使用。
下面我们用上面的模板类设计一个小程序:
1 #include <iostream> 2 #include "Stack.h" 3 #include <cstdlib> 4 #include <ctime> 5 6 const int Num = 10; 7 using namespace std; 8 9 int main(int argc, const char * argv[]) { 10 srand(time(0)); 11 cout << "请输入栈的长度:"; 12 int stacksize; 13 cin >> stacksize; 14 Stack<const char *> st(stacksize); 15 16 const char * in[Num] = { 17 "1:小红","2:小蓝","3:小余","4:小黄","5:小明","6:小样","7:小平","8:小曲","9:小波","10:小萌" 18 }; 19 const char *out[Num]; 20 int processed = 0; 21 int nextin = 0; 22 while (processed < Num) { 23 if (st.isempty()) 24 st.push(in[nextin++]); 25 else if(st.isfull()) 26 st.pop(out[processed++]); 27 else if(rand() % 2 && nextin < Num) 28 st.push(in[nextin++]); 29 else 30 st.pop(out[processed ++]); 31 } 32 for (int i = 0; i < Num; i ++) { 33 cout << out[i] << endl; 34 } 35 cout << "完成"; 36 return 0; 37 } 38 39 两次输出结果: 40 请输入栈的长度:5 41 1:小红 42 2:小蓝 43 3:小余 44 5:小明 45 4:小黄 46 7:小平 47 8:小曲 48 10:小萌 49 9:小波 50 6:小样 51 完成 52 53 请输入栈的长度:5 54 1:小红 55 2:小蓝 56 3:小余 57 5:小明 58 4:小黄 59 6:小样 60 10:小萌 61 9:小波 62 8:小曲 63 7:小平 64 完成
4、数组模板示例和非类型参数
模板常用做容器类,这是因为类型参数的概念非常适合于将相同的存储方案用于不同的类型。确实,为容器类提供可重用代码是引入模板的主要动机,所以我们来看另外一个例子,深入讨论模板设计和使用的其他几个方面。具体地说,将探讨一些非类型(或表达式)参数以及如何使用数组来处理继承族。
下面我们介绍一种使用模板参数来提供常规数组的大小:
1 #include <stdio.h> 2 #include <iostream> 3 4 template<class Type, int n>//提供非类型参数 5 class ArrayTP{ 6 private: 7 Type ar[n]; 8 public: 9 ArrayTP(){}; 10 explicit ArrayTP(const Type & v); 11 virtual Type & operator[](int i); 12 virtual Type operator[](int i)const; 13 }; 14 template<class T , int n> 15 ArrayTP<T,n>::ArrayTP(const T & v){ 16 for (int i = 0; i < n; i ++ ) { 17 ar[i] = v; 18 } 19 } 20 template <class T, int n> 21 T & ArrayTP<T, n>::operator[](int i){ 22 if (i < 0 || i >= n) { 23 std::cout << "输入的数字超出了范围\n"; 24 std::exit(EXIT_FAILURE); 25 } 26 return ar[i]; 27 } 28 template<class T, int n> 29 T ArrayTP<T, n>::operator[](int i)const{ 30 if (i < 0 || i >= n) { 31 std::cout << "超出了范围。\n"; 32 std::exit(EXIT_FAILURE); 33 } 34 return ar[i]; 35 }
template<class Type, int n>
关键字class(或在这种上下文中等价的关键字typename)指出T为类型参数,int指出n的类型为int。这种参数(指定特殊的类型而不是用作范型名)称为非类型(non-type)或表达式(expression)参数。假设有下面的声明:
ArrayTP<double, 12>arr;
上面的声明将导致编译器定义名为ArrayTP<double, 12>的类,,并创建一个类型为ArrayTP<double,12>的arr对象。定义类时,编译器将使用double替换T,使用12替换n。
表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。另外,模板代码不能修改参数的值,也不能使用参数的地址。另外,实例化模板时,用作表达式参数的值必须是常量表达式。
与Stack中使用的构造函数方法相比,这种改变数组大小的方法有一个优点。构造函数方法使用的是new和delete管理的对内存,而表达式参数方法使用的是为自动变量维护的内存栈。这样执行速度更快,尤其是在使用很多小型数组时。
表达式参数的主要缺点是,没种数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明:
ArrayTP<double, 12> arr1;
ArrayTP<double, 13> arr2;
但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:
Stack<int> stack1;
Stack<int> Stack2;
另一个区别是,构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
5、模板多功能性
可以将用于常规类的技术用于模板类。模板类可用作基类,也可用做组件类,还可用作其他模板的类型参数。例如,例如下面用数组模板来实现栈模板:
#include <iostream> #include "Stack.h" #include "ArrayTP.h" using namespace std; int main(int argc, const char * argv[]) { Stack<ArrayTP<int, 12> > stack{12};//在这条语句中,C++98要求使用至少一个空白字符将两个>符号分开,以免与>>混淆。C++11中不要求这样做。 int num = 4; while (!stack.isfull()) { ArrayTP<int, 12> arr{num}; stack.push(arr); num += 20; } return 0; }
在第一条语句中,C++98要求使用至少一个空白符将两个>符号分开,以免与运算符>>混淆。C++11不要求这样做。
(1)递归使用模板
另一个模板多功能性的例子是,可以递归使用模板。例如,可以这样使用ArrayTP模板:
ArrayTP<ArrayTP<int, 5>, 10> twodee;
这使得twodee是一个包含10个元素的数组,每个元素都是一个包含5个int元素的数组。与之等价的常规数组声明为:
int twodee[10][5];
请注意,在模板语法中,维de顺序与等价的二维数组相反。
(2)使用多个类型参数
模板可以包含多个类型参数。例如,可以定义个一个模板类Item接受两个类型参数:
template<class T1, class T2>
class Item{
private:
T1 item1;
T2 item2;
public:
......
};
(3)默认类型模板参数
类模板的另一项特性是,可以为类型参数提供默认值:
template<class T1, class T2 = int> class Topo{...};
这样,如果省略T2的值,编译器将会使用默认类型int:
Topo<double, double> m1;
Topo<double> m2;
6、模板的具体化
类模板与函数模板很相似,因为可以隐式实例化、显式实例化和显式具体化,它们统称为具体化。模板以范型的方式描述类,而具体化是使用具体的类型生成类声明。
(1)隐式实例化
到目前为止,所有的模板示例使用的都是隐式实例化,即它们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。
(2)显式实例化
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间中。例如,下面的声明将ArrayTP<string,100>声明为一个类:
template class ArrayTP<string, 100>;
在这种情况下,虽然没有创建或提及对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
(3)显式具体化
显式具体化是特定类型(用于替换模板中的范型)的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。
具体化类模板定义的格式如下:
template<> class Classname<specialized-type-name>{...};
早期的编译器可能只能识别早期的格式,这种格式不包括前缀template<>:
class Classname<specialized-type-name>{....};
(4)部分具体化
C++还允许部分具体化,即部分限制模板的通用性。例如,部分具体化可以给类型参数之一指定具体的类型:
//通用模板
template<class T1, class T2> class Pair{...};
//将T2部分具体化为int
template<class T1> class Pair<T1,int>{...};
关键字template后面的<>声明的是没有被具体化的类型参数。因此,上面的第二个声明将T2具体化为int,但T1保持不变。注意,如果指定所有的类型,则<>内将为空,这将导致显式具体化:
template<> class Pair<int, int>{...};
如果有多个模板可供选择,编译器将使用具体化最高的模板。给定上述三个模板,情况如下:
Pair<double,double> p1;//使用通用模板
Pair<double,int> p2;//使用Pair<T1,int>部分显式具体化模板
Pair<int,int> p3;//使用Pair<int,int>显式具体化模板
也可以通过为指针提供特殊版本来部分具体化现有的模板:
template<class T>//通用模板
class Feed{...};
template<class T*>//指针部分具体化模板
class Feed{....};//定义代码
如果提供的类型不是指针,编译器将使用通用版本;如果提供的是指针,则编译器使用指针具体化版本:
Feed<char> fb1;//使用通用模板,T是char
Feed<char *> fb2;//使用T*部分具体化,T是char
如果没有进行部分具体化,则第二个声明将使用通用模板,将T转换为char *类型。如果进行了部分具体化,则第二个声明将使用具体化模板,将T转换为char。
部分具体化特性使得能够设置各种限制。例如,可以这样做:
//通用模板
template<class T1, class T2, class T3>class Trio{...};
//用T2部分具体化T3
template<class T1, class T2> class Trio<T1,T2,T2>{...};
//用T1*部分显式具体化T2和T3
template<class T1> class Trio<T1, T1 *, T1 *>{...};
给定上述声明,编译器将作出如下选择:
Trio<int, short, char *> t1;//使用通用模板
Trio<int, char> t2;//使用Trio<T1,T2,T2>
Trio<int,int *, int *> t3;//使用Trio<T1, T1 *, T1 *>
7、成员模板
模板可用作结构、类和模板类的成员。要完成STL的设计,必须使用这项特性。下面的程序是一个简单的模板类示例,该模板类将另一个模板类和和模板函数作为其成员。
1 #include <stdio.h> 2 #include <iostream> 3 using std::cout; 4 using std::endl; 5 template<class T> 6 class Beta{ 7 private: 8 template<typename V> 9 class Hold{ 10 private: 11 V val; 12 public: 13 Hold(V v = 0):val(v){}; 14 void show()const{cout << val << endl;} 15 V Value()const {return val;}; 16 }; 17 Hold<T> q; 18 Hold<int> n; 19 public: 20 Beta(T t, int i):q(t),n(i){}; 21 template<typename U> 22 U blab(U u, T t){return (n.Value() + q.Value())* u / t;} 23 void Show()const{q.show();n.show();} 24 };
#include <iostream> #include "Beta.h" using namespace std; int main(int argc, const char * argv[]) { Beta<double> guy{3.5,3}; guy.Show(); cout << guy.blab(10, 2.3) << endl; cout << guy.blab(10.0, 2.3) << endl; return 0; } 输出结果: 3.5 3 28 28.2609
8、将模板用作参数
模板可以包含类型参数和非类型参数。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL。
9、模板类和友元
模板类声明也可以有友元。模板的友元分3类:
*非模板友元
*约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;
*非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
(1)模板类的非模板友元函数
在模板类中将一个常规函数声明为友元:
template<class T>
class HasFriend{
public:
friend void counts();
....
};
上面的声明使counts()函数成为模板所有实例化的友元。
counts()函数不是通过对象调用的(它是友元,不是成员函数),也没有对象参数,它可以访问全局对象;可以使用全局指针访问非全局对象;可以创建自己的对象;可以访问独立于对象的模板类的静态数据成员。
如果要为友元函数提供模板类参数,必须指明具体化。例如:
template<class T>
class HasFriedn
{
public:
friend void report(HasFriedn<T> &);
};
注意:report()本身并不是模板函数,而只是使用一个模板作为参数。这意味着必须为要使用的友元定义显式具体化,像上面的report()可以给他定义其他类型的显式具体化模板:
friend void report(HasFriend<int> &);
(2)模板类的约束模板友元函数
可以修改前面的例子,使友元函数本身成为模板。具体地说,为约束模板友元作准备,要使类的每一个具体化都获得与友元匹配的具体化。这比非模板友元复杂些,包含以下3步。
首先,在类定义的前面声明每个模板函数。
template<typename T> void counts();
template<typename T>void report(T &);
然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化。
template<class T1>
class HasFriend{
public:
friend void counts<T1>();
friend void report<>(HasFriend<T1> &);
};
声明中的<>指出这是模板具体化。对于report(),<>可以为空,因为可以从函数参数推断出如下模板类型参数:
HasFriend<T1>
然而,也可以使用:
friend void report<HasFriedn<T1> >(HasFriend<T1> &);
最后,为友元提供模板定义。
(3)模板类的非约束模板友元函数
通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。
对于非约束友元,友元模板类型参数与模板类类型参数是不同的:
template<typename T>
class ManyFriend{
....
template<class T1, class T2> friend void show(T1 &, T2 &);
};
10、模板别名
可以使用typedef为模板具体化指定别名:
typedef ArrayTP<doub,12> arrd;
typedef ArrayTP<int, 13> arre;
C++11新增了一项功能——使用模板提供一系列别名,如下面所示:
templaty<class T>
using arrtype = std::array<T,12>;
这样将arrtype定义为一个模板别名,可使用它来指定类型,如下所示:
arrtype<double> arr;
这将会使用arrtype<T>表示类型std::array<T,12>;
C++11允许将语法using=用于非模板。用于非模板时,这种语法与常规typedef等价:
typedef const char * pc1;
using pc2 = const char *;