Accelerated C++笔记
虽然两年前就已经接触过C++,但是之前短暂的学习根本不足以让我系统掌握这门语言的用法。现在由于有项目开发的需求,我决定重新开始较为系统地学习C++。由于有C语言的基础,在入门阶段我使用Accelerated C++这本书来学习。这本书虽然较为老旧,但是内容不多,读起来也比较轻松。这篇文章是对我在看书和编程时遇到的问题和重点的总结备忘,内容比较零碎,还请大家谅解。
语言特性
基础知识
声明:告诉编译器有这个对象存在,不需要建立内存
定义:需要建立内存的声明,也即实例化一个对象
main
函数返回一个整数类型的值作为结果,0
表示成功void
类型的函数可以不需要return
语句,也可以写return;
变量是一个具有名称的对象,对象是计算机中一段具有类型的内存空间(在C++中,一切皆对象)
在花括号中定义的变量,作用域只在它所在的花括号中
对象类型还有接口,接口就是可以对对象进行的操作的集合
重载:
- 运算符的重载:一个运算符对于不同类型的操作数有不同的含义,如
1 + 1
与'A' + 'B'
中的加号具有不同的含义 - 函数重载:两个功能不同的函数在具有相同名称时,当它们的参数不同时,会发生重载
- 运算符的重载:一个运算符对于不同类型的操作数有不同的含义,如
关于
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) {}; //函数收到的参数是常量
修饰成员函数:
1
2int func(int n) const {}; //成员函数不能修改任何除了mutable修饰的的成员变量
//且不可调用非const成员函数修饰函数返回值:
1
2const int func(int n) {}; //函数的返回值是常量,一般不需要这样用
//除了重载运算符外,一般不要将返回值类型定为对某个对象的const引用
const
修饰成员函数时,根本上是修饰了该函数所在对象的this
指针,如果对象有非const
和const
两个重载函数时,调用哪一个是由对象是否是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
之前插入元素
不要将重载函数作为参数传递给模板函数,否则编译器将不知道需要使用的是重载函数哪个版本
将函数作为参数:将函数作为参数时,在声明的时候参数函数的编写形式类似于函数的声明,返回值和参数函数的参数必须与参数函数相一致,但是名称不需要一致
函数的缺省参数:在声明一个函数时,如果在参数后写上
= x
(x
为某个值、序列、或者函数),那么这个参数默认值为x
,在调用这个函数可以该参数缺省函数的递归调用:在函数内部调用自身
内联函数:在函数声明与定义时,于函数名字前添加了
inline
关键字的函数,在编译时所有调用内联函数的地方都会被替换成函数体的代码以提高运行效率,对一般的函数,只有程序运行到调用函数的那个地方时,函数名才会被替代成相对应的函数代码,内联函数的长度不应超过10行main
函数:- 完整形式:
main(int argc, char** argv)
- 参数:
argv
:指向一个数组首元素地址的指针,数组中的每一个元素都是都指向一个字符串参数的指针argc
:argv
指向的数组中的指针个数
argv
的第一个元素指向编译出的程序的名字的字符串的首地址- 在程序中可以直接使用这两个对象
- 完整形式:
NULL
,nullptr
,NaN
,INF
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
个元素的容器c
,c
的元素是t
的复制container<Type> c(b, e);
:定义一个有n
个元素的容器c
,c
的元素是迭代器[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
容器,erase
和push_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
来人工确定元素的顺序
自定义类型
作为核心语言的一部分,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
的一个成员函数,否则会出现函数未定义的编译错误将成员函数定义在类内部与类外部的区别:在类内部定义的成员函数,实际就是把函数的调用扩展成内联的,这样可以提高程序运行效率
成员函数如果不是定义在类内部,必须在相对应的源文件(
.cpp
)中定义,否则会产生重复定义的问题(模板类除外)如果在自定义类与全局环境中有同名函数,在函数名字前加上
::
(该运算符左侧没有内容)代表使用的是全局环境中的那个函数如果需要访问一个私有的数据成员,可以定义一个存取器函数(如下),但是它会破坏类的封装性:
1
std::string name() const { return name; }
对声明为
static
的成员函数,它只能在类中定义
构造函数
构造函数是类的一个特殊的成员函数,它定义了对象的初始化方式。它的名字是类的名字且没有返回值,它会在创建一个自定义类型的对象时被自动调用。
如果没有自定义构造函数,那么编译器会为我们自己创建一个
构造函数列表:在
public
作用范围内的构造函数的声明构造函数的定义:
1
Student_info::Student_info(double midterm, double final);
上面定义了一个参数为
midterm
和final
的构造函数,下面给出声明与初始化例:1
Student_info::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
为整型 - 如果将一个函数名作为另一个函数的参数,那么编译器会将这个参数转换成一个指向那个函数的指针
- 使用指针时可能导致问题出现的原因:
- 复制一个指针时不会复制指针所指的对象
- 删除一个指针不会释放对象所占用的内存
- 删除一个对象但是没有删除指向该对象的指针会产生一个空悬指针
- 定义了一个指针但是不进行初始化
数组
数组是最基础的容器,它是核心语言的一部分。
- 数组元素个数必须在编译时确定,它不能动态增减尺寸
- 使用
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
关键字限定的对象,系统只会对它进行一次内存分配,直到程序结束才释放 - 动态分配:使用
new
与delete
关键字分配的对象
new
与delete
:使用new
创建的对象一直存在直到对其使用delete
或者程序结束为对象分配或释放内存:
new T;
:为一个没有名字的T
类型的对象分配内存new T(args);
:与上面类似,但是内存中存有值args
int* p = new int(100);
:将一个int
类型的指针p
指向new
开辟的存有100
的内存空间delete p;
:删除指针p
并且释放其指向的内存空间
为数组分配或释放内存:
new T[n];
:为一个具有n
个T
类型的对象数组分配内存,并且返回一个指向数组首元素的指针,数组中的每一个对象都会被默认初始化- 在上面的表达式中如果
n
为0
,那么数组中将没有任何元素,并且new
无法返回一个有意义的指针 T* p = new T[n];
:将指针p
指向新开辟的数组的首地址delete[] p;
:从后向前释放指针p
指向的数组所用的全部内存空间,如果索引运算符内添加了索引,那么只释放索引指定的那个内存空间
malloc()
函数:- 库:
<stdlib>
- 参数:
size_t
类型,为需要分配的内存空间大小 - 返回一个
void*
类型(未知类型)的指针,在使用时需要进行强制类型转换,若失败则返回NULL
- 使用例:
char* cp = (char*)malloc(10);
malloc
不会对分配的空间进行初始化,而new
会
- 库:
内存分区:
- 堆区:由用户手动申请与释放,若不释放则在程序结束后释放,使用
new
或malloc
申请的内存在此区域 - 栈区:由系统管理,主要存放函数的参数以及局部变量
- 静态存储区:在编译时就已经分配好内存空间并且进行初始化,主要存放静态变量、全局变量以及常量
- 代码区:存放程序体的二进制代码
- 堆区:由用户手动申请与释放,若不释放则在程序结束后释放,使用
内存分配器
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 | template <class T> |
template <class T>
定义了一个模板头,它告诉系统环境定义了一个模板函数,而且这个函数有一个类型参数- 如果要使用嵌套类型(也即由模板参数定义的类型),那么必须要使用
typename
关键字,如:typedef typename vector<T>::size_type vec_sz;
,使系统将这个名称当做一个类型处理 - 模板函数的实例化过程在不同系统下有所不同
- 模板函数的正确性是由参数之间的正确关系决定的,如果参数之间产生了可能导致歧义和信息丢失的关系,那么这个函数的程序很有可能是有错误的,甚至无法通过编译
- 在编写模板函数的时候,为了使函数可以处理存储于各种数据结构中的数值,并且能够作用于容器的一部分而非整个容器,一般我们使用迭代器参数而不是容器参数
迭代器的类型
下面的迭代器每一种都对应了一个特定的迭代器操作集合,每一个迭代器种类还对应了一个访问容器的策略,同时也对应了特定的算法。在编写模板函数时需要注意不同迭代器的选择。
- 输入迭代器
In
:该种迭代器对一个序列提供了顺序只读访问的操作,它支持的操作有:++
、==
、!=
、*(读)
- 输出迭代器
Out
:该种迭代器对一个序列提供了顺序写入的操作,它要求调用它的程序不可以在对迭代器的两个赋值之间执行超过一次的自增操作,也不能在没有对迭代器递增时对其多次赋值 - 正向迭代器
For
:该种迭代器对一个序列提供了顺序读-写访问的操作,它可以在对一个元素赋值后再读取它的值,因此不需要满足输出迭代器的一次赋值要求,它支持的操作有:*(读写)
、++
、==
、->
,所有的标准库容器都满足正向迭代器的要求 - 双向迭代器
Bi
:该迭代器对一个序列提供了可逆读-写访问的操作,它不仅有正迭代器的功能,还支持--
(也即逆序访问)的操作,所有的标准库容器都支持双向迭代器 - 随机迭代器
Ran
:该迭代器对一个序列提供了随机访问的操作,它的行为有点类似索引,不仅有双向迭代器的功能,而且支持算术运算,若p
和q
是两个迭代器,那么它们支持:p + n
、p - n
、n + p
、p - q
(得到的是两个迭代器之间的距离,整型)、p < q
、p > q
、p >= q
,向量和字符串迭代器都是随机访问迭代器,而链表迭代器则是双向迭代器
模板类
模板类用于生成一个类,这个类生成的多个对象可以分别存储不同类型的数据。模板类相当于定义了存储自定类型的数据结构的一个容器。
声明例:
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
关键字 - 如果某个重载运算符函数是类的一个成员函数:
- 它的左操作数(二元运算符)或者唯一的操作数(一元运算符)必须是调用它的对象
- 它的左操作数(二元运算符)会以
this
指针的方式默认地传递给这个成员函数
- 如果某个重载运算符函数不是类的一个成员函数,那么重载运算符函数的第一个参数是左操作数,第二个参数是右操作数
- 运算符重载的声明有点像定义了一个
this
指针:this
关键词只在成员函数内部有效,代表指向函数操作对象的指针,指针的类型由函数的操作对象决定复制控制:
- 复制构造函数:用于复制一个已经存在的同类型的对象,以此来初始化一个新的对象,它只带一个与类本身类型相同的参数,并且由于复制不应该改变已有对象的值,因此它的类型应该为常量引用类型
- 复制构造函数的函数体应该为自定义的复制操作,如果函数体为空,那么将使用系统默认的方式进行复制
- 在复制对象时,由于对象的副本的地址与原对象不同,因此不可以将原对象中指向原对象本身的指针同时复制过去,应当重置这些指针使得它们对新对象有意义
赋值运算符:
- 赋值操作函数:把一个对象中已经存在的值擦除,然后装入一个新的值
- 类中可以重载赋值运算符,如:
Name& operator=(const Name& n) {}
- 这里的参数为右操作数的一个引用
- 在函数体内可以通过该语句判断是否是自我赋值:
&n != this
,该语句比较了左右操作数的地址,如果两者相等则为自我赋值 - 赋值与复制的区别:复制是将一个值插入一个新的对象,不需要删除操作
- 构造函数只控制初始化操作,
operator=
函数只控制赋值操作
在使用
=
为变量赋初始值时调用的是赋值构造函数,在赋值表达式中=
调用的是赋值操作函数如果一个类没有显式地定义构造函数或者析构函数,那么编译器将自动生成执行这些操作的默认函数
默认的析构函数在删除一个指针变量时不会释放该指针指向的对象占用的内存空间
三位一体原则:如果类需要一个析构函数,那么它同时有可能需要一个复制构造函数和一个赋值操作函数
友元函数:如果一个函数定义在类的声明外部,但是又需要它具有访问
private
成员数据的权限,那么可以在类内部声明这个函数时,前面加上friend
关键字,将其声明为一个友元函数模板特化:只有当模板类的实际的对象类型
T
为指定类型时,才调用指定类型对应的模板成员函数,声明与定义如下:1
2
3
4
5template<>
int function(int var)
{
...
}模板函数与模板类都是在编译期确定类型的,如果不使用就不编译,编译器的工作就是帮你实现复制粘贴多个不同类型的函数或类
对象的初始化
- 初始化与赋值:
- 初始化:在创建一个变量时赋予它一个初始值
- 赋值:将对象的当前值擦去,并且用一个新值替代
- 下列情况发生时会进行初始化:
- 声明一个变量
- 函数入口处用到函数参数
- 函数返回处用到函数返回值
- 构造初始化
- 通过显式地调用构造函数进行初始化称为显式初始化,否则称为隐式初始化
- 使用
=
初始化一个对象称为拷贝初始化,不用等号则是直接初始化 - 如果对象没有进行显式初始化,则会进入隐式初始化的状态,在隐式初始化状态下会进行缺省初始化的过程
- 关于不同种类对象的缺省初始化方式:
- 全局或者静态的对象:空串或者
0
- 对象为局部的内部类型:未定义的值
- 内部类型作为类的成员:在某些条件下会被数值初始化为
0
- 自定义类型且有构造函数:使用构造函数初始化
- 自定义类型且无构造函数:递归地对对象中的每一个数据成员进行相应的数值初始化或者缺省初始化
- 全局或者静态的对象:空串或者
类型转换
- 自动类型转换:
- 自动转换的例子很常见,如将
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
指向的内存位置与temp
相同,i
相当于是temp
的一个别名 - 引用作函数参数:
- 非常量引用:
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
关键字来指定继承的方式
- 上面定义了一个类
继承关系是可以嵌套的
基类和继承类的关系:基类就是继承类,继承类不是基类,对于他们的引用或者指针同样有这个关系
基类不可以强制转换为派生类
关于基类或者继承类作为函数参数:
- 形式参数与实际参数匹配的情形不讨论
- 如果形式参数是基类类型的,传入一个继承类的对象会将其转换为基类
- 如果形式参数是继承类型的,那么实际传入的对象只有继承类的部分
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
3while (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>
中,使用时需要先将这两个类型的对象实例化,并且将字符串形式的文件名作为参数在使用这两个类型的对象时有点类似于
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;
就是一个不变式,它在生命周期中,必须时刻等于输出的行数,否则会导致逻辑和程序流程控制的混乱,由于在这里不变式作为循环的一个判断依据,因此也被称为循环不变式
异常控制
异常控制用于给程序的使用者给出错误提示,提高程序的鲁棒性。程序抛出异常后,会在抛出异常的地方终止执行并且跳转到程序的另一部分(有点类似于中断),并向该部分提供一个异常对象,异常对象中含有程序可以处理异常信息。例如:
1 | try { |
应当在编程时加入判断,是否满足产生异常的条件,如:
1 | if (something == 0) { |
上面的语句意为,当满足条件时,程序会抛出一个domian_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>
参数:
- 迭代器,序列的起始
- 迭代器,序列的末尾
- 谓词,可选参数,是自定义的比较方法
功能:用于比较向量中的内容并按顺序存放,在未注明的情况下,对任何待排序的向量元素类型,它都会使用
<
来操作关于谓词:是一个函数,返回需值是布尔值
getline()
- 库:
<string>
- 参数:
- 输入流,一般为
cin
- 字符串引用,直接填目标字符串的名字
- 输入流,一般为
- 功能:用于读一行的内容并将这些内容存储到目标字符串中
- 如果遇到文件结尾或者无效输入,
getline()
的返回值会指示失败
copy()
- 库:
<algorithm>
- 参数:
- 迭代器,指示需要复制的元素的开头
- 迭代器,指示需要复制的元素的末尾后一个元素
- 迭代器适配器
- 功能:将
[b,e)
之间的元素按迭代器适配器指定的方式添加到目标容器的末尾
equal()
- 库:
<algorithm>
- 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 迭代器,指示第二个序列的开头(或者末尾)
- 功能:从第二个序列的开头(或者末尾)开始,按顺序一个元素对应一个元素地比较两个序列中的内容,如果相等的话则返回
True
find()
- 库:
<algorithm>
- 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 被查找的值
- 功能:在指定的序列中查找是否存在第三个参数的值,如果找到了,则返回序列中第一次出现这个值的位置,否则返回这个序列的末尾
find_if()
- 库:
<algorithm>
- 参数:
- 迭代器,指示被查找的序列的开头
- 迭代器,指示被查找的序列的末尾
- 谓词,检测自己出参数并且返回一个布尔值
- 功能:对序列中的每个元素都调用谓词,直到谓词产生一个
True
的结果,它会返回一个指示被找到的元素的迭代器,如果没有找到,则返回指示被查找序列末尾的迭代器
search()
- 库:
<algorithm>
- 参数:
- 迭代器,指示被查找的序列的开头
- 迭代器,指示被查找的序列的末尾
- 迭代器,指示查找的目标序列的开头
- 迭代器,指示查找的目标序列的末尾
- 功能:查找第一第二个参数所指示的序列中有没有第三第四个参数所指示的序列,它会返回一个指示被找到的元素的迭代器,如果没有找到,则返回指示被查找序列末尾的迭代器
transform()
- 库:
<algorithm>
- 参数:
- 迭代器,指示待转换序列的开头
- 迭代器,指示待转换序列的末尾
- 迭代器,存放处理后的元素的容器的开头
- 函数,
transform()
将这个函数作用于待转换序列以获得期望得到的元素
- 功能:对待转换序列中的元素执行指定函数,并将转换后的结果存储于第三个参数指定的序列中
accumulate()
- 库:
<numeric>
- 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 单一变量
- 功能:将序列中的元素之和加上变量中的值后再存储到这个变量中
- 注意,第三个变量中的值类型会影响最终的结果的类型
remove_if(),remove()
- 库:
<algorithm>
- 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 谓词,返回一个布尔值
- 功能:排列序列中的内容,它将视那些使得谓词为
True
的元素所在的位置为空闲位置,并将后面那些使谓词为False
的元素依次放置在空闲位置中,它的返回值是一个迭代器,它指示了那些所有使谓词为False
的元素的末尾的位置 remove()
的前两个参数含义与remove_if()
相同,但是第三个参数是一个值,该函数的作用与remove_if()
相同,不同之处在于是移动了那些等于第三个参数的元素
partition(),stable_partition()
- 库:
<algorithm>
- 参数:
- 迭代器,指示一个序列的开头
- 迭代器,指示一个序列的末尾
- 谓词,返回一个布尔值
- 功能:两个函数的功能都是将谓词作用于序列中的元素,然后将那些使得谓词为
True
的元素位于序列的头部,返回一个指向第一个使得谓词为False
的元素的迭代器,它们的区别在于stable_partition()
会保持元素的顺序不变,而partiotion()
则有可能导致元素的顺序混乱
rand()
- 库:
<cstdlib>
- 参数:无
- 功能:在
[0,RAND_MAX)
之间返回一个伪随机数,RAND_MAX
是一个大整数,它也是在<cstdlib>
中定义的 - 如何返回在某个指定范围内的随机整数:基本思想是将可以利用的随机数分为长度相等的存储桶,存储桶的个数就是我们需要的随机数的最大值,然后再计算一个随机数并且返回它所在的桶的编号,程序如下:
1 | int nrand(int n) |
编程习惯
该部分的内容很多,涉及范围也很广,因此在这里我只纪录根据我个人实际情况需要注意的要点。更全面和详细的内容可以参考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
- 不可以让用户用指针直接访问类中的私有成员
- 使用句柄类时,当成员函数会改变底层对象的值时需要酌情进行复制操作,以保证指针的有效性
Accelerated C++笔记