0%

1
pear config-set http_proxy ""

C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。

例如有下面的函数模板,它用来获取两个变量中较大的一个:

1
2
3
template<class T> const T& Max(cosnt T& a, const T& b) {
return a > b ? a : b;
}

请读者注意 a > b 这条语句,> 能够用来比较 int、float、char 等基本类型数据的大小,但是却不能用来比较结构体变量、对象以及数组的大小,因为我们并没有针对结构体、类和数组重载 >

另外,该函数模板虽然可以用于指针,但比较的是地址大小,而不是指针指向的数据,所以也没有现实的意义。

除了 >+ - * / 等运算符也只能用于基本类型,不能用于结构体、类、数组等复杂类型。总之,编写的函数模板很可能无法处理某些类型,我们必须对这些类型进行单独处理。

模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种 "游戏规则",让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显式具体化(Explicit Specialization)。

函数模板和类模板都可以显式具体化,下面我们线讲解函数模板的显式具体化,再讲解类模板的显式具体化。

函数模板的显式具体化

在讲解函数模板的显式具体化语法之前,我们先来看一个显式具体化的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
#include<string>
using namespace std;

typedef struct {
string name;
int age;
float score;
} STU;

// 函数模板
template<class T> const T& Max(const T& a, const T& b);
// 函数模板的显式具体化(针对 STU 类型的显式具体化)
template<> const STU& Max<STU>(const STU& a, const STU& b);
// 重载 <<
ostream & operator<<(ostream &out, const STU &stu);

int main()
{
int a = 10;
int b = 20;
cout << Max(a, b) << endl;

STU stu1 = {"张三", 16, 95.5};
STU stu2 = {"李四", 18, 90.0};
cout << Max(stu1, stu2) << endl;

return 0;
}

template<class T> const T& Max(const T& a, const T& b) {
return a > b ? a : b;
}

template<> const STU& Max<STU>(const STU &a, const STU& b) {
return a.score > b.score ? a : b;
}

ostream & operator<<(ostream &out, const STU &stu) {
out << stu.name << " , " << stu.age << " , " << stu.score;
return out;
}

运行结果:

1
2
20
张三 , 16 , 95.5

本例中,STU 结构体用来表示一名学生(Student),它有三个成员,分别是姓名(name)、年龄(age)、成绩(score);Max() 函数用来获取两份数据中较大的一份。

要想获取两份数据中较大的一份,必然会涉及到对两份数据的比较。对于 int、float、char 等基本类型的数据,直接比较它们本身的值即可,而对于 STU 类型的数据,直接比较它们本身的值不但有语法错误,而且毫无意义,这就要求我们设计一套不同的比较方案,从语法和逻辑上都能行得通,所以本例中我们比较的是两名学生的成绩(score)。

不同的比较方案最终导致了算法(函数体)的不同,我们不得不借助模板的显式具体化技术对 STU 类型进行单独处理。第 14 行代码就是显式具体化的声明,第 34 行代码进行了定义。

请读者注意第 34 行代码, Max<STU> 中的 STU 表明了要将类型参数 T 具体化为 STU 类型,原来使用 T 的位置都应该使用 STU 替换,包括返回值类型、形参类型、局部变量的类型。

Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写做 template<>

另外,Max<STU> 中的 STU 是可选的,因为函数的形参已经表明,这是 STU 类型的一个具体化,编译器能够逆推出 T 的具体类型。简化后的函数声明为:

1
template<> const STU& Max(const STU& a, const STU& b);

函数的调用规则:

对于给定的函数名,可以有非模板函数、模板函数、显式具体化模板函数以及它们的重载版本。在调用函数时,显式具体化优先于常规模板,而非模板函数优先于显式具体化和常规模板。

类模板的显式具体化

除了函数模板,类模板也可以显式具体化,并且它们的语法是类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
using namespace std;

// 类模板
template<class T1, class T2> class Point {
public:
Point(T1 x, T2 y): m_x(x), m_y(y) {}
void display() const;
private:
T1 m_x;
T2 m_y;
};

