Advertisement

《Essential C++》笔记

阅读量:

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. 指针不仅可以设置默认值为\texttt{cout}(表示未被有效指向)还可以将其初始化为其他合法的指针类型;
  4. 引用不可以设为\texttt{cout}实际上通过示例代码\texttt{ostream}\&\texttt{os}=\texttt{cout}理解到引用是可以被赋值的对象。

2.4 使用局部静态对象

为了在每次函数调用时避免重复计算问题,在每个函数体内引入一个局部静态对象是一个合理的选择。(为了在不同函数之间实现通信而将对象定义在文件级别(file scope)始终是一种高风险的做法,并且会破坏各函数之间的独立性使其难以理解)

复制代码
    const vector<int>* fibon_seq(int size)
    {
    	static vector<int> elems;
    	return &elems;
    }

此方法涉及以下两个方面:首先,在使用引用类型的函数时,并不能直接获取局部变量;其次,在使用指针类型的函数时,在释放这些指针之后可能会导致不可预测的结果。因此,在这种情况下采用局部静态变量是合理的选择;因为它们不会被系统回收。

2.5 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)这一特定实现。
通过使用函数指针 作为输入参数.\quad该算法将接受并存储一个指向需要执行操作的目标算法类型的指针。
更复杂的参数处理方案是将多个计算数列的功能整合到一个数组中.\quad这样做的好处是可以方便地切换和管理不同的算法实现。
为此我们创建了一个包含多个数列生成器的数组.\quad这个过程使得算法能够灵活地选择不同的计算路径以适应不同的需求。
如何确定所需功能的具体实现?

练习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)。
容器分为两类:
序列式容器(顺序类型容器)(例如向量(vector)和列表(list));
关联式容器(映射类型容器)(例如映射表(map)和集合(set))。

3.1 指针的算术运算

本节讲解实现stl中find函数,从初级版不断扩充其通用性。

  1. 接收一个类型为vector的参数vec,并通过循环遍历其大小属性来逐个查找相关元素;
  2. 该功能支持多种数据类型(前提是这些类型都具有自定义比较运算符),具体实现方式是使用模板类定义一个向量容器vector并对其进行遍历操作;
  3. 此外还支持另一种数据结构——数组(无需自定义函数重载),可以通过以下两种方式传递数组信息:
    a) 直接提供指针及长度参数的方式:即接受形参为T* array以及int size;
    b) 或者采用带有哨兵对象的指针传递方式:即接受形参为T* array以及另一个哨兵类型的指针sentinel。
    这种设计特点在于取消了传统的数组对象作为独立参数存在的必要性,并将其替换成更加灵活的指针形式(其中一些指针可以直接通过[]运算符进行访问操作)。为了简化每次循环操作时的状态判断逻辑,默认情况下会提供一个辅助函数begin() {return vec.empty() ? 0 : &vec[0];} ,这一机制旨在让代码在处理空容器与非空容器时都能保持一致性和简洁性;

3.2 了解Iterators(泛型指针)

紧接着上一节的讨论:

  1. 支持 list (非连续空间存储):与之前不同的是,在while (first != last) { cout << *first << ' '; ++first; }这种情况下,默认情况下迭代器类型中的 first 和 last 可以被视为指向器的对象(即它们支持 *、!= 和 ++ 操作),这使得 template 模板函数能够统一以指针的方式进行操作。
  2. 定义 iterator 时需要关注的主要依据有两点:一是容器类别(它决定了如何获取下一个元素的操作),二是内部存储的数据类型(它决定了如何访问数据);
  3. 目前 find() 函数已经具备很强的通用性并取得了不错的成果,但这一过程并未完成:因为该函数的核心操作依赖于相等操作符(==),所以如果底层数据类型没有提供这种操作符(例如自定义 class 类型),或者用户希望赋予不同的意义,则该函数的实际适用性就会大打折扣。
  4. 解决这一问题主要有两种途径可供选择:第一种是在调用 find() 函数时传递一个自定义比较函数指针;第二种则是采用所谓的 function object(这是一种特殊的 class 对象)。

