C++学习笔记——C++中的代码重用
C++的一个主要目标是促进代码重用。除了公有继承之外,还可以通过包含、私有继承、保护继承实现。公有继承实现 is-a 关系,其余实现 has-a 关系。通过多重继承能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
包含对象成员的类
包含是C++实现代码重用的技术之一,包含指的是创建一个包含其他类对象的类。如下面的Student类所示,Student类包含 string 类对象和 valarray<double> 类对象。Student类将name和scores对象声明为私有的,意味着Student类的成员函数可以通过string和valarray<double>类的公有接口来访问和修改name和scores对象。但在Student类外面不能这么做。即Student类获得了其成员对象的实现,但是没有继承接口。
使用公有继承时,类可以继承接口,可能还继承实现(纯虚函数提供接口,不提供实现),获得接口也是is-a关系的组成部分。使用包含,类可以获得实现,但是不能获得接口。
初始化包含的对象使用成员名而不是类名,被包含对象的接口不是公有的,但可以在类方法中使用,例如可以用Student类的Student方法访问name和scores类对象的方法。
1 #pragma once
2
3 #include<iostream>
4 #include<string>
5 #include<valarray>
6
7 class Student {
8 private:
9 typedef std::valarray<double> ArrayDb;
10 std::string name;
11 ArrayDb scores;
12 std::ostream& arrOut(std::ostream& os) const;
13 public:
14 Student() : name("Null"), scores() {}
15 explicit Student(const std::string& s)
16 : name(s), scores() {}
17 explicit Student(int n)
18 : name("Null"), scores(n) {} //使用explicit防止单参数构造函数的隐式转换
19 Student(const std::string& s, int n) //初始化列表使用成员名来初始化,因为初始化的是成员对象,不是继承的对象(初始化继承的对象要使用类名)
20 : name(s), scores(n) {} //初始化的顺序是按声明时的顺序,不是成员初始化列表中的顺序
21 Student(const std::string& s, const ArrayDb& a)
22 : name(s), scores(a) {}
23 Student(const char* str, const double* pd, int n)
24 : name(str), scores(pd, n) {}
25 ~Student(){}
26 double Average() const;
27 const std::string& Name() const;
28 double& operator[](int i) {
29 return scores[i];
30 }
31 double operator[](int i) const {
32 return scores[i];
33 }
34
35 friend std::istream& operator>>(std::istream& is, Student& st);
36 friend std::istream& getline(std::istream& is, Student& st);
37 friend std::ostream& operator<<(std::ostream& os, const Student& st);
38 };
私有继承
私有继承也是实现 has-a 关系的途径之一,使用私有继承,基类中的公有成员和保护成员都成为派生类的私有成员,基类方法不会成为派生对象公有接口的一部分。包含将类对象作为一个命名的成员对象添加到类中,与包含不同的是私有继承将对象作为一个为未命名的继承对象添加至类中。术语 子对象 表示通过继承或包含添加的对象。
如下所示,通过私有继承为Student类提供了两个无名称的子对象成员,这是私有继承和包含的第一个区别。私有继承的特性和包含相同:获得实现,但不获得接口。
1 class Student : private std::string, private std::valarray<double> { //多重私有继承
2 public:
3 ...
4 };
1、初始化基类组件
不同于包含,隐式地继承组件与使用包含的代码不同。包含可以使用显式地成员对象名来描述对象,而隐式继承组件需要使用类名来表标识,这是私有继承和包含的第二个区别。
1 Student(const char* str, const double* pd, int n)
2 : name(str), scores(pd, n) {} //use object names for containment
3
4 Student(const char* str, const double* pd, int n)
5 : std::string(str), std::valarray(pd, n) {} // use class names for inheritance
2、访问基类的方法
使用私有继承,只能在派生类的方法中使用基类方法。用 类名+作用域解析运算符 来调用基类的方法,包含显式使用对象名来调用方法。
1 double Student::Average() const{
2 if(std::valarray::size() > 0)
3 return std::valarray::sum() / std::valarray::size():
4 else
5 return 0;
6 }
3、访问基类对象
可以使用作用域解析运算符访问基类的方法,但使用私有继承时,继承的组件对象是没有名称的,所以要使用 强制类型转换 来访问内部的基类对象。this指针指向用来调用类方法的对象,*this为用来调用类方法的对象。同时使用强制类型转换创建一个引用,避免调用构造函数创建新的对象。
1 const string& Student::Name() const {
2 return (const string&) *this;
3 }
4、访问基类的友元函数
友元不属于类,所有不能通过类名显式地限定函数名,只能通过显式地转换为基类来调用基类的友元函数。假设第 6 行代码中的 plato 为 Student 对象,该语句会调用上面的函数,stu 指向 plato。在私有继承中,在不进行显式类型转换的情况下,不能将派生类的引用或指针赋给基类引用或指针,因为不使用类型转换可能导致递归调用;另一个原因是如果类使用多重继承,编译器无法确定要转换成哪个基类。
os << (const string&) st << endl; 这行代码显式地将 st 转化为 string 对象引用,从而调用函数 operator<<(ostream&, const string&)。
1 ostream& operator<<(ostream& os, const Student& st) {
2 os << (const string&) st << endl; //如果去掉(const string&)强制类型转化,会导致递归调用
3 ...
4 }
5
6 cout << plato;
5、包含还是继承的选择
可以使用包含和私有继承建立 has-a 关系。通常更倾向于使用包含,类声明中标识被包含类的显式命名对象,代码可以通过名称引用这些对象。包含能包括多个同类的子对象,而继承只能有一个。
继承使关系较抽象,且多重继承时,需要额外处理(继承多个包含同名方法的独立的类或共享祖先的独立基类)。但是私有继承也有自己的特性,例如可以重新定义虚函数(包含类不能)、可以访问原有类的保护成员。
Tips:通常用包含建立 has-a 关系,若新类需要访问原有类的保护成员,或需要重新定义虚函数,则使用私有继承
保护继承
保护继承是私有继承的变体,使用保护继承时,基类的公有成员和保护成员都成为派生类的保护成员。不论是私有继承还是保护继承,基类的接口在二代类中都是可用的,在继承层次之外不可用。不同之处在于,私有继承的第三代类不能使用基类的接口。而使用保护继承时,基类的公有方法在第二代类中变成保护成员,第三代类此时仍可以访问基类的公有接口。
1 class Studnet : protected std::string, protected std::valarray<dobule>{
2 ...
3 };
特征
|
公有继承
|
保护继承
|
私有继承
|
公有成员变成
|
派生类公有成员 | 派生类保护成员 | 派生类私有成员 |
保护成员变成
|
派生类保护成员 | 派生类保护成员 | 派生类私有成员 |
私有成员变成
|
只能用基类接口访问 | 同公有继承 | 同公有继承 |
能否向上隐式转换
|
是
|
是(只能在派生类中)
|
否
|
使用using重新定义访问权限
使用保护派生或私有派生时,若想在类外面使用基类的方法,方法一是定义一个使用该基类方法的派生类方法。另一个是使用 using 声明(类似名称空间),指出派生类可以使用特定的基类成员。
using声明只能使用成员名——没有圆括号、函数特征标和返回类型,且using声明只适合于继承,不适用于包含。
1 class Student : private std::string, private std::valarray<double>{
2 ...
3 public:
4 using std::valarray<double>::min;
5 using std::valarray<double>::max;
6 };
多重继承
多重继承(MI)指的是有多个直接继承的类。公有MI表示 is-a 关系,私有MI和保护MI表示 has-a 关系。继承时需要使用关键字指出使用哪种继承方式,否则采用私有继承。公有多重继承比保护和私有多重继承要考虑更多。
MI需要考虑的问题:
1、从两个不同的基类继承同名方法;
2、从两个或更多相关基类继承同一个类的多个实例。
为解决上述问题,需要使用新规则和不同的语法
假设从Singer和Waiter派生出SingerWaiter类,由于Waiter和Singer都继承了一个Worker组件,所以SingingWaiter将包含两个Worker组件,这导致二义性。如第6、第7行代码所示,pw指针此时不知道要指向 ed 中的哪个Worker组件(ed包含两个Worker对象),解决办法是可以使用类型转换来指向所想要的Worker对象。
1 class Worker {...};
2 class Waiter : public Worker{...};
3 class Singer : public Worker{...};
4 class SingingWaiter : public Singer, public Waiter{...};
5
6 SingingWaiter ed;
7 Worker* pw = &ed; // ambiguous
8
9 Worker* pw1 = (Waiter*) &ed; //use Worker in Waiter
10 Worker* pw2 = (Singer*) &ed; //use Worker in Singer

