Head First C 读书笔记

最近在重新学习 C 语言,本文是 Head First C 这本书里关于 C 的一些基础知识的总结笔记。

内存存储结构

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

指针

数组与指针

  • 数组变量可以被用作指针
  • 数组变量指向数组中的第一个元素
  • 如果把函数参数声明为数组,它会被当称指针处理
  • 指针变量也是变量。。保存的数字,也可以通过 & 获取它的地址
// 在C语言中,变量和值是存在不同的内存空间里
void fortune_cookie(char msg[])
{
    printf("msg is: %s\n", msg);
    // 由于传递了指针
    // 所以这里 sizeof(msg) 其实是指针变量的大小罢了
    printf("msg has %lu bytes", sizeof(msg)); // 8
}
char quote[] = "abcde";
// 数组变量就像指针
printf("quote addr is %p\n", quote);
fortune_cookie(quote);

区别在于

  • 数组的地址就是。。。数组的地址,&foo == foo,然而 &bar != bar
  • 当创建指针变量时,计算机会为它分配4或8个字节。但如果 创建数组,计算机会为数组分配存储空间,不会为数组变量分配任何空间,编译器仅仅会在出现它的地方把他替换成数组的起始位置,所以数组变量不可以指向其他地方,foo = bar 会报错
  • 存储区不同,char *cards = "JQK" 数值存储在常量区,无法修改,可显示写成 const char *cards = "JQK
  • char cards[] = "JQK" 数值存储在常量区,同时会copy到中,可以修改
int i = 121;
printf("%lu\n", sizeof(i)); // 4
printf("%lu\n", sizeof(int)); // 4

char foo[] = "abcdefghijk";
char *bar = foo;
printf("%s - %lu\n", foo, sizeof(foo)); // abcdefghijk - 12
// 这里取不到数组的长度信息,指针退化
printf("%s - %lu\n", bar, sizeof(bar)); // abcdefghijk - 8
printf("%c - %lu\n", *bar, sizeof(*bar)); // a - 1

foo[0] == *bar
foo[1] == *(bar+1) // 由于地址是一个数字,所以可以对其进行`指针算数运算`

指针的类型

  • 不同类型的指针算数运算会不一样
int nums[] = {1,2,3}
// int 通常占4个字节,地址会+4
nums + 1

char str[] = "abc";
// char 占一个字节,地址+1
str + 1

指针数组

// 保存指针的数组
char *names_for_dog[] = {"Bowser", "Bonza", "Snodgrass"}

结构体指针

  • 可以像创建其他类型的指针那样创建结构体指针
  • 如下代码:对于结构体指针,(*f).agef->age 效果相同
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);
}

函数指针

  • 函数名其实就是函数指针(不完全等于,不能做运算)
  • 定义:返回类型(*指针变量)(参数类型)
  • PS:char** 表示一个指针,指向字符串数组
char** album_names(char *artist, int year)
{
    // ...
}
char** (*names_fn)(char*, int);
names_fn = album_names;
char** results = names_fn("Sacha Distel", 1972);

动态存储

  • 使用 malloc() 申请堆内存
  • 使用 free() 释放
  • 使用 strdup() 复制字符串,避免字符串共享指针导致的数据错误
  • 使用 valgrind 调试内存溢出错误
  • 如下代码会在程序运行阶段创建一个动态创建 node 结构体
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
    char *question;
} node;
int main()
{   
    node *n = malloc(sizeof(node));
    n->question = "??";
    printf("%s", (*n).question);
    free(n);
    return 0;
}

静态库与动态库

静态库(编译时链接程序)

头文件:

  • 复制到 /usr/local/include 文件夹中即可,系统 .h 头文件位于 /usr/include
  • 使用绝对路径
  • gcc -I 指定头文件 gcc -I/my_header_file_path ...

