C3P第三部分
第三部分
第十三章
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数
合成拷贝构造函数可以用来阻止拷贝该类类型的对象
class Foo{ public: Foo(const Foo&); }
合成的拷贝构造函数:会将参数的成员逐个拷贝到正在创建的对象中,内置类型直接拷贝,虽然不能直接拷贝一个数组,但会逐个元素进行拷贝,类类型则使用元素的拷贝函数
拷贝初始化通常使用拷贝构造函数来完成
- 用
=
定义变量时。 - 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。
拷贝赋值运算符
赋值运算符通常返回一个指向其左侧运算对象的引用
合成拷贝赋值运算符可以用来禁止该类型对象的赋值
析构函数
析构函数释放对象使用的资源,并销毁对象的非static数据成员,析构函数不接受参数,没有返回值
在一个析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。析构函数体本身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的
当指向一个对象的引用或指针离开作用域时,析构函数不会执行
合成析构函数可以用来阻止该类型的对象被销毁
三/五法则
- 需要析构函数的类也需要拷贝和赋值操作
- 需要拷贝操作的类也需要赋值操作,反之亦然
使用=default
类内使用=default
修饰成员的声明时,隐式声明了该成员为内联的
如果不希望合成的成员是内联函数,则应该对其类外定义使用=default
阻止拷贝
- 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
- 定义删除的函数:
=delete
,虽然声明了它们,但是不能以任何方式使用它们。 - 析构函数不能是删除的成员。
- 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的
- 对于具有引用或无法默认构造的
const
成员的类,编译器不会为其合成默认构造函数。如果具有引用成员的类合成拷贝赋值运算符,则赋值后,左侧运算对象仍指向与赋值前一样的对象,因此对于有引用成员的类,合成拷贝赋值运算符也被定义为删除的
拷贝控制和资源管理
通常,管理类外资源的类必须定义拷贝控制成员
类的行为像值:对象有自己的状态,副本和原对象是完全独立的
如果将一个对象赋予它自身,赋值运算符必须能正确工作;大多数赋值运算符组合了析构函数和拷贝构造函数的工作
行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据
IO类型和unique_ptr
不允许拷贝或赋值,因此它们的行为不像值也不像指针
HasPtr
程序
定义行为像值的类
1 |
|
定义行为像指针的类
1 |
|
交换操作
除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap
的函数。拷贝一个类值的HasPtr
会分配一个新string
并将其拷贝到HasPtr
指向的位置,理论上这些内存分配是不必要的,因为可以只交换指针
类值版本下:
1 |
|
由于三个swap
参数类型不同, 不会导致递归循环,swap
函数应该调用特定版本的swap
而不是std::swap
在赋值运算符中使用swap
定义swap的类通常用swap来定义它们的赋值运算符,使用拷贝并交换的技术
1 |
|
参数不是引用,传入的是右侧运算对象的一个副本,交换副本与*this
中的数据成员,*this
将指向新分配的string
——右侧运算对象中string
的一个副本,当赋值运算符结束时,hp
将被销毁,析构函数delete
掉hp
指向的内存,即原来左侧运算对象的内存。并且,它自动处理了自赋值情况,通过在改变左侧运算对象之前拷贝右侧运算对象保证了自赋值的正确
拷贝控制示例
两个类通过拷贝控制进行簿记操作,Message
和Folder
分别表示电子邮件消息和消息目录,每个Message
对象可以出现在多个Folder
中,每个Folder
中记录其中的所有Message
对象
在Message
中保存一个它所在Folder
的指针的set
,每个Folder
中保存它包含的Message
的指针的set
。Message
类提供save
和remove
操作,用于创建或删除一个对象时与Folder
相关联
1 |
|
对于拷贝构造函数和拷贝赋值运算符,都需要实现已关联的folders
进行更新,因为是拷贝,所以要在每个与原Message
对象有关联的Folder
对象都增加与新拷贝得到的Message
对象进行关联,同理删除也是。另外,拷贝赋值运算符需要额外删除关联,是为了自赋值情况能正常运行,非
[TOC]
自赋值情况下,也需要让原对象删除原有的关联,如果为空则不删除。
1 |
|
Folder
类封装了对set
中每个Message
对象添加和删除该folder
对象的函数,减少代码冗余,其余与Message
类一致
在以上两种类的拷贝赋值运算符中,都没有使用到拷贝并交换技术,是因为此技术适用在有动态内存分配情况下,如HasPtr
的类值实现,否则会增加复杂度
动态内存管理类程序
定义一个类似于vector
的类,需要自己进行内存分配,就要定义拷贝控制成员来管理内存,这个类只用于string
1 |
|
alloc
是一个静态成员,类型是allocator<string>
,能够分配StrVec
的内存alloc_n_copy
会分配内存,并拷贝一个给定范围中的元素free
会销毁构造的元素并释放内存chk_n_alloc_
保证StrVec
至少有容纳一个新元素的空间,如果没有则调用reallocate
分配更多内存reallocate
在内存用完时为StrVec
分配新内存
1 |
|
用allocator
分配内存时,内存是未构造的,需要用construct
函数在此内存中构造一个对象,同时first_free
指针往后递增一位,指向下一个未构造的元素
1 |
|
rs - ls
尾后指针减去首元素指针得出元素空间大小,传给allocate
函数返回所分配空间的首地址,unitialzed_copy
由ls
和rs
之间的元素拷贝到data
起始的内存中,返回最后一个构造元素之后的位置,即返回的pair
由新构建的首元素指针和尾后指针组成
1 |
|
destroy
会执行string
的析构函数,释放string
自己分配的内存空间。当元素销毁后,要调用deallocate
函数释放StrVec
分配的内存空间,传递的必须是之前allocate
调用所返回的指针,因此在调用前应先对指针判空
1 |
|
在拷贝构造函数中直接调用alloc_n_copy
,返回值是一个指针的pair
,first
指向的是第一个构造的元素,second
指向的是最后一个元素之后的位置,由于alloc_n_copy
分配的空间是刚好容纳给定的元素,所以cap
也是在最后一个元素之后的位置
1 |
|
同样地,拷贝赋值运算符也调用alloc_n_copy
函数来初始化指针,为了排除赋值左边对象初始值的影响,要先调用free()
函数对原对象进行析构,为了正确处理自赋值的情况,**free
要在调用alloc_n_copy
之后调用**
1 |
|
首先确定重新分配内存的大小,新分配的容量加倍,如果原对象为空,则分配容纳一个元素的空间。在for循环中,使用construct
构建新的string
对象,同时用elem
表示初始内存的地址并将其不断递增,第二个参数调用move
,返回值将会使得construct
使用string
的移动构造函数,那么这些string
管理的内存将不会被拷贝
1 |
|
StrVec
类添加一个构造函数,它接受一个 initializer_list<string>
参数,可以直接调用alloc_n_copy
函数
手撕string实现代码
对象移动
[TOC]
很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能
标准库容器、string
和shared_ptr
类既可以支持移动也支持拷贝。IO
类和unique_ptr
类可以移动但不能拷贝。
右值引用
通过&&
获得右值引用,只能绑定到一个将要销毁的对象,常规引用可以称之为左值引用。左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值
- 返回左值引用的函数,以及赋值、下标、解引用和前置递增递减运算符,可以将左值引用绑定这类返回左值表达式
- 返回非引用的函数,以及算术、关系、位和后置递增递减运算符,不能用左值引用绑定,但可以用**
const
的左值引用或一个右值引用绑定**
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象
不能将一个右值引用绑定到一个右值引用类型的变量上
int && rr1 = 42;
int && rr2 = rr1; //错误,rr1是左值
move函数:
int &&rr3 = std::move(rr1);
move
告诉编译器,我们有一个左值,但我希望像右值一样处理它。- 调用
move
意味着:除了对rr1
赋值或者销毁它外,我们将不再使用它。
移动构造函数和移动赋值运算符
类似于拷贝构造函数,移动构造函数的第一个参数是该类类型的右值引用,其余额外的参数都必须有默认实参
一旦资源完成移动,源对象必须不再指向被移动的对象
1 |
|
移动构造函数接管给定的StrVec
中的内存,将给定对象中的指针都置为nullptr
,移后源对象会被销毁,将在其上运行析构函数
1 |
|
对于自赋值情况,移动赋值运算符通常会先检查this指针与sv的地址是否相同,如果相同,右侧和左侧对象指向相同的对象,则不做任何事直接返回该StrVec
对象;否则,释放左侧运算对象的内存,并接管右侧对象的内存,最后再将右侧对象的指针置为nullptr
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员(非static)都能移动时,编译器才会为它合成移动构造函数或移动赋值运算符
有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者类成员定义自己的拷贝构造函数且编译器不能为其合成移动构造函数,那么合成的移动操作定义为删除
定义了移动操作的类也必须定义自己的拷贝操作,否则,合成拷贝操作都被默认定义为删除的
对于既有移动构造函数,也有拷贝构造函数的类,将会移动右值,拷贝左值,但如果没有移动构造函数,右值也会被拷贝
拷贝并交换赋值运算符和移动操作
[如前所示](# 在赋值运算符中使用swap
),非引用参数意味着参数要进行拷贝初始化,对于左值将被拷贝,而右值则会被移动,因此,单一的赋值运算符就实现了拷贝赋值运算符和移动赋值运算符
hp = hp2;
hp = std::move(hp2);
第一个赋值,右侧运算对象是一个左值,因此将使用拷贝构造函数来初始化;第二个赋值,调用std::move
将一个右值引用绑定到hp2
上,移动构造函数是精准匹配的,因此,将用移动构造函数拷贝指针,而不分配任何内存
但是,haspt
会执行两次的拷贝,一次在调用move
函数,另一次在移动赋值运算符拷贝给this
指针;而对于普通的移动构造版本,则只会执行一次拷贝
移动迭代器
移动迭代器的解引用运算符生成一个右值引用
make_move_iterator
函数讲一个普通迭代器转换为一个移动迭代器
1 |
|
[原版本](# 动态内存管理类程序)中使用一个for
循环来调用construct
从旧内存将元素拷贝到新内存中。作为一种替换,将使用uninitialized_copy
来构造新分配的内存,传入的是移动迭代器,解引用运算生成符生成的是一个右值引用,意味着construct
将使用移动构造函数来构造元素
右值引用和成员函数
区分移动和拷贝的重载函数通常有一个版本接受一个cosnt T&
,另一个版本接受T &&
通过在参数列表后放置一个引用限定符,可以强制赋值运算符左侧运算对象是左值或者右值
1 |
|
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符
1 |
|
会产生递归并且最终溢出。
1 |
|
与上一题不同,本题的写法可以正确利用右值引用版本来完成排序。原因在于,编译器认为Foo(*this)是一个[无主]的右值,对它调用sorted会匹配右值引用版本
第十四章 重载运算与类型转换
基本概念
如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,成员运算符函数的(显式)参数数量比运算符的运算对象少一个
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数
赋值、下标、调用和成员访问运算符必须是成员函数
输入和输出运算符
必须是普通的非成员函数,需要读写非公有数据成员时,一般被声明为友元
输出运算符的第一个形参是一个非常量ostream
对象的引用,因为向流写入内容会改变其状态,且无法拷贝一个ostream
对象;第二个形参是一个常量的引用,是想要打印的类类型
1 |
|
输出运算符尽量减少格式化操作,不应该打印换行符
输入运算符的第一个形参是一个非常量istream
对象的引用,第二个形参是将要读入的对象的引用,非常量
1 |
|
if
语句检查读取操作是否成功,如果发生IO错误,则运算符将给定的对象重置为空Sales_data
输入运算符必须处理输入可能失败的情况
算术和关系运算符
一般不需要改变运算对象的状态,所以形参都是常量的引用。计算两个运算对象并得到一个新值,通常位于一个局部变量之内,最终返回该局部变量的一个副本
如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算数运算符,可以避免使用临时对象
相等运算符和不等运算符的一个应该把工作委托给另一个
如果存在唯一一种逻辑可靠的<
定义,则应该考虑为这个类定义<
运算符。如果同时还包含==
,则当且晋档<
的定义和++
产生的结果一直时才定义<
运算符
赋值运算符
1 |
|
与拷贝赋值运算符不同,这个运算符无需检查对象向自身的赋值,因为形参initializer_list
确保il
与this
指向的不是同一个对象
下标运算符
以所访问元素的引用作为返回值,一般同时定义下标运算符的常量版本和非常量版本
递增和递减运算符
定义递增和递减运算符的类应该同时定义前置版本和后置版本
前置运算符应该返回递增或递减后对象的引用;后置运算符应该返回递增或递减前对象的值,后置版本接受一个额外的,不被使用的int
类型的形参
1 |
|
注意前置递增运算符先将当前值传递给check
函数,而递减运算符是先递减curr
,再调用check
函数
后置版本无需检查有效性,因为返回的是递增或递减前的状态的副本
成员访问运算符
1 |
|
解引用运算符检查curr
是否在作用范围内,如果在则返回curr
指针所指元素的引用;箭头运算符调用解引用运算符并返回解引用结果元素的地址
函数调用运算符
如果累定义了调用运算符,则该类的对象称作函数对象
一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别
lambda
捕获变量:lambda
产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,由lambda
产生的类当中的函数调用运算符是一个const
成员函数
标准库函数对象:
算术 | 关系 | 逻辑 |
---|---|---|
plus<Type> |
equal_to<Type> |
logical_and<Type> |
minus<Type> |
not_equal_to<Type> |
logical_or<Type> |
multiplies<Type> |
greater<Type> |
logical_not<Type> |
divides<Type> |
greater_equal<Type> |
|
modulus<Type> |
less<Type> |
|
negate<Type> |
less_equal<Type> |
比较两个指针将产生未定义的行为,但可以通过标准库函数对象比较指针内存地址
1 |
|
统计大于1024的值有多少个
1 |
|
找到第一个不等于pooh的字符串
1 |
|
将所有的值乘以2
1 |
|
判断一个给定的int
值是否能被 int
容器中的所有元素整除
1 |
|
可调用对象与function
可调用对象:函数、函数指针、lambda
表达式、重载调用运算符的类和bind
创建的对象
每一个lambda
都有自己唯一的未命名的类类型,函数和函数指针的类型则由返回值和实参类型决定,但两个不同类型的可调用对象可以有相同的调用形式,调用形式包含调用的返回类型和实参类型,int (int, int)
1 |
|
定义一个函数表,用于存储指向这些可调用对象的指针map<string, int(*)(int, int)> calc
calc.insert("+", add);
可添加add
的指针至函数表中,但不能直接存入mod
和divide
,因为它们并不是函数指针类型,与map
中所要求的类型不匹配,但可以通过function
类型来解决
标准库function类型:
操作 | 解释 |
---|---|
function<T> f; |
f 是一个用来存储可调用对象的空function ,这些可调用对象的调用形式应该与类型T 相同。 |
function<T> f(nullptr); |
显式地构造一个空function |
function<T> f(obj) |
在f 中存储可调用对象obj 的副本 |
f |
将f 作为条件:当f 含有一个可调用对象时为真;否则为假。 |
定义为function<T> 的成员的类型 |
|
result_type |
该function 类型的可调用对象返回的类型 |
argument_type |
当T 有一个或两个实参时定义的类型。如果T 只有一个实参,则argument_type |
first_argument_type |
第一个实参的类型 |
second_argument_type |
第二个实参的类型 |
声明一个function
类型,它可以表示接受两个int
,返回一个int
的可调用对象。function<int(int, int)>
1 |
|
重载、类型转换与运算符
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型
operator type() const;
一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const
int i = 42;
cin << i;
使用istream
的bool
类型转换运算符将cin
转换成bool
,而这个bool
会被提升为int
进而执行左移42位的操作
可以通过显式的类型转换运算符防止自动隐式转换,这样,在执行类型转换时,需通过显式的强制类型转换
static_cast<int> (si) + 3;
向bool
的类型转换通常用在条件部分,因此operator bool
一般定义成explicit
的
1 |
|
第一条语句指明类型转换得到的值为const int
,不能对其进行修改
第二条语句指明不能对类对象进行修改
两个类提供相同的类型转换,例如A定义一个接受B的构造函数,同时B类定义了一个转换目标是A的类型转换运算符,则产生二义性
1 |
|
同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题
1 |
|
ld = si + ld;
中两个类都不能相互转换,因此不能使用重载+,2si
转换成int
,ld
转换成double
或float
,将产生二义性
ld = ld + si;
中可以使用ld
的重载+,ld
在左,接受double
的右侧运算对象,精确匹配;还可以ld
转换成double
或float
,si
转换成int
,再使用内置+,但优先级低于前者,故无二义性
第十五章 面向对象程序设计
定义基类和派生类
层次关系的根部是一个基类,其他类可通过直接或间接继承基类,继承得到的类称为派生类
派生类要通过派生类列表显式指出从哪个基类继承而来,如class Bulk_quote : public Quote{//类定义};
,且只在类定义时才指出派生类列表,声明中只包含类名但不包含派生类列表class Bulk_quote;
对于某些函数,基类希望它的派生类个自定义适合自己的版本,此时基类就将这些函数声明成虚函数,virtual
派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override
关键字
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义
派生类对象中包含了:派生类自己定义的成员的子对象(非静态)以及所继承的基类的子对象,即派生类对象中也含有基类对应的组成部分,可以把派生类对象当成基类对象来使用,通过将基类的指针或引用绑定到派生类对象中的基类部分上
1 |
|
这种转换称为派生类到基类的转换
派生类必须使用基类的构造函数去初始化它的基类部分
如果将某个类作为基类,则该类必须已定义而非只有声明
防止继承发生可以在类名或函数名后加上关键字final
静态类型与动态类型
静态类型在编译时已知,是根据变量声明或表达式生成的类型;动态类型则是变量或表达式表示的在内存中的类型,在运行时才可知
如上例子中p
或者r
在绑定bulk
后动态类型与静态类型不一样了,静态类型为Quote
而动态类型为Bulk_quote
因为一个基类对象可能是派生类对象一部分也可能不是,所以不存在从基类向派生类的隐式类型转换
用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,派生类部分则会被忽略掉
虚函数
使用虚函数可以执行动态绑定,动态绑定只有通过指针或者引用调用虚函数时才会发生
当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同
当派生类覆盖了某个虚函数时,该函数在某类中的形参必须与派生类中的形参严格匹配
如果我们想覆盖某个虚函数,但不小心把形参列表弄错了,这个时候就不会覆盖基类中的虚函数。加上override
可以明确程序员的意图,让编译器帮忙确认参数列表是否出错
如果通过基类引用或指针调用函数,则使用基类中定义的默认实参,因此派生类中定义的默认实参最好与基类一致
抽象基类
纯虚函数用于清晰地告诉用户当前的函数是没有实际意义的。只用在函数体的位置前书写=0
就可以将一个虚函数说明为纯虚函数
含有纯虚函数的类是抽象基类,抽象基类不能被直接创建成对象
访问控制与继承
protected
: 基类和和其派生类还有友元可以访问。private
: 只有基类本身和友元可以访问。
派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
只有当公有继承自基类时,用户代码才能将派生类转换成基类
1 |
|
派生访问说明符的目的是:控制派生类用户对于基类成员的访问权限。比如struct Priv_Drev: private Base{}
意味着在派生类Priv_Drev
中,从Base
继承而来的部分都是private
的
供派生类访问 应声明为受保护的,则派生类及其友元不能访问私有成员;由基类及其基类的友元访问 应声明为私有的
不能继承友元关系
使用using
改变个别成员的可访问性,using
声明语句中名字的访问权限由该using
声明语句之前的访问说明符决定
继承中的类作用域
派生类的作用域嵌套在其基类的作用域之内
派生类的成员将隐藏同名的基类成员
构造函数与拷贝控制
如果基类的析构函数不是虚函数,则delete
一个指向派生类对象的基类指针将产生未定义的行为。因此基类通常定义一个虚析构函数virtual ~Quote() = default;
如果一个类定义了析构函数,即使是通过=default
的形式使用合成的版本,编译器也不会合成移动操作
派生类构造函数在初始化阶段不仅要初始化派生类自己的成员,还负责初始化基类部分。拷贝、赋值和移动同理,而析构函数只负责销毁派生类自己分配的资源
当为派生类定义对应的拷贝或移动构造函数时,通常使用对应的基类构造函数来初始化派生类对象的基类部分
容器与继承——Basket类程序
如果希望使用容器存放具有继承关系的对象时,由于派生类赋值给基类对象时,其中的派生类对象会被忽略掉,因此实际上存放的应该是基类的(智能)指针
为了实现basket
存放shared_ptr
,定义一个表示购物篮的类,并提供添加和打印输出的接口,私有成员定义一个multiset
存放指向不同Quote
的shared_ptr
即multiset<shared_ptr<Quote>>
1 |
|
multiset
中存放的是shared_ptr
,因此需要自定义小于运算符,初始化items
并令其使用compare
函数
1 |
|
for
循环中,*iter
解引用得到指向Quote
的智能指针,调用upper_bound
函数可以返回multiset
中第一个非*iter
智能指针指向的元素,替代了递增操作,意味着不需要打印重复的书本,在相同的书中只取一本作为代表打印。**iter
解引用则返回Quote
元素
接下来要实现add_item
函数,因为存放的是Quote
类型的智能指针,而并不确定指针所指向的具体类型,即该指针的静态类型有可能与动态类型不一样,在make_shared(*类型*)
或者shared_ptr pt = new *类型*
时无法确定使用基类还是派生类,如果使用基类,则派生类中的非基类部分会被忽略。因此,给Quote
类添加一个虚函数,返回一个新申请的当前对象的拷贝
1 |
|
实现add_item
函数
1 |
|
新版本add_item
可以直接传入quote
对象,程序自动生成相应类型的智能指针。clone
无论是拷贝或者是移动数据,都会返回shared_ptr
,然后调用insert
直接加入到items
中
第十六章——模板与泛型编程
函数模板
template <typename T> int compare(const T &v1, const T &v2){}
以关键字 template
开始,后接模板形参表,模板形参表是用尖括号<>
括住的一个或多个模板形参的列表,用逗号分隔,不能为空
类型模板参数——在关键字typrename
或者class
之后,可以用来指定返回类型或函数的参数类型
非类型模板参数——用来表示一个值而非一个类型,通过特定的类型名指定非类型参数,当一个模板实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替
1 |
|
一个非类型参数可以是一个整数,或者是一个指向对象或函数类型的指针或引用;绑定到非类型整型参数的实参必须是一个常量表达式,必须具有静态的生存期。
inline
或constexpr
说明符放在模板参数列表之后,返回类型之前
template <typename T> inline T min(const T&, const T&);
编写类型无关的代码
- 模板中的函数参数是
const
的引用 - 函数体中的条件判断仅使用
<
比较运算
模板编译
只有当实例化出模板的一个特定版本时,编译器才会生成代码
- 普通函数的声明和类定义放在头文件,普通函数的定义和类成员函数的定义放在源文件中
- 函数模板或类模板成员函数的声明和定义都要在头文件中