1.7 文件的读写
#include <fstream>
ifstream ifile("input.txt");
ofstream ofile("output.txt", ios_base::app);//追加模式
fstream iotile("test.txt", ios_base::in|ios_base::app);
iofile.seekg(0);//定位至起始处
2.1 如何撰写函数
思想:
- 对函数传入的参数做“是否合理”的判断;
- 如果不合理,可采用终止程序操作
exit()
通常需要带参数,这里填-1; - 最好的方法是采用异常处理:不过第七章才开始讨论。
2.2 调用一个函数
引用:
- 声明的时候就要初始化;
- 后期不可修改(本身就是另一对象的引用,无法修改);
- 引用的对象必须是可修改的(左值);
- 除非定义为常量引用。
指针和引用作为函数参数的区别:
- 指针在使用前必须判断是否为空;
- 引用具体指向某个对象,所以不需要;
- 指针可以设置默认值0(表示并未指向任何对象);
- 引用不能设为0(原来以为引用不能设置默认值,后来看见
ostream &os = cout
,说明也可以)。
2.4 使用局部静态对象
为了解决每次调用函数,重复计算的问题,使用局部静态对象。(为了解决函数间的通信问题而将对象定义于file scope内,永远都是一种冒险,还会打乱不同函数间的独立性,使他们难以理解)
const vector<int>* fibon_seq(int size)
{
static vector<int> elems;
return &elems;
}
此方法对应:1、引用类型函数无法返回局部变量;2、指针类型函数返回局部变量后会不可预估。所以现在返回局部静态变量则是可行的,因为他的内存不会释放。
2.5 inline函数
一个大函数拆分成几个小函数,调用多个函数有可能影响执行效能。
C++提供了一个解决办法,将这些函数声明为inline,调用的地方在编译时会以一份函数码副本取而代之。
有一定要求:体积小、常被调用、计算并不复杂(递归就不行)。
2.6 重载函数
2.7 模板函数
如果函数具备多种实现方式,我们可将它重载。
如果重载函数间只是参数的数据类型不同,可引入模板函数。
模板函数同时也可以是重载函数。
// 模板函数再经重载
template <typename T> void display(const string &msg, const vector<T> &vec);
template <typename T> void display(const string &msg, const list<T> &lst);
2.8 函数指针
为了使bool fibon_elem(int pos, int &elem);
函数获得更好的通用性(调用任一个计算数列的函数),而不是仅仅调用fibon_seq(pos)
。
引入函数指针作为参数。bool seq_elem(int pos, int &elem, const vector<int>* (*seq_ptr)(int));
更为复杂的函数指针数组的定义:const vector<int>* (*seq_array[])(int) = {fibon_seq, lucas_seq, ...);
为此将6个求数列的函数都放在了一个数组中,方便调用。
但另一个问题又产生了,怎么找到想要函数的位置呢?这里引入枚举类:enum:enum ns_type {ns_fibon, ns_lucas, ...};
练习2.5、2.6
标准库中求数组中的最大元素值。
#include <algorithm>
vector<T> vec;
T *max_element(vec.begin(), vec.end());
T a[];
T *max_element(a, a + size);// 数组的地址,数组大小
3 泛型编程风格
STL主要由两种组件构成:容器(container)、泛型算法(generic algorithm)。
容器分为:序列式容器(sequential container)(vector、list)、关联式容器(associative container)(map、set)。
3.1 指针的算术运算
本节讲解实现stl中find函数,从初级版不断扩充其通用性。
- 传入
vector<int> vec
参数,利用循环到vec.size()
,逐一查找; - 支持int 和其它类型(前提是有相等运算符):利用template 传入
vector<T> vec
,循环到vec.size()
; - 支持 array(不需要函数重载):
T *array, int size
或者T *array, T *sentinel
。由此发现具体的array从参数表中消失了,取而代之的是指针(并且可以和vec一样,采用[]运算符直接取值)(引入begin() {return vec.empty() ? 0 : &vec[0];}
是为了简便每次使用的判断);
3.2 了解Iterators(泛型指针)
紧接着上一节的讨论:
- 支持 list (非连续空间存储):
while (first != last) { cout << *first << ' '; ++first; }
一样可以将iterators类型的first、last当做指针,唯一的差别在于三个运算符* != ++
由iterator classes提供,这样就能在template泛型的类型之后,统一用指针操作了。 - 由此得出定义iterator时需要的信息:1、容器类别(决定了如何取下一元素(
++
运算符));2、内部元素的类型(决定了如何取值(*
运算符)); - 目前find()已经有了很大的通用性,已经收获丰硕,但并未结束:因为函数的内部实现依赖了相等运算符
==
,所以如果最底部的元素没有提供这个运算符(比如自己定义的class),或者用户希望自己赋予不同的意义,这个函数的通用性就欠佳了。 - 两个解决办法:1、传入一个函数指针,取代原本固定使用的相等运算符;2、运用所谓的 function object (这是一种特殊的class)。
注意事项:哨兵(sentinel)拿来和其他元素的地址作比较是合法的,但不能进行读取或者写入操作。
3.3 所有容器的共通操作
下列为所有容器类(包括string
类)的共通操作:
- equality
==
和 inequality!=
运算符,返回true
或false
。 - assignment
=
运算符,将某个容器复制给另一个容器。 empty()
会在容器无任何元素时返回true
,否则返回false
。size()
传用容器内当前含有的元素数目。clear()
删除所有元素。begin()
返回一个iterator
,指向容器的第一个元素。end()
返回一个iterator
,指向容器的最后一个元素的下一个位置。insert()
将单一或某个范围内的元素安插到容器内。erase()
将容器内的单一元素或某个范围内的元素删除。
3.4 使用序列式容器
vector
和list
是两个最主要的序列式容器。作用和效率不一样。
- vector:随机存取方便,但插入和删除效率低(因为右边每个元素都会移动)(操作最后一个元素除外)。
- list:插入和删除方便,但随机存取效率低(每次都需要遍历)。
- deque:和vector相似,但对于前端和后端元素的插入和删除都方便。
定义序列式容器的5种方法:
- 产生空的容器:
list<string> slist; vector<int> ivec;
- 产生特定大小的容器,每个元素都以其默认值为初值:
list<int> ilist(1024); vector<string> svec(32);
- 产生特定大小的容器,并未每个元素指定初值:
vector<int> ivec(10, -1); list<string> slist(16, "unassigned");
- 通过一对
iterators
产生容器。这对iterators用来标示一整组作为初值的元素区间(数组的指针也符合):int ia[8] = {1,1,2,3,5,8,13,21}; vector<int> fib(ia, ia+8);
- 根据某个容器产生出新容器。复制原容器内的元素,作为新容器的初值:
list<string> slist; list<string> slist2(slist);
前端/后端的插入/删除:
push_front()
和pop_front()
(vector不支持)push_back()
和pop_back()
front()
和back()
插入函数insert()
的4种变形:
iterator insert(iterator position)
:在position前插入所属类型的默认值,返回指向默认值的iterator。iterator insert(iterator position, elemType value)
:在position前插入value,返回指向value的iterator。void insert(iterator position, int count, elemType value)
:在position之前插入count个value。void insert(iterator1 position, iterator2 first, iterator2 last)
:在position前插入另一个容器从first到last的元素。
删除函数erase()
的2中变形:
iterator erase(iterator position)
:删除position所指元素,返回的iterator指向删除元素的下一位置。iterator erase(iterator first, iterator last)
:抹除 [first, last] 范围内的元素,返回的iterator指向删除的最后元素的下一位置。
3.5 使用泛型算法
#include <algorithm>
find()
:用于搜寻无序集合,范围 [first, last] 。返回 iterator:若找到,指向该值,否则指向 last。binary_search()
:二分查找,必须为有序集合,由程序员保证。count()
:返回数值相符的元素数目。search()
:是否存在子序列。存在指向子序列起始处,否则指向last。
引申的函数:
max_element()
sort(first, last)
copy(first, last, first2)
:程序员要保证第二个容器拥有足够的空间。如果不确定,可以使用所谓的inserter(3.9节)。
3.6 如何设计一个泛型算法
#include <functional>
在公司看了一遍,不懂,下班前又看了一遍,回到家后又花个多小时敲了一遍。终于有了点感悟:
- 首先利用函数指针做到了“和比较操作无关”;
- 然后先不要管
find_if()
的实现,往后面看; - 标准库定义了许多
Function Objects
,其中包括了前面需要手写的各类型比较函数,而且对不同元素类型的使用也比较方便less<T>
(这么做还能提高效率:令call运算符成为inline,因而消除“通过函数指针来调用函数”时需付出的额外代价)。 find_if()
需要一元运算符,标准库提供了Function Object Adapters
(配接器)将less<int>
转为一元运算符的函数。利用bind1st()
或bind2nd()
转了之后再使用比较函数时传入一个参数即可。- 最后解决“和元素类型无关”、“和容器类型无关”。
要点提示:将Function Objects
理解成函数类,定义了一个函数对象之后(定义方法和容器一样),使用方法就跟函数一样。
3.7 使用Map
#include <map>
map对象有一个名为first的member,对应于key,另一个名为second的member,对应于value。
map<string, int> words;
map<string, int>::iterator it = words.begin();
for(; it != words.end(); it++)
cout << "key: " << it->first << " value: " << it->second << endl;
查询map内是否存在某个key,有3中方法:
- 判断直接由key索引得到的value值。缺点是如果不存在,索引后会创建对应key和对应类型的默认value值。
- 利用map的find()函数(不是泛型算法的find()):
words.find("hello");
返回的是iterator,指向该成员或者指向words.end()。 - 利用map的count()函数:
if (words.count("hello"))
任何一个key值在map内最多只会有一份,如果要存储多份相同的key值,必须使用multimap,本书并不讨论。
3.8 使用Set
#include <set>
Set由一群keys组合而成(可以想象成没有value的map对象)。
set<string> word_exclusion;
// 判断是否包含某个key值:
if (word_exclusion.count("hello"))
continue;
对于任何的key值,set只能存储一份。如果要存储多份相同的key值,必须使用multiset。本书并不讨论。
map元素依据对应类型默认的less-than运算进行排列。
int ia[10] = {2,3,5,8,5,3,1,5,8,1};
vector<int> vec(ia, ia+10);
set<int> iset(vec.begin(), vec.end());
// iset的元素将是{1,3,5,8}
- 加入单一元素:
iset.insert(6);
- 加入某个范围的元素:
iset.insert(vec.begin(), vec.end());
- 在set身上进行迭代:
set<int>::iterator it = iset.begin(); for(; it != iset.end(); it++) cout << *it << ' ';
泛型算法中和set相关的算法:set_intersection()
,set_union()
,set_difference()
,set_symmetric_difference()
。
3.9 如何使用Iterator Inserters
#include <iterator>
在3.6节对filter()的实现中,将来源端(容器)中符合条件的元素一一赋值到目的端(容器),必须保证目的端有足够大容量。之前用和来源端同样大小,不免有些浪费。于是想到了push_back()这类安插方法,但这里用其它的取代它们。
back_inserter()
:会以容器的push_back()
函数取代赋值运算符,参数填入目的端容器即可。inserter()
:会以容器的insert()
函数取代赋值运算符。参数一是目的容器,二是安插位置的起始点。front_inserter()
:会以容器的push_front()
函数取代赋值运算符,只适用于list
和deque
。- 以上的
adapters
都不能用在array
身上。
3.10 使用iostream Iterators
#include <iterator>
流式泛型指针:这是我个人总结出的名字。它靠输入输出流来初始化,得以像泛型指针那样操作容器。
vector<string> text;
istream_iterator<string> is(cin); // 用标准输入流初始化变量,等于提供了first iterator
istream_iterator<string> eof; // 不指定istream对象,代表了end-of-file,等于提供了last iterator
copy(is, eof, back_inserter(text)); // 复制到vector容器中
ostream_iterator<string> os(cout, " "); // 连接至标准输出设备," "为连接符,支持C-style。
copy(text.begin(), text.end(), os); // 复制到os所表示的输出流上面
通常我们是操作文件而非标准输入输出设备。用fstream的对象代替iostream的就可以了。
练习
stable_sort()
:会在符合排序条件的原则下维护元素间原本的相对排序。typedef vector<string> vstring
:简化那些声明起来十分麻烦的类型。string::size_type
、string::size()
、string::find_first_of()
、string::substr()
。copy(first, last, back_inserter(vec))
:copy()会采用赋值运算符来复制每个元素,由于input vector是空的,第一个元素赋值操作就会导致溢位(overflow)错误。所以使用back_inserter()是必须的,避免赋值运算符,改以push_back()函数来安插所有元素。partition(first, last, even_elem())
会将区间[first,last)中的元素重新排列,满足判断条件pred的元素会被放在区间的前段,不满足pred的元素会被放在区间的后段。该算法不能保证元素的初始相对位置,如果需要保证初始相对位置,应该使用stable_partition.
4 基于对象的编程风格
4.1 如何实现一个 class
在 class 主体内定义的函数,自动视为 inline 函数。其他地方要定义 inline 函数,也应该同在 .h 头文件中(主体之外)。不是 inline 函数,可以再代码中定义(.cpp)。
int Stack::count( const string &elem ) const
在count之前加
{ return ::count( _stack.begin(), _stack.end(), elem); }::
运算符修饰,代表调用全局范围运算符,否则将递归调用自己。
4.2 什么是Constructors(构造函数) 和 Destructors(析构函数)
Triangular t;
:调用默认无参构造函数;Triangular t2(10, 3);
:调用两个参数的构造函数;Triangular t3 = 8;
:调用一个参数的,等同于Triangular t3(8);
Triangular t5();
:这是定义一个函数,返回Triangular类型。- 在声明处使用默认参数,可以更灵活地初始化数据成员。
- 另一种初始化的方式是使用成员初值表:
Tri::Tri(const Tri &tri): _len(tri._len), _bp(tri._bp){}
,这种方式可以不考虑参数的顺序,选择性地初始化数据成员。
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;
};
destructor 并非绝对必要。上述 Triangular 例子中的3个 data members,它们以储值(by value)方式来存放,这些members 在 class object 被定义之后便以存在,并在 class object 结束其生命时被释还,因此 destructor 没什么事好做。
C++编程最难的部分之一,便是了解何时需要定义 destructor 而何时不需要。
成员逐一初始化:用一个对象初始化另一个对象时,通常会将成员逐一初始化。但有的时候不能这么做,比如上例中的Matrix类,因为两个对象的指针指向同一个数组,所以当其中一个生命周期结束,被释放后,另一个对象还在引用操作的话,是非常严重的错误行为。因此需要重写拷贝构造函数(copy constructor):Matrix::Matrix(const Matrix &mat){}
。在函数体内用另外的数组副本给新对象的指针赋值。
当我们设计 class 时,必须问问自己,在此 class 之上进行“成员逐一初始化”的行为模式是否适当?如果是,我们就不需要另外提供 copy constructor 。如果不是,我们就必须另行定义 copy constructor, 并在其中撰写正确的初始化操作。(同样有必要撰写 copy assignment operator(参阅 4.8 节))。
4.3 何谓mutable(可变)和const(不变)
这一节不是很明白。
不改变数据成员的成员函数,可以在参数列表后面加上 const 修饰符,编译器会检查这些函数,如果函数内改变了,则给出错误或者警告。
const 修饰的函数和没修饰的可以重载,被const 修饰的对象会自动调用const 修饰的。
Mutable Data Member:mutable int _next;
这样修饰后,就可以被声明为 const member functions
调用,并可以通过编译。
4.4 什么是 this 指针
Triangular tr1, tr2;
tr1.copy(tr2); // 程序员编写代码
copy(&tr1, tr2); // 编译器内部转换代码
// 作用是引入this指针,可以让我们取用其调用者的一切。
4.5 Static Class Member (静态的类成员)
类中声明的 static data member 只存在一份。
当一个 member function 不对任何 non-static data member 进行存取时,可以将它声明为 static(声明时加,定义时不需要加)。
由于最近工作繁忙,加上这部分有些看不明白,慢慢失去了动力。为了能重拾斗志,决定快速浏览后跳至下一章了
5 面向对象编程风格
前三节比较好理解,快速浏览了,不过以后可以试着实现以下。决定先通读一遍。
5.3 不带继承的多态
这一节说的情况和我目前做的工作很像,开发同一类系统,写了多个文件,然后用枚举类,感觉设计得不是很好(设计费功夫,后期维护工程量大)。所以激起了我的兴趣,想要看看接下来是怎么通过面向对象编程风格解决这个问题的。
5.4 定义一个抽象基类(Abstract Base Class)
- 找出所有子类共通的操作行为(函数);
- 根据“是否与型别相依(type-dependent)”,来区分是否使用虚拟函数(virtual functions)。(有些函数(
print()
)可能并不好区分,先假设相关); - 判断函数的使用权限(public、protected、private(基本没有,违背设计基类的初衷));
知识点:
- 纯虚函数:每个虚函数,要么自己有定义,要么设为“纯”虚函数,代表对于该类并无实质意义。
virtual void gen_elems(int pos) = 0;
。 - 如果类声明了一个及以上纯虚函数,程序无法为它产生任何对象,这种类只能作为派生类的子对象,并且其派生类还必须为所有虚拟函数提供了确切的定义。
- 基类是为了提供接口,所以可以没有任何数据成员。
- 因为它没有任何数据成员,所以构造函数也没有必要。
- 但是要注意,析构函数必须要声明为虚函数。
Parent *p = new Child(); delete p;
p是基类指针,但实际指向派生类,delete时。。。还没完全理解
随学习持续更新中。。。