全面、详细、通俗易懂的C语言语法和标准库

系列文章目录

一、基于WSL2和Clion搭建Win下C开发环境
二、make、makeFile、CMake、CMakeLists的使用
三、全面、详细、通俗易懂的C语言语法和标准库

前言

本文并不是一篇C语言入门文章,它包含几乎所有C99语法及常用标准库的基本知识,请放心食用。

变量

在C语言中通过变量来代指内存中一个具体的存储空间。一个变量由以下几部分组成:

  • 变量类型:变量存储值的类型
  • 变量名:变量的名字
  • 变量值:变量存储的值
  • 变量地址:变量的地址

在这里插入图片描述

变量的作用域

作用域指的是变量生效的范围。C 语言的变量作用域有以下两种:

  • 文件作用域:文件作用域指的是,在源码文件顶层声明的变量,从声明的位置到文件结束都有效。
  • 块作用域:
    • 块作用域指的是由大括号{}组成的代码块,它形成一个单独的作用域。凡是在块作用域里面声明的变量,只在当前代码块有效,代码块外部不可见。
    • 代码块可以嵌套,即代码块内部还有代码块,这时就形成了多层的块作用域。它的规则是:内层代码块可以使用外层声明的变量,但外层不可以使用内层声明的变量。如果内层的变量与外层同名,那么会在当前作用域覆盖外层变量。

变量的性质

在C语言中每个变量都具有以下性质:

  • 存储期限:存储期限决定了为变量释放内存空间的时间:
    • 自动存储期限:具有自动存储期限的变量会在所属程序块被执行时获得内存空间,在结束时释放内存空间。
    • 静态存储期限:具有静态存储期限的变量在程序运行的整个期间都会占用内存空间。
    • 动态内存:内存分配在堆上,根据需求自行释放。
  • 链接:链接决定了变量在文件之间的共享范围:
    • 内部链接:具有内部链接的变量只在所属文件内起作用。
    • 外部链接:具有外部链接的变量可以在不同文件内起作用。
    • 无链接:无链接的变量只在所属文件的所属块作用域内起作用。

变量的默认存储期限和链接与变量的作用域有关:

  • 块作用域的变量具有自动存储权限并且无链接。
  • 文件作用域的变量具有静态存储权限和外部链接。

变量的声明和定义

在使用变量之前一定要对其进行定义和声明:

  • 变量的声明:变量的声明用于指定变量的声明说明符和声明符,在程序中一个变量可以被声明多次。
  • 变量的定义:变量的定义用于为变量分配存储空间,在程序中一个变量只能被定义一次。

通常情况下变量的声明和变量的定义是同时发生的,但在使用extern关键字并且在它不失效的情况下,只有变量的声明而没有变量的定义。

声明符和声明说明符

声明符用于指明变量名,声明说明符分为以下三类,它们有各自的作用:

  • 存储类型符:用于指明变量的性质
  • 类型限定符:用于修饰限定变量
  • 类型说明符:用于指明变量的类型

存储类型符

存储类型符在变量声明中最多出现一种,并且必须在声明说明符的最前面。

  • autoauto修饰的变量具有自动存储期限、块作用域并且无连接,它只对块作用域的变量有效,因为对于块作用域的变量它是默认的。
    在这里插入图片描述
  • staticstatic修饰的文件作用域的变量具有静态存储期限、文件作用域和内部链接;static修饰的块作用域的变量具有静态存储期限、块作用域并且无连接。
    在这里插入图片描述
  • externextern用于在当前作用域引入其它文件中定义的变量。extern声明的变量会具有静态存储期限和外部连接并且不会占用内存空间。但如果在引入变量时又对变量进行了初始化,那么extern将失效。
    在这里插入图片描述
  • registerregister的性质和auto完全一致。但使用register声明的变量会请求编译器把它存储在寄存器中,并且由于寄存器没有地址,所以对register声明的变量使用取地址运算符是非法的,但该请求不一定得到应允。

类型限定符

  • constconst修饰的变量是只读的,不能被修改,因此也别成为常量。
  • volatilevolatile关键字通常用于声明指向易变内存空间的指针,它告诉编译器该内存空间的数据是易变的,所以不要对其及逆行优化,并且在每次通过指针取值时都必须从内存中直接获取。
  • restrict:见下文受限指针

类型说明符

基本类型

基本类型包含以下三类:

  • 整数类型:C语言允许使用十进制、八进制和十六进制书写整数类型值,其中八进制必须以0开头,十六进制必须以0x开头。编译器默认将整数常量当作int型处理,可以通过添加后缀的方式使编译器改变默认处理类型。在不加后缀的情况下,如果int型存不下十进制数,那么编译器会依次尝试、long intlong long int。对于八进制和十六进制,编译器会依次尝试unsigned intlong intunsigned long intlong long intunsigned long long int
类型说明符后缀
short int-
unsigned short int-
int-
unsigned intu
long intl
unsigned long intul
long long intll
unsigned long long intull
  • 浮点类型:编译器默认将浮点常量当作double型处理,可以通过添加后缀的方式使编译器改变默认处理类型。
类型说明符后缀
floatf
double-
long doublel
  • 字符类型:编译器将字符类型当作小整数类型处理。
类型说明符后缀
char-
自定义类型

自定义类型包含以下几种,主体内容在下文:

  • 结构
  • 联合
  • 枚举
扩展类型

扩展类型就是从基本类型和自定义类型或扩展类型扩展的类型,当一个变量的类型说明符中出现*[]时就说明该变量是一个扩展类型,其中:

  • *表示该变量是一个指针变量,它存储着一个指针,扩展自的类型表示指针指向内存存储值的类型。
  • []表示该变量是一个数组变量,是一片连续的存储空间,它存储着指向数组第一个元素的指针,扩展自的类型表示连续存储空间中存储值的类型。

很多时候类型说明符中也会出现(),它的作用是区别优先级,它们三者的优先级如下:
( ) > [ ] > ∗ ()>[]>* ()>[]>

正确认识变量

对于一眼看不出来的变量声明,可以使用以下方法进行分析:

  • 以标识符为中心由内往外读声明符。
  • 在做选择时,优先级始终是()大于[]大于*

下面举几个例子:

