C

参考文档

环境配置

需要GCC,可以使用MinGW-w64,并配置/bin目录到环境变量。
编译文件使用命令:

1
2
gcc main.c
gcc main.c -o main.out

注意:在VC与GCC上,对于long型有关的变量,其长度定义的不一致。

版本历史

C语言有:C89(90),C99,C11。

C99

新增关键字:_Bool, _Complex, _Imaginary, inline, restrict
复合字面量:初始化结构的时候允许对特定的元素赋值,struct test{int a, b, c, d;} foo = { .a = 1, .c = 3, 4, .b = 5 };
格式化字符串中,利用 \u 支持 unicode 的字符。
修改了 /% 处理负数时的定义,这样可以给出明确的结果,例如在C89中-22 / 7 = -3, -22% 7 = -1,也可以-22 / 7= -4, -22% 7 = 6。 而C99中明确为 -22 / 7 = -3, -22% 7 = -1,只有一种结果。

C11

新增关键字:_Alignas, _Alignof, _Atomic, _Generic, _Noreturn, _Static_assert, _Thread_local

alignof(T)返回T的对齐方式,aligned_alloc()以指定字节和对齐方式分配内存,头文件<stdalign.h>定义了这些内容。
fopen()增加了新的创建、打开模式x,在文件锁中比较常用。
quick_exit(),又一种终止程序的方式,当exit()失败时用以终止程序。
多线程:头文件<threads.h>定义了创建和管理线程的函数,新的存储类修饰符_Thread_local限定了变量不能在多线程之间共享。
_Atomic类型修饰符和头文件<stdatomic.h>
改进的Unicode支持和头文件<uchar.h>

常量

前缀:0x表示十六进制,0表示八进制,默认是十进制。
后缀:u表示无符号,l表示长整数。
size_t:64位中表示long long unsigned int,非64位中为long unsigned int。size_t大于等于地址线宽度。是sizeof的返回类型,就是表示一个size。
ptrdiff_t:用来保存两个指针减法操作的结果。

字符串常用函数

1
2
3
4
5
6
strcpy(s1, s2);  // 复制s2到s1
strcat(s1, s2); // 连接s2到s1末端
strlen(s); // s长度
strcmp(s1, s2); // 比较
strchr(s, ch); // 查找ch位置
strstr(s1, s2); // 查找子串s2位置

结构体

结构体在函数传参中可以值传递,也可以地址传递。值传递时,不会改变原有的结构体,也就是调用时,结构体被复制了一份。

位域

1
2
3
4
5
6
struct bs{
unsigned a:4;
unsigned :4; /* 空域 */
unsigned b:4; /* 从下一单元开始存放 */
unsigned c:4
}

标准输入输出

1
2
3
// 返回值表示读取成功的变量数,为EOF表示读取结束
int scanf(const char *format, ...);
int printf(const char *format, ...);

占位修饰符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"%+" 显示正负
"%-" 左对齐
"% " 显示负或空
"%#" 显示进制格式,科学计数格式
"%0" 0填充前导数值
"%4" "%*" 字宽最小值,*表示从参数中获取
"%5.2f" 保留几位小数和整数
"%6.4hd" short int / unsigned short int, hhd 表示 char/uchar
"%j" int uint
"%l" long
"%ll" long long
"%L" long double
"%t" 表示 ptrdiff_t
"%z" 表示 size_t

文件读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FILE *fopen( const char * filename, const char * mode );
int fclose( FILE *fp ); // 成功返回0,错误返回EOF
// 读取一个字符,错误返回EOF
int fgetc( FILE * fp );
// 读取n-1个字符到buf,并在buf末尾追加null
char *fgets( char *buf, int n, FILE *fp );
int fscanf(FILE *fp, const char *format, ...);
int fputc( int c, FILE *fp );
int fputs( const char *s, FILE *fp );
int fprintf(FILE *fp,const char *format, ...);
// 二进制I/O函数,用于读写存储块
size_t fread(void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);

size_t fwrite(const void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);


int feof ( FILE * fp ); // 判断是否到结尾
int ferror ( FILE *fp ); // 判断是否出错
void rewind ( FILE *fp );// 移动指针到开头
int fseek ( FILE *fp, long offset, int origin ); // 移动指针到某个位置
int ftell ( FILE * fp ); // 文件长度

FILE结构体为

1
2
3
4
5
6
7
8
// 定义FILE结构体
typedef struct _iobuf {
int cnt; // 剩余的字符,如果是输入缓冲区,那么就表示缓冲区中还有多少个字符未被读取
char *ptr; // 下一个要被读取的字符的地址
char *base; // 缓冲区基地址
int flag; // 读写状态标志位
int fd; // 文件描述符
} FILE;

预定义宏

