第0章 环境准备

主要是讲环境的搭建、编辑器的选择、以及为什么不用IDE。

环境搭建
这里主要关心的是linux环境,使用命令apt install build-essential来安装开发工具。再安装完成后,使用cc -version验证安装是否成功。

其他原则

  1. 挑选编辑器原则是熟悉够用,不要去追求完美的编辑器,不要在这个上面浪费时间
  2. IDE的诸多特性导致人变懒,降低对程序的敏感程序。
  3. 长时间依赖于IDE会导致没法用上语言的最新特性

第1章 准备好编译器

主要是以一个小demo来验证编译器是否可用,同时挨个解释每行代码的意义。

回忆点:

  1. include作用是将对应的文件导入到当前的代码中,里面通常包括各种函数的定义和全局变量的声明
  2. main函数的作用是提供一个操作系统执行程序的入口,同时在执行结束后,返回int值给Unix系统。具体int值代表的含义跟Unix系统有关
  3. 代码编译: make ex1
  4. 代码执行:./ex1

注意事项:

代码文件ex1.c, 执行make时是make ex1, 不是make ex1.c,否则会提示“Nothing to be done..”

第2章:初识Makefile

主要介绍使用Makefile简化程序的编译,同时介绍了Makefile的基本用法。
如果不用Makefile,则编译命令可以写成 CFLAGs=”-Wall” make ex1,因为Linux中可以通过这种方式将变量传递进去,相应于执行export CFLAGs=’-Wall”

Makefile中使用tab替代空格,保持格式的统一,不要tab和空格混用,导致Makefile识别失败。

完整的Makefile如下:

  1. CFLAGS=-Wall -g
  2. all: ex1
  3. clean:
  4. rm -rf ./ex1

-Wall参数:代表将警告信息打印出来
-g:代表输出调试信息
clean部分直接执行shell命令

课外资料

man make小结

  1. 除了c言语,make可以用来编译其他语言,不仅仅是语言,也可以用make来完成各种各种任务,因为make里面可以直接调用shell命令
  2. make每次编译的时候,不是全量编译,会根据Makefile和最后一次修改的文件来做决定。也即是说如果文件已经编译过,但是未更新,不会被再次编译

    man cc小结

    cc-gnu项目下的c和c++编译器
    知识点如下:

  3. g++和gcc使用的参数基本相同

  4. cc主要用于预处理、编译、 组装、链接,各个命令选项会作用了各个阶段。

需要查询的命令选项:
-Wall:开启一个警告全家桶,里面包括很多大家公认能在编码阶段避免的警告。这个全家桶包括很多警告标签,此处不再列举。
-g:gcc会产生很多调试信息,用于gdb等工具的调试

Makefile常用命令与选项

第3章 格式化输出

本节内容主要是将关于printf的格式化输出,例子中使用的是%d和%c,来输出数字和字符串。
在break环境,考虑两种情况:printf有占位符没有变量,printf有占位符有变量(变量未赋值)

正常代码如下:

  1. #include <stdio.h>
  2. int main(int argc, char ** argv)
  3. {
  4. int age=9;
  5. int height = 72;
  6. printf("I am %d years old. \n",age);
  7. printf("I am %d inches tall. \n", height);
  8. return 0;
  9. }

printf(“I am %d years old. \n”);
如果用这种形式输出,会输出一个很大的随机数。

makefile如下:

  1. CFLAGS=-Wall -g
  2. all: ex3
  3. clean:
  4. rm -rf ./ex3

课外资料

1. man printf 与man 3 printf的区别

man默认几个类型的内容:
1代表标准命令
2代表系统调用
3代表常用的函数与函数库,主要是c语言的库
8代表系统管理员命令

当执行man printf时,先从系统命令查找,结果看到的是系统命令里面的printf。
而执行man 3 printf的时候,直接查找c语言的函数库里面的printf。

printf的格式控制符

%d:输出有符号10进制整数
%ld:输出长整数
%x: 以十六进制格式输出
%c:输出一个字符

第4章 使用gdb调试

