《Essential C++》笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fenglingfeixian/article/details/80550052

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 如何撰写函数

思想:

  1. 对函数传入的参数做“是否合理”的判断;
  2. 如果不合理,可采用终止程序操作exit() 通常需要带参数,这里填-1;
  3. 最好的方法是采用异常处理:不过第七章才开始讨论。

2.2 调用一个函数

引用:

  1. 声明的时候就要初始化;
  2. 后期不可修改(本身就是另一对象的引用,无法修改);
  3. 引用的对象必须是可修改的(左值);
  4. 除非定义为常量引用。

指针和引用作为函数参数的区别:

  1. 指针在使用前必须判断是否为空;
  2. 引用具体指向某个对象,所以不需要;
  3. 指针可以设置默认值0(表示并未指向任何对象);
  4. 引用不能设为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个求数列的函数都放在了一个数组中,方便调用。
但另一个问题又产生了,怎么找到想要函数的位置呢?这里引入枚举类:enumenum 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函数,从初级版不断扩充其通用性。

扫描二维码关注公众号,回复: 6728954 查看本文章
  1. 传入vector<int> vec参数,利用循环到vec.size(),逐一查找;
  2. 支持int 和其它类型(前提是有相等运算符):利用template 传入vector<T> vec,循环到vec.size()
  3. 支持 array(不需要函数重载):T *array, int size或者T *array, T *sentinel。由此发现具体的array从参数表中消失了,取而代之的是指针(并且可以和vec一样,采用[]运算符直接取值)(引入begin() {return vec.empty() ? 0 : &vec[0];}是为了简便每次使用的判断);

3.2 了解Iterators(泛型指针)

紧接着上一节的讨论:

  1. 支持 list (非连续空间存储):while (first != last) { cout << *first << ' '; ++first; }一样可以将iterators类型的first、last当做指针,唯一的差别在于三个运算符* != ++由iterator classes提供,这样就能在template泛型的类型之后,统一用指针操作了。
  2. 由此得出定义iterator时需要的信息:1、容器类别(决定了如何取下一元素(++运算符));2、内部元素的类型(决定了如何取值(*运算符));
  3. 目前find()已经有了很大的通用性,已经收获丰硕,但并未结束:因为函数的内部实现依赖了相等运算符==,所以如果最底部的元素没有提供这个运算符(比如自己定义的class),或者用户希望自己赋予不同的意义,这个函数的通用性就欠佳了。
  4. 两个解决办法:1、传入一个函数指针,取代原本固定使用的相等运算符;2、运用所谓的 function object (这是一种特殊的class)。

注意事项:哨兵(sentinel)拿来和其他元素的地址作比较是合法的,但不能进行读取或者写入操作。

3.3 所有容器的共通操作

下列为所有容器类(包括string类)的共通操作:

  • equality ==和 inequality !=运算符,返回truefalse
  • assignment =运算符,将某个容器复制给另一个容器。
  • empty()会在容器无任何元素时返回true,否则返回false
  • size()传用容器内当前含有的元素数目。
  • clear()删除所有元素。
  • begin()返回一个iterator,指向容器的第一个元素。
  • end()返回一个iterator,指向容器的最后一个元素的下一个位置。
  • insert()将单一或某个范围内的元素安插到容器内。
  • erase()将容器内的单一元素或某个范围内的元素删除。

3.4 使用序列式容器

vectorlist是两个最主要的序列式容器。作用和效率不一样。

  • vector:随机存取方便,但插入和删除效率低(因为右边每个元素都会移动)(操作最后一个元素除外)。
  • list:插入和删除方便,但随机存取效率低(每次都需要遍历)。
  • deque:和vector相似,但对于前端和后端元素的插入和删除都方便。

定义序列式容器的5种方法:

  1. 产生空的容器:list<string> slist; vector<int> ivec;
  2. 产生特定大小的容器,每个元素都以其默认值为初值:list<int> ilist(1024); vector<string> svec(32);
  3. 产生特定大小的容器,并未每个元素指定初值:vector<int> ivec(10, -1); list<string> slist(16, "unassigned");
  4. 通过一对iterators产生容器。这对iterators用来标示一整组作为初值的元素区间(数组的指针也符合):int ia[8] = {1,1,2,3,5,8,13,21}; vector<int> fib(ia, ia+8);
  5. 根据某个容器产生出新容器。复制原容器内的元素,作为新容器的初值: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>

  1. find():用于搜寻无序集合,范围 [first, last] 。返回 iterator:若找到,指向该值,否则指向 last。
  2. binary_search():二分查找,必须为有序集合,由程序员保证。
  3. count():返回数值相符的元素数目。
  4. search():是否存在子序列。存在指向子序列起始处,否则指向last。

引申的函数:

  • max_element()
  • sort(first, last)
  • copy(first, last, first2):程序员要保证第二个容器拥有足够的空间。如果不确定,可以使用所谓的inserter(3.9节)。

3.6 如何设计一个泛型算法

#include <functional>
在公司看了一遍,不懂,下班前又看了一遍,回到家后又花个多小时敲了一遍。终于有了点感悟:

  1. 首先利用函数指针做到了“和比较操作无关”;
  2. 然后先不要管find_if()的实现,往后面看;
  3. 标准库定义了许多Function Objects,其中包括了前面需要手写的各类型比较函数,而且对不同元素类型的使用也比较方便less<T>(这么做还能提高效率:令call运算符成为inline,因而消除“通过函数指针来调用函数”时需付出的额外代价)。
  4. find_if()需要一元运算符,标准库提供了Function Object Adapters(配接器)将less<int>转为一元运算符的函数。利用bind1st()bind2nd()转了之后再使用比较函数时传入一个参数即可。
  5. 最后解决“和元素类型无关”、“和容器类型无关”。

要点提示: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中方法:

  1. 判断直接由key索引得到的value值。缺点是如果不存在,索引后会创建对应key和对应类型的默认value值。
  2. 利用map的find()函数(不是泛型算法的find()):words.find("hello");返回的是iterator,指向该成员或者指向words.end()。
  3. 利用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}
  1. 加入单一元素:iset.insert(6);
  2. 加入某个范围的元素:iset.insert(vec.begin(), vec.end());
  3. 在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()函数取代赋值运算符,只适用于listdeque
  • 以上的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_typestring::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
{ return ::count( _stack.begin(), _stack.end(), elem); }
在count之前加::运算符修饰,代表调用全局范围运算符,否则将递归调用自己。

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 Membermutable 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)

  1. 找出所有子类共通的操作行为(函数);
  2. 根据“是否与型别相依(type-dependent)”,来区分是否使用虚拟函数(virtual functions)。(有些函数(print())可能并不好区分,先假设相关);
  3. 判断函数的使用权限(public、protected、private(基本没有,违背设计基类的初衷));

知识点:

  • 纯虚函数:每个虚函数,要么自己有定义,要么设为“纯”虚函数,代表对于该类并无实质意义。virtual void gen_elems(int pos) = 0;
  • 如果类声明了一个及以上纯虚函数,程序无法为它产生任何对象,这种类只能作为派生类的子对象,并且其派生类还必须为所有虚拟函数提供了确切的定义。
  • 基类是为了提供接口,所以可以没有任何数据成员。
  • 因为它没有任何数据成员,所以构造函数也没有必要。
  • 但是要注意,析构函数必须要声明为虚函数Parent *p = new Child(); delete p;p是基类指针,但实际指向派生类,delete时。。。还没完全理解

随学习持续更新中。。。

猜你喜欢

转载自blog.csdn.net/fenglingfeixian/article/details/80550052