C语言复习(持续更新~)

本文最后更新于:2023年9月4日 下午

C语言复习

1 数据类型与关键字

1.1 数据类型

  • 基本数据类型

    • 整型(Integer Types): 用于存储整数值。主要包括int、short、long等不同变体。
    • 字符型(Character Type): 用于存储单个字符。通常使用char。
    • 浮点型(Floating-Point Types): 用于存储浮点数(带有小数点的数字)。包括float和double。
    • 布尔型(Boolean Type): 用于表示真(1)或假(0)值。C99引入了_Bool类型。
  • 复合数据类型

    • 数组(Array): 一组具有相同数据类型的元素,通过索引访问。
    • 指针(Pointer): 存储变量的内存地址,用于间接访问变量的值。指针还可以用于动态内存分配。
    • 结构体(Structure): 允许将不同数据类型的成员组合在一起,创建一个自定义的复合数据类型。
    • 联合(Union): 类似于结构体,但联合的所有成员共享同一块内存,只能同时存储其中一个成员的值。
    • 枚举(Enumeration): 允许定义一组具名的整数常数,以提高代码的可读性。
    • typedef: 用于创建已有数据类型的别名,使代码更加清晰。

32位操作系统下常见编译器下的数据类型大小及表示的数据范围:

类型名称 类型关键字 占字节数 其他叫法 表示的数据范围
字符型 char 1 signed char -128 ~ 127
无符号字符型 unsigned char 1 none 0 ~ 255
整型 int 4 signed int -2,147,483,648 ~ 2,147,483,647
无符号整型 unsigned int 4 unsigned 0 ~ 4,294,967,295
短整型 short 2 short int -32,768 ~ 32,767
无符号短整型 unsigned short 2 unsigned short int 0 ~ 65,535
长整型 long 4 long int -2,147,483,648 ~ 2,147,483,647
无符号长整型 unsigned long 4 unsigned long 0 ~ 4,294,967,295
单精度浮点数 float 4 none 3.4E +/- 38 (7 digits)
双精度浮点数 double 8 none 1.7E +/- 308 (15 digits)
长双精度浮点数 long double 10 none 1.2E +/- 4932 (19 digits)
长整型 long long 8 __int64 -9223372036854775808~9223372036854775808

1.2 关键字

1.自动变量类型关键字:

  • auto:用于声明自动存储持续时间的变量。

2.控制流关键字:

  • if:用于条件语句,执行基于条件的代码块。
  • else:用于条件语句中的可选分支,当条件不满足时执行。
  • switch:用于多重选择语句,根据表达式的值跳转到不同的分支。
  • case:用于在switch语句中标识不同的分支。
  • defaultswitch语句中的默认分支。

3.循环关键字:

  • for:用于循环语句,指定初始化、条件和迭代表达式。
  • while:用于循环语句,根据条件循环执行代码块。
  • do:用于循环语句,先执行代码块,然后根据条件继续执行。

4.跳转关键字:

  • goto:用于无条件跳转到指定的标签位置。

5.函数关键字:

  • return:用于从函数中返回值。
  • void:用于声明函数不返回值。

6.存储类关键字:

  • auto:用于声明自动存储类别的变量(已在前面提到)。
  • register:用于声明寄存器存储类别的变量。
  • static:用于声明静态存储类别的变量和函数。
  • extern:用于声明外部链接存储类别的变量和函数。

7.数据类型关键字:

  • int:用于声明整型数据类型。
  • char:用于声明字符数据类型。
  • float:用于声明单精度浮点数据类型。
  • double:用于声明双精度浮点数据类型。
  • short:用于声明短整型数据类型。
  • long:用于声明长整型数据类型。
  • signed:用于声明有符号整数数据类型。
  • unsigned:用于声明无符号整数数据类型。

8.其他关键字:

  • sizeof:用于获取数据类型或表达式的大小(字节数)。
  • typedef:用于创建自定义数据类型的别名。
  • enum:用于声明枚举类型。
  • struct:用于声明结构体类型。
  • union:用于声明联合类型。
  • const:用于声明常量。
  • volatile:用于声明易失变量,表示可能会被意外更改。
  • static:用于声明静态成员或局部变量。

2 输入与输出

2.1 输入函数

1.scanf
用于从标准输入(通常是键盘)读取数据。它可以读取不同类型的数据,如整数、浮点数和字符串。格式化字符串指定了读取的数据类型。
例如:

1
2
3
int age;
printf("Enter your age: ");
scanf("%d", &age);

2.getchar
输入单个字符,保存到字符变量中。

3.gets
输入一行数据,保存到字符串变量中。

2.2 输出函数

1.printf
用于将格式化数据输出到标准输出(通常是屏幕)。可以使用格式化字符串指定输出的格式和内容。
例如:

1
2
int num = 37;
printf("The number is: %d\n", num);

常见的输出控制符:
(1) %d:以十进制形式输出整数。
示例:printf("%d", 42); // 输出:42

(2) %f:以浮点数形式输出实数(默认保留6位小数)。
示例:printf("%f", 3.14159); // 输出:3.141590

(3) %c:输出单个字符。
示例:printf("%c", 'A'); // 输出:A

(4) %s:输出字符串。
示例:printf("%s", "Hello, World!"); // 输出:Hello, World!

(5) %o:以八进制形式输出整数。
示例:printf("%o", 18); // 输出:22

(6) %x%X:以十六进制形式输出整数,%x 输出小写字母,%X 输出大写字母。
示例:printf("%x", 255); // 输出:ff

(7) %u:以无符号十进制形式输出整数。
示例:printf("%u", 123); // 输出:123

(8) %e%E:以指数形式输出浮点数,%e 输出小写字母,%E 输出大写字母。
示例:printf("%e", 0.00123); // 输出:1.230000e-03

(9) %g%G:以%f 或 %e 格式输出浮点数,视数值大小自动选择。
示例:printf("%g", 123456.789); // 输出:123457

(10) %%:输出百分号 % 字符。
示例:printf("100%%"); // 输出:100%