template<class T1, class T2> // 这里要带上模板头
void Point<T1, T2>::display() const {
cout << "x=" << m_x << ", y=" << m_y << endl;
}

// 类模板的显式具体化
template<> class Point<char*, char*> {
public:
Point(char *x, char *y): m_x(x), m_y(y) {}
void display() const;
private:
char *m_x; // x 坐标
char *m_y; // y 坐标
};

// 这里不能带模板头 template<>
void Point<char*, char*>::display() const {
cout << "x=" << m_x << " | y=" << m_y << endl;
}

int main()
{
(new Point<int, int>(10, 20))->display();
(new Point<int, char*>(10, "东经180度"))->display();
(new Point<char*, char*>("东经180度", "北纬210度"))->display();
return 0;
}

运行结果:

1
2
3
x=10, y=20
x=10, y=东经180度
x=东经180度 | y=北纬210度

Point<char*, char*> 表明了要将类型参数 T1、T2 都具体化为 char* 类型,原来使用 T1、T2 的位置都应该使用 char* 替换。Point 类有两个类型参数 T1、T2,并且都已经被具体化了,所以整个类模板就不再有类型参数了,模板头应该写作 template<>

可以发现,当在类的外部定义成员函数时,普通类模板的成员函数前面要带上模板头,而具体化的类模板的成员函数不能带模板头。

部分显式具体化

在上面的显式具体化例子中,我们为所有的类型参数提供了实参,所以最后的模板头为空,也即 template<>。另外 C++ 还允许只为一部分类型参数提供实参,这称为部分显式具体化。

部分显式具体化只能用于类模板,不能由于函数模板。

仍然以 Point 为例,假设我们现在希望 "只要横坐标 x 是字符串类型" 就以 | 来分隔输出结果,而不管纵坐标 y 是什么类型,这种要求就可以使用部分显式具体化技术来满足。请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>

using namespace std;

// 类模板
template<class T1, class T2>
class Point {
public:
Point(T1 x, T2 y) : m_x(x), m_y(y) {}
void display() const;

private:
T1 m_x;
T2 m_y;
};

template<class T1, class T2>
// 这里需要带上模板头
void Point<T1, T2>::display() const {
cout << "x=" << m_x << ", y=" << m_y << endl;
}

// 类模板部分显式具体化
template<typename T2>
class Point<char *, T2> {
public:
Point(char *x, T2 y) : m_x(x), m_y(y) {}
void display() const;

private:
char *m_x; // x 坐标
T2 m_y; // y 坐标
};

template<typename T2>
// 这里需要带上模板头
void Point<char *, T2>::display() const {
cout << "x=" << m_x << " | y=" << m_y << endl;
}

int main() {
(new Point<int, int>(10, 20))->display();
(new Point<char *, int>("东经180度", 10))->display();
(new Point<char *, char *>("东经180度", "北纬210度"))->display();
return 0;
}

输出:

1
2
3
x=10, y=20
x=东经180度 | y=10
x=东经180度 | y=北纬210度

本例中,T1 对应横坐标 x 的类型,我们将 T1 具体化为 char*

模板头 template<typename T2> 中声明的是没有被具体化的类型参数;类名 Point<char*, T2> 列出了所有类型参数,包括未被具体化和已经被具体化的。

类名后面之所以要列出所有的类型参数,是为了让编译器确认 "到底是第几个类型参数被具体化了",如果写作 template<typename T2> class Point<char*>,编译器就不知道 char* 代表的是第一个类型参数,还是第二个类型参数。

在使用类模板创建对象时,程序员需要显式的指明实参(也就是具体的类型)。例如对于下面的 Point 类:

1
template<typename T1, typename T2> class Point;

我们可以在栈上创建对象,也可以在堆上创建对象。

1
2
Point<int, int> p1(10, 20); // 在栈上创建对象
Point<char*, char*> *p = new Point<char*, char*>("东经180度", "北纬210度"); // 在堆上创建对象

因为已经显式地指明了 T1、T2 的具体类型,所以编译器就不用再自己推断了,直接拿来使用即可。

