究极C++避坑指南

前言

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

memsetBite(字节) 为单位进行赋值。如下:

#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;
}

十、输入输出的速度

输入有:scanfcin。输出有:printfcout。但他们的速度加上不同的优化各不相同。

本机测试(输出 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题面的主人公一样,分数最后变成一场空。

  1. 解引用 nullptr指针;

  2. 解引用 new定义失败的指针;

  3. 不恰当使用 erase成员函数;

  4. 重复销毁已经被销毁的指针;

  5. 指针越界访问;

  6. 用指针修改 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编译器完全支持。也就是说,atoiitoaauto等 C++11 的新指令完全支持。

2.2 不同IDE设置

  1. Geany与Code::Blocks

他们俩默认支持 C++98,咋办?可以在代码第一行加上:#pragma GCC diagnostic error "-std=c++11"

  1. DEV-C++

几乎所有人都知道这个 IDE 吧。去到 工具[T] => 编译选项[C] ,在编译器标签里勾选 编译时加入以下命令: 在下面的命令框中输入 -std=c++11,即可启用 C++11 编译器。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值