# 常用库
- ANSI C 标准,制定了一些标准库,提供了一些常用的函数、常量、宏等。
- 一款 C 语言编译器,如果遵守 ANSI C 标准,则必须内置这些标准库,方便用户调用。
- 用户 include 这些标准库的头文件,就可以调用它们。
- 用户也可以下载第三方编写的库,例如:
windows.h
:提供了 Windows 系统的很多 API 。比如Sleep()
函数,用于暂停程序一定毫秒。unistd.h
:提供了类 Unix 系统的很多 API 。比如sleep()
函数,用于暂停程序一定秒。curl/curl.h
:提供了 libcurl 库的一些函数,用于 HTTP、HTTPS、FTP 等网络协议的通信。cJSON.h
:用于处理 JSON 格式的字符串。
# assert.h
- assert.h 提供了 assert() 函数。
- 源代码:
# undef assert # ifdef NDEBUG # define assert(x) ((void)0) # else # define assert(e) ((e) ? (void)0 : _assert(# e, __FILE__, __LINE__)) # endif
- assert() 称为断言,用于检查一个条件表达式是否成立,方便用户进行调试。
- 如果取值为真,则无操作。
- 如果取值为假,则调用 _assert() ,打印报错信息,并调用 abort() 来终止程序。
- 在生产环境,用户可以定义一个名为 NDEBUG 的宏,使 assert() 失效。
- 例:执行该程序,输出如下:
int x = 1; assert(x > 1);
test.c:8: main: Assertion `x > 1' failed. Aborted
# pthread.h
C 语言如何创建多线程?
- 在 Linux 系统上,可调用 pthread.h 库的 API 创建线程。比如 pthread_create() 。
- pthread.h 符合 POSIX 标准,适用于类 Unix 系统。也有兼容 Windows 系统的版本:pthreads-w32 。
- 编译时,需要链接 pthread.h 库,例如:
gcc test.c -o test -l pthread
- 在 Windows 系统上,可调用 windows.h 库的 API 创建线程。比如 CreateProcess() 。
- C11 制定了一个标准的线程库 threads.h ,但有的编译器不支持 C11 。
- 在 Linux 系统上,可调用 pthread.h 库的 API 创建线程。比如 pthread_create() 。
int pthread_create(pthread_t *id, const pthread_attr_t *attr, void *(*) (void *), void *arg);
- 用途:创建一个线程,来运行一个函数。
- 需要传入一个 id 变量,用于记录线程 ID 。
- 需要传入一个 attr 变量,用于配置线程的属性。如果传入 NULL ,则采用默认属性。
- 待运行的函数,应该定义成
void *fun(void *arg)
的格式。 - 需要传入一个 arg 变量 ,作为待运行的函数的实参。如果不需要传参,则传入 NULL 。如果有多个参数,则需要封装成一个结构体。
- 如果线程创建成功,则返回 0 。
- 如果线程创建成功,则返回非零值。
- 用途:创建一个线程,来运行一个函数。
void pthread_exit(void *retval);
- 用途:终止当前线程的运行。
- 需要传入一个变量 retval ,用于存储当前线程的返回值。
- 用途:终止当前线程的运行。
int pthread_cancel(pthread_t id);
- 用途:向指定 id 的那个线程,发送 cancel 请求。
- 目标线程可能立即终止,也可能稍后终止,也可能忽略 cancel 请求。
- 用途:向指定 id 的那个线程,发送 cancel 请求。
int pthread_join(pthread_t id, void **retval);
- 用途:暂停当前线程的运行,等待指定 id 的那个的线程终止。
- 当目标线程终止之后,其返回值会存储在 retval 变量中。
- 用途:暂停当前线程的运行,等待指定 id 的那个的线程终止。
例:
#include <stdio.h> #include <pthread.h> void *fun1(){ printf("fun1() end.\n"); return 0; } typedef struct{ char name[10]; int age; } Horse; void *fun2(void *p){ Horse h = *(Horse *)p; printf("name: %s , age: %d\n", h.name, h.age); printf("fun2() end.\n"); return 0; } int main(){ int rc; pthread_t id; // 创建第一个线程 rc = pthread_create(&id, NULL, fun1, NULL); if (rc) printf("Failed to create the thread fun1().\n"); // 创建第二个线程 Horse horse = {"Jack", 5}; rc = pthread_create(&id, NULL, fun2, &horse); if (rc) printf("Failed to create the thread fun2().\n"); // 阻塞主线程的运行,以免提前终止子线程 pthread_join(id, NULL); printf("main() end.\n"); return 0; }
# stdafx.h
- Visual Studio 2010 中,将
stdio.h
、stdlib.h
、windows.h
等头文件都包含到了stdafx.h
(标准应用程序框架的扩展)中。- 它属于预编译头文件,将一些经常调用的代码预先编译好了。
- 导入该头文件时,需要使用双引号,即
#include "stdafx.h"
。
# stdarg.h
- stdarg.h 用于定义变参函数。
- 变参函数,没有声明具体的形参列表。因此调用该函数时,可以传入类型不定、数量不定的实参。
- 例如
printf()
、scanf()
就属于变参函数。
- 例:定义一个变参函数
#include <stdio.h> #include <stdarg.h> // 导入头文件 stdarg.h void print_int(int argc, ...) // 定义函数时,形参只有一个 argc 参数(表示参数个数),然后是省略号 { va_list ap; // 创建一个 va_list 类型的变量,用于存储参数列表 va_start(ap, argc); // 初始化参数列表,存储 argc 个参数 int i; for(i = 0; i < argc; i++) printf("%d\n", va_arg(ap, int)); // 读取下一个参数,预期为 int 类型 va_end(ap); // 销毁参数列表 } int main(){ print_int(3, 10, 11, 12); // 调用函数,第一个实参为 3 ,表示后面还有 3 个实参 return 0; }
# stddef.h
- stddef.h 定义了一些变量类型、宏。其它标准库头文件,大多导入了这个头文件。
- stddef.h 定义了 size_t 数据类型,通常用于接收 sizeof 的返回值。
- 在 32 位 CPU 上,size_t 被定义为
unsigned int
。 - 在 64 位 CPU 上,size_t 被定义为
unsigned long long
。
- 在 32 位 CPU 上,size_t 被定义为
# stdio.h
- stdio.h 用于处理标准输入和输出(standard input and output),也就是如何在终端输入、输出字符串。
- stdio.h 定义了三个特殊的文件,表示在电脑终端显示的三种内容:
- stdin :标准输入。用户通过键盘在终端输入的字符串,通常会写入 stdin 文件,程序可通过 stdin 读取该字符串。
- stdout :标准输出。程序通过 print() 函数打印的字符串,通常会写入 stdout 文件,然后显示在终端。
- stderr :标准错误。程序的报错信息,通常会写入 stderr 文件,然后显示在终端。
# stdout
以下几个函数,用于输出字符串:
int putchar(int char); // 将一个字符 char 输出到 stdout // 例如:putchar('H'); int puts(const char *str); // 将一个字符串 str 输出到 stdout ,并在末尾加上 \n // 例如:puts("Hello World!"); int printf(const char *format, ...); // 按照 format 格式生成一个字符串,输出到 stdout // 省略号 ... 可以填入任意个变量,用于嵌入任意个值,到 format 字符串中 // 该函数的返回值是一个数字,表示成功输出了几个字符 int fprintf(FILE *stream, const char *format, ...); // 按照 format 格式生成一个字符串,输出到 stream 文件流 int sprintf(char *str, const char *format, ...); // 按照 format 格式生成一个字符串,输出到 str // 如果函数执行成功,则返回输出的字符串长度(不包括末尾的空字符 \0 ) // 如果函数执行失败,则返回一个负数 int snprintf(char *str, size_t n, const char *format, ...); // 与 sprintf 类似,但限制生成的字符串最大长度为 n bytes (包括末尾的空字符 \0 ) // 如果生成的字符串长度大于 n ,则只取前 n-1 个字符,然后加上一个空字符 \0 结尾,输出到 str // 如果函数执行成功,则不管生成的字符串长度是否大于 n ,总是返回输出到 str 的字符串长度
与 sprintf() 函数相比,snprintf() 更安全,可避免输出的字符串,超过 str 容量。
char *buffer = malloc(sizeof(char) * 10); int len = 0; len = snprintf(buffer, 6, "%d.1234", 0); printf("buffer=%s\n", buffer); // 输出为:buffer=0.123 printf("len=%d\n", len); // 输出为:len=6
# format
printf()、scanf() 等函数,函数名都带 f 后缀,表示采用特定格式(format)来处理字符串。
- format 是一个字符串,用于控制格式,它可以包含任意个普通字符、格式控制符。
格式控制符,表示此处存在一个值,并声明其数据类型、显示格式。
- 存在多种格式控制符:
%d // 表示 decimal 十进制数字,对应 int 或 short 数据类型 %i // 在 printf() 中,%d 与 %i 效果相同。在 scanf() 中,%i 能识别八进制前缀 0 、十六进制前缀 0x %u // unsigned int 或 unsigned short %ld // long %lu // unsigned long %lx // long 型数值,但采用十六进制格式 %f // float %lf // double %Lf // long double %c // char 数据类型 %s // 字符串 %e // 按科学计数法显示的值 %o // 八进制格式的值 %#o // 八进制格式的值,并且加上 0 前缀 %x // 十六进制格式的值,并且每个字母小写 %X // 十六进制格式的值,并且每个字母大写 %#x // 十六进制格式的值,并且加上 0x 前缀 %#X // 十六进制格式的值,并且加上 0X 前缀 %p // 指针指向的内存地址,是一个十六进制格式的值,加上 0x 前缀
- 各个格式控制符,都以百分号 % 开头。如果用户想在 format 字符串中包含一个普通的百分号,则需要使用
%%
。 - 例:
#include <stdio.h> int main(){ int x = 10; printf("%d\n", x); // 输出为:10 printf("%f\n", 10.2); // 输出为:10.200000 printf("%d\n", 10.2); // 用 %d 处理浮点数会出错,这里输出为:2147483638 printf("%f\n", 3); // 用 %f 处理整数会出错,这里会输出最近一个浮点数:10.200000 printf("%e\n", 10.2); // 输出为:1.020000e+01 int octal_num = 075; int hex_num = 0x1F; printf("octal_num=%o, hex_num=%X\n", octal_num, hex_num); // 输出为:octal_num=75, hex_num=1F printf("octal_num=%o, hex_num=%X\n", 10, 10); // 输出为:octal_num=12, hex_num=A printf("%d\n", &x); // 输出为:681891340 printf("%x\n", &x); // 输出为:28a4d60c printf("%p\n", &x); // 输出为:0x7ffc28a4d60c return 0; }
- 存在多种格式控制符:
输出一个值时,可以用
%m.n
的格式,使得至少为 m ,使得小数点后的位数为 n 。- m 用于控制输出的最少字符数(包括小数点)。
printf("--%5d--", 1); // 如果实际总位数小于 m ,则在左侧加上空格(因为排版为右对齐),补齐位数 // 输出为:-- 1-- printf("--%05d--", 1); // 可以改用 0 补齐位数 // 输出为:--00001-- printf("--%5s--", "test"); // 同理,可以控制 %s 输出的最少字符数 // 输出为:-- test--
- n 用于控制小数点后的保留位数。
printf("--%5.2f--", 1.0); // 输出为:-- 1.00-- printf("--%05.2f--", 1.0); // 输出为:--01.00-- printf("--%5f--", 1.0); // 如果不指定 n ,则默认小数点后默认保留 6 位 // 输出为:--1.000000-- printf("--%5.2f--", 1.056); // 如果小数点后实际位数大于 n ,则进行 5 舍 6 入,不是四舍五入 // 输出为:-- 1.06--
- 输出负数时,默认包含负号。而输出正数时,默认不包含正号,除非使用
%+
。printf("--%5d--", 1); // 输出为:-- 1-- printf("--%+5d--", 1); // 输出为:-- +1--
- 默认排版为右对齐,如果使用
%-
,则改为左对齐。printf("--%5d--", 1); // 输出为:-- 1-- printf("--%-5d--", 1); // 输出为:--1 --
- m 用于控制输出的最少字符数(包括小数点)。
# stdin
以下几个函数,用于读取字符串:
int getchar(void); // 从 stdin 读取一个字符,作为函数的返回值 // 例如:char c = getchar(); char *gets(char *str); // 从 stdin 读取一个字符串,存储到 str 中 // 例如:char str[10]; gets(str); char *fgets(char *str, int n, FILE *stream); // 从 stream 文件读取一个字符串,存储到 str 中 // 最多读取 n-1 个字符,并在 str 末尾写入一个空字符 int scanf(const char *format, ...); // 从 stdin 读取一个字符串,并按照 format 格式,从中提取值 // 省略号 ... 可以填入任意个变量,用于存储提取的各个值 // 并且,这里不能直接填入变量的名称,而需要填入变量的地址,比如用 & 取地址运算符 // 该函数的返回值是一个数字,表示成功解析了几个值 int sscanf(const char *str, const char *format, ...); // 读取 str 这个字符串,并按照 format 格式,从中提取输入值
例:从 stdin 读取一个值
int x = 0; printf("Please enter: "); scanf("%d", &x); // 假设用户输入 10 ,则 x 会被赋值为 10 // 假设用户输入 10.2 ,则 x 会被赋值为 10 ,因为 %d 只会识别整数 // 假设用户输入 a ,则 x 依然取值为 0 ,因此 %d 只会识别整数 printf("You entered: %d\n", x); scanf("x=%d", &x); // 假设用户输入 x=10 ,则 x 会被赋值为 10 // 假设用户输入 10 ,则 x 依然取值为 0 ,因为输入必须符合 x=%d 格式,才能被解析 scanf("%3d", &x); // 这是只提取数字的前 3 位 // 假设用户输入 123456 ,则 x 会被赋值为 123 scanf("%*d.%d", &x); // 这是只提取小数部分 // %* 表示解析一个值之后,不存储到变量 // 假设用户输入 3.14 ,则 x 会被赋值为 14
例:从 stdin 读取多个值
int a=0, b=0 , c=0; printf("Please enter: "); int ret = scanf("%d %d %d", &a, &b, &c); // 假设用户输入 1 2 3 ,则结果为 ret=3, a=1, b=2, c=3 // 假设用户输入 1 2 3 ,插入多个空字符,则依然能解析成功,结果为 ret=3, a=1, b=2, c=3 // 假设用户输入 1 b ,包含非数字,则从二个变量开始解析失败,结果为 ret=1, a=1, b=0, c=0 printf("ret=%d, a=%d, b=%d, c=%d\n", ret, a, b, c); if(ret != 3) printf("Failed to read all values.\n");
例:从 stdin 读取一个字符串
char str[10]; printf("Please enter: "); scanf("%s", str); printf("You entered: %s\n", str);
使用
%[字符组]s
的格式解析输入字符串时,只会提取在字符组中存在的字符,其它字符则当作定界符。- 例:
scanf("%3[0-9]s", str); // 假设用户输入 123456 ,则结果为 str=123 // 假设用户输入 12a3 ,则结果为 str=12
- 用
a-z
、0-9
的格式可匹配一组 ASCII 编码连续的字符。 - 如果想直接匹配负号
-
,则必须将它放在字符组的末尾。 - 使用
%[^字符组]
的格式读取输入时,只会提取在字符组中不存在的字符。 - 当 scanf() 读取到定界符时,会停止解析当前这个值,然后解析下一个值。
- scanf() 中,默认的定界符是空字符。gets() 中,默认的定界符是换行符。
- 假设用户用键盘输入
Hello World!
然后按下 Enter 键。用scanf("%s", str);
会解析得到Hello
,用gets(str);
则会解析得到Hello World!
。
- 例:
# stderr
大部分情况下,用户只会用到 stdin、stdout 。如果需要使用 stderr ,则可通过以下方式,输出字符串作为报错:
fprintf(stderr, "test\n"); fprintf(stderr, "%s\n", "test");
errno.h
定义了一个全局变量 errno ,用于记录 C 语言程序,最近调用的一个函数的报错信息。- 如果调用一个函数出错,则通常返回值为负数,并且给 errno 赋值一个数字,作为错误代码。
- 如果调用一个函数成功,则通常返回值为 0 ,并且给 errno 赋值为 0 。
- 例:
#include <stdio.h> #include <errno.h> int main(){ printf("errno=%d\n", errno); // 此时没有函数出错,因此输出为: errno=0 return 0; }
- POSIX 定义了一些错误代码,及其描述。
- 例如 errno=1 对应的错误描述为 Operation not permitted 。
- 例如 errno=2 对应的错误描述为 No such file or directory 。
stdio.h
提供了perror()
函数,用于查找 errno 对应的错误描述,打印到 stderr 。- 例:
#include <stdio.h> int main(){ perror("result"); // 此时没有函数出错,因此输出为: result: Success return 0; }
- 例:
# 读写文件
FILE *fopen(const char *filename, const char *mode);
- 用途:打开磁盘中一个文件。
- 如果函数执行成功,则返回一个
FILE *
类型的文件指针 ,代表一个文件流,用户可以从中读、写数据。 - 如果函数执行失败,则返回 NULL 。
- 如果函数执行成功,则返回一个
- 打开文件时,有多种 mode 模式:
r # 只读模式。文件指针指向文件开头,如果文件不存在则报错 w # 只写模式。文件指针指向文件开头(因此写入时会覆盖原数据),如果文件不存在则自动创建它 a # 追加(只写)模式。文件指针指向文件末尾,如果文件不存在则报错 r+ # r+w 模式。如果文件不存在则报错 w+ # w+r 模式。如果文件不存在则自动创建它 a+ # a+r 模式。如果文件不存在则报错
- 以上模式,都会将文件中每个字节的数据,当作 ASCII 字符处理,称为字符类型的数据流,简称为字符流。
- 如果在模式中加入
b
字母,则会将每个字节的数据,当作二进制值处理,称为二进制流。比如 "rb"、"wb"、"ab+"
- FILE 结构体的定义如下:
typedef struct{ int fd; // 记录文件描述符 unsigned char *buf; // 每打开一个文件,会创建一个缓冲区。这样每次写入数据到文件时,先将数据放到缓冲区,然后批量写入磁盘,从而减少写磁盘的次数 size_t size; // 记录该文件的体积,单位为 bytes size_t pos; // 记录当前读写的位置,是文件中第几个字节 } FILE;
- 使用 r 模式打开文件时,指针 FILE.pos 最初指向文件中的第 0 个字节。
- 每读取一个 char 字符,指针 FILE.pos 就会向后移动 1 个字节。
- 移动到文件末尾时,会读取到文件结束符 EOF (宏定义为 -1 )。
- 用途:打开磁盘中一个文件。
int fseek(FILE *stream, long int offset, int whence);
- 用途:修改 FILE.pos ,赋值为 whence + offset 。
- offset 表示字节偏移量。
- whence 表示定位的起点,可以取值为:
#define SEEK_SET 0 // 表示文件开头 #define SEEK_CUR 1 // 表示当前位置 #define SEEK_END 2 // 表示文件末尾
- 例:
fseek(f, 5L, SEEK_SET);
- 用途:修改 FILE.pos ,赋值为 whence + offset 。
long ftell(FILE *stream);
- 用途:返回 FILE.pos 的值。
- 如果函数执行失败,则返回 -1L 。
- 例:
long pos = ftell(f); if(pos == -1L) return -1;
- 用途:返回 FILE.pos 的值。
例:读取文件
// 打开文件 FILE *f = fopen("/tmp/1.txt", "r"); // 检查文件是否成功打开 if(f == NULL){ printf("Failed to open the file.\n"); return -1; } int c; while (c != EOF){ c = fgetc(f); // fgetc() 函数用于从文件读取一个 char 字符 putchar(c); // 将一个 char 字符打印到终端 } // 读写完一个文件之后,应该调用 fclose() 函数关闭该文件,从而释放文件描述符,允许其它进程修改该文件。 fclose(f);
例:使用 fprintf() 和 fscanf() 读写字符流
f = fopen(filename, "w"); int x = 1, y = 2; fprintf(f, "%d%d", x, y); fclose(f); f = fopen(filename, "r"); // 关闭文件,再以读取模式打开文件,从而让文件指针重新指向第 0 个字节 fscanf(f, "%d%d", &x, &y); fclose(f);
以下函数,用于读写二进制流
size_t fread(void *buffer, size_t size, size_t count, FILE *stream); // 从文件流 stream 读取二进制数据,每次读取 size 个字节,读取 count 次,写入到 buffer 中 // 如果函数运行成功,则返回实际的 count size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream); // 从 buffer 读取二进制数据,每次读取 size 个字节,读取 count 次,写入到文件流 stream 中 // 如果函数运行成功,则返回实际的 count
例:
#define COUNT 3 typedef struct{ char name[10]; int age; } Horse; Horse array[COUNT] = {{"AA", 1}, {"BB", 2}, {"CC", 3}}; FILE *f = NULL; char *filename = "/tmp/1.txt"; // 写入 f = fopen(filename, "wb"); if (f == NULL) { printf("Failed to open the file.\n"); return -1; } if (fwrite(array, sizeof(Horse), COUNT, f) != COUNT) { printf("Error writing the file.\n"); return -1; } fclose(f); // 读取 f = fopen(filename, "rb"); if (f == NULL) { printf("Failed to open the file.\n"); return -1; } if (fread(array, sizeof(Horse), COUNT, f) != COUNT) { printf("Error reading the file.\n"); return -1; } fclose(f); int i; for (i = 0; i < COUNT; i++) { printf("name: %s, age: %d\n", array[i].name, array[i].age); }
# stdlib.h
- stdlib.h 是标准函数库(standard library),提供了一些常用的函数。
# 关于数字
int abs(int x);
- 用途:输入一个整数,返回它的绝对值。
- 例:
printf("%d\n", abs(-1)); // 输出为:1
int atoi(const char *str);
- 用途:输入一个字符串,将它转换成整数并返回。如果转换失败,则返回 0 。
- 例:
printf("%d\n", atoi("Hello")); // 输出为:0 printf("%d\n", atoi("3")); // 输出为:3 printf("%d\n", atoi("3.14")); // 输出为:3
double atof(const char *str);
- 用途:输入一个字符串,将它转换成浮点数并返回。如果转换失败,则返回 0.0 。
- 例:
printf("%lf\n", atof("Hello")); // 输出为:0.000000 printf("%lf\n", atof("3")); // 输出为:3.000000 printf("%lf\n", atof("3.14")); // 输出为:3.140000 printf("%lf\n", atof("-31.4e-1f")); // 输出为:-3.140000
int rand(void);
- 用途:返回一个伪随机数。
- 原理:
- 根据 seed ,随机生成一组整数,组成一个序列。这些整数的取值范围为 0 至
RAND_MAX=0x7FFFFFF
。 - 每次调用该函数,就会返回序列中下一个整数。
- 只要 seed 相同,每次重启程序,都会得到相同的随机数。
- 根据 seed ,随机生成一组整数,组成一个序列。这些整数的取值范围为 0 至
- 例:
int max = 10, min = 1; int x = rand()%(max-min) + min; //生成介于 max 与 min 之间的随机数 printf("%d\n", x);
void srand(unsigned int seed); //用于设置随机数的种子,使得函数产生不同的随机数序列
- 用途:设置随机数算法的种子,不同的种子会让 rand() 函数产生不同的随机数序列。
- 如果别人不知道种子,就不能预测随机数的取值,相当于真随机数。
- 例:
srand(123); printf("%d\n", rand(););
# 关于内存
void *malloc(size_t size);
- 用途:请求分配一块内存空间,体积为 size 个字节。
- 如果请求成功,则返回一个指向该内存地址的指针。
- 如果请求失败,则返回 NULL 。
- 什么情况下使用 malloc() ?
- 当用户需要存储一些数据时,如果创建变量,则该变量会一直占用内存空间,直到变量的作用域结束。
- 为了节省内存空间,可以在需要存储数据时,通过 malloc() 临时分配一块内存空间。在不需要存储数据时,调用 free() 释放内存空间。
- 例:
char *str = malloc(10*sizeof(char)); // 分配一块内存空间,可以存储 10 个 char 字符 if(str == NULL) // 申请分配内存之后,应该先检查返回的指针是否有效,再进行下一步操作 return -1; printf("%s\n", str); // 输出为乱码,因为这块内存空间,此时每个字节的取值是未知的 strcpy(str, "Hello"); printf("%s\n", str); // 输出为:Hello free(str); // 释放这块内存空间
- malloc() 的返回值是 void * 类型的指针,即通用指针。
- C 语言中,将通用指针,赋值给其它类型的指针时,会发生自动类型转换。
- C++ 中,通用指针不会自动类型转换,只能强制类型转换:不过 C++ 分配内存空间时,很少使用 malloc() ,而是使用关键字 new 。
char *str = (char *)malloc(10*sizeof(char));
- 用途:请求分配一块内存空间,体积为 size 个字节。
void free(void *p);
- 用途:假设用户请求分配了一块内存空间,地址记在指针 p 中。用户可以调用 free() ,释放这块内存空间。
- 每块内存空间,只能被释放一次。如果重复释放,则会导致程序崩溃:为了避免这种情况,建议每次调用 free() 之后,就将指针存储的内存地址抹除:
free(p); free(p);
free(p); p = NULL; // 此后如果再调用 free(p) ,则相当于 free(NULL) ,不会引发问题
void *calloc(size_t num, size_t size);
- 用途:请求分配一块内存空间,体积为 num*size 个字节,并将每个字节赋值为 0 。
- 如果请求成功,则返回一个指向该内存地址的指针。
- 如果请求失败,则返回 NULL 。
- 例:
char *str = calloc(10, sizeof(char)); if(str == NULL) return -1; printf("%s\n", str); // 输出为空 strcpy(str, "Hello"); printf("%s\n", str); // 输出为: Hello free(str);
- 用途:请求分配一块内存空间,体积为 num*size 个字节,并将每个字节赋值为 0 。
void *realloc(void *p, size_t size);
- 用途:假设用户请求分配了一块内存空间,地址记在指针 p 中。用户可以调用 realloc() ,将这块内存空间的体积,改为 size 个字节。
- 如果请求成功,则返回一个指向该内存地址的指针。
- 如果请求失败,则返回 NULL 。
- 如果 p 为空指针,则 realloc() 会分配一块新的内存空间。
- 例:
char *str = malloc(10); strcpy(str, "Hello"); str = realloc(str, 20); // 可以扩大内存空间 str = realloc(str, 10); // 也可以缩小内存空间 printf("%s\n", str); // 输出为: Hello
- 用途:假设用户请求分配了一块内存空间,地址记在指针 p 中。用户可以调用 realloc() ,将这块内存空间的体积,改为 size 个字节。
# 管理内存
程序运行时,会占用一定容量的内存空间,并将它们分为 stack、heap 等几个用途不同的区域。
程序运行时,需要分配多块内存空间来存储数据,主要有两种分配方式:
- 静态的内存分配方式
- :在程序编译时,就决定了如何分配这些内存空间。在程序启动时,会立即申请这些内存空间,然后一直占用。直到程序终止,才释放这些内存空间。
- 比如程序代码、全局变量、静态变量,采用静态的内存分配方式。
- 动态的内存分配方式
- :在程序运行时,可以随时申请这些内存空间,随时释放它们。
- 比如局部变量、malloc() 分配的内存,采用动态的内存分配方式。
- 静态的内存分配方式
C 语言的主要特色之一是,可以调用 malloc() 动态分配内存空间,可以通过指针直接访问内存地址。
- 而 Java、Python 等语言中,不允许用户直接分配指定容量的内存空间,也不允许直接访问内存地址。
- 用户只能以创建变量(实际上是创建对象)的方式,来存储数据。当变量被创建时,会自动分配内存空间。当对象停止使用时,会自动释放内存空间。
- 优点:用户不必直接管理内存空间,工作量大幅减少。
- 缺点:用户的操作权限变少,不能精打细算地分配内存空间,不能随意访问任意内存地址。
- 而 Java、Python 等语言中,不允许用户直接分配指定容量的内存空间,也不允许直接访问内存地址。
C 语言允许用户直接管理内存,容易引发一些内存问题:
访问无效的内存地址
- 可能该内存地址的格式不正确,比如空指针。
- 可能该内存地址的格式正确,但没有分配给当前进程使用,比如被 free() 的指针。
- 可能该内存地址已分配给当前进程使用,但存储着未知的数据,比如野指针。
- 影响:可能导致程序故障,甚至崩溃。
内存越界访问
- :访问一块内存空间时,使用的内存地址,超出了这块内存空间的地址范围。
- 例:程序用 malloc() 申请一块 2 bytes 的内存空间,得到一个指针。然后往该指针指向的内存地址,写入 3 bytes 的数据。此时第 3 个 byte ,就会写入当前内存的地址范围之外。
- 为了避免这种问题,建议用户不要使用 gets、sprintf、strcpy、strcat、 等函数,因为它们没有限制字符串的长度,可能导致内存越界访问。建议改用 fgets、snprintf、strncpy、strncat 等函数。
- 越界访问的内存地址,分为两种情况:
- 该内存地址,已分配给当前进程使用,但属于另外一块内存空间(比如被另外一个 mallo() 分配)。读取它,可能导致程序故障。写入它,可能修改程序存储的其它数据,导致程序故障。
- 该内存地址,没有分配给当前进程使用。读取它、写入它,都会导致程序崩溃。
- Linux 内核会为每个进程维护一个页表,记录该进程申请使用了哪些内存地址。当进程访问一个内存地址时:
- 如果该内存地址,记录在当前页表中,则说明已分配给该进程。因此 Linux 会允许访问。
- 如果该内存地址,不记录在当前页表中,则说明未分配给该进程。因此 Linux 会拒绝访问,发出 segment fault 信号,导致进程崩溃。
- 除了堆内存,如果进程越界访问内核内存空间、只读存储区、代码段,Linux 也会拒绝访问,发出 segment fault 信号,导致进程崩溃。
缓冲区溢出攻击(Buffer Overflow Attack)
- :指用户利用内存越界访问,对程序进行恶意攻击。
- 通常是程序的开发者,将程序交给其他人使用,但有人故意引发内存越界访问。比如向程序的一个变量(通常用作缓冲区,允许用户修改)中,故意写入过长的数据,超出该变量的内存地址范围,写入到该变量隔壁的内存地址,从而修改隔壁的数据(比如变量、函数返回地址)。
栈溢出(stack overflow)
- :发生在 stack 区域的缓冲区溢出。
- 比如读写一个数组时,下标越界,这可能读写隔壁变量的内存地址。
内存溢出(out of memory)
- :指程序占用的内存,超出了允许的最大容量。
- 程序的 stack 区域,容量较小,容易达到上限。
- 比如 Linux 默认设置了每个线程的 stack 容量最大为 8MB 。
- 程序运行时,会将所有函数参数、局部变量存储在 stack 区域。如果创建一个很大的数组类型局部变量,可能超出栈区的最大容量,导致程序崩溃。
- 每次调用一个函数,都会在栈区分配一块内存来存储函数参数、局部变量,当函数执行完毕时才释放内存。如果一个函数调用另一个函数,后者再调用另一个函数,调用层数太深,则占用内存可能超出栈区的最大容量,导致程序崩溃。
- 程序的 heap 区域,容量没有上限。如果不断增长,则主机可用内存会越来越少。最终主机内存耗尽,触发 OOM 杀死进程。
内存泄漏(memory leak)
- :指程序 heap 区域的内存容量不断增长,未来可能发生内存溢出。
- 这通常是因为程序不断调用 malloc() ,占用越来越多的内存,没有用 free() 全部释放。
# 关于系统
void exit(int status);
- 用途:使当前进程终止运行,并返回一个整数,作为退出码。
- C 语言程序启动时,会执行 main() 函数。等到 main() 函数执行完毕,程序才终止。不过调用 exit() ,可以让程序立即终止。
void abort(void);
- 用途:发送 SIGABRT 信号,使当前进程终止运行。
char *getenv(const char *name);
- 用途:从系统读取一个名为 *name 的环境变量,返回它的值。如果没找到该环境变量,则返回 NULL 。
- 例:
const char *value = getenv("PWD"); if(value != NULL) printf("%s\n", value);
int system(const char *string);
- 用途:输入一个字符串,作为一条终端命令,让本机操作系统执行。
- 例:
system("pause"); // 在 Windows 系统执行 pause 命令,会暂停终端 system("cls"); // 在 Windows 系统执行 cls 命令,会清空终端 system("exit"); // 在 Windows 系统执行 cls 命令,会退出终端
# string.h
string.h 提供了一些处理字符串的函数。如下:
# 读取字符串
size_t strlen(const char *str);
- 用途:返回字符串的长度,也就是包含多少个字符,不考虑末尾的空字符。
- 例:
char str[] = "Hello"; printf("%d\n", strlen(str)); // 输出为:5 printf("%d\n", sizeof(str)); // 输出为:6 。因为包含末尾的空字符
char *strchr(const char *str, int chr);
- 用途:在字符串中,查找指定一个字符,返回第一次找到该字符的内存地址。如果没有找到,则返回 NULL 。
- 例:
char *pstr = strchr("Hello", 'e'); if(pstr != NULL) printf("%s\n", pstr); // 输出为:ello
int strcmp(const char *str1, const char *str2);
- 用途:逐个字符地比较两个字符串。如果字符数相同、同位字符也相同,则返回 0 ,否则返回非零。
int strncmp(const char *, const char *, size_t n);
- 用途:与 strcmp() 相似,但最多比较 n 个字符。
- 例:
printf("%d\n", strcmp("Hello", "Hello!")); // 输出为:-1 printf("%d\n", strncmp("Hello", "Hello!", 5)); // 输出为:0
# 拷贝字符串
char *strcpy(char *dst, const char *src);
- 用途:拷贝 src 字符串,写入 dst 字符串的内存空间,这会覆盖 dst 字符串的原有字符。函数执行成功后,会返回 dst 字符串的内存地址。
- 源码:
char *strcpy(char *dst, const char *src){ char *r = dst; assert((dst != NULL) && (src != NULL)); while (*r++ = *src++); // 逐个字符地赋值,当赋值到空字符(ASCII 码为 0 )时,结束 while 循环 return dst; }
- 例:
char *pstr = malloc(10*sizeof(char)); strcpy(pstr, "Hello"); printf("%s\n", pstr); // 输出为:Hello strcpy(pstr, "Hi"); // 拷贝 "Hi" ,这会覆盖 pstr 的第 0、1 个字符,并将第 2 个字符改为空字符 printf("%s\n", pstr); // 输出为:Hi printf("%s\n", pstr+3); // 输出为:lo 。此时 pstr 的第 3、4 个字符,没有被覆盖
int strncpy(char *dst, const char *src, size_t n);
- 用途:与 strcpy() 相似,但最多拷贝 n 个字符(包括空字符)。
- 如果
strlen(src) + 1 < n
,则 strncpy() 会将缺少的几位字符,用空字符补齐。 - 如果
strlen(src) + 1 > n
,则 strncpy() 不会完整拷贝 src 字符串。而且,不会拷贝 src 末尾的空字符,导致 dst 缺少空字符,读取 dst 字符串时可能内存越界。 - 因此,要谨慎考虑字符串的长度。
- 如果
- 例:
char *src = "Hello"; int len = (strlen(src) + 1); char *dst = malloc(sizeof(char) * len); strncpy(dst, src, len); printf("%s\n", dst); // 输出为:Hello
- 用途:与 strcpy() 相似,但最多拷贝 n 个字符(包括空字符)。
char *strcat(char *dst, const char *src);
- 用途:拷贝 src 字符串,写入 dst 字符串的末尾。使得 src 的第一个字符,覆盖 dst 原本的空字符。
- 例:
char *pstr = malloc(10*sizeof(char)); strcat(pstr, "Hello"); strcat(pstr, "!"); printf("%s\n", pstr); // 输出为:Hello!
char *strncat(char *dst, const char *src, size_t n);
- 用途:与 strcat() 相似,但最多拷贝 n 个字符(包括空字符)。
void *memcpy(void *dst, const void *src, size_t n);
- 用途:从 src 内存地址,拷贝 n 个字节的数据,写入 dst 内存地址。
- strcpy() 用于拷贝字符串,遇到空字符就会停止拷贝。而 memcpy() 用于拷贝任意字节数据,遇到空字符也不会停止拷贝。
void *memset(void *src, int x, size_t n);
- 用途:将从 src 内存地址开始的 n 个字节,都赋值为 x 。
- 动态分配一块内存空间之后,它每个字节的取值是未知的,称为垃圾值。通常用 memset() 初始化每个字节,以免读取到垃圾值。例:
char *pstr = malloc(10*sizeof(char)); memset(pstr, 0, sizeof(10));
# time.h
- time.h 提供了获取时间、日期的函数。
- 例:
#include <stdio.h> #include <time.h> int main(){ // 从操作系统获取当前时间。取值为 1970 年至今的秒数,称为 Unix timestamp time_t timestamp; time(×tamp); printf("timestamp: %ld\n", timestamp); // 将 timestamp 时间戳,转换成年月日时分秒的格式,采用本地时区 struct tm *time; time = localtime(×tamp); printf("Year: %d\n", 1900 + time->tm_year); printf("Month: %d\n", 1 + time->tm_mon); printf("Day: %d\n", time->tm_mday); printf("Hour: %d\n", time->tm_hour); printf("Minute: %d\n", time->tm_min); printf("Second: %d\n", time->tm_sec); printf("Weekday: %d\n", time->tm_wday); // 输出 Www Mmm dd hh:mm:ss yyyy 格式的时间字符串 printf("Time String: %s", asctime(time)); return 0; }