本章主要看了视频,下面是具体视频内容:
点击查看【bilibili】
为了调试,作者特意写一个会crash的代码,如下:

  1. #include <stdio.h>
  2. int crash()
  3. {
  4. char *test = NULL;
  5. printf("%c", test[0]);
  6. }
  7. int main(int argc, char **argv)
  8. {
  9. int age=10;
  10. int height=180;
  11. printf("my age is %d \n", age);
  12. printf("my height is %d \n", height);
  13. crash();
  14. }

下面是基本的调试技巧小结:
gdb ex3
break main:在main函数入口增加断点
break crash: 在crash函数增加断点
run: 开始运行服务(会在断点处暂停)
bt: 打印当前被调用函数的回溯信息(bt显示的是从入口点到当前函数或者断点的调用栈信息)
s: 命令全称step,运行下一行,相当于step in
n:命令全称next, 运行下一行,相当于step over
c:命令全称continue,继续运行,直到下一个断点
list:打印出后面10行待执行的源代码, list 10列出后10行,list 10列出前10行
info threads:列出当前所有线程
info break:查看当前所有断点

第5章 运算符

主要分成下面几类运算符:
1.数学运算符: + - / % ++ —
2.关系运算符: > == != < >= <=
2.逻辑运算符: && || ! ?:
3.位运算符: 与& 或| 非^ ~ << >>
4.其他
取值 &取地址

第6章 记住C语法

主要介绍了c的基本语法,包括c里面的全部关键字。
基本的条件、循环、switch等基本语法。

同时,推荐了使用闪卡来进行基本语法的记录。

变量和类型

代码如下:

  1. #include <stdio.h>
  2. int main(int argc, char* argv[])
  3. {
  4. int distance = 100;
  5. float power = 2.345f;
  6. double super_power = 56789.4532;
  7. char initial = 'A';
  8. char first_name[] = "Zed";
  9. char last_name[] = "Shaw";
  10. printf("You are %d miles away. \n", distance);
  11. printf("You have %f levels of power. \n", power);
  12. printf("You have %f awesome super powers. \n", super_power);
  13. printf("I have an initial %c. \n", initial);
  14. printf("I have a first name %s. \n", first_name);
  15. printf("I have a last name %s \n", last_name);
  16. printf("My whole name is %s %c. %s. \n", first_name, initial, last_name);
  17. int bugs = 100;
  18. double bug_rate=1.2;
  19. printf("You have %d bugs at the imaginary rate of %f. \n", bugs, bug_rate);
  20. long universe_of_defects = 1L * 1024L * 1024L * 1024L;
  21. printf("The entire universe has %ld bugs. \n", universe_of_defects);
  22. double expected_bugs = bugs * bug_rate;
  23. printf("You are expected to have %f bugs. \n", expected_bugs);
  24. double part_of_universe = expected_bugs / universe_of_defects;
  25. printf("That is only a %e portion of the universe. \n", part_of_universe);
  26. char nul_byte = '\0';
  27. int care_percentage = bugs * nul_byte;
  28. printf("Whice meas you should care %d%%. \n", care_percentage);
  29. return 0;
  30. }

字符串定义: char lastName[] = “hello, world”;
字符定义:char initial = ‘A’;
单精度定义:float power = 2.345f

unsigned无符号代表什么含义?

问题整理

1.|下面的代码输出结果有什么区别?

  1. #include <stdio.h>
  2. int main(int argc, char *argv){
  3. int i = -2;
  4. printf(">>>>>1: %d \n", i);
  5. printf(">>>>>2: %u \n", i);
  6. }

2.|输出结果为:
>>>>>1: -2
>>>>>2: 4294967294

解释: printf中%d输出的是有符号十进制整数,%u输出的是无符号十进制整数。 虽然i储存在内存里面的是同一个值,但是可以被编译器解释为不同的类型。

第10章 switch语句

基本原则:

  1. 永远要记得加上default语句,避免case未匹配的情况发生
  2. 先写case、break,然后再写里面的代码,避免忘记break语句
  3. 尽量避免多个case贯穿下来(中间没有break),如果真有该需求,最好加上注释//on purpose
  4. 如果可能的话,尽量使用if-else语句