而对于函数模板,调用函数时可以不显式地指明实参(也就是具体的类型)。请看下面的例子:

1
2
3
4
5
6
7
8
// 函数声明
template<typename T> void Swap(T &a, T &b);

// 函数调用
int n1 = 100, n2 = 200;
Swap(n1, n2);
float f1 = 12.5, f2 = 56.93;
Swap(f1, f2);

虽然没有显式地指明 T 的具体类型,但是编译器会根据 n1 和 n2、f1 和 f2 的类型自动推断出 T 的类型。

这种通过函数实参来确定模板实参(也就是类型参数的具体类型)的过程称为模板实参推断。

在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找类型参数的具体类型。

模板实参推断过程中的类型转换

对于普通函数(非模板函数),发生函数调用时会对实参的类型进行适当的转换,以适应形参的类型。这些转换包括:

  • 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。

  • 派生类向基类的转换:也就是向上转型。

  • const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *

  • 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针。

  • 用户自定的类型转换。

例如有下面两个函数原型:

1
2
void func1(int n, float f);
void func2(int *arr, const char *str);

它们具体的调用形式为:

1
2
3
4
5
6
int nums[5];
char *url = "http://baidu.com";
// 12.5 会从 double 转换为 int,45 会从 int 转换为 float
func1(12.5, 45);
// nums 会从 int[5] 转换为 int*,url 会从 char* 转换为 const char*
func2(nums, url);

而对于函数模板,类型转换则受到了更多的限制,仅能进行 const 转换和 数组或函数指针转换,其他的都不能应用于函数模板。

例如有下面几个函数模板:

1
2
3
4
5
template<typename T> void func1(T a, T b);
template<typename T> void func2(T *buffer);
template<typename T> void func3(const T &stu);
template<typename T> void func4(T a);
template<typename T> void func5(T &a);

它们具体的调用形式为:

1
2
3
4
5
6
7
8
int name[20];
Student stu1("CPP", 20, 96.5); //创建一个 Student 类型的对象

func1(12.5, 30); // Error
func2(name); // name 的类型从 int[20] 转换为 int*,所以 T 的真实类型为 int
func3(stu1); // 非 const 转换为 const,T 的真实类型为 Student
func4(name); // name 的类型从 int[20] 转换为 int*,所以 T 的真实类型为 int*
func5(name); // name 的类型依然为 int[20],不会转换为 int*,所以 T 的真实类型为 int[20]

对于 func1(12.5, 30),12.5 的类型为 double,30 的类型为 int,编译器不知道该将 T 实例化为 double 还是 int,也不会尝试将 int 转换为 double,或者将 double 转换为 int,所以调用失败。

请读者注意 name,它本来的类型是 int[20]

  • 对于 func2(name)func4(name),name 的类型会从 int[20] 转换为 int,也即将数组转换为指针,所以 T 的类型分别为 int 和 int

  • 对于 func5(name),name 的类型依然为 int[20],不会转换为 int*,所以 T 的类型为 int[20]。

可以发现,当函数形参是引用类型时,数组不会转换为指针。这个时候读者要注意下面这样的函数模板:

1
template<typename T> void func(T &a, T &b);

如果它的具体调用形式为:

1
2
3
int str1[20];
int str2[10];
func(str1, str2);

由于 str1、str2 的类型分别为 int[20] 和 int[10],在函数调用过程中又不会转换为指针,所以编译器不知道应该将 T 实例化为 int[20] 还是 int[10],导致调用失败。

为函数模板显式地指明实参类型

函数模板的实参推断是指 "在函数调用过程中根据实参的类型来寻找类型参数的具体类型" 的过程,这在大部分情况下是奏效的,但是当类型参数的个数较多时,就会有个别的类型无法推断出来,这个时候就必须显式地指明实参。

下面是一个实参推断失败的例子:

1
2
3
4
5
template<typename T1, typename T2> void func(T1 a) {
T2 b;
}

func(10); // 函数调用

