泛型函数是指那些参数类型或者返回值类型直到使用这个函数的时候才能确定的函数。
这些待定的参数并不是完全随意的,函数使用未知类型参数的方式会约束参数的类型。比如在定义一个泛型函数时,使用了两个参数x和y,并且使用了表达式x+y,此时这种使用方式就产生了一个约束条件:x和y的类型必须能够支持+操作。
实现泛型函数的语言特性叫做模板函数(templatefunction)。
模板允许我们为行为相似的一族函数(或者类型)编写一个单独的定义,族中的每个函数(或类型)的不同都归因于它们的模板参数不同。
我们能够编写模板函数,也就暗示了,不同类型的对象仍然可以有相同的行为。模板参数允许根据共同的行为来编写函数,即使当定义这个模板时,并不知道对应于模板参数的特定类型。当使用一个模板时,就知道这些类型了,而且当我们编译并链接程序时,这些类型也是明确的。对于泛型参数来说,系统并不担心如何操作那些在执行过程中会改变的类型对象,它只需要在编译时考虑这个问题。
一个模板函数的例子:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template <
class T>
T median ( vector { typedef typename vector vec_sz size = v.size(); if ( 0 == size ) throw domain_error ( "median of an empty vector" ); sort ( v.begin(), v.end() ); vec_sz mid = size / 2; return size % 2 == 0 ? ( v[mid] + v[mid - 1] ) / 2 : v[mid]; } |
第一行是模板头,表明这里定义的是一个模板函数,并且类型参数为T,T是一个在使用时才能确定的类型,并不是一个变量。系统会在编译器把T和一个具体的类型绑定,之后,在出现T的地方,系统就会把它替换为相应绑定的类型。这也就是实例化。
第4行typename是类型名指示符,它的使用是为了告诉系统vector
关于typename的用法,C++语言标准规定:
1
2 3 |
A name used in a template declaration ordefinition and that is dependent on a template - parameter is
assumed not to namea type unless the applicable name lookup finds a type name or the name isqualified by the keyword typename. |
大意是:一个用在模板声明或者定义中的名字,而且这个名字依赖于模板参数,那么它默认并不是一个类型名,除非这个名字通过名字查找找到了一个类型名,或者这个名字有typename关键字来指定。(自己翻译的,很烂,将就着看看吧)
也就是说无论什么时候,如果你用一个依赖于模板参数的类型时,比如vector
考虑下面的错误代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template <
typename T>
void foo( const T &t) { // 声明一个指向某个类型为T::bar的对象的指针 T::bar *p; } struct StructWithBarAsType { typedef int bar; }; int main() { StructWithBarAsType x; foo(x); } |
这段代码看起来能通过编译,但是事实上这段代码并不正确。因为编译器并不知道T::bar究竟是一个类型的名字还是一个某个变量的名字。究其根本,造成这种歧义的原因在于,编译器不明白T::bar到底是不是“模板参数的非独立名字”,简称“非独立名字”。注意,任何含有名为“bar”的项的类T,都可以被当作模板参数传入foo()函数,包括typedef类型、枚举类型或者变量等。
模板实例化就是程序在编译期或者链接期,使用具体的类型来替代类型参数。因此要求模板的声明和定义都是系统可以访问的。
注意:如果定义的模板函数中的类型参数没有出现在函数的参数列表中,那么调用这个函数的程序就无法推断出这个参数需要使用什么具体的类型来实例化,比如:
1
2 3 4 |
template<
class T> T zero ()
{ return 0; } |
这里的类型参数仅仅用在了返回值类型上,如果在调用时没有指定实例化类型,则系统无法判断T需要使用什么类型实例化,所以应该采用下面的方式调用:
1
|
double x = zero<
double>();
|
REFERENCE:
http://pages.cs.wisc.edu/~driscoll/typename.html
http://zh.wikipedia.org/zh-cn/Typename
__________________________________________________________________________________________________________________________________________________________________________________
不同的容器有不同的功能,导致其迭代器对操作的支持都不一样,各种算法在完成其功能时,会选择更有利的迭代器。因此,容器、迭代器、算法和操作这四者是相互关联、相互影响的。凡是两个迭代器支持的操作完全相同,那么就会给予他们相同的名字,也就是说,迭代器的分类是依据操作进行的。标准库定义了种迭代器。
顺序只读访问
顺序只写访问
顺序读写访问
可逆访问
随机访问
q、p<=q、p>=q
__________________________________________________________________________________________________________________________________________________________________________________
在第三章,我们详细讨论了为什么在程序中大多采用从0开始的计数方案,而且提倡使用不对称区间(http://blog.csdn.net/duanxu_yzc/article/details/13614287#t17),现在我们来看看:
为什么要使用最后一个元素的下一个位置的迭代器?
容器越界值:每个容器的end成员函数的返回值,其迭代器为越界迭代器。对一个越界迭代器解引用的结果是未定义的。对其进行计算也是未定义的。对开头元素之前的迭代器也是一样。
__________________________________________________________________________________________________________________________________________________________________________________
标准库提供了输入输出流迭代器:istream_iterator和ostream_iterator,使我们可以像通过迭代器操作普通容器一样来操作流。
用法举例:
1
2 3 4 |
istrean_iterator<
int> in_iter(cin);
// int_iter为一个输入流迭代器,用作从cin中读取int值 istream_iterator eof; // eof也是一个输入流迭代器,指示尾后位置(eof或者非目标值) while (in_iter != eof) vec.push_back (*in_iter++); |
1
2 3 |
// 更简洁的做法 istraem_iterator< int> in_iter(cin), eof; vector< int> vec(in_iter, eof); |
必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的osteam_iterator:
1
2 |
copy (vec.begin(), vec.end(), ostream_iterator<
int>(cout,
""));
cout << endl; |
===========================================================================================================
本章以定义一个新的Student_info类为例进行分析。
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 |
class Student_info {
public: std::string name() const { return n; } bool valid() const { return !homework.empty(); } std::istream &read ( std::istream & ); double grade () const; Student_info (); Student_info ( std::istream & ); private: std::string n; double midterm, final; std::vector< double> homework; }; istream &Student_info::read ( istream &in ) { in >> n >> midterm >> final; read_hw ( in, homework ); return in; } double Student_info::grade () const { return ::grade ( midterm, final, homework ); } bool compare ( const Student_info &x, const Student_info &y ) { return x.name() < y.name(); } Student_info::Student_info(): midterm( 0), final( 0) {} Student_info::Student_info ( istream &is ) { read(is); } int main () { vector Student_info record; string::size_type maxlen = 0; while ( record.read(cin) ) { maxlen = max ( maxlen, record.name().size() ); students.push_back(record); } sort ( students.begin(), students.end(), compare ); for ( vector i != students.size(); ++i ) { cout << students[i].name() << string ( maxlen + 1 - students[i].name().size(), ' ' ); try { double fina_grade = students[i].grade(); streamsize prec = cout.precision(); cout << setprecision( 3) << final_grade << setprecision(prec) << endl; } catch ( domain_error e ) { cout << e.what() << endl; } } return 0; } |
第1行以关键字class开头,说明Student_info是我们定义的一个类。
类是一种可以把相关数据值结合到数据结构中,从而把数据结构看做一个实体的机制。
__________________________________________________________________________________________________________________________________________________________________________________
第2行中和第13行中分别出现了public和private关键字,这两个关键字是C++语言的保护机制。这种保护机制允许用户在定义类时,指定那些成员是公有的(也就是可以被所有用户访问到的),哪些成员是私有的(也就是用户不能访问的)。public和private都是保护标签(protection label),它们在一个类的定义中可以按任意顺序出现,也可以多次出现。
其实在第1行中,我们可以使用struct来代替class,这两个关键字都可以用来定义一个新的类,它们之间的唯一区别就是默认保护方式不同。默认保护会应用于第一个保护标签出现之前的所有成员。class的默认保护方式为私有,而struct的为公有。
一般来说,对于简单的类型,打算公开它的数据结构,就使用struct,而对于自己定义的类型,并且要控制成员的访问性,就应该使用class。
__________________________________________________________________________________________________________________________________________________________________________________
1
2 3 4 |
std::string name()
const
{ return n; } |
第3行到第5行定义了一个名叫name的成员函数,根据定义可以看出,这个函数是向调用者返回学生的姓名。原本,这个函数应该有一个Student_info类型的参数,而且,返回值应该写成s.n(假如有形参为s),但是这个函数定义中,既没有参数,也没有成员n的归属类。原因是,这个函数是类Student_info的一个成员函数,所以我们只有一种方式来调用它,就是s.name(),也就是必须指定调用者,而这个调用者正好就是上面分析中所缺少的主角。所以,可以在定义中不需要出现这个调用者。
这个定义中还有一个值得注意的地方,就是name()后面的const,这个关键字用在函数参数列表和{之间,用来说明这个成员函数不会修改类中的任何成员数据。
再看看另外一个成员函数read的定义,它位于第21到第26行,这个成员函数定义在类之外,因此必须指明函数所属的空间:Student_info。即便这个函数是定义在类之外的,但是在参数列表中依然不需要这个类型的形参。
我们观察一下定义于第33行到第36行的一个非成员函数compare,这个函数因为不是成员函数,所以在参数列表中使用了Student_info类型的参数,并且使用了显示的调用者x.name()。
__________________________________________________________________________________________________________________________________________________________________________________
我们继续观察name函数的定义,这个成员函数并没有定义在类之外,而是直接定义在类的内部,这样可以避免函数的调用开销,而是直接把函数内联(inline)展开。
另外,这个定义是public的,但是函数返回的内容n(学生姓名)又是private的,使用一个公有的函数让用户访问一个私有的成员数据,看起来似乎有些令人疑惑,但是,有些时候这是很合理的。在这里,我们不希望用户直接和底层数据结构打交道,但是用户又必须访问n这个成员,所以最好的方式便是使用这种访问器函数(accessor function)的形式,提示用户可以通过一个函数(const)来达到访问底层数据的目的。
__________________________________________________________________________________________________________________________________________________________________________________
观察代码的第30行,这是成员函数grade的定义,其中有一个奇怪的表达式 ::grade ( midterm,final, homework ),这里调用的grade函数是一个重载函数,而且这个重载函数不是类的一个成员,使用::就是为了声明这点,否则,系统就会调用Student_info::grade,这样会因为传递了过多的参数而报错。
__________________________________________________________________________________________________________________________________________________________________________________
观察第11-12和38-42行,它们是构造函数的声明和定义。
构造函数的名字和类本身的名字相同,而且它没有返回值,可以定义多个构造函数,系统通过参数的数量和类型来区分。
构造函数(constructor)是特殊的成员函数,它定义了对象如何进行初始化。,我们没有办法显式地调用一个构造函数,但是创建一个类的对象时就会自动得调用适当的构造函数。如果类没有定义任何构造函数,编译器就会为我们合成一个。
合成的构造函数将初始化对象的数据成员,成员初始化的值取决于对象的创建方式。如果对象是一个局部变量,那么数据成员将会被默认初始化。如果是下面的三种情况中之一,那么对象的成员会被数值初始化:第一种情况是,对象被用来初始化一个容器元素;第二种是,为map添加一个新元素,而对象是这个添加动作的副作用;第三种则是,定义一个有特定长度的容器,对象是这个容器的元素。这些规则有些复杂,但它们的实质不外是:
Tips:
默认初始化:当我们不为变量提供一个初始值时,我们就会隐式地依赖于默认初始化。默认初始化取决于变量的类型。对于类的对象来说,类会自己说明如果没有指定初始值时如何初始化。对于内置类型来说,默认初始化会导致初始值为无意义的垃圾值。
值初始化为使用合适的值对对象进行初始化(值初始化书中没有明确解释,网上也没有找到合适的参考)。
REFERENCE:
关于C++中的赋值和初始化较深刻的探讨请参考:http://www.cppblog.com/ly4cn/archive/2007/09/27/33039.html
默认构造函数
Student_info::Student_info(): midterm(0), final(0){}
没有参数列表的构造函数,它的工作是确保对象的数据成员被适当地初始化。
在:和{之间的序列是构造函数的初始化列表,使用对应圆括号中的值初始化这些给定的成员。由于函数体中没有任何语句,所以,其它没有给定初始值的两个数据成员会被隐式地初始化。
Tips:
在我们创建一个新的类对象的时候会有以下几个连续的步骤:
1.系统分配内存以保存这个对象。
2.按照构造函数初始化列表初始化这个对象。
3.执行构造函数的函数体。
系统会初始化每一个对象的所有数据成员—不管这些成员有没有在构造函数初始化列表中出现。构造函数的函数体有可能会随后改变这些初始值,不过这个初始化动作是在构造函数的函数体开始执行之前发生的。一般来说,在构造函数的函数体中为一个成员赋值并不是太理想的做法,更好的做法是明确地为成员指定一个初始化值,这样我们就可以避免做两次相同的工作。
带有参数的构造函数
Student_info::Student_info ( istream &is )
{
read(is);
}
这个构造函数将初始化的工作委托给了read函数。
===========================================================================================================
指针
数组
函数
字符串
__________________________________________________________________________________________________________________________________________________________________________________
标准错误流:cerr紧急错误立即输出,clog在日志上输出。
输入文件对象类型:ifstream,输出文件对象类型:ofstream。
标准库说明:把ifstream定义为一种istream,把ofstream定义为一种ostream。导致程序中的istream和ostream都可以分别替换为ifstream和ofstream。
定义在
用法:ifstream infile ("in.txt" ) ofstream outfile ("out.txt" )
stringfile ( "file.txt" ) ifstreaminfile ( file.c_str() )
实际参数是一个指向文件名首字符的指针,如果把文件名存储在一个string中,则可以使用成员函数c_str产生合适的指针参数。
__________________________________________________________________________________________________________________________________________________________________________________
C++中提供了3中内存管理方式,分别是自动内存管理(如:局部变量)、静态分配内存(如:静态变量)和动态分配内存(如new、delete)。我们看看动态分配内存。
new和delete可以为对象分配空间和释放空间,具体使用方法如下:
===========================================================================================================
本章定义了一个类似标准库中的vector的类Vec,一起看看这个山寨版的vector是如何造出来的。
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 103 104 105 106 107 108 109 110 |
template <
class T>
class Vec {
public: typedef T* iterator; typedef const T* const_iterator; typedef size_t size_type; typedef T value_type; typedef T& reference; typedef const T& const_reference; Vec() { create(); } explicit Vec ( size_type n, const T& t = T() ) { create (n, t); } Vec ( const Vec& v ) { create (v.begin(), v.end()); } Vec& operator= ( const Vec&); ~Vec() { uncreate(); } T& operator[] (size_type i) { return data[i]; } const T& operator[] (size_type i) const { return data[i]; } void push_back ( const T& t) { if (avail == limit) grow(); unckecked_append (t); } size_type size() const { return avail-data; } iterator begin() { return data; } const_iterator begin() const { return data; } iterator end() { return avail; } const_iterator end() const { return avail; } private: iterator data; iterator avail; iterator limit; allocator void create(); void create (size_type, const T&); void create (const_iterator, const_iterator); void uncreate(); void grow(); void unchecked_append ( const T&); }; template < class T> Vec { if ( &rhs != this ) { uncreate(); create (rhs.begin(), rhs.end()); } return * this; } template < class T> void Vec { data = avail = limit = 0; } template < class T> void Vec { data = alloc.allocate (n); limit = avail = data + n; uninitialized_fill (data, limit, val); } template < class T> void Vec { data = alloc.allocate (j-i); limit = avail = uninitialized_copy (i, j, data); } template < class T> void Vec { if (data) { iterator it = avail; while (it != data) alloc.destroy(--t); alloc.deallocate (data, limit-data); } data = limit = avail = 0; } template < class T> void Vec { size_type new_size = max ( 2*(limit-data), ptrdiff_t( 1)); iterator new_data = alloc.allocate (new_size); iterator new_avail = uninitialized_copy (data, avail, new_data); uncreate(); data = new_data; avail = new_avail; limit = data + new_size; } template < class T> void Vec { alloc.construct (avail++, val); } |
第1行template
__________________________________________________________________________________________________________________________________________________________________________________
第3-8行,定义了一些可供用户使用的类型名称,之所以把原本的类型名通过typedef来重新命名,一方面是为了隐藏内部数据的结构和组织形式,另一方面是为了和标准库的命名规则相符合。由于Vec是建立于build-in array的基础上的,所以我们使用指针来作为它的迭代器:typedef T* iterator; typedef const T* const_iterator;。
__________________________________________________________________________________________________________________________________________________________________________________
第10-11行,声明了两个构造函数,一个是不带参数的,另一个是带有两个参数的,这两个函数的声明和定义包含了大量的信息,我们逐一讨论。
1、第一个是不带参数的构造函数。
2、第二个构造函数中,有两个参数,一个是大小,另一个是类型为T的一个引用对象,并且使用了默认参数,这个默认参数是类型T的默认构造函数,如果调用者没有提供第二个参数,系统便会使用类型T的默认初始化值来初始化Vec中的元素。
3、两个构造函数的任务全交由create函数完成,两个create函数的定义在第62-72行,不带参数的create函数的定义data = avail = limit = 0;中,将3个分别表示数据起始位置、实际存储的最后一个数据的下一个位置和可供存储位置的下一个位置的迭代器(在这里其实就是指针)均置为0,表示创建一个空的Vec。关于为什么要使用avail,我们会在后面讨论。第二个create函数首先调用alloc的成员函数allocate,分配n个元素的空间,并将空间的地址给data,接着设置limit和avail的值,最后使用函数uninitialized_fill将初始化值填充到分配的空间中。
4、这里我们没有使用new和delete来动态分配内存,原因是:new会强制使用T的默认构造函数初始化每个元素。我们希望能主动地初始化每个元素,所以选用了标准库提供的allocator类来分配空间,这样我们可以控制空间的初始化。我们在Vec类的定义中使用了allocator类的四个成员函数和两个非成员函数:
allocate成员函数用来分配一块被指定了类型但却未被初始化的内存块,这些内存已足以储存相应类型对象的元素。被指定了类型的内存,意思是这块内存块将用来储存类型为T的值,我们可以通过使用一个T*类型的指针来得到它的地址。未被初始化的内存,意思是这块内存是原始的,在这块内存块中没有储存任何实际的对象。
deallocate成员函数则是用来释放未被初始化的内存,它带有两个参数:一个是allocate函数返回的指针;另一个是该指针指向的内存块的大小。
construct成员函数是用来在allocate申请分配但尚未被初始化的内存区域上进行初始化,生成单个的对象,destroy成员函数则用来删除这个对象。construct构造函数带有两个参数:一个是allocate函数返回的指针:另一个是用来复制到指针指向的内存块的对象值。destroy函数调用析构函数,删除它的参数所指对象的元素,再次是这块内存成为未初始化的状态。
另外两个非成员函数,它们是uninitialized_copy和uninitialized_fill函数。这两个函数对allocate所分配的内存进行初始化。uninitialized_fill函数向内存块中填充一个指定的值。在函数调用结束后,前两个参数指针指向的内存区间中的元素都被初始化成第三个参数所指对象的内容。uninitialized_copy函数的工作机理类似于标准库中的copy函数,它用来把前两个参数指针所指向的内存区间中的值复制到第三个参数指针所指向的目标内存块中。像uninitialized_fill函数一样,它假定目标内存块尚未被初始化,而不是已经储存着一个实际对象的值,它将在目标内存块中构造新的对象。
有了对上面几个函数的讲解,create函数的原理就很简单了。
5、最后,我们看看explicit关键字的使用,这个关键字只能用在类的内部声明构造函数中,而且只能作用于只有一个参数的构造函数上,意思是,只有在用户明确地调用这个构造函数的地方,编译器才能使用这个构造函数,阻止隐式转换。(不太明白)
__________________________________________________________________________________________________________________________________________________________________________________
第13行也是一个构造函数,不过是一个复制构造函数(copy constructor),它把一个新的对象初始化为另一个已有的、类型相同的对象的副本。与其他构造函数相同,赋值构造函数也是一个成员函数,名字和类的名字相同,并且只有一个参数:源对象的一个const引用。这个函数将工作交给了create函数的另一个版本。在定义复制构造函数时要注意,必须给新对象分配独立的底层数据空间,不能和原对象公用底层数据,否则,在操作这两个对象的其中一个时也会影响到另一个对象,达不到复制的效果。(如:类中包含指针类型成员)。
Tips:
隐式复制:将对象值传递到一个函数,从一个函数返回一个对象值
显式复制:在初始化时的显式复制
__________________________________________________________________________________________________________________________________________________________________________________
第14行是对赋值操作符的定义声明,具体定义在第52-62行。其实这就是操作符的重载,我们在需要重载的操作符前添加operator,并把它们当做一个函数名,其它的工作就和定义一个普通的成员函数没有什么区别了。对于=操作符,我们希望把某个对象a的值全部复制给另一个对象b,并在此之前要首先清空b的数据释放为b分配的内存,然后重新分配空并赋值(相当于重新构造对象b),使用create函数完成(对于底层数据的处理和复制构造函数一样),并返回左操作数的值引用,为了防止自身赋值带来的隐患,还添加了if ( &rhs != this )作为判断。
this关键字只在成员函数内部有效,表示的是指向成员函数操作的对象的指针。对于=来说,this指代的就是左操作数,因此函数返回值*this就是左操作数的值。
Tips 1:初始化和赋值的区别
初始化是创建一个新对象,同时为这个对象提供一个值;赋值是清空先前的值,再提供值。
初始化的形式有多种,但是赋值只出现在有=操作符的时候,并且左操作数一定是一个已经初始化过了的对象。
构造函数都是用来控制初始化的,而operator=成员函数是用来控制赋值的。
Tips 2:如果一个类需要一个析构函数,那么它也需要一个复制构造函数和一个赋值操作符;如果需要复制构造函数,也需要赋值操作符,反之亦然。
__________________________________________________________________________________________________________________________________________________________________________________
第15行定义的是析构函数(destructor),析构函数也是一个成员函数,以~开头,后面是类的名字和空的参数列表。析构函数用来销毁对象中的所有数据并释放空间,这个函数的工作交由uncreate函数完成。uncreate函数定义在第81到91行,由alloc.destroy函数来逐一销毁元素,再由alloc.deallocate函数释放分配的空间。
__________________________________________________________________________________________________________________________________________________________________________________
第17-18行是对[]操作符的重载。
__________________________________________________________________________________________________________________________________________________________________________________
第20-24行定义了一个成员函数push_back。在看这个函数之前我们先看看位于93-105行的grow函数,我们要实现push_back函数,就必须能够实现为Vec动态分配内存,我们选择了allocator类,但是如果我们每次调用push_back函数时,都为Vec增添一个内存空间,这样运行的效率会大大降低,为了解决这个问题我们打算采用预分配的方案,也就是只要push_back函数需要更多的空间,我们就分配当前使用空间的两倍内存空间,并把已有的数据复制到新分配的空间上,再销毁原来的数据和空间,最后将未使用的空间开始位置记为avail,将整个分配的空间的末尾记为limit。理解了这个原理,grow函数的工作原理就很简单了,另外max (2*(limit-data),ptrdiff_t(1))是为了避免原空间大小为0时带来的隐患,让新分配的空间大小至少为1,而不可能为0。
===========================================================================================================
快速入口:
【第四章—第七章】【初识C++ Accelerated C++ 学习笔记】
【第0章—第三章】【初识C++ Accelerated C++ 学习笔记】