![]()
引言
关于由C转向C++的一些拓展与注意事项。
使⽤C++刷算法的好处
- 在已经学习过C语⾔的前提下,学习C++并使⽤它刷算法的学习成本⾮常低~只需要⼏个⼩时就可以学会~
- C++向下兼容C,C语⾔⾥⾯的语法⼤部分都可以在C++⽂件中运⾏,所以学习C++对刷算法时编程语⾔的表达能⼒进⾏扩充有益⽆害,例如C语⾔的输⼊输出(
scanf和printf)⽐C++快,那么就可以在使⽤C++刷算法同时使⽤scanf和printf提⾼代码运⾏效率 - C++拥有丰富的STL标准模版库,这也是PAT甲级、LeetCode等题⽬中经常需要⽤到的,单纯使⽤C语⾔解决问题会⽐C++的STL解决该问题麻烦很多~
- C++的 string 超级好⽤~⽐C语⾔⾥⾯的char数组好⽤多啦~⽤了就再也不想回去的那种~
- C++可以在某⼀变量使⽤前随时定义该变量,⾮常⽅便
- 在解决⼀些较为简单的PAT⼄级题⽬的时候(例如⼀些时间复杂度限制不严格的题⽬),
cin、cout输⼊输出⾮常⽅便~⽤过的都说好~ (๑• . •๑)
虽然C++是⼀⻔⾯向对象语⾔,但是对于刷算法这件事⽽⾔,我们并不需要掌握它⾯向对象的部分~只需要掌握刷算法的时候需要⽤到的部分(基本输⼊输出、STL标准模板库、 string 字符串等)就可以啦~C语⾔和C++有很多相似之处,且C++向下兼容C语⾔,所以我没有说的地⽅就直接⽤C语⾔的语法表示就好~以下是正⽂,先来段代码⽅便讲解:
|
名称空间
这句话是使⽤“std”这个名称空间( namespace )的意思~因为有的时候不同⼚商定义的函数名称彼此之间可能会重复,为了避免冲突,就给所有的函数都封装在各⾃的名称空间⾥⾯,使⽤这个函数的时候就在main函数前⾯写明⽤了什么名称空间,⼏乎在C++中使⽤到的⼀些⽅法如 cin 、 cout 都是在 std 名称空间⾥⾯的,所以可以看到 using namespace std;这句话⼏乎成了我每段C++代码的标配,就和 return 0; ⼀样必须有~其实也可以不写这句话,但是使⽤ std ⾥⾯的⽅法的时候就会麻烦点,要在⽅法名前加上 std:: ,⽐如写成这样:
std::cin >> n; |
我觉得这样⽐较丑,所以不管要不要⽤到,直接每道题的代码标配得写 using namespace std; 就好啦~
输入输出
就如同 scanf 和 printf 在 stdio.h 头⽂件中⼀样, cin 和 cout 在头⽂件 iostream ⾥⾯,看名字就知道, io 是输⼊输出 input 和 output 的⾸字⺟, stream 是流,所以这个 iostream 头⽂件⾥包含的⽅法就是管理⼀些输⼊输出流的~
cin 和 cout ⽐较⽅便,不⽤像C语⾔⾥的 scanf 、 printf 那样写得那样繁琐, cin >> n; 和scanf("%d", &n); ⼀样的意思(⽽且⽤ cin 再也不⽤担⼼像 scanf ⼀样忘记写取地址符 & 了耶),注意 cin 是向右的箭头,表示将内容输⼊到 n 中~
同样, cout << n; 和 printf("%d", n); ⼀样的意思,此时 cout 是向左的两个箭头,注意和 cin 区分开来~
⽽且不管 n 是 double 还是 int 或者是 char 类型,只⽤写 cin >> n; 和 cout << n; 这样简单的语句就好,不⽤像C语⾔中需要根据 n 的类型对应地写 %lf 、 %d 、 %c 这样麻烦~
endl 和 "\n" 的效果⼤致相同, endl 的全称是end of line,表示⼀⾏输出结束,然后输出下⼀⾏~(微⼩区别是,使⽤ endl 会⽐使⽤ "\n" 换⾏后多⼀个刷新输出缓冲区的操作,but不必care这些细节…)⼀般如果前⾯是个字符串引号的话直接 "\n" ⽐较⽅便,如果是变量之类的我觉得写 endl 会⽐较好看~想⽤哪个就⽤哪个~
cout << "hello, guy~\n"; |
cin 和 cout 虽然使⽤起来更⽅便,但是输⼊输出的效率不如 scanf 和 printf 快,所以如果是做PAT⼄级⾥⾯那种简单、对时间复杂度要求不⾼的题⽬,直接⽤ cin 和 cout 会觉得写起来⽐较省事⼉;如果题⽬对时间复杂度要求⽐较⾼,全都改成 scanf 和 printf 可以提⾼代码的输⼊输出效率,⽐如有的时候发现⽤ cin 、 cout 做题⽬超时了,改成 scanf 和 printf 就AC了~
头文件
C++的头⽂件⼀般是没有像C语⾔的 .h 这样的扩展后缀的,⼀般情况下C语⾔⾥⾯的头⽂件去掉 .h 然后在前⾯加个 c 就可以继续在C++⽂件中使⽤C语⾔头⽂件中的函数啦~⽐如:
变量声明
C语⾔的变量声明⼀般都在函数的开头,但是C++在⾸次使⽤变量之前声明即可~(当然也可以都放在函数的开头),⽽且⼀般C语⾔⾥⾯会在 for 循环的外⾯定义 i 变量,但是C++⾥⾯可以在 for 循环内部定义~(关于这点, VC++6.0 ⾥⾯可能会发现代码复制进去编译不通过,这是因为这个编译器太⽼啦,建议不要⽤这么上古的编译器啦~)⽽且在 for 循环⾥⾯定义的局部变量,在循环外⾯就失效啦(就是脱离这个局部作⽤域就会查⽆此变量的意思),所以⼀个 main 函数⾥⾯可以定义好多次局部变量 i ,再也不⽤担⼼写的循环太多变量名 i 、 j 、 k 不够⽤啦~
|
bool变量
bool 变量有两个值, false 和 true ,以前⽤C语⾔的时候都是⽤ int 的 0 和 1 表示 false 和 true的,现在C++⾥⾯引⼊了这个叫做 bool (布尔)的变量,⽽且C++把所有⾮零值解释为 true ,零值解释为 false ~所以直接赋值⼀个数字给 bool 变量也是可以的~它会⾃动根据 int 值是不是零来决定给 bool 变量赋值 true 还是 false ~
bool flag = true; |
定义常量
和C语⾔相同,C++⾥⾯可以使⽤ const 这个限定符定义常量,⽐如 int 类型的常量 a 这样定义:
const int a = 99999999; // a为int类型的常量,它的值不可更改 |
string类
以前⽤ char[] 的⽅式处理字符串很繁琐,现在有了 string 类,定义、拼接、输出、处理都更加简单啦~不过 string 只能⽤ cin 和 cout 处理,⽆法⽤ scanf 和 printf 处理:
string s = "hello world"; // 赋值字符串 |
⽤ cin 读⼊字符串的时候,是以空格为分隔符的,如果想要读⼊⼀整⾏的字符串,就需要⽤ getline~
s 的⻓度可以⽤ s.length() 获取~(有⼏个字符就是⻓度多少,不存在 char[] ⾥⾯的什么末尾的结束符之类的~)
string s; // 定义⼀个空字符串s |
string 中还有个很常⽤的函数叫做 substr ,作⽤是截取某个字符串中的⼦串,⽤法有两种形式:
string s2 = s.substr(4); // 表示从下标4开始⼀直到结束 |
结构体
定义好结构体 stu 之后,使⽤这个结构体类型的时候,C语⾔需要写关键字 struct ,⽽C++⾥⾯可以省略不写:
struct stu { |
引用&传值
这个引⽤符号 & 要和C语⾔⾥⾯的取地址运算符 & 区分开来,他们没有什么关系,C++⾥⾯的引⽤是指在变量名之前加⼀个 & 符号,⽐如在函数传⼊的参数中 int &a ,那么对这个引⽤变量 a 做的所有操作都是直接对传⼊的原变量进⾏的操作,并没有像原来 int a ⼀样只是拷⻉⼀个副本(传值),举两个例⼦:
void func(int &a) { // 传⼊的是n的引⽤,相当于直接对n进⾏了操作,只不过在func函数中换了个名字叫a |
void func(int a) {// 传⼊的是0这个值,并不会改变main函数中n的值 |
STL
vector
之前C语⾔⾥⾯⽤ int arr[] 定义数组,它的缺点是数组的⻓度不能随⼼所欲的改变,⽽C++⾥⾯有⼀个能完全替代数组的动态数组 vector (有的书⾥⾯把它翻译成⽮量, vector 本身就是⽮量、向量的意思,但是叫做动态数组或者不定⻓数组我觉得更好理解,绝⼤多数中⽂⽂档中⼀般不翻译直接叫它vector~),它能够在运⾏阶段设置数组的⻓度、在末尾增加新的数据、在中间插⼊新的值、⻓度任意被改变,很好⽤~它在头⽂件 vector ⾥⾯,也在命名空间 std ⾥⾯,所以使⽤的时候要引⼊头⽂件 #include <vector> 和 using namespace std;
vector 、 stack 、 queue 、 map 、 set 这些在C++中都叫做容器,这些容器的⼤⼩都可以⽤ .size()获取到,就像 string s 的⻓度⽤ s.length() 获取⼀样~( string 其实也可以⽤ s.size() ,不过对于vector 、 stack 、 queue 、 map 、 set 这样的容器我们⼀般讨论它的⼤⼩ size ,字符串⼀般讨论它的⻓度 length ~其实 string ⾥⾯的 size 和 length 两者是没有区别、可以互换使⽤的。
|
vector 可以⼀开始不定义⼤⼩,之后⽤ resize ⽅法分配⼤⼩,也可以⼀开始就定义⼤⼩,之后还可以对它插⼊删除动态改变它的⼤⼩~⽽且不管在 main 函数⾥还是在全局中定义,它都能够直接将所有的值初始化为0(不⽤显式地写出来,默认就是所有的元素为0),再也不⽤担⼼C语⾔⾥⾯出现的那种 int arr[10]; 结果忘记初始化为0导致的各种bug啦~
vector<int> v(10); // 直接定义⻓度为10的int数组,默认这10个元素值都为0 |
不管是 vector 、 stack 、 queue 、 map 还是 set 都有很多好⽤的⽅法,这些⽅法都可以在官⽅⽹站中直接查询官⽅⽂档,上⾯有⽅法的讲解和代码示例~官⽅⽂档是刷题时候必不可少的好伙伴~如果你⽤的是 Mac OS 系统,下载软件 Dash 然后在⾥⾯下载好C++,平时查⽂档会更⽅便,我平时做开发写算法都在 Dash ⾥⾯查⽂档,内容和官⽅⽂档是⼀样的~)PS:经此教程读者Keil Glay提醒, Windows 下有受 Dash 启发⽽开发的离线⽂档浏览器 Zeal ,和 Dash 的功能⼀样,使⽤ Windows 的⼩伙伴可以下载 Zeal 看离线官⽅⽂档~
⽐如进⼊官⽹搜索 vector ,就会出现 vector 拥有的所有⽅法,点进去⼀个⽅法就能看到这个⽅法的详细解释和代码示例~当然我们平时写算法⽤不到那么多⽅法啦,只有⼏个是常⽤的~以下是⼀些常⽤的 vector ⽅法:
|
容器 vector 、 set 、 map 这些遍历的时候都是使⽤迭代器访问的, c.begin() 是⼀个指针,指向容器的第⼀个元素, c.end() 指向容器的最后⼀个元素的后⼀个位置,所以迭代器指针 it 的for循环判断条件是 it != c.end()
访问元素的值要对 it 指针取值,要在前⾯加星号~所以是 cout << *it;这⾥的auto相当于 vector<int>::iterator 的简写,关于 auto 下⽂有讲解~
set
set 是集合,⼀个 set ⾥⾯的各元素是各不相同的,⽽且 set 会按照元素进⾏从⼩到⼤排序~以下是 set 的常⽤⽤法:
|
map
map 是键值对,⽐如⼀个⼈名对应⼀个学号,就可以定义⼀个字符串 string 类型的⼈名为“键”,学号 int 类型为“值”,如 map<string, int> m; 当然键、值也可以是其它变量类型~ map 会⾃动将所有的键值对按照键从⼩到⼤排序, map 使⽤时的头⽂件 #include <map> 以下是 map 中常⽤的⽅法:
|
stack
栈 stack 在头⽂件 #include <stack> 中,是数据结构⾥⾯的栈~以下是常⽤⽤法:
|
queue
队列 queue在头⽂件 #include <queue> 中,是数据结构⾥⾯的队列~以下是常⽤⽤法:
|
unordered_map & unordered_set
unordered_map 在头⽂件 #include <unordered_map> 中, unordered_set 在头⽂件 #include<unordered_set> 中~
unordered_map 和 map (或者 unordered_set 和 set )的区别是, map 会按照键值对的键 key 进⾏排序( set ⾥⾯会按照集合中的元素⼤⼩进⾏排序,从⼩到⼤顺序),⽽ unordered_map (或者 unordered_set )省去了这个排序的过程,如果偶尔刷题时候⽤ map 或者 set 超时了,可以考虑⽤ unordered_map (或者 unordered_set )缩短代码运⾏时间、提⾼代码效率~⾄于⽤法和 map 、 set 是⼀样的~
位运算
bitset ⽤来处理⼆进制位⾮常⽅便。头⽂件是 #include <bitset> , bitset 可能在PAT、蓝桥OJ中不常⽤,但是在LeetCode OJ中经常⽤到~⽽且知道 bitset 能够简化⼀些操作,可能⼀些复杂的问题能够直接⽤ bitset 就很轻易地解决~以下是⼀些常⽤⽤法:
|
sort函数
sort 函数在头⽂件 #include <algorithm> ⾥⾯,主要是对⼀个数组进⾏排序( int arr[] 数组或者 vector 数组都⾏), vector 是容器,要⽤ v.begin() 和 v.end() 表示头尾;⽽ int arr[] ⽤ arr 表示数组的⾸地址, arr+n 表示尾部~
|
⾃定义cmp函数
sort 默认是从⼩到⼤排列的,也可以指定第三个参数 cmp 函数,然后⾃⼰定义⼀个 cmp 函数指定排序规则~ cmp 最好⽤的还是在结构体中,尤其是很多排序的题⽬~⽐如⼀个学⽣结构体 stu 有学号和成绩两个变量,要求如果成绩不同就按照成绩从⼤到⼩排列,如果成绩相同就按照学号从⼩到⼤排列,那么就可以写⼀个 cmp 数组实现这个看上去有点复杂的排序过程:
|
注意: sort 函数的 cmp 必须按照规定来写,即必须只是 > 或者 < ,⽐如: return a > b; 或 者 return a < b; ⽽不能是 <= 或者 >= ,因为快速排序的思想中, cmp 函数是当结果为 false 的时候迭代器指针暂停开始交换两个元素的位置,当 cmp 函数 return a <= b 时,若中间元素前⾯的元素都⽐它⼩,⽽后⾯的元素都跟它相等或者⽐它⼩,那么 cmp 恒返回 true ,迭代器指针会不断右移导致程序越界,发⽣段错误~
cctype头⽂件函数
刚刚在头⽂件那⼀段中也提到, #include <cctype> 本质上来源于C语⾔标准函数库中的头⽂件 #include <ctype.h> ,其实并不属于C++新特性的范畴,在刷PAT⼀些字符串逻辑题的时候也经常⽤到,但是很多⼈似乎不了解这个头⽂件中的函数,所以在这⾥单独提⼀下~
可能平时我们判断⼀个字符是否是字⺟,可能会写:
char c; |
但是在 cctype 中已经定义好了判断这些字符应该所属的范围,直接引⼊这个头⽂件并且使⽤⾥⾯的函数判断即可,⽆需⾃⼰⼿写(⾃⼰⼿写有时候可能写错或者漏写~)
|
不仅仅能判断字⺟,还能判断数字、⼩写字⺟、⼤写字⺟等~C++官⽅⽂档中对这些函数归纳成了⼀个表格,我列出了官⽹的函数与所属范围总结表,有兴趣的可以看⼀下:https://www.liuchuo.net/archives/2999
总的来说常⽤的只有以下⼏个:
isalpha字⺟(包括⼤写、⼩写)
islower(⼩写字⺟)
isupper(⼤写字⺟)
isalnum(字⺟⼤写⼩写+数字)
isblank(space和\t)
isspace( space 、\t、\r、\n)
cctype 中除了上⾯所说的⽤来判断某个字符是否是某种类型,还有两个经常⽤到的函数: tolower和 toupper ,作⽤是将某个字符转为⼩写或者⼤写,这样就不⽤像原来那样⼿动判断字符c是否是⼤写,如果是⼤写字符就 c = c + 32; 的⽅法将 char c 转为⼩写字符啦~这在字符串处理的题⽬中也是经常⽤到:
char c = 'A'; |
C++11
C++11是2011年官⽅为C++语⾔带来的新语法新标准,C++11为C++语⾔带来了很多好⽤的新特性,⽐如 auto 、 to_string() 函数、 stoi 、 stof 、 unordered_map 、 unordered_set 之类的~现在⼤多数OJ都是⽀持C++11语法的,有些编译器在使⽤的时候需要进⾏⼀些设置才能使⽤C++11中的语法,否则可能会导致编译器上编译不通过⽆法运⾏,总之C++11的语法在OJ⾥⾯是可以使⽤的~⽽且很多语法很好⽤~以下讲解⼀些C++11⾥⾯常⽤的新特性~
atuo声明
auto 是C++11⾥⾯的新特性,可以让编译器根据初始值类型直接推断变量的类型。⽐如这样:
auto x = 100; // x是int变量 |
当然这个在算法⾥⾯最主要的⽤处不是这个,⽽是在STL中使⽤迭代器的时候, auto 可以代替⼀⼤⻓串的迭代器类型声明:
// 本来set的迭代器遍历要这样写: |
基于范围的for循环
除了像C语⾔的for语句 for (i = 0; i < arr.size(); i++) 这样,C++11标准还为C++添加了⼀种新的 for 循环⽅式,叫做基于范围(range-based)的for循环,这在遍历数组中的每⼀个元素时使⽤会⽐较简便~⽐如想要输出数组 arr 中的每⼀个值,可以使⽤如下的⽅式输出:
int arr[4] = {0, 1, 2, 3}; |
i 变量从数组的第⼀个元素开始,不断执⾏循环, i 依次表示数组中的每⼀个元素~注意,使⽤ int i 的⽅式定义时,该语句只能⽤来输出数组中元素的值,⽽不能修改数组中的元素,如果想要修改,必须使⽤ int &i 这种定义引⽤变量的⽅式~⽐如想给数组中的每⼀个元素都乘以 2 ,可以使⽤如下⽅式:
int arr[4] = {0, 1, 2, 3}; |
这种基于范围的 for 循环适⽤于各种类型的数组,将上述两段代码中的 int 改成其他变量类型如 double 、 char 都是可以的~另外,这种 for 循环⽅式不仅可以适⽤于数组,还适⽤于各种STL容器,⽐如 vector 、 set 等~加上上⾯⼀节所讲的C++11⾥⾯很好⽤的 auto 声明,将 int 、 double 等变量类型替换成 auto ,⽤起来就更⽅便啦~
// v是⼀个int类型的vector容器 |
to_string
to_string 的头⽂件是 #include <string> , to_string 最常⽤的就是把⼀个 int 型变量或者⼀个数字转化为 string 类型的变量,当然也可以转 double 、 float 等类型的变量,这在很多PAT字符串处理的题⽬中很有⽤处,以下是示例代码:
|
stoi & stod
使⽤ stoi 、 stod 可以将字符串 string 转化为对应的 int 型、 double 型变量,这在字符串处理的很多问题中很有帮助~以下是示例代码和⾮法输⼊的处理⽅法:
|
stoi如果遇到的是⾮法输⼊(⽐如stoi(“123.4”),123.4不是⼀个int型变量):
会⾃动截取最前⾯的数字,直到遇到不是数字为⽌(所以说如果是浮点型,会截取前⾯的整数部分)
如果最前⾯不是数字,会运⾏时发⽣错误
stod如果是⾮法输⼊:
会⾃动截取最前⾯的浮点数,直到遇到不满⾜浮点数为⽌
如果最前⾯不是数字或者⼩数点,会运⾏时发⽣错误
如果最前⾯是⼩数点,会⾃动转化后在前⾯补0
不仅有stoi、stod两种,相应的还有:
stof(string to float)
stold(string to long double)
stol(string to long)
stoll(string to long long)
stoul(string to unsigned long)
stoull(string to unsigned long long)
Dev-Cpp
如果想要在 Dev-Cpp ⾥⾯使⽤C++11特性的函数,⽐如刷算法中常⽤的 stoi 、 to_string 、 unordered_map 、 unordered_set 、 auto 这些,需要在设置⾥⾯让dev⽀持C++11,需要在菜单栏中⼯具-编译选项-编译器-编译时加⼊ -std=c++11 这句命令即可~