C 语言教程笔记
- 错误处理机制
C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。
assert.h
assert.h
头文件定义了宏assert()
,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。
int a = -10;
// assert(a >= 0);
// 等价于
if (a < 0)
{
puts("a is less than 0");
abort();
}
- 使用assert()的优缺点
优点: 它不仅能自动标识文件和出问题的行号
缺点: 引入了额外的检查,增加了程序的运行时间。
- 无需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h>语句的前面,定义一个宏 NDEBUG (assert 机制是通过预处理指令 #ifdef NDEBUG 来控制的。assert 宏定义在 assert.h 头文件中,其目的是在程序运行时检查条件是否为真,如果为假(即条件不满足),则触发程序中止并输出一条错误信息。)
#define NDEBUG
#include <assert.h>
- static_assert()
C11 引入了静态断言
static_assert()
,用于在编译阶段进行断言判断。
static_assert(constant-expression, string-literal);
static_assert()接受两个参数,第一个参数constant-expression是一个常量表达式,第二个参数string-literal是一个提示字符串。如果第一个参数的值为false,会产生一条编译错误,第二个参数就是错误提示信息。
static_assert(sizeof(int) == 4, "64-bit code generation is not supported.");
注意,static_assert()只在编译阶段运行,无法获得变量的值。如果对变量进行静态断言,就会导致编译错误。
int positive(const int n) {
//编译报错,因为编译时无法知道变量n的值
static_assert(n > 0, "value must > 0");
return 0;
}
ctype.h
- 字符测试函数
isalnum():是否为字母数字
isalpha():是否为字母
isdigit():是否为数字
isxdigit():是否为十六进制数字符
islower():是否为小写字母
isupper():是否为大写字母
isblank():是否为标准的空白字符(包含空格、水平制表符或换行符)
isspace():是否为空白字符(空格、换行符、换页符、回车符、垂直制表符、水平制表符等)
iscntrl():是否为控制字符,比如 Ctrl + B
isprint():是否为可打印字符
isgraph():是否为空格以外的任意可打印字符
ispunct():是否为标点符号(除了空格、字母、数字以外的可打印字符)
- 字符映射函数
tolower():如果参数是大写字符,返回小写字符,否则返回原始参数。 toupper():如果参数是小写字符,返回大写字符,否则返回原始参数。
// 将字符转为大写
ch = toupper(ch);
注意,这两个函数不会改变原始字符。
- errono.h
errno.h声明了一个 int 类型的 errno 变量,用来存储错误码(正整数)。
如果这个变量有非零值,表示已经执行的程序发生了错误。
int x = -1;
errno = 0;
double y = sqrt(x);
printf("testErrno errno:%d,y != y:%d\n",errno,(y != y));
//NaN 检测
if (y != y)
{
fprintf(stderr, "sqrt error; result is NAN");
exit(EXIT_FAILURE);
}
if (errno != 0) {
fprintf(stderr, "sqrt error; program terminated.\n");
exit(EXIT_FAILURE);
}
上面示例中,计算一个负值的平方根是不允许的,会导致errno不等于0。
如果要检查某个函数是否发生错误,必须在即将调用该函数之前,将errno的值置为0,防止其他函数改变errno的值。
float.h
inttypes.h
iso646.h
iso646.h头文件指定了一些常见运算符的替代拼写。比如,它用关键字and代替逻辑运算符&&。
and 替代 &&
and_eq 替代 &=
bitand 替代 &
bitor 替代 |
compl 替代 ~
not 替代 !
not_eq 替代 !=
or 替代 ||
or_eq 替代 |=
xor 替代 ^
xor_eq 替代 ^=
limits.h
下面的示例是使用预处理指令判断,int 类型是否可以用来存储大于 100000 的数。
#if INT_MAX < 100000
#error int type is too small
#endif
上面示例中,如果 int 类型太小,预处理器会显示一条出错消息。
可以使用limit.h里面的宏,为类型别名选择正确的底层类型。
#if INT_MAX >= 100000
typedef int Quantity;
#else
typedef long int Quantity;
#endif
上面示例中,如果整数类型的最大值(INT_MAX)不小于100000,那么类型别名Quantity指向int,否则就指向long int。
local.h
locale.h是程序的本地化设置,主要影响以下的行为。
数字格式 货币格式 字符集 日期和时间格式 它设置了以下几个宏。
LC_COLLATE:影响字符串比较函数strcoll()和strxfrm()。
LC_CTYPE:影响字符处理函数的行为。
LC_MONETARY:影响货币格式。
LC_NUMERIC:影响printf()的数字格式。
LC_TIME:影响时间格式strftime()和wcsftime()。
LC_ALL:将以上所有类别设置为给定的语言环境。
函数
char *setlocale(int category, const char *locale):设置或读取地域化信息。这个函数允许你指定用于字符分类、字符转换、货币格式、日期和时间格式、数字格式等的区域设置。你可以使用环境变量中的默认设置,或者指定特定的区域设置,如 "en_US.UTF-8"
。
struct lconv *localeconv(void):获取当前区域设置的信息,如小数点、千分位分隔符等。这些信息存储在 struct lconv 结构体中
。
locale_t newlocale(int category_mask, const char *locale, locale_t base):创建一个新的本地化对象。
freelocale(locale_t locale):释放一个本地化对象。
locale_t uselocale(locale_t newloc):设置或查询线程的本地化对象
math.h
- 判断一个值的类型
isfinite(1.23) // 1
isinf(1/tan(0)) // 1
isnan(sqrt(-1)) // 1
isnormal(1e-310)) // 0
- double round(double x);
round()函数以传统方式进行四舍五入,比如1.5舍入到2,-1.5舍入到-2。
round(3.14) // 3.000000
round(3.5) // 4.000000
round(-1.5) // -2.000000
round(-1.14) // -1.000000
- double trunc(double x);
trunc()用来截去一个浮点数的小数部分,将剩下的整数部分以浮点数的形式返回。
trunc(3.14) // 3.000000
trunc(3.8) // 3.000000
trunc(-1.5) // -1.000000
trunc(-1.14) // -1.000000
- ceil ceil()返回不小于其参数的最小整数(double 类型),属于“向上舍入”。
ceil(7.1) // 8.0
ceil(7.9) // 8.0
ceil(-7.1) // -7.0
ceil(-7.9) // -7.0
- floor floor()返回不大于其参数的最大整数,属于“向下舍入”。
floor(7.1) // 7.0
floor(7.9) // 7.0
floor(-7.1) // -8.0
floor(-7.9) // -8.0
- 四舍五入
double round_nearest(double x) {
return x < 0.0 ? ceil(x - 0.5) : floor(x + 0.5);
}
- double fmod(double x, double y); 返回第一个参数除以第二个参数的余数,就是余值运算符%的浮点数版本,因为%只能用于整数运算。 它在幕后执行的计算是x - trunc(x / y) * y,返回值的符号与x的符号相同。
fmod(5.5, 2.2) // 1.100000
fmod(-9.2, 5.1) // -4.100000
fmod(9.2, 5.1) // 4.100000
- 浮点数比较函数
isgreater():返回x > y的结果。
isgreaterequal():返回x >= y的结果。
isless():返回x < y的结果。
islessequal():返回x <= y的结果。
islessgreater():返回(x < y) || (x > y)的结果。
有了运算符 > 和 >= ,为什么还需要 isgreater 和 isgreaterequal? 当涉及到 NaN(不是一个数字)时,它们的比较结果可能是未定义的,因为 NaN 与任何值(包括它自己)的比较都是 false 但是此时用 isgreater 是可以正常工作的
- int isunordered(any_floating_type x, any_floating_type y);
isunordered()返回两个参数之中,是否存在 NAN。
isunordered(1.0, 2.0) // 0
isunordered(1.0, sqrt(-1)) // 1
isunordered(NAN, 30.0) // 1
isunordered(NAN, NAN) // 1
- 其他函数
signal.h
signal.h提供了信号(即异常情况)的处理工具。所谓“信号”(signal),可以理解成系统与程序之间的短消息,主要用来表示运行时错误,或者发生了异常事件。
- signal(SIGINT, handler)
void (*signal(int sig, void (*func)(int)))(int);
其中 sig 是信号码,func 是信号处理函数。如果 func 是 SIG_DFL,则信号会使用默认的处理方式;如果是 SIG_IGN,则忽略该信号;否则,当信号发生时会调用 func 指定的函数。signal 函数返回信号之前的处理函数,如果设置失败则返回 SIG_ERR。
- int raise(int sig);
它接受一个信号值作为参数,表示发出该信号。它的返回值是一个整数,可以用来判断信号发出是否成功,0 表示成功,非 0 表示失败。
void handler(int sig) {
printf("Handler called for signal %d\n", sig);
}
signal(SIGINT, handler);
raise(SIGINT);
上面示例中,raise()触发 SIGINT 信号,导致 handler 函数执行。
- 示例
int running = 0;
void sigint_handler(int sig)
{
running = 0;
printf("Caught signal %d\n", sig);
}
void signalTest()
{
// 设置 SIGINT 信号的处理程序为 sigint_handler 函数
signal(SIGINT, sigint_handler);
running = 1;
while (running)
{
printf("Running...\n");
sleep(1);
}
}
- rainse 使用
void sigint_handler(int sig)
{
running = 0;
printf("Caught signal %d\n", sig);
}
void signalTest2(){
// 设置 SIGINT 信号的处理程序为 sigint_handler 函数
signal(SIGINT, sigint_handler);
printf("Sending SIGINT to myself\n");
raise(SIGINT); // 向当前进程发送 SIGINT 信号
printf("Exiting...\n");
}
stdarg.h
<stdarg.h>
是一个提供可变参数功能的标准库头文件。它允许函数接受不定数量的参数。这在编写需要处理不同数量参数的通用函数时非常有用,例如 printf
函数或 scanf
函数。
- `va_list`:这是一个类型定义,用于创建一个指向参数列表的指针。
- `va_start(va_list ap, last)`:这个宏初始化 `va_list` 类型的变量 `ap`,使其指向可变参数函数的第一个可选参数。`last` 是最后一个固定参数的名称。
- `va_arg(va_list ap, type)`:这个宏用于访问 `ap` 指向的参数列表中的下一个参数,并将其作为指定类型 `type` 的值返回。每次调用 `va_arg` 都会更新 `ap` 使其指向参数列表中的下一个参数。
- `va_end(va_list ap)`:这个宏用于完成对参数列表的访问。在大多数实现中,这是一个空操作,但有些系统可能需要它来释放资源或重置状态。
- `va_copy(va_list dest, va_list src)`:这个宏用于复制一个 `va_list` 对象。`dest` 和 `src` 是 `va_list` 类型的变量,`dest` 被初始化为与 `src` 相同的状态。
//代码示例1
int sum(int count, ...) {
va_list args;
int total = 0;
// 初始化 args 以访问可变参数
va_start(args, count);
// 循环获取每个参数并累加
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
// 完成对参数列表的访问
va_end(args);
return total;
}
int total = sum(3, 1, 2, 3); // 调用 sum 函数,传入 3 个整数
printf("The sum is: %d\n", total);
//代码示例2
int my_print(int serial, const char *format, ...)
{
va_list va;
printf("The serial number is :%d\n", serial);
va_start(va, format);
int rv = vprintf(format, va);
return rv;
}
int x = 10;
float y = 3.2;
my_print(100, "x is %d, y is %f\n", x, y);
- int vprintf(const char *format, va_list arg); 允许你使用可变参数列表来输出格式化的文本到标准输出流(通常是屏幕)。这个函数非常有用,尤其是当你需要编写一个函数,该函数可以接受不同数量的参数,并且需要将这些参数格式化后输出。返回值 vprintf 返回成功写入到标准输出的字符数,不包括末尾的空字符(\0)。如果发生错误,函数返回 EOF。
void print_values(const char *format, ...) {
va_list args;
va_start(args, format); // 初始化 args 以访问可变参数列表
vprintf(format, args); // 使用 vprintf 输出格式化的参数
va_end(args); // 完成对参数列表的访问
}
int main() {
print_values("%d %d %d %d\n", 1, 2, 3, 4); // 调用 print_values 函数
return 0;
}
- vprintf 和 printf 的区别
- int vfprintf(FILE *stream, const char *format, va_list arg); 出格式化的参数到指定的文件流
stream:指向 FILE 结构的指针,表示要写入数据的目标文件流。 format:指向格式字符串的指针,该字符串定义了后续参数的格式化方式。 arg:va_list 类型的参数,它指向一个可变参数列表。 返回值 vfprintf 返回成功写入到文件流中的字符数,不包括末尾的空字符(\0)。如果发生错误,函数返回 EOF。
#include <stdio.h>
#include <stdarg.h>
void print_values(FILE *stream, const char *format, ...) {
va_list args;
va_start(args, format); // 初始化 args 以访问可变参数列表
vfprintf(stream, format, args); // 使用 vfprintf 输出格式化的参数到指定的文件流
va_end(args); // 完成对参数列表的访问
}
int main() {
FILE *file = fopen("output.txt", "w");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 调用 print_values 函数,将输出写入到文件中
print_values(file, "Value1: %d, Value2: %f, Value3: %s\n", 42, 3.14159, "Pi");
fclose(file); // 关闭文件
return 0;
}
- int vsprintf(char *str, const char *format, va_list arg);
它用于将格式化的数据写入到字符串中,同时允许可变数量的参数。这个函数类似于
sprintf
,但它使用va_list
类型的参数来接受可变参数列表,这使得它在处理不确定参数数量的情况下非常有用。
- `str`:指向字符数组的指针,用于存储格式化后的输出字符串。
- `format`:指向格式字符串的指针,该字符串定义了后续参数的格式化方式。
- `arg`:`va_list` 类型的参数,它指向一个可变参数列表。
返回值
`vsprintf` 返回成功写入到字符串中的字符数,不包括末尾的空字符(`\0`)。如果发生错误,函数返回 `EOF`。
void format_message(char *buffer, const char *format, ...)
{
va_list args;
va_start(args, format);
vsprintf(buffer, format, args);
va_end(args);
}
void vs_printf()
{
char buffer[100];
format_message(buffer, "Value:%d,Pi:%f,Text:%s\n", 42, 3.14, "example");
printf("Formatted message: %s", buffer); // 打印格式化后的字符串
}
stdbool.h
//将 _Bool 定义为 int 的别名
typedef int _Bool;
//将 bool 替换为 _Bool, 也就是 int 值
#define bool _Bool
//将 true 替换 为 1,false 替换为 0
#define true 1
#define false 0
/**
* 是否是偶数
*/
bool isEven(int num){
if (num % 2 ==0 )
{
return true;
}else{
return false;
}
}
# stddef.h
stddef.h
定义了两个宏。
NULL:空指针。 offsetof()
- offsetof()
offsetof()
是stddef.h
定义的一个宏,用来返回某个属性在 Struct 结构内部的起始位置。由于系统为了字节对齐,可能会在 Struct 结构的属性之间插入空字节,这个宏对于确定某个属性的内存位置很有用。
它是一个带参数的宏,接受两个参数。第一个参数是 Struct 结构,第二个参数是该结构的一个属性,返回 Struct 起始位置到该属性之间的字节数。
struct s {
char a;
int b[2];
float c;
};
printf("%zu\n", offsetof(struct s, a)); // 0
printf("%zu\n", offsetof(struct s, b)); // 4
printf("%zu\n", offsetof(struct s, c)); // 12
对于上面这个 Struct 结构,offsetof(struct s, a)
一定等于0
,因为a
属性是第一个属性,与 Struct 结构自身的地址相同。
系统为了字节对齐,在a
属性后面分配了3个空字节,导致b
属性存储在第4个字节,所以offsetof(struct s, b)
和offsetof(struct s, c)
分别是4和12。
stdint.h
- 标准 I/O 函数
以下函数用于控制台的输入和输出。
- printf():输出到控制台,详见《基本语法》一章。
- scanf():从控制台读取输入,详见《I/O 函数》一章。
- getchar():从控制台读取一个字符,详见《I/O 函数》一章。
- putchar():向控制台写入一个字符,详见《I/O 函数》一章。
- gets():从控制台读取整行输入(已废除),详见《I/O 函数》一章。
- puts():向控制台写入一个字符串,详见《I/O 函数》一章。
- 文件操作函数
以下函数用于文件操作,详见《文件操作》一章。
- fopen():打开文件。
- fclose():关闭文件。
- freopen():打开一个新文件,关联一个已经打开的文件指针。
- fprintf():输出到文件。
- fscanf():从文件读取数据。
- getc():从文件读取一个字符。
- fgetc():从文件读取一个字符。
- putc():向文件写入一个字符。
- fputc():向文件写入一个字符。
- fgets():从文件读取整行。
- fputs():向文件写入字符串。
- fread():从文件读取二进制数据。
- fwrite():向文件写入二进制数据。
- fseek():将文件内部指针移到指定位置。
- ftell():获取文件内部指针的当前位置。
- rewind():将文件内部指针重置到文件开始处。
- fgetpos():获取文件内部指针的当前位置。
- fsetpos():设置文件内部指针的当前位置。
- feof():判断文件内部指针是否指向文件结尾。
- ferror():返回文件错误指示器的状态。
- clearerr():重置文件错误指示器。
- remove():删除文件。
- rename():文件改名,以及移动文件。
- 字符串操作函数
以下函数用于操作字符串,详见《字符串操作》一章。
- sscanf():从字符串读取数据,详见《I/O 函数》一章。
- sprintf():输出到字符串。
- snprintf():输出到字符串的更安全版本,指定了输出字符串的数量。
- tmpfile()
tmpfile()
函数创建一个临时文件,该文件只在程序运行期间存在,除非手动关闭它。它的原型如下。
tmpfile()
返回一个文件指针,可以用于访问该函数创建的临时文件。如果创建失败,返回一个空指针 NULL。
FILE* tempptr;
tempptr = tmpfile();
调用close()
方法关闭临时文件后,该文件将被自动删除。
tmpfile()
有两个缺点。一是无法知道临时文件的文件名,二是无法让该文件成为永久文件。
- 可变参数操作函数 (1)输出函数
下面是`printf()`的变体函数,用于按照给定格式,输出函数的可变参数列表(va_list)。
- vprintf():按照给定格式,输出到控制台,默认是显示器。
- vfprintf():按照给定格式,输出到文件。
- vsprintf():按照给定格式,输出到字符串。
- vsnprintf():按照给定格式,输出到字符串的安全版本。
它们的原型如下,基本与对应的`printf()`系列函数一致,除了最后一个参数是可变参数对象。
(2)输入函数
下面是`scanf()`的变体函数,用于按照给定格式,输入可变参数列表 (va_list)。
- vscanf():按照给定格式,从控制台读取(默认为键盘)。
- vfscanf():按照给定格式,从文件读取。
- vsscanf():按照给定格式,从字符串读取。
stdlib.h
stdlib.h 定义了下面的类型别名。
- size_t:sizeof 的返回类型。
- wchar_t:宽字符类型。
stdlib.h 定义了下面的宏。
- NULL:空指针。
- EXIT_SUCCESS:函数运行成功时的退出状态。
- EXIT_FAILURE:函数运行错误时的退出状态。
- RAND_MAX:rand() 函数可以返回的最大值。
- MB_CUR_MAX:当前语言环境中,多字节字符占用的最大字节数。
-
abs(),labs(),llabs() 这三个函数用于计算整数的绝对值。
abs()
用于 int 类型,labs()
用于 long int 类型,llabs()
用于 long long int 类型。 -
div(),ldiv(),lldiv()
这三个函数用来计算两个参数的商和余数。div()
用于 int 类型的相除,ldiv()
用于 long int 类型的相除,lldiv()
用于 long long int 类型的相除。
div_t div(int numer, int denom);
ldiv_t ldiv(long int numer, long int denom);
lldiv_t lldiv(long long int numer, long long int denom);
这些函数把第2个参数(分母)除以第1个参数(分子),产生商和余数。这两个值通过一个数据结构返回,div()
返回 div_t 结构,ldiv()
返回 ldiv_t 结构,lldiv()
返回 lldiv_t 结构。
这些结构都包含下面两个字段,
int quot; // 商
int rem; // 余数
它们完整的定义如下。
typedef struct {
int quot, rem;
} div_t;
typedef struct {
long int quot, rem;
} ldiv_t;
typedef struct {
long long int quot, rem;
} lldiv_t;
下面是一个例子。
div_t d = div(64, -7);
// 输出 64 / -7 = -9
printf("64 / -7 = %d\n", d.quot);
// 输出 64 % -7 = 1
printf("64 %% -7 = %d\n", d.rem);
- 字符串转成数值
stdlib.h
定义了一系列函数,可以将字符串转为数字。
- atoi():字符串转成 int 类型。
- atof():字符串转成 double 类型。
- atol():字符串转成 long int 类型。
- atoll():字符串转成 long long int 类型。
int atoi(const char* nptr);
double atof(const char* nptr);
long int atol(const char* nptr);
long long int atoll(const char* nptr);
- str 系列函数(浮点数转换)
stdlib.h
还定义了一些更强功能的浮点数转换函数。
- strtof():字符串转成 float 类型。
- strtod():字符串转成 double 类型。
- strtold():字符串转成 long double 类型。
它们的原型如下。
float strtof(
const char* restrict nptr,
char** restrict endptr
);
double strtod(
const char* restrict nptr,
char** restrict endptr
);
long double strtold(
const char* restrict nptr,
char** restrict endptr
);
它们都接受两个参数,第一个参数是需要转换的字符串,第二个参数是一个指针,指向原始字符串里面无法转换的部分。
- `nptr`:待转换的字符串(起首的空白字符会被忽略)。
- `endprt`:一个指针,指向不能转换部分的第一个字符。如果字符串可以完全转成数值,该指针指向字符串末尾的终止符`\0`。这个参数如果设为 NULL,就表示不需要处理字符串剩余部分。
它们的返回值是已经转换后的数值。如果字符串无法转换,则返回`0`。如果转换结果发生溢出,errno 会被设置为 ERANGE。如果值太大(无论是正数还是负数),函数返回`HUGE_VAL`;如果值太小,函数返回零。
char *inp = " 123.4567abdc";
char *badchar;
double val = strtod(inp, &badchar);
printf("%f\n", val); // 123.456700
printf("%s\n", badchar); // abdc
字符串可以完全转换的情况下,第二个参数指向`\0`,因此可以用下面的写法判断是否完全转换。
if (*endptr == '\0') {
// 完全转换
} else {
// 存在无法转换的字符
}
如果不关心没有转换的部分,则可以将 endptr 设置为 NULL。
- rand()
rand()
函数用来生成 0~RAND_MAX 之间的随机整数。RAND_MAX
是一个定义在stdlib.h
里面的宏,通常等于 INT_MAX。
如果希望获得整数 N 到 M 之间的随机数(包括 N 和 M 两个端点值),可以使用下面的写法。
int x = rand() % (M - N + 1) + N;
比如,1 到 6 之间的随机数,写法如下。
int x = rand() % 6 + 1;
- srand()
rand()
是伪随机数函数,为了增加随机性,必须在调用它之前,使用srand()
函数重置一下种子值。
srand()
函数接受一个无符号整数(unsigned int)作为种子值,没有返回值。
void srand(unsigned int seed);
通常使用time(NULL)
函数返回当前距离时间纪元的秒数,作为srand()
的参数。
#include <time.h>
srand((unsigned int) time(NULL));
- exit(),quick_exit(),_Exit()
这三个函数都用来退出当前正在执行的程序。
void exit(int status);
void quick_exit(int status);
void _Exit(int status);
它们都接受一个整数,表示程序的退出状态,0
是正常退出,非零值表示发生错误,可以使用宏EXIT_SUCCESS
和EXIT_FAILURE
当作参数。它们本身没有返回值。
它们的区别是,退出时所做的清理工作不同。exit()
是正常退出,系统会做完整的清理,比如更新所有文件流,并且删除临时文件。quick_exit()
是快速退出,系统的清理工作稍微少一点。_Exit()
是立即退出,不做任何清理工作。
下面是一些用法示例。
exit(EXIT_SUCCESS);
quick_exit(EXIT_FAILURE);
_Exit(2);
- getenv()
getenv()
用于获取环境变量的值。环境变量是操作系统提供的程序之外的一些环境参数。
char* getenv(const char* name);
它的参数是一个字符串,表示环境变量名。返回值也是一个字符串,表示环境变量的值。如果指定的环境变量不存在,则返回 NULL。
下面是输出环境变量$PATH
的值的例子。
printf("PATH is %s\n", getenv("PATH"));
- system()
system()函数用于执行外部程序。它会把它的参数字符串传递给操作系统,让操作系统的命令处理器来执行。
void system( char const * command );
- 内存管理函数
stdlib.h 提供了一些内存操作函数,下面几个函数详见《内存管理》一章,其余在本节介绍。
malloc():分配内存区域 calloc():分配内存区域。 realloc():调节内存区域大小。 free():释放内存区域。
- alligned_alloc()
多系统有内存对齐的要求,即内存块的大小必须是某个值(比如64字节)的倍数,这样有利于提高处理速度。aligned_alloc()就用于分配满足内存对齐要求的内存块,它的原型如下。
void* aligned_alloc(size_t alignment, size_t size);
它接受两个参数。
alignment:整数,表示内存对齐的单位大小,一般是2的整数次幂(2、4、8、16……)。 size:整数,表示内存块的大小。 分配成功时,它返回一个无类型指针,指向新分配的内存块。分配失败时,返回 NULL。
char* p = aligned_alloc(64, 256); 上面示例中,aligned_alloc()分配的内存块,单位大小是64字节,要分配的字节数是256字节。
- qsort qsort()用来快速排序一个数组。它对数组成员的类型没有要求,任何类型数组都可以用这个函数排序。
void qsort(
void *base,
size_t nmemb,
size_t size,
int (*compar)(const void *, const void *)
);
该函数接受四个参数。
base:指向要排序的数组开始位置的指针。
nmemb:数组成员的数量。
size:数组每个成员占用的字节长度。
compar:一个函数指针,指向一个比较两个成员的函数。
string.h
- strchr strchr()和strrchr()都用于在字符串中查找指定字符(类似于 Java 中的 subString)。不同之处是,strchr()从字符串开头开始查找,strrchr()从字符串结尾开始查找,函数名里面多出来的那个r表示 reverse(反向)。
char* strchr(char* str, int c);
char* strrchr(char *str, int c);
char *str = "Hello world";
char c = 'o';
char *strchrResult = strchr(str, c);
if (strchrResult != NULL)
{
printf("Character '%c' found at position: %ld, result:%s\n", c, strchrResult - str + 1,strchrResult);
}
else
{
printf("Character '%c' not found in the string.\n", c);
}
- 指定位置截图字符串
在 C 语言中,没有标准函数直接支持从指定的开始和结束位置截取字符串。但是,你可以通过组合使用一些基本的字符串处理函数来实现这个功能。以下是一个简单的示例,展示如何从给定的字符串中截取从 start 位置到 end 位置的子串:
#include <stdio.h>
#include <string.h>
char* substring(const char *str, int start, int end) {
// 检查边界条件
if (str == NULL || start < 0 || end >= strlen(str) || start > end) {
return NULL;
}
// 计算子串的长度
size_t len = end - start + 1;
// 分配足够的内存来存储子串和空终止符
char *result = malloc(len + 1);
if (result == NULL) {
// 内存分配失败
return NULL;
}
// 复制子串到结果缓冲区
for (int i = 0; i < len; ++i) {
result[i] = str[start + i];
}
// 添加空终止符
result[len] = '\0';
return result;
}
int main() {
const char *str = "Hello, World!";
int start = 7;
int end = 12;
char *sub = substring(str, start, end);
if (sub != NULL) {
printf("Substring: %s\n", sub);
free(sub); // 记得释放分配的内存
} else {
printf("Failed to extract substring.\n");
}
return 0;
}
- strspn(),strcspn()
strspn()用来查找属于指定字符集的字符串长度,strcspn()正好相反,用来查找不属于指定字符集的字符串长度。
size_t strspn(char* str, const char* accept);
size_t strcspn(char *str, const char *reject);
这两个函数接受两个参数,第一个参数是源字符串,第二个参数是由指定字符组成的字符串。
strspn()从第一个参数的开头开始查找,一旦发现第一个不属于指定字符集范围的字符,就停止查找,返回到目前为止的字符串长度。如果始终没有不在指定字符集的字符,则返回第一个参数字符串的长度。
strcspn()则是一旦发现第一个属于指定字符集范围的字符,就停止查找,返回到目前为止的字符串长度。如果始终没有发现指定字符集的字符,则返回第一个参数字符串的长度。
char str[] = "hello world";
int n;
n = strspn(str1, "aeiou");
printf("%d\n", n); // n == 0
n = strcspn(str1, "aeiou");
printf("%d\n", n); // n == 1
- strpbrk
strpbrk()在字符串中搜索指定字符集的任一个字符。
char* strpbrk(const char* s1, const char* s2);
它接受两个参数,第一个参数是源字符串,第二个参数是由指定字符组成的字符串。
它返回一个指向第一个匹配字符的指针,如果未找到匹配字符,则返回 NULL。
char* s1 = "Hello, world!";
char* s2 = "dow!";
char* p = strpbrk(s1, s2);
printf("%s\n", p); // "o, world!"
上面示例中,指定字符集是“dow!”,那么s1里面第一个匹配字符是“Hello”的“o”,所以指针p指向这个字符。输出的话,就会输出从这个字符直到字符串末尾的“o, world!”。
- strstr 在一个字符串里面,查找另一个字符串。
char *strstr(
const char* str,
const char* substr
);
它接受两个参数,第一个参数是源字符串,第二个参数是所要查找的子字符串。
如果匹配成功,就返回一个指针,指向源字符串里面的子字符串。如果匹配失败,就返回 NULL,表示无法找到子字符串。
char* str = "The quick brown fox jumped over the lazy dogs.";
char* p = strstr(str, "lazy");
printf("%s\n", p == NULL ? "null": p); // "lazy dogs."
上面示例中,strstr()用来在源字符串str里面,查找子字符串lazy。从返回的指针到字符串结尾,就是“lazy dogs.”。上面示例中,指定字符集是“dow!”,那么s1里面第一个匹配字符是“Hello”的“o”,所以指针p指向这个字符。输出的话,就会输出从这个字符直到字符串末尾的“o, world!”。
- strtok
用来将一个字符串按照指定的分隔符(delimiter),分解成一系列词元(tokens)。
char* strtok(char* str, const char* delim);
它接受两个参数,第一个参数是待拆分的字符串,第二个参数是指定的分隔符。
它返回一个指针,指向分解出来的第一个词元,并将词元结束之处的分隔符替换成字符串结尾标志\0。如果没有待分解的词元,它返回 NULL。
如果要遍历所有词元,就必须循环调用,参考下面的例子。
strtok()的第一个参数如果是 NULL,则表示从上一次strtok()分解结束的位置,继续往下分解。
#include <stdio.h>
#include <string.h>
int main(void) {
char string[] = "This is a sentence with 7 tokens";
char* tokenPtr = strtok(string, " ");
while (tokenPtr != NULL) {
printf("%s\n", tokenPtr);
tokenPtr = strtok(NULL, " ");
}
}
上面示例将源字符串按照空格,分解词元。它的输出结果如下。
This
is
a
sentence
with
7
tokens
注意,strtok()会修改原始字符串,将所有分隔符都替换成字符串结尾符号\0。因此,最好生成一个原始字符串的拷贝,然后再对这个拷贝执行strtok()。
特别注意:strtok 函数会修改它所处理的字符串,因为它在每个分隔符的位置插入空字符(\0)来终止当前的子串。这意味着 strtok 只能用于可修改的字符串(即不是字符串字面量)。因此 源字符串不能定义成
//这种方式申明的字符串为 "字符串字面量", 是不能修改的
char * str2 = "This is a sentence with 7 tokens";
- strerror()
strerror()函数返回特定错误的说明字符串。
char *strerror(int errornum);
它的参数是错误的编号,由errno.h定义。返回值是一个指向说明字符串的指针。
// 输出 No such file or directory
printf("%s\n", strerror(2));
内存操作函数
以下内存操作函数,详见《内存管理》一章。
memcpy():内存复制函数。
memmove():内存复制函数(允许重叠)。
memcmp():比较两个内存区域。
- memcpy
memcpy 是 C 语言标准库中的一个函数,用于从源内存地址复制指定数量的字节到目标内存地址。这个函数定义在 <string.h> 头文件中。
函数原型
c
void *memcpy(void *dest, const void *src, size_t n);
dest:指向目标内存块的指针,数据将被复制到这个位置。
src:指向要复制的数据源的指针。
n:要复制的字节数。
返回值
memcpy 返回一个指向目标内存块 dest 的指针。
下面是一个使用 memcpy 函数的示例
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, World!";
char dest[20]; // 确保有足够的空间来存储复制的数据
// 使用 memcpy 复制字符串
memcpy(dest, src, strlen(src) + 1); // +1 用于复制空终止符 '\0'
printf("Source: %s\n", src);
printf("Destination: %s\n", dest);
return 0;
}
- memmove 将一块内存区域的内容复制到另一块内存区域,并且能够正确处理源区域和目标区域重叠的情况。这与 memcpy 函数不同,后者在源区域和目标区域重叠时可能会导致未定义的行为。
void *memmove(void *dest, const void *src, size_t n);
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
/**
* 将字节数的值从源指向的位置复制到目标指向的内存块。复制就像使用了中间缓冲区一样,允许目标和源重叠
* 从 arr 中拷贝 5 个元素到 第 3个位置上
* 输出:1 2 1 2 3 4 5 8 9 10
*/
memmove(arr+2, arr, sizeof(arr[0])*5);
//sizeof(arr[0])计算的结果是arr数组中一个元素的字节大小,乘5代表5个
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
- memcmp
memcmp 函数是 C 语言标准库中的一个函数,用于比较两个内存区域的内容。它定义在 <string.h> 头文件中。
函数原型
c
int memcmp(const void *str1, const void *str2, size_t n);
str1:指向第一个内存区域的指针。
str2:指向第二个内存区域的指针。
n:要比较的字节数。
返回值
memcmp 函数返回一个整数,表示比较结果:
如果返回值小于 0,则表示 str1 在字典顺序上小于 str2。
如果返回值等于 0,则表示 str1 和 str2 相等。
如果返回值大于 0,则表示 str1 在字典顺序上大于 str2。
下面是一个使用 memcmp 函数的示例:
#include <stdio.h>
#include <string.h>
int main() {
const char *str1 = "Hello";
const char *str2 = "World";
int result = memcmp(str1, str2, strlen(str1));
if (result < 0) {
printf("'%s' is less than '%s'\n", str1, str2);
} else if (result > 0) {
printf("'%s' is greater than '%s'\n", str1, str2);
} else {
printf("'%s' is equal to '%s'\n", str1, str2);
}
return 0;
}
- memcmp 和 strcmp 有什么区别?
主要区别
比较对象:
memcmp 比较内存区域,可以是比较任何类型的二进制数据。
strcmp 比较字符串,是基于字符的比较。
终止条件:
memcmp 根据指定的字节数进行比较,不会检查空终止符。
strcmp 在遇到空终止符时停止比较。
返回值:
memcmp 和 strcmp 都返回一个整数,表示比较结果,但 memcmp 的返回值范围可能更大,因为它比较的是字节值,而 strcmp 比较的是字符的 ASCII 值。
使用场景:
当你需要比较两个二进制数据块时,使用 memcmp。
当你需要比较两个字符串的内容时,使用 strcmp。
- memchr 用于在内存区域中查找指定字符。
const char *str3 = "Hello,World!";
int ch = 'W';
void *result = memchr(str3, ch, strlen(str3));
if (result != NULL) {
printf("Character '%c' found at position: %ld\n", ch, result - (void *)str3 + 1);
} else {
printf("Character '%c' not found in the string.\n", ch);
}
str:指向要搜索的内存区域的指针。
c:要搜索的字符。
n:要搜索的字节数。(超过 n 个字节数则不再搜索了)
- memset 用于将一块内存区域的所有字节设置为特定的值
char str4[] = "Hello,World!";
//用于将一块内存区域的所有字节设置为特定的值。
memset(str4, 'b', 5);
//输出 bbbbb,World!
printf("%s\n",str4);
函数原型
c
void *memset(void *str, int c, size_t n);
str:指向要设置的内存区域的指针。
c:要设置的值,将会被转换为 unsigned char。
n:要设置的字节数。
返回值
memset 函数返回指向内存区域起始地址的指针。
time.h
time_t 是一个表示时间的类型别名,可以视为国际标准时 UTC。它可能是浮点数,也可能是整数,Unix 系统一般是整数。
许多系统上,time_t 表示自时间纪元(time epoch)以来的秒数。Unix 的时间纪元是国际标准时 UTC 的1970年1月1日的零分零秒。time_t 如果为负数,则表示时间纪元之前的时间。
struct tm {
int tm_sec; // 秒数 [0, 60]
int tm_min; // 分钟 [0, 59]
int tm_hour; // 小时 [0, 23]
int tm_mday; // 月份的天数 [1, 31]
int tm_mon; // 月份 [0, 11],一月用 0 表示
int tm_year; // 距离 1900 的年数
int tm_wday; // 星期几 [0, 6],星期天用 0 表示
int tm_yday; // 距离1月1日的天数 [0, 365]
int tm_isdst; // 是否采用夏令时,1 表示采用,0 表示未采用
};
- time()
time()接受一个 time_t 指针作为参数,返回值会写入指针地址。参数可以是空指针 NULL。
time()的返回值是 time_t 类型的当前时间。 如果计算机无法提供当前的秒数,或者返回值太大,无法用time_t类型表示,time()函数就返回-1。
time_t now;
// 写法一
now = time(NULL);
// 写法二
time(&now);
- 计算某个操作耗费的时间(精确到秒)
time_t begin = time(NULL);
// ... 执行某些操作
time_t end = time(NULL);
printf("%d\n", end - begin);
- ctime
ctime()用来将 time_t 类型的值直接输出为人类可读的格式。 ctime()的参数是一个 time_t 指针,返回一个字符串指针。该字符串的格式类似“Sun Jul 4 04:02:48 1976\n\0”,尾部包含换行符和字符串终止标志。
char* ctime( time_t const * time_value );
下面是一个例子。
time_t now;
now = time(NULL);
// 输出 Sun Feb 28 18:47:25 2021
printf("%s", ctime(&now));
注意,ctime()会在字符串尾部自动添加换行符。
- localtime localtime()函数用来将 time_t 类型的时间(本地时间),转换为当前时区的 struct tm 结构。
struct tm* localtime(const time_t* timer);
time_t now_time_t = time(NULL);
struct tm *now = localtime(&now_time_t);
//2094-9-7 15:48:
printf("%d-%d-%d %d:%d:%d\n", (now->tm_year + 1900), now->tm_mon, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec);
- gmtime gmtime()函数用来将 time_t 类型的时间,转换为 UTC 时间的 struct tm 结构。 它与 localtime() 的区别就是返回值,前者是本地时间,后者是 UTC 时间。
time_t now_time_t = time(NULL);
struct tm *gmtimeResult = gmtime(&now_time_t);
printf("%d-%d-%d %d:%d:%d\n", (gmtimeResult->tm_year), gmtimeResult->tm_mon, gmtimeResult->tm_mday, gmtimeResult->tm_hour, gmtimeResult->tm_min, gmtimeResult->tm_sec);
- asctime
asctime()函数用来将 struct tm 结构,直接输出为人类可读的格式。该函数会自动在输出的尾部添加换行符。
time_t now = time(NULL);
// 输出 Local: Sun Feb 28 20:15:27 2021
printf("Local: %s", asctime(localtime(&now)));
// 输出 UTC : Mon Mar 1 04:15:27 2021
printf("UTC : %s", asctime(gmtime(&now)));
- mktime
mktime()函数用于把一个 struct tm 结构转换为 time_t 值。
time_t mktime(struct tm* tm_ptr);
mktime()的参数是一个 struct tm 指针。
mktime()会自动设置 struct tm 结构里面的tm_wday属性和tm_yday属性,开发者自己不必填写这两个属性。所以,这个函数常用来获得指定时间是星期几(tm_wday)。 下面是一个例子。
struct tm some_time = {
.tm_year=82, // 距离 1900 的年数
.tm_mon=3, // 月份 [0, 11]
.tm_mday=12, // 天数 [1, 31]
.tm_hour=12, // 小时 [0, 23]
.tm_min=00, // 分钟 [0, 59]
.tm_sec=04, // 秒数 [0, 60]
.tm_isdst=-1, // 夏令时
};
time_t some_time_epoch;
some_time_epoch = mktime(&some_time);
// 输出 Mon Apr 12 12:00:04 1982
printf("%s", ctime(&some_time_epoch));
- difftime
difftime 函数是 C 语言标准库中的一个函数,用于计算两个时间点之间的差异,以秒为单位。这个函数定义在 <stdio.h> 头文件中。 函数原型:
double difftime(time_t time1, time_t time2);
time1:第一个时间点,类型为 time_t。
time2:第二个时间点,类型为 time_t。
返回值
difftime 函数返回两个时间点之间的差异,以秒为单位,类型为 double。如果 time1 参数表示的时间早于 time2 参数表示的时间,返回值将是负数。
以下是一个例子:
time_t begin = time(NULL);
// 5 s
// sleep(5);
// 5 ms
usleep(1 * 1000 * 1000);
time_t end = time(NULL);
double duration = difftime(end,begin);
- strftime()
strftime()函数用来将 struct tm 结构转换为一个指定格式的字符串,并复制到指定地址。
size_t strftime(
char* str,
size_t maxsize,
const char* format,
const struct tm* timeptr
)
strftime()接受四个参数。
第一个参数:目标字符串的指针。 第二个参数:目标字符串可以接受的最大长度。 第三个参数:格式字符串。 第四个参数:struct tm 结构。 如果执行成功(转换并复制),strftime()函数返回复制的字符串长度;如果执行失败,返回-1。
char s[128];
time_t now = time(NULL);
// %c: 本地时间
strftime(s, sizeof s, "%c", localtime(&now));
puts(s); // Sun Feb 28 22:29:00 2021
// %A: 完整的星期日期的名称
// %B: 完整的月份名称
// %d: 月份的天数
strftime(s, sizeof s, "%A, %B %d", localtime(&now));
puts(s); // Sunday, February 28
// %I: 小时(12小时制)
// %M: 分钟
// %S: 秒数
// %p: AM 或 PM
strftime(s, sizeof s, "It's %I:%M:%S %p", localtime(&now));
puts(s); // It's 10:29:00 PM
// %F: ISO 8601 yyyy-mm-dd 格式
// %T: ISO 8601 hh:mm:ss 格式
// %z: ISO 8601 时区
strftime(s, sizeof s, "ISO 8601: %FT%T%z", localtime(&now));
puts(s); // ISO 8601: 2021-02-28T22:29:00-0800
- %zu 和 %lu 的区别
array
- 数组定义
int a[5] = {1, 2, 3};
// 等价于
int a1[5] = {1, 2, 3, 0, 0};
// 数组初始化时,可以指定为哪些位置的成员赋值。数组的2号、9号、14号位置被赋值,其他位置的值都自动设为0。
int a2[10] = {1, [5] = 7, [2] = 19, [7] = 7};
- 数组长度
int a4[] = {[2] = 6, [9] = 12};
printf("size of a4:%zu\n",sizeof(a4) / sizeof(a4[0]));
省略成员数量时,如果同时采用指定位置的赋值,那么数组长度将是最大的指定位置再加1。 数组a的最大指定位置是9,所以数组的长度是10。
- 数组地址 数组作为函数参数,以下 2 种函数原型是等价的:
// 写法一
int sum(int arr[], int len);
// 写法二
int sum(int* arr, int len);
int sum(int* arr, int len) {
int i;
int total = 0;
// 假定数组有 10 个成员
for (i = 0; i < len; i++) {
total += arr[i];
}
return total;
}
- *和&运算符也可以用于多维数组。
int a[4][2];
// 取出 a[0][0] 的值
*(a[0]);
// 等同于
**a
上面示例中,由于a[0]本身是一个指针,指向第二维数组的第一个成员a[0][0]。所以,(a[0])取出的是a[0][0]的值。至于**a,就是对a进行两次运算,第一次取出的是a[0],第二次取出的是a[0][0]。同理,二维数组的&a[0][0]等同于*a。
- 数组名指向的地址是不能更改的。声明数组时,编译器自动为数组分配了内存地址,这个地址与数组名是绑定的,不可更改,下面的代码会报错。
int ints[100];
ints = NULL; // 报错
int a[5] = {1, 2, 3, 4, 5};
// 写法一
int b[5] = a; // 报错
// 写法二
int b[5];
b = a; // 报错
上面两种写法都会更改数组b的地址,导致报错。
- 数组指针加减法
int a5[] = {1, 2, 3, 4, 5};
for (size_t i = 0; i < 5; i++)
{
printf("position:%zu, value1:%d, value2:%d, address:%p\n", i, *(a5 + i), a5[i], (a5 + i));
}
C 语言里面,数组名可以进行加法和减法运算,等同于在数组成员之间前后移动,即从一个成员的内存地址移动到另一个成员的内存地址。比如,a + 1返回下一个成员的地址,a - 1返回上一个成员的地址。
上面示例中,通过指针的移动遍历数组,a + i的每轮循环每次都会指向下一个成员的地址,(a + i)取出该地址的值,等同于a[i]。对于数组的第一个成员,(a + 0)(即*a)等同于a[0]。
由于数组名与指针是等价的,所以下面的等式总是成立。
a[b] == *(a + b)
上面代码给出了数组成员的两种访问方式,一种是使用方括号a[b],另一种是使用指针*(a + b)。
- 使用指针遍历数组
int a5[] = {1, 2, 3, 4, 5};
int *ptr = a5;
while (*ptr)
{
printf("%d ", *ptr);
ptr++;
}
上面示例中,通过p++让变量p指向下一个成员。
注意,数组名指向的地址是不能变的,所以上例中,不能直接对a进行自增,即a++的写法是错的,必须将a的地址赋值给指针变量p,然后对p进行自增。
- 数组指针加减法
如果指针变量p指向数组的一个成员,那么p++就相当于指向下一个成员,这种方法常用来遍历数组。
int *ptr = a5;
while (*ptr)
{
printf("%d ", *ptr);
ptr++;
}
- 通过数组起始位置遍历
int calcSum(int *start, int *end)
{
int total = 0;
while (start < end)
{
total += *start;
start++;
}
return total;
}
int sum = calcSum(a5, a5 + 5);
printf("sum:%d\n", sum);
- 多维数组指针加减法
int arr[4][2];
// 指针指向 arr[1] (第一行元素,也就是 arr[0][0])
arr + 1;
// 指针指向 arr[0][1]
arr[0] + 1
// 第一行是 1,2, 第二行是 3,4
int a6[2][2] = {{1, 2}, {3, 4}};
// 第一行第2个元素 a6[0] + 1 等价于 &a6[0][1]
printf("a6[0] + 1: %p\n", a6[0] + 1);
// 第一行第2个元素
printf("a6[0][1]: %p\n", &a6[0][1]);
// 第一行第2个元素 a6 + 1 等价于 &a6[1][0]
printf("a6+1: %p\n", a6 + 1);
// 第一行第2个元素
printf("a6[1][0]: %p\n", &a6[1][0]);
- 指针相减,等到两个指针之间元素个数
int* p = &a[5];
int* q = &a[1];
printf("%d\n", p - q); // 4
printf("%d\n", q - p); // -4
- 数组的复制
由于数组名是指针,所以复制数组不能简单地复制数组名。 复制数组最简单的方法,还是使用循环,将数组元素逐个进行复制。
for (i = 0; i < N; i++)
a[i] = b[i];
上面示例中,通过将数组b的成员逐个复制给数组a,从而实现数组的赋值。
另一种方法是使用memcpy()函数(定义在头文件string.h),直接把数组所在的那一段内存,再复制一份。
memcpy(a, b, sizeof(b)); 上面示例中,将数组b所在的那段内存,复制给数组a。这种方法要比循环复制数组成员要快。
int arr7[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int copy[20] = {};
memcpy(copy, arr7, sizeof(arr7));
- 声明参数数组
数组作为函数的参数,一般会同时传入数组名和数组长度。
int sum_array(int a[], int n) {
// ...
}
int a[] = {3, 5, 7, 3};
int sum = sum_array(a, 4);
上面示例中,函数sum_array()的第一个参数是数组本身,也就是数组名,第二个参数是数组长度。
由于数组名就是一个指针,如果只传数组名,那么函数只知道数组开始的地址,不知道结束的地址,所以才需要把数组长度也一起传入。
- 变长数组
/**
* 变长数组作为函数参数
* 数组a[n]是一个变长数组,它的长度取决于变量n的值,只有运行时才能知道。
* 所以,变量n作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组a[n]的长度,否则就会报错。
*/
int sum_array(int n, int a[n]){
}
int a[] = {3, 5, 7, 3};
int sum = sum_array(4, a);
上面示例中,数组a[n]是一个变长数组,它的长度取决于变量n的值,只有运行时才能知道。所以,变量n作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组a[n]的长度,否则就会报错。
- 变长数组作为函数参数的好处
变长数组作为函数参数有一个好处,就是多维数组的参数声明,可以把后面的维度省掉了。
// 原来的写法
int sum_array(int a[][4], int n);
// 变长数组的写法
int sum_array(int n, int m, int a[n][m]);
上面示例中,函数sum_array()的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度。但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数。
cli.md
$ ./foo hello world
上面示例中,程序foo接收了两个命令行参数hello和world。
程序内部怎么拿到命令行参数呢?C 语言会把命令行输入的内容,放在一个数组里面。main()函数的参数可以接收到这个数组。
#include <stdio.h>
int main(int argc, char* argv[]) {
for (int i = 0; i < argc; i++) {
printf("arg %d: %s\n", i, argv[i]);
}
}
上面示例中,main()函数有两个参数argc(argument count)和argv(argument variable)。这两个参数的名字可以任意取,但是一般来说,约定俗成就是使用这两个词。
第一个参数argc是命令行参数的数量,由于程序名也被计算在内,所以严格地说argc是参数数量 + 1。
第二个参数argv是一个数组,保存了所有的命令行输入,它的每个成员是一个字符串指针。
- 利用argc,可以限定函数只能有多少个参数。
#include <stdio.h>
int main(int argc, char** argv) {
if (argc != 3) {
printf("usage: mult x y\n");
return 1;
}
printf("%d\n", atoi(argv[1]) * atoi(argv[2]));
return 0;
}
上面示例中,argc不等于3就会报错,这样就限定了程序必须有两个参数,才能运行。 另外,argv数组的最后一个成员是 NULL 指针(argv[argc] == NULL)。所以,参数的遍历也可以写成下面这样。
for (char** p = argv; *p != NULL; p++) {
printf("arg: %s\n", *p);
}
上面示例中,指针p依次移动,指向argv的每个成员,一旦移到空指针 NULL,就表示遍历结束。由于argv的地址是固定的,不能执行自增运算(argv++),所以必须通过一个中间变量p,完成遍历操作。
enum
如果一种数据类型的取值只有少数几种可能,并且每种取值都有自己的含义,为了提高代码的可读性,可以将它们定义为 Enum 类型,中文名为枚举。
enum colors {RED, GREEN, BLUE};
printf("%d\n", RED); // 0
printf("%d\n", GREEN); // 1
printf("%d\n", BLUE); // 2
上面示例中,假定程序里面需要三种颜色,就可以使用enum命令,把这三种颜色定义成一种枚举类型colors,它只有三种取值可能RED、GREEN、BLUE。这时,这三个名字自动成为整数常量,编译器默认将它们的值设为数字0、1、2。相比之下,RED要比0的可读性好了许多。
- typedef 命令可以为 Enum 类型起别名。
typedef enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} RESOURCE;
RESOURCE r;
- 声明 Enum 类型时,在同一行里面为变量赋值。
enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} r = BRICK, s = WOOD;
- Enum 常量可以是不连续的值。
enum { X = 2, Y = 18, Z = -2 };
- Enum 常量也可以是同一个值。
enum { X = 2, Y = 2, Z = 2 };
- 如果一组常量之中,有些指定了值,有些没有指定。那么,没有指定值的常量会从上一个指定了值的常量,开始自动递增赋值。
enum {
A, // 0
B, // 1
C = 4, // 4
D, // 5
E, // 6
F = 3, // 3
G, // 4
H // 5
};
文件操作 file
-
fgetc(FILE* fp)
-
标准流
Linux 系统默认提供三个已经打开的文件,它们的文件指针如下。
- `stdin`(标准输入):默认来源为键盘,文件指针编号为`0`。
- `stdout`(标准输出):默认目的地为显示器,文件指针编号为`1`。
- `stderr`(标准错误):默认目的地为显示器,文件指针编号为`2`。
- fclose()
fclose()用来关闭已经使用fopen()打开的文件。它的原型定义在stdio.h。
int fclose(FILE* stream);
if (fclose(fp) != 0)
printf("Something wrong.");
它接受一个文件指针fp作为参数。如果成功关闭文件,fclose()函数返回整数0;如果操作失败(比如磁盘已满,或者出现 I/O 错误),则返回一个特殊值 EOF(详见下一小节)。
- EOF
C 语言的文件操作函数的设计是,如果遇到文件结尾,就返回一个特殊值。程序接收到这个特殊值,就知道已经到达文件结尾了。 头文件stdio.h为这个特殊值定义了一个宏EOF(end of file 的缩写),它的值一般是-1。这是因为从文件读取的二进制值,不管作为无符号数字解释,还是作为 ASCII 码解释,都不可能是负值,所以可以很安全地返回-1,不会跟文件本身的数据相冲突。
需要注意的是,不像字符串结尾真的存储了\0这个值,EOF并不存储在文件结尾,文件中并不存在这个值,完全是文件操作函数发现到达了文件结尾,而返回这个值。
- freopen freopen()用于新打开一个文件,直接关联到某个已经打开的文件指针。这样可以复用文件指针。它的原型定义在头文件stdio.h。
FILE* freopen(char* filename, char* mode, FILE* stream);
它跟fopen()相比,就是多出了第三个参数,表示要复用的文件指针。其他两个参数都一样,分别是文件名和打开模式。
freopen("output.txt", "w", stdout);
printf("hello");
上面示例将文件output.txt关联到stdout,此后向stdout写入的内容,都会写入output.txt。由于printf()默认就是输出到stdout,所以运行上面的代码以后,文件output.txt会被写入hello。
freopen()的返回值是它的第三个参数(文件指针)。如果打开失败(比如文件不存在),会返回空指针 NULL。
freopen()会自动关闭原先已经打开的文件,如果文件指针并没有指向已经打开的文件,则freopen()等同于fopen()。
- fgetc()、getc()
fgetc()和getc()用于从文件读取一个字符。它们的用法跟getchar()类似,区别是getchar()只用来从stdin读取,而这两个函数是从任意指定的文件读取。它们的原型定义在头文件stdio.h。
int fgetc(FILE *stream)
int getc(FILE *stream);
- fputc(), putc()
fputc()和putc()用于向文件写入一个字符。它们的用法跟putchar()类似,区别是putchar()是向stdout写入,而这两个函数是向文件写入。它们的原型定义在头文件stdio.h。
int fputc(int char, FILE *stream);
int putc(int char, FILE *stream);