func() 有两个类型参数,分别是 T1 和 T2,但是编译器只能从函数调用中推断出 T1 的类型来,不能推断出 T2 的类型来,所以这种调用是失败的,这个时候就必须显式地指明 T1、T2 的具体类型。

"为函数模板显式地指明实参" 和 "为类模板显式地指明实参" 的形式是类似的,就是在函数名后面添加尖括号 <>,里面包含具体的类型。例如对于上面的 func(),我们这样来指明实参:

1
func<int, int>(10);

显式指明的模板实参会按照从左到右的顺序与对应的模板参数匹配:第一个实参与第一个模板参数匹配,第二个实参与第二个模板参数匹配,以此类推。只有尾部(最右)的类型参数的实参可以省略,而且前提是它们可以从传递给函数的实参中推断出来。

对于上面的 func(),虽然只有 T2 的类型不能自动推断出来,但是由于它位于类型参数列表的尾部,所以必须同时指明 T1 和 T2 的类型。对代码稍微作出修改:

1
2
3
4
5
6
7
template<typename T1, typename T2> void func(T2 a) {
T1 b;
}

// 函数调用
func<int>(10); // 省略 T2 的类型
func<int, int>(20); // 指明 T1、T2 的类型

由于 T2 的类型能够自动推断出来,并且它位于参数列表的尾部,所以可以省略。

显式地指明实参时可以应用正常的类型转换

上面我们提到,函数模板仅能进行 const 转换和 数组或函数指针转换 两种形式的类型转换,但是当我们显式地指明类型参数的实参时,就可以使用正常的类型转换(非模板函数可以使用的类型转换)了。

例如对于下面的函数模板:

1
template<typename T> void func(T a, T b);

它的具体调用形式如下:

1
2
func(10, 23.5); //Error
func<float>(20, 93.7); // Correct

在第二种调用形式中,我们已经显式地指明了 T 的类型为 float,编译器不会再为 T 的类型到底是 int 还是 double 而纠结了,所以可以从容地使用正常的类型转换了。

当需要对不同的类型使用同一种算法(同一个函数体)时,为了避免定义多个功能重复的函数,可以使用模板。然而,并非所有的类型都使用同一种算法,有些特定的类型需要单独处理,为了满足这种需求,C++ 允许对函数模板进行重载,程序员可以像重载常规函数那样重载模板定义。

比如,交换两个变量的值,有两个方法,一个是使用指针,另一个是使用引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 方案1:使用指针
template<typename T> void Swap(T *a, T *b) {
T temp = *a;
*a = *b;
*b = temp;
}

// 方案2:使用引用
template<class T> void Swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}

这两种方案都可以交换 int、float、char、bool 等基本类型变量的值,但是却不能交换两个数组。

对于方案1,调用阿函数时传入的是数组指针,或者说是指向第 0 个元素的指针,这样交换的仅仅是数组的第 0 个元素,而不是整个数组。数组和指针本来是不等价的,只是当函数的形参为指针时,传递的数组也会隐式地转换为指针。

对于方案2,假设传入的是一个长度为 3 的 int 类型数组(该数组的类型是 int[3]),那么 T 的真实类型为 int[3]T temp 会被替换为 int [3]temp,这显然是错误的。另外一方面,语句 a=b; 尝试对数组 a 赋值,而数组名是常量,它的值不允许被修改,所以也会产生错误。

交换两个数组唯一的办法就是逐个交换所有的数组元素,请看下面的代码:

1
2
3
4
5
6
7
8
template<typename T> void Swap(T a[], T b[], int len) {
T temp;
for (int i = 0; i < len; i++) {
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}

在该函数模板中,最后一个参数的类型为具体类型(int),而不是泛型。并不是所有的模板参数都必须被泛型化。

下面是一个重载函数模板的完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<iostream>
using namespace std;

template<class T> void Swap(T &a, T &b); // 模板1:交换基本类型的值
template<typename T> void Swap(T a[], T b[], int len); // 模板2:交换两个数组

template<typename T> void printArray(T arr[], int len); // 打印数组元素

int main() {
// 交换基本类型的值
int m = 10, n = 99;
Swap(m, n); // 匹配模板1
cout << m << ", " << n << endl;

// 交换两个数组
int a[3] = {1, 2, 3};
int b[3] = {4, 5, 6};
int len = sizeof(a) / sizeof(int); // 数组长度
Swap(a, b, len); // 匹配模板2
printArray(a, len);
printArray(b, len);

return 0;
}

template<class T> void Swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}

