C语言学习笔记

更新:因为开始写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++用的更多的是newdelete,C里面用的是mallocfree
假设有这么一个

typedef struct tmpNode {
      struct tmpNode *next;
      int value;
} Node;

Node *head;

我们要给head指针分配内存空间,用的就是

head = (Node*) malloc(sizeof(Node));

这个可以类比给Nodenew了一个对象,不带初始化函数
这里分配的内存不包括结构体内部指针指向的地址的内存,这个还需要在自己写的初始化函数里再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

假设要实现floatint的最大值函数

#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!\n"); } while (0)

这个宏函数会检查一个表达式EXP,但是这里把主体语句用一个do while(0)括起来了
好处大概有这么几个:

  1. 可以包裹多条语句,在展开之后可以保证这些语句在一个大括号内,这样可以保证操作对于if then else的整体性
  2. (新增)这样做的话宏的内部相对独立,就可以随便开临时变量了

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 *pint const *p:指向常量的指针(a pointer that points to a const)

指针常量int *const p:一个自己是常量的指针(a const pointer)

上面两类还可以复合/套娃....类比就行

指针变量的阅读

具体可以看这篇文章

链接

符号表

这里的符号表和编译原理的符号表又不太一样

编译的时候可以多个编译单元编译成.o文件,最后合并成一个可执行文件(elf),这个合并的过程就是链接

在链接的时候,需要维护一些跨编译单元的1. 函数调用2. 全局变量引用,符号表就是用来给链接器提供这个信息的

根据这个角度,就可以知道为什么局部变量和宏不会出现在符号表中了,因为它们在链接时1. 不会被引用和修改 2. 已经被展开了

staticinline

inline表示建议编译器内联这段函数,但仅含有inline的函数定义不是一个函数声明,因此不会出现在符号表中

在GCC开启-O0级别优化的时候,就会出错

所以有两种方法解决:

  1. inline定义后加一个函数声明
  2. 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就是自己维护的了,想怎么做就怎么做

posted @ 2020-12-27 00:08  jjppp  阅读(262)  评论(0编辑  收藏  举报