目标文件:

  • 编译时指定完整路径名
  • 存档多个 .o 文件:ar -rcs libsafe.a checksum.o encrypt.o .a 为存档文件,必须以lib开头。可用 nm 查看存档文件 nm libsafe.a 或者 ar -t,(使用 ar -x libsafe.a encrypt.o 可提取目标文件)然后将 .a 文件保存在库目录中,通常是 /usr/local/lib ,或者其他目录
  • 最终编译时 gcc main.c -L./ -lsafe -o main -L 指定库目录,-l 指定库名
  • PS:-l 参数应该在源代码后面出现 gcc main.c -L./ -lsafe -o main

动态库(运行时链接程序)

  • 静态库的缺点是任何修改都需要重新编译全部代码,不能热插拔
  • 动态库和 .a 文件很像,不同的是这些目标文件在动态库中链接成了一段目标代码

创建一个动态库

  1. gcc -I./include -fPIC -c hfcal.c -o hfcal.o 相比编译成目标文件,多了 -fPIC 参数,告诉gcc创建与位置无关的代码
  2. gcc -shared hfcal.o -o ./libs/libhfcal.so 注意不能修改文件名
  3. 使用方式跟静态库一样,-l 指定库名即可
  4. 由于代码库是动态链接的,所以移动 .so 文件之后,代码就无法运行了,Linux 下需要export 环境变量 LD_LIBRARY_PATH 指定 .so 目录

gcc 指令

  • -c 预编译
  • -I 指定头文件(include)目录
  • -L 指定库目录
  • -l 指定库
  • -o 重命名生成文件
  • -fPIC 创建与位置无关代码
  • -shared 生成动态链接库

系统调用

  • 可使用 system() 进行系统调用
  • 列表函数:execl()execlp()execle()
  • 数组函数:execv()execvp()execve()
  • 速记:l 列表,v向量,p 路径,e 环境变量
  • fork() 创建一个子进程
system("ls ~");
pid_t pid = fork(); // fork一个子进程
if (pid == -1) {
    fprintf(stderr, "fork fail: %s\n", strerror(errno));
    return 1;
}
if (pid == 0) {
    // if 代码块的代码会在子进程里单独运行
    execl("/bin/ls", "/bin/ls", "/", NULL);
}

// 下面代码还会执行一行
execlp("echo", "echo", "/////", NULL);
char *env_vars[] = {"T_NAME=t_name", NULL};
execle("/bin/echo", "/bin/echo", "$T_NAME", NULL, env_vars);

char *my_args[] = {"/bin/ls", "/", NULL};
execv("/bin/ls", my_args);
execvp("ls", my_args);

if (execl("/bin/hhhhh", "/bin/ls", "/", NULL) == -1) {
     puts(strerror(errno));
}

进程间通信

管道通信

  • waitpid() 等待子进程结束之后再返回
  • fileno() 查询文件描述符号
  • dup2() 修改文件描述符表
  • pipe() 创建两条数据流,可用于链接父子进程
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>

int main()
{
    FILE *my_file = fopen("reports.log", "w");
    // 获取文件描述符号
    int descriptor = fileno(my_file);
    // 将文件描述符号注册到标准输出
    dup2(descriptor, 1);
    printf("%i\n", descriptor); // 内容将输出到reports.log中

    pid_t pid = fork();
    if (pid == -1) {
        fprintf(stderr, "fork fail: %s\n", strerror(errno));
        return 1;
    }
    if (pid == 0) {
        sleep(1);
        execl("/bin/echo", "/bin/echo", "im in sub process", NULL);
    }

    int pid_status;
    // 等待子进程执行结束
    if (waitpid(pid, &pid_status, 0) == -1) {
        fprintf(stderr, "waitpid fail: %s\n", strerror(errno));
    }

    // 使用pipe链接父子进程
    int fd[2]; // fd[1] 写管道,fd[0] 读管道
    if (pipe(fd) == -1) {
        fprintf(stderr, "pipe fail: %s\n", strerror(errno));
    }

    // 再创建一个子进程
    pid_t pid1 = fork();
    if (pid1 == -1) {
        fprintf(stderr, "fork fail: %s\n", strerror(errno));
        return 1;
    }
    if (pid1 == 0) {
        close(fd[0]); // 关闭读管道
        dup2(fd[1], 1); // 把写管道注册到子进程的标准输出
        execlp("echo", "echo", "pipe to parent process", NULL);
    }

    dup2(fd[0], 0); // 把读管道注册到父进程的标准输入
    close(fd[1]); // 关闭写管道

    char line[30];
    fgets(line, 30, stdin);
    printf("%s", line);

    return 0;
}