1 #pragma once 2 3 #include<string> 4 5 class Worker { 6 private: 7 std::string name; 8 int id; 9 public: 10 Worker() : name("null"), id(0) {} 11 Worker(const std::string str, int n) : name(str), id(n) {} 12 13 virtual ~Worker() = 0; 14 virtual void set(); 15 virtual void show() const; 16 }; 17 18 class Waiter : public Worker { 19 private: 20 int panache; 21 public: 22 Waiter() : Worker(), panache(0) {} 23 Waiter(const std::string& str, int n, int p) : Worker(str, n), panache(p) {} 24 Waiter(const Worker& wk, int p) : Worker(wk), panache(p) {} 25 26 void set(); 27 void show() const; 28 29 }; 30 31 class Singer : public Worker { 32 protected: 33 enum {other, alto, contralto, soprano, bass, baritone, tenor}; 34 enum {Vtypes = 7}; 35 private: 36 static const char* pv[Vtypes]; 37 int voice; 38 public: 39 Singer() : Worker(), voice(other) {} 40 Singer(const std::string& str, int n, int v = other) : Worker(str, n), voice(v) {} 41 Singer(const Worker& wk, int v = other) : Worker(wk), voice(v) {} 42 void set(); 43 void show() const; 44 };

1 #include <iostream> 2 #include "Worker0.h" 3 4 using std::cout; 5 using std::cin; 6 using std::endl; 7 8 Worker::~Worker() {} 9 10 void Worker::set() { 11 cout << "Enter worker's name: " << endl; 12 getline(cin, name); 13 cout << "Enter worker's id: " << endl; 14 cin >> id; 15 while (cin.get() != '\n') 16 continue; 17 } 18 19 void Worker::show() const { 20 cout << "Name: " << name << endl; 21 cout << "id: " << id << endl; 22 } 23 24 void Waiter::set() { 25 Worker::set(); 26 cout << "Enter waiter's panache rating:" << endl; 27 cin >> panache; 28 while (cin.get() != '\n') 29 continue; 30 } 31 32 void Waiter::show() const { 33 cout << "Category: waiter" << endl; 34 Worker::show(); 35 cout << "Panache Rating: " << panache << endl; 36 } 37 38 const char* Singer::pv[] = { "other", "alto", "contralto", "soprano", "bass", "baritone", "tenor" }; 39 40 void Singer::set() { 41 Worker::set(); 42 cout << "Enter number for singer's vocal range:" << endl; 43 for (int i = 0; i < Vtypes; ++i) { 44 cout << i << ": " << pv[i] << " "; 45 } 46 cout << endl; 47 while (cin >> voice && (voice < 0 || voice >= Vtypes)) 48 cout << "Please enter a value >= 0 and < " << Vtypes << endl; 49 while (cin.get() != '\n') 50 continue; 51 } 52 53 void Singer::show() const { 54 cout << "Category: Singer" << endl; 55 Worker::show(); 56 cout << "vocal range: " << pv[voice] << endl; 57 }
这里存在一个问题:为什么需要Worker对象的两个拷贝?正常来说SingingWaiter对象也只要一个Worker组件,为此 C++引入一种新技术——虚基类,来解决这个问题。
1、虚基类
虚基类使得从多个类(它们基类相同)派生出的对象只继承一个基类对象。在类声明中,使用关键字virtual(顺序不要求),将Worker作为Singer和Waiter的虚基类,这样SinggerWaiter类中只包含Worker对象的一个副本,本质上继承的Singer和Witer对象共享一个Worker对象。
1 class Worker {...};
2 class Waiter : virtual public Worker{...};
3 class Singer : virtual public Worker{...};
4 class SingingWaiter : public Singer, public Waiter{...};
2、新的构造函数规则
使用虚基类时,类的构造函数需要采用新的方法。对于非虚基类,基类构造函数是唯一可以出现在初始化列表中的构造函数,它将信息传递给基类,初始化基类部分。例如派生类只能调用基类的构造函数,基类只能调用基类的基类的构造函数(如果有),并将所需的信息一层一层的传递上去。但如果Woker为虚基类时,这种自动传递将不起作用。
对于下述代码,如果允许自动传递信息,将有两条途径可以传递信息(通过Waiter类和Singer类传递)。为了避免这种冲突,C++在基类是虚的情况下,禁止信息通过中间类自动传递给基类。
1 SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other)
2 :Waiter(wk, p), Singer(wk, v) {} //flawed
上述构造函数只初始化成员,wk参数的信息不会传给子对象Waiter和Singer。但是编译器又必须在构造派生对象之前构造基类对象的组件,因此这种情况下编译器将使用Worker的默认构造函数,如果不想使用基类的默认构造函数,则需要显式调用所需的基类构造函数。
Tips:如果类有间接虚基类,且不想使用该虚基类的默认构造函数,那么需要显式的调用该虚基类的某个构造函数。
1 SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other)
2 :Worker(wk), Waiter(wk, p), Singer(wk, v) {} //显式调用基类构造函数
3、哪个方法?
如果派生类对象没有新的数据成员,对于基类中的某个方法没有重新定义,而是试图直接调用继承的某个方法,多重继承情况下这将导致二义性。对于单继承来说,如果没有重新定义函数,则将使用最近祖先中的定义。在多重继承下,每个直接祖先可能都有一个相同的函数,从而导致二义性。
解决方法一个是使用作用域解析运算符,指出要使用哪个版本的函数。
1 SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
2 newhire.Singer::show();
3 //或者在SingingWaiter中重新定义show()函数
4 void SingingWaiter::show() const {
5 Singer::show();
6 }
另一个是使用模块化的方式。采用这种方法对象仍可以使用show方法,Data()方法需要声明为保护方法,作为协助公有接口的辅助方法(继承层次结构中的类可以使用,其他地方无法使用)。如果将Data()声明为私有方法,则Worker的派生类Waiter类和Singer类无法访问基类Worker中的Data方法(保护访问类的优势在这里体现)。
1 //对于单继承,可以让派生类调用基类的方法。假设HeadWaiter类是从Waiter类派生而来的
2 void Worker::show() const {
3 cout << "Name: " << name << endl;
4 cout << "Employee ID: " << id << endl;
5 }
6
7 void Waiter::show() const {
8 Worker::show();
9 cout << "Panache Rating: " << panache << endl;
10 }
11
12 void HeadWaiter::show() const {
13 Waiter::show();
14 cout << "Presence Rating: " << presence << endl;
15 }
16
17 //但是上述这种递增方式对于SingingWaiter来说,会忽略Waiter组件
18 void SingingWaiter::show() const {
19 Singer::show(); //Waiter组件呢?
20 }
21 //可以同时调用Waiter版本的show()来补救,但此时会产生新的问题
22 void SingingWaiter::show() const {
23 Singer::show(); //新的问题产生——显示SingingWaiter类的姓名和id两次
24 Waiter::show(); //Singer::show()、Waiter::show()都会调用Worker::show();
25 }
26
27 //解决方法之一是使用模块化方式
28 void Worker::Data() const {
29 cout << "Name: " << name << endl;
30 cout << "Employee ID: " << id << endl;
31 }
32
33 void Waiter::Data() const {
34 cout << "Paneche Rating: " << paneche << endl;
35 }
36
37 void Singer::Data() const {
38 cout << "Vocla range: " << pv[voice] << endl;
39 }
40
41 void SingingWaiter::Data() const {
42 Singer::Data();
43 Waiter::Data();
44 }
45
46 void SingingWaiter::show() const {
47 cout << "Category: singing waiter" << endl;
48 Worker::Data();
49 Data();
50 }
在祖先相同且使用MI时,必须引入虚基类,修改构造函数初始化列表的规则。
4、其他有关MI的问题:
① 混合使用虚基类和非虚基类
如果基类是虚基类,派生类包含基类的一个子对象,如果基类不是虚基类,派生类包含多个子对象。通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。如下面代码所示,类M包含3个基类子对象。
1 class B {...};
2 class C : public virtual B {...};
3 class D : public virtual B {...};
4 class X : public B {...};
5 class Y : public B {...};
6 class M : public C,public D,public X, public Y {...};
② 虚基类和支配
使用虚基类会改变C++解析二义性的方式。也就是说在使用虚基类的情况下,没有用类名对方法进行限定也不一定会导致二义性——派生类中的名称优先于直接或间接祖先类中的相同名称。
类C中的q()定义优先于类B中的q()定义,因为类C是从类B派生而来,因此F中可以直接使用q()表示C::q()。但是在F中使用非限定的omg()将导致二义性,即使E中的omg方法是私有的。同样,即使C::q()是私有的,也优先于B::q()。这种情况下,可以在F中调用B::q(),但如果不限定q(),意味着要调用不可访问的C::q()。
1 class B {
2 public:
3 short q();
4 ...
5 };
6
7 class C : public virtual B {
8 public:
9 long q();
10 int omg();
11 ...
12 };
13
14 class D : public C {...};
15
16 class E : public virtual B {
17 private:
18 int omg();
19 ...
20 };
21
22 class F : public D, public E {...};
MI小结
不使用虚基类的MI不会引入新的规则,如果一个类从两个不同的类那里继承两个同名的成员,需要在派生类中使用类限定符区分,否则编译器将指出二义性。
如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。
如果使用虚基类的MI,从虚基类的一个或多个实例派生出来的类只继承一个基类对象。实现这种特性需要满足的要求: 1、有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数(使用虚基类时,构造函数的自动传递不起作用),这对间接非虚基类是非法的。 2、通过优先规则解决名称的二义性。
MI会增加编程的复杂度,这是主要由于派生类通过多条途径继承同一个基类引起的。在避免这种情况后,必要时对继承的名称进行限定。
类模板
继承和包含并不能满足代码重用的需要,正如Stack和Queue那样,可以根据传递的类型,定义用于存储不同类型的容器类。例如可以定义存储int、double或string对象的Queue类,除了保存的数据类型不同,其余的Queue类代码都是相同的。这其中用到了模板类技术。不需要编写多个不同的类声明,只需要编写一个能够接收泛型的模板类,编译器根据模板类和传递给模板类的具体类型建立类。同函数模板一样,类模板不能减少实际生成的代码量,只是提供一种生成通用类声明的便捷方式。
1、定义类模板
模板类以 template <class Type> 开头,template关键字告诉编译器要定义一个模板,尖括号的内容相当于函数的参数列表,关键字class(较新的C++实现使用typename关键字,两者相同)表明 Type 是一个接收泛型的类型参数。Type的值一般为具体的类型,起到占位符的作用。同样可以使用模板成员函数替换原有的类方法,函数头以相同的模板声明打头 template <class Type> 。类限定符也有点不同,需要从 className:: 改为 className<Type>:: 。当然,如果在模板类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。