这些输出控制符可以与一些修饰符一起使用,例如用于指定字段宽度、精度等。例如:

  • %5d:输出整数,至少占5个字符宽度。
  • %.2f:输出浮点数,保留两位小数。
  • %10s:输出字符串,至少占10个字符宽度。

2.putchar
输出单个字符。

3.puts
输出字符串。

2.3 文件输入和输出

1.文件打开函数

  • fopen:用于打开文件,并返回一个文件指针,供后续操作使用。

2.文件读取函数

  • fscanf:类似于scanf,但从文件中读取数据。
  • fgets:从文件中读取一行文本。

3.文件写入函数

  • fprintf:类似于printf,但将输出写入文件。
  • fputs:将文本写入文件。
  • fputc:写入单个字符到文件。

4.文件关闭函数

  • fclose:用于关闭已打开的文件。

以下是一个读取文件并将内容输出到屏幕的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main() {
FILE *file = fopen("example.txt", "r"); // 打开文件只读模式
if (file == NULL) {
printf("Failed to open the file.\n");
return 1;
}

char buffer[100];
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}

fclose(file); // 关闭文件

return 0;
}

3 运算符与表达式

3.1 运算符

1. 算术运算符:

  • +:加法运算符,用于相加两个操作数。
  • -:减法运算符,用于从第一个操作数中减去第二个操作数。
  • *:乘法运算符,用于将两个操作数相乘。
  • /:除法运算符,用于将第一个操作数除以第二个操作数。
  • %:取余运算符,用于计算除法的余数部分。
  • ++:自增运算符,将操作数的值增加1。
  • --:自减运算符,将操作数的值减少1。

2. 关系运算符:

  • ==:等于运算符,检查两个操作数是否相等。
  • !=:不等于运算符,检查两个操作数是否不相等。
  • <:小于运算符,检查第一个操作数是否小于第二个操作数。
  • >:大于运算符,检查第一个操作数是否大于第二个操作数。
  • <=:小于等于运算符,检查第一个操作数是否小于等于第二个操作数。
  • >=:大于等于运算符,检查第一个操作数是否大于等于第二个操作数。

3. 逻辑运算符:

  • &&:逻辑与运算符,用于检查两个条件是否同时为真。
  • ||:逻辑或运算符,用于检查两个条件是否至少一个为真。
  • !:逻辑非运算符,用于取反一个条件的值。

4. 赋值运算符:

  • =:赋值运算符,将右侧的值赋给左侧的变量。
  • +=-=*=/=%=++=--= 等:复合赋值运算符,用于在赋值的同时进行其他运算。

5. 位运算符:

  • &:按位与运算符,对两个操作数的每一位进行与操作。
  • |:按位或运算符,对两个操作数的每一位进行或操作。
  • ^:按位异或运算符,对两个操作数的每一位进行异或操作。
  • ~:按位取反运算符,对操作数的每一位取反。

6. 移位运算符:

  • <<:左移运算符,将第一个操作数的二进制表示左移指定的位数。
  • >>:右移运算符,将第一个操作数的二进制表示右移指定的位数。

7. 条件运算符:

  • ? ::条件运算符(三元运算符),根据条件选择两个操作数中的一个进行返回。

8. 逗号运算符:

  • ,:逗号运算符,用于分隔多个表达式,返回最后一个表达式的值。

9. sizeof

  • sizeof:以字节为单位返回某操作数的大小,用于求某一类型变量的长度。其运算对象可以是任何数据类型或变量。

3.2 表达式

在C语言中,表达式是由操作数和运算符组成的组合,可以执行各种计算和操作。表达式的结果可以是一个值,一个变量,或者一个组合的值。表达式可以包含常量、变量、运算符和函数调用等。
例如:

1
2
3
4
3+2
a=(2+b/3)/5
x=i++
m=2*5

4 语句结构

4.1 选择结构

1.if else
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main() {
int score;

printf("Please enter your score: ");
scanf("%d", &score);

if (score >= 90) {
printf("You got an A.\n");
} else if (score >= 80) {
printf("You got a B.\n");
} else if (score >= 70) {
printf("You got a C.\n");
} else if (score >= 60) {
printf("You got a D.\n");
} else {
printf("You failed.\n");
}

return 0;
}

2.switch case
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

int main() {
int choice;

printf("Choose an option:\n");
printf("1. Start\n");
printf("2. Pause\n");
printf("3. Stop\n");
printf("Enter your choice: ");
scanf("%d", &choice);

switch (choice) {
case 1:
printf("Starting...\n");
break;
case 2:
printf("Pausing...\n");
break;
case 3:
printf("Stopping...\n");
break;
default:
printf("Invalid choice.\n");
break;
}

return 0;
}

4.2 循环结构

1.for循环
例如:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
for (int i = 1; i <= 5; i++) {
printf("Iteration %d\n", i);
}

return 0;
}

注:使用C89标准的编译器(如VC++6.0)需要在for循环外声明变量,否则编译无法通过。

2.while与do while
while示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
int count = 1;

while (count <= 5) {
printf("Iteration %d\n", count);
count++;
}

return 0;
}

do while示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
int count = 1;

do {
printf("Iteration %d\n", count);
count++;
} while (count <= 5);

return 0;
}

4.3 break与continue

1.break
用于完全终止循环,跳出循环体。可用于switch、for、while、do while等结构中。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
for (int i = 1; i <= 5; i++) {
if (i == 3) {
break; // 当 i 为 3 时,终止循环
}
printf("Iteration %d\n", i);
}

return 0;
}

2.continue
用于跳过当前迭代,进入下一次迭代。只能在循环中使用
例如:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 当 i 为 3 时,跳过当前迭代
}
printf("Iteration %d\n", i);
}

return 0;
}

5 函数

5.1 函数的定义、声明和调用

1.函数定义
函数定义由函数的返回类型、函数名、参数列表和函数体组成。例如:

1
2
3
int add(int a, int b) {
return a + b;
}

2.函数声明
函数声明指定函数的返回类型、函数名和参数列表,通常在函数使用之前进行。这样可以让编译器了解函数的原型。例如:

1
int add(int a, int b);

注:1.在main函数内调用之前未定义的函数需要在前面先声明该函数;2.C语言不支持函数重载。

