Accelerated C++笔记

Accelerated C++笔记

虽然两年前就已经接触过C++,但是之前短暂的学习根本不足以让我系统掌握这门语言的用法。现在由于有项目开发的需求,我决定重新开始较为系统地学习C++。由于有C语言的基础,在入门阶段我使用Accelerated C++这本书来学习。这本书虽然较为老旧,但是内容不多,读起来也比较轻松。这篇文章是对我在看书和编程时遇到的问题和重点的总结备忘,内容比较零碎,还请大家谅解。

语言特性

基础知识

  • 声明:告诉编译器有这个对象存在,不需要建立内存

  • 定义:需要建立内存的声明,也即实例化一个对象

  • main函数返回一个整数类型的值作为结果,0表示成功

  • void类型的函数可以不需要return语句,也可以写return;

  • 变量是一个具有名称的对象,对象是计算机中一段具有类型的内存空间(在C++中,一切皆对象)

  • 在花括号中定义的变量,作用域只在它所在的花括号中

  • 对象类型还有接口,接口就是可以对对象进行的操作的集合

  • 重载:

    • 运算符的重载:一个运算符对于不同类型的操作数有不同的含义,如1 + 1'A' + 'B'中的加号具有不同的含义
    • 函数重载:两个功能不同的函数在具有相同名称时,当它们的参数不同时,会发生重载
  • 关于const

    • const表示该值为常量(只读),必须在定义它时进行初始化,变量也可以对常量进行初始化

    • 如果给一个引用常量对象的函数传输一个非常量对象,那么这个对象会被视为常量

    • 不可以对常量对象调用非常量函数

    • 关于const的位置:

      • 修饰指针变量:

        1
        2
        3
        const int* pt = &n;	//指针所指的数据是常量,但是指针是变量,可以指向其他地址
        int* const pt = &n; //指针所指的数据是变量,指针是常量,只能指向n的地址
        const int* const pt = &n; //指针和其所指的数据都不可以改变
      • 修饰函数参数:

        1
        int func(const int n) {}; //函数收到的参数是常量
      • 修饰成员函数:

        1
        2
        int func(int n) const {}; //成员函数不能修改任何除了mutable修饰的的成员变量
        //且不可调用非const成员函数
      • 修饰函数返回值:

        1
        2
        const int func(int n) {}; //函数的返回值是常量,一般不需要这样用
        //除了重载运算符外,一般不要将返回值类型定为对某个对象的const引用
    • const修饰成员函数时,根本上是修饰了该函数所在对象的this指针,如果对象有非constconst两个重载函数时,调用哪一个是由对象是否是const决定的

    • 所有const成员函数是一个类的常量接口,该类的所有常量对象只能调用常量接口

  • 关于static

    • static表示该变量或函数是静态的,只能声明定义一次
    • 静态全局变量:具有全局寿命,在程序运行的整个过程中存在,它在进入main()函数初始化,在退出main()函数后被销毁,它只在声明它的文件中可见
    • 静态局部变量:在函数第一次执行到声明它的语句时进行初始化,它具有全局寿命,但是只在声明它的函数中可见,在单线程时多次调用包含静态局部变量的函数不会导致该静态局部变量被多次定义与初始化
    • 静态函数:它只在声明它的文件中可见,其他文件中可以声明名字相同的函数而且不会发生冲突
    • 静态数据成员:可以让该类的所有对象访问,只初始化一次,而且可以实现全局变量做不到的信息隐藏,它需要在类外部进行初始化
    • 静态成员函数:一般声明和定义都在类中,它只能访问其他静态成员函数和静态数据成员,它没有this指针
      • 如果静态成员函数的声明在类中,定义在类外部,那么在外部定义时不能够加static关键字,否则就相当于声明了两次
      • 静态成员函数没有与其关联的对象,它可以通过类名或者对象名直接访问
    • 使用static 修饰的变量是可以修改的
  • 成员函数:某种类型的对象有一个或者多个函数成员,调用这些函数可以获得一个值,如str.size()可以获得字符串str的长度

  • using声明:作用域为它所在的花括号内

    • using namespace std;:程序中默认使用std这个命名空间
    • using namespace-name::name;:使用name指代namespace-name::name;
    • using std::cout;:使用cout指代std::cout,并且我们不会定义自己的cout
  • 类型转换的原则:较小的类型转换为较大的类型,有符号的类型会转换为无符号类型,算数值会被转换为布尔值,0可以视为false,而其他值都被视为true

  • 缺省初始化:一般来说,在对象定义时如果没有初始化,那么它的内容就是其内存单元中的随机内容,但是下面几种情况下,编译器会进行缺省初始化

    • 全局变量
    • 自定义类型的对象在没有初始化程序的情况下
  • 左值与右值

    • 左值:位置值,也即内存地址
    • 右值:地址中存的值
  • 增量/减量运算符(按照优先级排序):

    • i++:对i加一,并且返回原始值
    • i--:对i减一,并且返回原始值
    • ++i:对i加一,并且返回相加后的值
    • --i:对i减一,并且返回相减后的值
  • 关于clear()方法:

    • 对字符串:将字符串的首地址内容存放\0
    • 对向量:清空向量内容
    • 对流:清除流内部的错误状态
  • 泛型算法:可以用于不同类型的容器和不同的数据类型的算法

  • 迭代器适配器:是一个函数,会产生与其参数相关的属性的迭代器以做他用,它会按照它给定的方式来对迭代器进行操作,下面给出了一些例子:

    • back_inserter(des):将元素添加到des容器的末尾
    • front_inserter(des):将元素添加到des容器的头部(链表支持该种操作,向量和字符串不支持)
    • inserter(c, it):在c的迭代器it之前插入元素
  • 不要将重载函数作为参数传递给模板函数,否则编译器将不知道需要使用的是重载函数哪个版本

  • 将函数作为参数:将函数作为参数时,在声明的时候参数函数的编写形式类似于函数的声明,返回值和参数函数的参数必须与参数函数相一致,但是名称不需要一致

  • 函数的缺省参数:在声明一个函数时,如果在参数后写上= xx为某个值、序列、或者函数),那么这个参数默认值为x,在调用这个函数可以该参数缺省

  • 函数的递归调用:在函数内部调用自身

  • 内联函数:在函数声明与定义时,于函数名字前添加了inline关键字的函数,在编译时所有调用内联函数的地方都会被替换成函数体的代码以提高运行效率,对一般的函数,只有程序运行到调用函数的那个地方时,函数名才会被替代成相对应的函数代码,内联函数的长度不应超过10行

  • main函数:

    • 完整形式:main(int argc, char** argv)
    • 参数:
      • argv:指向一个数组首元素地址的指针,数组中的每一个元素都是都指向一个字符串参数的指针
      • argcargv指向的数组中的指针个数
    • argv的第一个元素指向编译出的程序的名字的字符串的首地址
    • 在程序中可以直接使用这两个对象
  • NULLnullptrNaNINF

    • NULL:历史上曾经用来指代空指针,现在已有了替代,值实际上是0
    • nullptr:C++11引入的关键字,指代空指针
    • NaN:表示Not a Number,无效数字
    • INF:表示Infinite,无穷大,超出了浮点数的表示范围
  • 如果非成员函数的调用位置在定义之前,那么需要先对其进行声明;如果调用位置在定义之后,就不需要声明了

  • 前置声明:如果两个类的定义是相互依赖的,那么在定义第一个类前必须先对第二个类进行前置声明,告诉编译器有这个类存在,否则会出现无穷嵌套的问题

