C语言复习笔记
C语言复习笔记
Lesson 1: 输入输出
program程序 Operations运算符 expressions表达式 statements语句
理解第一个程序
hello.c
的各个部分:1
2
3
4
5#include : preprocessor directive 预处理指令
main: function 函数
int main(): takes no arguments, return an integer 无参数, 返回整型
main: .c contains one and only one main function 一个.c文件有且仅有一个main函数
printf: print+f: format 格式化输出%f或者%lf都表示匹配输出double类型的值;但是scanf输入只能使用%lf匹配
如果是输入输出的是long double类型, 对应的格式串是
%Lf
.const double PI;
定义常量, 常量要全大写命名。% 和 f 中间可以有一些东西. 例如
%10.2f
这里的点不是小数点, 而是分隔的意思, 10表示字段的宽度为 10 个字符, 如果浮点数本身不足 10 个字符, 那么会在左侧用空格填充; 2表示小数点后要保留2位数字.%.2d
或%02d
// 最少这个整数要输出2个数字,不足则前面补0%.2s
// 输出前2个字符 (如果不足2个字符就有多少输出多少), 空字符在尾部补齐%2s
// 输出长度为至少为2的字符串, 不够就在前面补空格注意字符串定义时要多定义一位,因为字符串的最后是\0,
例如:
char first_name[5] = "Tayu";
%c在匹配用户输入的时候不会忽略空白符,所以要注意在scanf中就要加入空白符以匹配掉0到任意个空白符。
1
2scanf("%s%s %c",
first_name, last_name, &gender);字符串越界, 会出现UB. 所以要用
%9s
限制输入的最长长度。%*lf
的意思是虽然让你输入, 但是变量该是什么还是什么. 这样的话scanf(“%*f”,&a);
用来输入double类型也是可以的。printf
默认遇到 '\0' 就会停止打印.pow
函数: 注意可能引起报错, 底数 a为负数并且指数 b 不是整数, 将会导致 domain error 错误
Lesson 2: if-for-array
要养成分行定义多个变量的编码风格.
变量在声明的时候一定尽可能给一个初始值,
int a = 0;
较小数程序:
1
2
3
4
5if (a >= b) {
min = b;
} else {
min = a;
}? : //Ternary Operator 三目运算符
输出单个字符也要用"", 因为是字符串:
printf("A");
undefined behavior:未定义行为
例:
char a;
scanf("%d", &a);
就是一个undefined behavior (UBs)
c语言标准没有规定发生什么情况, 出现任何情况都有可能, 这取决于编译器厂商.
所以, Avoid UBs! ! !
用宏定义的常量叫符号常量,
const
关键字定义的叫字面常量.推荐所有的数组长度都用宏定义的符号常量
可变长数组:
int n;
int a[n];
这样做是非常不推荐的
数组访问越界错误, 经典的UB, 编译器会悄无声息地换成另一个值.
for循环的第一部分 int i = 1; 可以是一个定义(C99以后才可以).
下标0的元素可以被赋值一个不影响后面元素的值 , 后面所有其余元素自动被赋值为0.
一个变量有左值和右值, i = 1; //左值, 指代的是i所在的空间
j = i; //右值,指的才是它真正的值
int num[] = {0};
这样只是开了一个元素, 不要以为开了任意个. 并且也不要不写长度.int num[20] = {[2] = 1};
这种写法是可以的int numbers[NUM] = {};
什么都没有, 标准上这样不允许.不推荐不对数组初始化.
如果字符数组不初始化(即在等号后面写上字面量的话), 那么它是什么值都有可能(每个元素都是垃圾值), 且最后一位也==不是'\0'==而是垃圾值. 所以要么就给出字面量, 要么就使用
sprintf
初始化, 否则自己一定记得把结尾后面的元素手动写成'\0'. (另:memset(str, 0, strlen(str))
是毫无问题的字符串初始化为全零的操作)
Lesson 3: for-while
VLA: variable-length array 可变长数组 C99 introduces VLA
C11 makes it optional 也就是说C11又把它去掉了(也就是说有没 有取决于编译器)
而且VLA在声明时不需要(也不支持)初始化
每写一段代码之后就应当下意识去运行一下, 以免错误积累...
如何输入任意次直到EOF:
1
2int len = -1;
while (scanf("%d", &numbers[++len]) != EOF); //先加,再判断。len++就是先判断再加这个操作值得掌握.
do-while 语句:
1
2
3do {
// ...
} while (a > 0); //注意这里有一个分号, 不要忽略了程序员在写代码时不要太聪明 (不是说不设计一个好的算法) , 是说要让代码可读且简单.
写代码就像写诗 -- 多换行.
在合适的场景多用布尔变量.
scanf是有返回值的. 一般来说, 匹配了几个就返回几.
%d%d%d, &a, &b, &c
如果输入 1 2就返回2
如果输入 abc 不能匹配, 就返回0
如果是 " " 这样的空输入就返回EOF, (end of file, 文件结束符, 一般来说其值= -1 )
读取int数组:
1
2
3
4do {
scanf("%d", &n);
arr[i++] = n;
} while( getchar() != '\n');读取字符串:
1
while( (string[i++] = getchar()) != '\n'); // 读入一个无换行的字符串,美中不足是最后一个位置会是'\n'(垃圾值)
strlen()
返回字符串长度memset()
只能初始化值为0和-1
Lesson 4: 多维数组
game-of-life.c
中涉及的系统调用:1
2
3
4
5
6
7
8Sleep(1000); //表示暂停的毫秒 ,在windows.h头文件下
system("cls"); //清屏
//Linux
sleep(1); //表示暂停的秒数; 在unistd.h下
system("clear"); // 在stdlib.h下
printf("\033c"); // 清空二维数组实现上下左右移动:
1
2
3
4
5
6
7
8
9
10int vectors[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; // vector[i][j]可以认为是第i组里面的第j个元素
int count = 0;
for (int k = 0; k < 4; ++k) {
int newI = i + vectors[k][0];
int newJ = j + vectors[k][1];
if (arr[newI][newJ] == '?') { // 实现上下左右方向的统计雷的个数
count++;
}
}
Special Lesson: Learning debug
Don't be too afraid, and learn from coding!
- debugging思想: 削减到最小出现bug的问题 (以方便集中注意力debug, 如果你不能debug, 那么至少你要先把bug的出现域缩小, 你可以另写一个函数, 最好就几行, 以便锁定bug, 节省自己和别的帮你debug的人的精力!) -- by Mr. Niu
- 编写测试代码, 调用检查函数, 尝试发现到底错在哪里.
- 有用的检查错误命令:
gcc -Wall hello.c
gcc -Wall -Werror hello.c
clang -Wall hello.c
- 运行时内存检查: gcc -fsanitize=address hello.c 可以看到泄漏的内存总大小 / 泄漏位置, 有用极了.
- 函数性能分析 Linux 下用
perf
Lesson 5: 函数
函数名命名使用大驼峰 (谷歌).
形参可以和实参同名.
避免全局变量.
int IsPrime(int number);
函数先声明, 后定义.
int BinarySearch(int key, int dict[], int len);
// 这里的形参不能不写上数组长度.int BinarySearch(int key, const int dict[], int len)
如果你不想修改数组, 那么传入时一定要用
const
修饰.多维数组传值: 一定要写除了第一维以外所有维的数值, 如:arr[] [][][][[LEN] [LEN][LEN]
在函数定义上面写 /** 加回车, 以对函数功能及参数含义作注解.
C语言只有传值, 以传地址的值的方法来实现传引用.
无参数输入的函数声明: int Fun(void); C语言的老特性, 不能省略void, 否则可能出问题. 但是C++等其它语言则不需.
Lesson 6: 递归
how? C语言语法支持自调用甚至可以调用main函数自己 (c++就不能自调用main).
why? 适用情景: 解决问题时又遇到了和原问题差不多的问题.
会不会一直递归下去, 无穷无尽? 有可能.
要用递归的思维去思考问题: 子任务是比原来任务规模更小的任务, 直到最后碰到了一个最小的任务, 然后把它直接解决掉. (困难, 需要有意识训练, 需要花很长很长的时间).
训练递归思维 (Ask the mirror right questions) :
==What is a smaller task?== (更小的问题是什么?)
How to solve the task given the solution to the smaller one? (如果知道了小问题的解, 怎么推出原问题的解?)
What is the smallest task? (最小规模的任务是什么?)
像一个计算机一样思考, 理解递归的底层实现.
栈空间(Stack), 堆空间(Heap), 栈帧(Stack Frame):
Stack
main (bottom) a 25 | b 37 | min 25 (top) min a 25 b 37 (new top) 栈空间(用以存储局部变量), 生长的方向是从上往下(自底向顶).
调用一个函数时,会为这个函数准备一个栈帧 栈帧存储局部变量. 函数结束后, 栈帧消失.
遵循FILO的一个结构. 最后调用的最先消失, 先是往栈空间不断压栈帧的过程, 后是不断弹出栈帧的过程.
想要更深入理解栈空间/堆空间, 请移步计算机系统基础.
注意函数里变量的生命周期问题, 函数里的数组如果是在栈空间里, 那么这个数组会随着函数的结束而消失, 所以你无法通过
{int a[1005]; ... return a;}
来用返回这个数组的首地址的方式试图返回这个数组/ 字符串字面量.解决方案 1.
malloc
函数申请堆空间, 但是要注意判断是否申请成功, 且调用者须把空间free掉. 2. 操作外面的数组 (传入的/ 全局的)
Lesson 7: 数据类型
基础数据类型 int double char bool
聚合数据类型 []
整型 short (int) , int(至少4个字节), long (int), long long (int) (至少8个字节) 只是不会减少, 但不一定严格递增 signed vs. unsigned
printf函数 无符号整型
%u
, 有long就加一个lprintf("ULONG_MAX = %lu\n\n", ULONG_MAX);
有符号整数溢出是UB, 无符号整数溢出会发生回绕现象.
size_t 就是一个unsigned long long. 输出时使用%zu
混用有符号和无符号就会出错 -1 > 256(unsigned). 所以尽量不要用无符号, 除非你很有把握不会与有符号数比较. (有符号会被隐式转换成无符号的)
注意 可以使用
typedef unsigned long long int size_t
如果只写一个char, 无从得知是有符号还是无符号. (看具体系统)
隐式类型转换 (1) 算术/逻辑表达式(类型提升) (2)定义初始化, 赋值(类型转换) (3)函数调用时(类型转换) (4)函数赋值时(类型转换)
想要明确表示是float 后缀要加F.
float pi = 3.1415926F
不加F的话就不是float, 默认是double.long double 的话后缀加L.
在C语言中, 0开头的数字为八进制数, 010就是十进制的8.
epsilon 表示浮点数所能表示的两个最近的能精确表示的数字之间的gap. (这个gap不可能为0, 为什么?)
浮点数实在太复杂, 但是好在我们实际上基本用不到. (如: 做web开发就几乎和浮点数无任何关系)
浮点数表示能力不够的情况除了overflow还有underflow也就是太接近0了, 无法表示(且这个gap一定会存在).
Lesson 8: 指针
printf("%p", &radius);
// 打印地址变量(指针)要用%p
, 其输出为一个十六进制数存储的其实是int类型中4个连续字节的首地址, 而不是一个字节一个字节地存储. 拿到这个首地址就可以根据int这个变量类型来解析出那4个所要的字节.
左值与右值, 等号左边叫左值, 代表变量所在的一块==存储空间==(not the first address), 而右值代表空间里的那个内容
所以, 我们可以使用
(*ptr) = 20;
这样的语句来给所指的变量赋值int a = 0;
直接访问a, 或者可以int *prt = &a
; 访问*p来间接访问a*
叫作"解引用运算符"或者"间接寻址运算符".==动态数组: ==
1
2
3
4
5
6
7
8
9
10
int *numbers = (int *) malloc(len * sizeof(int)); // (老版本 / C++)需要强制类型转换, 因为malloc返回值是void型; 且返回的是整块内存的首地址(所以使用指针变量来存).
int *numbers = malloc(len * sizeof(*numbers)); // 本质上就是数组声明
// recommended way
for (int i = 0; i < len; i++) {
printf("%d ", &numbers[i]);
} // 把这块连续的空间当成数组使用(也可作其他用途)如果写
int *prt = ...
那么其实是无法判定指向的是一个int型变量还是一个以此变量开头的数组(需要C程序员自己清楚).==指针与数组== 数组名即是数组第一个元素的地址(指向int的一个指针) 所以, 可以把指向一块内存空间的首个元素的指针名接作为数组名使用.
即
numbers[i]
就等价于*(numbers + i)
&numbers[i]
等价于numbers + i
数组的名字本质上不是一个变量, 而永远都是这个数组首元素的地址.我们只能取它的值, 而不能修改它(右值).
如果硬想把它变成一个变量, 则再定义一个指针等于它.
int *prt = arr;
指针的加法
*(numbers + 1)
并不是在数值上加1, 而是说一次跳过一块内存. (区别于普通的整数运算)类似的, 指针和指针可以相减, 不可以相加, 相减结果表示连续的一块内存地址两个地址之间相差的元素数. (注意不是字节数)
那么
arr[i]
就是*(arr + i)
就是*(i + arr)
就是i[arr]
// 但是你不要这么写, 码风不好.另外
&arr[i]
就是&(*(arr + i))
就是arr + i
动态内存申请的空间用完了还需要还回去.
为什么要还, 因为此空间不是存在栈帧里的, 而是在堆上面
如果不free的话, 申请这块内存的指针已经没了, 但是内存里这块空间还被占据着, 没人能够再访问它. 发生内存泄漏 (引用这块内存的指针变量已经没有了但是没有释放被引用的内存)
1
free(numbers); // 注意: 同一块内存不能free两次
Q: 怎么分析是否一块内存是否被free了两次, 这其实是无法静态分析出的.
如果不是堆空间不可free.
申请内存有可能失败, 失败就返回一个空指针.(指向NULL的指针)
1
2
3
4if (numbers == NULL) { // 就是空指针
printf("Error; No dynamic memory any more.\n");
return 0;
}calloc
: malloc 之后赋予所有元素0, 而非垃圾值realloc
: 重新申请一个内存, 并且把原来的数组已有值赋给现在的内存.指针和字符串 1) 声明 char msg[20] = "Hello World!"; 也可以 char *msg = "Hello World!";
2) 默认的使用指针定义的字符串字面量不可以修改char *msg = "Hello World!"; msg[0] = "N"; // 这是UB
3) 但是如果用数组定义字符串字面量就可以修改 char msg[20] = "Hello World!"; msg[0] = "N"; // 允许
()++的优先级高于*, (至于++(), 和 * 优先级相同) 所以以下语句先让
str++
, 且str++
返回的是自增之前的值(不同于++str
).1
*str++ != '\0';
const char *str
作为参数的意思是, 不允许通过str这个指针来修改它指向的字符, 不等同于不能修改这个字符本身.const char *str
和char const *str
是一样的, 限制str指针所指向的字符不能修改. 但是,char * const str
就不同了, 它限制了str 这个指针是一个常量指向char类型, 限制指针不能被修改 (即str++
不允许)size_t
是什么类型, 可以理解成无符号长长整型, 且与机器无关, 配合%zu使用.指针是怎么来的? 1) 所有的数据存放在内存里 2) 内存可以编号, 即地址, 通过地址可访问内容 3) 一个数据类型占有若干个byte(字节), 那么把其占据的几个byte的第一个byte的地址作为指针的值.
Lesson 9: 指针高级
strcpy
:1
2
3void StrCpy(char *dest, const char *src) {
while ((*dest++ = *src++)); // 写成指针, 去掉循环控制变量, '\0' 的ascii就是 0
}strncmp
: 比较两个字符串中的最多前n个字符指针数组: 每个元素是一个指针.
1
2
3
4
5
6
7
8
9
10const char *names[LEN] //指针数组定义
Swap(&str[i], &str[min_index]);
// 相应的, 要再取一次地址, 才能传入一个二级指针
void Swap(char **left, char **right) {
char *tmp = *left; // left 实际上是二级指针, 用 * 解一次引用解出来一个指向char的指针
*left = *right; // 这样修改的是指向字符串字面量的指针
*right = tmp;
}关于多维数组, 你只需要记住: C语言没有二维数组, 只有数组的数组.
二维数组是一个指向数组的指针, 二维数组是一个二级指针.
1
2int score[ROWS][COLS] = malloc(ROWS * COLS * sizeof **score); // 感觉不如写sizeof(int)
if (score == NULL) return 0;运算符优先级 :
(suffix)++, (), [], . , ->
++(prefix) , * , & , ! , ~ , sizeof
*, /, %
+, -
<<, >>
[] // 取数组下标符具有左结合性, 即
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int student_score_table[5][3] = {
{0, 10, 20},
{10, 20, 30},
{20, 30, 40},
{30, 40, 50},
{40, 50, 60}
}; // 可以理解为是一个大小为5的数组, 数组里面是一个大小为3的数组
/**指针访问二维数组*/
printf("student_score_table[3][2] = %d\n",
student_score_table[3][2]);
printf("student_score_table[3][2] = %d\n",
(*(student_score_table + 3))[2]);
printf("student_score_table[3][2] = %d\n",
(*(*(student_score_table + 3) + 2)));
Lesson 10: 函数指针/结构体
main函数里面是可以有参数的, main函数可以接受两个参数
argc
(argument count计数, 调用了几个参数) 和argv
(argument vector) 一个数组, 数组里每个元素是一个指针, 指向char.int main(int argc, char *argv[]) {}
C语言中约定
argv[0]
存储了程序的名字(argv[0] is the name of program)(当然一些环境或操作系统中argv[0]
是一个空指针, 不存放任何东西但是占了位), 所以真正的存入argv[]是从下标1开始存入的.C语言标准规定
argv[argc]
is NULL (一定是一个空指针).如: hello world
argc == 3;
argv[0] == "echo"(文件名), argv[1] == "hello", argv[2] == "world", argv[3] ==NULL
printf
函数可以结合表达式使用printf((argc > 1) ? "%s " : "%s", *++argv);
void qsort( void *ptr, size_t count, size_t size, int (*comp)(const void *, const void *) );
参数含义: 1. 待排序数组首地址(数组名) 2. 数组中元素数 3. 每一个元素的字节数 4. comp是一个指针, 指向一个函数 ==(函数指针, functional pointer)==, 指向的函数有两个参数是void型, 返回值是int.
如何获取数组长度:
1
2int integers[] = {1, 2, 3, 4}
int size_of_integers = sizeof integers / sizeof *integers; // 计算数组长度 sizeof一个数组就是返回整个数组的字节数, 注意这里sizeof不要在后面跟(), 但是sizeof(int)需要在C语言中函数名本身就是一个指针.
CompareInts()
这个函数, 在作为第四个参数传入qsort()
时, 只要写CompareInts
就代表一个函数指针.当函数名出现在表达式中时, C语言解释其为函数指针(也就是说, C语言中, 函数就是函数指针).
函数指针和其它指针一样用, 你还可以构建一个函数指针数组.
1
double (*fps[2])(double) = {sin, cos}; // fps是一个数组, 有两个元素, 每个元素是一个指针, 指向一个函数, 这个函数接受一个double返回一个double
函数指针说到底是一个变量, 是变量就可以声明, 可以修改.
1
int (*comp)(const void *, const void *) = CompareInts; // 很诡异的一个声明和赋值, 这样之后comp就完全等价于CompareInts, 在C语言中是不需要再对函数指针再解引用了(即不需要写*comp, 区别于传统指针)
除了
qsort
, C语言还有bsearch
1
void *bsearch(const void *key, const void *base, size_t nmem, size_t size, int (*comp)(const void *, const void *)); // base指向被查找的数组, nmem为查找长度, size为每个元素的大小, comp同qsort()的
那么有了函数指针, 函数就是一个一般变量了, 可以作为函数的参数, 函数的返回值, 数组的元素 , 可以声明, 定义, 可以赋值, 函数指针可以干任何一般变量能做到的. 函数就成为了一等公民.
复杂的函数声明: 核心是指针与数组 /
*
,[]
,()
三个运算符的结合与优先.题目1: 解释以下输出
1
2
3int (*arr)[3];
printf("%lu", sizeof (arr)); // 8
printf("%lu", sizeof (*arr)); // 12题目2: 解释以下声明
1
void (*signal(int sig, void (*handler)(int)))(int);
使用
typedef
定义特定类型函数指针的别名typedef int (*NICKNAME)(int, int);
这样NICKNAME fun;
就是对fun的一个声明.struct: 一种数据类型, 与int, char等等一样.
使用
typedef struct musician {, , , ,} Musician;
// 用别名Musician替代struct musician结构体填充/内存对齐, 结构体的存储是有要求的, 结构体成员里面最大字节数如果为8, 那么必须要首地址是8的倍数, 尾地址也要是8的倍数.
结构体也可以通过赋值号( = )来赋值, 当然, 前提是两个结构体类型相同.
结构体变量也可以直接作为参数进行函数传参, 但是, 应该使用结构体指针进行传参以避免直接传结构体降低程序效率.
结构体成员名可以和main函数中的变量重名.
使用 结构体变量名 . 内部的变量名 (点表达式) 来访问结构体内部.
结构体变量名就是一个结构体变量, 对其取址(&musician)得到的就是一个结构体指针(指向结构体变量的指针) (用结构体指针可以实现结构体的跨函数传递, 比直接传递结构体性能更高).
C语言不支持结构体直接判断相等. 即
struct1 == struct2
是不可以的(C中能用==判断相等的只能是基本数据类型), 思考:ptr1 == ptr2
意味着什么?struct1
和struct2
的每一个成员都相等, 又意味着什么?
结构体指针示例:
1
2
3
4void PrintMusician(Musician *m) { // m为一个结构体指针(特别常用的做法)
(*m).name // 通过指针访问结构体内部的变量方法1(注意.的优先级最高, 所以括号不能省略)
m->gender // 通过指针访问结构体内部的变量方法2
}动态申请结构体:
1
2
3
4
5
6
7struct book *book1;
book1 = malloc(sizeof(*book1));
//也可以写成:
struct book *book1 = malloc(sizeof(*book1));
// malloc之后下意识检查是否malloc到了空指针
if (book1 == NULL) {...}enum
枚举类型.
定义某一个数据类型, 这种类型只有几种可能, 在定义的同时将这些可能一一列举出来, 这样的类型就叫枚举.
Lesson 11: 链表
链表的每一个节点(Node)就是一个结构体, 这个结构体的第一个域可以是一个整数, 第二个域需要是一个指针(叫作next, 指向链表的下一个节点). 链表就是通过指针把多个结构体连在一起.
链表和数组的区别在于数组的内存空间是连续的, 而链表每个节点都是
malloc
动态申请来的, 没法保证每一个的内存空间是连续的.所以要访问链表的某个节点就只能从头开始往后扫描(最坏的时间复杂度是O(n)而数组为O(1) ).
(好处在于灵活, 所以插入/删除链表节点时间复杂度O[1], 而插入/删除数组元素时间复杂度最 坏O(n) )
链表实现了数据的动态保存, 不需要预先分配内存空间, 而是在需要时动态申请, 整个空间可以根据需要扩大或缩小.
双向链表: 一个指针(pre)指向前面, 一个指针(next)指向后面.
循环链表: 最后一个节点的next指针指向Head.
解决链表问题: 先想一般情况, 再讨论特殊情况 (空链表/链表只有一个节点), 再看这两类有没有可以合并的.
删除节点函数Delete写的时候不应该传递被删除节点, 应传前一个指针. (双向链表无所谓)
如果删了head或tail节点, 要记得更新head或tail.
如果只想要在某函数符合特定条件的时候才调用一个函数, (如只有只剩1人时才return幸存者编号, 其它情况不使用该函数).
1
2
3
4
5
assert(IsSingleton(list)); // 做一个判断, 不成立就报错. (assert 断言)
/**
* assert一般用于调试, 真正发布时, 要将assert去掉
**/常见的链表操作补充:
1
2void Insert(LinkedList *list, Node *prev, int val);
Node *Search(LinkedList *list, int val);特殊头节点: 不存数据, 链表的长度. 对头节点的删除操作, 不用特殊化处理. (方便读取链表的长度, 可用于频繁获取链表长度的情形)
打印/遍历链表:
1
2
3
4while(p != NULL) {
printf("%d", p->data);
p = p->next;
}
Lesson 12: 预处理
预处理就是字符串替换.
宏函数可以接受不定长度的参数
例如
1
2
3
4
5
// or another way:
// #define print(format, ...) printf(format, ##__VA_ARGS__)
print(1, "%s", "To C or not to C");当你想要把多个语句放在一起作为一个宏函数时, use
do{ }while(0)
这样的好处是, 如果调用宏函数, 就需要在末尾加;
1
2
3
if (1) foo(); // 这样就十分自然地写出了看起来没有问题, 实际上也没问题的代码.一些系统给的宏
__x86_64__
如果定义了就是在64-bit 环境下.1
2
3
4
5
6
7
8
9
10
11
12
13
// do sth
// do sth
// 帮助定位错误的宏:
__FUNCTION__
__LINE__
__VA_ARGS__ // C99引入, 表示一个或多个参数, 类似函数可变参数中的省略号
// 示例:#
和##
运算符:1
2##
运算符##
可以将两个记号 (如标识符) "粘合" 在一起, 成为一个记号. (##运算符被称为"记号粘合") 如果其中一个操作数是宏参数, "粘合"会在形式参数被相应的实参替换后发生.1
2
3
4
int MK_ID(1), MK_ID(2), MK_ID(3);
// 就是int i1, i2, i3;1
2
3
4
5
6
7
type type##_max(type x, type y) { \
return x > y ? x : y; \
}
GENERIC_MAX(float)
// float float_max(float x, float y) { return x > y ? x : y;}如果对于字符串进行宏定义替换, 替换之后又要进行 ## 或者是 # 的操作, 又不希望因为直接做##或#而忽略掉之前的宏定义, 就可以使用一个temp宏作为中介使得一趟替换之后没有到位, 而是转入temp宏, 这样就能够使得字符串替换不被忽略掉.
预处理指令
#include
包含#define
宏定义(根据条件判断来选择编译的内容)
#if
如果, 则预定义#else
否则, 预定义#elif
否则如果, 则预定义#endif
结束条件判断#ifdef
如果定义了, 则 (if define)#ifndef
如果没定义, 则 (if not define)#include
两种形式 <> 到配置目录中找和 " "从当前目录开始找, 无则到配置目录里找.
文件包含允许嵌套. A包含B, B包含C, 那么A包含了B和C
#define
宏定义, 可以带参数.
条件编译 (为了提高可移植性的)
#if (常量表达式) --> 注意常量, 非变量
仅当表达式为真, 才编译它与
#endif
之间的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
printf("程序调试中\n");
printf("程序测试中\n");
printf("欢迎使用正式版!\n");
/*-----------------------------------*/
define PI 3.14常用宏函数:
1
2
3
4
5
6
7
8
9
do { \ // 为什么不给a, b加括号, 因为 = 是除了 , 最低优先级的运算符
int t = a; \
a = b; \
b = t; \
} while (0) //宏需写在代码头文件下面,此为简单版,交换两个 int 型数字
// 最简洁的swap
x ^= y ^= x ^= y; //异或真神奇!不过这究竟是为什么呢?定义宏函数:
1
2
3
4
5
6
7
8
static inline int Dist(int x1, int y1, int x2, int y2) {
return abs(x1 - x2) + abs(y1 - y2);
}#include
预编译的机制, 其实就是把被 include 的代码copy到当前的c文件中, 只是做了一个substitute的工作, 你可以写出以下的代码:1
2
3
4
5
6
7// hello.c
int main() {
printf(
);
return 0;
}1
2// inc.h
1
2// hello.h
hello world用gcc预编译
gcc -E a.c | less
然后用vim命令
\printf
搜索对应的函数的声明extern int printf (const char *__restrict __format, ...);
将这行代码copy也可以实现
printf
函数.其实
#include <stdio.h>
就是把以上预编译的东西全部粘贴过来, 而其中起作用的就是printf
的声明.这就是C语言的规定,
#include
做的就是纯文本的复制粘贴.预处理过程:
1
2
3
4
5
6
7
8
9
int main() {
printf("Yes\n");
printf("No\n");
}这段代码被预编译为:
1
2
3
4
5
6
7
int main() {
printf("Yes\n");
}其它信息全部被擦除了. 一个变量如果是在预编译指令中, 那么不需要定义就可以使用, 其值是系统赋予的, 如
__x86_64__
, 而本例中, aa和bb都是空, 所以二者相等.适当使用预编译指令, 不要滥用.
预编译也称为元编程 (meta-programing) , 发生在实际编译之前. (C++的模板元编程) (gcc的预处理器同样可以处理汇编代码)
Lesson 13: 位运算
移位运算符: << >> ( 有符号数右移, 高位补什么视编译器而定, 一般补的是符号位(最高位) )
为什么有符号数右移视编译器而定? 历史久, 需要兼容各类系统, C标准没有规定.
向右移动x位相当于除以\(2^x\)
移位运算的使用
- 在能满足需求的情况下, 使用无符号数进行移位运算
- 如果需要使用有符号数的移位运算, 请验证高位补偿的规则并考虑可移植性问题.
按位与 ( & ) 按位或 ( | ) 按位异或 ( ^ )
一元运算 按位取反 ( ~ ) 会对操作数进行整型提升 (不能忽略前面的位)
优先级: ~ > 二元算术运算 > <<, >> > 关系运算 > & > | > 逻辑运算
几个常见的错误示例:
status & 0x4000 != 0
表示先不等于0, 然后与运算i << 2 + 1
表示先把 2 + 1, 再移位使用位运算访问位 将num的 (从低开始的) 第i位读取出来.
1
num & (1 << i)
将该位设置成0
1
num &= ~(1 << i)
将该位设置成1
1
num |= (1 << i)
使用位运算访问位域
将 1 << i 替换为多个1bit (如 0x0070) 操作第4 - 6位
异或加密
1
2
3a ^ a = 0
a ^ 0 = a
a ^ b ^ b = a加密方式: 有信息M, 秘钥K, 可以使用M' = M ^ K获得密文, 再使用M' ^ K = M ^ K ^ K = M进行解密.
bit vector 位向量
一个长度为N的bit流可以表示一个最多有N个元素的集合. 下标 i 对应元素的布尔值如果为true就代表i这个数字在集合set中, 以此表示一个N个元素的集合.
有什么用? 代替bool数组, (因为bool类型是C99后才有的), 可以换取空间优势.
做集合的 交 / 并 / 差 运算时性能可观.
例: 交换一个32位整数的高16位和低16位.
1
y = ((x & 0xFFFF) >> 16) | ((x >> 16) & 0xFFFF)
例: 取以2为底的对数的整数部分 [Log2 x]
1
2
3
4
5
6
7
8
9int getLg(int x) {
int ans = 0;
if(x & 0xffff0000) {ans += 16; x &= 0xffff0000;}
if(x & 0xff00ff00) {ans += 8; x &= 0xff00ff00;}
if(x & 0xf0f0f0f0) {ans += 4; x &= 0xf0f0f0f0;}
if(x & 0xcccccccc) {ans += 2; x &= 0xcccccccc;}
if(x & 0xaaaaaaaa) {ans += 1; x &= 0xaaaaaaaa;}
return ans;
}
Sp Lesson-1 git
git就是一系列快照, git add 就是把文件加到临时快照里, commit就是把临时快照持久化, 而分支就是不同的程序员在进行各自的开发时使用便于管理的.
常用命令:
git init
git status
git commit -m ""
git log
git add -A //把当前文件夹中所有的纳入跟踪, 添加到暂存区
git checkout fe923 // fe923就是你某一次提交代号的前几位
git checkout 8f49c // 回到某一次具体的提交(可以是已经被回退掉的版本号)
git checkout -b "git" // 新建并转到命名为"git"的分支
git checkout master // 回到"master"分支
git branch // 查看分支信息
git merge // 合并分支
git merge git 进入一个界面, vim模式, 先按i进行insert编辑后, 按esc退出
主流的版本控制器 Git SVN
版本控制分类 1) 本地版本控制 2) 集中版本控制(所有的版本数据存在服务器上, 代表工具
SVN
) 3) 分布式版本控制 (每个人都拥有全部的代码, 服务器或者任一用户的保存损坏了都不会有影响, 只要再重新拷贝一份就可以了, 代表Git)Git是目前世界上最先进的分布式版本控制系统
2005 Linus Torvalds 用2周时间开发出了自己的版本控制系统, 也就是后来的Git (为了辅助Linux内核, 开源, 免费)
Git Bash: Unix 与 Linux 风格的命令行, 使用最多, 推荐最多
Git CMD: Windows 风格的命令行
Git GUI: 图形界面的Git, 不建议使用
环境变量只是为了全局使用.
Git 基本理论
工作区域
Git 本地有三个工作区域: 工作目录 ( Working Directory ) , 暂存区 (Stage/ Index) , 资源库 (Repository / Git Directory) , 如果在加上远程的git仓库 (Remote Directory) 就可以分成四个工作区域.
- ==Workspace==: 工作区, 就是你平时存放项目代码的地方
- Index / Stage: 暂存区, 用于临时存放你的变动, 事实上只是一个文件.
- Repository: 仓库区 ( 或本地仓库 ) , 就是安全存放数据的位置, 有所有的版本的数据. 其中HEAD指向最新放入仓库的版本.
- ==Remote==: 远程仓库, 托管代码的服务器.
HEAD一开始指向的是master分支 (主分支).
在文件中有一个隐藏文件夹
.git
其中有Stage 和 Local Repo文件夹git的工作流程:
- 在工作目录中添加修改文件;
- 将需要进行版本管理的文件放入暂存区域;
git add .
(.表示全部) - 将暂存区域的文件提交到git仓库
Git项目搭建
创建工作目录与常用指令
工作目录 (WorkSpace) 可以是项目的目录, 不要有中文.
本地仓库搭建
一种方法是创建全新的仓库, 另一种是克隆远程仓库.
创建全新的仓库,
git init
执行后在项目目录多出了一个 .git目录, 关于版本的所有信息都在这个目录里面.克隆远程仓库到本地.
git clone ...
然后就行了HTTP 利于匿名访问 适合开源项目 可以方便被别人克隆和读取(但没有push权限, push需验证) SSH 不利于匿名访问 比较适合内部项目 只要配置了SSH公钥即可自由实现clone和push操作 (HTTP 和 SSH协议的对比)
Git文件操作
文件4种状态
- Untracked: 未跟踪,在文件夹中, 但没有加入到git库,
不参与版本控制. 通过
git add
状态变为Staged
. - Unmodify: 文件已经入库, 未修改,
即版本库中的文件快照与文件夹中内容一致, 如果被修改,
状态变为
Modified
. 如果使用git rm
则成为Untracked
文件. - Modified: 文件已修改, 仅仅是修改,
并没有进行其他的操作. 有两个去处,
通过
git add
可进入暂存Stage
状态, 使用git checkout
则丢弃修改, 返回到unmodify
状态, 这个git checkout
即从库中取出文件, 覆盖当前修改. - Staged: 暂存状态,
执行
git commit
则将修改同步到库中, 这时库中的文件和本地文件又变为一致, 文件为Unmodify
状态. 执行git reset HEAD filename
取消暂存, 文件状态为Modified
.
- Untracked: 未跟踪,在文件夹中, 但没有加入到git库,
不参与版本控制. 通过
git文件操作命令:
1
2
3
4
5
6
7git init
git status # Untracked
git add .
git status # to be commit (在*暂存区*里)
git commit -m "commit message" # 提交到本地仓库
git status
git push忽略文件
在主目录下建立".gitignore"文件, 此文件有如下规则:
- 忽略文件中的空行和以井号 ( # ) 开始的行.
- 可以使用Linux通配符. 例如 : 星号( * )代表任意多个字符, 问号( ? )代表一个字符, 方括号 ( [...] ) 代表可选字符范围, 大括号( {...} )代表可选的字符串等.
- 如果名称的最前面有一个感叹号 ( ! ) , 表示例外规则, 将不被忽略.
- 如果名称的最前面是一个路径分隔符 ( / ) , 表示要忽略的的文件在此目录下, 而子目录中的文件不忽略.
- 如果名称的最后面是一个路径分隔符 ( / ) , 表示要忽略的是此目录下该名称的子目录, 而非文件 (默认文件或目录都忽略).
1
2
3
4
5*.txt # 忽略所有.txt结尾的文件
!lib.txt # 但lib.txt除外
/temp # 仅忽略项目目录下的TODO文件, 不包括其它目录temp
build/ # 忽略build/目录下的所有文件
doc/*.txt # (忽略某个文件夹下的文件)会忽略 doc/notes.txt 但不包括 doc/server/arch.txtGit分支
- master: 主分支
- dev: 开发(develop)用
- v4.2: 不同版本的分支
git中常用的分支命令:
1
2
3
4
5
6
7
8
9
10git branch # 列出所有本地分支
git branch -r # 列出所有远程分支
git branch [name] # 新建一个分支, 但依然停留在当前分支
git checkout -b [branch] # 新建一个分支, 并切换到该分支
git merge [branch] # 合并分支到当前分支
git branch -d [name] # 删除分支
# 删除远程分支
git push origin --delete [name]
git branch -dr [remote / branch]如果多个分支冲突了, 只要协商即可.
git commit --allow-empty
允许无修改
Sp Lesson-2 Vim
u
撤销上一步操作ctrl v
块操作强制退出
:q!
查找命令:
:/
正向查找:?
反向查找n
前向查找N
反向查找快速查找 : 1) 将光标移动到目标单词上 2) 按下
*
启动正向查找#
反向查找gg
: 回到顶部G
: 回到底部$
: 跳转行末0
: 行首Ctrl+f
:向文件尾翻一屏Ctrl+b
:向文件首部翻一屏Ctrl+d
:向文件尾部翻半屏Ctrl+u
:向文件首部翻半屏dw
从光标当前的位置开始删除,直到删到单词最后。daw
算是1的属性扩充版,这个命令可以直接删除光标所在的一个单词。为了方便记忆,可以记忆为delete a word缩写。bdw
这也是一个复合命令。b可以让光标回退到单词开头的位置,而dw
则是第1个描述过的命令。ciw
修改一个单词,change in word的缩写。Shift d
从光标处删除到行末尾.
vs
分栏e 文件名
切换到文件重命名某一个字符串
%s/原字符串/新的字符串/g
Sp Lesson-3 命令行
cat /hello.txt
查看文件 ( 将文件内容输出至终端 ) (catch 抓来看看)date
获取当前的日期和时间pwd
获取当前的路径 (print work directory)Linux下有一个整体的根目录. (从UNIX过来) Windows没有 (从DOS过来)
.
表示当前目录,./home
..
表示上一级目录echo
: 回声man ls
: man即manual, 手册which ls
: 查看命令的位置echo hello > hello.txt
输出重定向到hello.txt
里time
对于任何一个命令都可以使用time
来查看运行时间.time ./a.out
来查看可执行文件的运行时间timeout
倒计时 .. 秒, 若进程没有终止则强行停止. (可用于检测程序性能, 强行终止死循环等)find
寻找文件, 如find . | grep \.cpp$
grep
命令用于查找文件里符合条件的字符串或正则表达式wc
统计行数, 加-l
只输出行数.find . | grep \.c$ | xargs cat | wc -l
利用管道协作, 统计所有c语言代码行数 (xargs 的功能, 将输入转换成后面命令的参数)使用
... | vim -
, 用vim打开前面命令的输出.什么是管道命令?
命令1 | 命令2
命令1的标准输出是命令2的标准输入. (以前面命令的输出作为后面一个命令的输入, 以此类推 )(注: 管道命令后面接的命令必须能够接收输入的命令, 不能接不能接收输入的命令, 比如ls cp mv等)
命令行中, 若要使用括号, 需要写作
$( ... )
左括号前加$Linux命令行编程:
1
2
3
4for i in $(seq 1 100) # 所谓 $(seq 1 100) 本质上就是文本, 即 1 2 3 4 5 ... 100
do
echo $i
done(命令行的 2 个关键词: 文本, 工具)
编写 .sh 文件 (shell 脚本) 来帮你对拍/ 调试程序.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27!/bin/bash
gcc bad.c -o bad.exe
gcc ok.c -o ok.exe
for i in $(seq 1 5)
do
echo ======= Test case $i ========
cp hello${i}.in hello.in
./a.out
cat hello.out
done
for T in $(seq 1 100)
do
echo Testcase $T
./gen # 生成输入
echo "100 + 200" > in.txt
diff -q \
<(timeout 1 ./ok.exe < in.txt) \
<(timeout 1 ./bad.exe < in.txt)
if [[ $? != 0]]
then
echo 'Error!'
fi
done创建
run.sh
文件, 编写文件内容为:1
2
3
4
5for i in $(seq 1 3)
do
./a.out < in$i.txt > out$i.txt
diff ans$i.txt out$i.txt || echo "Error on Testcase #$i"
done这样就实现了一个对拍脚本.
1
2:: 在终端中运行脚本
bash run.sh
Sp Lesson-4 gdb
GDB
:1
2gcc -g ...
gdb ...start
next (n) 5
: 步过5行step (s)
: 步入continue (c)
: ==跳转至下一断点所在行== (from wherever we currently are)finish
: tell gdb to finish this current function call and then stop once we finish the callbt
: back trace 查看若干级函数调用的历史记录和参数值list
: 看代码b codes/main.c: 127
: 设置断点( 另可在函数上打断点,
b main
)info b (i b)
: 查看断点信息run (r)
: 从断点处或从头开始执行 (运行程序)print i (p i)
print &i
: 查看 i 的地址值set logging on
: 将gdb输出结果打印至日志文件中.watch
: 设置watchpoint以实时观察一个变量是否变化, 一旦变化, 就会输出变化信息. (查看设置了哪些watchpointinfo breakpoints(watchpoints)
) (用于避免反复p
, 一般watch的是核心变量)delete ID
: 删除断点 (delete 删除所有断点)display i
: 每次n
都打印出i的值.undisplay ID
: 停止跟踪ID下标的那个变量, 一般第一个display的下标为1up
: 返回上一级函数 (找到是谁在调用当前行)backtrace
: 不再是一次一次地up , 而是直接全部 print the entire call stack , 看到所有的涉事指针作为参数传递时的函数调用栈. (进行 N 次up
)fin
: 执行完当前函数, 返回到上一调用层, 并打印返回值whatis j (what j)
: 可以得知 j 的数据类型gdb
高级target record-full
: 记录所有东西rn
(reversed-next) : 回退一步 (rs (reversed-step) rc (reversed-continue)
)set var x=15
: 改变x的值但是不退出gdb运行.