3.函数调用
函数通过其名称和参数列表来调用。调用函数时,传递的实际参数会与函数的形式参数相对应。例如:

1
int result = add(3, 5);

5.2 变量

在C语言中,变量是对程序中数据所占内存空间的一种抽象定义,定义变量时,用户定义变量的名、变量的类型,这些都是变量的操作属性。不仅可以通过变量名访问该变量,系统还通过该标识符确定变量在内存中的位置。

变量的保留时间又称为生存期,从时间角度,可将变量分为静态存储动态存储两种情况:

(1)静态存储是指变量存储在内存的静态存储区,在编译时就分配了存储空间,在整个程序的运行期间,该变量占有固定的存储单元,程序结束后,这部分空间才释放,变量的值在整个程序中始终存在。

(2)动态存储是指变量存储在内存的动态存储区,在程序的运行过程中,只有当变量所在的函数被调用时,编译系统才临时为该变量分配一段内存单元,函数调用结束,该变量空间释放,变量的值只在函数调用期存在。

变量的作用范围又称为作用域,从空间角度,可以将变量分为全局变量局部变量

(1)局部变量是在一个函数或复合语句内定义的变量,它仅在函数或复合语句内有效,编译时,编译系统不为局部变量分配内存单元,而是在程序运行过程中,当局部变量所在的函数被调用时,编译系统根据需要,临时分配内存,调用结束,空间释放。

(2)全局变量是在函数之外定义的变量,其作用范围为从定义处开始到本文件结束,编译时,编译系统为其分配固定的内存单元,在程序运行的自始至终都占用固定单元。

在计算机中,保存变量当前值的存储单元有两类,一类是内存,另一类是CPU的寄存器。变量的存储类型关系到变量的存储位置,C语言中定义了4种存储属性,即自动变量(auto)、外部变量(extern)、静态变量(static)和寄存器变量(register),它关系到变量在内存中的存放位置,由此决定了变量的保留时间和变量的作用范围。

1.自动变量(auto)

  • auto是默认的变量存储类别,意味着在函数内部声明的变量默认为自动变量。
  • 自动变量在函数调用时分配内存,在函数退出时释放内存,它们的生命周期与函数的生命周期相关联。
  • 自动变量的值不会保留函数调用之间的状态,每次函数调用时都会重新初始化。
1
2
3
void example_function() {
auto int x = 10; // auto 是默认的,可以省略
}

2.外部变量(extern)

  • extern关键字用于声明在其他文件中定义的全局变量,使得当前文件可以访问这些变量。
  • 外部变量的定义在另一个文件中,可以在当前文件中使用,但是不会在当前文件中重新定义。
1
2
3
4
5
// 文件1:file1.c
int global_variable = 42;

// 文件2:file2.c
extern int global_variable; // 声明外部变量,不需要重新定义,只是引用

3.静态变量(static)

  • static关键字用于改变变量的作用域和生命周期。
  • 在函数内部声明的静态变量具有函数作用域,但是在函数调用之间保持其值,不会重新初始化。
  • 在文件作用域(函数外部)声明的静态变量只能在当前文件中访问,不能被其他文件访问。
1
2
3
4
void example_function() {
static int count = 0; // 静态变量,保持值在函数调用之间
count++;
}

4.寄存器变量(register)

  • register关键字用于向编译器建议将变量存储在寄存器中,以便更快地访问。
  • 寄存器变量只能存储在寄存器中,不能直接获取其内存地址。
  • 编译器可以选择是否将变量存储在寄存器中,也可以忽略这个建议。
1
2
3
void example_function() {
register int i; // 将 i 存储在寄存器中(编译器可选)
}

5.3 main函数

main函数是C语言程序的入口点,它是每个C程序都必须包含的一个特殊函数。main函数作为程序的入口点,是整个程序的起始点。当程序运行时,操作系统会从main函数开始执行,然后按照程序的逻辑顺序执行后续的代码。

1.函数签名
main函数的标准签名是(C99及以上标准):

1
2
3
4
int main(void) {
// 函数体
return 0;
}

这里,int是返回类型,表示函数返回一个整数值。void表示main函数没有参数。

2.参数
main函数可以没有参数(使用void)或带两个参数:int argcchar *argv[]

  • argc(argument count)表示命令行参数的数量,包括程序名称本身。
  • argv(argument vector)是一个指向字符串数组的指针,每个字符串是一个命令行参数。

3.返回值
main函数的返回值通常用来表示程序的执行状态,一般约定返回0表示成功,非零值表示出现错误。

4.函数体
main函数的函数体内包含程序的主要逻辑。这里是程序的实际操作和算法部分。
以下是一个带参数的main函数的示例:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, char *argv[]) {
printf("Number of arguments: %d\n", argc);

for (int i = 0; i < argc; i++) {
printf("Argument %d: %s\n", i, argv[i]);
}

return 0;
}

在这个示例中,main函数接受命令行参数,并输出参数的数量和每个参数的内容。

5.4 预定义函数

预定义函数,也称为内置函数或库函数,是在C语言中已经定义好并可以直接使用的函数。这些函数提供了各种常见的操作,包括输入输出、数学运算、字符串处理等。在使用预定义函数之前,通常需要包含相应的头文件。

常见的预定义函数及其功能:

1. 输入输出函数:

  • printf:格式化输出函数,用于将格式化的数据输出到屏幕。
    • 功能:将格式化的数据输出到标准输出(屏幕)。
    • 头文件:#include <stdio.h>
  • scanf:格式化输入函数,用于从标准输入(键盘)获取输入数据。
    • 功能:根据格式字符串从标准输入获取数据。
    • 头文件:#include <stdio.h>
  • getchar:从标准输入读取一个字符。
    • 功能:从标准输入读取单个字符。
    • 头文件:#include <stdio.h>
  • puts:输出字符串到屏幕,并自动添加换行符。
    • 功能:将字符串输出到标准输出,然后添加换行。
    • 头文件:#include <stdio.h>