新类型

vector(向量)类型

向量类型可以理解为大小可以按需要随意增长的数组

  • 定义:vector<double> grade;,该语句定义了一个类型为double类型的向量grade
  • 一个向量中的所有数值具有相同的类型
  • 需要使用<vector>头文件
  • grade.push_back(x):将x添加到grade向量的末尾
  • 描述向量长度的数据类型:vector<TYPE>::size_type
  • 访问向量中的指定元素:类似于数组,如grade[i],若一个向量有grade.size()个元素,那么它的第一个元素为grade[0]或者grade.begin(),最后一个元素为grade[grade.size() - 1]或者 grade.end()
  • 删除向量中的元素:
    • 使用erase()成员函数,如:grade.erase(grade.begin() + i)
    • erase()会在删除掉一个元素的同时,将这个元素后面的元素向前移动一个单位
    • erase()会返回一个迭代器(见下文),并且指向刚刚被删除的元素后面的那个元素
  • 向量元素的预分配:可以减少重复的内存分配带来的系统开销
    • v.reverse(n):对向量v保留n个元素的内存空间,但不进行初始化,该操作不会改变容器的大小
    • v.resize(n):给v一个长为n的新长度
      • 新长度小于当前长度:向量位于长度n后的元素会被删除
      • 新长度大于当前长度:向量当前长度后与长度n前的空余位置会添加上新的元素,并且会进行初始化

list(双向链表)类型

链表中的每一个元素都有一个指针指向后一个元素,list(双向链表)的每一个元素都有两个指针,一个指向前一个元素,一个指向后一个元素。list的特性决定了我们可以快速地在容器的任何一个位置增删元素。在顺序访问的情况下,list的访问速度比vector差;但是对于大规模随机访问或者增删元素的情况,list的性能远远好于vector

  • 定义示例:list<double> grade;
  • 需要使用<list>头文件
  • list不支持索引
  • 具有sort()成员函数,它有一个可选的参数compare用于提供排序的比较方法,如果不提供该参数则默认使用<来排序