int (*funPtr)(int,int);//先是一个指针--指向了一个函数
int (*funPtrArr[5])(int,int);//先是一个数组--后存储了指针--指针指向一个函数
int (*(*funPtrArrPtr)[2])(int,int);//先是一个指针--指针指向一个数组--数组存储着指针--指针指向一个函数

不完整类型

不完整类型是在编写C语言大型程序时极其重要工具,C语言对不完整类型的描述是:描述了变量,但缺少定义变量大小所需要的信息。不完整类型将会在程序的其它地方将信息补充完整。这就起到了很好的类型封装作用。由于编译器不知道不完整类型的大小,所以它的使用是受限的:

  • 不能使用不完整类型来定义变量,但可以定义一个指向不完整类型的指针。
  • 不能对不完整类型使用sizeof运算符。
  • 数组、结构和联合的成员不可以具有不完整类型。
  • 函数的形式参数不可以具有不完整类型。

变量的初始化和赋值

在使用变量前不仅要对其进行定义和声明,还要对其进行初始化和赋值:

  • 变量的初始化:变量的初始化发生在变量定义时且只会被执行一次,它分为自动初始化和手动初始化。
    • 自动初始化:只有具有静态存储期限的变量在定义时会基于类型自动初始化零值。
    • 手动初始化:具有静态存储期限的变量的手动初始化值必须是常量。
  • 变量的赋值:变量的赋值发生在变量定义之后。

表达式和运算符

表达式是表示如何计算的公式,任何一个表达式后加上一个分号就变成了一个语句。一些特殊的表达式如下:

  • 常量表达式:不能包含变量和函数调用的表达式,常用于case语句后。
  • 逗号表达式:可以使用逗号将多个表达式分隔而组成一个新的表达式,这个新表达式的值为最后一个子表达式的值,其它子表达式的值都将被抛弃。

左值和右值

左值表示一个内存空间,不能是常量或者表达式的计算结果。左值以外的值都是右值。赋值运算符要求它的左操作数必须是左值。

表达式中的类型转换

在C语言中类型转换分为两种:第一种是编译器自己就能处理的隐式转换,第二种是使用强制运算符的显示转换。

隐式转换

当发生下列情况会发生隐式转换:

  • 算术表达式或逻辑表达式中操作数的类型不相同:此时会将操作数转换成适用于两个数值的最小类型:
    • 当任意操作数为浮点类型时,会按照floatdoublelong double的顺序转换。
    • 当两个操作数都不是浮点类型时,首先将两个操作数中能转换为int型的转换为int型,如果此时两个操作数的类型相同,过程结束。否则依次尝试以下规则:
      • 如果两个操作数都是有符号数或无符号数,将类型小的操作数转换为类型大的操作数的同类型;
      • 如果无符号数的存储范围大于等于有符号数的存储范围,则将有符号操作数转换为无符号操作数的类型;
      • 如果有符号数可以表示所有无符号数,则将无符号操作数转换为有符号操作数的类型。
  • 赋值运算符右侧表达式的类型和左侧变量的类型不匹配:唯一的转换原则是把赋值运算右边的表达式转换成左边变量的类型。
  • 函数调用时实参和形参类型不匹配:如何转化实际参数的规则与编译器是否在调用前遇到函数的原型有关:
    • 如果编译器在调用前遇见函数原型,那么就进行上文第一种的转换;
    • 如果编译器在调用前没有遇见函数原型,那么就会把float类型实参转换为double类型,把charshort int类型实参转化为int型。
  • return语句中表达式类型和函数返回值类型不匹配:进行上文第一种的转换。

强制转换

强制类型转换使用强制转换表达式进行转化,强制类型转换,当将大类型数据转换为小类型数据时会发生数据丢失。

(type)expression

typedef运算符

typedef可以用来定义数据类型,typedef定义的类型会被编译器加入到它所识别的类型名列表。

typedef <primitiveType> <newType>

sizeof运算符

sizeof表达式用于计算存储类型、常量、变量和表达式值的字节数,它的值一个size_t类型的值,这是一种无符号整数类型,所以在使用sizeof表达式时最好将它的值转换为unsigned long类型。

sizeof(value)

预处理器

C程序在编译之前会先使用预处理器处理代码。预处理器首先会清理代码,进行删除注释、多行语句合成一个逻辑行等工作。然后执行预处理指令。预处理指令以#号开头默认在行尾结束,如果需要在下一行延续指令,那么必须在当前行的末尾使用\进行换行,它可以出现在源文件的任何地方,且不需要以分号结尾。根据指令的作用可以将预处理指令分为宏定义指令、条件编译指令、文件包含指令以及其它指令。

宏定义

宏定义的格式如下,宏的替换列表也可以包含对其它宏的调用。一个宏不可以被定义两遍,除非新的定义和旧的定义是一样的。当预处理器遇见宏定义时,会将文件中的宏(标识符)全部替换为替换列表。

#define 标识符 替换列表
#undef 标识符

带参数的宏

宏定义也可以带有参数,参数列表也可以为空,参数没有类型,也没有类型检查。标识符和左括号之间必须没有空格,如果有空格,预处理器会将括号右边的内容全部视为替换列表。如果在替换列表中使用了参数,那么每个参数都应该放在括号中,这么做可以保证替换前后的语义一致。

#define  标识符(x1,x2,x3...)   替换列表

在调用宏时可以少传任意数量的参数,但实参列表必须要有和全参调用一样多的逗号。宏也支持可变参数列表,语法和函数相同,__VA_ARGS__是一个专门的标识符,只能出现在具有可变参数列表的宏的替换列表中,代表所有与省略号相对应的参数。

多表达式、多语句的宏

当需要在宏定义中包含多个表达式时,可以使用逗号运算符进行分隔:

#define 标识符 (expr1,expr2)

当需要在宏定义中包含多条语句时,可以将语句放在do-while循环中,并将条件设置为假:

#define 标识符       \
	do{             \
		expr1; 	\
		expr2;  	\
	}while(0)

宏中的运算符

#运算符将宏的一个参数转换为字符串字面量。如果空参数成为该运算符的操作数,那么运算结果将是一个空串。

#define input(a,b) scanf(#a"is%d" #b"is%d",&a,&b)

##运算符可以将两个记号粘合在一起成为一个记号。如果该运算符之后的一个参数为空,那么它将被不可见的位置标记代替,