2. 数学函数:

  • sqrt:计算平方根。
    • 功能:返回一个数的平方根。
    • 头文件:#include <math.h>
  • pow:计算幂次方。
    • 功能:计算一个数的指定次幂。
    • 头文件:#include <math.h>
  • abs:计算绝对值。
    • 功能:返回一个数的绝对值。
    • 头文件:#include <stdlib.h>
  • sincostan:三角函数。
    • 功能:计算三角函数的值。
    • 头文件:#include <math.h>

3. 字符串函数:

  • strlen:计算字符串长度。
    • 功能:返回一个字符串的长度(字符个数)。
    • 头文件:#include <string.h>
  • strcpystrncpy:复制字符串。
    • 功能:将一个字符串复制到另一个字符串中。
    • 头文件:#include <string.h>
  • strcatstrncat:拼接字符串。
    • 功能:将一个字符串连接到另一个字符串的末尾。
    • 头文件:#include <string.h>
  • strcmpstrncmp:比较字符串。
    • 功能:比较两个字符串是否相等。
    • 头文件:#include <string.h>
  • strchrstrrchr:在字符串中查找字符。
    • 功能:查找指定字符在字符串中的第一个和最后一个出现位置。
    • 头文件:#include <string.h>

4. 标准I/O函数:

  • fopenfclose:打开和关闭文件。
    • 功能:打开和关闭文件以进行读写操作。
    • 头文件:#include <stdio.h>
  • fprintffscanf:格式化文件输出和输入。
    • 功能:类似于printfscanf,但是用于文件。
    • 头文件:#include <stdio.h>
  • fgetsfputs:读取和写入文件中的字符串。
    • 功能:读取和写入文件中的文本数据。
    • 头文件:#include <stdio.h>
  • feofferror:检测文件结束和错误。
    • 功能:检测文件流的状态。
    • 头文件:#include <stdio.h>

5. 内存管理函数:

  • malloc:分配指定大小的内存块。
    • 功能:分配一块指定大小的内存,返回指向该内存块的指针。
    • 头文件:#include <stdlib.h>
  • calloc:分配多个元素的内存块,并初始化为零。
    • 功能:分配一块多个元素的内存,返回指向该内存块的指针,且内存块初始化为零。
    • 头文件:#include <stdlib.h>
  • realloc:重新分配已分配内存的大小。
    • 功能:修改之前分配的内存块的大小,返回指向新内存块的指针。
    • 头文件:#include <stdlib.h>
  • free:释放之前分配的内存块。
    • 功能:释放之前使用malloccallocrealloc分配的内存块。
    • 头文件:#include <stdlib.h>

6 数组

6.1 数组的概念

数组是若干个相同类型的变量在内存中有序存储的集合。

对于数组概念的理解:

  • 数组用于存储一组数据;
  • 数组里面存储的数据类型必须是相同的;
  • 数组在内存中会开辟一块连续的空间;

C语言中数组可以按所存储的元素类型和维数分类:

1.按所存储的元素类型分类

  • 字符数组,如char str[10];
  • 整型数组,如int arr[5];
  • 浮点型数组,单精度float f[3];,双精度double d[3];
  • 指针数组,如char *arr[20];
  • 结构体数组,如struct stu boy[10];
  • ……

2.按维数分类

  • 一维数组,如:int a[30];,类似于一排平房
  • 二维数组,如:int a[3][4];,可以看成一栋楼房有很多层,每层有多个房间,也类似于数学中的矩阵
  • 多维数组,如:int a[3][4][10];,三维数组是由多个相同的二维数组构成的……

6.2 数组定义和初始化

1.定义一维数组
一维数组定义示例:

1
2
3
int a[100]; //定义一个数组名为a,存储100个int类型的数组,其元素分别是a[0]~a[99]
float b[10]; //数组名为b的,存储10个float类型的数组,其元素分别是b[0]~b[9]
char c[256]; //定义一个数组名为c的字符型数组,长度为256,其元素分别是c[0]~c[255]

亦可在定义同时初始化为元素赋值:

1
2
3
int a[100]={1,2,3,4,5}; //定义一个整型数组a,前5个元素即赋值为1,2,3,4,5,后95个元素值值全部为0
float b[10]={1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9,0.0}; //定义float数组b并对全部float类型的元素都分别赋值
char c[256]={'C','l''a','n','g','u','a','g','e'}; //定义一个数组名为c的字符型数组,并对前9个元素进行赋值,其余元素全部为'\0'

2.定义二维数组
二维数组的定义:

1
2
3
4
5
6
int a[3][4]; /*定义一个整形二维数组a,有3行4列共12个元素分别为:
a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] a[1][1] a[1][2] a[1][3]
a[2][0] a[2][1] a[2][2] a[2][3]
*/
char arry[10][10]; //定义一个字符型二维数组arry,有10行10列,依次为arry[0][0]~arry[9][9]共100个元素

二维数组初始化:

1
2
int a[3][4]={{1,2,3,4},{10,20,30,40},{100,200,300,400}}; //定义一个三行四列的二维数组,按行赋值
int a[3][4]={1,2,3,4,10,20,30,40,100,200,300,400}; //定义一个三行四列的二维数组并对其中的12(3*4)个元素进行赋值

6.3 字符串、字符数组、字符串数组

此部分参考自博客 https://blog.csdn.net/u011852211/article/details/117597546 ,感谢原作者。

1.字符串的定义
C语言中字符串的定义有几种方式:

1
2
3
char *str1 = {"Hello world!"};  // 方式一 (可省略{})
char str2[] = {"Hello world!"}; // 方式二 (可省略{})
char str3[] = {'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\0'}; // 方式三

1.几种字符串定义方式之间的区别:
(1) 方式一的本质是定义了一个char型指针str1, 指向的是字符串常量Hello world!,因此str1所指向地址中的内容是不可更改的,即不能使用类似str1[0] = 'h';的语句对其进行赋值操作。但是指针str1仍然可以指向其他地址,例如可利用str1 = str2;语句将str1指向str2所指向的地址。 此外,字符串的结尾会被编译器自动加上结束符'\0'
(2) 方式二定义了以一个char型数组str2str2指向数组第一个元素所处内存的地址。此时内存空间是由栈分配的,地址一经分配就不能更改,因此str2不能再指向其他内存空间,但其所指向的内存空间中的内容是可以更改的,即可以使用类似str2[0] = 'h';的语句对其进行赋值操作。字符串的结尾也会被编译器自动加上结束符'\0'
(3)方式三中如果没有指定大小的话,编译器只会会根据字符串大小分配空间,但不会在字符串结尾添加'\0'。为避免其他异常情况的出现,务必在字符串结尾处手动加上'\0'。以该方式定义字符串时不允许有空的单字符'',即' '中的空格不能省略;

