2017年7月

Head First C 笔记 - 高级函数

高级函数

函数指针

继续看例子,这个案例用于给不同的人发送不同的消息。

#include <stdio.h>

enum response_type {
    DUMP, SECOND_CHANGE, MARRAGE
};

typedef struct {
    char *name;
    enum response_type type;
} response;

void dump(response r)
{
    printf("Dear: %s ", r.name);
    puts("dump");
}

void second_change(response r)
{
    printf("Dear: %s ", r.name);
    puts("second_change");
}

void marrage(response r)
{
    printf("Dear: %s ", r.name);
    puts("marrage");
}

void (*replies[])(response) = {dump, second_change, marrage};

int main()
{
    response res[] = {
        {"Mike", DUMP}, {"Tom", SECOND_CHANGE}, {"Jim", MARRAGE}, {"KangKang", SECOND_CHANGE}
    };

    int i;
    int len;

    len = sizeof(res) / sizeof(response); // 计算数组长度
    for (i = 0; i < len; i++) {
        (replies[res[i].type])(res[i]);
        // switch(res[i].type) {
        // case DUMP:
        // dump(res[i]);
        // break;
        // case SECOND_CHANGE:
        // second_change(res[i]);
        // break;
        // default:
        // marrage(res[i]);
        // break;
        // }
    }
    return 0;
}

函数名就是指向函数的指针,但跟指针又不完全相同,在底层,函数名是L-value,指针是R-value。比如:int类型的指针可以表示成int *,但函数指针不能写成function *,因为在C语言中没有函数类型,函数的类型是由参数,返回值这些东西组合定义的。所以函数指针的定义更复杂,如下:

返回类型(*指针变量)(参数类型)

上面案例中最核心的是这样代码:void (*replies[])(response) = {dump, second_change, marrage};,它创建了一个函数指针的数组,优化了后面的switch调用。

排序

#include <stdio.h>
#include <string.h>
#include <stdio.h>

int compare_scores(const void* score_a, const void* score_b)
{
    int a = *(int*)score_a;
    int b = *(int*)score_b;
    return a - b;
}

int compare_names(const void* a, const void* b)
{
    char** sa = (char**)a;
    char** sb = (char**)b;
    return strcmp(*sa, *sb);
}

int main(int argc, char const *argv[])
{
    int scores[] = {2,4,1,9,8};
    int i;
    // 升序
    qsort(scores, 5, sizeof(int), compare_scores);
    for (i = 0; i < 5; i++) {
        printf("%i ", scores[i]);
    }

    // 字典序
    char *names[] = {"Karen", "Mark", "Ada", "Brett"};
    qsort(names, 4, sizeof(char*), compare_names);
    for (i = 0; i < 4; i++) {
        printf("%s ", names[i]);
    }

    return 0;
}

有了函数指针,我们就可以将函数当成参数传递了,qsort()函数用于排序,它会接收一个比较器函数指针,用于判断两个数据的大小。它的定义如下:

qsort(void *array, size_t length, size_t item_size, int (*compare)(const void *, const void *))

最后一个参数是比较器函数指针,注意其中的参数void *可以保存任意类型的指针。

由于比较器的参数类型是void *,所以需要做下类型转换。上面案例中的int a = *(int*)score_a;char** sa = (char**)a;,就是将void指针转换为相应类型的变量,然后再做比较。

可变参数

可变参数函数使用到了标准库中的(预处理会被替换成其他代码),位于stdarg.h中。

#include <stdio.h>
#include <stdarg.h>

void print_ints(int args, ...)
{
    va_list ap;
    va_start(ap, args);
    int i;
    for (int i = 0; i < args; ++i) {
        printf("%i\n", va_arg(ap, int));
    }
    va_end(ap);
}

int main(int argc, char const *argv[])
{
    print_ints(3, 111, 222, 333);
    return 0;
}
  • args报错了参数的数量,va_list用于保存传过来的其他参数
  • va_start说明可变参数从哪里开始
  • va_arg用于读取参数
  • va_end用于销毁va_list

Head First C 笔记 - 动态存储

动态存储

很多场景是在程序运行前你不知道自己需要多少存储空间,比如网页响应前你就不知道需要多少存储空间。所以要在运行时动态创建,这个操作通常在上进行。

下面这个案例展示了程序运行时动态创建一个叫island的链表,包括了内存的申请和释放,最后打印显示。

#include <stdio.h>
#include <string.h>

typedef struct {
    char *name;
    char *opens;
    char *closes;
    struct island *next;
} island;

void display(island *start)
{
    island *i = start;
    for (; i != NULL; i = i->next) {
        printf("Island: %s", i->name);
    }
}

