前言
C++ 是一个很好的编程语言,但它因为许多特性而受到一些人的唾弃。一起来看看这份避坑指南,你一定会有收获。如果有更好的点子,请评论或私信。
一、赋值运算
平时,我们经常用到赋值运算。但你知道它有返回值吗?对比以下程序:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a,b;
cin>>a>>b;
if(a=b)cout<<"Yes"<<endl;//a=b 表示将b的值赋给a,返回a
else cout<<"No"<<endl;
return 0;
}
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a,b;
cin>>a>>b;
if(a==b)cout<<"Yes"<<endl;//a==b 表示 a与b相等
else cout<<"No"<<endl;
return 0;
}
利用这点,可以写出以下代码(读入字符串):
#include <bits/stdc++.h>
using namespace std;
int main()
{
char ch;
while(ch=getchar()!='\n')
{
...
}
return 0;
}
二、自加或自减带返回值
看下面两个:
int a=1,b;
b=++a;//先++,再赋值
cout<<a<<' '<<b<<endl;
int a=1,b;
cin>>a;
b=a++;//先赋值,再++
cout<<a<<' '<<b<<endl;
关于数组与 ++
与 --
操作时,一定要注意!
三、万能头文件
万能头文件包含了几乎所有 C++ 头文件,在比赛中经常被人们使用。它的写法是:
#include <bits/stdc++.h>
但它也有不好的地方,比如下面:
#include <bits/stdc++.h>
using namespace std;
int map[100];//与 STL的map容器重名,编译错误
int main()
{
for(int i=0;i<100;i++)cin>>map[i];
for(int i=0;i<100;i++)cout<<map[i]<<' ';
return 0;
}
一定要慎用万能头文件!
四、vector 越界
有人会问:vector
不是不定长数组吗?怎么也会越界?看下面:
#include <bits/stdc++.h>
using namespace std;
int main()
{
vector<int> vec;//长度为0
vec[1]=1;//越界
return 0;
}
这下明白了吧。相当于下面:
#include <bits/stdc++.h>
using namespace std;
int a[0];
int main()
{
a[1]=1;//越界
return 0;
}
vector
长度为
0
0
0,却访问了
v
e
c
1
vec_1
vec1,肯定越界啦。可以在定义时写成下面两种方法:
#include <bits/stdc++.h>
using namespace std;
int main()
{
vector<int> vec(100);//定义一个长度为100的vector变量vec
vec[1]=1;
return 0;
}
#include <bits/stdc++.h>
using namespace std;
int main()
{
vector<int> vec;//长度为0
vec.resize(100);//将长度变成100
vec[1]=1;
return 0;
}
五、隐式类型转换
编译器有时会“偷偷”地把类型给转换。如:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a=true;//bool -> int
if(a)//int -> bool
{
char ch=32;//int -> char
printf("%d\n",ch);
int i=3;
double j=3.1;
cout<<i+j<<endl;//int -> double
}
return 0;
}
隐式类型转换有时会造成 bug 的出现,一定要小心!
六、宏
宏可以用符号代替一些指令,如:
#define pi 3.1415926
#define INF 0x3f3f3f3f
宏十分方便,可以让代码精简。但有时也会产生 bug:
#include <bits/stdc++.h>
using namespace std;
#define mul(a,b) a*b
#define mul2(a,b) (a)*(b)
int main()
{
cout<<mul(1+2,3+4)<<' '<<mul2(1+2,3+4)<<endl;
return 0;
}
m u l mul mul 执行算法: 1 + 2 × 3 + 4 = 11 1+2\times3+4=11 1+2×3+4=11
m u l 2 mul2 mul2 执行算法: ( 1 + 2 ) × ( 3 + 4 ) = 21 (1+2)\times(3+4)=21 (1+2)×(3+4)=21
要看好运算顺序呀!
七、vector大小
上面第四章讲过 vector
的注意事项和第五章的类型转换,这里讲讲 size
。
vector.size()
的函数其实返回的是 unsigned int
型,它和一个 int
型加减会让 int
变成 unsigned int
,如果结果是负数,就会变成一个很大的值(下溢)。建议写代码的时候写成 (int)vector_name.size()
。
八、空指针
大家一般给指针变量赋空值应该是下面这样的吧:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int *p=NULL;
return 0;
}
其实,NULL
是一个宏,它代表的是
0
0
0,可以起到赋空指针值的作用。但其实有个专门的 nullptr
赋空指针值。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int *p=nullptr;//正确,赋空指针
int a=nullptr;//错误,无法转换成int
bool pd=nullptr;//正确,可以转成bool
return 0;
}
九、memset和fill
memset
以 Bite(字节) 为单位进行赋值。如下:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a[100];
memset(a,0,sizeof(a));//a数组全为零
memset(a,1,sizeof(a));
return 0;
}
至于上述代码第 7 7 7 行,是这样的:
一个
i
n
t
int
int 占
4
4
4 字节,每个字节赋值为一,则
a
a
a 数组每个元素的值为二进制下的
000000010000000010000000100000001
000000010000000010000000100000001
000000010000000010000000100000001(即十进制下的
16843009
16843009
16843009)。那么如何赋值为
1
1
1 呢?此时 fill
就能来帮忙。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a[100];
fill(a,a+100,3);//将a全部赋值为3
return 0;
}
十、输入输出的速度
输入有:scanf
,cin
。输出有:printf
,cout
。但他们的速度加上不同的优化各不相同。
本机测试(输出 1 1 1 到 100000 100000 100000):
cout:17s
printf:31s
关闭同步流+cout:1s
cout.tie(0)+cout:17s
O1,O2等优化:无效
关闭同步流的指令:ios::sync_with_stdio(false)
。
我又测试了输出
1
1
1 到
1000000
1000000
1000000:
cout:141s
printf:314s
关闭同步流+cout:3s
现在你知道谁的速度快了吗?
十一、未定义行为
几乎每个编程语言都有未定义行为(Undefined Behavior,简称 UB),最著名的是 C/C++(因为它强大的兼容性)。用一张图总结:
UB 一般不容易检查出来,有时会导致“我在本地 AC,却在 OJ/比赛上 RE/WA/TLE”的情况。UB 分为很多种,下面来逐个介绍。
11.1 地址错误型
11.1.1 数组或 vector
越界
在一个经验丰富的 OIer 的代码里,这个应该在程序里很少见。我们在第四章已经介绍过 vector
越界的相关内容。这里就不详细介绍。
11.1.2 指针漂移
看到这个标题,你可能会有疑问。看看下面这两个代码,你就明白了。
#include <bits/stdc++.h>
using namespace std;
int a[5]={0,1,2,3,4};
int main()
{
int *p=a+5;
cout<<*p<<endl;
return 0;
}
#include <bits/stdc++.h>
using namespace std;
set<int> s;
void init()
{
for(int i=0;i<10;i++)s.insert(i);
}
int main()
{
init();
for(set<int>::iterator it=s.begin();it!=s.end();it++)
{
int tmp=*it;
if(tmp==5)s.erase(it);
else cout<<tmp<<endl;
}
return 0;
}
我们先看第一个代码。这里的 p p p 指针指向了一个啥也不是的位置,输出是一个随机值。
第二个代码中,当
i
t
it
it 指针删掉
5
5
5 以后,这里的 s.end()
和 it
已经失效,于是便 RE 了。
11.1.3 重复销毁已经被销毁的指针
比如:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int *p=new int;
delete p;
delete p;
return 0;
}
我们很容易发现, p p p 被销毁了两次,从而导致 RE。
11.1.4 总结
有下面几种指针 UB,比较难察觉,有一些上面没有列出来,现在在这里总结。千万不要像P4565题面的主人公一样,分数最后变成一场空。
-
解引用
nullptr
指针; -
解引用
new
定义失败的指针; -
不恰当使用
erase
成员函数; -
重复销毁已经被销毁的指针;
-
指针越界访问;
-
用指针修改
const
类型变量。
11.2 变量问题型
11.2.1 有符号型变量溢出
先来看一个代码:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int x;
cin>>x;
if(x+1<x)cout<<"Overflow!";
else cout<<"Not overflow!";
return 0;
}
现在,请把这份代码拷贝到自己的 IDE 里,测试几组数据,看看结果怎样?再打开 -O2 优化,再看看结果?
如果你的编译器版本较高或开了 -O2 优化,输入 2147483648 2147483648 2147483648,发现输出的竟然是 Not overflow! \texttt{Not overflow!} Not overflow!!这是怎么回事?
原来,是因为 O2 优化惹的祸!我们脱离计算机,谁都知道 x + 1 > x x+1>x x+1>x。所以,编译器会自动帮我们把程序改成这样:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int x;
cin>>x;
cout<<"Not overflow!";
return 0;
}
注意,无符号型变量溢出是正常的。将上述程序的
x
x
x 改为 unsigned
类型,再试试?会发现结果就对了。
11.2.2 除 0 0 0 操作
我们都知道, 0 0 0 是不能当除数的。所以,编译器会把结果看成 N a N NaN NaN(即 not a number)。
11.2.3 自动省略无限循环代码
来看看这份代码:
#include <bits/stdc++.h>
using namespace std;
bool function()
{
unsigned cnt=0;
while(1)
if(cnt<0)return true;//永远不可能返回
return false;//永远不可能返回
}
int main()
{
if(function())cout<<"Amazing!";
else cout<<"Normal!";
return 0;
}
如果在低版本编译器下,程序死循环;但在新版本编译器下,程序无输出,直接结束。原来,是因为新版本编译器直接对 main()
函数进行优化,直接优化成空。
11.2.4 顺序问题
看看下面代码:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a=1;
cout<<(a++ + ++a)<<endl;
return 0;
}
根据我们第二章的内容,这个程序到底输出结果是什么?编译器不一定是按照从左到右进行运算的。当然,有人为了考验编译器,写出下面的代码:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a=1;
cout<<(++++++++++a)<<endl;
return 0;
}
当然,这个代码也能够运行。
11.2.5 访问未初始化变量
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a;
if(a)cout<<"true"<<endl;
if(!a)cout<<"false"<<endl;
return 0;
}
这个代码会输出什么?在老的编译器里,这两个字符串都会被输出。
*关于编译器
2.1 CSP C++编译器配置
据我所知,目前C++14编译器完全支持。也就是说,atoi
,itoa
,auto
等 C++11 的新指令完全支持。
2.2 不同IDE设置
- Geany与Code::Blocks
他们俩默认支持 C++98,咋办?可以在代码第一行加上:#pragma GCC diagnostic error "-std=c++11"
。
- DEV-C++
几乎所有人都知道这个 IDE 吧。去到 工具[T] => 编译选项[C] ,在编译器标签里勾选 编译时加入以下命令: 在下面的命令框中输入 -std=c++11
,即可启用 C++11 编译器。