课外作业

程序返回代码的含义

在linux系统里面,执行程序或者脚本的时候,返回0代表一切正常,非0则代表有问题。
程序返回0,代表程序正常。返回其他值,都代表什么?

c的头文件中定义的一些错误码:

  1. #define EX_OK 0 /* successful termination */
  2. #define EX__BASE 64 /* base value for error messages */
  3. #define EX_USAGE 64 /* command line usage error */
  4. #define EX_DATAERR 65 /* data format error */
  5. #define EX_NOINPUT 66 /* cannot open input */
  6. #define EX_NOUSER 67 /* addressee unknown */
  7. #define EX_NOHOST 68 /* host name unknown */
  8. #define EX_UNAVAILABLE 69 /* service unavailable */
  9. #define EX_SOFTWARE 70 /* internal software error */
  10. #define EX_OSERR 71 /* system error (e.g., can't fork) */
  11. #define EX_OSFILE 72 /* critical OS file missing */
  12. #define EX_CANTCREAT 73 /* can't create (user) output file */
  13. #define EX_IOERR 74 /* input/output error */
  14. #define EX_TEMPFAIL 75 /* temp failure; user is invited to retry */
  15. #define EX_PROTOCOL 76 /* remote error in protocol */
  16. #define EX_NOPERM 77 /* permission denied */
  17. #define EX_CONFIG 78 /* configuration error */
  18. #define EX__MAX 78 /* maximum listed value */

1:代表通用错误
2:shell内建错误
126:命令调用不能执行
128+n:信号为n的致命错误。如kill -9时,返回的就是137(128+9)
130:用ctrl +c来结束脚本

第11章 数组和字符串

本节内容主要介绍了字符串和数组的基本用法。
包括以下内容:
1.数组声明和初始化、修改
.|数组初始化时,如果位数不够,如何补全?
.|int list[4] = {0}

数组声明的时候,其他未指明值的话,会被默认填充0

list[3] = 12
或者奇怪的写法:
list[3] = ‘a’; //会将a的ascII码的值赋给list[3]

2.字符串的声明和初始化、修改
.|字符串声明的两种方式是什么?
.|char name[4] = {‘a’}

和上面相同,未指明值的位默认填充0。以%d输出值时,name[1]到name[3],对应的值都是0.

char *vname = “wchi”

这种方法声明更加灵活,也更为常用。 在输出vname里面各个元素的时候,可以用vname[0]等输出。 也就说,真实用法和数组相同。

  1. char name[4] = {'a','a','a','a'};
  2. char *vname = name;
  3. printf("vname %s \n", vname);

从上面的代码可以看出,两个字符串的声明方式是互通的。
char数组可以直接赋给char , char 也可以以数组的形式来访问元素。

注意事项

.|字符串数组,最后一位有什么要求?
.|字符串数组最后一位必须以0结束。
char name[4] = {‘a’,’b’,’c’,0};
或者
char name[4] = {‘a’,’b’,’c’,’\0’};

第12章 Sizes and Arrays

该节内容主要有如下内容:

  1. 数组的定义方式
  2. 使用sizeof获取数组的大小
  3. 两种定义字符数组的方式

需要注意的内容如下:
.|数字类型数组的大小如何计算?
.|数字类型数组大小 = sizeof(数组元素类型) 元素个数
比如 int list[] = {1,2,3}
该数组大小 = sizeof(int)
3 = 2 * 3 = 6
当前前提是int是2个字节。

这边还是有注意事项:将字符串赋给数组,数组最后一位会多加一个0,这就是数组的大小,比我们看到的字符要大1一个。

.|下面的代码中name的大小是多少?
char name[]=”Zed”;
printf(“%d”, sizeof(name));
.|输出结果:4
主要是因为该字符串里面还有一个隐藏的0,存储的时候需要分配4个字节。

