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 | int a; |
回车符也是字符,可以使用getchar();
来吃掉这个回车符号。
1 | int a; |
利用scanf
函数的一些机制,如:scanf("%d%*c", &i)
,%*c
表示读一个字符,并不赋值给任何变量。
1 | int a; |
另外,关于scanf
还有一个比较有特点的地方,就是无法读入空格字符,所以包含空格字符的字符串就得使用gets
或者fgets
函数来读入了。还有一个就是&
的使用。注意,除了读入字符串外,其他都需要使用&
,估计这是不少新手会犯的错误了😀,问题是不会编译不报错也不警告,偶尔不仔细,老手都要找半天...
if
if
关键字可不“坑”,“坑”的是使用它的人(又黑自己一把...😂),if
一般和else
及else 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
这类语句的判断。众所周知,expression
为true
执行statement1
,为false
则执行statement2
,else if
同理。所以严格上讲,if
认为的true
是非0(其他语句,如while
的条件判断机制应该也是这样),啥意思?就是说,不管expression
的值是1
还是-1
,if
都是认为是true
(建议尝试),所以,可别看到表达式的值是负数,就认为条件不成立了。
parameter passing
这里所介绍的参数传递主要是针对C
语言内的函数。
众所周知,C
的函数采用的是值传递的方式,也即传入到函数内的参数,不管传入的值如何修改,依然不会改变main
或其他函数内变量的值,要想改变有多种方式,这里简单介绍三种,具体如下:
使用全局变量
利用函数返回值,形如:a = abs(a);
这样的使用方法
使用指针,利用&
传入变量地址,修改指针指向的地址保存的值,如:*p = x;
的用法
如果只是介绍这些,那太简单了,这里想要说明的是指针在函数参数传递过程中的变化。如果一个函数的参数中存在指针,并且这个函数内会改变传入这个指针的指向(如:链表遍历),针对这种情况,
C
依然遵循值传递的规则,也就是说,即便指针被传入函数中了,它也不会改变这个指针在main
或其他函数中的指向,具体请看下面的代码:1 |
|
上述这段代码的两次输出结果是一致的,也即指针也是遵循值传递的原则的。
typedef
关于typedef
的用法,这里不做过多介绍,只收集一下平常见到的使用方法。
为基本数据类型定义新的类型名
1 | typedef int Data; |
上述代码的作用能达到的效果就是int
和Data
等价,也即int a;
和Data a;
是两种相同的写法,此种方法在跨平台移植和作有意义的类型名时很方便。
为自定义结构类型定义类型名称
注意说法,为自定义结构类型定义类型名称,注意只是类型名称哦,下面是一般的使用方法。1
2
3
4typedef struct point
{
int x, y;
} Point;
如果需要为结构定义指针,需要换一种方法:1
2
3
4
5typedef struct LNode *List;
struct LNode{
int Data;
List Next;
};
不过一般而言,标准的写法是下面这种:1
2
3
4
5struct LNode{
int Data;
struct LNode *Next;
};
typedef struct LNode *List;
两种写法都是编译通过的(No Warnings),究竟怎么写,就是智者见智的事情了。
为数组定义类型名称
1 | typedef int int_array[10]; |
按照上述代码, array
就是一个容量为10
的整型数组了。
为指针定义名称
1 | /*odinary pointer*/ |
上述代码中,int *(*a[5])(int, char *)
实际上是定义了一个返回值为int*
的函数指针数组,函数的有两个,分别是int
和char *
。
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
这个元素,整个矩阵是4
行3
的,若行、列下标从0
开始,行、列循环变量分别为i
、j
,那么8
这个元素对应的下标(i = 2, j = 2
)就是i * C + j = 2 * 3 + 2 = 8
,实际上也就是从左往右依次按序数下来的结果。
character
二维数组也有字符二维数组和整型二维数组,字符二维数组的使用会简单点,咱先从简单的来。
字符二维数组实际上很容易理解,可以把二维数组想象成一根一根“辣条”(别说你没吃过...囧),这一根根“辣条”,每一根都是一个一维字符数组(其实就是一个字符串啦~)。1
char str[3][5];
上面这行代码的意思是声明一个字符串数组,这个字符串数组的容量大小是3,每个字符串能存储的最大长度是4(因为\0
也要占一位),也可以按照下面的方法初始化:1
2
3char str[3][5] = {"str1",
"str2",
"str3"};
之所以分开三行写,是因为想展示出一条一条的样子(笑ing),这是可以直接输出的哦~
如果你指针学的不错的话,你一定会发现str[0]
、str[1]
和str[2]
其实就是分别指向这三个字符串的指针。
所以,字符串数组还可以这样声明和初始化:1
2
3char *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
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;
利用typedef
和enum
关键字构造枚举。
1
2
3
使用宏定义直接定义(C99
就是这样干的,可以看看stdbool.h
头文件的内容)。
operator
关于C
语言运算符的问题,实际上就是不同运算符之间优先级(precedence)的问题,这部分问题,主要是针对应试吧,生产环境中大概写个测试程序就能得出结论了吧~
优先级 | 运算符 | 结合性 |
---|---|---|
1 | () | 从左到右 |
2 | !、+、-、++、– | 从右到左(单目+、-) |
3 | *、/、% | 从左到右 |
4 | +、- | 从左到右 |
5 | <、<=、>、>= | 从左到右 |
6 | ==、!= | 从左到右 |
7 | && | 从左到右 |
8 | || | 从左到右 |
9 | =、+=、-=、*=、/=、%= | 从左到右 |
注意:上述表格第二行中,“单目+、-”指的即是正负号。