C 语言教程笔记

53 阅读25分钟

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
  • 其他函数

image.png

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 的区别

image.png

  • 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_SUCCESSEXIT_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 在遇到空终止符时停止比较。
返回值:

memcmpstrcmp 都返回一个整数,表示比较结果,但 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 的区别

image.png

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);