__DATE__ 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
__TIME__ 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__STDC__ 当编译器以 ANSI 标准编译时,则定义为 1。
字符串常量化运算符(#):转化常量为字符串
标记粘贴运算符(##):拼接合并变量
#pragma:使用标准化方法,向编译器发布特殊的命令到编译器中

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
#include <errno.h>
extern int errno; // 保存错误信息
perror("An error occered") // 返回传入的字符串,并追加一个冒号和空格
strerror(errno) // 返回一个指针,指向errno代表的文本
stderr // 用来输入错误信息的文件流
// 例如
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror(errno));
// 退出
exit(EXIT_SUCCESS); // 正常退出
exit(EXIT_FAILURE); // 不正常退出

可变参数

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdarg.h>
int func(int, ... ){
va_list valist;
/* 为 num 个参数初始化 valist */
va_start(valist, num);
/* 访问所有赋给 valist 的参数 */
for (i = 0; i < num; i++){
sum += va_arg(valist, int);
}
/* 清理为 valist 保留的内存 */
va_end(valist);
}

内存管理

1
2
3
4
void *calloc(int num, int size);  // 在堆区动态分配 num 个长度为 size 的连续空间 + 初始化为 0
void free(void *address);
void *malloc(int num); // 在堆区分配,不会初始化
void *realloc(void *address, int newsize); // 重新分配

命令行参数

1
2
// argv 第0个 文件名,第1个,首个参数
int main( int argc, char *argv[] )

Linux 下我们可使用 getopt 和 getopt_long 来对命令行参数进行解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, char *argv[]){
char *optstr = "p:n:m:c:";
struct option opts[] = {
{"path", 1, NULL, 'p'},
{"name", 1, NULL, 'n'},
{"mtime", 1, NULL, 'm'},
{"ctime", 1, NULL, 'c'},
{0, 0, 0, 0},
};
int opt;
while((opt = getopt_long(argc, argv, optstr, opts, NULL)) != -1){
switch(opt) {
case 'p': // ...
}
}
findInDir(path);
return 0;
}

虚内存模型

Linux:
由低地址到高地址分别为:保留区,代码区,常量区,全局变量区,堆(以及未分配内存),动态链接库,(未分配内存以及)栈(向低地址增长)内核空间。

Windows

多线程

参考文档

在头文件 threads.h 中,定义和声明了支持多线程的宏、类型和函数。所有直接与线程相关的标识符,均以前缀 thrd_ 作为开头。

线程操作:

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
28
// 创建并开始执行一个新线程
// thr: thrd_t 类型
// func: 例如 int parallel_sum(void *arg)
// arg: 指向参数的指针
// 返回值: 是否创建成功 thrd_success
int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);
// 阻塞等待某线程
// result: 线程返回值
int thrd_join(thrd_t thr, int *result);
// 当线程 thr 执行完成后,自动释放线程占用的所有资源
int thrd_detach(thrd_t thr);
// 返回其所在线程的线程标识。
thrd_t thrd_current(void);
// 两个线程标识符 thr0、thr1 分别引用了两个不同线程时,返回 0。
int thrd_equal(thrd_t thr0, thrd_t thr1);
// 使得正在调用的线程等待一段时间。仅当该函数收到唤醒的信号时,它才提前返回。
// duration: 等待时间
// remaining: 剩余时间
int thrd_sleep(const struct timespec*duration, struct timespec*remaining);
// 尝试中断当前调用的线程,并将 CPU 时间分给另一个线程。
void thrd_yield(void);
// 以 result 作为结果值结束正在调用线程。
_Noreturn void thrd_exit(int result);

struct timespec{
time_t tv_sec; // 秒≥0
long tv_nsec; // 0 ≤纳秒≤999 999 999
}

当每个线程为各自的变量使用全局标识符时,为保留这些变量各自的数据,可以采用线程对象和线程存储。

线程对象(全局或静态对象):每一个线程拥有属于自己的线程对象实例(不共享)

1
thread_local int var = 10;

线程存储:线程内部使用。
通过初始创建一个全局的键(key)表示一个指向线程存储的指针。

1
2
3
4
5
6
7
8
9
10
11
12
// 创建Key
// dtor: 例如 void destructor(void *data); 用作析构函数
int tss_create(tss_t *key, tss_dtor_t dtor);
// 删除Key
void tss_delete(tss_t key);
// 设置key所表示的tss指针为val引用的内存地址
// val: 例如 malloc(size)
// 返回thrd_error或者thrd_success
int tss_set(tss_t key, void*val);
// 返回内存块指针
// 返回thrd_error或者thrd_success
void* tss_get(tss_t key);

线程间通信:使用条件变量,以等待来自另一个线程的通知。互斥要自行实现。

