1、main函数
当计算机运行程序时,它需要一些方法来判断程序是否运行成功,计算机正是通过检查main()函数的返回值来做到这一点的。如果让main()函数返回0,就表明从程序运行成功;如果让它返回其他值,就表示程序在运行时出了问题。
如果想检查程序的退出状态,可以在Windows命令提示符中输入:echo %ErrorLevel%或在Linux或Mac中端中输入:echo $? 。
2、编译并运行命令行
3、字符数组与字符串
为什么字符要从0开始编号?为什么不是1?
字符的索引值是一个偏移量:他表示当前要引用的这个字符到数组中第一个字符之间有多少字符。
为什么要这样做?
计算机在存储器中以连续字节的形式保存字符,并利用索引计算出字符在存储器中的位置。如果计算机知道c[0]在存储器1 000 000号单元,那么就可以很快计算出c[98]在1 000 000 + 98号单元。
字符串字面值(双引号里的)和字符数组的区别:字符串字面值是常量,也就是说这些字符一旦创建完毕,就不能再修改它们;如果改了,GCC通常会显示总线错误(C语言采取不同的方式在存储器中保存字符串字面值。总线错误意味着程序无法更新那一块存储空间)
4、布尔运算和位运算
为什么不能只写一个 | 和 & ?
也不是不行。& 和 | 操作符总是计算两个条件,而&&和 || 可以跳过第二个条件。
那要 | 和 & 有什么用?
对逻辑表达式求值只是它们的一个用处,它们还能对数字的某一位进行布尔运算。
5、条件选择
使用switch语句有什么好处?
(1)让代码更清晰,一段代码处理一个变量的结构,结构一目了然,相反,一连串的 if 语句就没那么清晰了;
(2)可以用下落逻辑在不同的分支之间复用代码。
二、字符数组与字符串
6、const到底是什么意思?它能让字符串变成只读么?
加不加const,字符串字面值都是只读的,const修饰符表示,一旦你试图用const修饰过的变量去修饰数组,编译器就会报错。
7、为什么数组变量不保存在存储器中?既然它存在,就应该在某个地方,不是么?
程序在编译期间,会把所有对数组变量的引用替换成数组的地址。也就是说 ,在最后的可执行文件中,数组变量并不存在。既然数组变量从来不需要指向其他地方,有和没有其实都一样。
8、为了避免对只读存储区的字符串进行修改而产生段错误,可以不再将char指针设置为字符串字面值,像这样:char *s = “Some string”;
但是把指针设为字符串字面值又没错,问题出在你试图修改字符串字面值。如果你想把指针设为字符串字面值,必须确保使用了const关键字:
const char *s = “Some string”;
这样一来,如果编译器发现有代码试图修改字符串,就会提示编译错误:
s[0] = ‘S’;
monte.c:7: error: assignment of read-only location
三、小工具解决大问题
1、例题
42.362400, -71.098465, Speed = 21
42.363327, -71.097588, Speed = 23
42.363255, -71.096710, Speed = 17
转换格式为:
data = {
{latitude: 42.363400, longitude: -71.098465, info: ‘Speed = 21’},
{latitude: 42.363327, longitude: -71.097588, info: ‘Speed = 23’},
{latitude: 42.363255,longitude: -71.096710, info: ‘Speed = 17’},
}
#include <stdio.h>
int main()
{
float latitude;
float longitude;
char info[80];
int started = 0;
puts("data = {");
while (scanf("%f, %f, %79[^\n]", &latitude, &longitude, info) == 3) //[^\n]:相当于在说:把这一行余下来的字符都给我
{
if (started)
{
printf(",\n");
}else
{
started = 1;
}
printf("{latittude: %f, longitude: %f, info: '%s'}", latitude, longitude, info);
}
puts("\n]");
return 0;
}
2、有没有什么办法不用改代码,甚至不用重新编译,就能让程序使用文件?
有一种小工具叫过滤器(filter),它逐行读取数据,对数据进行处理,再把数据写到某个地方。如果你的计算机是Unix,或者你在Windows上安装了Cygwin就已经拥有很多过滤器工具了。
head:显示文件前几行的内容。
tail:显示文件最后几行的内容。
sed:流编辑器,从来搜索和替换文本。
3、重定向数据
在用scanf()从键盘读取数据、printf()向显示器写数据时,这两个函数其实并没有直接使用键盘、显示器,而是用了标准输入和标准输出。
程序运行时,操作系统会创建标准输入和标准输出。
操作系统控制数据如何进出标准输入、标准输出。如果在命令提示符或终端运行程序,操作系统会把所有键盘输入都发送到标准输入;默认情况下,如果操作系统从标准输出读到数据,就发送到显示器。
scanf()和printf()函数并不知道数据从哪里来,也不知道数据要到哪里去。它们不关心这一点,只管从标准输入读数据,向标准输出写数据。
**因此,就可以重定向标准输入、标准输出,让程序从键盘意外的的地方读数据、往键盘意外的地方写数据,例如:文件。**
**用 < 重定向标准输入**
**用 > 重定向标准输出**
< 操作符告诉操作系统,程序的标准输入跟 “ < (文件名) ”中的文件名相连,而不是键盘。
lh@ubuntu:~/Desktop/HaiFanC$ ./zhuanhua_JS <../ditu/input >../ditu/output
4、默认情况下,标准错误会发送到显示器
当操作系统创建了一个新的进程,它会把标准输入指向键盘,而将标准输出指向屏幕。同时操作系统还会创建标准错误,和标准输出一样,标准错误会默认发送到显示器。
5、fprintf() 打印到数据流
fprintf() 会把数据发送到数据流。
当调用printf()函数时,printf()其实调用了fprintf()。
例如:printf(“hello world”); 等价于:
fprintf(stdout, “hello world”);
fprintf()可以让你决定把文本发送到哪里,既可以是stdout,也可以是stderr,还可以是其他。
但是不能是stdin。
同样的,fscanf(stdin, …) 和 scanf() 是一样的。
当然也可以重定向标准错误:
2>重定向标准错误
例程:
#include <stdio.h>
int main()
{
int i = 0;
chat input[10] = {0};
while (scanf("%9s", input) == 1)
{
i += 1;
if (i % 2)
{
fprintf(stdout, "%s\n", input);
}else
{
fprintf(stderr, "%s\n", input);
}
}
return 0;
}
lh@ubuntu:~/Desktop/HaiFanC$ ./jimi <../jimi/input >../jimi/msg 2>../jimi/errmsg
6、用管道连接输入与输出
符号 | 表示管道,它能连接一个进程的标准输出和另一个进程的标准输入。
例程:
/*
beimuda.c 只发送落在百慕大三角内的数据,beimuda工具输出、输入数据的格式相同。
*/
#include <stdio.h>
int mian()
{
float latitude;
float longitude;
char info[80];
while (scanf("%f, %f, %79[^\n]", &latitude, &longitude, info) == 3)
{
if ((latitude > 26) && (latitude < 34))
{
if ((longitude > -76) && (longitude < -64))
{
printf("%f, %f, %s\n", latitude, longitude, info);
}
}
}
return 0;
}
编译:
(./beimuda | ./geo2json) < input.csy >output.json
7、输出多个文件—-创建自己的数据流
程序运行时,操作系统会为它创建三条数据流:标准输入、标准输出和标准错误。但有时你需要从创建自己的数据流。
每条数据流用一个指向文件的指针来表示,可以用fopen()函数创建新数据流。
FILE *in_file = fopen("input.txt", "r");
FILE *out_file = fopen("output.txt", "w");
fopen() 函数接收两个参数:文件名和模式。
三种模式:w(写文件)、r(读文件)、a(在文件末尾追加数据)
fprintf(out_file, "hello %s", zhengyuan);
fscanf(in_file, "%79[^\n]", sentence);
最后,用完数据流,别忘了关闭它。
虽然所有的数据流在程序结束后会自动关闭,但你仍应该自己关闭它们:
fclose(in_file);
fclose(out_file);
注:当在程序中打开文件准备读写时,最好检查一下有没有错误发生。如果数据流打开失败会返回1.
FILE *in = fopen("bucunzai.txt", "r");
改成:
FILE *in;
if (!(in = fopen("bucunzai.txt", "r")))
{
fprintf(stderr, "dabukai\n");
return 1;
}
8、最多能有几条数据流?
这取决于操作系统。通常情况下,一个进程最多可以有256条数据流。但请记住,数据流的数量是有限的用完后应该关闭它们。
9、命令行参数
int main(int argc, char *argv[])
{
if (argc != 6)
{
fprintf(stderr, "You need to give 5 arguments\n");
return 1;
}
}
argc:记录数组中元素的个数
10、由库代劳
unistd.h头文件不属于C标准库,而是POSIX库中的一员。POSIX的目标是创建一套能够在所有主流操作系统上使用到的函数。
命令行选项
getopt(),每一次调用都会返回命令行中下一个参数。
rock_to -e 4 -a Brasilia Tokyo London
两个选项:一个选项接收值,-e代表“引擎”;另一个选项代表了开或关,-a代表“无敌模式”。可以循环调用getopt()来处理这两个选项。
#include <unistd.h>
while ((ch = getopt(argc, argv, "ae:")) != EOF)
{
switch(ch)
{
...
case 'e':
engine_count = optarg;
...
}
argc -= optind;
argv += optind;
}
字符串ae:告诉getopt()函数“a和e是 有效选项”,e后面的冒号表示“-e后面需要在跟一个参数”, getopt()会用optarg变量指向这个参数。
循环结束后,为了让程序读取命令行参数,需要调整一下argc和argv变量,跳过所有选项,最后argv数组将变成:
Brasilia Tokyo London
argv[0] 会指向选项后的第一个参数。
例程:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *delivery = "";
int thick = 0;
int count = 0;
char ch;
while ((ch = getopt(argc, argv, "d:t")) != EOF)
{
switch(ch)
{
case 'd':
delivery = optarg;
break;
case 'e':
thick = 1;
break;
default:
fprintf(stderr. "Unknown option: '%s'\n", optarg);
return 1;
}
argc -= optink;
argv += optink;
if(thick)
puts("Thick crust");
if (delicery[0])
printf("To be delivered %s\n", delivery);
}
puts("Ingredients:");
for (count = 0; count < argc; count++)
puts(argv[count]);
return 0;
}
11、使用类型转换把float值存进整形
例程:
int x = 7;
int y = 2;
float z = x / y;
printf("z = %f\n", z); //z = 3.0000
因为x 和 y 都是整型,而两个整数相除,结果是一个舍入的整数。
int x = 7;
int y = 2;
float z = (float)x / y;
//如果编译器发现有整数在加、减、乘、除浮点数,会自动替你完成转换,因此可以减代码中显示类型转换的次数。
printf("z = %f\n", z); //z = 3.5000
12、不同平台上数据类型的大小不同,怎么知道int或double栈了多少字节,最大最小值时多少呢?
#include <stdio.h>
#include <limits.h> //含有表示整型大小的值
#include <float.h> //含有表示float和double类型大小的值
int main()
{
printf("The value of INT_MAX is %i\n", INT_MAX);
printf("The value of INT_MIN is %i\n", INT_MIN);
printf("An int takes %z bytes\n", sizeof(int));
printf("The value of FLT_MAX is %i\n", FLT_MAX");
printf("The value of FLT_MIN is %i\n", FLT_MIN);
printf("An float takes %z bytes\n", sizeof(float));
return 0;
}
13、代码的复用、函数复用
13.1、异或加密
不安全但易于操作,加解密使用同一段代码
void encrypt(char *message)
{
char c;
while (*message)
{
*message = *message ^ 31;
message++;
}
}
将这段共享代码放入一个单独的.c文件中,只要编译器在编译程序是包含共享代码,就可以在多个程序中使用相同的代码了。
共享代码需要自己的头文件。
gcc message_hider.c encrypt.c -o message_hider
13.2、多文件的程序,就算是一个很小的改动,也可能话很长时间编译才能看到结果,怎么样才能提高重新编译程序的速度?
不要重新编译所有文件:
保存目标代码的副本,将生成的目标代码保存在文件中,就不需要重新生成它了。加入修改了某个源文件,可以重新创建一个文件的目标代码,然后把所有的目标文件传给编译器,让编译器把它们连接起来。
gcc -c *.c
gcc *.o -o launch
如果修改了一个源文件:
gcc -c thruster.c
gcc *.o -o launch
这样可以节省很多的时间
14、make工具
要是有工具能自动重新编译那些修改过的源文件就好了。
make是一个可以替你运行编译命令的工具。make会检查源文件和目标文件的时间戳,如果目标文件过期,make就会重新编译。
对于每个目标,make需要知道两件事:
(1)依赖项:生成目标需要哪些文件
(2)生成方法:生成该文件要用哪些指令。
launch.o: launch.c launch.h thruster.h
gcc -c launch.c
thruster.o: thruter.h thruster.c
gcc -c thruster.c
launch: launch.o thruster.o
gcc launch.o thruster.o -o launch
注:生成方法必须以tab开头,如果用空格缩进,就无法生成程序。
四、结构、联合、位字段
1、结构体空洞
结构字段在存储器中并不一定是挨着摆放的,有时两个字段之间会有小的空隙。
因为计算机总希望数据能对齐字边界,如果计算机的字长是32位,就不希望某个变量跨越32位的边界保存。
因为计算机按字从存储器中读取数据,如果某个字段跨越了多个字,CPU就必须读取多个存储单元,并以某种方式把督导的值合并起来。会很慢。
2、匿名结构
匿名结构就是没有名字的结构,typedef struct { … } spider_man; 有一个叫spider_man的别名,但没有结构名,很多时候,如果创建了别名,也就不需要结构名了。
3、指定初始化器
可以用“指定初始化器”按名设置结构和联合字段,属于c99标准。绝大多数现代编译器都支持“指定初始化器”,但如果c语言的变种,可能不支持(比如:c++不支持)
typedef truct
{
const char *color;
int gears;
int height;
}bike;
bike b = {.height = 17, .gears = 23};
4、
margarita m = {2.0, 1.0, {0.5}}; //成功编译
margaruta m;
m = {2.0, 1.0, {0.5}}; //不能编译,因为只有把{2.0, 1.0, {0.5}}和结构声明写在一行里,编译器才知道它代表结构,否则,编译器会认为是数组。
5、枚举记录联合中保存了什么值
编译器不会记录你在联合中设置或读取过哪些字段。我们完全可以设置一个字段,读取另一个字段,但有时这会造成很严重的后果。
可以通过创建枚举,记录我们在联合中保存了什么值。
#include <stdio.h>
typedef enum
{
COUNT,
POUNDS;
PINTS
}unit_of_measure;
typedef union
{
short count;
float weight;
float volume;
}quantity;
typedef struct
{
const char *name;
const char *country;
quantity amount;
unit_of_measure units;
}fruit_order;
void display(fruit_order order)
{
printf("This order contains");
if (order.units == PINTS)
{
printf("%2.2f pints of %s\n, order.amount.volume, order.name");
}else if (order.units == POUNDS)
{
printf("%2.2f lbs of %s\n", order.amount.weight, order.name);
}else
{
printf("%i %s", order.amount.count, order.name);
}
}
int main()
{
fruit_order apples = {"apples", "England", .amount.count = 144, COUNT};
fruit_order strawberries = {"starawberries", "Spain", .amount.weight = 17.6, POUNDS};
fruit_order oj = {"orange juice", "U.S.A", .amount.volume = 10.5, PINTS};
display(apples);
display(strawberries);
display(oj);
return 0;
}
6、位字段(bitfield)
如果编译器发现结构中只有一个位字段,还是会把它填充成一个字,多以位字段总是结合在一起。
位字段不仅仅是为了节省空间,如果要读取底层的二进制信息,位字段会非常有用。比如读写某类自定义二进制文件。
位字段应该声明成unsigned int。
五、动态存储
1、存储器泄漏
一旦申请了堆上的空间,这块空间再也不能分配出去,知道告诉C标准库你已经用完了。对存储器的空间有限,如果再代码中不断地申请堆空间,很快就会发生存储器泄漏。
每次在代码中用malloc()函数请求对存储,就应该有相应的代码用free()函数归还存储空间。虽然程序结束以后,所有堆空间会自动释放,但是用free()显式释放你创建的所有动态存储器是一种好的做法。
2、strdup()函数可以吧字符串复制到堆上
strdup()函数计算字符串的长度,然后调用malloc()函数在堆上分配相应的空间,
然后srdup()函数把所有字符复制到堆上的新空间。
strdup()把新的字符串放在堆上,所以千万记得要用free()函数释放空间。
3、二叉树,嫌疑人识别从程序
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 typedef struct node
6 {
7 char *question;
8 struct node *no;
9 struct node *yes;
10 }node;
11
12 int yes_no(char *question)
13 {
14 char answer[3];
15 printf("%s? (y/n): ", question);
16 fgets(answer, 3, stdin);
17 return answer[0] == 'y';
18 }
19
20 node *create(char *question)
21 {
22 node *n = malloc(sizeof(node));
23
24 n->question = strdup(question);
25 n->no = NULL;
26 n->yes = NULL;
27
28 return n;
29 }
30
31 void release(node *n)
32 {
33 if (n)
34 {
35 if (n->no)
36 {
37 release(n->no);
38 }
39 if (n->yes)
40 {
41 release(n->yes);
42 }
43 if (n->question)
44 {
45 free(n->question);
46 }
47 free(n);
48 }
49 }
50
51 int main()
52 {
53 char question[80];
54 char suspect[20];
55 node *start_node = create("Does suspect have a mustache");
56 start_node->no = create("Loretta Barnsworth");
57 start_node->yes = create("Vinny the Spoon");
58
59 node *current;
60 do
61 {
62 current = start_node;
63 while (1)
64 {
65 if (yes_no(current->question))
66 {
67 if (current->yes)
68 {
69 current = current->yes;
70 }else
71 {
72 printf("SUSPECT IDENTIFIED\n");
73 break;
74 }
75 }else if (current->no)
76 {
77 current = current->no;
78 }else
79 {
80 printf("Who's the suspect? ");
81 fgets(suspect, 20, stdin);
82 node *yes_node = create(suspect);
83 current->yes = yes_node;
84
85 node *no_node = create(current->question);
86 current->no = no_node;
87
88 printf("Give me a question that is TRUE for %s but not for %s ", suspect, current->question);
89 fgets(question, 80, stdin);
90 current->question = strdup(question);
91
92 break;
93 }
94 }
95 }while (yes_no("Run again"));
96
97 release(start_node);
98
99 return 0;
100 }
4、软件取证:使用valgrind
valgrind通过伪造malloc()可以监控分配在堆上的数据。当程序向分配堆存储器时,valgrind将会拦截你对malloc()和free()的调用,然后运行自己的malloc() 和 free() 。valgrind的malloc() 会记录调用它的是哪段代码和分配了哪段存储器。程序结束时,valgrind会回报堆上有哪些数据,并告诉你这些数据由哪段代码创建。
使用前编译时
gcc -g spies.c -o spies
// -g 告诉编译器要记录要编译代码的行号
六、函数指针(高级函数)
1、如何创建函数指针?
int (*werp_fn) (int);
warp_fn = go_to_wrap_apeed;
wrap_fn(4);
char** (*names_fn) (char*, int);
names_fn = album_names;
char** results = names_fn("Sacha Distel", 1972);
例程:
int sports_no_bieber(char *s)
{
return strstr(s, "sports") && !strstr(s, "bieber");
}
void find(int (*match)(char*))
{
int i;
puts("Search results:");
for (i = 0; i < NUM_ADS; i++)
{
if (match(ADS[i]))
{
printf("%s\n", ADS[i]);
}
}
}
int main()
{
find(sports_no_bieber);
return 0;
}
2、排序函数如何才能对任何类型的数据排序?
c标准库的排序函数会接收一个比较器函数指针,用来判断两个数据是大于、小于还是等于。
qsort(void *array,
size_t length,
size_t item_size,
//升序排列整型得分
int compare_sores(const void *score_a, const void *score_b)
{
int a = *(int *)socre_a;
int b = *(int *)score_b;
return a - b;
}
//降序排列整型得分
int compare_score_desc(const void *score_a, const void *score_b)
{
int a = *(int *)score_a;
int b = *(int *)score_b;
return b - a;
}
//比较面积
typedef struct
{
int width;
int height;
}rectangle;
int compare_areas(const void *a, const void *b)
{
rectangle *ra = (rectangle*)a;
rectangle *rb = (rectangle*)b;
int area_a = (ra->width * ra->height);
int area_b = (rb->width * rb->height);
return area_a - area_b;
}
//按字母序排列名字,区分大小写
int compare_names(const void *a, const void *b)
{
char** sa = (char**)a;
char** sb = (char**)b;
return strcmp(*sa, *sb);
}
//面积从大到小排列
int compare_areas_desc(const void* a, const void* b)
{
return compare_ares(b, a);
}
//逆字母序排列名字
int compare_name_desc(const void* a, const void* b)
{
return compare_names(b, a);
}
int main()
{
int scores[] = {543, 323, 32, 554, 11, 3, 112};
int i;
qsort(scores, 7, sizeof(int), compare_scores_des);
puts("order: ");
for (i = 0; i < 7; i++)
{
printf("Score = %i\n", scores[i]);
}
return 0;
}
注意:用来给字符串数组排序的比较器函数使用了char**, 它是什么意思?
字符串数组中的每一项都是字符指针(char*),当qsort()调用比较器函数时,会发送两个指向数组元素的指针,也就是说比较器函数接收到的是指向字符指针的指针,也就是 char**。
3、如何创建函数指针数组
void (*replies[])(response) = {dump, second_chance, marriage};
改成函数指针数组:
enum response_type {DUMP, SECOND_CHANCE, MARRIAGE};
void (*replies[])(response) = {dump, second_chance, marriage}; //注意函数名的顺序与枚举类型的顺序要相同。
int main()
{
repoonse r[] = {{"MIKE", DUMP}, {"LUIS", SECOND_CHANCE}, {"MATT", SECOND_CHANCE}, {"William", MARRIAGE}};
int i;
for (i = 0; i < 4; i++)
{
(replies[r[i].type])(r[i]);
}
return 0;
}