template<typename T> void Swap(T a[], T b[], int len) {
T temp;
for (int i = 0; i < len; i++) {
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}

template<typename T> void printArray(T arr[], int len) {
for (int i = 0; i < len; i++) {
cout << arr[i] << ", ";
}
cout << endl;
}

输出:

1
2
3
99, 10
4, 5, 6,
1, 2, 3,

C++ 除了支持函数模板,还支持类模板(Class Template)。函数模板中定义的类型参数可以用在函数声明和定义中,类模板中定义的类型参数可以用在类声明和实现中。类模板的目的同样是将数据的类型参数化。

声明类模板的语法为:

1
2
3
template<typename 类型参数1, typename 类型参数2, ...> class 类名 {
//
}

类模板和函数模板都是以 template 开头(当然也可以使用 class,目前来讲它们没有任何区别),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。

一旦声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。

假如我们现在要定义一个类来表示坐标,要求坐标的数据类型可以是整数、小数和字符串,例如:

  • x = 10、y = 10

  • x = 12.88、y = 129.65

  • x = "东经180度"、y = "北纬210度"

这个时候就可以使用类模板,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T1, typename T2> // 这里不能有分号
class Point {
public:
Point(T1 x, T2 y): m_x(x), m_y(y) {}
public:
T1 getX() const; // 获取 x 坐标
void setX(T1 x); // 设置 x 坐标
T2 getY() const; // 获取 y 坐标
void setY(T2 y); // 设置 y 坐标
private:
T1 m_x; // x 坐标
T2 m_y; // y 坐标
}

x 坐标和 y 坐标的数据类型不确定,借助类模板可以将数据类型参数化,这样就不必定义多个类了。

注意:模板头和类头是一个整体,可以换行,但是中间不能有分号。

上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:

1
2
3
4
template<typename 类型参数1, typename 类型参数2, ...>
返回值类型 类名<类型参数1, 类型参数2, ...>::函数名(形参列表) {
//
}

下面就对 Point 类的成员函数进行定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T1, typename T2> // 模板头
T1 Point<T1, T2>::getX() const /* 函数头 */ {
return m_x;
}

template<typename T1, typename T2>
void Point<T1, T2>::setX(T1 x) {
m_x = x;
}

template<typename T1, typename T2>
T2 Point<T1, T2>::getY() const {
return m_y;
}

template<typename T1, typename T2>
void Point<T1, T2>::setY(T2 y) {
m_y = y;
}

请读者仔细观察代码,除了 template 关键字后面要指明类型参数,类名 Point 后面也要带上类型参数,只是不加 typename 关键字了。另外需要注意的是,在类外定义成员函数时,template 后面的类型参数要和类声明时一致。

使用类模板创建对象

上面的两段代码完成了类的定义,接下来就可以使用该类创建对象了。使用类模板创建对象时,需要指明具体的数据类型。请看下面的代码:

1
2
3
Point<int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, char*> p3(12.4, "东经180度");

与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。

除了对象变量,我们也可以使用对象指针的方式来实例化:

1
2
Point<float, float> *p1 = new Point<float, float>(10.6, 109.3);
Point<char*, char*> *p = new Point<char*, char*>("东经180度", "北纬210度");

需要注意的是,赋值号两边都要指明具体的数据类型,且要保持一致。下面的写法是错误的:

1
2
3
4
// 赋值号两边的数据类型不一致
Point<float, float> *p = new Point<float, int>(10.6, 109);
// 赋值号右边没有指明数据类型
Point<float, float> *p = new Point(10.6, 109);

综合示例

将上面的类定义和类实例化的代码整合起来,构成一个完整的示例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<iostream>
using namespace std;

template<class T1, class T2> // 这里不能有分号
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y) {}
public:
T1 getX() const; // 获取 x 坐标
void setX(T1 x); // 设置 x 坐标
T2 getY() cosnt; // 获取 y 坐标
void setY(T2 y); // 设置 y 坐标
private:
T1 m_x; // x 坐标
T2 m_y; // y 坐标
};

