多态与模板
抽象类与纯虚函数
定义
虚函数还可以进一步声明为纯虚函数,只要包含有一个纯虚函数的类,即为“抽象类”。语法为函数最后加上 = 0
。
需要注意,抽象类不允许定义对象。
定义基类为抽象类,主要用途是为派生类规定共性“接口”。此外,抽象类的定义能避免赋值型对象切片,保证只有指针和引用能被向上类型转换(因为抽象类无法定义对象,但是可以有指针和引用指向它)。
例如:
| class A {
public:
virtual void f() = 0; // 可在类外定义函数体提供默认实现。派生类通过 A::f() 调用
};
A obj; // 不准抽象类定义对象!编译不通过!
|
以及更为具体的例子:
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 | #include <iostream>
using namespace std;
class Pet {
public:
virtual void motion() = 0;
};
void Pet::motion() { cout << "Pet motion: " << endl; }
class Dog: public Pet {
public:
void motion() override {
Pet::motion();
cout << "dog run" << endl;
}
}; // 重写覆盖后,也可以通过如 d1.Base::foo() 来调用基类函数
class Bird: public Pet {
public:
void motion() override {
Pet::motion();
cout << "bird fly" << endl;
}
};
int main() {
Pet* p = new Dog; // 向上类型转换
p->motion();
p = new Bird; // 向上类型转换
p->motion();
// p = new Pet; // 不允许定义抽象类对象
return 0;
}
/*
output:
Pet motion:
dog run
Pet motion:
bird fly
*/
|
注意到先前有提到过,重写覆盖 override
与重写隐藏 redefining
相似,也可以通过如 d1.Base::foo()
来调用基类函数。此例中虽然发生了重写覆盖,但是通过直接调用 Pet::motion()
来调用了基类函数。
抽象类继承
基类纯虚函数被派生类重写覆盖之前仍是纯虚函数。因此当继承一个抽象类时,除纯虚析构函数外(随后解释),必须实现所有纯虚函数,否则继承出的类也是抽象类。
纯虚析构函数
析构函数也可以是纯虚函数,纯虚析构函数仍然需要函数体。
目的:使基类成为抽象类,不能创建基类的对象。如果有其他函数是纯虚函数,则析构函数无论是否为纯虚的,基类均为抽象类。
实际上编程的时候不会写纯虚析构函数,只需要写虚析构函数就可以了。
纯虚析构函数和其他纯虚函数的区别
一般的纯虚函数被派生类重写覆盖之前仍是纯虚函数。如果派生类不覆盖纯虚函数,那么派生类也是抽象类。
但对于对于纯虚析构函数而言,即便派生类中不显式实现析构函数,编译器也会自动合成默认析构函数,完成重写覆盖,使得派生类不是抽象类。故而,即使派生类不显式覆盖纯虚析构函数,只要派生类完全覆盖了其他纯虚函数,该派生类就不是抽象类,可以定义派生类对象。
例子
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 | #include <iostream>
using namespace std;
class Base {
public:
virtual void func() = 0;
};
class Derive1: public Base { }; // Derive1 仍为抽象类
class Derive2: public Base {
public:
void func() {
cout << "Derive2::func" << endl;
}
};
int main() {
// Derive1 d1; // 编译错误,Derive1 仍为抽象类
Derive2 d2;
d2.func();
return 0;
}
/*
output:
Derive2::func
*/
|
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 | #include <iostream>
using namespace std;
class Base{
public:
virtual ~Base() = 0;
};
Base::~Base() { cout << "Base destroyed" << endl; }
class Derive1: public Base { };
class Derive2: public Base {
public:
virtual ~Derive2() { cout << "Derive2 destroyed" << endl; }
};
int main() {
Base* p1 = new Derive1;
Base* p2 = new Derive2;
delete p1;
cout << "------" << endl;
delete p2;
return 0;
}
/*
output:
Base destroyed
------
Derive2 destroyed
Base destroyed
*/
|
一些选项的解释:
A. 晚捆绑依赖于虚函数表与虚函数,只对类中虚函数起作用,并且只对基类指针和引用起作用。
D. 一个纯虚函数就会使得一个类成为纯虚函数。
向下类型转换
定义
指向派生类对象的基类指针、引用转换成派生类指针、引用,则称为向下类型转换(类层次中向下移动)。
当我们用基类指针表示各种派生类时(向上类型转换),保留了他们的共性,但是丢失了他们的特性。如果此时要表现特性,则可以使用向下类型转换。
比如我们可以使用基类指针数组对各种派生类对象进行管理,当具体处理时我们可以将基类指针转换为实际的派生类指针,进而调用派生类专有的接口。
但是需要注意到,这里是说用基类指针可以管理派生类,故而可以把这个指针向下转换,生成实际使用的派生类指针。但是,如果是基类指针管理基类对象,也就是基类指针本来就指向基类对象时,基类对象本身就没有派生类多出的数据和服务,还要把这个基类指针向下转换为派生类指针,要么是错的,要么是危险的。
转换方式
安全向下类型转换(基类向派生类的转换)
C++ 提供了一个特殊的显式类型转换,称为 dynamic_cast
,是一种安全的向下类型转换。使用 dynamic_cast
的对象必须有虚函数,因为它使用了存储在虚函数表中的信息判断实际的类型。
所谓对象必须有虚函数,实际上是指有继承关系时,基类和派生类都有虚函数。基类有虚函数,是多态的,那么派生类当然也有虚函数。重写覆盖是用虚函数覆盖虚函数,仍然可以用 Base::
的方式调用基类虚函数,不破坏多态性。而重写隐藏更不会破坏其多态性。
语法:T2
是 T1
的派生类,obj_p
、obj_r
分别是 T1
类型的指针和引用,二者都指向一个派生类 T2
的对象。
| T2* pObj = dynamic_cast<T2*>(obj_p); // 失败时返回 nullptr
T2& refObj = dynamic_cast<T2&>(obj_r); // 失败时异常
|
快速向下类型转换
如果我们知道目标的操作是安全的,可以使用 static_cast
来加快速度。static_cast
在编译时静态浏览类层次,只检查继承关系。没有继承关系的类之间,必须具有转换途径才能进行转换(要么自定义,要么是语言语法支持),否则不过编译。因为快速,static_cast
运行时无法确认是否正确转换。
语法:T2
是 T1
的派生类,obj_p
、obj_r
分别是 T1
类型的指针和引用,二者都指向一个派生类 T2
的对象。
| T2* pObj = static_cast<T2*>(obj_p); // 转换为 T2 指针
T2& refObj = static_cast<T2&>(obj_r); // 转换为 T2 引用
|
不安全:如果基类指针本来就指向基类对象,向下类型转换就是不合法的。此时不保证指向目标是 T2
对象,可能导致非法内存访问。
例子
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 | #include <iostream>
using namespace std;
class B {
public:
virtual void f() { }
};
class D: public B {
public:
int i {2018};
};
int main() {
D d;
B b;
// D d1 = static_cast<D>(b); // 未定义类型转换方式,注意这个直接是对象的转换而不是指针的转换
// D d2 = dynamic_cast<D>(b); // 这是不合法的,dynamic_cast 只允许指针和引用转换
D* pd1 = static_cast<D*>(&b); // 有继承关系,允许转换,让 D 类型指针 pd1 指向了 B 类型的对象 b
if (pd1 != nullptr) {
cout << "static_cast, B*(B) --> D*: OK" << endl;
cout << "D::i=" << pd1->i <<endl;
} // 但是不安全,pd1 访问 D 中成员 i 时,可能是非法访问
D* pd2 = dynamic_cast<D*>(&b);
if (pd2 == nullptr) // 不允许不安全的转换
cout << "dynamic_cast, B*(B) --> D*: FAILED" << endl;
B* pb = &d;
D* pd3 = static_cast<D*>(pb);
if (pd3 != nullptr) {
cout << "static_cast, B*(D) --> D*: OK" << endl;
cout << "D::i=" << pd3->i <<endl;
}
D* pd4 = dynamic_cast<D*>(pb);
if (pd4 != nullptr) { // 转换正确
cout << "dynamic_cast, B*(D) --> D*: OK" << endl;
cout << "D::i=" << pd4->i <<endl;
}
return 0;
}
/*
output:
static_cast, B*(B) --> D*: OK
D::i=4817112
dynamic_cast, B*(B) --> D*: FAILED
static_cast, B*(D) --> D*: OK
D::i=2018
dynamic_cast, B*(D) --> D*: OK
D::i=2018
*/
|
注意到前文提过危险转换的情况,如果基类指针本来就指向基类对象,向下类型转换就是不合法的。从而我们观察到,使用 dynamic_cast
将 &b
转为 D
的指针是错误的,返回了空指针。而 static_cast
虽然实现了转换,但是输出的完全是非法内存。且在大多数编译器上,static_cast
这样操作会 RE。
另一方面,我们现在讨论的的都是指针转换,而对象的直接转换是类型转换,故而不加其他定义时用 static_cast
和 dynamic_cast
都是不安全的。
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 | #pragma once
#include <string>
#include <iostream>
class Animal {
private:
std::string name, type;
Animal(const std::string &_name, const std::string &_type):
name(_name), type(_type) { }
};
class Dog: public Animal {
public:
Dog(const std::string &_name): Animal(_name, "dog") { };
};
class Bird: public Animal {
public:
Bird(const std::string &_name): Animal(_name, "bird") { };
};
void action(Animal* animal, std::vector<Dog>& dogzone, std::vector<Bird>& birdzone) {
Dog* point1 = dynamic_cast<Dog*>(animal);
Bird* point2 = dynamic_cast<Bird*>(animal);
if (point1 != nullptr) {
dogzone.push_back(std::move(*point1));
} else {
birdzone.push_back(std::move(*point2));
}
}
|
这个题放上了伪代码,给出了第二类非法转换的例子。一个基类派 D
生出了多个派生类 A
、B
、C
。比如基类指针 d
指向了派生类 A
对象 a
,但是如果我们把 d
向下类型转换为 B
类指针,也是非法转换。从这个角度可以看出,在上例中,Animal
类的 type
是 private
,没有接口是没法访问的。我们本来需要根据 type
来决定向下转换的类型,但是访问不了。于是我们直接向下类型转换,直接通过是否转为 nullptr
来检测是否转换正确。
总结
dynamic_cast
与 static_cast
相比:
-
相同点:都可完成向下类型转换
-
不同点:
static_cast
在编译时静态执行向下类型转换
dynamic_cast
会在运行时检查被转换的对象是否确实是正确的派生类。额外的检查需要 RTTI(英语:Run-Time Type Information),因此要比 static_cast
慢一些,但是更安全。
一般使用 dynamic_cast
进行向下类型转换。
指针转换的重要原则:清楚指针所指向的真正对象。
- 指针或引用的向上转换总是安全的。
- 向下转换时用
dynamic_cast
,安全检查。
- 避免对象之间的转换。尽可能用指针、引用进行转换。这样可以通过验证虚函数表判断转换是否安全。
历史遗留问题,关于传参的本质(下面这个程序有很大问题):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | #include <iostream>
using namespace std;
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int x = 1, y(2);
swap(x, y);
cout << x << endl;
return 0;
}
/*
output:
2
*/
|
我们试图运行该程序,很显然,输出了 2
。我们似乎已经察觉到了地址和指针之间的美妙关系,也就是直接输出指针的值就是指针指向的地址。我们隐约察觉到,这里 int* a
貌似会指向 x
的地址,所以我们试图把地址打印出来。
(下面这个程序有很大问题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | #include <iostream>
using namespace std;
void swap(int *a, int *b) {
cout << a << endl;
int tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int x = 1, y(2);
swap(x, y);
cout << &x << endl;
cout << x << endl;
return 0;
}
/*
output:
0x7ffebcbe46e8
2
*/
|
看上去很对,但是,似乎少了点什么?我们 swap
函数里对 int* a
的输出呢?这些程序有什么问题?
原因是,在 std
空间里本身就包含有 swap
函数,这么写 swap
根本无法调用我们自己定义的 swap
,故而不可使用 std
空间(这也告诫了我们少用 std
空间,或者把函数名字起的复杂点)。
我们禁用了 std
空间后:
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 | #include <iostream>
// using namespace std;
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int x = 1, y(2);
swap(x, y);
std::cout << x << std::endl;
return 0;
}
/*
output:
main.cpp:11:2: error: no matching function for call to 'swap'
swap(x,y);
^~~~
main.cpp:3:6: note: candidate function not viable: no known conversion from 'int' to 'int *' for 1st argument; take the address of the argument with &
void swap(int *a, int *b)
^
1 error generated.
*/
|
我们终于发现了问题,在调用参数的时候,我们根本无法让一个 int x
传入 int* a
。首先,传参数的过程可以理解为赋值语句:比如我们函数定义 void swap(int a, int b)
。那么调用时,swap(x,y)
需要 int a = x, int b = y
这两个赋值语句都没法实现。但是我们调用 swap(&x, &y)
时,实际上是 int* a = &x, int* b = &y
,这两个赋值都可以实现,且 &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 | #include <iostream>
// using namespace std;
void swap(int *a, int *b) {
std::cout << a << std::endl;
int tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int a = 1, b(2);
std::cout << &a << std::endl;
swap(&a, &b);
std::cout << a << std::endl;
return 0;
}
/*
output:
0x7fff8f992eb8
0x7fff8f992eb8
2
*/
|
从这个例子可以看出来,对实参取地址,然后 int* a = &x
,就会使得 a
指针确切的指向 x
的内存空间,所以可以通过指针 a
控制 x
的内存空间。哪怕 x
的生命周期结束了,退出了函数体之后把 a
指针析构掉,因为我们没有人为定义 delete a
,而默认析构函数不会 delete a
,故而也不会影响 x
的内存空间。综上所述,终于解决了历史遗留问题。我们再做两个实验来验证下:
实验一,在函数体内 delete a
,果然清除了 x
内存空间,导致 x
非法访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | #include <iostream>
void swap(int *a, int *b) {
std::cout << a << std::endl;
int tmp = *a;
*a = *b;
*b = tmp;
delete a;
return;
}
int main() {
int x = 1, y(2);
std::cout << &x << std::endl;
swap(&x, &y);
std::cout << x << std::endl;
return 0;
}
/*
output:
free(): invalid pointer
*/
|
实验二,传引用也是如此:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | #include <iostream>
void swap(int &a, int &b) {
std::cout << &a << std::endl;
int tmp = a;
a = b;
b = tmp;
return;
}
int main() {
int x = 1, y(2);
std::cout << &x << std::endl;
swap(x, y);
std::cout << x << std::endl;
return 0;
}
/*
output:
0x7fff2af01638
0x7fff2af01638
2
*/
|
基于上述引例,考察下这个例子:
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>
using namespace std;
class Pet {
public:
virtual ~Pet() { }
};
class Dog: public Pet {
public:
void run() { cout << "dog run" << endl; }
};
class Bird: public Pet {
public:
void fly() { cout << "bird fly" << endl; }
};
void action(Pet* p) {
auto d = dynamic_cast<Dog*>(p);
auto b = dynamic_cast<Bird*>(p);
if (d)
d->run();
else if (b)
b->fly();
}
int main() {
Pet* p[2];
p[0] = new Dog;
p[1] = new Bird;
for (int i = 0; i < 2; ++i) {
action(p[i]);
}
return 0;
}
/*
output:
dog run
bird fly
*/
|
注意到,在这个例子里面,向下类型转换都不是重点,重点在传参的这一步。因为我们的 p[i]
是的类型是 Pet*
,指向的内存空间是 Dog
或者 Bird
,所以这个传参是合法的!也就是赋值语句,传参过程中 Pet* p = p[i]
是合法的,这个传参就是合法的(将传参视为给函数参数列表里的形参赋值的过程)。
抽象类与纯虚函数
多重继承
优点:清晰,符合直觉。结合多个接口
缺点:
- 二义性:如果派生类
D
继承的两个基类 A
、B
,有同名成员 a,则访问 D
中 a
时,编译器无法判断要访问的哪一个基类成员。
- 钻石型继承树带来的数据冗余:右图中如果
InputFile
和 OutputFile
都含有继承自 File
的 filename
变量,则 IOFile
会有两份独立的 filename
,而这实际上并不需要。
优化方案:
最多继承一个非抽象类(has-a),可以继承多个抽象类(接口)。这样可以避免多重继承的二义性,也可以一个对象可以实现多个接口。
例子
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 | #include <iostream>
using namespace std;
class WhatCanSpeak {
public:
virtual ~WhatCanSpeak() { }
virtual void speak() = 0;
};
class WhatCanMotion {
public:
virtual ~WhatCanMotion() { }
virtual void motion() = 0;
};
class Human: public WhatCanSpeak, public WhatCanMotion {
void speak() { cout << "say" << endl; }
void motion() { cout << "walk" << endl; }
};
void doSpeak(WhatCanSpeak* obj) { obj->speak(); }
void doMotion(WhatCanMotion* obj) { obj->motion(); }
int main() {
Human human;
doSpeak(&human);
doMotion(&human);
return 0;
}
/*
output:
say
walk
*/
|
多态
定义
按照基类的接口定义,调用指针或引用所指对象的接口函数,函数执行过程因对象实际所属派生类的不同而呈现不同的效果(表现),这个现象被称为“多态”。这个定义非常繁琐,实际上就是按照实际类型调用函数罢了。
当利用基类指针、引用调用函数时,虚函数在运行时确定执行哪个版本,取决于引用或指针对象的真实类型。
非虚函数在编译时绑定,故而调用在指针权限范围内优先级最高的函数。关于指针的权限范围在先前已经叙述过。
当利用类的对象直接调用函数时,无论什么函数,均在编译时绑定。
产生多态效果的条件:继承、虚函数、引用或指针。
优势
全部设计抽象类指针指向派生类后,C++ 语言可以用一段相同的代码,在运行时完成不同的任务,这些不同运行结果的差异由派生类之间的差异决定,不必对每一个派生类特殊处理,只需要调用抽象基类的接口即可。大大提高程序的可复用性。另一方面,不同派生类对同一接口的实现不同,能达到不同的效果,提高了程序可拓展性和可维护性。
例子:
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 | #include <iostream>
using namespace std;
class Animal {
public:
void action() {
speak();
motion();
}
virtual void speak() { cout << "Animal speak" << endl; }
virtual void motion() { cout << "Animal motion" << endl; }
};
class Bird: public Animal {
public:
void speak() { cout << "Bird singing" << endl; }
void motion() { cout << "Bird flying" << endl; }
};
class Fish: public Animal {
public:
void speak() { cout << "Fish cannot speak ..." << endl; }
void motion() { cout << "Fish swimming" << endl; }
};
int main() {
Fish fish;
Bird bird;
fish.action();
bird.action();
Animal *pBase1 = new Fish;
Animal *pBase2 = new Bird;
pBase1->action();
pBase2->action();
return 0;
}
/*
output:
Fish cannot speak ...
Fish swimming
Bird singing
Bird flying
Fish cannot speak ...
Fish swimming
Bird singing
Bird flying
*/
|
很有意思的是,直接通过派生类对象调用 action
函数时,由于派生类没有重定义 action
,故而直接调用了基类的 action
。之后以 fish
为例,调用了 fish.speak()
和 fish.motion()
。然而注意到,Fish
类已经重写覆盖了 Animal
的虚函数 speak()
,故而会调用自身重定义的 speak()
、motion()
同理。
下方的通过基类指针调用就是典型的重写覆盖与动态绑定。
函数模板与类模板
意义与定义
如果我们想实现对于整数、浮点数、自定义类的排序函数,这些排序本质上算法是相同的,但是我们可能需要写很多个相似的函数。
有些算法实现与类型无关,所以可以将函数的参数类型也定义为一种特殊的“参数”,这样就得到了“函数模板”。
定义函数模板的方法(可以不写在分开的两行)。
| template <typename T>
ReturnType Func(Args);
|
如:任意类型两个变量相加的“函数模板”:
| template <typename T>
T sum(T a, T b) { return a + b; }
|
注:typename
也可换为 class
。
| template <class T>
T sum(T a, T b) { return a + b; }
|
模板必须在头文件中实现,原因比较复杂,涉及链接原理,不做赘述。
实例化与自动推导
函数模板在调用时,编译器能自动推导出实际参数的类型(这个过程叫做实例化)。所以,形式上调用一个函数模板与普通函数没有区别,如:
| cout << sum(9, 3);
cout << sum(2.1, 5.7);
|
调用类型需要满足函数的要求。本例中,要求类型 T
定义了加法运算符。
当多个参数的类型不一致并且不强制要求类型推导时,无法推导:cout << sum(9, 2.1)
。
可以强制指定类型推导方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | #include <iostream>
using namespace std;
template <class T>
T sum(T a, T b) { return a + b; }
int main() {
cout << sum<int>(9.9, 2.5) << endl;
cout << sum<float>(9.9, 2.5) << endl;
cout << sum<double>(9.9, 2.5) << endl;
}
/*
output:
11
12.4
12.4
*/
|
从上例中可看出先转换再进行加法。
例子
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 | #include <iostream>
#include <algorithm>
template<class T>
void sort(T* data, int len) {
for (int i = 0; i < len; i++) { // 选择排序
for (int j = i + 1; j < len; j++) {
if (data[i] > data[j])
std::swap(data[i], data[j]); // 交换元素位置
}
}
}
template<class T>
void output(T* data, int len) {
for (int i = 0; i < len; i++)
std::cout << data[i] << " ";
std::cout << std::endl;
}
int main() {
int arr_a[] = {3, 2, 4, 1, 5};
sort(arr_a, 5); // 调用 int 类型的 sort
output(arr_a, 5); // 调用 int 类型的 output
float arr_b[] = {3.2, 2.1, 4.3, 1.5, 5.7};
sort(arr_b, 5); // 调用 float 类型的 sort
output(arr_b, 5); // 调用 float 类型的 output
return 0;
}
/*
output:
1 2 3 4 5
1.5 2.1 3.2 4.3 5.7
*/
|
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 | #include <iostream>
#include <algorithm>
template<class T>
void sort(T* data, int len) {
for (int i = 0; i < len; i++) { // 选择排序
for (int j = i + 1; j < len; j++) {
if (data[i] > data[j])
std::swap(data[i], data[j]); // 交换元素位置
}
}
}
template<class T>
void output(T* data, int len) {
for (int i = 0; i < len; i++)
std::cout << data[i] << " ";
std::cout << std::endl;
}
class MyInt {
public:
int data;
MyInt(int val): data(val) { };
}
int main() {
MyInt arr_c[] = {3, 2, 4, 1, 5};
sort(arr_c, 5);
output(arr_c, 5);
return 0;
}
/*
output:
main.cpp: In instantiation of 'void sort(T*, int) [with T = MyInt]':
main.cpp:33:15: required from here
main.cpp:9:15: error: no match for 'operator>' (operand types are 'MyInt' and 'MyInt')
*/
|
模板编译错误会引起上百行的错误,关注上方几行。sort
中需要 operator>
但 MyInt
并不支持。
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 | #include <iostream>
#include <algorithm>
template<class T>
void sort(T* data, int len) {
for (int i = 0; i < len; i++) { // 选择排序
for (int j = i + 1; j < len; j++) {
if (data[i] > data[j])
std::swap(data[i], data[j]); // 交换元素位置
}
}
}
template<class T>
void output(T* data, int len) {
for (int i = 0; i < len; i++)
std::cout << data[i] << " ";
std::cout << std::endl;
}
class MyInt {
public:
int data;
MyInt(int val): data(val) { };
bool operator>(const MyInt& b) { //用于sort
return data > b.data;
}
friend std::ostream& operator<<(std::ostream& out, const MyInt& obj) {
out << obj.data;
return out;
}
};
int main() {
MyInt arr_c[] = {3, 2, 4, 1, 5};
sort(arr_c, 5);
output(arr_c, 5);
return 0;
}
/*
output:
1 2 3 4 5
*/
|
完全重载了流运算符和大小运算符。
模板原理
对函数模板的处理是在编译期进行的,每当编译器发现对模板的一种参数的使用,就生成对应参数的一份代码。
| template<typename T>
T sum(T a, T b) { return a + b; }
int main() {
int a = sum(1, 2); // 生成并编译 int sum(int, int)
double b = sum(1.0, 2.0); // 生成并编译 double sum(double, double)
return 0;
}
|
类模板
定义
在定义类时也可以将一些类型信息抽取出来,用模板参数来替换,从而使类更具通用性。这种类被称为“类模板”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | #include <iostream>
using namespace std;
template <typename T>
class A {
T data;
public:
A(T _data): data(_data) { }
void print() { cout << data << endl; }
};
int main() {
A<int> a(1); // 函数模板可以自动推导类型,但是类模板不可以
a.print();
return 0;
}
|
成员函数也可以如此定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | #include <iostream>
using namespace std;
template <typename T>
class A {
T data;
public:
A(T _data): data(_data) { }
void print();
};
template<typename T>
void A<T>::print() { cout << data << endl; }
int main() {
A<int> a(1);
a.print();
return 0;
}
|
模板参数
我们可以将类模板视为一种特殊的“函数”,需要向其中传入参数才能够定义一个完整的类,而这些传入的参数有两种。一种是类型参数,也就是在 typename
或者 class
之后的参数(下例中的小 T
),第二种是非类型参数,这个可能有非常多种,比如整数、枚举、指针(指向对象或函数),引用(引用对象或引用函数),无符号整数等等,比如下例就是无符号整数来定义了数组的大小。
实际上,模板比较复杂时,尖括号里可以放很多,可以是类型也可以是非类型。当然类型参数也可以有多个。
| template<typename T, unsigned size>
class array {
T elems[size];
};
array<char, 10> array0;
|
注意到,size
只是一个无符号整数的名字而已,unsigned
和 typename
才是关键,完整的传入两个参数才可形成一个完整的类,并定义这个类的对象。
一个模板可能会写出多个新的类,这些类是不同的,不过碰巧都用了同一个模板的名字(比如上例中的 array
模板名字)。
1
2
3
4
5
6
7
8
9
10
11
12
13 | template<typename T, unsigned size>
class array {
T elems[size];
};
int main() {
int n = 5;
// array<char, n> array0; // 不能使用变量
const int m = 5;
array<char, m> array1; // 可以使用常量
array<char, 5> array2; // 或具体数值
return 0;
}
|
所有模板参数必须在编译期确定,因而不可以使用变量。
例如:
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>
#include <algorithm>
template<class T, unsigned size>
class MyArr {
T data[size];
public:
void sort() {
for (int i = 0; i < size; i++) { // 选择排序
for (int j = i + 1; j < size; j++) {
if (data[i] > data[j])
std::swap(data[i], data[j]); // 交换两者位置
}
}
}
void output() {
for (int i = 0; i < size; i++)
std::cout << data[i] << " ";
std::cout << std::endl;
}
void input() {
for (int i = 0; i < size; i++)
std::cin >> data[i];
}
};
int main() {
MyArr<int, 5> arr_a;
arr_a.input();
arr_a.sort();
arr_a.output();
MyArr<float, 5> arr_b;
arr_b.input();
arr_b.sort();
arr_b.output();
return 0;
}
/*
input:
3 2 4 1 5
3.2 2.1 4.3 1.5 5.7
output:
1 2 3 4 5
1.5 2.1 3.2 4.3 5.7
*/
|
模板与多态
模板使用泛型标记,使用同一段代码,来关联不同但相似的特定行为,最后可以获得不同的结果。模板也是多态的一种体现。
但模板的关联是在编译期处理,称为静多态。
模板的特点:
- 往往和函数重载同时使用
- 高效,省去函数调用
- 编译后代码增多
基于继承和虚函数的多态在运行期处理,称为动多态。
虚函数的特点:
所谓的省去函数调用是指,如果 doSomethingOnA
和 doSomethingOnB
的实现除了类型不同,其他基本一致的话则可省去如下代码。
| if (typeof(a) == A) {
doSomethingOnA(a);
} else if (typeof(a) == B) {
doSomethingOnB(a);
}
|
直接写一个模板然后 dosomethingOnWhatever
。