[关闭]
@fcxxzux 2017-02-15T21:25:43.000000Z 字数 4199 阅读 1350

Learning a Part of C++(for ACM/ICPC) (1) 简介

一、目标

没什么高大上的目标,就是简单介绍一下对ACM/ICPC来讲比较实用的C++知识,顺带把最近做题中使用C++所踩过的坑给记载下来,各位谨防中招。

前提:熟悉C语言的基本语法

内容:

下面开工!

二、关乎ACM的C与C++的不同

1、更强的类型检查

我们先举一个栗子:

Exp 1.01

以下一份代码,保存为不同的.c和.cpp文件,分别用C语言和C++编译器编译,这两个文件能否编译通过,提示什么?

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int* test(){
  4. int ans=0;
  5. return ans;
  6. }
  7. int main(){
  8. test();
  9. return 0;
  10. }

至少你想把一个int型变量强行塞给一个int的指针类型变量是不可能通过编译的了!

还有一个问题:const这个关键词,他的含义和能力有了一点加强:

Exp 1.02

以下一份代码,保存为不同的.c和.cpp文件,分别用C语言和C++编译器编译,观察编译器所提示的Error。

  1. #include <stdio.h>
  2. const int n=5;
  3. int arr[n];
  4. void test(int *p){
  5. *p=10;
  6. }
  7. int main(){
  8. test(&n);
  9. printf("%d\n",n);
  10. return 0;
  11. }

首先,在C语言中,隐式类型转换完全可能把const这个约束丢弃了,而在C++中,const是不会被隐式类型转换丢弃的,不会把const的指针、引用等通过隐式类型转换来转换成相应的非const类型。

其次,在C语言中,用const修饰的变量仍然被视为变量,而不是一个常量,不能当做静态数组的下标范围使用,而在C++中,这里n就被视作一个int类型的常量,能当做数组的下标范围使用。

Tips 1.01

强烈推荐使用const来定义常量,应该尽量避免再使用#define定义常量。
首先,const指定了具体类型,不会出现一些意想不到的类型不正确的情况
其次,#define只做了字面上的替换,并没有考虑前后文的语义,而const会做完运算后只存储一个具体值,不会出现下面的问题:


Exp 1.03
以下一份代码中,数组a和b的大小分别是多少?为什么?
(我很疑惑,明明开了2倍的最大边数的大小的数组,为什么我还会RE……)

  1. #include <stdio.h>
  2. const int NMAX_VER1=100000+5;
  3. #define NMAX_VER2 100000+5
  4. int a[NMAX_VER1*2],b[NMAX_VER2*2];
  5. int main(){
  6. printf("%u %u\n"
  7. ,sizeof(a)/sizeof(int)
  8. ,sizeof(b)/sizeof(int));
  9. return 0;
  10. }

2、函数重载与参数默认值

(不要求会多么绝妙的使用,认识这个为主,后面会有地方要用)

(我承认这里不是个很好的例子,后面会有要用的地方的)
假设,一个题里你既要根据两个给定的点算2点间距离,又要根据两点间的横纵坐标差计算一下距离,在C语言中怎么写?

  1. struct Point{
  2. double x;
  3. double y;
  4. };
  5. double getDistancebyPoint(struct Point a,struct Point b){
  6. return sqrt(pow(a.x-b.x,2)+pow(a.y-b.y,2));
  7. }
  8. double getDistancebyDelta(double dx,double dy){
  9. return sqrt(dx*dx+dy*dy);
  10. }

这两个函数做了相似的事情,为了支持不同的变量类型,我们不得不给他们名字后面再加一串。如果能起个相同的函数名字就好了。

所以,C++有一个新特性叫函数重载。我们修改一下上面的代码,使用C++编译器编译:

  1. struct Point{
  2. double x;
  3. double y;
  4. };
  5. double getDistance(Point a,Point b){
  6. return sqrt(pow(a.x-b.x,2)+pow(a.y-b.y,2));
  7. }
  8. double getDistance(double dx,double dy){
  9. return sqrt(dx*dx+dy*dy);
  10. }

Tips 1.02
在C++里,你不需要再typedef struct Point Point;就可以直接使用这个Point的类型名了

这两个函数有相同的名字,只是参数不一样,通过了编译。
这个特性就是函数重载了。

Tips 1.03
函数的重载,要求有相同的函数名,不同的参数列表,这样才能通过函数的调用形式来区分到底要执行哪个。
相同的函数名,还完全一样的参数列表,只是不一样的返回值类型,编译器并没有办法区分这几个重载函数,会出现编译错误。


至于参数默认值,我们来看下面的例子:


Exp 1.04
以下一份代码,输出什么?

  1. #include <stdio.h>
  2. int getSum(int a,int b=5,int c=6){
  3. return a+b+c;
  4. }
  5. int main(){
  6. printf("%d\n",getSum(10));
  7. printf("%d\n",getSum(10,2));
  8. printf("%d\n",getSum(10,2,1));
  9. return 0;
  10. }

其实这里可以看做,getSum由编译器实现了函数重载,实现了以下3个版本:

  1. int getSum(int a){
  2. int b=5,c=6;
  3. return a+b+c;
  4. }
  5. int getSum(int a,int b){
  6. int c=6;
  7. return a+b+c;
  8. }
  9. int getSum(int a,int b,int c){
  10. return a+b+c;
  11. }

于是,你不能再声明一个:

  1. int getSum(int a){
  2. return a+42;
  3. }

编译器会提示,有二义性,无法选择使用哪一个版本的函数。

还有一点需要注意:有默认值的函数参数,必须是参数列表的最后几个

我们将会在之后的类的封装之构造函数部分再次涉及上面的内容。

3、引用

有时候会碰到这种情况:
你被迫一个函数内完成计算,还要返回2个值……
怎么办?
1、写个结构体吧!结构体里有2个成员,分别代表2个不同的返回值
2、一个用return返回,另一个用指针的形式返回,如下:

  1. int calc(int inputParameter,int * outputParameter){
  2. *outputParameter=6*inputParameter;
  3. return 3*inputParameter;
  4. }

(请注意:这里这个outputParameter,不能是int类型,不然,函数参数只是传值的,你在内部的修改传不出来的。)

话说,用指针有些不太好的地方:
1、如果传入NULL指针,写进去就违规访问了
2、我就不喜欢在里面在看到解引用的*了,我也不想结构体成员用->访问,太丑了……

于是C++引入了新的概念,引用。

Exp 1.05
以下一份代码,输出什么?

  1. #include <stdio.h>
  2. int calcWithoutRef(int inputParameter,int outputParameter){
  3. printf("in calcWithoutRef %p\n",&outputParameter);
  4. outputParameter=6*inputParameter;
  5. return 3*inputParameter;
  6. }
  7. int calcWithRef(int inputParameter,int & outputParameter){
  8. printf("in calcWithRef %p\n",&outputParameter);
  9. outputParameter=6*inputParameter;
  10. return 3*inputParameter;
  11. }
  12. int main(){
  13. int receive=0;
  14. printf("in main %p\n",&receive);
  15. int ret=calcWithoutRef(10,receive);
  16. printf("%d %d\n",ret,receive);
  17. ret=calcWithRef(15,receive);
  18. printf("%d %d\n",ret,receive);
  19. return 0;
  20. }

引用类型,从某个角度来讲,你可以视作相应的原始变量的一种别名。
这个别名,和原始变量共享相同的内存空间。
注意到,这里参数传入引用,函数里只是会访问相同的地址罢了,而不会把那个对象的值复制进去,所以,如果穿的是个结构体引用类型,就不会复制一份结构体,加快调用速度,你在函数里对这个结构体引用类型的修改也会影响外部这个结构体实际的值。当然,如果你不想修改,你可以选择用const关键词约束:

  1. int calcWithRef(int inputParameter,const int & outputParameter);

什么,不实用?

我们看个例子吧——当然是分享一些模板了:
1、求逆元(没学过逆元没关系,一,这很常用,不会也会被迫知道什么时候用的,二,离散数学课有):
回头补一句:逆元不是什么时候都用的。逆元是,当你涉及到要计算,保证b和c互质的情况下,才能使用的,这样把转化成,才能保证结果的正确性

  1. typedef long long ll;
  2. void extgcd(ll a,ll b,ll& d,ll& x,ll& y){
  3. if(!b){
  4. d=a;x=1;y=0;
  5. }else{
  6. extgcd(b,a%b,d,y,x);
  7. y-=x*(a/b);
  8. }
  9. }
  10. ll mod_inv(ll a,ll n){//a在模n意义下的逆元
  11. ll d,x,y;
  12. extgcd(a,n,d,x,y);
  13. return d==1?(x+n)%n:-1;
  14. }

2、求欧拉回路的核心DFS(CopyRight Claris):

  1. void dfs(int x){
  2. for(int&i=g[x];i;){
  3. if(vis[i]){
  4. i=nxt[i];
  5. continue;
  6. }
  7. vis[i]=vis[i^1]=1;
  8. int j=w[i];
  9. dfs(v[i]);
  10. ans[++cnt]=j;
  11. }
  12. }

还有一个必须要用引用的地方,我们在之后讲到STL的sort的时候(第三讲,运算符重载)会介绍一下(不用就很容易导致超时的悲剧发生……)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注