template<class T1, class T2> // 模板头
T1 Point<T1, T2>::getX() const /* 函数头 */ {
retrun m_x;
}

template<class T1, class T2>
void Point<T1, T2>::setX(T1 x) {
m_x = x;
}

template<class T1, class T2>
T2 Point<T1, T2>::getY() const {
return m_y;
}

template<class T1, class T2>
void Point<T1, T2>::setY(T2 y) {
m_y = y;
}

int main() {
Point<int, int> p1(10, 20);
cout << "x=" << p1.getX() << ", y=" << p1.getY() << endl;

Point<int, char*> p2(10, "东经180度");
cout << "x=" << p2.getX() << ", y=" << p2.getY() << endl;

Point<char*, char*> *p3 = new Point<char*, char*>("东经180度", "北纬210度");
cout << "x=" << p3->getX() << ", y=" << p3->getY() << endl;

return 0;
}

运行结果:

1
2
3
x=10, y=20
x=10, y=东经180度
x=东经180度, y=北纬210度

在定义类型参数时,我们使用了 class,而不是 typename,这样做的目的是让读者对两种写法都熟悉。

用类模板实现可变长数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include<iostream>
#include<cstring>
using namespace std;

template<class T>
class CArray
{
int size; // 数组元素的个数
T *ptr; // 指向动态分配的数组

public:
CArray(int s = 0); // s 代表数组元素的个数
CArray(CArray &a);
~CArray();
void push_back(const T &v); // 用于在数组尾部添加一个元素 v
CArray & operator=(const CArray &a); // 用于数组对象间的赋值
int length() { return size; }
T & operator[](int i )
{
// 用以支持根据下标访问数组元素,如 a[i] = 4; 和 n = a[i] 这样的语句
return ptr[i];
}
};

template<class T>
CArray<T>::CArray(int s): size(s)
{
if (s == 0)
ptr = NULL;
else
ptr = new T[s];
}

template<class T>
CArray<T>::CArray(CArray &a)
{
if (!a.ptr) {
ptr = NULL;
size = 0;
return;
}
ptr = new T[a.size];
memcpy(ptr, a.ptr, sizeof(T) * a.size);
size = a.size;
}

template<class T>
CArray<T>::~CArray()
{
if(ptr) delete []ptr;
}

template<class T>
CArray<T> & CArray<T>::operator=(const CArray &a)
{
// 重载运算符 =,使 = 坐边对象里存放的数组,大小和内容都和右边的对象一样
if (this == &a) // 防止 a=a 这样的赋值导致出错
return *this;
if (a.ptr == NULL) {
// 如果 a 里面的数组是空的
if (ptr)
delete []ptr;
ptr = NULL;
size = 0;
return *this;
}
if (size < a.size) {
// 空间不够大,需要分配新的空间
if (ptr)
delete []ptr;
ptr = new T[a.size];
}

memcpy(ptr, a.ptr, sizeof(T) * a.size);
size = a.size;
return *this;
}

template<class T>
void CArray<T>::push_back(const T &v)
{
// 在数组尾部添加一个元素
if (ptr) {
T *tmpPtr = new T[size + 1]; // 重新分配空间
memcpy(tmpPtr, ptr, sizeof(T) * size);
delete []ptr;
ptr = tmpPtr;
} else
ptr = new T[1];
ptr[size++] = v; // 加入新的数组元素
}

int main()
{
CArray<int> a;
for (int i = 0; i < 5; i++) {
a.push_back(i);
}
for (int i = 0; i < a.length(); ++i)
cout << a[i] << " ";
return 0;
}