C语言常见问题

概述本次分享主要介绍我在C语言中提出的三个基础小问题

一、头文件<stdio.h>与”stdio.h”的区别

C语言包含头文件有两种写法#include <stdio.h>和#include “stdio”(#include后面可以不空格),两种写法的区别如下:

  1. #include <头文件> : 编译器只会从系统配置的库环境中去寻找头文件,不会搜索当前文件夹。通常用于引用标准库头文件。
  2. #include “头文件” : 编译器会先从当前文件夹中寻找头文件,如果找不到则到系统默认库环境中去寻找。一般用于引用用户自己定义使用的头文件。

举例:

1.我们新建一个项目xx,只包含main.cpp一个文件

img

可以看到,无论是#include”stdio.h”还是#include<stdio.h>程序都能正常运行

解析:1)使用#include<stdio.h>添加头文件时,编译器到标准库头文件中查找,发现stdio.h头文件,将其复制到当前行;2)而使用#include”stdio.h”添加头文件时,编译器先到当前项目所在的文件夹中查找stdio.h头文件,发现没有这个头文件,于是编译器到系统默认库环境中去寻找,发现stdio.h头文件,将其复制到当前行。这里展开的stdio.h头文件和#include<stdio.h>展开的头文件是同一个。

2.在xx项目中添加一个头文件stdio.h,并使用#include”stdio.h”

img

img

编译器报错,原因是printf未声明。

解析:使用#include”stdio.h”添加头文件时,编译器先到当前项目所在的文件夹中查找stdio.h头文件,发现用户自定义的stdio.h头文件,于是编译器将其复制到当前行。这里展开的头文件是用户自定义的头文件,用户自定义的stdio.h头文件没声明printf函数,所以当然会报错。

3.再往stdio.h文件里添加#include<stdio.h>

img

程序就能正常运行了。

附加:

1.stdio.h
定义了输入输出操作,包括标准格式化输入输出(scanf、printf),文件格式化输入输出(fscanf、fprintf),字符字符串输入输出(getchar、getsput、char、puts…)等等。

一般情况下,C程序都会包含输入输出操作,因此这是最常用的头文件。此外涉及文件的操作也常常需要包含stdio.h头文件。

2.stdlib.h
定义了几个通用功能,包括动态内存管理(malloc、free等),随机数生成(rand、srand),与环境的通信,整数算术,搜索,排序和转换。

涉及链表的操作,需要该头文件。

3.math.h
定义了三角函数、反三角函数、指数函数、对数函数、开平方sqrt()、开立方cbrt()、取绝对值fabs()等常见数学运算函数。

//注意:fabs参数为double类型,而abs参数为int类型。fabs是求实数的绝对值,abs是求整数的绝对值

4.stdbool.h
定义了bool类型。

在C99标准之前,C语言没有bool类型。C99标准中新增的头文件stdbool.h中引入了bool类型,与C++中的bool兼容。

此外C99新增的关键字_Bool可以表示布尔类型。

5.string.h
定义了操纵字符串和数组的函数。包括下列几个函数:

char * strcpy ( char *destin, char *source);//复制字符串

char * strncpy ( char *dest, char *src, size_t n);//将字符串src中最多n个字符复制到字符数组dest中

char * strcat ( char *destin, char *source);//连接字符串

int strcmp ( char *str1, char *str2);//看ASCII码,str1>str2,返回值 > 0;两串相等,返回0

size_t strlen ( const char *s);//求字符串的长度,从字符串的首地址开始到遇到第一个’\0’停止计数

void swab ( char *from, char *to, int nbytes);//交换字节

二、main与return 0

1.int main

C99标准规定C语言main函数返回类型一定为int,并且只有两种写法:

    1)标准写法一(无参数)

    int main(void) { /* ... */ }

    在C++中,int main()和int main(void)没有任何区别。但是在C语言中,int main()是可以传入参数的,而int main(void)则不能传入参数。

    2)标准写法二(带参数)

    int main(int argc,char *argv[]) { /* ... */ } 

    int main(int argc,char **argv) { /* ... */ } //与标准写法二等价

    C语言允许main函数带参数,其中argc表示参数个数,argv[0]为程序的路径,argv[1]指向命令行中执行程序名后的第一个字符串,argv[2]指向第个字符串...以此类推。

2.void main

void main(),这种写法在老版本的C语言代码中比较常见,不过现在的一些教材中仍存在这种写法,事实上这种写法是错误的、不规范的,因为C标准中从来没有规定void main()这种写法。

但,有同学会说:“我尝试了void main(),是可以使用的。”的确,有的编译器比如VC6.0(C89)是支持void main这种写法的,但并非所有编译器都是如此,很多编译器是不支持这种写法的,比如codeblocks(如下图)。所以为了程序的可移植性,最好不要采用void main这种写法。

3.return 0

作用是返回整数0给操作系统,表示程序正常运行结束了。由于C99规定C语言的main函数一定是int类型,也就是说main函数会返回一个整数。如果返回0,则代表程序正常退出;如果返回值非0,则表示程序出现了异常。这个返回值在操作系统的命令行窗口是可见的。(注意一下main函数返回值是返回给操作系统的,只要你设置返回值了,并不会影响你当前所编写程序的执行。)

有时即使我们忘了写return 0,程序也能正常运行,这是因为C99 规定编译器要自动在生成的目标文件中加入return 0。但是要注意,vc6.0编译器并不会自动为你加上return 0(可能因为vc6.0是98年的产品,所以不支持这个特性),这时编译器会报错。所以,我们平时在写代码的过程中,一定要养成给main函数设置返回值的习惯。