island* create(char *name)
{
    island *i = malloc(sizeof(island));
    i->name = strdup(name); // 这里使用了strdup函数,避免了潜在的共享指针错误p284
    i->opens = "09:00";
    i->closes = "17:00";
    return i;
}

void release(island *start)
{
    island *i = start;
    island *next = NULL;
    for (; i != NULL; i = next) {
        next = i->next;
        free(i->name);
        free(i);
    }
}

int main(int argc, char const *argv[])
{
    char name[50];
    island *start = NULL;
    island *i = NULL;
    island *next = NULL;
    for (; fgets(name, 50, stdin) != NULL; i = next) {
        next = create(name);
        if (start == NULL)
            start = next;
        if (i != NULL)
            i->next = next;
    }
    display(start);
    return 0;
}

注意几点:

  • 递归的结构体必须要使用typedef创建一个别名,所以才可以在结构体中定义:struct island *next;
  • 程序在运行时动态创建了链表,使用malloc()申请了堆内存,使用free()释放
  • 使用strdup()复制字符串,避免字符串共享指针导致的数据错误

Head First C 笔记 - 结构,联合与位字段

结构,联合与位字段

结构体,联合与位字段可以组合起来描述这个复杂的世界。

结构体

看例子学习:

#include <stdio.h>

typedef struct {
    const char *food;
} preferences;

typedef struct {
    const char *name;
    int age;
    preferences care;
} fish;

void inc_age(fish *f)
{
    // 效果相同
    (*f).age++;
    f->age++;
}

int main()
{
    fish snoppy = {"Snoppy", 4, {"Meat"}};
    inc_age(&snoppy);
    printf("%s - %i - %s", snoppy.name, snoppy.age, snoppy.care.food);
}

注意几点:

  • 使用typedef关键字创建结构别名
  • (*f).age != *f.age,因为*f.age == *(f.age),你懂的
  • (*f).age === f->age,这样更易读

联合,枚举与位字段

继续看案例:

#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;
    unsigned int is_new:1; // 位字段,可节省空间
    quantity amount;
    unit_of_measure units;
} fruit_order;


int main()
{
    fruit_order apples = {"apple", "China", 1, .amount.weight=4.2, POUNDS};
    printf("%2.2f\n", apples.amount.weight);
    if (apples.units == POUNDS) {
        printf("bingo!");
    }
    return 0;
}

注意几点:

  • union用来定义一种叫"量"的类型,然后用户自己决定需要使用那个字段。p246
  • 当定义联合时,计算机只会为其中最大的字段分配空间
  • 由于我们不知道在联合中具体存了什么类型的值,这个案例中用枚举来标记了存储的类型
  • 位字段可以节省空间,位数可调

Head First C 笔记 - 使用多个源文件

使用多个源文件

大型软件通常会拆分源代码为多个小模块,最终将他们编译成一个可执行程序,关注以下几个关键点。

头文件

函数的定义顺序可能会导致编译错误(使用前需要先定义),头文件的作用就是为了做函数声明,让编译器不用做假设,可以防止编译出错。

共享代码

共同的特性,最好能共享代码。所以多个文件要想共享一份代码的话,自然要将共享的代码放在一个单独的.c文件中。那多个文件如何编译呢?

参考如下案例:
encrypt.h

void encrypt(char *message);

encrypt.c

#include "encrypt.h"
// 用于加密字符串
void encrypt(char *message)
{
    while (*message) {
        *message = *message ^ 31;
        message++;
    }
}

message_hider.c

#include <stdio.h>
#include "encrypt.h"

int main()
{
    char msg[80];
    while (fgets(msg, 80, stdin)) {
        encrypt(msg); // 这样通用的代码就能共享了
        printf("%s\n", msg);
    }
}

如何编译呢?很简单,只需要把源文件都传递给gcc即可。
gcc message_hider.c encrypt.c -o message_hider

使用make

首先需要了解下gcc的编译过程。

  1. 预处理:修改代码
  2. 编译:转换成汇编
  3. 汇编:生成目标代码
  4. 链接:放在一起

一个小的改动,不要重新编译全部的文件,耗时耗力。所以我们可以先将c源文件编译成目标代码文件,如果修改了某个源文件,只需要重新编译这个文件,最后链接目标文件即可。

同样是上面的案例,先编译成目标文件:

gcc -c *.c,生成.o文件

然后链接,编译器可以识别这些文件是目标文件:

gcc *.o -o message_hider

ok,这时如果修改了message_hider.c,那么只需要执行gcc -c message_hider.c,然后重新链接就行了。

新的问题,假设有很多源文件,你记不住修改了哪些文件,该怎么办?可以使用make来自动化这个过程。

encrypt.o: encrypt.c encrypt.h
    gcc -c encrypt.c

