引言
关于由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 这句命令即可~