2.获取字符串的长度
常用运算符sizeof()strlen()函数这两种方式来计算字符串的长度。
sizeof()的值是在编译时计算得到的,因此不能用于计算动态分配的内存空间大小。sizeof()可用于基本类型、结构体以及数组等静态分配的对象所占空间大小的计算,其返回值与内存中存储的内容无关。
例如,在32位系统中,char类型变量占用的空间为一个字节 ,即sizeof(char)的值为1。而字符型指针char *的本质是一个int型变量,所以其占用的空间大小为四个字节,即sizeof(char *)的值为4。
函数strlen()的函数原型为size_t __cdecl strlen(const char *); ,其声明位于头文件string.h中。strlen()是在运行时计算的,其返回值为从给定的地址开始到遇见的第一个NULL之间的长度。 返回的长度并不包含NULL所占用的空间。

有关运算符sizeof()与函数strlen()的区别:

sizeof() strlen()
编译时计算。 运行时计算。
数组、结构体等静态变量。 char *类型的变量,必须以'\0'结尾。
数组名传给sizeof()不会退化。 数组名传给strlen()会退化为指针。

利用sizeof()strlen()分别计算上述三种定义方式定义的字符串的长度:

1
2
3
4
5
6
7
8
9
printf("sizeof(str1)=%d\n", sizeof(str1));
printf("sizeof(str2)=%d\n", sizeof(str2));
printf("sizeof(str3)=%d\n", sizeof(str3));
printf("sizeof(str4)=%d\n", sizeof(str4));

printf("strlen(str1)=%d\n", strlen(str1));
printf("strlen(str2)=%d\n", strlen(str2));
printf("strlen(str3)=%d\n", strlen(str3));
printf("strlen(str4)=%d\n", strlen(str4));

计算结果为:

1
2
3
4
5
6
7
8
sizeof(str1)=4    // 即sizeof(char *),返回的是字符型指针的大小,故sizeof无法计算方式一定义的字符串的长度。
sizeof(str2)=13 // 包含'\0'。
sizeof(str3)=13 // 包含'\0'。
sizeof(str4)=16 // 返回的是实际分配的内存大小,而不是字符串的长度。
strlen(str1)=12 // 不包含'\0'。
strlen(str2)=12 // 不包含'\0',故比sizeof(str2)的值小1。
strlen(str3)=12 // 不包含'\0',故比sizeof(str3)的值小1。
strlen(str4)=12 // 返回的是字符串的实际长度(不包含'\0'),而不是实际分配的内存大小。

2.字符串的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 逐个访问字符串中的字符并逐行打印

// 思路一:根据数组长度逐个遍历
void travel_str(void)
{
int i = 0;
char str[] = {"Hello World!"};
int len = strlen(str); // 计算字符串大小

// 逐个遍历
for(i=0;i<len;i++)
{
printf("%c\n", str[i]);
}
}

// 思路二:利用指针进行遍历
void travel_str(void)
{
char str[] = {"Hello World!"};
char *ch = str;

// 不能直接采用原指针str遍历,因为此处的str不能改变其指向的地址。
// 即使可以也会因为str指向了别处导致str原来指向的内存无法被释放,造成内存泄露。
while(*ch != '\0') // 以'\0'作为字符串结束标志
{
printf("%c\n", *ch++);
}
}

3.字符串数组的定义

1
2
3
4
// 方式一:必须指定第二维的大小,且应大于等于数组最长字符串的长度
char str_arr1[][10] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};
// 方式二
char *str_arr2[] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};

4.字符串数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 遍历数组中的字符串

// 思路一
void travel_str_array(void)
{
unsigned char i = 0, size = 0;
// char str_arr[][10] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};
char *str_arr[] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};

size = sizeof(str_arr)/sizeof(str_arr[0]); // 获取数组大小

for(i=0; i<size; i++)
{
// printf("%s\n", str_arr[i]);
printf("%s\n", *(str_arr+i));
}
}

// 思路二
void travel_str_array(void)
{
char *str_arr[] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", NULL};
char **str = str_arr; // 采用临时指针指向原数组,避免因原数组指针移动导致内存泄露。

// 采用该方法遍历时建议采用方法二定义数组,并在数组最后手动添加NULL。
while(*str != NULL)
{
printf("%s\n", *str++);
}

// 另一种循环方式
#if 0
char **ptr = NULL;
for(ptr=str_arr; *ptr!=NULL; ptr++)
{
printf("%s\n", *ptr);
}
#endif
}

若采用指针遍历字符串数组时,务必在数组最后手动添加NULL,以确保能够准确找到字符串数组的结尾。否则,指针会指向其他非目标位置,甚至导致程序崩溃。
若通过计算数组大小来遍历字符串数组时,尾部无需添加NULL。如果手动添加了NULL ,则在遍历数组时应将数组长度减去1,因为编译器多分配了一个指向NULL的指针。访问NULL指针会导致程序崩溃。具体分析见第五点。

5.遍历字符串数组中的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void travel_str_array_by_char(void)
{
unsigned char i,j = 0;
char *str_arr[] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", NULL};
int len = sizeof(str_arr)/sizeof(str_arr[0]);
char **str = str_arr;

// 利用指针遍历字符串数据中的字符
for(str=str_arr; *str!=NULL; str++)
{
for(j=0; j<strlen(*str);j++)
{
printf("%c ", *((*str)+j));
}
printf("\n");
}

// 利用字符串数组大小和字符串长度来遍历字符串数组中的字符
for(i=0; i<len-1; i++)
{
for(j=0;j<strlen(str_arr[i]);j++)
{
// printf("%c ",*(*(str_arr+i)+j));
printf("%c ",str_arr[i][j]);
}
printf("\n");
}

// 错误示例
#if 0
for(i=0; i<len; i++)
{
// 当i=len-1时,str_arr[i] = NULL,此时strlen(NULL)访问NULL指针,程序崩溃。
for(j=0;j<strlen(str_arr[i]);j++)
{
// printf("%c ",*(*(str_arr+i)+j));
printf("%c ",str_arr[i][j]);
}
printf("\n");
}
#endif
}