#define same(a) (i##a)
int same(1),same(2),same(3);//等价于 int i1,i2,i3;

预定义的宏

C语言中一些常用的预定义宏如下:

名称说明
__LINE __当前程序行的行号,表示为整型常量
__FILE __当前源文件名,表示字符串型常量
__DATE__编译程序的日期,表示为mm dd yy 形式的字符串常量
__TIME __编译程序的时间 ,表示hh:mm:ss形式的字符串型常量
__STDC __如果编译器符合C标准,那么它的值为1
__STDC_VERSION __支持的C标准版本

条件编译

条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片段的行为。

#if…#endif

当编译器遇见#if命令时,会计算常量表达式的值,如果常量表达式的值为假,那么它们之间的程序段将在预处理时从源代码中删除,否则就保留在程序中。值得注意的是#if命令会把没有定义的标识符当作值为零的宏对待。

#if 常量表达式
	...
	#elif 常量表达式
		...
	#else
		...
#endif

#ifdef/#ifndef…#endif

#ifdef#ifndef命令会判断一个标识符是否是一个定义过的宏。

#ifdef 标识符
	...
	#else
	...
#endif

#ifndef 标识符
	...
	#else
	...
#endif

defined

defined是一个预处理运算符,如果它的参数是一个定义过的宏,就会返回1,否则返回0。因此#ifdef#ifndef指令可以通过以下形式代替:

#if defined 标识符
#if !defined 标识符

文件包含

多数情况下一个程序要分为多个源文件,其中一个源文件必须包含一个main函数,这个main函数会被启动代码调用,而启动代码是由编译器添加到程序中的,是程序和操作系统之间的桥梁。

头文件

为了在多个源文件之间共享信息,可以把这些信息放到一个单独的文件中,然后使用#include命令把该文件的内容带到每个源文件中,把按照这种方式包含的文件称为头文件。通常会为每一个.c源文件创建相对应的同名头文件并且在源文件中包含同名头文件,然后再建立一个总的头文件将其它头文件都包含进去。如果要共享一个函数,那么首先将函数定义在一个源文件中,然后在同名头文件中包含这个函数的声明。如果要共享一个变量,那么首先将变量声明在一个源文件中,然后在同名头文件中使用extern关键字进行声明,extern关键字会告诉编译器该变量是在程序中的其它位置定义的,因此不需要为它分配内存空间,在数组的声明时可以省略数组的长度。

#include

#include命令告诉预处理器打开指定的文件,并把这些文件的内容插入到当前文件中。该命令有三种格式:

  • 第一种格式用于包含C库中的头文件,预处理器在执行此指令时直接搜索系统头文件所在的目录;
  • 第二种格式用于包含自己编写的头文件,预处理器在执行此文件时先搜索当前目录,然后再搜索系统头文件所在的目录;
  • 第三种格式中的记号就是宏定义的标识符,也就是将头文件名用宏定义一个记号。
#include <文件名>
#include "文件名"
#include 记号

有时一个源文件可能重复包含相同的头文件,这可能会产生编译错误,如果有一个名为test的头文件那么可以在该文件中通过条件编译来解决问题:

#ifndef TEST_H
#define TEST_H
	...
#endif

其他指令

#error

#error可以让预处理器抛出一个错误,终止编译。

#error 消息

#line

#line指令可以使程序行从n开始编号,且该指令行不参与编号:

#line n //n为整数

它还可以带一个文件名字符串,那么指令后边的行会被认为来自该文件:

#line n fileName//n为整数

数组

数组变量代表内存中一片连续的存储空间,其中存储着相同类型的元素,在定义数组时,必须给出数组的大小:

int arr[10];//每有一个维度就增加一个括号

可以通过{}初始化数组,初始化时可以按照数组元素定义的顺序提供值,也可以直接在{}罗列出数组的元素值。并且提供的元素值可以少于数组元素的数量,但是不能为空,没有提供的元素会用零作为初始值,如果在定义数组变量时同时进行初始化,可以省略数组长度(多维数组只能省略第一维的长度)。

int arr[5]={1};//[1,0,0,0,0]
int arr[]={[1]=1,[5]=5};//[0,1,0,0,0,5]

数组变量本质是指向数组第一维元素的指针,并且编译器一旦为数组变量分配地址,这个地址就绑定这个数组变量了,这种绑定关系是不变的。因此数组变量不能作为左值出现,所以只能通过索引的方式为数组变量赋值:

arr[3]=3;

C语言还支持变长数组,变长数组的长度在程序执行时计算而不是在程序编译时计算,长度变量不能是一个被const修饰的具有静态存取期限的变量。

int length;
int arr[length];

变长数组的限制在于它没有静态存储期限并且不能初始化式。

字符串

字符串是用双引号括起来的字符序列,并以一个空字符\0来标识字符串的结束。C语言把字符串作为字符数组处理,当编译器在程序中遇到长度为n的字符串时,它会为字符串分配长度为n+1的内存空间,这块空间将用于存储字符串中的字符以及一个用来标志字符串末尾的空字符。

字符串常量

使用字符指针初始化的字符串称为字符串常量,字符串常量是不可改变的,原因是系统会将字符串常量保存在内存的常量区,这个区是不允许用户修改的,因此在声明字符串常量时可以习惯性的加上const

const char *ch="abcdef";

如果一行写不开可以在第一行以\结尾,第二行顶格写完:

const char *ch="abc\
def";

或者将它们分别用双引号引起来仅以空白字符分割,编译器会自动把它们打包成一个字符串常量:

const char *ch="abc" "def";

字符串变量

使用字符数组初始化的字符串称为字符串变量,字符串变量可以修改。在初始化的时候字符数组的长度应该比字符串的长度长一,也可以不指定数组长度。编译器会自动追加空字符来标识结尾。

char ch[]="abcdef";

<string.h>

<string.h>头文件提供了一些字符串处理函数。