注意 matters: 将哨兵(sentinel)与其他元素内存地址进行比较是合法的操作, 但这种情况下不允许执行读取或写入操作。

3.3 所有容器的共通操作

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

  • 等式运算符(如等式==和不等式!=)将用于判断两个对象之间的关系并返回布尔值(即true或false)。
  • 赋值运算符(如赋值=)用于将指定的对象复制给另一个对象。
  • 判断是否为空容器时会返回true。
  • size()方法用于获取当前容器中元素的数量。
  • clear()方法将清除所有存储于容器中的数据项。
  • begin()方法将提供一个迭代器对象来指向当前容器的第一个有效数据项的位置。
  • end()方法将提供一个迭代器对象来指向当前容器最后一个有效数据项之后的位置。
  • insert()方法允许向容器中插入单个数据项或者一段连续的数据区间。
  • erase()方法允许从指定位置开始删除单个数据项或者一段连续的数据区间。

3.4 使用序列式容器

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

  • vector:快速访问内存地址存储数据(高效便捷),但在向右扩展时会占用大量内存空间(时间复杂度较高),仅适用于尾部元素的操作(操作最后一个元素除外)。
  • list:在前后两端进行插入与删除操作非常高效便捷(无需额外空间),但在中间位置进行随机访问时需遍历所有元素(时间复杂度较高)。
  • deque:类似于vector,在两端进行增删操作非常高效便捷(无需额外空间),并且可以在两端同时执行增删操作(双端队列特性)。

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

初始化为空的字符串列表 和 未初始化整数向量。
创建长度固定的整数列表(默认初始值) 和 固定长度字符串向量。
数组初始值赋值 的整数向量 和 列表元素未指定初始值 的字符串列表。
基于迭代器范围构建整数向量。
基于现有列表创建新字符串列表。

前端/后端的插入/删除:

  • push_front()pop_front()(vector不支持)
  • push_back()pop_back()
  • front()back()

插入函数insert()的4种变形:

  • insert(iterator position)用于将属于其参数类型的一种默认值预先放置于指定位置。
    • insert(iterator.position. elemType.value)允许在此处插入一种特定类型的elemType.value。
    • insert.iterator.position. int.count. elemType.value该函数可在指定位置插入count个特定类型的elemType.value。
    • insert.iterator1.position. iterator2.first. iterator2.last允许在此处从另一个容器中导入多个元素。

删除函数erase()的2中变形:

  • iterator erase(iterator position):移除指定位置处的元素,并返回该迭代器指向被删元素后的下一个位置。
    • iterator erase(iterator first, iterator last):清除介于first和last之间的所有元素,并返回该迭代器指向被删最后一个元素之后的位置。

3.5 使用泛型算法

#include <algorithm>

  1. find()函数用于在无序集合中进行定位操作,默认指定的区间范围为[first, last]。它会返回一个迭代器对象,在找到目标值时指针会停留在该位置;如果未找到,则指针停留在last位置。
  2. binary_search()函数采用二分查找法实现元素定位。它仅适用于有序序列,并且依赖于程序员正确维护其有序性。
  3. count()函数的作用是统计并返回与给定条件匹配的所有元素数量。
  4. search()函数的功能是判断输入数据中是否存在符合条件的子序列。如果存在,则记录其起始位置;如果没有,则指针停留在last位置。

引申的函数:

  • find_max_element()
    • order(sorted_first, sorted_last)
    • 在调用copy(source_begin, source_end, destination_begin)之前,请确保目标范围有足够的容量来容纳所有元素。若不确定具体容量,则可参考第3章第9节以获取用于动态扩展的inserter组件。

3.6 如何设计一个泛型算法