三、++i与i++的详解

一、++i与i++
1.引例
对于如下程序,其输出结果是什么

#include <stdio.h>
int main()
{
    int i=1,a=0,b=0;
    a=i++;
    b=++i;
    printf("i=%d,a=%d,b=%d\n",i,a,b);
    return 0;
}

img

解析:

a=i++;等价于a=i;i=i+1;

b=++i等价于i=i+1;b=i;

通俗地讲,i++是先赋值再自增,++i是先自增再赋值

2.(i++)+(i++)+(i++)与(++i)+(++i)+(++i)
那么,如果一条语句中有多个i++/++i呢?例如:

#include <stdio.h>
int main()
{
    int i=2,j=2,a=0,b=0;
    a=(i++)+(i++)+(i++);
    b=(++j)+(++j)+(++j);
    printf("i=%d,j=%d,a=%d,b=%d\n",i,j,a,b);
    return 0;
}

首先明确一点,多个++之间的和运算没有统一的定义,也就是说在不同编译环境下其结果有可能不同

在codeblocks编译器下(gcc/g++):

img

解析:

我们可以从结果来猜测运算的情况:

a=(i++)+(i++)+(i++)=2+3+4=9

b=(++j)+(++j)+(++j)=4+4+5=13

1)初始化时:i <- 2,j <- 2

2)对于多个表达式相加的情况,gcc是从左往右累加的,并且是两个表达式两个表达式地加。

3)在执行a=(i++)+(i++)+(i++)时,会以(i++)为单位把i的值传送回来再自增,运算过程就是(2+3)+4=5+4=9;a <- 9;

4)执行b=(++j)+(++j)+(++j)时,

遇到第一个(++j),j先自增一次(j=3);在遇到“+”时,不会先把j的值传送回来,而是先计算加法运算的第二个操作数,于是遇到第二个(++j),j自增一次(j=4),此时j的值(4)才传送回来进行加法运算,这时右边的表达式相当于(4+4)+(++j)=8+(++j);接着遇到第二个“+”,就先进行第三个(++j)的运算,j再自增一次(j=5),传送回j的值(5)之后才进行加法运算,即8+5=13;a <- 13;

在vs编译器下:

img

解析:

我们可以从结果来猜测运算的情况:

a=(i++)+(i++)+(i++)=2+2+2=6

b=(++j)+(++j)+(++j)=4+4+5=13

1)初始化时:i <- 2,j <- 2

2)在执行a=(i++)+(i++)+(i++)时,vs会以整个赋值语句为单位,先用i的初始值进行运算,待赋值语句结束后,才对i进行三次自增,即2+2+2=6;a<-6 ;i=i+1;i=i+1;i=i+1;

3)执行b=(++j)+(++j)+(++j)时,vs与gcc相同,在遇到“+”时,不会先把j的值传送回来,而是先对加法的第二个操作数进行运算,待参与加法的第二个操作数运算完成后才传送回j的值,运算过程为(4+4)+5=13;b <- 13;

在TC编译器下:

如果有多个表达式参与和运算,则先计算各个表达式,再进行加运算

如果i和j的初始值都为2,则

a=(i++)+(i++)+(i++)=2+2+2=6,

b=(++j)+(++j)+(++j)=5+5+5=15

a(b)在不同编译器中运算的结果不同,主要是因为变量i(j)传值回来的时间不同,以及多个表达式进行和运算时执行的方式不同(例如gcc和vs执行多表达式的和预算时,只能先计算左边两个表达式的和,等计算出和以后再计算该和与第三个表达式的和,以此类推;而TC可以同时进行多个表达式的加法运算)。

此外注意:在C程序的实际执行过程中,并不是直接一条一条高级语言地执行,我们会先对C代码进行预编译,然后编译成汇编代码,汇编代码经汇编生成二进制代码,二进制代码经链接生成可执行文件,最后我们才运行这个可执行文件。所以本质上a(b)在不同编译器中运算的结果不同,是因为对应语句生成的汇编代码不同。

3.总结
我们不推荐类似(i++)+(i++)+(i++)多个自增表达式相加的写法。因为这类写法的实现没有统一的定义,在不同的编译环境下可能有不同的结果。

二、函数中的++
1.printf中的++

int main()
{
    int i=0,a=0,b=0;
    i=1;
    printf("a=%d,i=%d\n",a=i++,i);
    i=1;
    printf("b=%d,i=%d\n",b=++i,i);
    return 0;
}

codeblocks:

img

vs:

img

可以看到printf中的++也是一个不明确的行为

2.++作为函数的参数

#include <stdio.h>
void f(int i1,int i2,int * pi){
    printf("f(%d,%d)在函数体中,i在内存中存储的值为%d\n",i1,i2,*pi);
    return;
}
int main()
{
    int i=0;
    f(i++,i++,&i);
    i=0;
    f(++i,++i,&i);
    return 0;
}

codeblocks:

img

vs:

img

可以看到,i++作为参数传递时也是一个不明确的行为

3.总结
在调用printf或者其他函数时,我们要避免在函数的参数表达式中使用自增运算符++。尽量在函数调用语句的前一条语句或后一条语句完成对变量值的修改,同时也可以考虑使用i=i+1替代++i作为参数。

结束此次分享结束,内容涉及少但详尽,还请见谅