7 指针

这两篇大佬的博客写得太优秀了,直接拿来参考:

这部分内容太核心了,一时半会儿难以总结清楚,有时间我再补充啦~😁

8 结构体、共用体、枚举

8.1 结构体

1.结构体定义
一般形式:

1
2
3
4
5
6
struct 结构体名 
{
成员类型1 成员名1;
成员类型2 成名名2;
...
};

例如:

1
2
3
4
5
struct Student 
{
char name[20];
int age;
};

2.结构体变量的声明和初始化

(1) 方式一:定义结构体,声明变量并赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <string.h>

struct Student
{
char name[20];
int age;
};

void printAge(struct Student *pStu)
{
printf("%s's age is %d. \n", pStu->name, pStu->age);
}

int main()
{
// 声明变量同时初始化成员值
struct Student stu01 = {"zhangsan", 18};
// 先声明变量,然后给成员赋值
struct Student stu02;
strcpy(stu02.name, "lisi");
stu02.age = 20;

printAge(&stu01);
printAge(&stu02);
}

注:

  • 结构体变量只能在声明时整体初始化,不能在声明之后整体赋值,给一个已经声明的结构体变量赋值时只能单独对其成员赋值;
  • 数组也是类似,只能整体初始化,不能整体赋值。如果是字符数组想要整体赋值的话,可以使用strcpy函数:
    char * __cdecl strcpy(char * __restrict__ _Dest,const char * __restrict__ _Source);
    需要引入头文件string.h

(2) 方式二:定义结构体时声明变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <string.h>

struct Student
{
char name[20];
int age;
}stu01, stu02;
/**
相当于
struct Student stu01;
struct Student stu02;
*/
void printAge(struct Student *pStu)
{
printf("%s's age is %d. \n", pStu->name, pStu->age);
}

int main()
{
strcpy(stu01.name, "zhangsan");
stu01.age = 18;
strcpy(stu02.name, "lisi");
stu02.age = 20;

printAge(&stu01);
printAge(&stu02);
}

也可以直接在定义结构体和变量之后接着赋值:

1
2
3
4
5
struct Student
{
char name[20];
int age;
}stu01 = {"zhangsan", 18}, stu02 = {"lisi", 20};

(3) 匿名结构体

1
2
3
4
5
struct
{
char name[20];
int age;
}stu01 = {"zhangsan", 18}, stu02 = {"lisi", 20};

这种形式只能使用在声明结构体的同时也定义出结构体变量,由于没有结构体名,因此后续不可以再定义新的结构体变量。

(4) 定义结构体同时取别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>

typedef struct Student
{
char name[20];
int age;
}Stu;

int main()
{
Stu stu01 = {"zhangsan", 20};
Stu stu02;
strcpy(stu02.name, "lisi");
stu02.age = 18;

printf("%s's age is %d\n",stu01.name, stu01.age);
printf("%s's age is %d\n",stu02.name, stu02.age);
}

也可以给匿名结构体取别名:

1
2
3
4
5
typedef struct
{
char name[20];
int age;
}Stu;

这种形式声明了一个匿名结构体,但同时使用typedef为结构体设置了别名,所以之后依然可以使用这个别名去定义结构体变量。

3.内存对齐原则

结构体的对齐规则:

  • 第一个成员在与结构体变量偏移量为0的地址处
  • 其他的成员变量要对其到某个数字(对齐数:对齐数=编译器默认的一个对齐数与该成员大小的较小值,vs中默认值为8)的整数倍的地址处
  • 结构体总大小为最大对齐数(每一个成员变量都有一个对齐数)的整数倍
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

具体参考:

4.结构体指针

(1) 定义声明方式
结构体指针就是指向结构体变量的指针,表示的是这个结构体变量占内存中的起始位置。
结构体指针方式的定义方式例如:

1
2
3
4
5
6
7
struct Student
{
char name[20];
int age;
}stu01 = {"zhangsan", 18};

struct Student *pStu01 = &stu01;

也可以在定义结构体的同时定义结构体指针:

1
2
3
4
5
struct Student
{
char name[20];
int age;
}stu01 = {"zhangsan", 18}, *pStu01 = &stu01;

或者:

1
2
3
struct Student stu01;
struct Student *pStu01;
pStu01 = &stu01;

(2) 通过结构体指针访问成员
通过结构体指针可以获取结构体成员,一般形式为:
(*pointer).memberName
或者:
pointer->memberName

第一种写法中,.的优先级高于*(*pointer)两边的括号不能少;
第二种写法中,->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员,这也是->在C语言中的唯一用途;
以上的两种写法是等效的,通常采用后面的写法。

(3) 结构体指针作为函数参数
结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。
例如,输出一组学生的平均成绩和最高分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <string.h>

struct Student {
int stuNum;
char name[20];
float score;
}stus[] = {
{101, "lihua", 80.5},
{102, "zhangsan", 92.0},
{103, "wangwu", 88.5},
{104, "zhangwei", 90.5},
{105, "heliang", 85.0}
};

void analyseGrade(struct Student *pStus, int len);

int main()
{
int len = sizeof(stus)/sizeof(struct Student);
analyseGrade(stus, len);
return 0;
}

void analyseGrade(struct Student *pStus, int len)
{
int i;
float sum = 0;
float max = 0;
for(i=0;i<len;i++)
{
sum += (pStus+i)->score;
if((pStus+i)->score > max)
max = (pStus+i)->score;
}
printf("average score = %.2f\ntop score = %.2f\n", sum/len, max);

}

5.结构体数组

(1) 定义、声明、初始化
示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Student {
int stuNum;
char name[20];
float score;
};
int main()
{
int i;
struct Student stu[3];
for(i=0;i<3;i++) {
printf("请输入第%d个学生的信息:", i+1);
scanf ("%d%s%f", &stu[i].stuNum, &stu[i].name, &stu[i].score);
}
//......
}