//字符串复制
//将src中前n个字符复制到dest,并返回dest。
char * strcpy(char * restrict dest,const char * restrict src);
char * strncpy(char * restrict dest,const char * restrict src,size_t n); 
//字符串拼接
//将src中前n个字符拼接到dest的末尾,并返回dest。
char * strcat(char * restrict dest,const char * restrict src);
char * strncat(char * restrict dest,const char * restrict src,size_t n);
//字符串比较
//将s1和s2进行对比
int strcmp(const char * s1,const char * s2);
int strncmp(const char * s1,const char * s2,size_t n);
//字符串搜索
//在s中正向或反向搜索字符c,返回一个指向s中第一个c的指针
char * strchr(const char * s,int c);
char * strrchr(const char * s,int c);
//返回一个指向s1中任意一个字符匹配的最左边的一个字符
char * strpbrk(const char * s1,const char * s2);
//返回s1中第一个属于或不属于s2的字符的下标
size_t strspn(const char * s1,const char * s2);
size_t strcspn(const char * s1,const char * s2);
//返回一个指向s1中第一次出现s2的指针
char * strstr(const char * s1,const char * s2);
//返回s的长度
size_t strlen(const char *s);

结构

结构的成员可以类型不同,并且每个成员都有自己的名字,选择结构成员时需要指定成员的名字。结构的成员在内存中按顺序存储。声明结构和定义结构变量有以下几种形式,常用的形式是第三种。

struct {
	char * name;
	int age;
}person;
typedef struct {
	char * name;
	int age;
}Person;

Person person;
struct Person{
	char * name;
	int age;
};

struct Person person;

结构变量初始化时按照成员定义的顺序提供值,但是提供的成员值可以少于结构成员的数量,剩下的成员会用零作为初始值。

struct Person person={.name="eric",.age=18};
struct Person person={"eric",18};

一个结构变量只能通过另一个结构变量进行赋值,这时会生成一个全新的副本。系统会分配一块新的内存空间,大小与原来的变量相同,把每个属性都复制过去,即原样生成了一份数据:

struct Person person1, person2 = {
        "eric",
        18
};
person1 = person2;

空位对齐

结构变量占用的存储空间,不是各个属性存储空间的总和,而是最大内存占用属性的存储空间的倍数,其他属性会添加空位(即不使用的字节)与之对齐。这样可以提高读写效率。由于这个特性,在有必要的情况下,定义结构体时,可以采用存储空间递增的顺序,定义每个属性,这样就能节省一些空间。这也导致了结构变量不能作为左值出现,因为即使结构变量对应,结构中的空位也不一定对应。所以在赋值时只能通过结构成员进行:

person.name="eric";
person.age=18;

结构中的位域

可以通过以下形式给结构成员指定它们要占用的bit位:

struct BitFields{
	unsigned int field1:2;
	unsigned int       :2;
	unsigned int field2:2;
};

那么就将这样的成员称为位域,位域即一片在逻辑上连续的bit位,它在物理内存上连不连续由具体的实现决定,所以位域通常是没有地址的,C语言也禁止将取址运算符作用于位域。结构中位域的类型必须是intunsigned int,signed int ,但是最好是指明位域有无符号性,以免产生二义性。C语言还允许省略位域的名字,未命名的位域常作为位域间的填充,以保证其它位域存储在适当位置。

灵活数组成员

当结构的最后一个成员是数组时,其长度是可以忽略的,那么这种结构成员就称为灵活数组成员。灵活数组成员是一种特殊的不完整类型,它的特殊之处在于:

  • 灵活数组成员必须出现在结构的最后,而且结构必须至少还有一个其它成员。
  • 复制包含灵活数组的结构时,其它结构成员都会被复制,但不会复制灵活数组本身。
  • 在使用sizeof运算符来确定结构的字节数量时会忽略灵活数组的大小。
  • 可以使用包含灵活数组的结构定义变量。
  • 包含灵活数组的结构可以作为函数参数。

灵活数组成员的意义在于可以动态分配结构的内存大小。

联合

联合和结构类似,它内部可以包含各种成员,但编译器只会给联合分配能够存下联合中最大成员的内存空间,所有联合成员共用这一片内存空间,因此联合成员在这内存空间内会相互影响彼此覆盖。联合的定义、初始化和赋值和结构相同,唯一的不同在于在进行联合变量的初始化时只能初始化第一个成员。

枚举

枚举是一种由枚举常量组成的结构,在声明枚举时必须为每个常量命名:

enum {
	PASS,
	FAILED
}status;
typedef enum {
	PASS,
	FAILED
}Status;

Status status;
enum Status{
	PASS,
	FAILED
};

enum Status status;

C语言会把枚举常量当作整数处理,默认情况下,编译器会把0,1,2,...赋值给枚举中的常量,也可以在声明时手动赋值,当没有为枚举常量指定值时,它的值默认比前一个常量值大一。

enum Status{
	PASS=1,
	FAILED=0
};

在为枚举变量初始化或赋值时,它的值只能是枚举常量:

Status status=PASS;

函数

函数的声明和定义

在调用一个函数之前必须对其进行声明或定义:

int sum(int a,int b){
	return a+b;
}

在声明和定义一个函数时有以下几点要注意:

  • 可以使用staticextern修饰函数,使用static声明和定义的函数具有内部连接,使用extern声明和定义的函数具有外部连接,默认情况下函数具有外部连接。
  • 函数的形参具有和块作用域变量一样的默认性质。可以使用const修饰参数变量,表示函数内部不得修改该参数变量。
  • 函数的返回类型不能是数组类型。

函数的原型

函数原型是指每个函数的返回类型、函数名和参数类型。其他信息都不需要,也不用包括函数体,具体的函数实现可以后面再补上。通过使用函数原型,就可以避免将所有函数都定义在main函数之前,一般来说,每个源码文件的头部,都会给出当前脚本使用的所有函数的原型。

int fun (int,int);

函数参数的传值方式

C语言中所有函数的参数都是按值传递的。并且数组类型的形参并不代表一个数组变量,因为当数组类型作为实参传递时,实际传递的是数组第一个元素的地址而不是数组的副本。因此sizeof运算符不能通过数组形参计算出数组的长度。所以当函数包含数组类型的形参时,最好在包含一个指示数组长度的形参。当变长数组作为形参时,这一要求就是硬性的,并且最好使用以下形式定义函数:

void fun(int length,int arr[*]){
	...
}

可变参数列表和<stdarg.h>

C语言允许函数据有可变参数列表,它的定义方式如下:

int fun(int a,...){
	...
}

可变参数列列表必须有一个固定形参,并且...必须是形参列表的最后一个。<stdarg.h>头文件提供了处理函数可变形参的方法。