1 // StackTp.h -- a stack template 2 #pragma once 3 4 //定义Stack类模板 5 template <typename T> 6 class Stack { 7 private: 8 enum {MAX = 10}; 9 T items[MAX]; 10 int top; 11 public: 12 Stack(); 13 bool isEmpty(); 14 bool isFull(); 15 bool push(const T& item); 16 bool pop(T& item); 17 }; 18 19 //定义模板成员函数 20 template <typename T> 21 Stack<T>::Stack() { 22 top = 0; 23 } 24 25 template <typename T> 26 bool Stack<T>::isEmpty() { 27 return top == 0; 28 } 29 30 template <typename T> 31 bool Stack<T>::isFull() { 32 return top == MAX; 33 } 34 35 template <typename T> 36 bool Stack<T>::push(const T& item) { 37 if (top >= MAX) 38 return false; 39 items[top++] = item; 40 return true; 41 } 42 43 template <typename T> 44 bool Stack<T>::pop(T& item) { 45 if (top <= 0) 46 return false; 47 item = items[--top]; 48 return true; 49 }
模板不是类和成员函数定义,只是作为C++编译器指令说明如何生成类和成员函数定义。所以不能将模板成员函数放在独立的实现文件中,应当将所有模板信息放在一个头文件中。程序如果仅包含模板,编译器并不会生成模板类,只有在实例化或具体化后,编译器才会生成模板类。一般是声明一个类型为模板类的对象,并使用所需的具体类型替换泛型名。
注:必须显式地为类模板提供所需的类型,而函数模板不同,编译器可以根据函数的参数类型来确认要生成哪种函数。
1 Stack<int> st1; // create a stack of int
2 Stack<double> st2; //creat a stack of double
2、深入探讨模板类
可以将指针类型传递给Stack<T>模板吗?答案是可以,但是需要进行一些的处理。下面是直接使用之前定义的Stack<T>模板的结果(Stacktmp.h)。
1 Stack<char*> st; //create a stack of for pointers-to-char
2
3 char* po; //这旨在用char指针而不是string对象接收输入
4 cin >> po; //但是不可行,因为只创建指针,没有创建相应的内存空间
5 st.push(po);
6
7 char po[40]; //使用char数组,但与template<typename T> bool Stack<T>::pop(T& item)冲突
8 cin >> po; //pop中的参数item的值应为左值,而不是数组名,且也不能为数组名赋值
9 st.push(po);
10
11 char* po = new char[40]; //这为输入的字符串分配了空间且po为变量,与pop函数兼容
12 cin >> po; //但问题是po总指向相同的内存单元,每次加入到栈中的地址都相同
13 st.push(po);
使用指针栈时,应该在调用程序提供一个指针数组,让每个指针指向不同的字符串,这样将指针放入栈中是有意义的。下面重新定义Stack<Type>类,使得Stack<Type>类能够接收一个可选大小的参数,由于用到了动态数组,所以不能使用默认的析构函数、复制构造函数、赋值运算符,需要重新定义。

