第0章 环境准备
主要是讲环境的搭建、编辑器的选择、以及为什么不用IDE。
环境搭建
这里主要关心的是linux环境,使用命令apt install build-essential来安装开发工具。再安装完成后,使用cc -version验证安装是否成功。
其他原则
- 挑选编辑器原则是熟悉够用,不要去追求完美的编辑器,不要在这个上面浪费时间
- IDE的诸多特性导致人变懒,降低对程序的敏感程序。
- 长时间依赖于IDE会导致没法用上语言的最新特性
第1章 准备好编译器
主要是以一个小demo来验证编译器是否可用,同时挨个解释每行代码的意义。
回忆点:
- include作用是将对应的文件导入到当前的代码中,里面通常包括各种函数的定义和全局变量的声明
- main函数的作用是提供一个操作系统执行程序的入口,同时在执行结束后,返回int值给Unix系统。具体int值代表的含义跟Unix系统有关
- 代码编译: make ex1
- 代码执行:./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如下:
CFLAGS=-Wall -gall: ex1clean:rm -rf ./ex1
-Wall参数:代表将警告信息打印出来
-g:代表输出调试信息
clean部分直接执行shell命令
课外资料
man make小结
- 除了c言语,make可以用来编译其他语言,不仅仅是语言,也可以用make来完成各种各种任务,因为make里面可以直接调用shell命令
make每次编译的时候,不是全量编译,会根据Makefile和最后一次修改的文件来做决定。也即是说如果文件已经编译过,但是未更新,不会被再次编译
man cc小结
cc-gnu项目下的c和c++编译器
知识点如下:g++和gcc使用的参数基本相同
- cc主要用于预处理、编译、 组装、链接,各个命令选项会作用了各个阶段。
需要查询的命令选项:
-Wall:开启一个警告全家桶,里面包括很多大家公认能在编码阶段避免的警告。这个全家桶包括很多警告标签,此处不再列举。
-g:gcc会产生很多调试信息,用于gdb等工具的调试
Makefile常用命令与选项
第3章 格式化输出
本节内容主要是将关于printf的格式化输出,例子中使用的是%d和%c,来输出数字和字符串。
在break环境,考虑两种情况:printf有占位符没有变量,printf有占位符有变量(变量未赋值)
正常代码如下:
#include <stdio.h>int main(int argc, char ** argv){int age=9;int height = 72;printf("I am %d years old. \n",age);printf("I am %d inches tall. \n", height);return 0;}
printf(“I am %d years old. \n”);
如果用这种形式输出,会输出一个很大的随机数。
makefile如下:
CFLAGS=-Wall -gall: ex3clean: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的代码,如下:
#include <stdio.h>int crash(){char *test = NULL;printf("%c", test[0]);}int main(int argc, char **argv){int age=10;int height=180;printf("my age is %d \n", age);printf("my height is %d \n", height);crash();}
下面是基本的调试技巧小结:
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等基本语法。
同时,推荐了使用闪卡来进行基本语法的记录。
变量和类型
代码如下:
#include <stdio.h>int main(int argc, char* argv[]){int distance = 100;float power = 2.345f;double super_power = 56789.4532;char initial = 'A';char first_name[] = "Zed";char last_name[] = "Shaw";printf("You are %d miles away. \n", distance);printf("You have %f levels of power. \n", power);printf("You have %f awesome super powers. \n", super_power);printf("I have an initial %c. \n", initial);printf("I have a first name %s. \n", first_name);printf("I have a last name %s \n", last_name);printf("My whole name is %s %c. %s. \n", first_name, initial, last_name);int bugs = 100;double bug_rate=1.2;printf("You have %d bugs at the imaginary rate of %f. \n", bugs, bug_rate);long universe_of_defects = 1L * 1024L * 1024L * 1024L;printf("The entire universe has %ld bugs. \n", universe_of_defects);double expected_bugs = bugs * bug_rate;printf("You are expected to have %f bugs. \n", expected_bugs);double part_of_universe = expected_bugs / universe_of_defects;printf("That is only a %e portion of the universe. \n", part_of_universe);char nul_byte = '\0';int care_percentage = bugs * nul_byte;printf("Whice meas you should care %d%%. \n", care_percentage);return 0;}
字符串定义: char lastName[] = “hello, world”;
字符定义:char initial = ‘A’;
单精度定义:float power = 2.345f
unsigned无符号代表什么含义?
问题整理
1.|下面的代码输出结果有什么区别?
#include <stdio.h>int main(int argc, char *argv){int i = -2;printf(">>>>>1: %d \n", i);printf(">>>>>2: %u \n", i);}
2.|输出结果为:
>>>>>1: -2
>>>>>2: 4294967294
解释: printf中%d输出的是有符号十进制整数,%u输出的是无符号十进制整数。 虽然i储存在内存里面的是同一个值,但是可以被编译器解释为不同的类型。
第10章 switch语句
基本原则:
- 永远要记得加上default语句,避免case未匹配的情况发生
- 先写case、break,然后再写里面的代码,避免忘记break语句
- 尽量避免多个case贯穿下来(中间没有break),如果真有该需求,最好加上注释//on purpose
- 如果可能的话,尽量使用if-else语句
课外作业
程序返回代码的含义
在linux系统里面,执行程序或者脚本的时候,返回0代表一切正常,非0则代表有问题。
程序返回0,代表程序正常。返回其他值,都代表什么?
c的头文件中定义的一些错误码:
#define EX_OK 0 /* successful termination */#define EX__BASE 64 /* base value for error messages */#define EX_USAGE 64 /* command line usage error */#define EX_DATAERR 65 /* data format error */#define EX_NOINPUT 66 /* cannot open input */#define EX_NOUSER 67 /* addressee unknown */#define EX_NOHOST 68 /* host name unknown */#define EX_UNAVAILABLE 69 /* service unavailable */#define EX_SOFTWARE 70 /* internal software error */#define EX_OSERR 71 /* system error (e.g., can't fork) */#define EX_OSFILE 72 /* critical OS file missing */#define EX_CANTCREAT 73 /* can't create (user) output file */#define EX_IOERR 74 /* input/output error */#define EX_TEMPFAIL 75 /* temp failure; user is invited to retry */#define EX_PROTOCOL 76 /* remote error in protocol */#define EX_NOPERM 77 /* permission denied */#define EX_CONFIG 78 /* configuration error */#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]等输出。 也就说,真实用法和数组相同。
char name[4] = {'a','a','a','a'};char *vname = name;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
该节内容主要有如下内容:
- 数组的定义方式
- 使用sizeof获取数组的大小
- 两种定义字符数组的方式
需要注意的内容如下:
.|数字类型数组的大小如何计算?
.|数字类型数组大小 = 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
该节主要内容如下:
- 函数的定义和使用,本节主要介绍的是函数前置定义。
先定义函数,函数的实现具体放在后面。
- 另外涉及到了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[]) {
char *name = "Alan";printf("%d \n", name[1]);printf("%d \n", name+1);printf("%d \n", *(name+1));return 0;}
.|输出结果为:
108
2033768277
108
name[1]:直接输出name指向的第二个字符
name+1: name第二个字符的地址,因为地址是32位的,所以输出的数字很大
*(name+1): 取出(name第二个字符的地址)中所保存的值
.|printf:下面代码的输出内容是什么?
include
int main(int argc, char *argv[])
{
char *name = "Alan";printf("%s \n", name+1);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(元素数量)
