《Essential C++》阅读笔记
《Essential C++》阅读笔记
第一章 C++编程基础
1.1 如何撰写C++程序 class的定义一般分为两部分,分别写在不同的文件中: 1 头文件中,用来说明该class所提供的各种操作 2 程序文件中,包含了这些操作行为的实现内容 一般,所有内置数据类型都可以用同样的方式来输出 cout << cout的使用可以将数据内容连成单一输出: cout << "hello" << "world\n"; 对 using namespace std 做的解释:// 使用 std 命名空间 命名空间:是一种将库名称封装起来的方法,通过这种方法可以避免应用程序发生命名冲突的问题 1.2 对象的定义与初始化 定于对象与初始化的方式: int num1 = 0, num2 = 1; int num1(0);//构造函数语法 构造函数语法可以很好的解决初始化时需要多个参数的问题: #include <complex> complex<double> purei(0, 7); 进一步解释:尖括号表示complex class是一个template class(模板类),尖括号中的内容用说明传输的数据类型都需要以浮点数来表示//类似于java中的泛型 1.4 条件语句和循环语句 switch (/* 表达式的值必须是 !整数形式! */) { case 1: /* what you can do */ break; case 2: case 3: /**/ break; default: break; } 串长度: str.size() 1.5 如何用于Array和Vector array的定义,必须指定其尺度大小--array的大小必须是个常量表达式 使用vector://vector是个class template,必须在类名之后的尖括号内指定其元素类型 #include <vector> const int seq = 18; vector<int> pell_seq(seq_size); array和vector都可以通过索引来获取元素 pell_seq[1]; c++中的for循环内是可以定义并初始化变量的: for (int i = 0; i < 10; ++i) {/* anything */} array的初始化,可以不指定数组大小(编译器自动计算),如果给定了初始大小,但初始化值的个数不够,则其余都初始化为0 int elem_num[cnt/*可有可无*/] = { 1, 2, 3, 4, 5, 6, 7, 8, }; vector的初始化则不可以像array这样,但可以利用一个已初始化的array来给vector赋初值: vector<int> elem_seq(elem_num, elem_num + cnt);//这两个值是实际内存位置,标示出“用以将vector初始化”的元素范围 获取vector的大小: elem_seq.size(); 1.6 指针带来弹性 // 同c一样 *p是取值 &val是获取val的地址 int *p = &val; //*p 叫做提领 指针具有双重性质: 既可以让我们操作指针包含的内存地址,也可以让我们操作指针所指的对象值 如果p不指向任何对象(null指针),则提领p会导致未知的执行结果,故在使用指针时,必须在提领之前先确定它的确指向某对象 初始化指针,不指向任何对象: int *p = 0; 防止对null指针进行提领操作: if (p && *p != 1024) { *p = 1024; } 指针具有的一般形式: type_of_object_pointed_to *name_of_pointer_object 例子://指针数组 vector<int> *seq_addrs[seq_cnt] = {/* seq_addrs是一个数组,存放的数据类型是指针 */ &fibonacci, &lucas, &pell, } vector<int> fibonacci(1); fibonacci[0] = 1; if (!fibonacci.empty() && fibonacci[0] != 1) { fibonacci[0] = 1; }//fibonacci与empty()之间使用 . 成员选择运算符 vector<int> *pf = &fibonacci; if (pf && !pf->empty() && (*pf)[0] != 1) { (*pf)[0] = 1; }//pf 与empty()是通过指针来选择操作 -> 成员选择运算符 1.7 文件的读写 需要的头文件: #include <fstream> string file_name("file_name"); 写文件 ofstream outfile(file_name); if (!outfile) { /* 打开文件失败该如何处理 */ cerr << "open file failed!\n"; /* cerr代表标准错误设备,和cout一样,cerr将其输出结果定向到用户终端 两者的区别:cerr的输出结果并无缓冲(bufferred)情形--它会立即显示于用户终端中 */ } outfile << "helloworld" << endl; // 写内容到文件 endl是事先定义好的操作符,由iostream lib提供,操作符并不会将数据写入到iostream,也不会从中读取数据,其作用是在iostream上执行某些操作, endl会插入一个换行符,并清除输出缓冲区的内容,还有hex,oct,setpalette(n)分别用作16进制,8进制,设定浮点数显示精度为n 以上打开的文件,写入得内容会覆盖掉之前的内容 想要以追加的形式打开使用: ofstream outfile(file_name, ios_base::app); 读文件 ifstream infile(file_name); /* 判断文件是否打开成功是必要的 */ string in_str; infile >> in_str; // 这样可以直接读取指针指向的内容 /* 实验得到结论:当文件内的内容含有空白值的时候会默认读到它终止 比如文件内容为 helloworld 123 456 infile >> str; str接收到的内容是helloworld */ string file_content while (infile >> file_content) {// 每次迭代都会读取文件的下一行内容,这样的操作会持续到文件末才结束 /* 执行的操作 */ } 既读又写 fstream iofile(file_name); if (!iofile) { cerr << "open file failed!\n"; eixt(1); } 如果要以追加的形式打开则需要加参数: fstream iofile(file_name, ios_base::in|ios_base::app); 这样打开文档,文件位置会位于末尾,可以直接追加 如果想从文件头开始读取内容可以使用 iofile.seekg(0);//将指针移动到距离文件开始offset为0的位置
第二章 面向过程的编程风格
2.1 如何编写函数 函数必须先声明再使用,声明函数时不需要函数体(所谓的函数原型) if (post <= 0) { exit(-1);// 使用它必须包含 cstdlib 但实验中没包含也可以 } 函数如何可以间接的返回多个值: 采用多个参数的形式 bool fibon_elem(int pos, int &elem) {} 使用某类型的最大值/最小值: #include <limits> int max_int = numeric_limits<int>::max(); double min_dbl = numeric_limits<double>::min(); 函数中的每条return语句都被用来明确表示该处就是函数的退出点,如果函数体的最后一条语句不是return,那么最后一条语句之后便是该函数的隐式退出点 2.2 调用函数 两种参数传递方式:传值、传址 传值: void display(vector<int> vec) {} void swap(int val_1, int val_2) {} 传址: void display(vector<int> &vec) {} void swap(int &val_1, int &val_2) {} void swap(int *valp_1, int *valp_2) {}// 以指针的形式直接将参数的地址传入 当我们调用一个函数时,会在内存中建立起一块特殊区域,称为“程序堆栈”,这块特殊区域提供了每个函数参数的存储空间 它也提供了函数所定义的每个对象的内存空间--这些对象称为局部对象 reference://反射 别名 引用 referenc扮演着外界与对象之间一个间接手柄的角色,只要在类型名称和reference名称之间插入&符号就声明了一个reference int ival = 1024; int *pi = &ival; int &rval = ival;//这里没有任何取址和指针操作,就是直接将变量的值赋予它的引用 注意: 在C++中不允许我们改变reference所代表的对象,它们必须从一而终 这与指针还是有很大区别的,一个变量指针是可以接受不同的地址的 如何熟练的使用引用,记住一句话:你想让它起到什么作用的时候,它就起到什么作用,并且没有显示的指针与取址操作: 例子: pi = &rval;//此时rval起到代替ival这个变量的作用 void swap(int &val_1, int &val_2) {//此时这个reference表示对象作为函数参数传入时,对象本身不会复制出另一份----复制的是对象的地址 int temp = val_1; val_1 = val_2; val_2 = temp; } int arr[2] = {1, 2, }; swap(arr[0], arr[1]); 将参数声明为reference的理由: 1 希望直接对所传入的对象进行修改 2 降低复制大型对象的额外负担 复习到C的知识: void foo(const vector<int> *p_vec){}//const在前表示p_vec执行的对象是read-only的 但是这个指针是可以再次被赋值的 void foo(vector<int> * const p_vec){}//const在后面直接修饰p_vec,表示这个指针是read-only的,但是这个指针所指向的对象的内容是可以被修改的 对于引用来说,const便只能放在前面,放在后面会直接编译出错,这样就表示这个对象是read-only的 pointer和reference参数之间的差异: pointer可能(也可能不)指向某个实际对象,提领pointer时,一定要先确定其值非0 至于reference,则必定会代表某个对象,所以不需要做此检查 作用域及范围: 为对象分配的内存,其存活时间称为储存期或范围 对象在程序内的存活区域称为该对象的scope(作用域): 分为 local scope file scope:对象在函数意外声明,从其声明点至文件末尾都是可见的,对象具有static extent 指的是该对象的内存在main()开始执行之前便已经分配好了,可以一直存在至程序结束 内置类型的对象,如果定义在file scope之内,必定被初始化为0,如果被定义与local scope之内,程序员不指定初始值则不会被初始化 注意:local scope和file scope都是由系统自动管理的 动态内存管理: 动态范围:其内存由程序的空闲空间分配而来(堆内存) 这种内存由程序员自行管理,分配使用new 释放使用delete 可以自由分类内存的对象除了任意内置类型,也可以是程序知道的class类型 int *pi; pi = new int();//new 返回的是一个地址,所以需要使用同类型的指针来接收 由堆分配而来的对象,都未经过初始化,也可以使用构造器形式进行初始化: int *pi = new int(1024); 分配数组: int *parr = new int[24];//parr指向数组第一个元素的地址 注:C++没有任何语法可以从堆分配数组的同时为其元素设定初值 释放内存: delete pi; delete []parr; 释放之后需要将指针置0?? 特别得: 在delete之前不需要检测指针是否为0,编译器会替我们进行检测 new之后,在使用完这个对象后一定要记得delete来释放内存 不然对象就会永远不会被释放,造成内存泄漏 2.3 提供默认参数值 C++允许我们为全部或部分参数提供默认值: void bubble_sort(vector<int> &vec, ofstream *ofil = 0) { /**/ } 在设置默认值的时候要将反射换为指针,这样才可以为它设置默认值0,反射不同于指针,它要有明确的代表对象,无法设置默认值0 在函数调用时,使用单一参数则会使用第二参数的默认值,带上第二参数则会将默认值覆盖: bubble_sort(vec); ofstream ofil("data.txt"); bubble_sort(vec, &ofil); 查阅资料得:ofstream继承于ostream类 参数默认值的提供有两个规则: 1 默认值的解析操作由右边开始进行,如果为某个参数提供了默认值,那么这一参数右侧的所有参数都必须也具有默认值才行 2 默认值只能指定一次,可以在函数声明处,也可以在函数定义处,通常为了使函数多可用,在声明处进行默认值的设定 2.4 使用局部静态对象 在函数内使用static修饰的对象//内存中存储在静态存储区,在此存储区内的还有全局变量 向vector内添加一个元素使用的方法是push_back()//将元素添加到vector的末端 注:vector class提供有内存自动管理机制 2.5 声明inline函数 将一个函数拆分成多个,在实现功能上在将其顺序调用,这样虽然设计上更简洁,但是可能会影响其执行效率: 我自己猜的理由:每次函数的调用都会伴随着内存上的压栈与出栈,耗时 将函数声明为inline,表示要求编译器在每个函数调用点上,将函数的内存展开 面对一个inline函数,编译器可将该函数的调用操作改为以一份函数代码副本代替,这将获得性能改善,结果等同于把三个函数写入一个大的函数内,但依然维持三个独立的运算单元 实现上,就是在函数前面加上关键字 inline: 例子: inline bool fibon_elem(int pos, int &elem){ /*具体实现*/ } 注:将函数指定为inline,只是对编译器提出一种要求,编译器是否执行这项请求,需视编译器而定 适合声明为inline函数的一般都是 体积小、常被调用、从事的计算并不复杂的函数 重要提示:inline函数的定义常常被放在头文件中,由于编译器必须在它被调用的时候加以展开,所以这个时候其定义北徐是有效的 2.6 提供重载函数 参数列表不相同(可能是参数类型不同,可能是参数个数不同)的两个或多个函数,可以拥有相同的函数名称: void foo(char ch); void foo(const string &str); 疑问:void foo(int num); 与 void foo(int &num); 是重载么?????????? 注意:编译器是根据调用者提供的实际参数拿来和每个重载函数的参数比对,找出其中最合适的,进行函数的调用 编译器无法根据根据函数返回值类型来区别两个具备相同明名称的函数 2.7 定义并使用模板函数 //java中的泛型 将单一函数的内容与希望处理的各种类型绑定起来,使用的方式是函数模板(function template) 函数模板将参数列表中指定的全部(或部分)参数的类型信息抽离了出来 函数模板以关键字template开场,其后紧接着以成对尖括号(< >)包围起来的一个或多个标识符 template <typename Type> void display_message(const string &msg, const vector<Type> &vec) { cout << msg; for (int i = 0; i < vec.size(); ++i) { Type t = vec[i]; cout << t << ' '; } } 注:多个标识符的时候使用逗号分隔: template <typename T, typename F, class Cls> void my_foo(T t, F f, Cls c) { } 函数模板的参数列表通常都由两种类型构成,一类是明确的类型,另一类是暂缓决定的类型 在使用的时候,编译器会根据实际传入的确切的参数将标识符与具体的类型进行绑定,然后产生一份函数实例函数 标识符可以绑定为内置类型或用户自定义类型 2.8 函数指针带来更大的弹性 如何定义函数指针:保留函数的返回值与参数列表中的参数类型,并把函数名用 (*foo_ptr) 来代替 例子:功能函数 const vector<int> *get_vec(int size); 函数指针 const vector<int> *(*foo_ptr)(int) 具体的一个列子: bool seq_elem(int pos, int &elem, const vector<int> *(*foo_ptr)(int)) { const vector<int> *vec_ptr = foo_ptr(pos); if (!vec_ptr) { elem = 0; return false; } elem = (*vec_ptr)[pos - 1]; return true; } 注意:函数指针也可能是0的时候,所有还是要做好处理0值的准备 获取某个函数的地址:函数名便可以提供此函数的地址 定义函数指针数组: const vector<int> *(*seq_array[]) (int) = { foo1, foo2, foo3, }; 解释:首先,函数返回值类型不变 其次,函数参数列表类型不变 *seq_array[]中seq_arrray与[]先结合,说明seq_array是一个数组,最后前面那个*表示的是函数指针 所以seq_array表示的就是函数指针数组 枚举: enum my_enum {//my_enum 是个可有可无的标识符 ns_foo1, ns_foo2,//枚举中默认值从0 开始 }; //在使用的时候直接枚举的成员就可以 seq_ptr = seq_array[ns_foo1]; 2.9 设定头文件 明确一个问题: 对象的定义与声明:定义是从无到有,声明只是告诉你有这个对象 自己定义的头文件以 .h 结尾 在包含的时候使用 #include “my_test_h.h” 包含标准库的时候没有扩展名,并使用 #include <iostream> 解释: 如果此文件被认定为标准的或项目专属的头文件,便以尖括号将文件名括住,编译器搜索此文件时,会先在某些默认的磁盘目录中寻找 如果文件由成对的双括号括住,此文件便被认为是一个用户提供的头文件,搜索此文件时,会由要包含此文件的文件所在的磁盘目录开始找起 对于非const对象的声明与定义一样 const double pi = 3.14; const与inline函数一样,是“一次定义”规则下的例外,const object的定义只要一出文件之外便不可见 这意味着我们可以在多个程序代码文件中加以定义,不会导致任何错误 对于非const对象的声明要加上 extern 关键: const int seq_cnt = 6; extern const vector<int> *(*seq_array[seq_cnt])(int);//在file scope内定义对象,如果可能被多个文件访问,就应该被声明于头文件中
第三章 泛型编程风格
3.1 指针的算数运算 STL主要由两部分组件构成:一是容器:vector、list、set、map 二是泛型操作算法:find()、sort()、replace()、merge()等等 vector和list是顺序容器,内部的元素会维护第一、第二...直到最后的顺序 在顺序容器上主要进行迭代操作 set和map是关联容器 关联容器可以进行快速的元素查找 泛型算法在操作容器的时候是与元素类型无关的 array、与vector中的元素在内存中都是一块连续内存存储元素 array作为参数传入的时候,不是值传递,而是将数组的第一个元素的地址传入 int min(int array[]){} int min(int *array){} 在处理数组的时候,array没有像vector的内置函数可以直接获得大小,因此可以增加一个参数来表示数组的大小 int min(const int *array, int size){} 指针的运算:ptr + 1 //是指针向后移动,表示在内存上 地址向后移动一个类型的大小 比如64位机器上 int * 通常会向后移动4个字节 vector可以是空的:vector<string> vec;//定义了一个vector现在为空,没有元素 array不会存在空的情况 所以在处理vector的时候需要注意判空处理 使用 vec.empty() //如果vector为空则 vec[0]就是出现产生运行时异常(无法提领) list的元素以一组指针相互链接,向前和向后,//双向链表 在内存上的表现是可能使用非连续的内存区域 3.2 了解Iterator(泛型指针) //当作指针一样来使用 每个容器都提供一个 begin() 函数,可以返回一个iterator,指向第一个元素。还有一个 end() 函数返回一个iterator,指向最后一个元素的下一个位置 vector<string>::iterator iter = svec.begin();//iterator的定义方式 for (; iter != svec.end(); ++iter) { /* 遍历逻辑内的操作 */ } 注:其中 :: 表示此iterator乃是位于string vector定义内的嵌套类型 对于const vector: const vector<string> cs_vec; 使用const_iterator来进行遍历: vector<string>::const_iterator iter = cs_vec.begin(); 强调:iterator看作是指针,故获取当前位置上的元素使用 提领 *iter,如果要调用它指向的对象所提供的方法就使用箭头 iter->size() 3.3 所有容器(包含string类)的共通操作 判断两个容器是否相等/不相等 == 和 != 运算符,返回true或false 复制,将一个容器复制给另一个容器 = 判空,判断一个容器是否含有元素 empty(),容器不含有元素的时候返回true 容量,返回当前容器含有元素个数 size() 删除所有元素 clear() 例子: void comp(vector<int> &v1, vector<int> &v2) { // 两个vector是否相等 if (v1 == v2) { return; } // 两个vector之中是否有一个为空 if (v1.empty() || v2.empty()) { return; } vector<int> t; t = v1.size() > v2.size() ? v1 : v2; t.clear(); } 每个容器都提供了begin()很end()两个函数,分别返回指向容器的第一个元素和最后一个元素的一个位置的iterator 所有容器还都提供 insert()将单一或某个范围内的元素插入容器内 erase()将容器内的单一元素或某个范围内的元素删除 3.4 使用顺序性容器 顺序性容器用来维护一组排列有序、类型相同的元素 list(底层存储结构是双向链表 内存不连续 每个元素都包含value、back指针--指向前一个元素、front指针--指向下一个元素) vector(底层存储结构是数组 内存连续) deque(双端队列 底层存储结构与vector类似 也是连续的内存) vector在随机访问上效率高 list在插入和删除元素上效率更高 deque对于最前端元素的插入和删除效率更高,末端亦同 定义顺序性容器对象的方式有五种: 1 产生空容器 list<string> slist; vector<int> ilist; 2 产生特定大小的容器,初始值为默认初值 list<string> slist(32); vector<int> ilist(1024); 3 产生特定大小的容器,并为所有元素指定同一初值 vector<int> ilist(10, -1); list<string> slist(16, "hello world"); 4 通过一对iterator产生容器,用来标示一整组作为初值的元素的范围 int ia[8] {1, 2, 3, 4, 5, 6, 7, 8, }; vector<int> ilist(ia, ia + 8); 5 根据某个容器产生出新的容器,复制原容器内的元素作为新容器的初值 list<string> slist1; list<string> slist2(slist1); 对容器末尾进行插入和删除操作的两个函数: push_back() pop_back() list和deque(没有vector)还提供了push_front() 和 pop_front() 在头部进行插入和删除元素 pop_back() 与 pop_front() 函数不会返回被删除的元素值 读取最前端元素的值使用 front() 末端使用 back() 每个容器都拥有insert() 函数 并且支持四种变形: 1 iterator insert(iterator position, elemType value) 将value插入到position之前(注意:这个之前的由来是 []->[]->[]->[]->[] 这是前) 返回一个iterator 指向被插入的元素 2 void insert(iterator position, int count, elemType value) 在position之前插入count个元素,并且这些元素的值都是value 3 void insert(iterator1 position, iterator2 first, iterator2 last) 在position之前插入[first, last)所标示的各个元素 例子: int ia1[7] = {1, 1, 2, 3, 5, 55, 89}; int ia2[4] = {8, 13, 21, 34}; list<int> elems(ia1, ia1 + 7); list<int>::iterator it = find(elems.begin(), elems.end(), 55); elems.insert(it, ia2, ia2 + 4); 4 iterator insert(iterator position) 在position之前插入一个元素,元素初始值是其所属类型的默认值 每个容器都拥有erase() 函数 并且支持两种变形: 1 iterator erase(iterator posit) 删除posit所指的元素 例子: list<string>::iterator it find(slist.begin(), slist.end(), str); slist.erase(it); 2 iterator erase(iterator first, iterator last) 删除[first, last)范围内的元素 注:两个删除函数都返回最后删除的元素的下一个位置 注意:list不支持iterator的偏移运算 我们不能写成:slist.erase(it1, it1 + num_tries); 3.5 使用泛型算法 使用泛型算法,首先重要的一步:包含对一个的algorithm头文件 #include <algorithm>; 1 find(iterator first, iterator last, elemType value)用于无序集合中搜索是否存在某值 搜索范围由iterator[first,last)标出 如果找到目标,find会返回一个iterator指向该值 否则返回一个iterator指向last 2 binary_search(iterator first, iterator last, elemType value) 用于有序集合搜索 如果找到返回true 否则返回false 3 count() 返回数值相符的元素数目 4 search() 对比某个容器内是否存在某个子序列 例如在{1, 3, 5, 7, 2, 9, }中搜索{5, 7, 2, } 如果找到会返回一个iterator指向子序列起始处,否则返回一个iterator指向容器末尾 sort(iterator first, iterator last)可以对容器内元素进行排序: sort(ilist.begin, ilist.end); copy()函数 接受两个iterator 标示出复制范围 第三个iterator指向复制行为的目的地: vector<int> temp(vec.size()); copy(vec.begin(), vec.end(), temp.begin()); 3.6 如何设计一个泛型算法 补充:const_iterator 自身的值是可以被修改的 但是它指向的元素值是不可以被修改的 常用于 const 容器 Function Object(函数对象):是某种class的实例对象,这类class对function call运算符做了重载操作,如此一来可使function object被当成一般函数来使用(不太理解!!) 优势:可以提升效率 令call运算符成为inline,消除“通过函数指针来调用函数”时需付出的额外代价 标准库中事先定义了一组 function object://使用时 type 替换成内置类型或class类型 1 六个算数运算 plus<type>(+) minus<type>(-) negate<type>(取反) multiplies<type>(*) divides<type>(/) modules<type>(%) 2 六个关系运算 less<type>(<) less_equal<type>(<=) greater<type>(>) greater_equal<type>(>=) equal_to<type>(=) not_equal_to<type>(!=) 3 三个逻辑运算 logical_and<type>(&&) logical_or<type>(||) logical_not<type>(!) }//格式需求 使用function object 需要事先包含头文件: #include <functional> 例子: binary_search(vec.begin(), vec.end(), greater<int>()); Function Object Adapter(函数对象适配器):对函数对象进行再次适配满足环境需求 binder adapter(绑定适配器):会将function object 的参数绑定至某特定值,使二元函数对象转化为一元: bind1st:将指定值绑定至第一操作数 bind2nd:将指定值绑定至第二操作数 例子: find_if(iter, vec.end(), bind2nd(/*函数对象*/, /*要绑定的值*/)); 3.7 使用Map map对象有一个名为 first 的成员 和 一个名为 second 的成员(first代表key second代表value) 输入key/value的最简单方式: map<string, int> map1; map1["hello"] = 1; 例子://统计输入的单词频率 string tword; map<string, int> words; while (cin >> tword) { words[tword]++; } 注:words[tword] 会取出以 tword为key的value 如果map内不含有以tword为key的pair 则会将其加入到map并设定value为默认值 查询map是否存在某个key的三种方法: 1 直接暴力 words["hello"] 通过取出的值来判断是否含有此key 存在缺点:如果原map不含有这个key 则会将其加入map 设定value为默认值 2 使用map类的find()函数 int count = 0; map<string, int>::iterator it; it = words.find("hello"); if (it != words.end()) { count = it->second; } 注:find() 找到会发返回一个iterator 指定含有这个key的pair 找不到会返回end() 3 使用map类的count()函数 //返回特定项在map内的个数 if (words.count("hello")) { cout << words["hello"]; } 注:任何一个key在map内最多只会有一份 如果需要存储多份相同的key 使用 multimap 3.8 使用Set set中插入元素: set<int> iset; iset.insert(1);//插入一个元素 int arr[5] = {1, 2, 3, 4, 5, }; iset.insert(arr, arr + 5);//插入多个元素 对于任何key值,set只能储存一份 要存储多份相同的key值 必须使用 multiset 重点:默认情况下,set元素都是依据其所属类型默认值的 less-than 运算符进行排列 (这里没有像java一样 使用的是hash ) 判断是否含有key:使用set对象方法 count() iset.count(1);//有就是1 没有就是0 泛型算法中与set相关的算法: set_intersection() set_union() set_difference() set_symmetric_difference() 3.9 如何使用 Iterator Inserter #include <iterator> 注意:所有”会对元素进行 复制 行为“的泛型算法,例如 copy() copy_backwards() remove_copy() replace_copy() unique_copy() 等等 都是接受一个iterator,标示出复制的起始位置 每复制一个元素 都会被 !赋值! iterator 则会递增至下一个 所以,得提前确定目的容器的大小 而且要足够大来接收复制过来的元素 三个insertion adapter://用它来避免赋值算法 1 back_inserter() 会以容器的 push_back() 函数取代 赋值运算符: vector<int> result_vec; unique_copy(ivec.begin(), ivec.end(), back_inserter(result_vec)); 2 inserter() 会以容器的 insert() 函数取代赋值运算符 接收两个参数:一个是容器 一个是iterator指向容器内的插入操作起点 vector<int> result_vec; unique_copy(ivec.begin(), ivec.end(), inserter(result_vec, result_vec.end())); 3 front_inserter() 会以容器的 push_front() 函数取代 赋值运算符 这样再copy的时候 不用预先分配大小 而是在复制的过程中来自动分配 3.10 使用 iostream Iterator #include <iterator> istream_iterator ostream_iterator 例子: 从标准输入中读取字符串://需要一对 iterator first和last 用来标示元素范围 istream_iterator<string> is(cin);//first istream_iterator<string> eof; //last 注:对于标准输入设备而言 end-of-file 代表last 只要在定义istream_iterator时不为它指定 istream对象 它便代表end-of-file copy(is, eof, back_inserter(/*vector*/ text)); 输出字符串到标准输出: ostream_iterator<string> os(cout, /*分隔符*/" "); copy(text.begin(), text.end(), os); //这样输出的字符串会以 上面的分隔符分隔 读文件与写文件: 将istream_iterator 绑定至 ifstream 对象 将ostream_iterator 绑定至 ofstream 对象 例子: ifstream in_file("input_file.txt"); ofstream out_file("out_file.txt"); if (!infile || !out_file) { cout << "unable tp open the necessary files.\n"; } istream_iterator<string> is(in_file);// 进行绑定 istream_iterator eof; ostream_iterator<string> os(out_file);// 进行绑定 /* 剩下所有操作 都是在 iostream_iterator 进行 */
第四章 基于对象的编程风格
一般而言,class由两部分组成:一组公开(public)操作函数和运算符 一组私有(private)实现细节 public表示这个类对外公开的接口 class用户只能访问公开接口 private实现细节可由成员函数的定义以及类相关的任何数据组成 4.1 如何实现一个 Class 类声明以关键字 class 开始 后面接一个类名称: class Stack;// 此句只是作为Stack这个类的前置声明 将class名称告诉编译器 并未提供此class的任何其他信息 class定义的骨干: class Stack { public: // public 接口 private: // private 的实现部分 }; //注意 此处有分号 注:public部分在程序的任何地方都可以被访问 private部分只能在成员函数或 class friend 内被访问 编码习惯:数据成员在前面加上 下划线 class Stack { public: bool push(const string&); bool pop(string &elem); bool peek(string &elem); bool empty(); bool full(); int size() {//默认是inline函数 return _stack.size(); } private: vector<string> _stack;//编码习惯 }; 所有的成员函数都必须在class主体内进行声明 如果在声明的同时进行定义 则这个成员函数会被自动视为 inline 函数 如果要在程序代码文件定义函数并将其指定为内联函数 需要加上 inline 关键字 在class主体之外定义成员函数 必须指定这个函数的scope(类作用域解析 属于哪个类) 比如: inline bool Stack::empty() {//指定scope return _stack.empty(); } 对于非成员的inline函数 它应该被放在头文件中 class定义及其 inline成员函数通常会被放在与class同名的头文件中 比如上面的 Stack.h 非inline成员函数应该在程序代码文件中定义 该文件通常和class同名 后缀名是 .c .cc .cpp cxx 4.2 什么是构造函数和析构函数(constructor / destructor) 构造函数: 当实例化一个类的时候 传入的成员参数需要被初始化 这些是需要程序员来完成的任务 在实例化一个类的时候 可以传入不同的参数列表满足自己的需求 比如: Fibonacci fib1(7, 3); Fibonacci fib2(10); Fibonacci fib3(fib1); 构造函数的写法(类java):没有返回值 函数名与类名相同 例子: class Triangular { public: // 函数的重载 overload Triangular();// 默认构造函数 Triangular(int len); Triangular(int len, int beg_pos); }; 类对象在定义出来后,编译器会自动根据或得的参数来选择相应的 constructor 来调用 默认 没有参数 Triangular t1; 一个参数 Triangular t2(8);//或者 Triangular t2 = 8; 二个参数 Triangular t3(10, 3); 特别注意:默认 不传参数的时候 不能写成 Triangular t(); // 这样会被认为是定义了一个函数名 t 并且它的返回值是 Triangular 对象 默认构造函数不接受任何参数 可以写成 Triangular::Triangular() { _length = 1; _beg_pos = 1; _next = 0; } 但更常见的写法是 class Triangular { public: Triangular(int len = 1, int bp = 1);//在类主体内指定参数的默认值 }; 这样 就可以满足以上三种情况 Triangular tri1; //Triangular(1, 1); Triangular tri2(12);//Triangular(12, 1); Triangular tri3(8, 3);//Triangular tri3(8, 3); 成员初始化列表: 构造函数定义的第二种初始化语法 Triangular::Triangular(const Triangular &rhs) : _length(rhs._length), _beg_pos(rhs._beg_pos), _next(rhs._next) {}// 函数体是空的 注:成员初始化列表紧接在 参数列表后的冒号后面 以逗号分隔 //后面的每一个参数的括号像是这个参数的构造函数 再一个例子: Triangular::Triangular(int len, int bp) : _name("Hello") { _length = len > 0 ? len : 1; _beg_pos = bp > 0 ? bp : 1; _next = _beg_pos - 1; } 对应与构造函数的是析构函数: 析构函数的写法是: 没有返回值 没有参数 名字是 类名前加 ~ 类对象结束生命时,需要调用析构函数来处理善后 析构函数的主要功能就是 释放构造函数中或生命周期中分配的资源 注意:析构函数不是类的必须的部分 如果数据成员都是储值的方式存放 则在对象结束生命时会自动释放 例子: class Matrix { public: Matrix(int row, int col) : _row(row), _col(col) { _pmat = new double[ row * col];//堆上申请的内存 需要手动释放 } ~Matrix() { delete [] _pmat;// 析构函数来释放内存 } private: int _row, _col; double* _pmat; }; 成员逐一初始化: 默认情况下,以类对象作为参数传递给构造函数的时候 数据成员会被依次复制 // 所谓的 浅拷贝 产生的问题: 类内数据成员是指向某个对象的指针 在默认复制的情况下 便将指针的值进行了复制 考虑一种情况: 初始化 类对象1 (含有一个成员指针 指向一个类对象) 以对象1定义对象2 并作为参数进行初始化 (默认逐一复制) 对象2使用完 自动调用了析构函数 释放那个对象所占的内存 再次使用对象1 // 此时成员的指针已经指向了被释放的内存 !!!出错 此时需要我们自己来完成一个 复制构造函数// 进行深拷贝 Matrix::Matrix (const Matrix& rhs) : _row(rhs._row), _col(rhs._col){ int elem_cnt = _row * _col; _pmat = new double[elem_cnt]; for (i = 0; i < elem_cnt; ++i) { _pmat[i] = rhs._pmat[i]; } } 注:如果有必要为某个class编写 复制构造函数 那么同样有必要为它编写 复制赋值操作( 4.8 ) 4.3 何谓 multi(可变)和const(不变) 一个例子: int sum (const Triangular &train) { /* ... */ } 这里面的 train 是个 const reference 参数 因此编译器必须保证train在sum() 之中不会被修改 但是sum()所调用的任何一个 成员函数 都有可能更改 trian的值 为了确保trian的值不被更改 编译器必须确保类内部的成员函数都不会更改其调用者:class设计者必须在成员函数身上标注 const 例子: class Triangular { public: int length() const {// const 修饰符仅接于函数参数列表之后 return _length; } private: int _length; }; 注:凡是在class主体以外定义函数 如果它是一个 const成员函数 那必须同时在声明与定义中指定 const 又一个例子: class val_class { public: val_class(const BigClass &v) : _val(v) {} // 这里是不正确的 BigClass& val const () {// 在这里返回了一个 非 const 的 reference指向_val _val便被开放出去 有可能被其他地方修改 return _val; } private: BigClass _val; } 解决的办法:成员函数可以根据 const与否而重载 因此可设计两个函数 class val_class { public: const BigClass& val() const { return _val; } BigClass& val() { return _val; } }; 注意:没有一个const reference class 参数可以调用公开接口中的 non-const 成分 可变的数据成员: 列子: class Triangular { public: bool next(int &val) const; void next_reset() const { _next = _beg_pos - 1; } private: mutable int _next;// _next的值的改变并不会破坏对象的常量性 在const reference class作为参数的时候 // 使用 mutable 来修饰成员数据 可以做到修改成员数据但不破坏对象const性 int _beg_pos; int _length; } 4.4 什么是this指针//跟java一样 指向本类对象 一个例子说明问题: Triangular& Triangular::copy(const Triangular &rhs) { _length = rhs._length; return *this; } 编译器自动将this 指针添加到了每一个 成员函数的参数列表 于是copy被转换成: Triangular& Triangular::copy(Triangular *this, const Triangular &rhs) { this->_length = rhs._length; return *this; } 4.5 静态 类成员 static(静态)数据成员用来表示唯一的、可共享的成员 它可以在同一类的所有对象中被访问 class Triangular { public: //.. private: static vector<int> _elems; // 只用唯一一份实体 } 使用的时候要附上 class scope 运算符: vector<int> Triangular::elems; 静态成员函数:在函数前面加修饰词 static 表示这个函数是类函数 不被实例对象所有 class Triangular { public: static bool is_elem(int);// 只是声明 在外部进行明确定义 带有函数体 }; 注:在class主体外部进行 成员函数 的定义时,无需重复添加关键字 static (这个规则也适用于静态数据成员) 4.6 打造一个 Iterator Class 本节任务:了解如何对class斤进行运算符重载操作 针对class 定义 != * ++ 等运算符 定义运算符跟定义成员函数很像,运算符函数看起来就像是普通函数 唯一差别是它不指定名称 只需在运算符前加关键字 operator 例子: class Triangular_iterator { public: bool operator==(const Triangular_iterator&) const;//这个等号 可以用于比较 此类的两个对象是否相同 bool operator!=(const Triangular_iterator&) const; int operator*() const;// 提领 Triangular_iterator& operator++();// 前置 ++it Triangular_iterator operator++(int);// 后置 it++ }; 运算符重载的规则: 1 不可以引入新的运算符 除了 . .* :: ?: 这个四个运算符,其他的运算符都可以被重载 2 运算符的操作数 个数不可改变 二元运算符都需要两个操作数 一元运算符必须一个操作数 3 运算符的优先级不可以改变 例如除法优于加法 4 运算符的参数列表中 必须至少有一个参数为class类型 运算符定义方式: //像成员函数一样 inline int Triangular_iterator::operator*() const { return Triangular::_elems[_index]; } // 像非成员函数一样 inline int operator*(const Triangular_iterator &rhs) { // 非成员函数没有访问对象的非public的权利 后面的friend来解决这个问题 return Triangular::_elems[_index]; } 非成员运算符的参数列表中,一定会比相应的成员运算符多一个参数 也就是 this指针 对成员运算符而言 这个this指针隐式代表左操作数(这句话不是很理解) 递增的前置与后置的定义: 前置 inline Triangular_iterator& Triangular_iterator::operator++() { ++_index; check_integerity(); return *this; } 后置 inline Triangular_iterator Triangular_iterator::operator++(int) { Triangular_iterator tmp = *this; ++_index; check_integerity(); return tmp; } 解释:对++操作符进行了重载,后置版本要比前置版本多个int类型的参数 编译器会自动为后置版产生一个int参数(值必为0) 嵌套类型:// typedef 存在的名字 新名字 其中存在的名字可以是 任何一个内置类型、复合类型(?)或class类型 class Triangular { public: typedef Triangular_iterator iterator;// 在类内声明 并给它定义了新的名字 可以让用户不必知晓 这个class的实际名称 Triangular_iterator begin() const { return Triangular_iterator(_beg_pos); } }; 4.7 合作关系必须建立在友谊(friend)的基础上 任何class都可以将其他函数或类指定为朋友(friend) friend便具备了与类成员函数相同的访问权限 可以访问类的私有成员 例子: class Triangular { friend int operator*(const Triangular_iterator &rhs);// 在这声明为friend }; class Triangular_iterator { friend int operator*(const Triangular_iterator &rhs); }; 注:这样的声明可以出现在class定义的任何位置上 不受 private 或 public 的影响 注意:这个操作重载声明 不是成员函数 如果希望将数个重载函数都声明为某个class的friend 必须明确为每个函数都加上关键字 friend 一个类成员函数访问另一个类内的私有成员: class Triangular { //目的:让Triangular_iterator内的成员操作符或函数能访问 Triangular内的私有成员 friend int Triangular_iterator::operator*(); friend void Triangular_iterator::check_integerity(); }; 注:必须在Triangular之前提供Triangular_iterator类的定义让 Triangular 知道 让 class A 内的所有成员函数都能访问 class B 的成员: class B { friend class A; }; class A { /*...*/ }; 注:这样的方式来声明class间的友谊,不需要再友谊声明之前显现class的定义 (这里的A) 4.8 实现一个 copy assignment operator // = Triangular tri1(8), tri2(8, 9); tri1 = tri2;// 这里的赋值操作 含有深拷贝的话 或 有自己需求 需要重载 = 直接上例子: Matrix& Matrix:: operator=(const Matrix &rhs) { if (this != &rhs) { _row = rhs._row; _col = rhs._col; int elem_cnt = _row * _col; delete [] _pmat;// 释放之前的内存 _pmat = new double[elem_cnt];//重新申请复制过来的大小的内存 for (int i = 0; i < elem_cnt; ++i) { _pmat[i] = rhs._pmat[i]; } } return *this; //这里还没有异常处理 } 4.9 实现一个 function object //函数对象 所谓function object 就是一种 提供“function call 运算符”的class //operator() 如果 lt(ival); 是一个提供了函数调用运算符的函数对象 it是一个 class object 编译器便会再内部将此句转换为 lt.operator(ival); 列子: class LessThan { public: LessThan(int val):_val(val){} int comp_val() const { // getter return _val; } void comp_val(int nval) { // setter _val = nval; } bool operator()(int _value) const;// 函数调用需要的参数 _value 与 _val 进行比较 private: int _val; }; inline bool LessThan::operator()(int value) const { return value < _val; } 4.10 重载iostream运算符 // 有点类似于java的tostring()方法 class A { /* ...*/ }; 为了实现 cout << A << endl; 需要在A内实现iostream运算符的重载 ostream& operator<<(ostream &os, const Triangular &rhs) { os << "(" << rhs.beg_pos() << "," << rhs.length() << ")"; return os; } 注意:这是非成员函数 如果要将其设置为类的成员函数的话 左操作数必须是隶属于同一个class的对象 对于 istream 的重载类似 4.11 指针, 指向 Class Member Function 例子: void (num_sequence::*pm)(int) = 0; 解释: 1 定义了一个指针 这是一个num_sequence成员函数指针 2 指针指向的函数 的返回值 是 void 3 指针指向的函数 的参数列表 有一个参数 是int类型 4 定义的这个指针的初值 是 0 可以加入typedef来进行简化: typedef void (num_sequence::*PtrType)(int); PtrType pm = 0; 一点注意的地方: vector<vector<string> > seq;//定义了一个vector 是 seq 内部存的类型是 vector 并且这个容器存的是string //注意这里的空格 maximal munch 编程规则:每个符号序列总是以“合法符号序列”中最长的那个解释 >> 是合法的运算符序列 特别:a+++p 是 a++ + p 值是 a + p 指向成员函数的指针 与 指向函数的指针 的区别: 前者必须通过同一类的对象加以调用 而该对象便是此 成员函数 内的 this 指针所指之物 num_sequnence ns; num_sequnence *pns = &ns; PtrTpye pm = &num_sequnence::fibonacci; // 通过ns调用_pmf (ns.*pm)(pos);// .* 是 pointer to member selection运算符 针对 class object 工作 // 通过pns调用_pmf (pns->*pm)(pos);// ->* 是 pointer to member selection运算符 针对 pointer to class object 工作
第五章 面向对象编程风格
5.1 面向对象编程概念 老生常谈:继承 多态 基类 派生类 父类定义了所有子类共通的公有接口和私有实现 每个子类都可以总价或覆盖继承而来的东西 以实现其自身独特的行为 多态:让基类的 pointer 或 reference 得以十分透明地指向任何一个派生类的对象 动态绑定 // 面向对象编程风格的第三独特概念 由于程序执行之前就已解析出应该调用哪一个函数 这样的方式 是静态绑定 动态绑定指的是 通过 基类的 pointer 或 reference 来操控共通接口的时候 实际执行起来的操作需要等到运行时 5.2 漫游:面向对象编程思维 默认情况下,成员函数的解析皆在编译时静态进行 虚函数: 采用关键字 virtual 来修饰的函数 这样才可以在运行时动态进行绑定 例子: class LibMat { public: LibMat() {} virtual ~LibMat(){} // 这里进行了定义 virtual void print();// 这是声明 需要给出定义 }; 继承过程中 在实例化一个类的对象的时候 首先执行基类的构造函数 再执行本类的构造函数 派生类被销毁的时候析构函数的调用与构造函数相反 继承的写法:// 定义子类后面 跟 : 再跟基类 例子: class Son : public Father { // 此处的基类的访问权限 可以是 public protected private /* ...*/ } 老生常谈:访问权限 public:公有 protected:继承 private:私有 记住:要想动态绑定 必须要提供 虚函数 virtual 5.3 不带继承的多态 这里提到了枚举 enum ns_type { ns_unset, ns_fibonacci, ns_pell, ns_lucass }; static_cast<ns_type>(num);// static_cast是一个特殊转换记号 可将num转换为对应的 ns_type 枚举项 5.4 定义一个抽象基类 如何才能很好的定义一个抽象基类呢: 1 找出所有子类共通的操作行为 2 找出哪些操作行为与类型相关 找到有哪些操作行为必须根据不同的派生类而有不同的实现方式 这些行为应该被设计为 虚函数 3 找出每个操作行为的访问层级 如果某个操作行为应该让一般程序都能访问 则应该设计为 public “纯”虚函数: 定义虚函数的时候为其指定初始值为 0 例子: class num_sequence { public: virtual ~num_sequence() {}// 虚 析构函数 virtual int elem(int pos) const = 0;//纯虚函数 没有函数定义 是不完整的 }; 抽象类://不能被实例化 没有具体的对象 类比java 任何类如果声明有一个(或多个)纯虚函数,那么由于其接口的不完整性 程序无法为它产生任何对象 这种类只能作为派生类的子对象(subobject)使用 前提是这些派生类必须为所有虚函数提供确切的定义 记住:含有纯虚函数的类是抽象类 不能被实例化 抽象类的设计中 由于没有非类数据成员需要进行初始化操作 所以构造函数没有存在的意义 一般规则 凡是基类定义有虚函数 应该将其析构函数声明为virtual // 这样做的理由是 为了能在对象销毁时 动态绑定 在运行时来调用本类的析构函数 不建议将析构函数声明为 纯虚函数---虽然它不具有任何实质意义的实现内容 对于这类析构函数 最好提供空白定义: inline num_sequence::~num_sequence() {} 自己的理解: virtual 的设计目的是为了让基类成为抽象类 并且为派生来提供函数原型 等待派生类来具体实现 static 表示的是类成员 不会被继承到子类去 这样便不会与virtual共用 5.5 定义一个派生类 派生类由两部分组成:一是积累构成的子对象 由基类的 非类数据成员组成 二是派生类的部分 例子: class Fibonacci : public num_sequence {// 类进行继承声明之前,其基类的定义必须已经存在 /* ...*/ }; 子类必须为从基类继承而来的每个存虚函数提供对应的实现 除此之外 还必须声明子类专属的 成员 派生类的虚函数必须吻合基类中的函数原型 包括 返回值 参数列表 const 在类之外对虚函数进行定义时,不必指明关键字 virtual 当我们知道 要明确调用那个函数 便可以通过 class scope 运算符 明确告诉编译器 我们想调用哪一份函数实例 这样 运行时发生的虚拟机制便被遮掩了 每当派生类有某个 成员 与基类的 成员 同名 便会遮掩住基类的那份成员: 派生类内对该名称的任何使用 都会被解析为该派生类自身的那份成员 而非继承来的那份成员 要使用继承来的成员 需要指定class scope 但是 如果要利用多态性质 通过基类的 pointer 或 reference 来调用派生类内这个同名函数 却会调用基类的 而不会调用指针指的那个派生类的同名函数 总结:派生类内与基类同名的成员 如果基类没有设定为 virtual 则不会覆盖 5.6 运用继承体系 多态的利用 虚函数的重载 5.7 基类应该多抽象 数据成员 如果是 reference 必须在 构造函数的 成员初始化列表中加以初始化 因为 reference永远无法代表空对象 一旦被初始化后 便再也无法指向另一个对象了 5.8 初始化、析构、复制 对于基类的成员数据 最好为基类提供 构造函数 来处理基类所声明的所有数据成员的初始化操作 派生类对象的初始化行为: 调用基类的构造函数初始化 基类子对象 派生类的构造函数初始化 派生类子对象 派生类的构造函数不仅必须为派生类的数据成员进行初始化 还必须要为其基类的数据成员提供需要的值: inline Fibonacci::Fibonacci(int len, int beg_pos) : num_sequence(len, beg_pos, _elems) {}// num_sequence是 Fibonaci的基类 如果将一个对象作为参数传入构造函数来进行复制操作 默认复制行为可以满足 则不需要重载 复制构造函数 不满足时 需要自己定义 复制构造器: Fibonacci::Fibonacci(const Fibonacci &rhs) : num_sequence(rhs) {} 注:rhs在成员初始化列表中被传递给基类的复制构造函数 如果基类没有定义复制构造函数 默认的成员初始化程序被执行 如果 基类定义类复制构造函数 则它会被调用 赋值运算符也是同样的道理:默认行为满足 就不需要任何从重载操作 不满足 就需要自己来重载= 5.9 在派生类中定义一个虚函数 如果派生类从基类继承了纯虚函数 并且没有给出定义将其覆盖 那么这个派生类也是一个抽象类 不能有实例化对象 如果派生类覆盖了基类所提供的虚函数 那么派生类提供的新定义 其函数原型必须完全符合基类所声明的函数原型 包括:参数列表 返回值类型 常量性 其中 返回值类型必须完全吻合 有个例外----当基类的虚函数返回某个基类形式(通常是pointer 或reference): class num_sequence { public: virtual num_sequence *clone() = 0; }; class Fibonacci : public num_sequence { public: // 注意:在派生类中,关键字virtual不是必要的 Fibonacci *clone() { // 由于 多态的缘故 返回值隶属于 num_sequence 这个基类的子类 return new Fibonacci(*this); } }; 注:在派生类 为了覆盖基类的某个虚函数 而进行声明操作时 不一定得加上关键字 virtual 虚函数的静态解析 以下两种情况,虚函数机制不会出现预期行为: 1 基类的 构造函数 和 析构函数 内 在基类的构造函数中 派生类的虚函数聚堆不会被调用:因为如果被调用的话 便是调用派生类内的 而此时操作派生类内的未初始化的数据成员会产生错误 2 当时使用的是基类的对象 而非基类对象的pointer或reference void print(LibMat object, const LibMat *pointer) { object.print();// 这里使用是基类的print()函数 因为才传参的时候 对象被整体复制作为参数 pointer->print();// 这里会很久实际传入的类对象进行动态绑定 具体调用了那个类的print() 得看传入的对象是什么 } 当 int main() { AudioBook iWish("Her Pride of 10", "Stanley Lippman", "Jeremy Irons"); print(iWish, &iWish, iWish); } 注:第一个参数 将iWish这个对象传入的时候 只会将iWish内的“基类子对象”(LibMat的成分)复制到“为参数object而保留的内存”中 其他的子对象(属于Book和AudioBook的成分)被切掉 另外两个传入的只是对象的内存地址 是完整的 重点:在C++中 唯有用基类的 pointer 和 reference 才能够 支持面向对象编程概念 5.10 运行时的类型鉴定机制(RTTI) #include <typeinfo> typeid 运算符: inline const char* num_sequence::what_am_i() const { return typeid(*this).name(); } 注:typeid 会返回一个type_info对象 其中储存着与类型相关的种种信息 每一个多态类都对应一个 type_info对象 type_info class 也支持相等和不等两个比较操作: num_sequence *ps = &fib; if (typeid(*ps) == typeid(Fibonacci)) {//这里可以直接传入类名 // ... ps->gen_elems(64);// 想调用Fibonacci的函数 这会产生编译错误 虽然我们知道类型正确 但编译器不知道 // 要想调用 必须指示编译器 将ps的类型转换为 Fibonacci指针 } 无条件转换://编译器无法确认我们所进行额转换操作是否完全正确 所以需要在上面的条件语句内执行 Fibonacci *pf = static_cast<Fibonacci*>(ps); 更安全的转换: if (Fibonacci *pf = dynamic_cast<Fibonacci*>(ps)) {// 会进行运行时检查操作 // 检查ps所指对象是否属于Fibonacci类 如果是 转换操作发生 不是 会返回 0 if语句便不成立 pf->gen_elems(64); }
第六章 以template进行编程
6.1 被参数化的类型 参数化的类型:类型的相关信息可自template定义中剥离 每个 class template 或 function template 基本上都随它所作用或它所内含的类型而有性质上的变化 template <typename valType> class BTnode;// 前置声明 定义class template: template <typename valType> class BTnode { public: // ... private: valType _val; int _cnt; BTnode *_lchild; BTnode *_rchild; }; 二叉树类只含有一个数据成员: template <typename elemType> class BinaryTree { public: // ... private: BTnode<elemType> *_root; }; 6.2 Class Template 的定义 template <typename elemType> class BinaryTree { public: BinaryTree();// 默认构造函数 BinaryTree(const BinaryTree&);// 复制构造函数 ~BinaryTree();// 析构函数 BinaryTree& operator=(const BinaryTree&);// 赋值复制重载 bool empty() { return _root == 0; } void clear(); private: BTnode<elemType> *_root; void copy(BTnode<elemType> *tar, BTnode<elemType> *src); }; 在类体外 class template member function 的定义语法: template <typename elemType> inline BinaryTree<elemType>::// class 定义之外 在这个定义之后的虽有东西都视为位于clas定义范围 BinaryTree() : _root(0)// 在class定义范围之内 {} 6.3 Template类型参数的处理 将所有 template 类型参数视为“class类型”来处理 这意味着我们会把它声明为一个 const reference 而不是以 by value方式传递 注:这样的做法是为了效率保证 在构造函数的定义中 最好选择使用成员初始化列表来对成员进行赋予初始值 而不要在函数体内进行赋值: template <typename valType> inline BTnode<valType>:: BTnode(const valType &val)// 将val 作为类来看待 : _val(val)// 成员初始化列表 { // 函数体 // 最好不要这样 _val = val; } 构造函数体内对成员数据赋值操作可分解为两个步骤: 1 函数体执行前 类的默认构造函数会先作用与 成员身上 2 函数体内会以 赋值复制的方式将新值复制给成员 6.4 实现一个 Class Template 值得注意的一点: remove_value(const valType &val, BTnode *& prev);// 这里面的 prev是个reference to pointer 指向指针的指针 这样的目的是因为在函数体内不仅要可能需要修改指针指向的内容 也有可能会修改指针的值 6.5 一个以 Function Template 完成的Output运算符 直接上例子: template <typename elemType> inline ostream& operator<<(ostream &os, const BinaryTree<elemType> &bt) { os << "Tree: " << endl; bt.print(os);// print是二叉树类内的私有函数 要在树类内声明 这个运算符为friend return os; } 再次记忆:如果希望函数 A 能够访问一个类 B 的私有成员 或调用这个类的私有函数 则需要 在 B 内将A声明为 friend 6.6 常量表达式与默认参数值 Template参数表示非得 某种类型(Type)不可 我们可以使用常量表达式作为template参数 一个例子: template <int len>// len 这个参数作为template class num_sequence { public: num_sequence(int beg_pos = 1); // ... }; template <int len> class Fibonacci : public num_sequence<len> {// 此处将len传入 public: Fibonacci(int beg_pos = 1) : num_sequence<len>(beg_pos) {} // ... }; 使用的时候 Fibonacci<16> fib1;// 定义了一个长度是16的的 Fibonacci 数列 Fibonacci<64> fib2(17);// 指定了起始位置 模板参数也可以指定默认初始值 template <int len, int beg_pos> class num_sequence { // ... }; template <int len, int beg_pos = 1> class Fibonacci : public num_sequence<len, beg_pos> { // ... }; 这里参数默认值和一般函数的默认参数值一样 由左到右进行解析 并可以被覆盖 全局作用域内的函数以及对象 其地址也是一种常量表达式 也可以拿来表达这一形式的参数 例子:// 接受函数指针作为参数 template <void (*pf)(int pos, vector<int> &seq)> class numeric_sequence { /* ...*/ }; 使用: void pell(int pos, vector<int> &seq); numeric_sequence<pell> ns_pell(18, 8);// 函数名作为模板参数传入 6.7 以 Template 参数作为一种设计策略 函数对象内使用template 上例子: template <typename elemType, typename Comp = less<elemType>>// 这里指定了Comp的默认值 class LessThanPred { public: LessThanPred(const elemType & val) : _val(val) {} bool operator()(const elemType &val) const {// call return Comp(val, _val); } void val(const elemType &newval) {// setter _val = newval; } elemType val() const {// 还进行了重载 getter return _val; } private: elemType _val; } 一个提供比较功能的 函数对象: class StringLen { public: bool operator()(const string &s1, const string &s2) { return s1.size() < s2.size(); } }; 使用: LessThanPred<int> lti(1024); LessThanPred<string, StringLen> ltps("hello"); 6.8 Member Template Function 例子: template <OutStream>// 这里将输入模板化 class PrintIt { public: PrintIt(OutStream &os) : _os(os) {} // 成员模板函数 1 是成员函数 2 使用了模板 template <typename elemType>// 这里再次使用模板 类输出不同类型 void print(const elemType &elem, char delimiter = '\n') { _os << elem << delimiter; } }; 使用: PrintIt<ostream> to_standard_out(cout); to_standard_out.print("hello");
第七章 异常处理
7.1 抛出异常 异常处理机制有两部分: 异常的鉴定与发出 异常的处理方式 异常出现后 正常程序的执行便被暂停 于此同时 异常处理机制开始搜索程序中有能力处理这一异常的地方 异常被处理完毕之后 程序便会继续从异常处理点接着执行下去 throw 表达式产生异常: inline void Triangular_iterator:: check_integerity() { if (_index >= Triangular::_max_elems) { throw iterator_overflow(_index, Triangular::_max_elems);// 产生异常 } } 注:throw看起来有点像函数调用 throw表达式 会直接调用有两个参数的构造函数 异常是某种对象 最简单的异常 对象可以设计为 整数 或 字符串: throw 42;// int throw "hello world";// string 异常的定义 与正常的类定义没有什么区别 可以明确指出被抛出对象的名字: iterator_overflow ex(_index, Triangular::_max_elems);// 生成对象 throw ex;// 抛出 7.2 捕获异常 捕获异常使用 catch 关键字来完成 // 类java catch 可以来捕获不同的异常 分别进行差处理 当有异常抛出的时候 会逐一与每个catch子句对比 找到符合类型 执行子句的内容 catch子句内一般进行异常的处理 或异常信息的输出 如果在catch子句内做完该完成的任务 还可以再使用 throw 将捕获到的异常再次抛出 供外层程序来处理: catch (iterator_overflow) { log_message(iof.what_happened()); // 重新抛出异常令另一个catch子句接手处理 throw; } 如果要不对异常进行分类处理 可以直接捕获全部异常: catch (...) {// 此处的省略号代表全部类型异常 // what to do } 7.3 提炼异常 try-catch 子句 try { // 可能会抛出异常的代码块 } catch (...) { // 异常处理 } try 块内代码抛出异常后 异常处理机制开始查看 异常由何处抛出 并判断是否位于try块内 如果是 就检查相应catch子句 看它是否具备处理异常的能力 如果有 异常被处理 程序继续正常执行下去 程序存在多层调用链 如果内层的程序抛出异常 但throw表达式并非位于try内 异常处理机制不去处理它 异常处理机制会继续在这个函数 调用端 搜寻类型吻合的catch子句 一直向上搜索 直到找到可以捕获这一异常的子句 来处理 如果“函数调用链”不断地解开 一直回到了main()还是找不到合适的catch子句 则 C++ 规定 每个异常都应该被处理 main()内找不到合适的处理程序 便会调用标准库提供的 terminate() --默认行为:中断整个程序的执行 7.4 局部资源管理 主要考虑的问题是 申请的内存会不会得到合理的释放 例子: extern Mutex m; void f() { // 请求资源 int *p = new int; m.acquire(); process(p);// 当这句代码抛出异常 后面的资源释放 代码 不会被执行 m.release(); delete p; } 一种解决办法是 捕获所有异常、释放所有资源、再将异常重新抛出 void f() { try { // } catch (...) { m.release(); delete p; throw; } } 注意:需要考虑代码整洁与执行效率问题 资源管理:对对象而言 初始化操作发生于构造函数内 资源的请求也应该再构造函数内完成 资源的释放则应该在析构函数内完成 #include <memory> void f() { auto_ptr<int> p(new int);// 标准库提供的class template 它会自动删除通过new表达式分配的对象 这里的p MutexLock ml(m); process(p); // p和ml的析构函数会在此处被悄悄调用 } 注:auto_ptr 将 * 与 -> 运算符进行了重载 可以像使用指针一样来使用auto_ptr对象 auto_ptr<string> aps(new string("hello")); string *ps = new string("hello"); if ((aps->size() == ps->size()) && (*aps == *ps)) { // ... } 重点:在异常处理机制终结某个函数之前 C++保证 函数中所有局部对象的析构函数都会被调用 7.5 标准异常 当new表达式无法从空间空间分配足够的内存 它会抛出 bad_alloc 异常对象 ptext = new vector<string>; 会分配足够的内存 然后将vector<string> 默认构造函数应用于heap对象之上 然后再将对象地址设置给ptext 注意的是:先分配内存 再进行初始化(先有地 才可以盖房子) catch (bad_alloc) {// bad_alloc是class 不是一个object 因为我们只对抛出的异常类型感兴趣 没有必要声明任何对象 } 标准库定义了一套异常类体系 根部是名为 exception 的抽象基类 exception声明有一个what() 虚函数 会返回一个 const char * 用以表示被抛出异常的文字描述 我们可以自己编写异常类 来继承于 exception 然后必须提供自己的what() 函数 例子: #include <exception> class iterator_overflow : public exception { public: iterator_overflow(int index, int max) : _index(index), _max(max) {} int index() { return _index; } int max() { return _max; } const char* what() const;// 必须实现的waht() private: int _index; int _max; }; 补充: 类比与 C语言的 sprintf 将不同类型 格式化 到字符串 #include <sstream> ostringstream ex_msg;// ostringstream class 提供“内存上的输出操作” 输出到一个 string对象上 ex_msg << "hello world" << "," << _index << _max;// _index 是int类型 // 萃取出string对象 string msg = ex_msg.str(); // 萃取出 const char * 表述式 const char* c_str = msg.c_str();// string类提供了 c_str() 函数 将对象转成 C-stype字符串 同理还有 istringstream 类比与 C的 sscanf 可将非字符串数据的字符串表示转换为其实际类型
--------------------------------------------------------------------------------------------------------