在公司里对#include <functional>进行了深入学习,在家后又反复练习了几遍才逐渐掌握了它的使用方法。

首先通过函数指针确保了与比较操作无关;
在不深入探讨其实现细节的情况下,请继续往下阅读;
标准库提供了丰富的Function Objects功能模块。其中包含了一些自定义比较函数所需的类型(如lessthan),并且这种设计使得不同数据类型的处理更加便捷。此外,在这种情况下还能够提升效率:通过将调用符转换为内联操作(inline),从而避免了传统方式中通过函数指针调用时所涉及的额外开销。
该算法要求被查找条件满足为一个一元运算符;
最后解决了与元素类型以及容器类型相关的兼容性问题。

**要点提示:**将Function Objects视为函数类,在定义一个函数对象后(采用与容器相似的方式),其处理方式类似于处理函数。

一系列过程的最终目的

3.7 使用Map

#include <map>
该map包含一个成员变量first用于存储键;该map包含另一个成员变量second用于存储值。

复制代码
    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值时存在不足之处在于如果键不存在会导致后续操作生成新的键及其对应的默认default value类型。
  2. 使用std::map中的find()函数(非泛型算法实现)执行如下操作:words.find("hello");该函数返回一个指向该键位置或末尾 iterators的对象。
  3. 通过调用std::map中的count("hello")方法来确定"hello"键是否存在条件如下:if (words.count("hello"))

每个键在map中至多只有一个记录。若需要为同一个键存储多个记录,则需使用multimap数据结构。本书暂不涉及此方面的讨论。

3.8 使用Set

#include <set>
Set由一群keys组合而成(可以想象成没有value的map对象)。

复制代码
    set<string> word_exclusion;
    // 判断是否包含某个key值:
    if (word_exclusion.count("hello"))
    	continue;

每个键值,在set中只能存储一个实例。如果需要为相同的键值存储多个实例,则必须使用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
  2. 覆盖指定范围内的所有元素
  3. 遍历该集合的所有元素并输出其值

基于泛型技术的算法与集合相关的操作包括:set_intersection(), set_union(), set_difference(),以及set_symmetric_difference()等运算符

3.9 如何使用Iterator Inserters

在第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>
** streaming generic pointer:**这是本人命名的一个术语。该概念通过接受输入输出流来进行初始化,并类似于常规的 generic pointer处理容器。

复制代码
    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所表示的输出流上面

通常我们会对电子文档进行处理而非使用传统的IO设备。可以选择使用#include 中的类实例来替代std::iostream功能。

练习

  • stable_sort():它会在满足排序条件的基础上遵循原始序列中各元素之间的相对次序。
  • typedef vector<string> vstring:通过重定向类型定义的方式可以大大减少类型声明带来的麻烦。
  • std::string::size_type、std::string::size()、find_first_of()、substr()等成员函数。
  • std::copy(first, last, back_inserter(vec)):此操作将使用赋值运算符将每个元素复制到目标容器中。由于输入容器为空,在第一次复制操作时会发生溢出(overflow)错误。因此必须使用back_inserter()来实现这种高效的数据插入方式。
  • std::partition(first, last, even_elem()):该算法会根据谓词将区间内的所有元素重新排列为两部分——满足条件的部分位于前面而不满足条件的部分位于后面。需要注意的是该算法无法维持原有序列中的各元组之间的相对次序关系。如果需要同时保留原有顺序则应考虑采用稳定版本的算法。

4 基于对象的编程风格

4.1 如何实现一个 class

类体内部声明或定义的成员函数,默认被视为内联函数。若要在类体外部或别处声明或定义内联函数,则应同样包含于.h头文件中。若该成员函数并非内联函数,则可将其声明或定义于.cpp文件中。
为避免递归调用本类中的成员函数,在调用count之前需添加::运算符修饰。