示例2,在定义同时初始化:

1
2
3
4
5
6
7
8
9
10
11
12
struct Student {
int stuNum;
char name[20];
float score;
};
struct Student stus[5]= {
{101, "lihua", 80.5},
{102, "zhangsan", 92.0},
{103, "wangwu", 88.5},
{104, "zhangwei", 90.5},
{105, "heliang", 85.0}
};

或者:

1
2
3
4
5
6
7
8
9
10
11
struct Student {
int stuNum;
char name[20];
float score;
}stus[] = { //当对数组中全部元素赋值时,可不给出数组长度
{101, "lihua", 80.5},
{102, "zhangsan", 92.0},
{103, "wangwu", 88.5},
{104, "zhangwei", 90.5},
{105, "heliang", 85.0}
};

(2) 数组元素的引用

一般形式:
数组名[下标].成员名
如:

1
stu[0].score=89.5

6.位段

(1)概念

C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”(bit field) 。利用位段能够用较少的位数存储数据。

(2)声明和使用

  • 位段的成员可以是intunsigned intsigned intchar(属于整形家族)类型
  • 位段的成员名后边有一个冒号和一个数字

示例:

1
2
3
4
5
6
7
struct A
{
int _a:2; //开辟一个32个bit位,a占了2个
int _b:5; //在a开辟的bit位中占5个bit
int _c:10;//在a开辟的bit位中占10个bit
int _d:30;//另外开辟32个bit位,d占了30个
};

(3)位段大小计算
计算位段的大小需注意:

  • 位段不能跨字节存储
  • 位段不能跨类型存储

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>

struct Test
{
char a : 1;
char b : 6;
char c : 3;
}Test;

int main()
{
printf("size = %d\n", sizeof(struct Test));
return 0;
}

结果:size = 2

在第一个字节都被a,b占用的情况下,只剩一个bit位,而c要占3个,又因为位段不能跨字节存储,所以只能另外开辟一个字节来存储。所以大小应该为2。

示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>

struct Test
{
char a : 1;
char b : 6;
int c : 1;
}Test;

int main()
{
printf("size = %d\n", sizeof(struct Test));
return 0;
}

结果:size = 8

a和b共用一个字节后只剩下一个bit位,刚好c也只需要一个bit位,那这里能不能存呢?显然,当然不可以。位段不能跨类型存储,所以这里的c要从新开辟4个字节的空间,占用其中的1bit,最后别忘了要和结构体补齐,结果为8。

8.2 共用体

1.共用体概念

在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作”共用体”类型结构,简称共用体,也叫联合体。

2.声明和使用
共用体的关键字是union,其声明和使用的方式和结构体类似,如:

1
2
3
4
5
6
7
8
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;

结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。

结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

共用体在一般的编程中应用较少,在单片机中应用较多。使用实例可参考这篇博客

8.3 枚举

(1)概念

enum,枚举在C/C++/c#,还有Objective-C中,是一个被命名的整型常数的集合,枚举在日常生活中很常见。例如表示星期的SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, 就是一个枚举。枚举的声明与结构和联合相似。

(2)使用

枚举类型的定义形式为:
enum typeName{valueName1, valueName2, valueName3, ...... };

例如,列出一个星期有几天:

1
enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };

只给了成员名字,没给出名字对应的值的话,枚举值就会默认从0开始,逐个加1递增,即week 中的 Mon、Tues …… Sun 对应的值分别为 0、1 …… 6。

也可以指定每个名字对应的值,如:

1
enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };

或者只给第一个名字指定值:

1
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };

这样枚举值就从 1 开始递增,跟上面的写法是等效的。

定义枚举变量并赋值:

1
2
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a = Mon, b = Wed, c = Sat;

或者:

1
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;

使用示例,判断用户输入的数字代表星期几:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(){
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;
scanf("%d", &day);
switch(day){
case Mon: puts("Monday"); break;
case Tues: puts("Tuesday"); break;
case Wed: puts("Wednesday"); break;
case Thurs: puts("Thursday"); break;
case Fri: puts("Friday"); break;
case Sat: puts("Saturday"); break;
case Sun: puts("Sunday"); break;
default: puts("Error!");
}
return 0;
}

注意:

  • 枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量
  • Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量

枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。

9 文件操作

9.1 文件概念

存储在内存储器的集合,一般称为表,如数组;而存储在外部介质上的信息集合称为文件,如磁盘文件。
文件通常是驻留在外部介质(如磁盘等)上的,使用时才调入内存。

1.文件分类

(1) 从用户的角度分为:普通文件和设备文件

  • 普通文件:驻留在磁盘或其他外部介质上的一个有序数据集,其又分为:
    • 程序文件:如C源文件(后缀为.c)、目标文件(后缀为.obj)、可执行程序(.exe)
    • 数据文件:如一组待输入处理的原始数据,或者是一组输出的结果
  • 设备文件:与主机相连的各种外部设备,如显示器、打印机、键盘等。在操作系统中,把外部设备也看做一个文件来进行管理,把它们的输入、输出等同于对磁盘文件的读和写。如:通常把显示器定义为标准输出文件,在屏幕上显示有关信息就是向标准输出文件输出;把键盘被指定为标准输入文件,从标准输入文件上输入数据。

常见硬件设备所对应的文件:

文件 硬件设备
stdin 标准输入文件,一般指键盘;scanf()、getchar() 等函数默认从 stdin 获取输入。
stdout 标准输出文件,一般指显示器;printf()、putchar() 等函数默认向 stdout 输出数据。
stderr 标准错误文件,一般指显示器;perror() 等函数默认向 stderr 输出数据(后续会讲到)。
stdprn 标准打印文件,一般指打印机。