1 #pragma once 2 //Stcktp1.h -- modified Stack template 3 4 template <class T> 5 class Stack { 6 private: 7 enum {SIZE = 10}; 8 int stackSize; 9 T* items; 10 int top; 11 public: 12 explicit Stack(int ss = SIZE); 13 Stack(const Stack& st); 14 ~Stack() { delete[] items; } 15 bool isEmpty() { return top == 0; } 16 bool isFull() { return top == stackSize; } 17 bool push(const T& item); 18 bool pop(T& item); 19 Stack& operator=(const Stack& st); 20 }; 21 22 template<class T> 23 Stack<T>::Stack(int ss) : stackSize(ss), top(0) {} 24 25 template<class T> 26 Stack<T>::Stack(const Stack& st) { 27 stackSize = st.stackSize; 28 top = st.top; 29 items = new items[st.stackSize]; 30 for (int i = 0; i < top; ++i) { 31 items[i] = st.items[i]; 32 } 33 } 34 35 template<class T> 36 bool Stack<T>::push(const T& item) { 37 if (top >= stackSize) 38 return false; 39 items[top++] = item; 40 return true; 41 } 42 43 template<class T> 44 bool Stack<T>::pop(T& item) { 45 if (top <= 0) 46 return false; 47 item = items[--top]; 48 return true; 49 } 50 51 template<class T> 52 Stack<T>& Stack<T>::operator=(const Stack <T>& st) { //在类外面指定返回类型或使用作用域解析运算符时,必须使用完整的Stack<T> 53 if (this == &st) 54 return *this; 55 delete[] items; 56 top = st.top; 57 items = new T[stackSize]; 58 for (int i = 0; i < top; ++i) { 59 items[i] = st.items[i]; 60 } 61 return *this; 62 }
3、数组模板示例和非类型参数
对于一个允许指定数组大小的数组模板来说,实现的方法一个是在类中使用动态数组和构造函数参数来指定数组大小。另一种方法就是使用模板参数来提供常规的数组大小。表达式参数有一些限制,可以为整型、枚举、引用或指针,double m是不合法的,但是 double* rm 是可以的。模板代码不能改变参数的值,也不能使用参数的地址,且在实例化模板时,用作表达式的参数的值必须是常量表达式。
和第一种方法相比,构造函数方法是通过new和delete管理堆内存,而表达式参数方法使用的是自动变量维护的内存栈,执行速度更快(使用小型数组时)。表达式参数方法的缺点是,每种数组大小都会生成自己的模板,构造函数方法相对更通用。
1 #pragma once
2 //arraytp.h -- Array Template
3
4 #include<iostream>
5 #include<cstdlib>
6
7 template<class T, int n> //指出T为类型参数,n为int类型(非类型或表达式参数)
8 class ArrayTP {
9 private:
10 T ar[n];
11 public:
12 ArrayTP() {};
13 explicit ArrayTP(const T& v);
14 virtual T& operator[](int i);
15 virtual T operator[](int i) const;
16 };
17
18 template<class T, int n>
19 ArrayTP<T, n>::ArrayTP(const T& v) {
20 for (int i = 0; i < n; ++i) {
21 ar[i] = v;
22 }
23 }
24
25 template<class T, int n>
26 T& ArrayTP<T, n>::operator[](int i) {
27 if (i < 0 || i >= n) {
28 std::cerr << "Error in array limits: " << i << " is out of range\n";
29 std::exit(EXIT_FAILURE);
30 }
31 return ar[i];
32 }
33
34 template<class T, int n>
35 T ArrayTP<T, n>::operator[](int i) const {
36 if (i < 0 || i >= n) {
37 std::cerr << "Error in array limits: " << i << " is out of range\n";
38 std::exit(EXIT_FAILURE);
39 }
40 return ar[i];
41 }
4、模板多功能性
常规类的技术同样可以用于模板类。模板类可做基类、组件类,还可以作为其他模板的类型参数。除此之外,还可以递归使用模板
1 template<class T>
2 class Array {
3 private:
4 T entry;
5 ...
6 };
7
8 template <typename T>
9 class GrowArray : public Array<T> {...}; //模板类作为基类
10
11 template <typename T>
12 class Stack {
13 Array<T> ar; //模板类作为类组件
14 };
15
16 Array< Stack<int> > asi; //模板类作为类型参数
17
18 ArrayTP< ArrayTP<int, 5>, 10> twodee; //递归使用模板
模板还可以包含多个类型参数,如果希望类可以保存两种值,则可以创建并使用Pair模板。类模板的另一个新特性是可以为类型参数提供默认值 template <class T1, class T2 = int> class Topo {...}; ,但是不能为函数模板参数提供默认值。可以为非类型参数提供默认值。
1 template<class T1, class T2>
2 class Pair {
3 private:
4 T1 a;
5 T2 b;
6 public:
7 T1& first();
8 T2& second();
9 T1 first() const return { a; }
10 T2 second() const reutrn{ b; }
11
12 Pair(const T1& aval, const T2& bval) : a(aval),b(bval){}
13 Pair() {}
14
15 };
16
17 template<class T1, class T2>
18 T1& Pair<T1, T2>::first() {
19 return a;
20 }
21
22 template<class T1, class T2>
23 T2& Pair<T1, T2>::second() {
24 return b;
25 }
5、模板的具体化
与函数模板相似,类模板也有隐式实例化、显式实例化和显式具体化。
① 隐式实例化
即声明一个或多个对象,指出所需的类型,编译器根据通用模板提供的定义生成具体的类定义。
1 ArrayTP<int, 100> stuff; //隐式实例化
2
3 ArrayTP<double, 30>* pt; //不会生成类的实例化
4 pt = new ArrayTP<double, 30>; //编译器生成类的实例化并创建一个对象
② 显式实例化
使用关键字template并指出所需的类型来声明类,虽然没有创建或提及对象,但是编译器还是会生成相关的类声明。
1 template class ArrayTP<string, 100>; //生成ArrayTP<string, 100>类声明
③ 显式具体化
显式具体化为特定类型的定义,用于替换模板中的泛型。和函数模板的显式具体化一样,有时需要为特殊类型的实例化定义不同的模板,使其行为不同。当具体化模板和通用模板都与实例化请求匹配时,编译器优先使用具体化版本。具体化类模板定义的格式: template<> class ClassName<specialized-type-name> {...}; 。例如
1 template<> class SortedArray<const char*> {
2 ...
3 };
④ 部分具体化
部分具体化用来限制模板的通用性,可以给类型参数之一指定具体的类型。template后的<>声明的是没有被具体化的参数,如果指定所有的类型,则<>为空,此时导致显式具体化。有多个模板供选择时,编译器使用具体化程度最高的模板。
1 template <class T1, class T2> class Pair{...}; //常规类模板
2 template<class T1> class Pair<T1, int> {...}; //部分具体化,T1为类型参数
可以通过为指针提供特殊版本来部分具体化现有的模板
1 template<class T> class Feeb {...}; //#1
2 template<class T*> class Feeb {...}; //#2
3
4 Feeb<char> fb1; //使用#1版本
5 Feeb<char*> fb2; //使用#2版本
6、成员模板
模板可以作为结构、类或模板类的成员。blab()方法的U类型由该方法被调用时的参数值显式确定,T类型由对象的实例化类型确定。
1 template <typename T>
2 class beta {
3 private:
4 template<typename V>
5 class hold {
6 private:
7 V val;
8 public:
9 hold(V v = 0) : val(v) {}
10 void show() const { cout << val << endl; }
11 V value() const { return val; }
12 };
13
14 hold<T> q;
15 hold<int> n;
16 public:
17 beta(T t, int i) : q(t), n(i) {}
18 template<classs U>
19 U blab(U u, T t) { return (n.value() + q.value()) * u / t; }
20 void show() const { q.show(); n.show(); }
21 };