信号量通信

  • 使用 sigaction 创建一个信号处理的结构体实例
  • 使用 sigaction() 来注册 sigaction
  • 可以用 kill 命令,或者 raise() 函数 发送信号量,eg:kill -INT PID ,必杀技:kill -KILL PID 一定可以杀死进程
  • alarm() 可以定时发送 SIGALRM 信号,一个进程只能有一个定时器
  • SIG_DEL 还原默认的信号处理 或者忽略信号,catch_signal(SIGTERM, SIG_DEL)
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void godie(int sig)
{
    puts("what a cruel world");
    exit(1);
}

int catch_signal(int sig, void (*handler)(int))
{
    struct sigaction action;
    action.sa_handler = handler;
    sigemptyset(&action.sa_mask); // 用掩码来过滤要处理的信号
    action.sa_flags = 0;
    return sigaction(sig, &action, NULL); // 注册 sigaction
}

int main()
{
    // 捕捉中断信号
    if (catch_signal(SIGINT, godie) == -1) {
        puts("catch_signal fail");
        exit(2);
    }
    char name[30];
    fgets(name, 30, stdin);
    return 0;
}

一个有意思的小程序,测试你的数学水平:)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>

int score = 0;

void end_game(int sig)
{
    printf("Finnal score: %i\n", score);
    exit(0);
}

void times_up(int sig)
{
    puts("\nTIMES'S UP!");
    raise(SIGINT); // 发送中断信号,输出总分数
}

int catch_signal(int sig, void (*handler)(int))
{
    struct sigaction action;
    action.sa_handler = handler;
    sigemptyset(&action.sa_mask); // 用掩码来过滤要处理的信号
    action.sa_flags = 0;
    return sigaction(sig, &action, NULL); // 注册 sigaction
}

void error(char *msg)
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(1);
}

int main()
{
    catch_signal(SIGALRM, times_up);
    catch_signal(SIGINT, end_game);

    srandom(time(0));
    while(1) {
        int a = random() % 11;
        int b = random() % 11;
        char txt[4];
        alarm(5); // 5s之后发送 SIGALRM 信号
        
        printf("%i * %i = ", a, b);
        fgets(txt, 4, stdin);
        int answer = atoi(txt);
        if (answer == a * b) {
            score++;
        } else {
            printf("Wrong! Score: %i\n", score);
        }
    }
    return 0;
}

网络与套接字

server端

  • socket() 创建套接字
  • BLAB 四部曲:bind()listen()accept()开始对话
  • recv() 接收数据,send() 发送数据
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

int read_in(int socket, char *buf, int len)
{
    char *s = buf;
    int slen = len;
    int c = recv(socket, s, slen, 0);
    while ((c > 0) && (s[c-1] != '\n')) {
        // 移动指针
        s += c;
        slen -= c;
        c = recv(socket, s, slen, 0);
    }

    if (c < 0) {
        return c;
    } else if (c == 0) {
        buf[0] = '\0';
    } else {
        s[c-1] = '\0';
    }

    return len - slen;
}