(2) 从文件编码的方式的角度:ASCII码文件(文本文件)和二进制文件

  • ASCII码文件:也称为文本文件,这种文件在磁盘中存放时每个字符对于1个字节,用于存放对应的ASCII码,ASCII码文件可在屏幕上按字符显示,因此能够读懂文件内容。
    • ASCII码文件以字符形式存储,读写位复制,需要转换,传输效率低,占用外存空间较大
    • 例如:数1234按ASCII码存储,为00110001 00110010 00110011 00110100,占4个字节
  • 二进制文件:是按二进制的编码方式来存放文件的,只占2个字节,二进制文件虽然也可以在屏幕上显示,但其内容无法读懂
    • 二进制文件的存储形式与数据在内存中的存储形式相同,读写位复制,不需要转换,传输效率高,节省外存空间
    • 例如:数1234按二进制存储,为00000100 11010010,占2个字节

注:字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

2.文件缓冲区

ANSIC标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的输出缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存中的输入缓冲区,然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。当然,也并非时时刻刻都得具备这样的条件才能传入传出数据,若遇到紧急的条件无需等到缓冲区装满就可以直接传送。

注:因为缓冲区的存在。C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束时关闭文件。若不做,可能会导致读写文件的问题。

9.2 文件操作

1.文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。

例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

1
2
3
4
5
6
7
8
9
10
11
12
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};

typedef struct _iobuf FILE;

每当打开一个文件,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,一般都是通过一个FILE的指针来维护这个FILE结构的变量,在这个FILE结构体中第一个变量为文件名,通过文件名就可去维护相应的文件。

创建一个FILE*的指针变量:

1
FILE* pf;//文件指针变量

2.文件的打开与关闭

文件的打开与关闭都有相应的函数,如fopenfclose,且是成对出现的,上文也提到了若刷新缓冲区或者在文件操作结束时未关闭文件,就会导致出现读写文件的问题。在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

(1) 打开文件fopen函数:

1
FILE * fopen ( const char * filename, const char * mode );
  • filename是指定打开的文件名,可以包含盘符、路径、文件名,是字符串

  • mode指定打开的文件读写方式,是字符串,必须小写

  • 返回指定打开的文件的指针

文件的打开方式mode有:

文件使用方式 含义 如果指定文件不存在
“ r ”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
“ w ”(只写) 为了输入数据,打开一个文本文件 建立一个新的文件
“ a ”(追加) 向文本文件尾添加数据 建立一个新的文件
“ r+ ”(读写) 为了读和写,打开一个文本文件 出错
“ w+ ”(读写) 为了读和写,建立一个新的文件 建立一个新的文件
“ a+ ”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“ rb ”(只读) 为了输入数据,打开一个二进制文件 出错
“ wb ”(只写) 为了输入数据,打开一个二进制文件 建立一个新的文件
“ ab ”(追加) 向一个二进制文件尾添加数据 出错
“ rb+ ”(读写) 为了读和写,打开一个二进制文件 出错
“ wb+ ”(读写) 为了读和写,建立一个新的二进制文件 建立一个新的文件
“ ab+ ”(读写) 打开一个二进制文件,在文件尾进行读写 建立一个新的文件

其中,控制读写方式的字符串含义如下(可以不写):

打开方式 含义
“t” 文本文件。如果不写,默认为"t"
“b” 二进制文件。

整体来说,文件打开方式由 r、w、a、t、b、+ 六个字符拼成,各字符的含义是:

  • r(read):读
  • w(write):写
  • a(append):追加
  • t(text):文本文件
  • b(binary):二进制文件
  • +:读和写

(2) 关闭文件fclose函数:

1
int fclose ( FILE * stream );

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.dat", "w");
if (pf == NULL)
{
perror("fopen");
return 0;
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}

3.文件的顺序读写

(1) C语言中流的概念

在C语言中,”流”(stream)是一个用于输入和输出数据的抽象概念。它是连接程序与外部数据源或数据目标(如文件、终端、网络等)之间的桥梁。流提供了一种统一的方式来处理不同类型的输入和输出,使得程序能够以一致的方式与这些数据源或数据目标进行交互。

关于流的概念其他比较好的解释:

  • K&R 在 C Programming Language 书中提到流是这样定义的:流 (stream) 是与磁盘或其它外围设备关联的数据的源或目的地。
  • 流(stream)是一种面向多种设备的(通常是文件(file))的逻辑的接口(logical interface)。

根据数据形式,流可以分为文本流(字符流)和二进制流。

文本流和二进制流的主要差异:

  • 在文本流中输入输出的数据是字符或字符串,可以被修改;
  • 二进制流中输入输出是一系列二进制的0、1代码,不能以任何方式修改。

任何一个C语言程序运行的时候,会默认打开3个流,其类型都是 FILE*:

  • stdin —— 标准输入流(键盘)
  • stdout —— 标准输出流(屏幕)
  • stderr —— 标准错误流(屏幕)

更多关于流的基本概念参考这篇博客

(2) 常用函数

函数名 功能 适用性
fgetc() 字符输入函数 所有输入流
fputc() 字符输出函数 所有输出流
fgets() 文本行输入函数 所有输入流
fputs() 文本行输出函数 所有输出流
fscanf() 格式化输入函数 所有输入流
fprintf() 格式化输出函数 所有输出流
fread() 二进制输入 文件
fwrite() 二进制输出 文件

具体使用方式参考这两位大佬的博客:

10 预处理

10.1 预处理概念

C语言由源代码生成可执行程序的各阶段如下:
C源程序 -> 编译预处理 -> 编译、优化 -> 汇编程序 -> 链接程序 -> 可执行文件
在编译和链接之前,还需要对源文件进行一些文本方面的操作,比如文本替换、文件包含、删除部分代码等,这个过程叫做预处理,由预处理程序完成。

10.2 预处理指令与使用

预编译指令:

指令 描述
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译以下代码
#endif 结束一个 #if … #else 条件编译块
#error 当遇到标准错误时,输出错误信息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中
#using 将元数据导入程序编译
#line 指令告诉预处理器将编译器内部存储的行号和文件名更改为给定行号和文件名
# 空指令,无任何效果

具体使用方式参考这篇博客

11 多文件编程

未完待续~


本篇博客参考了:

感谢以上博客、教程及大佬们带来的启发和引导!


C语言复习(持续更新~)
https://blog.kevinchu.top/2023/08/23/c-notes/
作者
Kevin Chu
发布于
2023年8月23日
许可协议