typedef char* va_list;
void va_start(va_list ap,可变列表前的参数);
类型 va_arg(va_list ap,类型);
void va_copy(va_list dest,va_list src);
void va_end(va_list ap);

其中va_list是可变参数列表,所有非固定形参都存储在其中。va_start会指定可变参数列表的起始位置,va_arg会依次返回可变参数列表的每一个参数,并且可以指定我们希望的参数类型。va_copy用于将src中剩下的参数复制给destva_end用于清理资源,va_startva_copy的使用必须和va_end成对出现。

__func__

每一个函数都可以访问__func__标识符,它的行为像一个存储当前正在执行的函数名称的字符串变量。

指针

上文提到,每个变量都有一个内存地址,在C语言中,指针用来表示变量的地址。

指针变量

指针变量就是存储指针的变量。C语言要求指针变量只能存储一种指向内存中特定类型变量的指针,指针变量的声明与普通变量基本一致,唯一的不同就是要在指针变量前加一个星号。指针变量的大小与它的类型无关,只与操作系统的寻址位数相关,如果是64位操作系统,那么指针变量的大小将为8字节。

int * p;

指向指针的指针

指针变量也是有地址的,那么把指针变量的地址称为指向指针的指针,存储指向指针的指针的指针变量的定义方式如下:

int ** p;

以此类推通过增加星号的个数就可以定义指向指向指针的指针的指针变量。

取地址运算符和间接寻址运算符

取地址运算符&可以将某个变量的地址赋值给一个指针变量:

int a,*p;
p=&a;

一旦指针变量指向了变量,那么就可以通过间接寻址运算符*访问指针指向的变量了:

printf("%d",*p);

空指针

空指针是不指向任何地方的指针,用宏NULL来表示,在C语言中所有非空指针都为真,只有空指针为假。在声明指针变量时,最好使用空指针进行初始化:

int *p=NULL;

通用指针

void * 类型的指针变量存储的指针是通用指针,本质上它只是内存地址。通用指针可以被赋值给任何类型的指针变量。通用指针变量也可以被任何类型的指针赋值。

受限指针

使用restrict关键字声明的指针叫做受限指针,如果受限指针指向的变量在之后需要修改,那么该变量不允许通过除该指针之外的任何方式访问。restrict是一种对编译器优化的建议,有没有restrict程序的行为不会发生变化。

内存的动态分配与释放

内存的动态分配需要使用<stdlib.h>头中的函数:

//分配内存块,但不对内存块进行初始化。
//size:字节数
void * malloc(size_t size);
//分配内存块,并对内存块进行零值初始化。
//nmemb:元素个数
//size:元素大小
void * calloc(size_t nmemb,size_t size);
//调整先前分配内存块的大小。
//ptr:指向来自这三个函数分配的内存空间
//size:新内存空间的大小
//一但该函数返回,必须对指向原内存块的指针进行更新,因为该函数可能将内存块移动到了其他地方。
void * realloc(void * ptr,size_t size);

内存分配函数分配的内存块全部来自一个称为堆的存储池,程序可能分配了内存块但丢失了指向这些内存块的指针,这就会造成内存泄漏现象,因此在使用完内存之后就必须使用free函数进行手动释放:

//ptr:指向来自这三个函数分配的内存空间
void free(void * ptr);

原本指向被free函数释放的内存空间的指针就变成了悬空指针,它们不会指向任何地方。在<string.h>头文件还有一些有关内存操作的函数:

//将src中前n个字节拷贝到dest中,并返回dest,如果src和dest存在内存重叠,那么结果将是未定义的
void * memcpy(void * restrict dest,const void * restrict src,size_t n);
//将将src中前n个字节拷贝到dest中,并返回dest,不受内存重叠影响
void * memmove(void * dest,const void * src,size_t n);
//比较s1和s2前n个字节的大小
int memcmp(const void * s1,const void * s2,size_t n);
//返回一个指向s中第一次出现字节c的指针
void * memchr(const void * s,int c,size_t n);
//将s中前n个字节设置为c
void *memset(void *s,int c,size_t n);

指针与数组

当指针指向数组的元素时,就可以对指针进行算数和逻辑运算。

  • 指针加上整数:如果指针加上一个整数i,那么指针将向前移动i个单位。
  • 指针减去整数:如果指针减去一个整数i,那么指针将向后移动i个单位。
  • 两个指针相减:结果为两个指针之间的距离。
  • 两个指针比较:比较的是各自的内存地址哪一个更大。

一定要注意,这里提到的单位与指针的类型相关,请看下面的例子:

#include <stdio.h>

void distance(const void *, const void *);

int main() {
    int arr[][3] = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
    };
    int *p1 = &arr[0][0];
    int (*p2)[3] = &arr[0];
    int (*p3)[3][3] = &arr;
    distance(p1, p1 + 1);//1
    distance(p2, p2 + 1);//3
    distance(p3, p3 + 1);//9
    printf("%d\n", *p1);//1
    printf("%d\n", **p2);//1
    printf("%d\n", ***p3);//1

    printf("--p2行遍历--\n");
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("%d,", *(*(p2 + i) + j));
        }
        printf("\n");
    }
    /**
     * --p2行遍历--
     * 1,2,3,
     * 4,5,6,
     * 7,8,9,
     */
    printf("--p3行遍历--\n");
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("%d,", *(*(*p3 + i) + j));
        }
        printf("\n");
    }
    /**
     * --p3行遍历--
     * 1,2,3,
     * 4,5,6,
     * 7,8,9,
     */
    printf("--p2列遍历--\n");
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("%d,", *(*(p2 + j) + i));
        }
        printf("\n");
    }
    /**
     * --p2列遍历--
     * 1,4,7,
     * 2,5,8,
     * 3,6,9,
     */
    printf("--p3列遍历--\n");
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("%d,", *(*(*p3 + j) + i));
        }
        printf("\n");
    }
    /**
     * --p3列遍历--
     * 1,4,7,
     * 2,5,8,
     * 3,6,9,
     */
    return 0;
}

void distance(const void *a, const void *b) {
    printf("%ld\n", (b - a) / sizeof(int));
}

指针与结构

当一个指针指向结构变量时,可以使用右箭头选择操作符访问结构变量中的成员:

struct test{
	int a;
};
struct test var={1},*ptr;
ptr=&var;

那么下面两个表达式是一样的:

(*ptr).a
ptr->a

结构变量在嵌套自身时只能定义一个指针类型数据,因为只有指针变量的大小编译器是已知的,始终为计算机的字长,并且此结构在声明时必须使用结构标记的方式。

指针与函数

每一个函数都有一个地址,那么把函数的地址称为指向函数的指针,存储指向函数的指针的指针变量的定义方式如下:

void (*p)(int);

通过以下方式给指针变量赋值,这是因为当函数名后面没有括号时,编译器会产生一个指向该函数的指针,而不会产生函数调用的代码。

p=fun;

在使用指针变量调用函数时,可以通过以下两种方式:

(*p)(3);
p(3);

输入输出<stdio.h>

在C语言中术语流表示任意输入的源或任意输出的目的地。流可以是键盘、鼠标、屏幕、硬盘以及网卡等。在Linux中万物皆文件,所以也可以说流就是文件,因此在C语言中对流的访问都是通过文件指针FILE *实现的。<stdio.h>头文件定义了三个标准流,这些流可以直接使用,不需要对其进行声明、打开或关闭。

typedef struct xxx FILE;

//文件末尾标志
#define EOF (-1)

//缓冲区模式
//当缓冲区为空时,从流读入数据,当缓冲区满时,向流写入数据。对于没有交互式设备相连的流来说是默认的
#define _IOFBF 0x0000 
//每次从流读入一行数据或向流写入一行数据
#define _IOLBF 0x0040
//直接从流读入数据或直接向流写入数据
#define _IONBF 0x0004

//偏移基准
//文件的当前位置
#define SEEK_CUR    1
//文件的结束处
#define SEEK_END    2
//文件的起始处
#define SEEK_SET    0

FILE* __acrt_iob_func(unsigned _Ix);
//标准输入流,默认是键盘
#define stdin  (__acrt_iob_func(0))
//标准输出流,默认是屏幕
#define stdout (__acrt_iob_func(1))
//标准错误流,默认是屏幕
#define stderr (__acrt_iob_func(2))

文件操作

<stdio.h>头文件支持操作字符文件和二进制文件,本质上说所有文件都是二进制文件,只是字符文件中字节表示一个字符而二进制文件中单个字节没有什么意义。文件都存放在磁盘中,每次读写文件都直接访问磁盘是非常消耗系统资源的,因此就需要使用内存充当缓冲区来进行优化。以写文件为例:把输入流的数据写入到缓冲区,当缓冲区满了或关闭流时缓冲区就会自动将数据刷新到磁盘中。每个流都有与之相关的错误指示器和文件末尾指示器,当打开流时就会重置这些指示器,当遇见错误或文件末尾时就会设置这些指示器,一旦设置了指示器,它就会保持这种状态直到显式清除。每个流都有一个相关联的文件位置,当打开文件时,会将文件位置设置在文件开头或结尾(追加方式打开),在执行读写操作时文件位置会自动推进。

//打开文件,其中mode表示要对文件采取的操作,
//rb:打开文件用于读;wb:打开文件用于写(文件不需要存在);ab:打开文件用于追加写(文件不需要存在);
//rb+:打开文件用于读和覆盖写;wb+:打开文件用于读和覆盖写(文件不需要存在);ab+:打开文件用于读和追加写(文件不需要存在);
//将上述模式中的b去掉二进制文件的模式
//对于含有+的模式而言,如果没有调用文件定位函数就不能从读膜室切换为写模式
//对于含有+的模式而言,如果没有调用文件定位函数或fflush函数就不能从写膜室切换为读模式
FILE* fopen(const char * restrict fileName,const char * restrict mode);
//关闭文件,失败返回EOF
int fclose(FILE* stream);
//为已经打开的流附加上一个文件,返回stream
FILE* freopen(const char * restrict fileName,const char * restrict mode,FILE* stream);

//创建一个临时文件并用wb+模式打开,当关闭文件或结束程序时消失
FILE* tmpfile();
//产生临时文件名,并保存到s中,如果s是空指针,那么就保存到一个静态变量中并返回指向这个变量的指针
char *tmpnam(char * s);

//刷新stream的缓冲区
int fflush(FILE* stream);
//用于设置缓冲区
//buffer:指定缓冲区的地址
//mode:缓冲区的模式
//size:缓冲区的大小
void setbuf(FILE * restrict stream,char * restrict buffer);
int setvbuf(FILE * restrict stream,char * restrict buffer,int mode,size_t size);

//删除文件
int remove(const char * fileName);
//修改文件名
int rename(const char * oldFileName,const char * newFileName);

int feof(FILE * stream);
int ferror(FILE * stream);
//clearerr方法是显式清除这两种指示器的方式之一
void clearerr(FILE * stream);

//ftell返回当前的文件位置
long ftell(FILE * stream);
//将文件位置设置到文件起始处
void rewind(FILE * stream);
//以origin为基准,将文件位置偏移offset
int fseek(FILE * stream,long offset,int origin);
//是一个有符号的整数类型
typedef xxx fpos_t;
//将文件位置存储到position中
int fgetpos(FILE * stream,fpos_t * position);
//设置文件的位置为position
int fsetpos(FILE * stream,fpos_t const * position);

格式化输入输出

//向标准流输入输出
int printf(const char * restrict format,...);
int scanf(const char * restrict format,...);
//向指定流输入输出
int fprintf(FILE * restrict stream,const char * restrict format,...);
int fscanf(FILE * restrict stream,const char * restrict format,...);

printf函数的格式串可以包含普通字符和转换说明,普通字符会原样输出,转换说明描述了如何把剩余的实参转换为字符串格式显示出来。转换说明符由百分号开头和跟随其后的最多五个部分组成:

% 标志 最小字段宽度 精度 长度修饰符 转换说明符
  • 标志及其作用如下:
标志作用
-在字段内左对齐,默认是右对齐
+有符号转换得到的数以+-开头
空格有符号转换得到的非负数前面加空格
#以八进制数、十六进制非零数以及浮点数始终有小数点,不能删除由g或G转换输出的数的小数点
0用前导0在数的字段宽度内进行填充,如果转换是dioux,并且指定了精度,那么可以忽略该标志
  • 最小字段宽度:如果数据项太小无法达到这一宽度,那么会对字段进行填充,默认会在数据项左侧添加空格使其右对齐。字段宽度可以是整数也可以是字符*,如果是字符*那么字段宽度由下一个参数决定。
  • 精度:精度的含义依赖于转换说明符,如果转换说明符是dioux、,那么精度表示最小位数;如果是aef、那么精度表示小数点后的位数;如果是、g,那么精度表示有效数字的个数,如果是s,那么精度表示最大字节数。精度是由小数点后跟一个整数或者字符*构成的,如果出现字符*那么精度由下一个参数决定。
  • 长度修饰符:长度修饰符表明待显示的数据项类型的长度大于或小于特定转换说明中的正常值。
长度修饰符转换说明符含义
hhdiouxsigned char、unsigned char
nsigned char *
hdiouxshort int、unsigned short int
nshort int *
ldiouxlong int、unsigned long int
nlong int *
cwint_t
swchar_t
aefg无作用
lldiouxlong long int、unsigned long long int
nlong long int *
jdiouxintmax_t、uintmax_t
nintmax_t *
zdiouxsize_t
nsize_t*
tdiouxptrdiff_t
nptrdiff_t
Laefglong double
  • 转换说明符:
转换说明符说明
d、iint类型值转换为十进制形式
o、u、x把无符号整数转换为八进制、十进制或十六进制形式
fdouble类型值转换为十进制形式,默认保留小数点后六位
edouble类型值转换为科学计数法形式,默认保留小数点后六位
gdouble类型值转换为f形式或e形式,当数值的指数部分小于-4或大于等于精度值时会选择e形式显式,默认尾部的0不显示,且小数点仅在后边跟有数字时才显示出来。
c显式无符号字符的int类型值
s显式由实参指向的字符
pvoid * 显示为可打印形式
n相应的实参必须是指向int型的指针,在该实参中存储printf函数已经输出的字符数量,本身不显示输出
%写字符%

