Accelerated C++笔记
虽然两年前就已经接触过C++,但是之前短暂的学习根本不足以让我系统掌握这门语言的用法。现在由于有项目开发的需求,我决定重新开始较为系统地学习C++。由于有C语言的基础,在入门阶段我使用Accelerated C++这本书来学习。这本书虽然较为老旧,但是内容不多,读起来也比较轻松。这篇文章是对我在看书和编程时遇到的问题和重点的总结备忘,内容比较零碎,还请大家谅解。
语言特性
基础知识
声明:向编译器引入或重新声明一个名字及其类型等信息;声明不一定是定义
定义:完整定义一个实体;对变量而言,定义会创建对象并在需要时分配存储空间。模板实例化是另一个概念
main函数返回一个整数类型的值作为结果,0表示成功void类型的函数可以不需要return语句,也可以写return;变量是由声明引入的具名对象或引用。对象是一块具有类型和值的存储区域;函数、引用和类型本身不属于对象
在花括号中定义的变量,作用域只在它所在的花括号中
对象类型还有接口,接口就是可以对对象进行的操作的集合
重载:
- 运算符重载:为自定义类型赋予运算符含义,例如为复数类型定义
operator+。1 + 1与'A' + 'B'使用的都是内置加法,后者会先进行整数提升 - 函数重载:两个功能不同的函数在具有相同名称时,当它们的参数不同时,会发生重载
- 运算符重载:为自定义类型赋予运算符含义,例如为复数类型定义
关于
const:const表示不能通过该名字修改对象。const标量通常必须显式初始化;const类类型也可以由可用的默认构造函数完成初始化。非常量表达式可以用作const对象的初始值如果将非常量对象传给常量引用参数,只是不能通过该引用修改对象;对象本身不会因此变为常量
不可以对常量对象调用非常量函数
关于
const的位置:修饰指针变量:
1
2
3const int* pt = &n; //指针所指的数据是常量,但是指针是变量,可以指向其他地址
int* const pt = &n; //指针所指的数据是变量,指针是常量,只能指向n的地址
const int* const pt = &n; //指针和其所指的数据都不可以改变修饰函数参数:
1
int func(const int n) { return n; } //函数体内不能修改形参n;顶层const不影响函数接口
修饰成员函数:
1
2int func(int n) const { return n; } //成员函数不能修改除mutable修饰之外的成员变量
//且不能直接调用同一对象的非const成员函数修饰函数返回值:
1
int func(int n) { return n; } //对内置类型按值返回时一般不需要添加const
const修饰成员函数时,根本上是修饰了该函数所在对象的this指针,如果对象有非const和const两个重载函数时,调用哪一个是由对象是否是const决定的所有
const成员函数是一个类的常量接口,该类的所有常量对象只能调用常量接口
关于
static:static在不同上下文中会影响对象的存储期、名字的链接属性,或者表示成员属于类本身而不是某个对象- 静态全局变量:具有静态存储期,通常在
main()开始前完成初始化,并在程序正常终止时销毁;使用static声明时只在当前翻译单元中可见 - 静态局部变量:在函数第一次执行到声明它的语句时进行初始化,它具有全局寿命,但是只在声明它的函数中可见,在单线程时多次调用包含静态局部变量的函数不会导致该静态局部变量被多次定义与初始化
- 静态函数:它只在声明它的文件中可见,其他文件中可以声明名字相同的函数而且不会发生冲突
- 静态数据成员:属于类本身,由该类的所有对象共享。传统写法通常在类外定义;C++17起也可使用
inline static在类内定义并初始化 - 静态成员函数:它没有
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缺省初始化:对象定义时没有提供初始化器便会进行缺省初始化。自动存储期的内置类型对象可能具有不确定值,读取这种值通常会导致未定义行为,不能将其当作可用的随机数
- 全局变量和静态变量会先进行零初始化
- 类类型对象会按照构造函数和成员初始化规则进行初始化
左值与右值
- 左值:具有身份、通常能够取得地址的表达式,例如一个具名变量
- 右值:通常表示临时值或可被移动资源的表达式;现代C++还进一步区分纯右值、将亡值等类别
增量/减量运算符(按照优先级排序):
i++:对i加一,并且返回原始值i--:对i减一,并且返回原始值++i:对i加一,并且返回相加后的值--i:对i减一,并且返回相减后的值
关于
clear()方法:- 对字符串:将字符串长度变为
0,使字符串为空;具体存储方式属于实现细节 - 对向量:清空向量内容
- 对流:清除流内部的错误状态
- 对字符串:将字符串长度变为
泛型算法:可以用于不同类型的容器和不同的数据类型的算法
迭代器适配器:是一个函数,会产生与其参数相关的属性的迭代器以做他用,它会按照它给定的方式来对迭代器进行操作,下面给出了一些例子:
back_inserter(des):将元素添加到des容器的末尾front_inserter(des):将元素添加到des容器的头部(链表支持该种操作,向量和字符串不支持)inserter(c, it):在c的迭代器it之前插入元素
不要将重载函数作为参数传递给模板函数,否则编译器将不知道需要使用的是重载函数哪个版本
将函数作为参数:将函数作为参数时,在声明的时候参数函数的编写形式类似于函数的声明,返回值和参数函数的参数必须与参数函数相一致,但是名称不需要一致
函数的缺省参数:在函数声明中,可以在形参后写
= x来提供默认实参,其中x应是可用于初始化该形参的表达式;调用函数时可以省略相应的尾部实参函数的递归调用:在函数内部调用自身
内联函数:使用
inline说明符的函数可以在多个翻译单元中具有相同定义,因此常用于在头文件中定义短小函数。编译器是否把函数调用展开由优化器决定,与是否显式写出inline没有必然关系,也没有“不能超过10行”的语言限制main函数:- 完整形式:
int main(int argc, char** argv) - 参数:
argv:指向一个数组首元素地址的指针,数组中的每一个元素都是都指向一个字符串参数的指针argc:argv指向的数组中的指针个数
argv的第一个元素指向编译出的程序的名字的字符串的首地址- 在程序中可以直接使用这两个对象
- 完整形式:
NULL,nullptr,NaN,INFNULL:历史上用于表示空指针的宏,其展开结果是实现定义的空指针常量;现代C++应优先使用nullptrnullptr: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[0]或*grade.begin(),最后一个元素是grade[grade.size() - 1]或grade.back();grade.end()指向末元素之后,不能解引用 - 删除向量中的元素:
- 使用
erase()成员函数,如:grade.erase(grade.begin() + i) erase()会在删除掉一个元素的同时,将这个元素后面的元素向前移动一个单位erase()会返回一个迭代器(见下文),并且指向刚刚被删除的元素后面的那个元素
- 使用
- 向量元素的预分配:可以减少重复的内存分配带来的系统开销
v.reserve(n):为向量v预留至少能容纳n个元素的内存空间,但不创建元素,该操作不会改变容器的大小v.resize(n):给v一个长为n的新长度- 新长度小于当前长度:向量位于长度
n后的元素会被删除 - 新长度大于当前长度:向量当前长度后与长度
n前的空余位置会添加上新的元素,并且会进行初始化
- 新长度小于当前长度:向量位于长度
list(双向链表)类型
链表中的每一个元素都有链接信息指向后一个元素,list(双向链表)的每一个元素还可以沿两个方向遍历。list可以在已知迭代器的位置以常数时间插入或删除元素,而且通常不会使其他元素的迭代器失效;但它不支持随机访问,顺序遍历也常因缓存局部性较差而慢于vector。是否选用list应根据访问模式和元素稳定性需求决定。
- 定义示例:
list<double> grade; - 需要使用
<list>头文件 list不支持索引- 具有
sort()成员函数,它有一个可选的参数compare用于提供排序的比较方法,如果不提供该参数则默认使用<来排序
对string类型的新认识
string类型可以视为一种特殊的容器,它支持索引操作,也有相对应的迭代器(类似vector)substr(i, j)成员函数:创建一个新的字符串来保存s在区间[i,i+j)中的索引指示的字符的复制- 在C++中,字符串字面量(如
"hello")的类型是const char[N];它在多数表达式中会退化为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个元素的容器c,c的元素是t的复制container<Type> c(b, e);:定义容器c,其元素是迭代器范围[b,e)中元素的复制,元素数量为该范围的距离c.size():返回c中元素的个数,返回值类型为size_typec.empty():用于指示c中有无元素,如果没有则返回truec.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容器,erase会使被删除位置及其后的迭代器失效;push_back在发生重新分配时会使全部迭代器失效,否则会使原来的末后迭代器失效。对list执行erase只会使指向被删除元素的迭代器失效 - 如果一个容器支持索引,那么它的迭代器也支持索引,如:
iter[2]等价于*(iter + 2),iter[-1]等价于*(iter - 1)
map(映射表)类型
之前提到的向量、链表等容器都是顺序容器,而map则属于关联容器。关联容器的序列不依赖于插入元素时的顺序,它只依赖于元素本身的值。关联容器的特性可以使我们更快地对特定元素定位。
map容器中存储的是一个键-值对- 键:用于唯一标识和查找元素,作用有些类似索引,但它并不是指针
- 值:与该键关联的映射值
- 声明例:
map<string, int> m;,该语句声明了一个名为m,从字符串到整数的映射表,其键的类型为string,值的类型为int map按照比较器维护键的顺序,不能直接修改键,但可以修改映射值,也可以插入或删除元素- 对非常量
map使用operator[]访问一个不存在的键时,会插入该键,并对映射值进行值初始化;对于整数等内置类型,其初始值为0 pair(数对)类型:该类型提供了访问映射表的键和值的能力,映射表中的每个元素都是一个数对- 数对有两个成员,
first成员包含了键,second成员包含了值 - 间接引用映射表迭代器时,获得的就是这个映射表关联的一个数对
pair<const T1, T2> - 注意,映射表的键是常量
- 数对有两个成员,
map的访问方法:m[key],使用键key来索引访问映射表并返回一个左值,如果对于这个键没有一个合适的项目,那么则会新建一个元素map的另一种访问方法:若iter是一个映射表的迭代器,那么iter -> first可以得到数对的键,iter -> second可以得到数对的值- 常量映射表无法使用索引访问
find()成员函数:参数为需要查找的键,如果查找到了则返回一个指向这个元素的迭代器,否则返回映射表的末尾map会按照比较器维护键的顺序。自定义比较器时可写成map<K, V, Compare> m(cmp);,其中Compare是比较器类型,cmp是该类型的对象
自定义类型
作为核心语言的一部分,int、char、double等属于内置类型;vector、string等是标准库提供的类模板或类类型。用户也可以定义新的类,在其中组织数据、成员函数和访问接口,以此实现封装。
- 自定义类的格式:
1 | class Class_name { |
public:访问说明符,它指示类中的公有成员,类的所有用户都可以访问公有成员private:访问说明符,它指示类中的私有成员,类的私有成员对用户来说不能直接访问- 同样可以用
struct关键字来自定义类,但是它与class有所区别,如:
1 | struct Student_info { |
这里Student_info为一个具有5个数据成员的类型
关于
class和struct:class关键字声明的类型在第一个{和保护标识符之间的所有成员都是私有的,struct则相反类通常定义在头文件中,并可通过包含该头文件出现在多个翻译单元;各处定义必须保持一致并满足单一定义规则
在类型内部自定义成员函数时,可以直接访问对象内部的元素
如果是在头文件(
.h)对应的源文件(.cpp)中定义的成员函数,函数的名字前必须加上Class_name::的作用域限定运算符,以明确函数是Class_name的一个成员函数,否则会出现函数未定义的编译错误在类内部定义的成员函数通常隐式具有
inline属性,适合放在头文件中;编译器是否展开函数调用仍由优化器决定非模板成员函数常在相应的源文件(
.cpp)中定义。也可以在头文件中类外定义,但应使用inline等方式避免违反单一定义规则如果在自定义类与全局环境中有同名函数,在函数名字前加上
::(该运算符左侧没有内容)代表使用的是全局环境中的那个函数如果需要访问一个私有的数据成员,可以定义一个存取器函数(如下),但是它会破坏类的封装性:
1
std::string name() const { return name_; }
静态成员函数必须在类中声明,但可以在类外定义;类外定义时不再写
static关键字
构造函数
构造函数是类的一个特殊的成员函数,它定义了对象的初始化方式。它的名字是类的名字且没有返回值,它会在创建一个自定义类型的对象时被自动调用。
如果类没有声明任何构造函数,编译器通常会隐式声明默认构造函数;它是否可用还取决于基类和数据成员能否被默认构造
构造函数列表:在
public作用范围内的构造函数的声明构造函数的定义:
1
2Student_info::Student_info(double midterm, double final)
: midterm(midterm), final(final) {}上面定义了一个参数为
midterm和final的构造函数,下面给出声明与初始化例:1
Student_info stu(100, 99);
上面声明并初始化了一个名为
stu的Student_info类型的对象,并设置了其成员数据的值构造函数默认参数:构造函数的参数同样可以给定其默认值,从而可以进行缺省初始化
缺省构造函数:不带参数的构造函数,如:
1
Student_info::Student_info(): midterm(0), final(0) {};
使用该方式同样可以进行对象的缺省初始化
上面的语句中,
:与{之间的内容为构造函数初始化程序,程序会用括号中的值初始化相应的数据成员创建一个对象时的过程:
- 分配相对应的内存
- 执行构造函数初始化程序
- 执行构造函数函数体(在上面的例子中,函数体中没有内容)
不建议在构造函数的函数体中对对象进行初始化,如果这样那就相当于对对象做了两次初始化
explicit关键字:用于阻止构造函数参与不期望的隐式类型转换,但仍可通过直接初始化等显式方式调用
析构函数
析构函数在对象生命周期结束时被调用,用于释放对象管理的资源并完成清理工作。
- 析构函数的函数名是在类的名字前面加上
~ - 析构函数不带参数且没有返回值
- 定义例:
~Student_info() {}
指针
原始指针可以像随机访问迭代器一样遍历同一数组中的元素,但指针还可用于表示单个对象的地址、空地址等。
- 一个指针是一个存放对象地址的值,一个对象是只包含它本身一个元素的容器,那么指针就是指向这个元素的迭代器
- 对于一个指向对象
x的指针p,有:p:x的地址*p:x的内容,也即有*p = x,这里*是一个间接引用运算符&x:p的内容,也即有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为整型 - 如果将一个函数名作为另一个函数的参数,那么编译器会将这个参数转换成一个指向那个函数的指针
- 使用指针时可能导致问题出现的原因:
- 复制一个指针时不会复制指针所指的对象
- 仅让指针变量离开作用域不会释放它所指向的动态对象;使用
delete才会销毁相应对象并释放存储 - 删除一个对象后,仍保存原地址的指针会成为悬空指针
- 定义了一个指针但是不进行初始化
数组
数组是最基础的容器,它是核心语言的一部分。
- 数组元素个数必须在编译时确定,它不能动态增减尺寸
- 使用
size_t类型来表示一个数组的大小,它在<cstddef>头文件中 - 对于一个有
n个元素的数组,可以合法形成指向各元素以及末元素之后位置的指针,即位置范围[0,n];只有[0,n)内的指针可以解引用 - 数组支持索引操作
- 数组定义的两种方法:
- 只声明,需要显式地确定元素个数:
int num[5]; - 定义的同时进行初始化,无需显式地确定元素个数:
int num[] = {1, 2, 3, 4, 5} ;
- 只声明,需要显式地确定元素个数:
- C风格字符串是以空字符
\0结尾的字符序列,常以字符数组保存,可使用strlen计算终止空字符之前的字符数;std::string则是标准库类型 sizeof():返回一个size_t类型的值,其值以bytes为单位,如果需要返回数组的元素个数,那么可以用sizeof()的返回值除数组中每个元素占用空间的大小- 如果数组以指针的方式作为函数的参数,那么它会退化成一个指针,这个时候无法通过
sizeof()得到指针所指数组的长度,只能得到指针所占空间的大小
内存分配
一般来说有三种分配内存的方法:
- 自动分配:在使用局部变量时就使用了自动分配内存的策略,用时分配,用完释放
- 静态分配:使用
static关键字限定的对象,系统只会对它进行一次内存分配,直到程序结束才释放 - 动态分配:使用
new与delete关键字分配的对象
new与delete:使用new创建的对象一直存在,直到使用匹配的delete销毁它;若遗失最后一个指向它的指针,就会发生内存泄漏,程序结束时也不会为它自动调用析构函数为对象分配或释放内存:
new T;:为一个没有名字的T类型的对象分配内存new T(args);:与上面类似,但是内存中存有值argsint* p = new int(100);:将一个int类型的指针p指向new开辟的存有100的内存空间delete p;:销毁p所指的对象并释放其动态存储;指针变量p本身仍然存在,并会成为悬空指针
为数组分配或释放内存:
new T[n];:为一个具有n个T类型的对象数组分配内存,并且返回一个指向数组首元素的指针,数组中的每一个对象都会被默认初始化- 如果
n为0,数组中没有元素,但new T[0]仍会返回一个可传给delete[]的指针,该指针不能解引用 T* p = new T[n];:将指针p指向新开辟的数组的首地址delete[] p;:销毁并释放p所指的整个动态数组。方括号内不能填写索引,动态数组也不能通过delete[]单独释放某个元素
malloc()函数:- 库:
<cstdlib> - 参数:
size_t类型,为需要分配的内存空间大小 - 返回一个
void*类型(未知类型)的指针,在使用时需要进行强制类型转换,若失败则返回NULL - 使用例:
char* cp = (char*)malloc(10); malloc不会构造对象。new T会执行T的默认初始化,但对int等内置类型仍可能留下不确定值;new T()会进行值初始化
- 库:
内存分区:
- 动态存储区(通常称为堆):由程序申请和释放;若未释放就会造成内存泄漏。操作系统可能在进程结束后回收内存,但不会因此替程序执行对象析构
- 栈区:由系统管理,主要存放函数的参数以及局部变量
- 静态存储区:用于具有静态存储期的对象,如命名空间作用域变量和静态局部变量。它们的存储期贯穿程序运行,但初始化既可能是静态初始化,也可能在运行时完成
- 代码区:存放程序体的二进制代码
内存分配器
allocator:allocator<T>是一个类,它定义在<memory>头中,使用时需要加上std::- 在使用
allocator<T>类时,需要先将其实例化,如:std::allocator<T> alloc; - 在程序中通过调用对象的成员函数来实现内存分配,下面是一些常用的成员函数:
T* allocate(size_t):用于分配一块长度为size_t,类型为T但并未被初始化的内存块,并返回这块内存的地址void deallocate(T*, size_t):用于释放未被初始化的内存,第一个参数为allocate函数返回的指针,第二个参数为指针指向的内存块的大小construct(T*, T):旧版标准中的分配器成员,用于在已分配存储中构造对象。现代C++通常通过std::allocator_traits或std::construct_at完成该操作void destroy(T*):用于删除指针所指的对象,它会调用析构函数
另外两个内存分配中可能有用的库函数:
T* std::uninitialized_copy(T*, T*, T*):用于将前两个参数指针所指的区域中内存的值复制到第三个参数指针指向的目标内存块中,并且返回一个指向被初始化的内存中的末元素后一个元素的地址void std::uninitialized_fill(T*, T*, const T&):用于向前两个参数指针所指的区域的内存中复制第三个参数所引用的值
泛型函数
- 含义:以类型或操作作为参数编写的通用函数,用于解决一类结构相同的问题;其实例化后的参数类型和返回类型仍是确定的
- 函数中使用的变量必须支持函数中所进行的操作
模板函数
泛型函数的具体实现。模板函数可以让不同的对象享有共同的行为特性,在定义模板函数的时候我们不知道模板参数对应的特定类型。下面给出了定义模板函数的示例:
1 | template <class T> |
template <class T>定义了一个模板头,它告诉系统环境定义了一个模板函数,而且这个函数有一个类型参数- 如果要使用嵌套类型(也即由模板参数定义的类型),那么必须要使用
typename关键字,如:typedef typename vector<T>::size_type vec_sz;,使系统将这个名称当做一个类型处理 - 模板函数的实例化过程在不同系统下有所不同
- 模板函数的正确性是由参数之间的正确关系决定的,如果参数之间产生了可能导致歧义和信息丢失的关系,那么这个函数的程序很有可能是有错误的,甚至无法通过编译
- 在编写模板函数的时候,为了使函数可以处理存储于各种数据结构中的数值,并且能够作用于容器的一部分而非整个容器,一般我们使用迭代器参数而不是容器参数
迭代器的类型
下面的迭代器每一种都对应了一个特定的迭代器操作集合,每一个迭代器种类还对应了一个访问容器的策略,同时也对应了特定的算法。在编写模板函数时需要注意不同迭代器的选择。
- 输入迭代器
In:该种迭代器对一个序列提供了顺序只读访问的操作,它支持的操作有:++、==、!=、*(读) - 输出迭代器
Out:该种迭代器对一个序列提供了顺序写入的操作,它要求调用它的程序不可以在对迭代器的两个赋值之间执行超过一次的自增操作,也不能在没有对迭代器递增时对其多次赋值- 正向迭代器
For:该种迭代器对一个序列提供了可重复的顺序访问,它支持*、++、==、!=和->等操作。迭代器是否允许写入还取决于其引用类型,例如set迭代器不能修改键 - 双向迭代器
Bi:该迭代器对一个序列提供了可逆访问的操作,它不仅有正向迭代器的功能,还支持--。list、map等提供双向迭代器,而forward_list和无序关联容器通常只提供前向迭代器 - 随机访问迭代器
Ran:该迭代器的行为类似索引,不仅有双向迭代器的功能,而且支持算术运算。若p和q是同一序列的迭代器,它们支持p + n、p - n、n + p、p - q以及大小比较;p - q的结果类型是迭代器的difference_type。向量和字符串迭代器是随机访问迭代器,链表迭代器是双向迭代器
- 正向迭代器
模板类
类模板用于生成一组具有相同结构、但模板实参不同的类。类模板不一定是容器,也可以表示算法策略、资源管理器或其他抽象。
声明例:
1
2
3
4
5
6template <class T> class Name {
public:
private:
};上面的语句声明了一个名字为
Name,可以存储并处理T类型数据的模板类,这里的T类型会在将这个类实例化的时候确定,如Name<int> N;使用Name模板创建了一个T为int类型的对象模板定义通常需要在实例化点可见,因此常将声明和实现放在头文件中;也可以把实现放在源文件中并显式实例化所需类型
模板类的成员函数如果需要在类外部定义,那么为了将函数的作用域明确为在类的内部,需要做以下更改:
- 在函数的定义前面加上
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
2T& operator[](size_type i) { return data[i]; }
const T& operator[](size_type i) const { return data[i]; }- 运算符重载的声明有点像定义了一个
operator"运算符"函数 - 如果成员运算符不修改对象,应在参数列表后添加
const。返回类型前的const具有不同含义,并不是声明常量成员函数所必需的 - 如果某个重载运算符函数是类的一个成员函数:
- 它的左操作数(二元运算符)或者唯一的操作数(一元运算符)必须是调用它的对象
- 它的左操作数(二元运算符)会以
this指针的方式默认地传递给这个成员函数
- 如果某个重载运算符函数不是类的一个成员函数,那么重载运算符函数的第一个参数是左操作数,第二个参数是右操作数
- 运算符重载的声明有点像定义了一个
this指针:this关键词只在成员函数内部有效,代表指向函数操作对象的指针,指针的类型由函数的操作对象决定复制控制:
- 复制构造函数:用于复制一个已经存在的同类型的对象,以此来初始化一个新的对象,它只带一个与类本身类型相同的参数,并且由于复制不应该改变已有对象的值,因此它的类型应该为常量引用类型
- 如果不需要自定义复制,可省略复制构造函数或将其声明为
= default;显式编写空函数体不会自动执行成员逐一复制 - 在复制对象时,由于对象的副本的地址与原对象不同,因此不可以将原对象中指向原对象本身的指针同时复制过去,应当重置这些指针使得它们对新对象有意义
赋值运算符:
- 赋值操作函数:把一个对象中已经存在的值擦除,然后装入一个新的值
- 类中可以重载赋值运算符,如:
Name& operator=(const Name& n) {} - 这里的参数为右操作数的一个引用
- 在函数体内可以通过该语句判断是否是自我赋值:
&n != this,该语句比较了左右操作数的地址,如果两者相等则为自我赋值 - 赋值与复制的区别:复制是将一个值插入一个新的对象,不需要删除操作
- 构造函数只控制初始化操作,
operator=函数只控制赋值操作
使用
=初始化新对象称为拷贝初始化,可能调用复制构造函数、移动构造函数或发生复制消除;对已经存在的对象使用赋值表达式时,才调用赋值运算符编译器会根据类中已经声明的特殊成员函数以及基类、数据成员的能力,决定是否隐式声明或删除默认构造、析构、复制和移动操作;并非在所有情况下都会生成全部默认函数
默认的析构函数在删除一个指针变量时不会释放该指针指向的对象占用的内存空间
三位一体原则:如果类需要一个析构函数,那么它同时有可能需要一个复制构造函数和一个赋值操作函数
友元函数:如果一个函数定义在类的声明外部,但是又需要它具有访问
private成员数据的权限,那么可以在类内部声明这个函数时,前面加上friend关键字,将其声明为一个友元函数函数模板显式特化:在已经存在主函数模板的前提下,可以为指定类型提供特殊实现,例如:
1
2
3
4
5template<>
int function(int var)
{
...
}模板函数与模板类都是在编译期确定类型的,如果不使用就不编译,编译器的工作就是帮你实现复制粘贴多个不同类型的函数或类
对象的初始化
- 初始化与赋值:
- 初始化:在创建一个变量时赋予它一个初始值
- 赋值:将对象的当前值擦去,并且用一个新值替代
- 下列情况发生时会进行初始化:
- 声明一个变量
- 函数入口处用到函数参数
- 函数返回处用到函数返回值
- 构造初始化
- 通过显式地调用构造函数进行初始化称为显式初始化,否则称为隐式初始化
- 使用
=初始化一个对象称为拷贝初始化,不用等号则是直接初始化 - 如果定义对象时没有提供初始化器,就会按照对象的存储期和类型进行缺省初始化;不同初始化语法还可能触发零初始化、值初始化或默认构造等过程
- 关于不同种类对象的缺省初始化方式:
- 全局或者静态存储期对象:先进行零初始化,类类型随后还可能执行构造函数
- 对象为局部内置类型:如果只进行默认初始化,其值可能是不确定的,读取前必须先赋予有效值
- 内置类型作为类的数据成员:若未在成员初始化器或构造函数初始化列表中初始化,其值可能不确定;值初始化整个对象时则可能先被零初始化
- 自定义类型且有构造函数:使用构造函数初始化
- 自定义类型且无构造函数:递归地对对象中的每一个数据成员进行相应的数值初始化或者缺省初始化
类型转换
- 自动类型转换:
- 自动转换的例子很常见,如将
int类型的右值赋给double类型的左值时,该右值就会被自动转换为double类型 - 自动类型转换分为下面两种情况:
- 内置类型向自定义类型转换
- 自定义类型向内置类型转换
- 内置类型转换为用户自定义类型:需要在自定义类内部编写带单参数的构造函数
- 例如:
Name(const char* cp) { //将参数提供的内容存入临时的Name类型的对象中 },对Name类型,该构造函数会在编译器需要一个Name类型的对象却收到了一个const char*类型的对象的时候被调用
- 例如:
- 用户自定义类型转换为内置类型:需要在自定义类内部编写如下例所示的成员函数
- 例如:
operator int() { //将自定义类型转换为int类型 },该函数会在显式强制转换或者对象传入的值类型不符时被调用,函数的返回值由其类型名确定
- 例如:
- 自动转换的例子很常见,如将
- 若有两个类
A、B,类A中有参数类型为B的构造函数,类B中有强制转换为类A的操作符函数,这样会导致转换时出现二义性 - 混合类型表达式:在同一表达式中操作不同类型的对象时可能发生多次类型转换,也可能产生临时对象。是否真正带来额外内存或性能开销取决于具体类型、转换方式和编译器优化
引用
- 引用必须在声明时绑定到一个对象,例如
int& i = temp;,此后i就是temp的别名 - 引用一旦绑定便不能改绑;后续执行
i = other;是在给temp赋值,而不是让i引用other - 引用作函数参数:
- 非常量引用:
int function(vector<string>& count);,形式参数count是实际参数的别名,可以直接在函数内修改实际参数,而且不需要复制整个对象 - 常量引用:
int function(const vector<string>& count),不能通过该引用修改实际参数,同样避免了复制;引用在实现上可能使用地址,但语言语义是别名 - 一般来说,对于
int或者double类型的参数可以不需要引用,因为对它们的复制是非常快的
- 非常量引用:
- 值类型作函数返回值:
- 返回值可能被直接构造在调用方的存储中,也可能发生移动;现代编译器会按照复制消除等规则避免不必要的临时对象和复制
- 引用作函数返回值:
- 引用作函数返回值的时候,不可以引用局部变量,因为函数调用完成后局部变量就被销毁,这个引用就无效了
- 当引用作函数返回值时,不需要将变量重新复制一次保存在临时变量中
- 引用作返回值时可以作为左值
继承
基本概念
定义:
1
2
3
4
5
6
7class A {
public:...
private:...
};
class B: public A {
};- 上面定义了一个类
B,它是从A中继承而来,称A是基类(或者父类),B是继承类(派生类、子类) - 派生类对象中包含完整的基类子对象,包括基类的私有数据成员;派生类代码只能直接访问基类允许访问的公有或保护成员
- 同样可以使用
private或者protected关键字来指定继承的方式
- 上面定义了一个类
继承关系是可以嵌套的
基类和派生类的关系:在公有继承中,派生类对象“是一个”基类对象,因此派生类指针或引用可以隐式转换为基类指针或引用;反方向不一定成立
基类指针或引用不能无条件地安全转换为派生类。对多态基类可使用
dynamic_cast在运行时检查实际对象类型;在满足严格条件时也可以使用static_cast关于基类或者继承类作为函数参数:
- 形式参数与实际参数匹配的情形不讨论
- 如果形式参数是基类类型的,传入一个继承类的对象会将其转换为基类
- 如果形式参数是派生类类型,则实参必须是该派生类型或可转换为它的类型;若按值传递,函数中得到的是一个独立的派生类对象副本
protected:保护标识符,它指示类中的保护成员,类的保护成员可以被其继承类访问如果需要显式地表明某些成员是继承来的或者明确指明调用基类或者继承类的成员函数,可以使用范围(作用域)运算符
构造函数初始化列表:位于构造函数参数列表之后、函数体之前,用于直接初始化基类子对象和数据成员;编译器会根据提供的实参选择相应构造函数
派生类对象构造的步骤:
- 为整个对象分配内存空间
- 隐式(或者显式)调用基类的构造函数来初始化对象中的基类部分数据
- 用构造初始化器对派生类部分的数据进行初始化
- 如果有的话,执行派生类构造函数的函数体
继承关系中的函数覆盖:派生类中与基类虚函数具有匹配签名的函数会覆盖该虚函数,建议使用
override让编译器检查。返回类型通常相同,也允许满足规则的协变返回类型;非虚同名函数属于名称隐藏而不是动态覆盖
虚函数
- 定义:在基类中定义成员函数时前面加上
virtual关键字,在继承类中同名同参函数的virtual的特性也会被继承,因此不需要重复加 - 非纯虚函数在被实际使用时通常需要定义;纯虚函数可以只声明为
= 0,也可以额外提供类外定义 - 功能:由于继承类也是基类,因此当以引用或者指针的形式调用成员函数时,程序根据实际的对象类型来在基类与继承类中选择正确的同名成员函数,也即动态绑定
- 静态绑定:在编译时就确定调用的对象的成员函数的种类
- 多态:用一个类型表示多种类型的能力,这里动态绑定实现了多态
- 只有在以引用或者指针的形式调用虚拟函数的时候在运行时选择类型才有意义
- 虚析构函数:当可能通过基类指针删除派生对象时,基类析构函数必须声明为
virtual,以确保派生类和基类部分都被正确销毁;其函数体不一定为空 - 如果派生类声明了与基类同名但参数不同的函数,仍可能隐藏基类的全部同名重载;可用
using Base::func;重新引入基类重载 - 如果在构造函数内部调用虚函数,那么它会静态绑定成正在构造的对象所属于的类中的那个虚函数
代理类
- 代理类实际上就是一个普通的用户自定义类,它的成员数据是指向基类的指针
- 功能:存储和管理基类指针,这个基类指针既可以指向基类对象又可以指向派生类对象,使用代理类可以实现动态绑定,它将基类和派生类中的细节封装起来,提供了一个统一的接口
- 代理类中一般包含这些实现细节:
- 各种构造(析构)函数与复制控制函数:用于提供分配或者销毁内存空间的操作
- 基类和继承类中共有的操作:提供操作接口,实现动态绑定的功能
- 其他操作
- 关于代理类的复制操作:需要在它管理的基类和继承类中定义虚拟的复制控制函数,并且在代理类的复制操作中调用它,在复制时有必要判断指向被复制对象的指针是否为空指针
- 友元类:将一个类声明为另一个类的友元可以使另一个类中的所有成员都成为那个类的友元
句柄类
- 功能:上面提到的代理类可以视为一种简单的句柄类,但是它是通过复制对象来实现动态绑定的,句柄类的最主要功能就是实现在不用复制对象的情况下就实现动态绑定并且自动管理内存
- 一般来说,句柄类是一个模板类,因为它与它操作的模板之间是相互独立的,句柄类具有通用性
- 与代理类相似,句柄类要将被控制的类的全部接口封装起来
- 复制操作:需要先明确句柄采用独占所有权、深复制还是共享所有权。复制构造函数不能释放尚不存在的旧资源;复制赋值则需要安全处理旧资源和自赋值,避免悬空指针或重复释放
- 使用方法:在实际的程序中,通常使用一个接口类来管理基类与继承类,接口类的成员数据是一个句柄类,接口类通过操作句柄类来控制基类与继承类
- 接口类的构造函数必须对句柄类做初始化
计数句柄类
- 相比于一般的句柄类,计数句柄类在复制对象时会有选择的对底层的对象进行复制,这意味着计数句柄在复制时并不总是将底层对象的内容重新拷贝一份,而是让多个复制出来的句柄的指针同时指向同一个底层对象
- 为了实现上面的功能,需要在句柄类添加一个引用计数指针,它用来记录有多少个对象指向底层对象
- 复制构造函数:计数句柄类的复制构造函数可以共享底层对象,其基本操作如下:
- 将新句柄的对象指针和引用计数指针初始化为与源句柄相同的值
- 将引用计数加一,表示新增了一个指向同一底层对象的句柄
- 释放当前对象旧资源属于复制赋值运算符的职责,不属于复制构造函数
- 析构函数:同样要考虑当前的句柄类是不是最后一个指向该对象的句柄,具体操作不再赘述
- 在修改句柄类指向的对象的值时,亦需要考虑当前的句柄类是不是最后一个指向该对象的句柄,如果是的话,那么直接修改这个指针指向的对象的值即可,否则要先将当前所指向的对象的引用计数减一,然后基于旧对象复制一个新对象并将指针指向它,并将它的引用计数设为一,上述的操作可写为成员函数并在接口类中调用
抽象基类
- 概念:抽象基类是一种接口类,它用于提供一类对象的基本特征,再由派生类实现可以归入该类别的具体对象的详细行为
- 一般来说,抽象基类使用纯虚函数来实现这种特性
- 抽象基类可以使用句柄类来自动管理内存
纯虚函数
纯虚函数在声明末尾使用纯说明符
= 0。它可以没有实现,也可以在类外提供定义,声明例如下:1
virtual void display() = 0;
含有纯虚函数的类就是一个抽象基类,它不能有一个实际的对象
纯虚函数在抽象基类的派生类中得到实现
如果在派生类中继承而来的纯虚函数没有定义,那么它仍然是一个纯虚函数
输入与输出
标准输出流:
1
std::cout << "output" << std::endl;
上面的语句中,
std为命名空间,命名空间是为了区分在不同的库中,两个名字相同但是功能不同的函数::为作用域运算符<<为标准库的输出运算符std::cout为标准输出流std::endl是流操纵符,它先输出换行符,再刷新输出缓冲区。仅需要换行而不需要立即刷新时,通常使用'\n'流操纵符:用于调整流的格式或触发特定操作
源代码中相邻的字符串字面量会在编译期自动拼接,例如
"hello " "world";这不是输出流对任意字符串对象执行的操作控制器
setprecision(x):用于为后续输出设定x位有效位数,一般可以这样使用(使用完后注意重置为默认值):1
2
3
4//先保存初始的输出精度
streamsize prec = cout.precision();
//在输出流中调整输出精度为x后再调整回默认值
cout << setprecision(x) << grade << setprecision(prec);在现代C++中,链式移位表达式会按照规定的顺序完成各次输出;不过仍不应在复杂表达式中混合多个相互依赖的副作用,以免代码难以理解
setw(length)函数的使用:- 使用例:
cout << setw(10) << name << endl; - 属于控制器,用于控制输出字段的长度
- 当后面紧跟着的输出字段长度小于
length的时候,在该字段前面用空格补齐,当输出字段长度大于length时,全部整体输出 - 默认是右对齐,如果需要左对齐,可以在其前面添加
left控制器
- 使用例:
标准输入流:
1
std::cin >> name;
cin为标准输入流,>>运算符按照目标类型解析输入。读取字符串时通常在空白字符处停止;遇到输入结束或解析失败时会设置相应的流状态- 默认C locale中的空白字符包括空格、水平制表、换行、垂直制表、换页和回车
- EOF:在Windows系统下,EOF为
CTRL+Z,在Linux或者Unix系统下则为CTRL+D - 输入时
cin会略去首先输入的空白字符 cin >> a >> b;等价于cin >> a; cin >> b;
循环输入:
1
2
3while (cin >> x) {
//循环内容
}- 原理:每次提取成功后执行一次循环体;遇到类型不匹配、输入错误或输入结束时循环退出。在采用规范输入模式的交互式终端中,用户输入通常在按下回车后交给程序,但文件流等输入源不遵循这一交互过程
缓冲区:流库可能暂存输入或输出数据,以减少底层读写次数。输出缓冲区会在需要时将内容写往目标设备或文件
缓冲区刷新的几种情形:
- 缓冲区满
- 读取与某个输出流绑定的输入流时,会先刷新被绑定的输出流;例如
cin默认与cout绑定 - 手动刷新缓冲区
流迭代器:
- 在
<iterator>中定义,使用时必须指明类型 - 输入流迭代器
istream_iterator<Type>(cin):迭代器的参数是可选的,通常情况下将其与cin连接,从这里读值,该迭代器缺省值的性质是,一旦到达了文件末尾或者处于错误状态,那么这个迭代器会与缺省值相等 - 输出流迭代器
ostream_iterator<Type>(cout, delimiter):第一个参数指定输出流,第二个可选参数是C风格字符串分隔符,它会写在每个元素之后 - 流迭代器的本质是模板
- 在
头文件
<cctype>中的几个有用的函数:它们在字符c满足后面的条件时返回非零值,否则返回0isspace(c):空白isalpha(c):字母isdigit(c):数字isalnum(c):数字或者字母ispunct(c):标点符号isupper(c):大写字母islower(c):小写字母
标准错误流:
cerr:标准错误流,默认设置了unitbuf,通常会在每次输出操作后刷新;异常发生时仍需程序显式写入错误信息clog:倾向于生成日志,它具有着与cout一样的缓冲特性,平时存储错误信息,在适当的时候输出
文件的输入与输出:
ifstream:是一个用于读取文件内容的对象的类型ofstream:是一个用于向一个文件输出内容的对象的类型上面两个类型定义在
<fstream>中,使用时需要先将这两个类型的对象实例化,并且将字符串形式的文件名作为参数在使用这两个类型的对象时有点类似于
istream和ostream使用例:
1
2
3
4
5
6ifstream infile("in"); //定义了一个与in文件相关联的文件输入流
ofstream outfile("out"); //定义了一个与out文件相关联的文件输出流
string s;
while (getline(infile, s)) {
outfile << s << endl;
}注意:相对文件路径始终相对于进程的当前工作目录解析。调试器或启动脚本可以单独配置工作目录,它不一定等于工作区或可执行文件所在目录
流程与异常控制
不变式
不变式是程序在某些规定时刻必须保持为真的性质。类不变式通常要求在对象完成构造后以及每次公有操作前后成立;循环不变式要求在每次迭代开始或结束的指定位置成立。例如:
1 | //这里假设loop已经输出的行数 |
这里int loop = 0;是初始化语句,“loop等于已经输出的行数”才是循环不变式。循环体每输出一行后都应更新loop,以保证该关系在下一次迭代开始时仍成立。
异常控制
异常控制用于给程序的使用者给出错误提示,提高程序的鲁棒性。程序抛出异常后,会在抛出异常的地方终止执行并且跳转到程序的另一部分(有点类似于中断),并向该部分提供一个异常对象,异常对象中含有程序可以处理异常信息。例如:
1 | try { |
应当在编程时加入判断,是否满足产生异常的条件,如:
1 | if (something == 0) { |
上面的语句意为,当满足条件时,程序会抛出一个domain_error,这种错误类型为域错误,即输入在定义域外的情况。C++中还有许多其他的错误类型可供使用,如logic_error、runtime_error等。
多文件编程
文件种类:
main.cpp:主函数所在的文件file_x.h:头文件file_x.cpp:其他源代码文件
……
头文件的写法:
1
2
3
4
5
6
7
8
9
//#ifndef应位于可能产生声明或定义的有效内容之前,版权注释等可以放在它前面
//不要在头文件中使用using声明,为用户保留最大的灵活性
//在头文件中直接包含该头文件自身的声明与定义所依赖的库
//在头文件中声明对应源文件中定义的函数#include <系统环境提供的头文件>,#include "自己写的头文件.h"
部分库函数
sort()
库:
<algorithm>参数:
- 迭代器,序列的起始
- 迭代器,序列的末尾
- 谓词,可选参数,是自定义的比较方法
功能:对随机访问迭代器指定的范围进行排序,可用于
vector、deque、数组等;未提供比较器时通常使用<进行比较关于谓词:谓词是可调用对象,其结果可转换为布尔值
getline()
- 库:
<string> - 参数:
- 输入流,一般为
cin - 字符串引用,直接填目标字符串的名字
- 输入流,一般为
- 功能:用于读一行的内容并将这些内容存储到目标字符串中
- 如果遇到文件结尾或者无效输入,
getline()的返回值会指示失败
copy()
- 库:
<algorithm> - 参数:
- 迭代器,指示需要复制的元素的开头
- 迭代器,指示需要复制的元素的末尾后一个元素
- 输出迭代器,指示复制目标的起始位置
- 功能:将
[b,e)之间的元素复制到从第三个参数开始的位置。使用back_inserter作为第三个参数时,才会追加到目标容器末尾
equal()
- 库:
<algorithm> - 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 迭代器,指示第二个序列的开头;使用四迭代器重载时还需提供第二个序列的末尾
- 功能:从第二个序列的开头开始,逐个比较两个序列中的元素,如果相应范围相等则返回
true
find()
- 库:
<algorithm> - 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 被查找的值
- 功能:在指定的序列中查找是否存在第三个参数的值,如果找到了,则返回序列中第一次出现这个值的位置,否则返回这个序列的末尾
find_if()
- 库:
<algorithm> - 参数:
- 迭代器,指示被查找的序列的开头
- 迭代器,指示被查找的序列的末尾
- 谓词,检查自己的输入参数并返回可转换为布尔值的结果
- 功能:对序列中的每个元素调用谓词,直到谓词产生一个
true结果;它会返回指向该元素的迭代器,如果没有找到,则返回被查找序列的末后迭代器
search()
- 库:
<algorithm> - 参数:
- 迭代器,指示被查找的序列的开头
- 迭代器,指示被查找的序列的末尾
- 迭代器,指示查找的目标序列的开头
- 迭代器,指示查找的目标序列的末尾
- 功能:查找第一第二个参数所指示的序列中有没有第三第四个参数所指示的序列,它会返回一个指示被找到的元素的迭代器,如果没有找到,则返回指示被查找序列末尾的迭代器
transform()
- 库:
<algorithm> - 参数:
- 迭代器,指示待转换序列的开头
- 迭代器,指示待转换序列的末尾
- 输出迭代器,指示结果写入的起始位置;目标范围必须已有足够空间,或者使用插入迭代器
- 函数,
transform()将这个函数作用于待转换序列以获得期望得到的元素
- 功能:对待转换序列中的元素执行指定函数,并将转换后的结果存储于第三个参数指定的序列中
accumulate()
- 库:
<numeric> - 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 单一变量
- 功能:以第三个参数为初始值依次累加序列中的元素,并返回最终结果;函数不会自动把结果写回传入的变量
- 注意,第三个变量中的值类型会影响最终的结果的类型
remove_if(),remove()
- 库:
<algorithm> - 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 谓词,返回一个布尔值
- 功能:将使谓词为
false的保留元素移动到序列前部,并返回新的逻辑末尾;它不会真正缩小容器,通常还要调用容器的erase()删除尾部元素 remove()的前两个参数含义与remove_if()相同,第三个参数是待移除的值;它会把不等于该值的元素移动到序列前部
partition(),stable_partition()
- 库:
<algorithm> - 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 谓词,返回一个布尔值
- 功能:两个函数都会将使谓词为
true的元素放在序列前部,并返回指向第二组首元素的迭代器。stable_partition()会分别保持两组元素原有的相对顺序,而partition()不保证保持顺序
rand()
- 库:
<cstdlib> - 参数:无
- 功能:在
[0,RAND_MAX]之间返回一个伪随机整数,范围包含RAND_MAX;RAND_MAX也定义在<cstdlib>中 - 如何返回在某个指定范围内的随机整数:基本思想是将可以利用的随机数分为长度相等的存储桶,存储桶的个数就是我们需要的随机数的最大值,然后再计算一个随机数并且返回它所在的桶的编号,程序如下:
1 | int nrand(int n) |
编程习惯
该部分的内容很多,涉及范围也很广,因此在这里我只纪录根据我个人实际情况需要注意的要点。更全面和详细的内容可以参考C++ 风格指南 - 内容目录 — Google 开源项目风格指南 (zh-google-styleguide.readthedocs.io)。
- 在编写运行时间较长的程序时,应当在合适的时刻刷新缓冲区
- 循环的计数习惯:一般循环中的计数变量(假设为
i)都从0开始,这样它的含义就比较明确,也即,到目前为止,我们已经操作了i次。此外可以把循环条件设为i != expected_loop_times,这个不等式明确表明,当循环退出时,我们已经循环了expected_loop_times次 - 在定义变量来保存特定数据时,应该使用库中为特殊用途定义的类型。例如要定义一个存储字符串长度的变量
length,可以写std::string::size_type length;。还可以为库类型定义别名,如using str_size = std::string::size_type; - 在操作容器中的多个元素时,推荐使用左闭右开的表示方法,这个区间中的元素个数就是上下限之间的差,这个区间的右界被称为越界值,这样做的理由有三个:
- 如果区间没有元素,那么找不到一个最后的元素标记终点
- 按照上面的方法定义,只要两个迭代器相等,那么立即可以判断区间为空
- 该方法提供了一种自然的方式表达“区间之外”
- 在使用标准库提供的容器的时候,尽量使用对应的迭代器
- 一般都使用
double类型来进行浮点数计算 - 在编写程序时,需要预见一些异常或者无意义的情况,在这种情况下程序最好暂时终止或者退出
- 函数形式参数的名称最好不要太具体,因为某个函数可以用于多种含义不同的数据(这个思想对编写模板函数很重要)
- 在输出时如果需要在输出的内容之间插入分隔符(如空格、逗号等),应当对第一个元素做特殊处理,具体思想是:当第一个元素生成(或者确保它已经存在)之后,输出第一个元素,然后再判断之后的元素是否存在,如果存在的话,就将它们与分隔符一起输出
- 对于不希望改变对象内容的非静态成员函数,应当将其声明为
const成员函数;普通函数则可通过常量引用等参数表达只读意图 - 如果一个函数会改变一个对象的状态,那么这个函数应该作为对象的成员函数
- 在设计二元运算符的时候,为了能够让左右操作数都可以进行类型转换以保持操作数的对称性,尽量将二元运算符定义在类的声明之外
- 对可用单个实参调用、但不希望参与隐式类型转换的构造函数,通常应声明为
explicit;是否添加该关键字取决于是否希望允许隐式转换,而不是构造函数是否“创建新对象” - 不可以让用户用指针直接访问类中的私有成员
- 使用句柄类时,当成员函数会改变底层对象的值时需要酌情进行复制操作,以保证指针的有效性
Accelerated C++笔记
