C语言里爬过的“坑”

Intro

那是一个下着大雨的夜晚,室外急促的雨声正好映衬着室内紧张的气氛,快,只剩下最后一个BUG了!终于在大半个小时之后解决了,仔细一看,原来是之前碰到过一次的问题了...心中不免想到,要是上次有好好记录就好了。于是,就动了整理这篇Blog的心思啦~
此Blog会长期不定时更新,记录下自己在跟老爷子C玩耍的过程中,碰到的一些坑爹之处(菜请轻喷,嘿嘿)~

scanf

关于scanf先说一点,scanf函数是带返回值的,有的编译器会忽略掉这个返回值(没有告警产生),但是实际上是存在的,切记。

scanf这个函数给用户留下的,实际上不是它的返回值,而是这个函数对用户能向其输入的东西的规定(😓说了一大堆让人听不懂的话...)。换句话说,也就是scanf自身对输入流中的数据的获取的机制可能会让用户“坑爹”。
对于C语言而言,在读取键盘输入的数据时,一般是带缓存的数据输入,需要按回车键才能完成该“行”数据的输入确定。
scanf对这个回车确认符并不进行处理,回车符会留在输入缓存区中。因此,在下一个“字符”操作函数(getchar()scanf("%c", &x)gets(s)等)运行时,会读到这个回车确认符。
另外,在读取数值型数据或字符串(注意这里没有字符变量)时,scanf会从第一个非空白字符(空白字符指:回车、空格、TAB等)开始读取,自动忽略前面的空白字符,而遇到空白字符结束该类型数据的输入。
因此,对于这个回车确认符(空白字符)的处理,需要看下一个输入的数据类型是什么,如果是字符类,那就需要消除掉这个回车确认符,处理办法有多种方式,下面介绍3种方法:

使用fflush(stdin)命令强制刷新输入缓存,丢弃缓存中的数据,注意此种方法在windows下使用有效,linux无效,因为Linux没有fflush

1
2
3
4
5
int a;
char c;
scanf(“%d”, &a);
fflush(stdin); //clear the ‘enter’ char
scanf(“%c”, &c);

回车符也是字符,可以使用getchar();来吃掉这个回车符号。

1
2
3
4
5
int a;
char c;
scanf(“%d”, &a);
getchar(); //clear the ‘enter’ char
scanf(“%c”, &c);

利用scanf函数的一些机制,如:scanf("%d%*c", &i)%*c表示读一个字符,并不赋值给任何变量。

1
2
3
int a;
char c;
scanf(“%d%*c%c”, &a, &c);

另外,关于scanf还有一个比较有特点的地方,就是无法读入空格字符,所以包含空格字符的字符串就得使用gets或者fgets函数来读入了。还有一个就是&的使用。注意,除了读入字符串外,其他都需要使用&,估计这是不少新手会犯的错误了😀,问题是不会编译不报错也不警告,偶尔不仔细,老手都要找半天...

if

if关键字可不“坑”,“坑”的是使用它的人(又黑自己一把...😂),if一般和elseelse if配合使用(也可以单独使用),一般用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*if-else*/
if(expression){
statement1;
}else{
statement2;
}

/*if-else if-else*/
if(expression){
statement1;
}else if(expression){
statement2;
}else{
statement3;
}

上述内容,读懂很容易,但问题其实就在这个expression上,这个表达的值会影响if这类语句的判断。众所周知,expressiontrue执行statement1,为false则执行statement2else if同理。所以严格上讲,if认为的true非0(其他语句,如while的条件判断机制应该也是这样),啥意思?就是说,不管expression的值是1还是-1if都是认为是true(建议尝试),所以,可别看到表达式的值是负数,就认为条件不成立了。

parameter passing

这里所介绍的参数传递主要是针对C语言内的函数。
众所周知,C的函数采用的是值传递的方式,也即传入到函数内的参数,不管传入的值如何修改,依然不会改变main或其他函数内变量的值,要想改变有多种方式,这里简单介绍三种,具体如下:

使用全局变量

利用函数返回值,形如:a = abs(a);这样的使用方法

使用指针,利用&传入变量地址,修改指针指向的地址保存的值,如:*p = x;的用法


如果只是介绍这些,那太简单了,这里想要说明的是指针在函数参数传递过程中的变化。如果一个函数的参数中存在指针,并且这个函数内会改变传入这个指针的指向(如:链表遍历),针对这种情况,C依然遵循值传递的规则,也就是说,即便指针被传入函数中了,它也不会改变这个指针在main或其他函数中的指向,具体请看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void f(int *a);
int main(int argc, char const * argv[])
{
int *A;
printf("%p\n", A);
f(A);
printf("%p\n", A);
return 0;
}
void f(int *a)
{
a++;
}

上述这段代码的两次输出结果是一致的,也即指针也是遵循值传递的原则的

typedef

关于typedef的用法,这里不做过多介绍,只收集一下平常见到的使用方法。

为基本数据类型定义新的类型名

1
typedef int Data;

上述代码的作用能达到的效果就是intData等价,也即int a;Data a;是两种相同的写法,此种方法在跨平台移植和作有意义的类型名时很方便。

为自定义结构类型定义类型名称

注意说法,为自定义结构类型定义类型名称,注意只是类型名称哦,下面是一般的使用方法。

1
2
3
4
typedef struct point
{
int x, y;
} Point;

如果需要为结构定义指针,需要换一种方法:

1
2
3
4
5
typedef struct LNode *List;
struct LNode{
int Data;
List Next;
};

不过一般而言,标准的写法是下面这种:

1
2
3
4
5
struct LNode{
int Data;
struct LNode *Next;
};
typedef struct LNode *List;

两种写法都是编译通过的(No Warnings),究竟怎么写,就是智者见智的事情了。

为数组定义类型名称

1
2
typedef int int_array[10];
int_array array;

按照上述代码, array就是一个容量为10的整型数组了。

为指针定义名称

1
2
3
4
5
6
/*odinary pointer*/
typedef char* Pchar;

/*function pointer*/
typedef int *(*Pfun)(int, char *);
Pfun a[5]; //Pfun a[5] <==> int *(*a[5])(int, char *);

上述代码中,int *(*a[5])(int, char *)实际上是定义了一个返回值为int*的函数指针数组,函数的有两个,分别是intchar *

two-dimensional array

C语言中实际上并没有严格意义上的二维数组(其他程序设计语言可能也是如此?),因为物理内存的地址是连续且一块一块的,那C语言是如何去保存二维数组和寻址的呢?

按照上述的思路,先来总结一下二维数组的特点。很明显,二维数组包含三个参数:行(Row)、列(Column)和需要存的数据(Data),既然前面已经说过了物理内存是连续分布的,那毫无疑问,二维数组中的 Data 依然是连续的存储在物理内存中的,这个现象是不是在哪见过?没错,就是一维数组;接着,如何去寻址呢?这个问题其实可以用二维数组的来解决,假设总行数为R,总列数为C,找第2行,第3列的元素,按照顺序存储的规则,实际上对应元素的下标(下标从0开始)应该是2 * C + 3,一般化就是i * C + j
举个实例,按照如下的矩阵:
$$\begin{matrix}
1 & 2 & 3\\
4 & 5 & 6\\
7 & 8 & 9\\
10 & 11 & 12\\
\end{matrix}$$
C语言编译器要提取8这个元素,整个矩阵是43的,若行、列下标从0开始,行、列循环变量分别为ij,那么8这个元素对应的下标(i = 2, j = 2)就是i * C + j = 2 * 3 + 2 = 8,实际上也就是从左往右依次按序数下来的结果。

character

二维数组也有字符二维数组和整型二维数组,字符二维数组的使用会简单点,咱先从简单的来。
字符二维数组实际上很容易理解,可以把二维数组想象成一根一根“辣条”(别说你没吃过...囧),这一根根“辣条”,每一根都是一个一维字符数组(其实就是一个字符串啦~)。