1 //在beta模板外定义的版本 2 template <typename T> 3 class beta { 4 private: 5 template<typename V> 6 class hold; 7 8 hold<T> q; 9 hold<int> n; 10 public: 11 beta(T t, int i) : q(t), n(i) {} 12 template<typename U> 13 U blab(U u, T t); 14 void show() const { q.show(); n.show(); } 15 }; 16 17 template<typename T> 18 template<typename V> 19 class beta<T>::hold { //指出hold是beta<T>类的模板成员 20 private: 21 V val; 22 public: 23 hold(V v = 0) : val(v) {} 24 void show() const { std::cout << val << std::endl; } 25 V value() const { return val; } 26 }; 27 28 template<typename T> 29 template<typename U> 30 U beta<T>::blab(U u, T t) { 31 return (n.value() + q.value()) * u / t; 32 }
7、将模板作为参数
模板可以包含类型参数和非类型参数,模板还可以包含本身就是模板的参数。
1 template <template <typename T> class Thing> //使用模板参数
2 class Crab {
3 ...
4 };
5
6 template <typename T>
7 class King {
8 ...
9 };
10
11 Crab<King> legs; //King为一个模板类
此外还可以使用模板参数和常规参数。
1 template<template<typename T> class Thing, typename U, typename V>
2 class Crab {
3 private:
4 Thing<U> s1;
5 Thing<V> s2;
6 ...
7 };
8、模板类和友元
模板的友元分为3类:非模板友元、约束模板友元、非约束模板友元。
① 非模板友元函数
在模板类中,将常规函数声明为友元。如果声明了 HasFriend<int> hf 则 report()将成为 HasFriend<int>类的友元函数,如果声明 HasFriend<double> ,则 report()函数为 HasFriend<double>的友元函数(重载版本)。
若HasFriend模板有一个静态成员 nums,则这个类的每一个特定的具体化都有自己的静态成员。
1 template<class T>
2 class HasFriend {
3 public:
4 friend void counts(); //成为模板所有实例化的友元
5 };
6
7 template <class T>
8 class HasFriend {
9 friend void report(HasFriend<T>&); //若要提供模板类参数,需指明具体化
10 };

