最近在读《C Primer Plus》和《Expert C Programming》,发现用了很久的C有很多从来都不知道的特性(面壁)。。。

于是在这里整理一下,就当是学习笔记。以下代码亲测通过。
编译器为gcc4.7.2

###关于#include

#include是C预处理器指令,并不是C语言的语句。通常,C编译器在编译前要对源代码做一些准备工作,称为预处理。#符号表明这一行是在编译器接手之前由C预处理器处理的语句。

###关于sizeof与strlen
sizeof把用来标志字符串结束的不可见的空字符也计算在内,它计算存储所需的空间大小。

strlen则计算字符串的长度。

无论如何,像\b \a之类的不可见字符都会在计算大小时,被算在其中。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "stdio.h"
#include "string.h"
#define AAA "A B C\b\a"
int main(){
char buffer[20];
printf("What's your name?\n");
scanf("%s", buffer);
printf("sizeof%s: %d\n", buffer, sizeof(buffer));
printf("strlen: %d\n", strlen(buffer));
printf("strlen of another%s: %d\n", AAA, strlen(AAA));
printf("sizeof of another: %d\n", sizeof(AAA));
return 0;
}

这段代码的显示结果是

What's your name?
Carol Z
sizeofCarol: 20
strlen: 5
strlen of anotherA B : 7
sizeof of another: 8

可以看到sizeof包括了结尾的’\0’,但strlen没有,但是对于不可见的字符,它们是平等的。请注意这里奇葩的退格符\b。

由于scanf的特性,它接收到空白符就停止了,所以真正存在buffer里的字符串并不包括“ Z”,这就是为什么做网络实验那个聊天程序的时候,我们不得不用gets(尤其是对于每个单词之间都有空白符的国家的人,聊天只能一个单词一个单词发也太悲吹了)。我们会在后文继续介绍scanf,它在接收特定形式的输入的时候是十分有效的。

顺便说一句,之前一直以为是一个函数的sizeof在C语言中并不是一个函数而是一个操作符,我们来看看MSDN上关于sizeof的解释:

The sizeof keyword gives the amount of storage, in bytes,associated with a variable or a type  
 (including aggregate types).  
This keyword returns a value of type size_t.  

这样就说清楚了。

另外,当sizeof的操作数是个变量的时候,可以不需要加括号,如果是个类型名就要加上括号了(所以会让人以为是个函数啊:))。

###关于用户交互
printf与scanf乃一对好机油。
printf()函数的作用是打印到缓冲区。

当满足以下任意一种情况时,才打印到屏幕。
1 缓冲区满 2 遇到换行符 3 需要输入

%d显示float并不会把float转换成int显示,而是显示dirty number。同样,我们也不能期待%f把int转换成float。

%表示字符显示的位置,后面的修饰符表示显示的格式。

当被用作printf()的参数时,float被转换成double,所以用%f打印double也完全大丈夫,人家本来就是用来打印double的,看下面这段代码:

1
2
3
4
5
float n1 = 3.0;
double n2 = 3.0;
long n3 = 2000000000;
long n4 = 1234567890;
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);

由于n1从float被转换成了double,所以第一个%ld只能读到n1的前四个字节,第二个%ld读到n1的后四个字节,然后n2本来就是double,所以同理。n1和n2分完了所有的%ld,n3和n4就悲剧了。

scanf如果期待读到%d,那么输入为1a的话,是不会把那个a读入的。

与printf和scanf一样,putchar和getchar也是一对好机油。看名字应该就知道putchar用来打印一个自符,而getchar用来获取一个字符。所以我们尝试:

1
putchar(getchar());

很有意思,这个语句获取一个输入的字符,然后立即回显。

###关于_Bool
之前一直抱怨C没有bool类型,实在是太冤枉了。C99引入了_Bool表示bool类型。
看下面这段代码

1
2
3
4
5
6
#include "stdio.h"
int main(){
_Bool a = 1;
printf("sizeof: %d\n", sizeof(a));
return 0;
}

打印如下

sizeof: 1

所以bool类型占用1byte, 取值为0或者1,0表示false,1表示true。
就算在上面写的是 _Bool a = 2, 然后打印a结果也是1。本来担心是不是2在二进制下是10,会不会打印的时候打出来的是高位的1,然后换了4尝试,结果打印出来还是1。虽然不知道原理是什么(难道是每次只取第1位?还是强制类型转换成了bool?),但是非0的数打印出来都是1,包括负整数。不要挣扎了少年!