message_hider.o: message_hider.c encrypt.h
    gcc -c message_hider.c

message_hider: encrypt.o message_hider.o
    gcc encrypt.o message_hider.o -o message_hider

执行make message_hider,它会自动检查源文件的和目标文件的时间戳,如果有变动,就会重新编译。

Head First C 笔记 - 存储器和指针

存储器和指针

内存的存储结构

  • 栈(Stack):存储函数创建的变量值(局部变量)
  • 堆(Heap):动态存储分配
  • 全局量区(Globals):存储函数定义的变量值(全局变量)
  • 常量区:存储只读数据
  • 代码段:存储代码段

数组变量与指针

相同的地方

且看下面的案例:

#include <stdio.h>

void fortune_cookie(char msg[])
{
    printf("msg is: %s\n", msg); // msg is: abcde
    printf("msg has %lu bytes", sizeof(msg)); // msg has 8 bytes
}

int main(int argc, char const *argv[])
{
    char quote[] = "abcde";
    fortune_cookie(quote);
    return 0;
}

C语言中的字符串其实就是字符数组,上面将quote字符串传递给了函数,输出比较匪夷所思。

为什么sizeof(msg)长度是8?不应该是字符串的长度么?是因为数组变量可以当成指针使用,它指向数组在内存中的起始位置。可以做如下验证,得到的确实是一个地址。

printf("quote addr is %p\n", quote); // quote addr is 0x7ffeef53180a

其实在编译的时候,gcc也有相关的提示信息,根据提示,参数可由char *替代:

main.c:6:39: warning: sizeof on array function parameter will return size of
      'char *' instead of 'char []' [-Wsizeof-array-argument]
    printf("msg has %lu bytes", sizeof(msg));
                                      ^
main.c:3:26: note: declared here
void fortune_cookie(char msg[])
                         ^
1 warning generated.

所以sizeof(msg)不过是指针的大小罢了。

不同的地方

目前看起来数组变量与指针是一回事儿,但其实又不完全相同,还是来看案例:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char s[] = "abcde";
    char *t = s;

    printf("s has %lu bytes\n", sizeof(s)); // s has 6 bytes
    printf("t has %lu bytes\n", sizeof(t)); // t has 8 bytes

    printf("%d\n", &s == s); // 1
    printf("%d\n", &t == t); // 0
}

从上面的案例可以看出:

  1. sizeof(数组)就是数组的长度,这时C语言就开窍了
  2. 数组的地址,,就是数组的地址,&s == s
  3. 数组变量不能指向其他地方 p59

所谓的指针退化,就是上面案例的情况,将数组变量赋值给指针变量(char *t = s),这时候指针变量只会包含数组的地址信息,而对长度却无法感知。我们把这种信息的丢失称为退化

再来看一段代码:

char *cards = "JQK";
cards[1] = "A"; // 报错
char cards1[] = "JQK";
cards1[1] = 'A';

要注意的是,char *cards = "JQK" 是无法修改的,因为"JQK"存贮在了常量区,这部分存储是只读的,cards变量在栈上,它保存了"JQK"的地址。所以最好显示写成 const char *cards = "JQK,这样在编译期就能发现错误了。p73

char cards1[] = "JQK" 数值也存储在常量区,同时会copy到中,相当于创建了副本。所以可以修改。注意这里的cards1变量没有保持在存储器中,因为程序编译期间,会把所有对数组的引用替换成数组的地址。也就是说,在最后的可执行程序中,数组变量并不存在

指针运算

指针变量也是变量,保存的是数字,可以通过&获取它地址。所以指针是可以运算的:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char s[] = "abcde";
    char *t = s;

    printf("%c\n", t[0]); // a
    printf("%c\n", *t); // a
    printf("%c\n", *(t+2)); // c
}

要注意指针是有类型的,不同类型的指针运算表现会不一样,比如上面案例中,对char指针加1,指针就会指向下一个地址,因为char就占一个字节。但如果是int指针,地址就会加4,因为int通常占4个字节。

int nums[] = {1,2,3};
printf("%p\n", nums); // 0x7ffee69f580c
printf("%p\n", nums+1); // 0x7ffee69f5810,加4

用指针输入数据

看例子即可,主要说明了scanf()fgets()的区别。

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char foo[5];
    scanf("%s", foo);
    printf("%s\n", foo); // 可能会出现 segmentation fault

    char bar[5];
    fgets(bar, sizeof(bar), stdin);
    printf("%s\n", bar);
}

如果忘记了限制scanf()读取字符串的长度,用户输入的长度就有可能超过foo的长度,就有可能导致缓冲区溢出。而fgets()则不会,因为它强行限制了最大长度。