1
2
3
4
5
6
7
8
9
10
11
12
// 创建条件变量
int cnd_init(cnd_t *cond);
// 释放资源
void cnd_destroy(cnd_t *cond);
// 在等待条件变量的线程中唤醒一个
int cnd_signal(cnd_t *cond);
// 唤醒所有等待指定条件变量的线程
int cnd_broadcast(cnd_t *cond);
// 阻塞正在调用的线程,并释放指定的互斥。
int cnd_wait(cnd_t *cond, mtx_t *mtx);
// 阻塞正在调用的线程,但仅维持由参数 ts 指定的时间
int cnd_timedwait(cnd_t *restrict cond,mtx_t *restrict mtx,const struct timespec *restrict ts);

互斥操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化互斥锁
// mutextype:
// - mtx_plain 既不支持超时也不支持递归
// - mtx_timed 支持超时
// - mtx_plain | mtx_recursive
// - mtx_timed | mtx_recursive
int mtx_init(mtx_t *mtx, int mutextype);
// 释放资源
void mtx_destroy(mtx_t *mtx);
// 上锁,阻塞当前进程
int mtx_lock(mtx_t *mtx);
// 解锁
int mtx_unlock(mtx_t *mtx);

原子对象与原子操作:stdatomic.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明一个原子对象,数组和函数类型不能为原子类型。
// 原子对象的初始化不是一个原子操作。
_Atomic long counter = ATOMIC_VAR_INIT(0L);
_Atomic long counter = ATOMIC_INIT(0L);

// 原子操作
atomic_store();
atomic_exchange();
atomic_compare_exchange_strong();

// 开始和结束操作原子对象
atomic_flag_test_and_set();
atomic_flag_clear();

// 判断原子对象是否无锁(0有锁,1无锁,2始终无锁)
_Bool atomic_is_lock_free(const volatile A *obj);

内存次序:使用原子对象可以默认地防止此类重新排序。但是在较低的内存次序请求下,通过明确地使用原子操作提高性能。

每个原子操作的函数都有一个以 _explicit 结尾版本。如atomic_store_explicit()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum memory_order {
// 编译器可以自由地改变操作的顺序
memory_order_relaxed,
// 原子对象释放操作
memory_order_release,
// 原子对象捕获操作
memory_order_acquire,
// 消耗操作
memory_order_consume,
// 同时具有捕获和释放操作
memory_order_acq_rel,
// 顺序一致性
memory_order_seq_cst,
}

// 例如
atomic_store_explicit(&aptr, (intptr_t)&data,memory_order_release);
dp = (struct Data*)atomic_load_explicit(&aptr,
memory_order_acquire);

内存屏障(栅栏):对于一个原子操作的内存次序请求,也可以通过一个原子操作单独指定。栅栏允许更大程度的内存顺序优化。
若参数值为 memory_order_release,函数 atomic_thread_fence()建立一个释放栅栏(releas fence)。在这种情况下,原子写操作必须在释放栅栏之后发生。
若参数值为 memory_order_acquire 或 memory_order_consume,函数 atomic_thread_fence()建立一个捕获栅栏(acquire fence)。在这种情况下,原子读操作必须在捕获栅栏之前发生。

1
void atomic_thread_fence(memory_order order);

算法

1
2
3
4
5
6
7
8
9
// C 内置排序算法 stdlib.h 中
qsort(arr.data(), ARRAY_SIZE,sizeof(int), comp_function [a>b +, a<b -, a=b 0])
// C 内置二分搜索 stdlib.h 中,返回指向元素的指针
void *p = bsearch(&target, arr.data(), ARRAY_SIZE, sizeof(int), comp_function);

// C 内置计时器 time.h
clock_t clk = clock(); // 返回当前时间,毫秒
// 格式化输出到内存
snprintf(buf, 10, "%d", x);

参考库

assert.h: assert(ignore) ((void)0) 宏定义
ctype.h: 测试数据类型
errno.h: C 标准库中的特定函数修改它的值为一些非零值以表示某些类型的错误
float.h: 包含了一组与浮点值相关的依赖于平台的常量
limits.h: 决定了各种变量类型的各种属性
locale.h: 定义了特定地域的设置,比如日期格式和货币符号
math.h: 各种数学函数
setjmp.h: 定义了宏 setjmp()、函数 longjmp() 和变量类型 jmp_buf,该变量类型会绕过正常的函数调用和返回规则
signal.h: 定义了一个变量类型 sig_atomic_t、两个函数调用和一些宏来处理程序执行期间报告的不同信号
stdarg.h: 定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数可变时获取函数中的参数
stddef.h: 定义了各种变量类型和宏ptrdiff_t, size_t, wchar_t, NULL, offsetof
stdlib.h: size_t wchar_t NULL atof atoi atol strtod strtol等, malloc free, abort, exit, qsort, bsearch, system, abs, rand …
string.h: memcpy memmove memcmp memchr 以及字符串相关
time.h: struct tm, asctime, time, …