在”循环”那章证实了我之前的想法:所有非零值都被认为是真。

另外,C99中提供了一个头文件stdbool.h,用bool代替 _Bool,应用true和false代替1和0,当然这些事情我们也可以自己做。

顺便表达下for(;;)跟while(1)是一样的。

###关于ANSI C
经常在书里看到ANSI C怎样怎样,按照C99标准怎样怎样。它们之间是什么关系呢?

原来,ANSI C是一个叫美国国家标准协会的组织,它制定了C89/C90/C99/C11等一系列标准。

###关于四舍五入
从小学数学老师就教会我们四舍五入啦。不过计算机比较呆萌,你告诉它怎么做它才会怎么做。所以对于浮点数强制变成整数的情况,C99采用了“趋零截尾”的做法。记住名字应该就记住了这个意思:就是向零靠去掉小数部分嘛。比如1.6变成1,-2.9变成-2。所以请务必记住这个名字。来,再念一遍:趋零截尾。

###关于取模运算符%
不要对浮点数进行取模运算。

取模运算的符号与第一个操作数的符号相同。如果你硬是像我一样尝试了-0%3与0%4的话,你会看到两个结果都是0。

###关于类型转换
表达式中的有符号和无符号的char和short都被自动转换成int。

在包含两种数据类型的任何运算里,两个值都被转换成两种类型里的较高级别。

类型级别从高到低排序:long double > double > float > unsigned long long > long long > unsigned long > long > unsigned int > int 当long和int具有相同大小时,unsigned int > long。

在赋值语句里,计算的最后结果被转换成将要被赋值的那个变量的类型。

当作为函数的参数传递时,char和short会被转换成int,float会被转换成double。这里留下一个问题吧,据说可以通过函数原型来阻止自动提升的发生,第9章会讲到。

###关于结束
在stdio.h中EOF被定义为-1, 你可以printf(“%d\n”, EOF);打印出来确实是-1。那为什么文件中间有一个-1的时候并不代表文件结束呢?这里carol猜一下应该是因为文件是按照字符流存储的,所谓的文件里的-1代表的应该是’-‘与’1’两个字符才对的吧~

###关于#pragma
《Expert C Programming》上提到了pragma指示符,说是在gcc 1.34的时候是不支持这货的。据说是因为自由软件基金会(FSF)的一个gcc编译器设计者积极抵制这货。然后就拒绝编译程序,使用一段游戏来代替整个程序。虽然比较有意思,但是我的编译器已经看不到这个彩蛋了(那是多久以前的事情啊。。。),十分可惜啊。

哦,对了,关于#pragma的一些说明可以在这里找到,说得还是比较清楚啦。

###关于switch
在switch语句中,语句是从case语句开始执行的。也就是说,从switch到第一个case之间的赋值全部都无效。让我们来看一件有意思的事情:

1
2
3
4
5
6
7
8
9
10
11
12
#include "stdio.h"
int main(){
int a = 1;
switch(a){
int test = 1;
case 1: printf("test%d\n", test);
break;
default:
printf("default\n");
}
return 0;
}

执行的结果是

test-1216802816

因为有了之前那段解释,所以这里就比较好说了,因为那句int test = 1根本就没有执行到!不过这里有一个问题,既然没有执行到,那么为什么test不被认为是没有定义的?
顺便表示一下case后面跟的只能是常量,就算是const int也不可以,某些方面反应出了const并不真的表示常量。

###关于malloc
malloc用于在堆中申请一块空间,用完之后一定要记得用free释放掉,否则会造成内存的泄漏。嗯~这个大家都知道就不再多说。哦~顺便提一下内存的使用情况可以通过free命令查看。

其实我想表达的重点呢,是有一个叫calloc的函数。这个函数在返回指针之前会先把分配好的内存的内容都清空为0。多好的函(hai)数(zi)啊。

尤记得我们在写网络实验的时候,经常出现的代码片段是这样的:

1
2
char* buffer = malloc(1024);
memcpy(buffer, 0, 1024);

其实完全可以改写成:
1
char* buffer = calloc(1024, sizeof(char));

函数原型:void *calloc(unsigned n, unsigned size);
功能: 在内存的动态存储区中分配n个长度为size的连续空间,自动初始化该内存空间为零,函数返回一个指向分配起始地址的指针;如果分配不成功,返回NULL。