对string类型的新认识

  • string类型可以视为一种特殊的容器,它支持索引操作,也有相对应的迭代器(类似vector
  • substr(i, j)成员函数:创建一个新的字符串来保存s在区间[i,i+j)中的索引指示的字符的复制
  • 在C++中双引号中的字符串字面量(如"hello")默认为C string,也即const char*类型的

对容器的一些其他操作

  • c.rbegin():对于允许逆序访问的容器,该语句表示指向容器最后一个元素的迭代器,访问的顺序是逆向的
  • c.rend():对于允许逆序访问的容器,该语句表示指向容器第一个元素之前的迭代器
  • container<Type> c(c1);:若c1存在,那么该语句表示定义一个容器c且其为c1的复制
  • container<Type> c(n);:定义一个有n个元素的容器c并根据Type对其元素进行初始化
  • container<Type> c(n, t);:定义一个有n个元素的容器cc的元素是t的复制
  • container<Type> c(b, e);:定义一个有n个元素的容器cc的元素是迭代器[b,e)之间的复制
  • c.size():返回c中元素的个数,返回值类型为size_type
  • c.empty():用于指示c中有无元素,如果没有的话返回True
  • c.insert(d, b, e):复制[b,e)两个迭代器之间的元素,并将它们插入到c容器的d迭代器所指示的位置之前
  • c.erase(b, e):从c中删除[b, e)两个迭代器之间的元素,对于不同的迭代器,该操作的速度有所不同

iterator(迭代器)类型

迭代器用于代替索引,以库能控制的方式访问元素。

  • 每一个标准容器都有两种迭代器类型:
    • container-type::const_iterator:使用该迭代器只能读容器中的元素,对常量对象使用
    • container-type::iterator:使用该迭代器可以读也可以写容器中的元素
    • iterator类型可以转换为const_iterator类型,反之不行
  • const_iterator相当于const T* ptr,是常量对象的迭代器,迭代器本身可以改变;const iterator相当于T* const ptr,迭代器本身是常量,不能改变
  • 使用例:vector<double>::const_iterator iter = grade.begin();,声明了一个名为iter的迭代器,且初始值为grade的第一个元素
  • 迭代器可以使用增量运算符(重载过)
  • 迭代器相当于是一个指针,可以使用*运算符间接引用迭代器所指向元素的内容(访问的是左值),在使用时应当注意运算符的优先顺序
  • 如果要获得迭代器引用的内容的一个元素,可以使用*(iter).element或者iter -> element两种方式访问
  • 注意,对于不支持随机访问索引操作的容器,对迭代器的值进行手动更改(如给迭代器加或者减偏移量)将无法通过编译
  • 删除迭代器当前所指示的元素会使得该迭代器失效
  • vector容器,erasepush_back操作会使得被操作以及之后的元素的迭代器失效,而对list容器则不会
  • 如果一个容器支持索引,那么它的迭代器也支持索引,如:iter[2]等价于*(iter + 2)iter[-1]等价于*(iter - 1)

map(映射表)类型

之前提到的向量、链表等容器都是顺序容器,而map则属于关联容器。关联容器的序列不依赖于插入元素时的顺序,它只依赖于元素本身的值。关联容器的特性可以使我们更快地对特定元素定位。

  • map容器中存储的是一个键-值
    • 键:有点类似于顺序容器中的索引,它指向一个存储空间
    • 值:键所指向的存储空间中存放的内容
  • 声明例:map<string, int> m;,该语句声明了一个名为m,从字符串到整数的映射表,其键的类型为string,值的类型为int
  • 关联容器不可以对元素进行手动排序,一般不对关联容器的内容进行修改
  • 只要声明了一个关联容器,如果我们用一个未曾出现过的键作为映射表的索引,这个映射表会自动创建一个具有这个键的元素,且这个元素具有初始化的值,对于内部类型会被初始化为0
  • pair(数对)类型:该类型提供了访问映射表的键和值的能力,映射表中的每个元素都是一个数对
    • 数对有两个成员,first成员包含了键,second成员包含了值
    • 间接引用映射表迭代器时,获得的就是这个映射表关联的一个数对pair<const T1, T2>
    • 注意,映射表的键是常量
  • map的访问方法:m[key],使用键key来索引访问映射表并返回一个左值,如果对于这个键没有一个合适的项目,那么则会新建一个元素
  • map的另一种访问方法:若iter是一个映射表的迭代器,那么iter -> first可以得到数对的键,iter -> second可以得到数对的值
  • 常量映射表无法使用索引访问
  • find()成员函数:参数为需要查找的键,如果查找到了则返回一个指向这个元素的迭代器,否则返回映射表的末尾
  • 一般情况下map都是自动排序的,但是在可以通过map<K, V> m(cmp);语句在声明时使用cmp来人工确定元素的顺序

自定义类型

作为核心语言的一部分,intchardouble等类型都是内部类型;而vectorstring等类型属于标准库的内容,是自定义类型。用户可以自己定义新的类型,自定义其中的数据、函数、访问接口等,以此对数据结构进行封装。

  • 自定义类的格式:
1
2
3
4
5
6
class Class_name {
public:
//类型提供的接口
private:
//类型的实现
};
  • public:保护标识符,它指示类中的共有成员,类的所有用户都可以访问公有成员
  • private:保护标识符,它指示类中的私有成员,类的私有成员对用户来说是不可以直接访问的
  • 同样可以用struct关键字来自定义类,但是它与class有所区别,如:
1
2
3
4
5
6
 struct Student_info {
string name;
double midterm, final;
unsigned int gender;
unsigned int age;
};

​ 这里Student_info为一个具有5个数据成员的类型

  • 关于classstructclass关键字声明的类型在第一个{和保护标识符之间的所有成员都是私有的,struct则相反

  • 自定义的类型只能出现一次,它只能出现在一个头文件中

  • 在类型内部自定义成员函数时,可以直接访问对象内部的元素

  • 如果是在头文件(.h)对应的源文件(.cpp)中定义的成员函数,函数的名字前必须加上Class_name::的作用域限定运算符,以明确函数是Class_name的一个成员函数,否则会出现函数未定义的编译错误

  • 将成员函数定义在类内部与类外部的区别:在类内部定义的成员函数,实际就是把函数的调用扩展成内联的,这样可以提高程序运行效率

  • 成员函数如果不是定义在类内部,必须在相对应的源文件(.cpp)中定义,否则会产生重复定义的问题(模板类除外

  • 如果在自定义类与全局环境中有同名函数,在函数名字前加上::(该运算符左侧没有内容)代表使用的是全局环境中的那个函数

  • 如果需要访问一个私有的数据成员,可以定义一个存取器函数(如下),但是它会破坏类的封装性:

    1
    std::string name() const { return name; }
  • 对声明为static的成员函数,它只能在类中定义

构造函数

构造函数是类的一个特殊的成员函数,它定义了对象的初始化方式。它的名字是类的名字且没有返回值,它会在创建一个自定义类型的对象时被自动调用。

  • 如果没有自定义构造函数,那么编译器会为我们自己创建一个

  • 构造函数列表:在public作用范围内的构造函数的声明

  • 构造函数的定义:

    1
    Student_info::Student_info(double midterm, double final);

    上面定义了一个参数为 midtermfinal的构造函数,下面给出声明与初始化例:

    1
    Student_info::Student_info stu(100, 99);

    上面声明并初始化了一个名为stuStudent_info类型的对象,并设置了其成员数据的值

  • 构造函数默认参数:构造函数的参数同样可以给定其默认值,从而可以进行缺省初始化

  • 缺省构造函数:不带参数的构造函数,如:

    1
    Student_info::Student_info(): midterm(0), final(0) {};

    使用该方式同样可以进行对象的缺省初始化

  • 上面的语句中,:{之间的内容为构造函数初始化程序,程序会用括号中的值初始化相应的数据成员

  • 创建一个对象时的过程:

    1. 分配相对应的内存
    2. 执行构造函数初始化程序
    3. 执行构造函数函数体(在上面的例子中,函数体中没有内容)
  • 不建议在构造函数的函数体中对对象进行初始化,如果这样那就相当于对对象做了两次初始化

  • explicit关键字:函数名前添加了此关键字的构造函数只能在被显式调用时才能使用

析构函数

析构函数是在一个对象被删除的时候被调用的,它来定义如何删除一个对象的示例。

  • 析构函数的函数名是在类的名字前面加上~
  • 析构函数不带参数且没有返回值
  • 定义例:~Student_info() {}

指针

指针是一种随机存取的迭代器。

  • 一个指针是一个存放对象地址的值,一个对象是只包含它本身一个元素的容器,那么指针就是指向这个元素的迭代器
  • 对于一个指向对象x的指针p,有:
    • px的地址
    • *px的内容,也即有*p = x,这里 *是一个间接引用运算符
    • &xp的内容,也即有p = &x,这里&是一个取地址运算符
  • 一般使用0初始化指针变量,空指针的内容就是0
  • 指针具有类型,对于一种类型T,可以定义一个指向T类型的对象的指针:T* p;
  • 指针可以通过加减法修改它指向的值
  • 使用ptrdiff_t类型来表示两个指针的间距,它在<cstddef>头文件中
  • 野指针:指向被释放的内存空间的指针
  • 函数指针:
    • 一个指针在声明时如果带上参数,那么他就变成了一个函数指针
    • 示例:int (*fp)(int);,调用fp时以一个int类型的变量作为参数,同时返回一个int类型的结果,这意味着fp是一个具有一个int类型参数并且返回一个int类型结果的函数的指针
    • 如果存在这样的一个函数:int func(int),那么可以用fp = &func;或者fp = func;来让fp指向func
    • 如果fp指向func,那么可以使用(*fp)(i)或者fp(i)的方式调用func,其中i为整型
    • 如果将一个函数名作为另一个函数的参数,那么编译器会将这个参数转换成一个指向那个函数的指针
  • 使用指针时可能导致问题出现的原因:
    • 复制一个指针时不会复制指针所指的对象
    • 删除一个指针不会释放对象所占用的内存
    • 删除一个对象但是没有删除指向该对象的指针会产生一个空悬指针
    • 定义了一个指针但是不进行初始化

数组

数组是最基础的容器,它是核心语言的一部分。

  • 数组元素个数必须在编译时确定,它不能动态增减尺寸
  • 使用size_t类型来表示一个数组的大小,它在<cstddef>头文件中
  • 对于一个有n个元素的数组,只有指向[0,n)的指针是合法的
  • 数组支持索引操作
  • 数组定义的两种方法:
    • 只声明,需要显式地确定元素个数:int num[5];
    • 定义的同时进行初始化,无需显式地确定元素个数:int num[] = {1, 2, 3, 4, 5} ;
  • 字符串实际就是一个字符类型的数组,它的最后一个字符为空字符\0,使用strlen函数可以确定字符串的大小
  • sizeof():返回一个size_t类型的值,其值以bytes为单位,如果需要返回数组的元素个数,那么可以用sizeof()的返回值除数组中每个元素占用空间的大小
  • 如果数组以指针的方式作为函数的参数,那么它会退化成一个指针,这个时候无法通过sizeof()得到指针所指数组的长度,只能得到指针所占空间的大小

内存分配

  • 一般来说有三种分配内存的方法:

    • 自动分配:在使用局部变量时就使用了自动分配内存的策略,用时分配,用完释放
    • 静态分配:使用static关键字限定的对象,系统只会对它进行一次内存分配,直到程序结束才释放
    • 动态分配:使用newdelete关键字分配的对象
  • newdelete:使用new创建的对象一直存在直到对其使用delete或者程序结束

  • 为对象分配或释放内存:

    • new T;:为一个没有名字的T类型的对象分配内存
    • new T(args);:与上面类似,但是内存中存有值args
    • int* p = new int(100);:将一个int类型的指针p指向new开辟的存有100的内存空间
    • delete p;:删除指针p并且释放其指向的内存空间
  • 为数组分配或释放内存:

    • new T[n];:为一个具有nT类型的对象数组分配内存,并且返回一个指向数组首元素的指针,数组中的每一个对象都会被默认初始化
    • 在上面的表达式中如果n0,那么数组中将没有任何元素,并且new无法返回一个有意义的指针
    • T* p = new T[n];:将指针p指向新开辟的数组的首地址
    • delete[] p;:从后向前释放指针p指向的数组所用的全部内存空间,如果索引运算符内添加了索引,那么只释放索引指定的那个内存空间
  • malloc()函数:

    • 库:<stdlib>
    • 参数:size_t类型,为需要分配的内存空间大小
    • 返回一个void* 类型(未知类型)的指针,在使用时需要进行强制类型转换,若失败则返回NULL
    • 使用例:char* cp = (char*)malloc(10);
    • malloc不会对分配的空间进行初始化,而new
  • 内存分区:

    • 堆区:由用户手动申请与释放,若不释放则在程序结束后释放,使用newmalloc申请的内存在此区域
    • 栈区:由系统管理,主要存放函数的参数以及局部变量
    • 静态存储区:在编译时就已经分配好内存空间并且进行初始化,主要存放静态变量、全局变量以及常量
    • 代码区:存放程序体的二进制代码
  • 内存分配器allocator

    • allocator<T>是一个类,它定义在<memory>头中,使用时需要加上std::
    • 在使用allocator<T>类时,需要先将其实例化,如:std::allocator<T> alloc;
    • 在程序中通过调用对象的成员函数来实现内存分配,下面是一些常用的成员函数:
      • T* allocate(size_t):用于分配一块长度为size_t,类型为T但并未被初始化的内存块,并返回这块内存的地址
      • void deallocate(T*, size_t):用于释放未被初始化的内存,第一个参数为allocate函数返回的指针,第二个参数为指针指向的内存块的大小
      • void construct(T*, T):用于对使用allocate函数分配的未初始化的内存进行初始化,第一个参数为allocate函数返回的指针,第二个参数为需要复制的初始值
      • void destroy(T*):用于删除指针所指的对象,它会调用析构函数
  • 另外两个内存分配中可能有用的库函数:

    • T* std::uninitialized_copy(T*, T*, T*):用于将前两个参数指针所指的区域中内存的值复制到第三个参数指针指向的目标内存块中,并且返回一个指向被初始化的内存中的末元素后一个元素的地址
    • void std::uninitialized_fill(T*, T*, const T&):用于向前两个参数指针所指的区域的内存中复制第三个参数所引用的值

泛型函数

  • 含义:不知道参数和返回值类型的函数,用于解决一类抽象问题
  • 函数中使用的变量必须支持函数中所进行的操作

模板函数

泛型函数的具体实现。模板函数可以让不同的对象享有共同的行为特性,在定义模板函数的时候我们不知道模板参数对应的特定类型。下面给出了定义模板函数的示例:

1
2
3
4
5
template <class T>
T function(vector<T> v)
{
//函数内容...
}
  • template <class T>定义了一个模板头,它告诉系统环境定义了一个模板函数,而且这个函数有一个类型参数
  • 如果要使用嵌套类型(也即由模板参数定义的类型),那么必须要使用typename关键字,如:typedef typename vector<T>::size_type vec_sz;,使系统将这个名称当做一个类型处理
  • 模板函数的实例化过程在不同系统下有所不同
  • 模板函数的正确性是由参数之间的正确关系决定的,如果参数之间产生了可能导致歧义和信息丢失的关系,那么这个函数的程序很有可能是有错误的,甚至无法通过编译
  • 在编写模板函数的时候,为了使函数可以处理存储于各种数据结构中的数值,并且能够作用于容器的一部分而非整个容器,一般我们使用迭代器参数而不是容器参数

迭代器的类型

下面的迭代器每一种都对应了一个特定的迭代器操作集合,每一个迭代器种类还对应了一个访问容器的策略,同时也对应了特定的算法。在编写模板函数时需要注意不同迭代器的选择。

  • 输入迭代器In:该种迭代器对一个序列提供了顺序只读访问的操作,它支持的操作有:++==!=*(读)
  • 输出迭代器Out:该种迭代器对一个序列提供了顺序写入的操作,它要求调用它的程序不可以在对迭代器的两个赋值之间执行超过一次的自增操作,也不能在没有对迭代器递增时对其多次赋值
  • 正向迭代器For:该种迭代器对一个序列提供了顺序读-写访问的操作,它可以在对一个元素赋值后再读取它的值,因此不需要满足输出迭代器的一次赋值要求,它支持的操作有:*(读写)++==->,所有的标准库容器都满足正向迭代器的要求
  • 双向迭代器Bi:该迭代器对一个序列提供了可逆读-写访问的操作,它不仅有正迭代器的功能,还支持--(也即逆序访问)的操作,所有的标准库容器都支持双向迭代器
  • 随机迭代器Ran:该迭代器对一个序列提供了随机访问的操作,它的行为有点类似索引,不仅有双向迭代器的功能,而且支持算术运算,若pq是两个迭代器,那么它们支持:p + np - nn + pp - q(得到的是两个迭代器之间的距离,整型)、p < qp > qp >= q,向量和字符串迭代器都是随机访问迭代器,而链表迭代器则是双向迭代器

模板类

模板类用于生成一个类,这个类生成的多个对象可以分别存储不同类型的数据。模板类相当于定义了存储自定类型的数据结构的一个容器。

  • 声明例:

    1
    2
    3
    4
    5
    6
    template <class T> class Name {
    public:

    private:

    };
  • 上面的语句声明了一个名字为Name,可以存储并处理T类型数据的模板类,这里的T类型会在将这个类实例化的时候确定,如Name<int> N;使用Name模板创建了一个Tint类型的对象

  • 模板类的声明和实现必须全部写在头文件中

  • 模板类的成员函数如果需要在类外部定义,那么为了将函数的作用域明确为在类的内部,需要做以下更改:

    • 在函数的定义前面加上template <class T>
    • 在函数名前面添加Name::
    • 在所有的Name后面添加<T>
  • 自定义模板类的时候一定要全面地考虑其初始化、内存分配、复制的问题,必要时可以自定义这些操作

  • 模板类的构造函数例:

    • Name() {}:最普通的无参构造函数
    • Name(size_type n, const T& val = T()) {}:该构造函数的参数为一个指定了对象大小的size_type类型的参数n,以及提供了初始化的默认值的const T&的参数,它的缺省值由T的构造函数提供
  • 可以使用typedef关键字来自定义类型名,如:typedef T* iterator;

  • 运算符重载:在自定义类中可以对内置的运算符进行重载以实现需要的功能,如:

    1
    2
    T& operator[](size_type i) { return data[i]; }
    const T& operator[](size_type i) const { return data[i]; }
    • 运算符重载的声明有点像定义了一个operator"运算符"函数
    • 如果需要使运算符不改变对象的值,那么就在运算符重载的定义前面以及函数体前面加上const关键字
    • 如果某个重载运算符函数是类的一个成员函数:
      • 它的左操作数(二元运算符)或者唯一的操作数(一元运算符)必须是调用它的对象
      • 它的左操作数(二元运算符)会以this指针的方式默认地传递给这个成员函数
    • 如果某个重载运算符函数不是类的一个成员函数,那么重载运算符函数的第一个参数是左操作数,第二个参数是右操作数
  • this指针:this关键词只在成员函数内部有效,代表指向函数操作对象的指针,指针的类型由函数的操作对象决定

  • 复制控制:

    • 复制构造函数:用于复制一个已经存在的同类型的对象,以此来初始化一个新的对象,它只带一个与类本身类型相同的参数,并且由于复制不应该改变已有对象的值,因此它的类型应该为常量引用类型
    • 复制构造函数的函数体应该为自定义的复制操作,如果函数体为空,那么将使用系统默认的方式进行复制
    • 在复制对象时,由于对象的副本的地址与原对象不同,因此不可以将原对象中指向原对象本身的指针同时复制过去,应当重置这些指针使得它们对新对象有意义
  • 赋值运算符:

    • 赋值操作函数:把一个对象中已经存在的值擦除,然后装入一个新的值
    • 类中可以重载赋值运算符,如:Name& operator=(const Name& n) {}
    • 这里的参数为右操作数的一个引用
    • 在函数体内可以通过该语句判断是否是自我赋值:&n != this,该语句比较了左右操作数的地址,如果两者相等则为自我赋值
    • 赋值与复制的区别:复制是将一个值插入一个新的对象,不需要删除操作
    • 构造函数只控制初始化操作,operator=函数只控制赋值操作
  • 在使用=为变量赋初始值时调用的是赋值构造函数,在赋值表达式中=调用的是赋值操作函数

  • 如果一个类没有显式地定义构造函数或者析构函数,那么编译器将自动生成执行这些操作的默认函数

  • 默认的析构函数在删除一个指针变量时不会释放该指针指向的对象占用的内存空间

  • 三位一体原则:如果类需要一个析构函数,那么它同时有可能需要一个复制构造函数和一个赋值操作函数

  • 友元函数:如果一个函数定义在类的声明外部,但是又需要它具有访问private成员数据的权限,那么可以在类内部声明这个函数时,前面加上friend关键字,将其声明为一个友元函数

  • 模板特化:只有当模板类的实际的对象类型T为指定类型时,才调用指定类型对应的模板成员函数,声明与定义如下:

    1
    2
    3
    4
    5
    template<>
    int function(int var)
    {
    ...
    }
  • 模板函数与模板类都是在编译期确定类型的,如果不使用就不编译,编译器的工作就是帮你实现复制粘贴多个不同类型的函数或类

对象的初始化

  • 初始化与赋值:
    • 初始化:在创建一个变量时赋予它一个初始值
    • 赋值:将对象的当前值擦去,并且用一个新值替代
  • 下列情况发生时会进行初始化:
    • 声明一个变量
    • 函数入口处用到函数参数
    • 函数返回处用到函数返回值
    • 构造初始化
  • 通过显式地调用构造函数进行初始化称为显式初始化,否则称为隐式初始化
  • 使用=初始化一个对象称为拷贝初始化,不用等号则是直接初始化
  • 如果对象没有进行显式初始化,则会进入隐式初始化状态,在隐式初始化状态下会进行缺省初始化过程
  • 关于不同种类对象的缺省初始化方式:
    • 全局或者静态的对象:空串或者0
    • 对象为局部的内部类型:未定义的值
    • 内部类型作为类的成员:在某些条件下会被数值初始化为0
    • 自定义类型且有构造函数:使用构造函数初始化
    • 自定义类型且无构造函数:递归地对对象中的每一个数据成员进行相应的数值初始化或者缺省初始化

类型转换

  • 自动类型转换:
    • 自动转换的例子很常见,如将int类型的右值赋给double类型的左值时,该右值就会被自动转换为double类型
    • 自动类型转换分为下面两种情况:
      • 内置类型向自定义类型转换
      • 自定义类型向内置类型转换
    • 内置类型转换为用户自定义类型:需要在自定义类内部编写带单参数的构造函数
      • 例如:Name(const char* cp) { //将参数提供的内容存入临时的Name类型的对象中 },对Name类型,该构造函数会在编译器需要一个Name类型的对象却收到了一个const char*类型的对象的时候被调用
    • 用户自定义类型转换为内置类型:需要在自定义类内部编写如下例所示的成员函数
      • 例如:operator int() { //将自定义类型转换为int类型 },该函数会在显式强制转换或者对象传入的值类型不符时被调用,函数的返回值由其类型名确定
  • 若有两个类AB,类A中有参数类型为B的构造函数,类B中有强制转换为类A的操作符函数,这样会导致转换时出现二义性
  • 混合类型表达式:混合类型表达式意为在一个语句中使用运算符同时操作不同类型的对象的表达式,它在实现的过程中会进行多次类型转换,同时产生大量的临时变量,比较浪费内存

引用

  • 形如int& i;的变量即为引用变量,左侧的表达式是对其进行声明的过程
  • 如果存在一个整型变量temp,那么令i = temp;,则i指向的内存位置与temp相同,i相当于是temp的一个别名
  • 引用作函数参数:
    • 非常量引用:int function(vector<string>& count);,形式参数count存放的是实际参数的地址(&的意思有一些像取地址,但是二者是有区别的),可以直接在函数内更改实际参数的值,而且不需要重新复制一个变量,减少资源消耗
    • 常量引用:int function(const vector<string>& count),常量引用只能藉由实际参数的地址读取它的内容且无法进行修改,该操作同样不需要重新复制变量
    • 一般来说,对于int或者double类型的参数可以不需要引用,因为对它们的复制是非常快的
  • 值类型作函数返回值:
    • 对于值类型的返回值,编译器会先将这个值复制到一个临时变量内并且返回这个临时变量
    • 函数调用完成,这个临时变量在被使用一次后(也可能不被使用),它即被销毁
  • 引用作函数返回值:
    • 引用作函数返回值的时候,不可以引用局部变量,因为函数调用完成后局部变量就被销毁,这个引用就无效了
    • 当引用作函数返回值时,不需要将变量重新复制一次保存在临时变量中
    • 引用作返回值时可以作为左值

继承

基本概念

  • 定义:

    1
    2
    3
    4
    5
    6
    7
    class A {
    public:...
    private:...
    };

    class B: public A {
    };
    • 上面定义了一个类B,它是从A中继承而来,称A是基类(或者父类),B是继承类(派生类、子类)
    • 这里派生类继承了基类的公有部分,也即基类的公有成员才是子类的一部分
    • 同样可以使用private或者protected关键字来指定继承的方式
  • 继承关系是可以嵌套的

  • 基类和继承类的关系:基类就是继承类,继承类不是基类,对于他们的引用或者指针同样有这个关系

  • 基类不可以强制转换为派生类

  • 关于基类或者继承类作为函数参数:

    • 形式参数与实际参数匹配的情形不讨论
    • 如果形式参数是基类类型的,传入一个继承类的对象会将其转换为基类
    • 如果形式参数是继承类型的,那么实际传入的对象只有继承类的部分
  • protected:保护标识符,它指示类中的保护成员,类的保护成员可以被其继承类访问

  • 如果需要显式地表明某些成员是继承来的或者明确指明调用基类或者继承类的成员函数,可以使用范围(作用域)运算符

  • 构造初始化器:也即构造函数,在使用构造初始化器时直接将相应的参数填入构造函数中即可,编译器会根据参数的类型匹配相应的构造函数

  • 派生类对象构造的步骤:

    • 为整个对象分配内存空间
    • 隐式(或者显式)调用基类的构造函数来初始化对象中的基类部分数据
    • 用构造初始化器对派生类部分的数据进行初始化
    • 如果有的话,执行派生类构造函数的函数体
  • 继承关系中成员函数的覆盖:如果基类与继承类中有两个同名同参同返回值类型的非const函数,那么派生类的这个函数会覆盖基类的函数,当基类与继承类的函数都返回指向他们本身类型的指针时除外

虚函数

  • 定义:在基类中定义成员函数时前面加上virtual关键字,在继承类中同名同参函数的virtual的特性也会被继承,因此不需要重复加
  • 虚函数必须要有定义,不能只有声明
  • 功能:由于继承类也是基类,因此当以引用或者指针的形式调用成员函数时,程序根据实际的对象类型来在基类与继承类中选择正确的同名成员函数,也即动态绑定
  • 静态绑定:在编译时就确定调用的对象的成员函数的种类
  • 多态:用一个类型表示多种类型的能力,这里动态绑定实现了多态
  • 只有在以引用或者指针的形式调用虚拟函数的时候在运行时选择类型才有意义
  • 虚拟析构函数:当使用指针来控制对象时,调用delete函数会调用这个虚拟析构函数,一般虚拟析构函数都是空的
  • 如果基类与继承类中有同名不同参的函数,那么他们之间毫无关系,不需要用到虚函数的技术
  • 如果在构造函数内部调用虚函数,那么它会静态绑定成正在构造的对象所属于的类中的那个虚函数

代理类

  • 代理类实际上就是一个普通的用户自定义类,它的成员数据是指向基类的指针
  • 功能:存储和管理基类指针,这个基类指针既可以指向基类对象又可以指向派生类对象,使用代理类可以实现动态绑定,它将基类和派生类中的细节封装起来,提供了一个统一的接口
  • 代理类中一般包含这些实现细节:
    • 各种构造(析构)函数与复制控制函数:用于提供分配或者销毁内存空间的操作
    • 基类和继承类中共有的操作:提供操作接口,实现动态绑定的功能
    • 其他操作
  • 关于代理类的复制操作:需要在它管理的基类和继承类中定义虚拟的复制控制函数,并且在代理类的复制操作中调用它,在复制时有必要判断指向被复制对象的指针是否为空指针
  • 友元类:将一个类声明为另一个类的友元可以使另一个类中的所有成员都成为那个类的友元

句柄类

  • 功能:上面提到的代理类可以视为一种简单的句柄类,但是它是通过复制对象来实现动态绑定的,句柄类的最主要功能就是实现在不用复制对象的情况下就实现动态绑定并且自动管理内存
  • 一般来说,句柄类是一个模板类,因为它与它操作的模板之间是相互独立的,句柄类具有通用性
  • 与代理类相似,句柄类要将被控制的类的全部接口封装起来
  • 复制操作:在复制时需要将该句柄类的指针当前所指向的对象删除后再重新将指针指向被复制的对象的地址
  • 使用方法:在实际的程序中,通常使用一个接口类来管理基类与继承类,接口类的成员数据是一个句柄类,接口类通过操作句柄类来控制基类与继承类
  • 接口类的构造函数必须对句柄类做初始化

计数句柄类

  • 相比于一般的句柄类,计数句柄类在复制对象时会有选择的对底层的对象进行复制,这意味着计数句柄在复制时并不总是将底层对象的内容重新拷贝一份,而是让多个复制出来的句柄的指针同时指向同一个底层对象
  • 为了实现上面的功能,需要在句柄类添加一个引用计数指针,它用来记录有多少个对象指向底层对象
  • 复制构造函数:计数句柄类的复制构造函数不需要复制底层的对象,它的基本操作如下:
    • 对右操作数所指向的对象的引用计数指针加一,表示有一个新的计数句柄类指向同一个底层对象
    • 如果当前左操作数的引用计数指针值为1,那么意味着没有其他句柄指向这个底层对象了,复制操作会将左操作数的指针覆盖掉,所以这里将左操作数指向的对象和其计数指针所占的内存空间释放掉
    • 将右操作数的对象指针和引用技术指针的复制到左操作数中
  • 析构函数:同样要考虑当前的句柄类是不是最后一个指向该对象的句柄,具体操作不再赘述
  • 在修改句柄类指向的对象的值时,亦需要考虑当前的句柄类是不是最后一个指向该对象的句柄,如果是的话,那么直接修改这个指针指向的对象的值即可,否则要先将当前所指向的对象的引用计数减一,然后基于旧对象复制一个新对象并将指针指向它,并将它的引用计数设为一,上述的操作可写为成员函数并在接口类中调用

抽象基类

  • 概念:抽象基类是一种接口类,它用于提供一类对象的基本特征,然后再通过由其派生初来的派生类来实现可以归入该类对象下的其他特定类型的对象的详细特征
  • 一般来说,抽象基类使用纯虚函数来实现这种特性
  • 抽象基类可以使用句柄类来自动管理内存

纯虚函数

  • 纯虚函数的函数体为= 0,它没有具体的实现,声明例如下:

    1
    virtual void display() = 0;
  • 含有纯虚函数的类就是一个抽象基类,它不能有一个实际的对象

  • 纯虚函数在抽象基类的派生类中得到实现

  • 如果在派生类中继承而来的纯虚函数没有定义,那么它仍然是一个纯虚函数

输入与输出

  • 标准输出流:

    1
    std::cout << "output" << std::endl;
    • 上面的语句中,std为命名空间,命名空间是为了区分在不同的库中,两个名字相同但是功能不同的函数

    • ::为作用域运算符

    • <<为标准库的输出运算符

    • std::cout为标准输出流

    • std::endl为输出的结束,该控制器会刷新输出缓冲区,并且进行换行,如果没有使用此控制器或者人为添加换行符,那么输出内容不会换行

    • 控制器:在流中的控制器用于控制这个流

    • 两个以上的字符串如果只是由空格分开,在输出流中它们会自动连接起来

    • 控制器setprecision(x):用于为后续输出设定x位有效位数,一般可以这样使用(使用完后注意重置为默认值):

      1
      2
      3
      4
      //先保存初始的输出精度
      streamsize prec = cout.precision();
      //在输出流中调整输出精度为x后再调整回默认值
      cout << setprecision(x) << grade << precision(prec);
    • 注意,在输出流中如果存在多个步骤,如输出流中调用了函数,即使程序仍然会按照流的顺序输出内容,但是程序的执行顺序有可能不是按照流的输出顺序执行的,这样有可能造成程序逻辑上的混乱

    • setw(length)函数的使用:

      • 使用例:cout << setw(10) << name << endl;
      • 属于控制器,用于控制输出字段的长度
      • 当后面紧跟着的输出字段长度小于length的时候,在该字段前面用空格补齐,当输出字段长度大于length时,全部整体输出
      • 默认是右对齐,如果需要左对齐,可以在其前面添加left控制器
  • 标准输入流:

    1
    std::cin >> name;
    • cin为标准输入流,>>运算符将输入的东西保存到name中,直到遇到空白字符或者EOF标记
    • 空白字符:空格,制表键,换行符,回退键
    • EOF:在Windows系统下,EOF为CTRL+Z,在Linux或者Unix系统下则为CTRL+D
    • 输入时cin会略去首先输入的空白字符
    • cin >> a >> b;等价于cin >> a; cin >> b;
  • 循环输入:

    1
    2
    3
    while (cin >> x) {
    //循环内容
    }
    • 原理:输入首先会保存在缓冲区,然后直到按下回车,输入会以空白字符为界,按顺序将一串数据一个一个存储到变量x中,并且每存储一次就执行循环中的内容,直到遇到非法输入(类型与x不同)或者EOF退出循环
  • 缓冲区:输出/输入库会将输出保存在缓冲区中,只有在必要的时候,缓冲区的内容才会被送到输出装置中,从而将多个输出操作合为一个

  • 缓冲区刷新的几种情形:

    • 缓冲区满
    • 程序请求库从标准输入流中读取数据
    • 手动刷新缓冲区
  • 流迭代器:

    • <iterator>中定义,使用时必须指明类型
    • 输入流迭代器istream_iterator<Type>(cin):迭代器的参数是可选的,通常情况下将其与cin连接,从这里读值,该迭代器缺省值的性质是,一旦到达了文件末尾或者处于错误状态,那么这个迭代器会与缺省值相等
    • 输出流迭代器ostream_iterator<Type>(cout, string):迭代器的第一个参数为cout,说明迭代器会往cout中写内容,第二个参数指定了一个值(一般是字符串),这个值会写在每个元素之后,这个参数也可以缺省
    • 流迭代器的本质是模板
  • 头文件<cctype>中的几个有用的函数:它们在字符c满足后面的条件时返回True

    • isspace(c):空白
    • isalpha(c):字母
    • isdigit(c):数字
    • isalnum(c):数字或者字母
    • ispunct(c):标点符号
    • isupper(c):大写字母
    • islower(c):小写字母
  • 标准错误流:

    • cerr:即时输出错误信息,保证一旦发生异常就立刻输出信息
    • clog:倾向于生成日志,它具有着与cout一样的缓冲特性,平时存储错误信息,在适当的时候输出
  • 文件的输入与输出:

    • ifstream:是一个用于读取文件内容的对象的类型

    • ofstream:是一个用于向一个文件输出内容的对象的类型

    • 上面两个类型定义在<fstream>中,使用时需要先将这两个类型的对象实例化,并且将字符串形式的文件名作为参数

    • 在使用这两个类型的对象时有点类似于istreamostream

    • 使用例:

      1
      2
      3
      4
      5
      6
      ifstream infile("in");	//定义了一个与in文件相关联的文件输入流
      ofstream outfile("out"); //定义了一个与out文件相关联的文件输出流
      string s;
      while (getline(infile, s)) {
      outfile << s << endl;
      }
    • 注意:文件的路径如果使用的是相对路径,那么在调试时是工作区的目录的相对路径,在直接运行程序时则是相对于可执行文件所在位置的相对路径

流程与异常控制

不变式

不变式可以理解为,一个类或者对象在设计的时候,对它的假设条件,在它的整个生命周期内,这些必须恒为真。例如:

1
2
3
4
5
//这里假设loop已经输出的行数
int loop = 0;
while (loop != rows) {
//输出一行...
}

这里int loop = 0;就是一个不变式,它在生命周期中,必须时刻等于输出的行数,否则会导致逻辑和程序流程控制的混乱,由于在这里不变式作为循环的一个判断依据,因此也被称为循环不变式

异常控制

异常控制用于给程序的使用者给出错误提示,提高程序的鲁棒性。程序抛出异常后,会在抛出异常的地方终止执行并且跳转到程序的另一部分(有点类似于中断),并向该部分提供一个异常对象,异常对象中含有程序可以处理异常信息。例如:

1
2
3
4
5
6
7
8
try {
//该部分中任何一个语句发生了domain_error异常,都会停止执行下面的其他语句,转到执行catch下的语句
//如果没有发生异常,那么将不会执行catch中的语句
} catch (domain_error e) {
//发生异常后执行的语句
//创建了一个异常对象e,名为what的成员函数用到了被复制下来的异常参数
cout << e.what();
}

应当在编程时加入判断,是否满足产生异常的条件,如:

1
2
3
if (something == 0) {
throw domain_error(“Invalid Value!”);
}

上面的语句意为,当满足条件时,程序会抛出一个domian_error,这种错误类型为域错误,即输入在定义域外的情况。C++中还有许多其他的错误类型可供使用,如logic_errorruntime_error等。

多文件编程

  • 文件种类:

    • main.cpp:主函数所在的文件
    • file_x.h:头文件
    • file_x.cpp:其他源代码文件
      ……
  • 头文件的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #ifndef FILE_X_H
    #define FILE_X_H

    //#ifndef必须位于文件的第一行
    //不要在头文件中使用using声明,为用户保留最大的灵活性
    //在头文件中包含其对应的源文件中所有需要用到的库
    //在头文件中声明对应源文件中定义的函数

    #endif
  • #include <系统环境提供的头文件>#include “自己写的头文件.h”

部分库函数

sort()

  • 库:<algorithm>

  • 参数:

    1. 迭代器,序列的起始
    2. 迭代器,序列的末尾
    3. 谓词,可选参数,是自定义的比较方法
  • 功能:用于比较向量中的内容并按顺序存放,在未注明的情况下,对任何待排序的向量元素类型,它都会使用<来操作

  • 关于谓词:是一个函数,返回需值是布尔值

getline()

  • 库:<string>
  • 参数:
    1. 输入流,一般为cin
    2. 字符串引用,直接填目标字符串的名字
  • 功能:用于读一行的内容并将这些内容存储到目标字符串中
  • 如果遇到文件结尾或者无效输入,getline()的返回值会指示失败

copy()

  • 库:<algorithm>
  • 参数:
    1. 迭代器,指示需要复制的元素的开头
    2. 迭代器,指示需要复制的元素的末尾后一个元素
    3. 迭代器适配器
  • 功能:将[b,e)之间的元素按迭代器适配器指定的方式添加到目标容器的末尾

equal()

  • 库:<algorithm>
  • 参数:
    1. 迭代器,指示一个序列的开头
    2. 迭代器,指示一个序列的末尾
    3. 迭代器,指示第二个序列的开头(或者末尾)
  • 功能:从第二个序列的开头(或者末尾)开始,按顺序一个元素对应一个元素地比较两个序列中的内容,如果相等的话则返回True

find()

  • 库:<algorithm>
  • 参数:
    1. 迭代器,指示一个序列的开头
    2. 迭代器,指示一个序列的末尾
    3. 被查找的值
  • 功能:在指定的序列中查找是否存在第三个参数的值,如果找到了,则返回序列中第一次出现这个值的位置,否则返回这个序列的末尾

find_if()

  • 库:<algorithm>
  • 参数:
    1. 迭代器,指示被查找的序列的开头
    2. 迭代器,指示被查找的序列的末尾
    3. 谓词,检测自己出参数并且返回一个布尔值
  • 功能:对序列中的每个元素都调用谓词,直到谓词产生一个True的结果,它会返回一个指示被找到的元素的迭代器,如果没有找到,则返回指示被查找序列末尾的迭代器
  • 库:<algorithm>
  • 参数:
    1. 迭代器,指示被查找的序列的开头
    2. 迭代器,指示被查找的序列的末尾
    3. 迭代器,指示查找的目标序列的开头
    4. 迭代器,指示查找的目标序列的末尾
  • 功能:查找第一第二个参数所指示的序列中有没有第三第四个参数所指示的序列,它会返回一个指示被找到的元素的迭代器,如果没有找到,则返回指示被查找序列末尾的迭代器

transform()

  • 库:<algorithm>
  • 参数:
    1. 迭代器,指示待转换序列的开头
    2. 迭代器,指示待转换序列的末尾
    3. 迭代器,存放处理后的元素的容器的开头
    4. 函数,transform()将这个函数作用于待转换序列以获得期望得到的元素
  • 功能:对待转换序列中的元素执行指定函数,并将转换后的结果存储于第三个参数指定的序列中

accumulate()

  • 库:<numeric>
  • 参数:
    1. 迭代器,指示一个序列的开头
    2. 迭代器,指示一个序列的末尾
    3. 单一变量
  • 功能:将序列中的元素之和加上变量中的值后再存储到这个变量中
  • 注意,第三个变量中的值类型会影响最终的结果的类型

remove_if(),remove()

  • 库:<algorithm>
  • 参数:
    1. 迭代器,指示一个序列的开头
    2. 迭代器,指示一个序列的末尾
    3. 谓词,返回一个布尔值
  • 功能:排列序列中的内容,它将视那些使得谓词为True的元素所在的位置为空闲位置,并将后面那些使谓词为False的元素依次放置在空闲位置中,它的返回值是一个迭代器,它指示了那些所有使谓词为False的元素的末尾的位置
  • remove()的前两个参数含义与remove_if()相同,但是第三个参数是一个值,该函数的作用与remove_if()相同,不同之处在于是移动了那些等于第三个参数的元素

partition(),stable_partition()

  • 库:<algorithm>
  • 参数:
    1. 迭代器,指示一个序列的开头
    2. 迭代器,指示一个序列的末尾
    3. 谓词,返回一个布尔值
  • 功能:两个函数的功能都是将谓词作用于序列中的元素,然后将那些使得谓词为True的元素位于序列的头部,返回一个指向第一个使得谓词为False的元素的迭代器,它们的区别在于stable_partition()会保持元素的顺序不变,而partiotion()则有可能导致元素的顺序混乱

rand()

  • 库:<cstdlib>
  • 参数:无
  • 功能:在[0,RAND_MAX)之间返回一个伪随机数,RAND_MAX是一个大整数,它也是在<cstdlib>中定义的
  • 如何返回在某个指定范围内的随机整数:基本思想是将可以利用的随机数分为长度相等的存储桶,存储桶的个数就是我们需要的随机数的最大值,然后再计算一个随机数并且返回它所在的桶的编号,程序如下:
1
2
3
4
5
6
7
8
9
10
11
12
int nrand(int n)
{
if (n <= 0 || n > RAND_MAX) {
throw domain_error("Argument to nrand is out of range. ");
}

const int bucket_size = RAND_MAX / n;
int r;
do r = rand() / bucket_size;
while (r >= n);
return r;
}

编程习惯

该部分的内容很多,涉及范围也很广,因此在这里我只纪录根据我个人实际情况需要注意的要点。更全面和详细的内容可以参考C++ 风格指南 - 内容目录 — Google 开源项目风格指南 (zh-google-styleguide.readthedocs.io)

  • 在编写运行时间较长的程序时,应当在合适的时刻刷新缓冲区
  • 循环的计数习惯:一般循环中的计数变量(假设为i)都从0开始,这样它的含义就比较明确,也即,到目前为止,我们已经操作了i次,此外这样做还有一个好处,那就是我们可以把循环条件设为i != expected_loop_times,这个不等式明确地表明了,当循环退出时,我们已经循环了expectation_loop_times
  • 在定义一个变量来保存特定的数据结构时,应该使用库中为特殊用途而定义的类型,如现在想要顶一个存储字符串长度的变量length,那么可以有:std::string::size_type length;,另外还可以对库中提供的特殊类型重命名,如:typedef std::string::size_type str_size;就是将std::string::size_type这一类型在作用域内使用str_size来指代
  • 在操作容器中的多个元素时,推荐使用左闭右开的表示方法,这个区间中的元素个数就是上下限之间的差,这个区间的右界被称为越界值,这样做的理由有三个:
    • 如果区间没有元素,那么找不到一个最后的元素标记终点
    • 按照上面的方法定义,只要两个迭代器相等,那么立即可以判断区间为空
    • 该方法提供了一种自然的方式表达“区间之外”
  • 在使用标准库提供的容器的时候,尽量使用对应的迭代器
  • 一般都使用double类型来进行浮点数计算
  • 在编写程序时,需要预见一些异常或者无意义的情况,在这种情况下程序最好暂时终止或者退出
  • 函数形式参数的名称最好不要太具体,因为某个函数可以用于多种含义不同的数据(这个思想对编写模板函数很重要)
  • 在输出时如果需要在输出的内容之间插入分隔符(如空格、逗号等),应当对第一个元素做特殊处理,具体思想是:当第一个元素生成(或者确保它已经存在)之后,输出第一个元素,然后再判断之后的元素是否存在,如果存在的话,就将它们与分隔符一起输出
  • 对于不希望改变对象内容的函数,应当将它们声明为const类型
  • 如果一个函数会改变一个对象的状态,那么这个函数应该作为对象的成员函数
  • 在设计二元运算符的时候,为了能够让左右操作数都可以进行类型转换以保持操作数的对称性,尽量将二元运算符定义在类的声明之外
  • 对于在创建对象时调用的构造函一般不用声明为explicit,而对于用于构造新的对象的构造函数需要声明为explicit
  • 不可以让用户用指针直接访问类中的私有成员
  • 使用句柄类时,当成员函数会改变底层对象的值时需要酌情进行复制操作,以保证指针的有效性
Author

Astrobear

Posted on

2021-09-16

Updated on

2025-04-18

Licensed under


Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×