.|下面的代码中name的大小是多少?
char name[] = {“Z”, “e”, “d”};
printf(“%d”, sizeof(name));
.|输出结果:3
用{}定义的数组,{}里面元素的数量,就是数组的真实数量。
这种定义的方式不是很准确,主要用于证实大小用的。
正确的是char name[] = {“Z”, “e”, “d”, 0}

第14章 Writing And Using Function

该节主要内容如下:

  1. 函数的定义和使用,本节主要介绍的是函数前置定义。

先定义函数,函数的实现具体放在后面。

  1. 另外涉及到了ctype.h, 用到了isalpha和isblank

第15章 Pointers, Dreaded Pointers

.|printf的简单理解
.|printf接收的参数:格式符,数据地址
格式符的目的是,从指定内存地址中读取出来的数据,如何展现出来。
%s,以字符串的形式,读取内存中数据,到\0停止
%d,以十进制整型的形式,读取内存中的数据,读取长度为sizeof(int)
%ld,以十进制长整型的形式,读取内存中的数据,读取长度为sizeof(long)

.|
下面的代码输出内容是什么?**

include

int main(int argc, char *argv[]){ int ages[] = {97,43, 12, 89, 2}; printf(“%s \n”, &ages); return 0; }

.|输出的结果是:a
机器中,sizeof(int)=4,ages里面97的存储形式是
(gdb) x/10b &ages
0x7fffffffd990: 0x61 0x00 0x00 0x00
所以,在读取第一个97(十六进制为61)之后,就读取到\0,字符串到此结束。
so,输出的内容是:a
.|printf:下面的代码输出结果是什么?

include

int main(int argc, char *argv[]) {

  1. char *name = "Alan";
  2. printf("%d \n", name[1]);
  3. printf("%d \n", name+1);
  4. printf("%d \n", *(name+1));
  5. return 0;

}

.|输出结果为:
108
2033768277
108
name[1]:直接输出name指向的第二个字符
name+1: name第二个字符的地址,因为地址是32位的,所以输出的数字很大
*(name+1): 取出(name第二个字符的地址)中所保存的值

.|printf:下面代码的输出内容是什么?

include

int main(int argc, char *argv[])

{

  1. char *name = "Alan";
  2. printf("%s \n", name+1);
  3. return 0;

}

.|输出结果为:lan
name+1的地址是第二个字符的地址。
printf(“%s \n”, name+1); printf需要一个字符串,所以会从这个地址开始读取,一直读到\0为止。此处我们读取出来的就是lan。

.|printf:如何输出一个指针的地址?
.|char *name=”Alan”;
printf(“addr1: %p \n”, name);

个人理解

单步调试:
.|int ages[] = {23,43,12,89,2}
cur_age = ages
cur_age++
为什么cur_age - ages = 1??
.|下面是单步调试的过程:
(gdb) p cur_age - ages
$9 = 1
(gdb) p cur_age
$10 = (int
) 0x7fffffffd974
(gdb) p &ages
$11 = (int (*)[5]) 0x7fffffffd970
(gdb) p sizeof(int) //输出是4
单步调试可以看出,执行++操作后,cur_age往前移动了一个int的地址。

ages虽然是数组,但其实相当于指针,里面保存的是第一个元素的地址。
如下所示:
ages+1,会导致指向下一个元素
(gdb) x/x ages
0x7fffffffd970: 0x00000017
(gdb) x/x ages+1
0x7fffffffd974: 0x0000002b
(gdb) x/x ages+2
0x7fffffffd978: 0x0000000c
(gdb) x/x ages+3
0x7fffffffd97c: 0x00000059
(gdb) x/x ages+4
0x7fffffffd980: 0x00000002

虽然大部分情况下,指针和数组的操作基本相同,但是还是有注意的地方。
比如sizeof(cur_age)和sizeof(age)结果是不同的,cur_age是指针,sizeof(cur_age)的结果为8,其实是指针占用的大小。
age是数组,sizeof(age)给出的是数组的大小,为20,其实就是4(int大小) * 5(元素数量)

第16章 Structs And Pointers to Them