4.2 什么是Constructors(构造函数) 和 Destructors(析构函数)

  • 该对象t被通过默认无参构造函数进行初始化;
  • 该对象t2被通过指定两个参数的构造函数进行初始化;
  • 该对象t3被通过指定一个参数的构造函数等效替代赋值操作进行初始化;
  • 该对象t5被通过定义一个没有参数的构造函数进行初始化;
  • 在变量声明处适当设置默认参数以提高灵活性;
  • 另一种实现方式是利用成员初值表进行部分属性初始化,
    即 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;
    };

destroys并非必选项,在C++编程中这是一个常见的误区。其中涉及的三个成员变量采用值传递的方式存储数据,在对象创建后自动存在于内存中,并在其生命周期结束时自然消失。因此,在这种情况下无需显式地实现destroy操作。
掌握何时应显式地实现destroys和何时无需显式实现是C++编程中的难点之一。

成员逐一初始化: 当用一个对象来初始化另一个对象时,默认情况下会将每个成员进行单独的初始化操作。
然而,在某些情况下这种做法不可取:
比如上文中所述的Matrix类,
由于其内部成员共享同一个数组指针,
当其中一个对象被销毁后不再使用,
而另一个对象仍在执行相关操作,
这将导致严重的逻辑错误。
因此,在设计类的时候,
我们需要问自己:
在这种情况下,
是否应该继续采用这种"成员逐一初始化"的行为模式?
如果是的话,
我们就不需要为该类型提供专门的拷贝构造函数(copy constructor);
如果不是的话,
我们就必须自行定义一个专门的拷贝构造函数,
并在其中正确地实现数据复制逻辑。(同样也需要编写对应的复制赋值操作符,请参阅第4.8节)

4.3 何谓mutable(可变)和const(不变)

这一节的理解存在一定的困难。
不改变数据成员的成员函数,在参数列表后追加const修饰符时编译器会对这些函数进行检查:若函数内部进行了修改,则会触发错误或警告提示。
被const修饰的对象会自动调用其具有的const功能特性。
可变数据成员类型: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 数据成员。
若某个 member function 对任何 non-static 数据成员不进行访问,则可将其声明为 static(仅在声明时需附加 static 关键字)。

因为近期工作非常忙碌, 再加上这一部分看起来不太明白, 渐渐地失去了动力。为了重新找回精神, 决定迅速浏览完然后直接跳到下一部分。

5 面向对象编程风格

前几节内容相对容易理解,在大致过了一遍之后发现还有不少值得深入研究的地方。但是未来如果有机会的话或许可以尝试一下。打算先系统地阅读一遍并做好笔记。

5.3 不带继承的多态

这一节描述的情况与我当前的工作有相似之处,在开发具有相同功能的系统时编写了多个模块,并采用基于枚举的方法进行数据管理。这种设计虽然在初期投入较多精力(耗时较多),但后期维护工作量较大(维护工作量较大)。这促使我产生了进一步探索的兴趣(激发了我对后续内容的兴趣),想了解如何通过面向对象编程的方法来解决这些问题(想了解后续是如何通过面向对象编程风格解决这些问题)。

5.4 定义一个抽象基类(Abstract Base Class)

  1. 识别所有子类共有的行为特征;
  2. 基于‘类型相关性’判断是否采用虚拟函数;其中一些功能可能难以辨别,默认假设相关情况存在;
  3. 识别哪些权限被赋予了这些功能。

知识点:

  • 纯虚操作:每个虚拟操作若无自定义实现,则声明为纯虚拟操作以表明其无实际作用。
  • 若一个或多个纯虚拟操作被声明,则派生体无法构造实例;基类仅作接口提供者。
  • 基类仅作接口提供者而无数据成员。
  • 由于缺乏数据成员,在构造函数中无需显式初始化参数。
  • 但是要注意析构操作必须声明为纯虚拟操作。

参考自《Essential C++》

如有侵权,请联系本人删除。

全部评论 (0)

还没有任何评论哟~