scanf函数的格式串表示的是scanf函数在读取输入时试图匹配的模式,如果一旦发现输入与格式串不匹配,那么函数就会立即返回,不匹配的数据将会被放回等待下次读取。scanf函数的格式串由以下三部分组成:
`

  • 转换说明:和printf函数的转换说明类似,大多数转换说明都会跳过输入项开始处的空白字符(%[%c%n除外)。
  • 空白字符:scanf函数的格式串中的一个或多个连续的空白字符和输入流中的零个或多个空白字符匹配。
  • 非空白字符:除了%之外的所有非空白字符都必须和输入流中的相同字符匹配。

scanf函数的转换说明符组成部分如下:

% 字符* 最大字段宽度 长度修饰符
  • 字符*:赋值屏蔽,读入此数据项但是不把它赋值给对象,也不包含在函数返回的计数中。
  • 最大字段宽度:限制输入项中的字符数量,如果达到这个最大值,那么此数据项的转换将结束,转换开始处跳过的空白字符不进行统计。
  • 长度修饰符:表明用于存储输入数据项的变量的类型与特定转换说明中的常见类型长度不一致。
长度修饰符转换说明符含义
hhdiouxnsigned char *、unsigned char *
hdiouxnshort int *、unsigned short int *
ldiouxnlong int *、unsigned long int *
aefgdouble *
cs[ ]wchar_t *
lldiouxnlong long int *、unsigned long long int *
jdiouxnintmax_t *、uintmax_t *
zdiouxnsize_t *
tdiouxnptrdiff_t *
Laefglong double *
  • 转换说明符:
转换说明符说明
d匹配十进制整数
i匹配整数
o、u、x匹配无符号八进制、十进制或十六进制整数
a、e、f、g匹配单精度浮点数
c匹配单个字符
s匹配非空字符串
[ ]匹配来自集合的非空字符序列,然后在末尾添加空字符,实参是指向字符数组的指针,可以使用^进行前置否定
pprintf函数的输出格式匹配指针值
n相应的实参必须是指向int型的指针,把目前为止读到的字符数量存储到该实参
%匹配字符%

字符的输入输出

//向标准流输入输出
int putchar(int character);
int getchar();
//向指定流输入输出
int putc(int character,FILE * stream);
int getc(FILE * stream);
int fputc(int character,FILE * stream);
int fgetc(FILE * stream);
//把从流中读入的字符放回并清空文件末尾指示器
int ungetc(int c,FILE * stream);

行的输入输出

//向标准流输入输出
//puts在写入完成时会添加一个换行符
int puts(char const * s);
//gets函数遇见换行符停止并舍弃换行符
char *gets(char * s);
//向指定流输入输出
int fputs(const char restrict * s,FILE * restrict stream);
char* fgets(char * restrict s,int maxCount,FILE * stream);

块的输入输出

//ptr:数组地址
//size:数组元素大小
//nmemb:元素数量
size_t fread(void * restrict ptr,size_t size,size_t nmemb,FILE * restrict stream);
size_t fwrite(const void * restrict ptr,size_t size,size_t nmemb, FILE * restrict stream);

字符串的输入输出

//将格式化串输出到buffer中
int sprintf(char * restrict buffer,const char * restrict format,...);
int snprintf(char * restrict buffer,size_t n,const char * restrict format,...);
//从buffer中输入
int sscanf(const char * restrict buffer,const char * restrict format,...);

错误处理

<assert.h>

每次执行assert时,都会检查它的参数值是否为假。如果参数为假那么assert就会向stderr写一条消息并调用abort函数终止程序。

#define assert(expression) xxx

assert一般用于调试阶段,因此会在生产时期禁止它,禁止的方式很容易,只需要在包含<assert.h>头文件之前定义宏NDEBUG即可。

<signal.h>

<signaal.h>头文件提供了信号处理的工具,信号有两种类型:运行时错误和发生在程序之外的事件,大多数信号是异步的。<signaal.h>头文件定义了一系列的宏来表示不同的信号:

//中断信号
#define SIGINT 2   
//无效指令
#define SIGILL 4  
//浮点数异常
#define SIGFPE 8
//无效存储访问
#define SIGSEGV 11
//终止请求
#define SIGTERM 15
//Ctrl-Break sequence
#define SIGBREAK 21  
//异常终止
#define SIGABRT 22
//与其它平台兼容的SIGABRT
#define SIGABRT_COMPAT 6   

signal函数第一个参数是信号编码,第二个参数是一个指向信号发生时处理这一信号的函数的指针。当一个信号产生并调用特定的处理函数时,信号的编码会作为参数传给处理函数。在信函处理函数内只能调用signal函数和reise函数,并且不能使用具有静态存储权限的变量。如果信号是由abort函数或raise函数引发的,那么信号处理函数可以调用库函数或使用具有静态存储权限的变量。但是不能调用raise函数。一旦处理函数返回,程序会在信号发生点恢复并继续执行,但如果信号是SIGABRT,程序会直接终止;如果信号是SIGFPESIGILLSIGSEGV,那么处理函数返回的结果是未定义的。signal函数的返回值是一个指向前一个处理函数的指针。

void (*signal(int arg,void (*func)(int)))(int);

也可以使用一些预定义的处理函数:

typedef void (* _crt_signal_t)(int);
#define SIG_DFL ((_crt_signal_t)0)     // 默认行为
#define SIG_IGN ((_crt_signal_t)1)     // 忽视信号
#define SIG_GET ((_crt_signal_t)2)     // 返回当前值
#define SIG_SGE ((_crt_signal_t)3)     // signal gets error
#define SIG_ACK ((_crt_signal_t)4)     // 告知收到

raise函数可以模拟信号的产生:

int raise(int signal);

<setjmp.h>

通常情况下函数会返回到它被调用的位置,但是<setjmp.h>头文件可以使一个函数直接跳转到另一个函数而不需要返回。

int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);

setjmp用来标记程序中的一个位置,它接收一个jmp_buf 类型的参数,setjmp会将当前环境存储到该变量中。然后返回零。longjmp可以跳转到setjmp标记的位置。该函数的参数是使用setjmp函数时的同一个jmp_buf变量,该函数首先根据jmp_buf变量的内容恢复当前的环境,然后从setjmp宏中返回,此时setjmp宏返回的是val。如果val的值是0那么setjmp将返回1

其它标准库

截至C99版本,C语言中的标准库有以下24个,大多数编译器都会使用更大的库,但他们不属于标准库的范畴。

库名说明
<assert.h>诊断
<ctype.h>字符处理
<errno.h>错误
<float.h>浮点类型的特性
<limite.h>整数类型的大小
<locale.h>本地化
<math.h>数学计算
<setjmp.h>非本地跳转
<signal.h>非本地跳转
<stdarg.h>可变参数
<stddef.h>常用定义
<stdio.h>输入输出
<stdlib.h>常用实用程序
<string.h>字符串处理
<time.h>时间和日期
<complex.h>复数算数
<fenv.h>浮点环境
<inttypes.h>整数类型格式转换
<iso646.h>拼写转换
<stdbool.h>布尔类型和值
<stdint.h>整数类型
<tgmath.h>泛型数学
<wchar.h>扩展的宽字节和多字节实用工具
<wctype.h>宽字符分类和映射实用工具

<stddef.h>

<stddef.h>头文件提供了常用的类型和宏的定义:

//空指针
#define NULL ((void *)0)
//s是一个结构类型,m是一个结构成员
//计算结构的起点到指定成员间的字节数
#define offsetof(s,m) ((size_t)&(((s*)0)->m))
//是一个有符号的整数类型
//当指针相减时结果的类型
typedef xxx ptrdiff_t
//是一个无符号的整数类型
//sizeof运算符返回的类型
typedef unsigned xxx size_t

typedef unsigned short wchar_t

<stdbool.h>

<stdbool.h>头文件提供了布尔相关的宏:

#define bool	_Bool
#define false	0
#define true	1

<ctype.h>

<ctype.h>头文件提供了字符分类函数和字符大小写映射函数,这些函数都接收一个int类型的参数,将一个char类型的实数传入时会进行类型的隐式转换,由于char类型的有无符号性有具体的实现决定,因此这个隐式转换的结果也是不确定的,所以在使用前应该先将实参转换为unsigned char类型。

//是否是字母或数字
int isalnum(int c);
//是否是字母
int isalpha(int c);
//是否是十进制数字
int isdigit(int c);
//是否是十六进制数字
int isxdigit(int c);
//是否是小写字母
int islower(int c);
//是否是大写字母
int isupper(int c);
//是否是空白字符
int isspace(int c);
//转换为大写字母
int tolower(int c);
//转换为小写字符
int toupper(int c);

<stdlib.h>

<stdlib.h>头文件提供了一些通用的实用工具。

数值转换

数值转换函数用于将含有数值的字符串转换为整数形式。每个函数都会跳过字符串开始处的空白字符,在遇到第一个不属于数的字符处停止。如果不能转换那么函数返回零。

double atof(const char *nptr);

int atoi(const char *nptr);
long int atol(const char *nptr);
long long int atoll(const char *nptr);

double strtod(const char * restrict nptr,char ** restrict endptr);
float strtof(const char * restrict nptr,char ** restrict endptr);
long double strtold(const char * restrict nptr,char ** restrict endptr);

long int strtol(const char * restrict nptr,char ** restrict endptr,int base);
long long int strtoll(const char * restrict nptr,char ** restrict endptr,int base);

unsigned long int strtoul(const char * restrict nptr,char ** restrict endptr,int base);
unsigned long long int strtoull(const char * restrict nptr,char ** restrict endptr,int base);

伪随机数生成

这两个函数都可以返回一个0RAND_MAX的伪随机数。

int rand();
void srand(unsigned int seed);

与环境通信

//注册一个在程序结束时调用的钩子函数
int atexit(void (*func)());
//程序异常结束,不会调用钩子函数
void abort();
//程序正常结束并返回一个状态码
void exit(int status);
//程序正常结束并返回一个状态码,但不会调用钩子函数
void _Exit(int status);
//获取用户环境中的字符串
char *getenv(const char *name);
//运行另一个程序,返回被运行程序结束时的状态码
int system(const char *string);

搜索和排序

bsearch函数用于数组内搜索元素,qsort函数用于排序数组。

//key :指向要搜索的元素
//base:指向数组
//nmemb:数组元素的个数
//size:数组元素的大小
//compar:比较函数
void *bsearch(const void *key,const void * base,size_t nmemb,size_t size,int (*compar)(const void *,const void *));
void qsort(void *base,size_t nmemb, size_t size,int(*compar)(const void *,const void *));
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亻乍屯页女子白勺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值