1
char str[3][5];

上面这行代码的意思是声明一个字符串数组,这个字符串数组的容量大小是3,每个字符串能存储的最大长度是4(因为\0也要占一位),也可以按照下面的方法初始化:

1
2
3
char str[3][5] = {"str1", 
"str2",
"str3"};

之所以分开三行写,是因为想展示出一条一条的样子(笑ing),这是可以直接输出的哦~

如果你指针学的不错的话,你一定会发现str[0]str[1]str[2]其实就是分别指向这三个字符串的指针

所以,字符串数组还可以这样声明和初始化:

1
2
3
char *str[3] = {"str1", 
"str2",
"str3"};

严格上来讲,char *str[3]其实声明了一个指针数组,这个指针数组有3个字符型指针变量,分别指向三个字符串。其中的每个指针变量,其实可以当作每个字符串的头指针来用。

number

明白了字符二维数组后,数字型的二维数组理解起来就简单了。首先,它不再是“辣条”了,他是单独一个一个的,不过存储方式依然是顺序存储的,二维数组的逻辑结构,其实就是上面提到过的矩阵。不过它的容量计算很简单,也就是行列之积了。

how to use

如何使用这类二维数组,我们考虑三个方面的应用:输入输出传递

input and output

懂了输入,其实也就会输出了,那就只介绍输入了(偷懒😜)。

对于字符串数组(这样叫其实更合适也更易于理解),与普通一维数组一样,若有多个输入,则使用循环,逐个读入即可。

对于数字型二维数组,先输入行,还是先输入列取决于实际应用,由于不仅要输入行,还需要输入列,所以得使用二重循环搞定。

function parameter transfer

二维数组作为函数参数传递的过程就是个有点玄乎的过程了,不过切记一点,数组传递到函数中的都是指针

字符串数组传递时,有两种使用方法,分别如下:

1
void fun(char (*str)[5], int n);
1
void fun(char str[][5], int n);

上述的这两种方法有一个共同点,也即需要给定每个字符串的长度

数字型二维数组的传递与字符串数组的传递类似:

1
void fun(int (*array)[5], int n);
1
void fun(int array[][5], int n);

bool

早期的C标准内其实是没有bool类型的,原先一直存在于C++中,后来在C99标准发布的时候,加入了bool类型。

在使用bool类型时,需要引入头文件stdlib.h,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdbool.h>

bool func(int a) {
if(a >= 0) {
return true;
} else {
return false;
}
}

int main(int argc, char const *argv[]) {
printf("func(-1) = %d, func(1) = %d, func(0) = %d\n", func(-1), func(1), func(0));
return 0;
}

/*
Result:
func(-1) = 0, func(1) = 1, func(0) = 1
*/

明显可以看出此时false = 0, true = 1

若无法引入头文件stdbool.h时,该如何继续优雅的使用bool类型呢?

一般而言,有两种方法:

1
typedef enum{false, true} bool;

利用typedefenum关键字构造枚举。

1
2
3
#define bool int
#define false 0
#define true 1

使用宏定义直接定义(C99就是这样干的,可以看看stdbool.h头文件的内容)。

operator

关于C语言运算符的问题,实际上就是不同运算符之间优先级(precedence)的问题,这部分问题,主要是针对应试吧,生产环境中大概写个测试程序就能得出结论了吧~

优先级 运算符 结合性
1 () 从左到右
2 !、+、-、++、– 从右到左(单目+、-)
3 *、/、% 从左到右
4 +、- 从左到右
5 <、<=、>、>= 从左到右
6 ==、!= 从左到右
7 && 从左到右
8 || 从左到右
9 =、+=、-=、*=、/=、%= 从左到右

注意:上述表格第二行中,“单目+、-”指的即是正负号。


Buy me a coffee ? :)
0%