int main()
{
    int listener_d = socket(PF_INET, SOCK_STREAM, 0);
    if (listener_d == -1) {
        printf("create socket error");
    }

    int reuse = 1;
    // 设置重用端口选项
    if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) {
        printf("setsockopt fail");
    }

    // bind
    struct sockaddr_in name; // 创建套接字名
    name.sin_family = PF_INET;
    name.sin_port = (in_port_t)htons(3000);
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name));
    if (c == -1) {
        printf("bind fail");
    }

    // listen
    if (listen(listener_d, 10) == -1) {
        printf("listen fail");
    }

    puts("waiting for connection");

    // accept
    struct sockaddr_storage client_addr;
    unsigned int address_size = sizeof(client_addr);
    int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
    if (connect_d == -1) {
        printf("accept fail");
    }

    // recv
    char buf[20];
    read_in(connect_d, buf, sizeof(buf));

    // send
    if (send(connect_d, buf, strlen(buf), 0) == -1) {
        printf("send error");
    }

    sleep(1);
    close(connect_d);
}

client端

  • getaddrinfo() 函数解析域名
  • connect() 连接服务器
  • PS:下面代码有问题,执行出错,谁帮忙看下?
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <netdb.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

void error(char *msg)
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
}

int open_socket(char *host, char *port)
{
    struct addrinfo *res;
    struct addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_CANONNAME;
    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = 0;
    hints.ai_protocol = 0;
    // 解析域名
    if (getaddrinfo(host, port, &hints, &res) == -1) {
        error("getaddrinfo fail");
    }
    
    int d_sock = socket(res->ai_family, res->ai_addrlen, res->ai_protocol);
    if (d_sock == -1) {
        error("create socket fail");
    }
    int c = connect(d_sock, res->ai_addr, res->ai_addrlen);
    // 清除堆上的数据
    freeaddrinfo(res);
    if (c == -1) {
        error("connect fail");
    }
    return d_sock;
}

int say(int socket, char *s)
{
    int result = send(socket, s, strlen(s), 0);
    if (result == -1) {
        error("send fail");
    }
    return result;
}

int main(int argc, char *argv[])
{
    int d_sock;
    d_sock = open_socket("www.so.com", "80");
    char buf[255];
    sprintf(buf, "GET /s?q=%s http/1.1\r\n", argv[1]);
    say(d_sock, buf);
    say(d_sock, "Host: www.so.com\r\n\r\n");

    char rec[256];
    int c = recv(d_sock, rec, 256, 0);
    while(c) {
        if (c == -1) {
            error("recv fail");
            exit(1);
        }
        rec[c] = '\0';
        printf("%s", rec);
        c = recv(d_sock, rec, 256, 0);
    }
    close(d_sock);
    return 0;
}

线程

  • 如果线程读取并更新了相同的变量,结果可能会有问题(线程不安全)
  • 线程函数的返回值类型必须是void*
  • 使用互斥锁来保护共享数据
  • pthread_create() 创建线程来运行函数
  • pthread_join() 等待线程结束
  • pthread_mutex_lock() 加锁
  • pthread_mutex_unlock() 释放锁
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

int num = 200000;
// 创建互斥锁
pthread_mutex_t a_lock = PTHREAD_MUTEX_INITIALIZER;

void* sub(void *a)
{
    int i;
    for (i = 0; i < 20000; i++) {
        // 给每个线程添加锁
        pthread_mutex_lock(&a_lock);
        num = num - 1;
        pthread_mutex_unlock(&a_lock);
    }
    return NULL;
}

void error(char *msg)
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(1);
}

int main()
{
    int i;
    pthread_t t[10];
    for (i = 0; i < 10; i++) {
        // 最后一个参数是传入参数
        if (pthread_create(&t[i], NULL, sub, NULL) == -1) {
            error("pthread_create fail");
        }
    }

    void* result;
    for (i = 0; i < 10; i++) {
        if (pthread_join(t[i], &result) == -1) {
            error("recycle fail");
        }
    }

    printf("res: %i\n", num);
    return 0;
}

标签: c

添加新评论