1 #include<iostream> 2 3 using std::cout; 4 using std::endl; 5 6 template <typename T> 7 class HasFriend{ 8 private: 9 T item; 10 static int ct; 11 public: 12 HasFriend(const T& i): item(i) { ++ct; } 13 ~HasFriend() { ct--; } 14 friend void counts(); 15 friend void reports(HasFriend<T>&); 16 }; 17 18 template <typename T> 19 int HasFriend<T>::ct = 0; 20 21 22 void counts() { 23 cout << HasFriend<int>::ct << endl; 24 cout << HasFriend<double>::ct << endl; 25 } 26 27 void reports(HasFriend<int>& hf) { 28 cout << "HasFriend<int>: " << hf.item << endl; 29 } 30 31 void reports(HasFriend<double>& hf) { 32 cout << "HasFriend<double>" << hf.item << endl; 33 } 34 35 int main() { 36 counts(); 37 HasFriend<int> hf(10); 38 reports(hf); 39 }
② 约束模板友元函数
在类定义的前面声明每个模板函数,然后再将模板声明为友元。 声明中的<>指出这是模板具体化,对于report()来说,<>的内容可以为空。但是对于counts()不行,因为count()函数没有参数,编译器无法通过count()函数的参数推断出模板类型参数,必须使用模板参数语法(<TT>)以指明具体化。
1 //声明模板函数
2 template<typename T> void counts();
3 template<typename T> void report(T&);
4
5 //将模板函数声明为友元
6 template<typename TT>
7 class HasFriendT {
8 ...
9 friend void counts<TT>();
10 friend void report<>(HasFriend<TT>&);
11 };
③ 非约束模板函数
在类内部声明模板,创建非约束友元函数,每个函数具体化都是每个类具体化的友元。
1 template <typename T>
2 class ManyFriend {
3 ...
4 template <typename C, typename D> friend void show2(C&, D&);
5 };
9、模板别名(C++11)
可以使用 typedef为模板具体化指定别名:
1 typedef std::array<double, 12> arrd;
2 typedef std::array<int, 12> arri;
3 typedef std::array<std::string, 12> arrt;
C++11新增一项功能,可以使用模板提供一系列别名:
1 template<class T>
2 using arrtype = std::array<T, 12>;
3
4 arrtype<int> arr1;
5 arrtype<double> arr2;
6 arrtype<std::string> arr3;
C++11允许将using=语法用于非模板:
1 typedef const char* pc1; //typedef语法
2 using pc2 = const char*; //using=语法
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了