嗨翻C

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;
}

猜你喜欢

转载自blog.csdn.net/zyx_0604/article/details/76651499
今日推荐