更新:因为开始写PA2了,因此决定重回这个坑,顺便记录一下解决一些C问题的方法,不一定和语法相关
现在才知道 oi 用的是 C-with-STL... 感觉自己对这个看起来很单纯的语言仍然不够了解
打算仔仔细细地写一写自己不太会的东西,包括与 C++ 不同的语法、宏(macro)的技巧、指针的技巧等等
语法
enum
可以利用enum
的特点来引入最大下标开数组
enum {PC,R1,R2,...,R7,R_SIZE};
那么这里的R_SIZE
天然就是最大下标
数组初始化
简单的就不说了。通常会有这样的情形:我们需要一个lookup-table,并且希望这是const的
dom的范围很大,但是实际上有用的lookup项很少(比如说为每个二元运算分配一个优先级)
那么就可以这么写
enum TOKENS {
// blah blah blah....
} ;
// priority for _binary operators_
static const int priority[512] = {
[TK_OR] = 4,
[TK_AND] = 5,
[TK_NEQ] = 9,
[TK_EQ] = 9,
[TK_LEQ] = 10,
[TK_GEQ] = 10,
[TK_LT] = 10,
[TK_GT] = 10,
['+'] = 12,
['-'] = 12,
['*'] = 13,
['/'] = 13,
['%'] = 13,
};
struct
struct里面不能有函数(好严格..)
C语言中的结构体有两种定义方式
struct MyStruct {
int tmp;
} ;
struct MyStruct a;
或者可以
typedef struct tmpMyStruct {
tmpMyStruct *p;
int tmp;
} MyStruct;
MyStruct a;
这两者的区别在于下面的方法把struct
定义为一个类型,并且引入了一个中间名字tmpStruct
,这使得我们可以在结构体中定义结构体变量(考虑一个指针节点的组成)
malloc
/free
C++用的更多的是new
和delete
,C里面用的是malloc
和free
假设有这么一个
typedef struct tmpNode {
struct tmpNode *next;
int value;
} Node;
Node *head;
我们要给head
指针分配内存空间,用的就是
head = (Node*) malloc(sizeof(Node));
这个可以类比给Node
类new
了一个对象,不带初始化函数
这里分配的内存不包括结构体内部指针指向的地址的内存,这个还需要在自己写的初始化函数里再malloc
一次
一个我常用的小技巧是把初始化函数自己实现一遍,返回一个对象的指针
类似的,在free
的时候需要自己实现对象的析构:比如说free
一棵红黑树,比如说free
一个链表。通常递归free
掉所有节点就够了
const
数组下标
C语言中const修饰的int变量不能定义数组大小,因为这是一个只读变量而不是常量,通常我们会用#define
bool
C里面的bool是在C99以后才有的,如果要用true和false需要引用stdbool.h,或者自己define
逗号表达式
逗号的优先级最低,一段逗号表达式按顺序从左到右求值,一整段逗号表达式的值由最右边的式子决定
比如说(a = 3, a *= 2)
的值就是6
static
静态变量
分为全局静态变量和局部静态变量。静态变量放在.bss
或.data
字段中
全局静态变量在链接的时候不能被其他.o
文件引用,可以看成是一种private
封装
局部静态变量不在栈上,作用域和局部变量一致,生命周期却和全局变量一致
volatile
关键字
这个是看CSAPP学到的姿势
对于多线程共同访问一个全局变量的情况,可以使用volatile
来告诉编译器对于该变量只从内存中取值
这可以防止编译器过度优化使得无锁并行的程序出错
macro
未定义macro
的初值
考虑如下代码
#include <stdio.h>
int main(void) {
#if aa==bb
puts("YES");
#else
puts("NO");
#endif
return 0;
}
输出是YES
原因在于未定义的macro
默认值都是0
同时 #if
后跟着的表达式必须是常亮表达式。很好理解,因为是编译期行为
#ifndef
在多文件编程的时候我们会include若干.h文件。.h文件的include原理就是复制粘贴,因此如果多次include会出现奇奇怪怪的错
所以我们需要在.h前后加上
#ifndef _SOMETHINGSPECIAL_
#define _SOMETHINGSPECIAL_
// do something ...
#endif
其中_SOMETHINGSPECIAL_
是不同头文件不同的一个变量名,这个东西的意思是如果没有定义过这个宏就执行内部内容,否则就跳过。而内部代码定义了这样一个宏,就保证了只会被执行一次(即使被include
多次)
stringify和concatenation
假设要实现float
和int
的最大值函数
#define CONCAT_TMP(X, Y) X ## Y
#define CONCAT(X, Y) CONCAT_TMP(X, Y)
#define DEF_MAX
int CONCAT(max_, int)(int x, int y) { return x > y ? x : y;}
float CONCAT(max_, float)(int x,int y) { return x > y ? x : y;}
多行macro
&&任意参数
很多时候一行宏定义不够用,于是我们就可以通过在行末加''符号定义多行macro
如果出现了参数不明确但是操作一致的情况,我们还可以在定义的时候采用...作为形参,使用的时候用__VA_ARGS__
这个宏
#define IS_DEBUG true
#define DEBUG(...) {
if (IS_DEBUG)
printf(__VA_ARGS__);
}
while(0)
一个很怪的用法,大概可以写成下面这样:
#define CHECK(EXP) do {
if (EXP) printf("WARNING! " #EXP " CHECK FAILED!
"); } while (0)
这个宏函数会检查一个表达式EXP
,但是这里把主体语句用一个do while(0)
括起来了
好处大概有这么几个:
- 可以包裹多条语句,在展开之后可以保证这些语句在一个大括号内,这样可以保证操作对于
if then else
的整体性 - (新增)这样做的话宏的内部相对独立,就可以随便开临时变量了
X-macro
这个用于解决相关联的表项数据分布于不同文件时,如何方便修改的问题
一个比较烂的的例子是抄来的:
enum color {RED,GREEN,BLUE};
char *str[] = {"RED","GREEN","BLUE"};
实际代码中可能不止两处,可能相隔很远
现在如果要在红色和绿色之间加入黄色,那么所有硬编码的地方都需要修改
所谓 X macro就是这样一类解决多处硬编码的修改问题
#define COLOR(X)
X("RED",RED)
X("BLUE",BLUE)
X("GREEN",GREEN)
#define X(a,b) b,
enum color {COLOR(X)};
#undef X
#define X(a,b) a,
char *str[] = {COLOR(X)};
#undef X
怎么说呢,有点像call back function的感觉
指针
特殊指针
NULL
指针在C中的定义是(void *)0
常量指针const int *p
或int const *p
:指向常量的指针(a pointer that points to a const)
指针常量int *const p
:一个自己是常量的指针(a const pointer)
上面两类还可以复合/套娃....类比就行
指针变量的阅读
具体可以看这篇文章
链接
符号表
这里的符号表和编译原理的符号表又不太一样
编译的时候可以多个编译单元编译成.o
文件,最后合并成一个可执行文件(elf),这个合并的过程就是链接
在链接的时候,需要维护一些跨编译单元的1. 函数调用2. 全局变量引用,符号表就是用来给链接器提供这个信息的
根据这个角度,就可以知道为什么局部变量和宏不会出现在符号表中了,因为它们在链接时1. 不会被引用和修改 2. 已经被展开了
static
和inline
inline
表示建议编译器内联这段函数,但仅含有inline
的函数定义不是一个函数声明,因此不会出现在符号表中
在GCC开启-O0级别优化的时候,就会出错
所以有两种方法解决:
- 在
inline
定义后加一个函数声明 - 在
inline
关键字前加static
关键字
而对于仅含有static
的函数,如果它未被调用,则开启-Wall
和-Werror
的时候就会报Function defined but not used错,这是因为static
函数只可能被当前.c
文件调用,而查手册可以发现-Wunused-function
恰好会指出这种情况。解决的方法可以是用static inline
代替static
,或者删掉这个函数定义和声明
杂项
编译器选项
感觉-Wall
那个应该放这里的
64位下,对于开启-O1
及以上优化级别的程序,截止当前版本的GCC默认会省略%rbp
寄存器的保存(压栈、记录栈顶),这样既可以变快,又可以多一个通用寄存器。在做ICS Labs的时候要写一个自己的setjmp()
和longjmp()
,然后我就卡在了获得ret
地址这一步,原因就是这个东西破坏了调用规定,导致栈的行为不确定了....
解决方法很简单,直接自己写一整个的函数,而不是在函数内部内联汇编,这样callee就是自己维护的了,想怎么做就怎么做