类的包含(组合)、私有继承、保护继承、多重继承、虚基类、类模板
1.组合(包含):即创建一个包含其他类兑现的累
1)初始化被包含的对象 p439
构造函数可以使用成员初始化列表来初始化成员对象;对于成员对象,构造函数在成员初始化列表中使用成员对象名来调用特定的构造函数;初始化列表中的每一项都调用与之匹配的构造函数
class student { private: typedef std::valarray<double> ArrayDb; string name; ArrayDn scores; ... }; 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) {}
如果不适用舒适化列表语法,C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此如果省略初始化列表,C++将使用队员对象所属类的默认构造函数。
2)初始化顺序 p440
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。
2.私有继承 p443
1)使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员;这意味着基类方法不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们;
要进行私有继承,使用关键字 private 来定义类(private是默认值,因此省略访问限定符也将导致私有继承)
class Student : private std::string, private std::valarray<double> { ... public: ... };
2)组合(包含)将对象作为一个命名的成员对象添加到类中;而私有继承将对象作为一个未被命名的继承对象添加到类中;我们使用“子对象”来表示通过继承或包含添加的对象,私有继承了一个类,相当于继承了该类的一个无名对象。
上述的 Student 类不需要私有数据,因为两个基类已经提供了所需的所有数据成员;在上面的两个例子中,包含版本提供了两个被显示命名的对象成员,而私有继承提供了两个无名称的子对象成员。
3)初始化私有继承的类的无名称的子对象成员时,使用类名而不是成员名来表示构造函数
对于构造函数,包含使用这样的构造函数:
Student(const char * str, const double * pd, int n) : name(str), scores(pd,n) {}
对于私有继承,将使用类名而不是成员名来标识构造函数:
Student(const char * star, const double * pd, int n) : std::string(str), ArrayDb(pd, n) {}//ArrayDb 是 std::valarray<double> 的别名
4)使用包含时将使用对象名来调用方法,而使用私有继承时,使用类名和作用域解析运算符来调用基类中的方法 p444
double Student::Average() const { if (ArrayDb::size() > 0 ) //私有继承在派生类中使用类名和作用域解析运算符来调用基类方法 return ArrayDb::sum()/ArrayDb::size(); else return 0; }
5)私有继承时,用类名和作用域解析运算符可以访问基类的方法;如果要访问基类对象本身,需要使用强制类型转换 p445
在上面的例子中,Student类私有继承了string类,因此可以通过强制类型转换将Student对象转换为string对象,结果为继承而来的无名称的string对象
const string & Student::Name() const { return (const string &) *this;//*this 是调用该函数的一个Student对象,使用强制类型转换将Student对象转换为string对象;为了避免调用构造函数创建一个新对象,使用了强制类型转换来创建引用,该引用指向调用该方法的Student类对象中的无名称的string对象
}
6)私有继承中访问基类的友元函数 p445
可以通过显示转换为基类的指针或引用来调用基类的友元函数
ostream & operator<<(ostream & os, const Student & str) { os << "Scores for " << (const string &) stu << ":\n"; ... }
上述例子中将stu转换为string对象的引用,进而调用函数 operator<<(ostream &, const string &)
必须使用显示类型转换的原因之一是,因为类使用的是多重继承(MI),有可能在同时继承的两个基类中都有名为operator<<()的函数,那么编译器无法确定将stu转换为哪个基类;
此外,在公有继承中,也要使用显示类型转换;否则,下列代码
os << stu;
将仍与该友元函数本身匹配,进而产生递归调用。p426
注意,在公有继承中(p432),也是通过强制转换将派生列引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数(必须转换为基类引用或指针的原因是,如果形参是引用,那么非常量引用参数只能引用命名变量)
7)
在公有继承中,基类指针可以在不显式类型转换的情况下指向派生类对象;
在私有和保护继承中,因为基类的公有接口都将成为派生类的内部接口,因此派生类对象不能显式的使用基类的接口。所以,在不进行显式转换的情况下,基类指针或引用将不能指向派生类对象;p482
3.包含和私有继承的比较 p447
1)如果某个类需要3个string对象,使用包含可以声明3个独立的string成员;而继承只能使用一个这样的(无名称)的对象;
2)如果基类包含保护成员,使用包含不可访问包含对象的保护成员,但使用私有继承的派生类中可以访问基类的保护成员;
3)如果需要重新定义虚函数,私有派生类可以重新定义虚函数(但重新定义的虚函数只能在类中使用,不是公有的),但包含类不可以。
总结:通常,应使用包含来建立 has-a 关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应该使用私有继承。
4.保护继承 p448
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的;
当从派生类再派生出一个类时,私有继承和保护继承的区别便显现出来了:
使用私有继承时,第三代类将不能使用基类的接口,因为私有继承后基类的成员在派生类中都变成了私有成员;使用保护继承时,基类的公有和保护成员在派生类中还是保护成员,因此可以在第三代类中使用基类的公有和保护成员。
5.多重继承 MI p449
MI的两个主要问题是:从两个不同的基类中继承同名方法;从两个或更多相关基类那里继承同一个类的多个实例;
定义三个类:Worker,Waiter,Singer
class Worker // an abstract base class { 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; // pure virtual destructor 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 {private: static char *pv[7]; // 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; }; #endif
添加一个从Singer和Waiter类公有派生出的SingingWaiter类
class SingingWaiter: public Singer, public Waiter{...}
因为Singer和Waiter都继承了一个Worker组件,因此SingingWaiter将包含两个Worker组件,此时将引起问题:
通常可以将派生类对象的地址赋给基类指针,现在将出现二义性:
SingingWaiter ed; Worker * pw = &ed; //ambigous
通常,这种赋值将把基类指针设置为派生类对象中的基类对象的地址,但ed中包含两个Worker对象,有两个地址可供选择,所以应使用类型转换来指定对象:
Worker * pw1 = (Waiter *) &ed; //the worker in Waiter Worker * pw2 = (Singer *) &ed; //the worker in Singer
但是,SingingWaiter中应该和其他的Worker对象一样,也应该只包含一个姓名和一个ID,因此引入了虚基类。
1)虚基类:C++引入多重继承的同时,引入了虚基类技术(virtual base class) p453
虚基类使得从多个类(它们的基类相同)派生处的对象只继承一个基类对象。通过在派生类类声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类(virtual 和 public 的次序随意)
class Singer: virtual public Worker {...}; //注意,在派生类中使用关键字virtual,而不是在虚基类Worker中使用关键字virtual class Waiter: public virtual Worker {...};
2)使用虚基类时的构造函数:禁止信息通过中间类自动传递给基类 p454
对于下面的MI构造函数:
SingingWaiter(const Worker & wk, int p = 0, int v = 1) : Waiter(wk,p), Singer(wk,v) {} //Waiter(wk,p) 使用 Waiter(const Worker & wk, int p = 0):Worker(wk), panache(p){} //Singer(wk,v) 使用 Singer(const Worker 7 wk, int v = other):Worker(wk),voice(v){}
因为Worker是虚基类,禁止信息通过中间类自动传递给基类,因此上述构造函数将初始化成员panache和voice,但wk参数中的信息将不会传递给子对象Waiter。然而编译器必须在构造派生对象之前构造基类对象组件,因此编译器将使用Worker的默认构造函数。
我们可以显式地调用所需的基类构造函数:
SingingWatier(const Waiter & wk, int p = 0, int v = 1) : Worker(wk), Waiter(wk,p), Singer(wk,v) {}
注意,如果Worker是非虚基类,则上述构造函数是非法的。
总结:如果类有间接虚基类,则除非只需要使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
3)多重继承可能导致函数调用的二义性 p455
Waiter 和 Singer 类中都有 Show() 方法;如果没有在SingingWaiter类中定义 Show() 方法,并使用SingingWaiter对象调用继承的 Show() 方法,将导致二义性:
SingingWaiter newhire("Elise Hawsk", 2005, 6, soprano); newhire.Show(); //ambigous
对于单继承,如果没有重新定义 Show(),将使用最近祖先中的定义;在多重继承中,每个直接祖先都有一个 Show() 函数,使得上述调用是二义性的。
① 可以使用作用域解析运算符来显式地调用某一个直接祖先中的 Show() 方法:
SingingWaiter newhire("Elise Hawsk", 2005, 6, soprano); newhire.Singer::Show();
② 可以在派生类中重新定义 Show() 方法,并在该方法中决定使用哪一个直接祖先的 Show() 方法。
void SingingWaiter::Show() { Singer::Show(); }
4)混合使用虚基类和非虚基类 p461
如果基类是虚基类,派生类将包含基类的一个子对象;如果基类不是虚基类,派生类将包含多个子类对象;
当类通过多条虚途径和非虚途径继承某个特定的基类时,该类包含一个表示所有虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
上图中,类 M 从虚派生祖先(即类 C 和 D )哪里共继承了一个 B 类子对象;并从每一个非虚派生祖先(即类 X 和类 Y)分别继承了一个 B 类子对象,因此它包含3个B类子对象。
5)虚基类和支配:虚基类将改变C++解析二义性的方式
使用非虚基类时,如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员时,如果没有使用类名进行限定,将导致二义性;
使用虚基类时,如果某个名称优先于(domimates)其他所有名称,那么即使不使用限定符,也不会导致二义性。
其中:
①派生类的名称优先于直接或间接祖先类中的相同名称;
class B { public: short q(); ... }; class C : vritual public B { public: long q(); int omg(); ... }; class D : public { ... }; class E : virtual public B { private: int omg(); ... }; class F : public D, public E { ... };
上述代码中在类 F 中,可以使用 q() 来表示 C::q() 。但任何一个 omg() 定义都不优先于其他 omg() 定义,因为 C 和 E 都不是对方的基类,因此在 F 中使用非限定的 omg() 将导致二义性。
②虚二义性规则与访问规则无关;
在上述例子中,即使 E::omg() 是私有的,不能在 F 中直接访问,使用 omg() 仍然导致二义性;
即使 C::q() 是私有的,它也优先于 B::q() ,在这种情况下,可以在 F 中使用 B::q(), 但如果不限定 q(), 则将要调用不可访问的 C::q() 。
6.类模板 p462
以 Stack 类为基础来建立模板,类声明如下:
type 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); };
采用模板时,将使用模板定义替换 Stack 声明,使用模板成员函数替换 Stack 成员函数。
1)类模板的开头
template <class Type> //or template <typename Type>
可以使用自己的泛型名代替 Type,如 T 等;当模板被调用时,Type 将被具体的类型值(如 int 或 string)取代。
对于 Stack 类来说,应将声明中的所有的 typedef 标识符 Item 替换为 Type:
Item items[MAX]; //应改为: Type items[MAX];
使用模板成员函数替换原有的类方法,每个函数头都将以相同的模板声明打头: p463
template <class Type>
同样应该使用泛型名 Type 替换 Item;还需要将类限定符从 Stack:: 改为 Stack<Type>:: 。
bool Stack::push(const Item & item) { ... } //应改为 template <typename Type> bool Stack<Type>::push(const Tpye & item) { ... }
如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
注意,这些模板不是类和成员函数的定义。它们是 C++编译器指令,说明了如何生辰类和成员函数的定义;模板的具体实现被成为实例化或具体化;
不能将模板成员函数放在独立的实现文件中;由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。(可以将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件)
2)类模板的使用
使用所需的具体类型替换泛型名 Type
Stack<int> kernels; //create a stack of ints Stack<string> colonels; //create a stackof string objects
编译器将按照 Stack<Type>模板来生成两个独立的类声明和两组独立的类方法。
3)指针栈:使用 char* 替代 string 对象 p465
让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。
如果要使 Stack 构造函数能接受一个大小可选的参数,则需要使用 new 关键字动态创建栈的大小;这涉及在内部使用动态数组,因此 Stack 类需要包含一个析构函数、一个复制构造函数和一个重载赋值运算符函数。 p467
4)非类型参数/表达式参数 p469
template <class T, int n> //非类型参数/表达式参数 class ArrayTP { private: T ar[n]; public: ArrayTP() {}; explicit ArrayTP(const T & v); //使用 explicit 防止一个参数的构造函数的隐式转换 ... }; template <class T, int n> ArrayTP<T, n>::ArrayTP(const T & v) { ... }
关键字 class(也可以是 typename)指出 T 为类型参数,int 指出 n 的类型为 int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型参数或表达式参数。
ArrayTP<double, 12> eggweights;
上述声明将导致编译器定义名为 ArrayTP<double, 12>的类,并创建一个类型为 ArrayTP<double, 12>的 eggweight 对象;定义类时,编译器使用 double 替换 T,使用 12 替换 n。
表达式参数可以是整形、枚举、引用或指针。因此 double m 是不合法的,但 double * rm 和 double * pm 是合法的。
此外模板代码不能修改参数的值,也不能使用参数的地址;所以上述代码中不能使用 n++ 和 &n 。此外,实例化模板时(如使用 ArrayTP<double, 12> eggweights;),用作表达式参数的值(即 ArrayTP<double, 12> eggweights;中的 12)必须是常量表达式。
与在 Stack 中使用动态方式构造栈大小的方式(使用 new 和 delete 维护内存)相比,表达式参数的方法使用的是为自动变量维护的内存栈;
但使用该方法,每种数组大小都将生成自己的模板:
ArrayTP<double, 12> eggweights; ArrayTP<double, 13> donuts;
上述代码将生成两个独立的类声明,并创建两个对象。
5)模板的多功能性 p470
①模板类可用作基类,可用作组件类,还可用作其他模板的类型参数
template <typename T> // or <class T> class Array { private: T entry; ... }; template <typename Type> class GrowArray : public Array<Type> {...}; //使用模板类 Array<Type> 作基类 template <typename Tp> class Stack { Array<Tp> ar; //使用 Array<Tp> 做组件类 ... }; ... Array < Stack<int> > asi; //使用模板类 Stack<int> 做 Array<Type> 模板类的类型参数
②递归使用模板
ArrayTP< ArrayTP<int,5>, 10> twodee;
上述代码声明了一个包含了 10 个数组元素的数组,每个元素是一个包含 5 个 int 元素的数组。
③使用多个类型参数 p472
模板可以包含多个类型参数
template <class T1, class T2> class pair { private: T1 a; T2 b; ... };
④默认类型模板参数 p473
可以为模板类的类型参数提供默认值:
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
注意,虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值;然后,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的
6)模板的具体化 p473
类模板也有隐式实例化,显式实例化和显式具体化;
同样的,使用具体化之前首先要有一个同名的类模板,因为具体化要凸显与该类模板的不同。
①显式实例化
使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化;
template class ArrayTP<string, 100>;
上述代码显式声明了一个 ArrayTP<string, 100> 类型的类;
在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。
②显式具体化
需要为特殊类型实例化时,对模板进行修改,使其行为不同;
假如已经为表示排序后数组的类定义了一个模板:
template <typename T> class SortedArray { ... };
假设模板使用 > 运算符来对值进行比较;对于数字适用,但字符串将按照字母顺序排序,这要求类定义使用 strcmp(),而不是 > 来对值进行比较;这种情况下可以提供一个显式模板具体化,这将采用为具体类型定义的模板,而不是为泛型定义的模板;
当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。
显式具体化类模板定义的格式如下:
template <> class Classname<specialized-type-name> { ... };
如要使用新的表示法提供一个专供 const char * 类型使用 SortedArray 模板:
template <> class SortedArray<const char *> { ... };
那么,当有如下代码时:
SortedArray<int> scores; //使用通用模板定义 SotredArray<const char *> dates; //使用显式具体化模板定义
③部分具体化
部分具体化可以给类型参数之一指定具体的类型
template <class T1, class T2> class Pair {...}; // 通用类模板 template <class T1> class Pair<T1, int> {...}; //部分具体化,将 T2 设置为 int 类型
关键字 template 后面的<>声明的是没有被具体化的类型参数。
注意,如果指定所有的类型,则<>内将为空,这将导致显式具体化:
template <> class Pair<int, int> {...};
如果有多个模板可供选择,编译器将使用具体化程度最高的模板
Pair<double, double> p1; //使用通用 Pair 模板 Pair<double, int> p2; //使用 Pair<T1, int> 部分具体化模板 Pair<int, int> p3; //使用 Pair<int, int> 显式具体化模板
也可以通过为指针提供部分具体化模板:
template <class T> class Feeb { ... }; template <class T*> class Feeb { ... };
如果提供的类型参数不是指针,则编译器将使用通用版本;如果提供的是指针,则编译器将使用指针具体化版本:
Feeb<char> fb1; //使用通用 Feeb 模板,T = char Feeb<char *>fb2; //使用指针部分具体化版本,T = char
如果没有进行部分具体化,则第二个声明将使用通用模板,将 T 转换为 char * 类型。
7)成员模板 p474
模板可用作结构、类或模板类的成员
8)将模板用作参数 p476
模板可以包含类型参数(如 typename T)和非类型参数(如 int n);模板还可以包含本身就是模板的参数。
template <template <typename T> class Thing> class Crab { private: Thing<int> s1; Thing<double> s2; public: Crab() {}; ... }; ... Crab<Stack> nebula; // Stack must match template <typename T> class thing
上述代码中,模板参数是 template <typename T>class Thing, 其中 template<typename T> class 是类型,Thing 是参数。
Stack 必须是一个模板类,且其声明与模板参数 Thing 的声明匹配:
template <typename T> class Stack { ... };
Crab的声明声明了两个对象:
Thing<int> s1; Thing<double> s2;
而前面的 nebula 声明将用 Stack<int> 替换 Thing<int>, 用 Stack<double> 替换 Thing<double> 。总之,模板类参数 Thing 将被替换为声明 Crab 对象时被用作模板参数的模板类型。
9)模板类和友元
模板的友元有 3 类:
- 非模板友元
- 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型
- 非约束(unbound)模板有缘,即友元的所有具体化都是类的每一个具体化的友元