C语言基础语法
数据类型与变量
(1)变量是内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化。
(2)通过变量名,可以引用这块内存区域,获取里面存储的值。
(3)变量的构成包含三个要素:数据类型、变量名、存储的值。
变量必须先声明,后使用。可以先声明变量再赋值,也可以在声明变量的同时进行赋值。
标识符
C语言中变量、函数、数组名、结构体等要素命名时使用的字符序列,称为标识符
- 标识符由字母、数字和下划线组成,且第一个字符必须是字母或下划线。
- 标识符区分大小写,长度限制取决于编译器和目标平台的具体实现
- 标识符不能是C语言的关键字。
- 标识符命名应具有描述性,便于理解和维护。
整数类型
(1) 基本整数类型
类型 | 存储大小 (通常) | 取值范围 (通常) | 格式化符号 |
---|---|---|---|
char | 1字节 | -128 到 127 或 0 到 255 | %c |
short | 2字节 | -32,768 到 32,767 | %hd |
int | 4字节 | -2,147,483,648 到 2,147,483,647 | %d |
long | 4或8字节 | 取决于平台 | %ld |
long long | 8字节 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | %lld |
(2) 修饰符
signed
:有符号数(默认)unsigned
:无符号数(范围从0开始)short
:缩短整数长度long
:扩展整数长度
unsigned char byte = 255; // 0 到 255
signed short temperature = -30;
unsigned long population = 7800000000;
C99新增类型: 固定宽度整数类型
#include <stdint.h>
int8_t small; // 精确8位有符号整数
uint16_t medium; // 精确16位无符号整数
int32_t large; // 精确32位有符号整数
int64_t huge; // 精确64位有符号整数
浮点类型
类型 | 存储大小 | 精度 | 取值范围 | 格式化符号 |
---|---|---|---|---|
float | 4字节 | 6-7位小数 | ±1.2×10^-38 到 ±3.4×10^38 | %f |
double | 8字节 | 15-16位小数 | ±2.3×10^-308 到 ±1.7×10^308 | %lf |
long double | 10或16字节 | 19-20位小数 | 更大范围 | %Lf |
float pi = 3.14159f;
double atomic_mass = 1.66053906660e-27;
long double very_precise = 3.141592653589793238L;
C99新增类型: 复数类型
#include <complex.h>
double complex z = 1.0 + 2.0 * I;
void类型
- 表示"无类型"
- 主要用途:
- 函数不返回任何值:
void func()
- 函数无参数:
int func(void)
- 通用指针:
void*
- 函数不返回任何值:
数据类型转换
1. 隐式类型转换
自动发生的转换,遵循类型提升规则
int i = 10;
float f = 3.14;
double d = i + f; // i转换为float,然后结果转换为double
2. 显式类型转换(强制转换)
double x = 3.14159;
int y = (int)x; // y = 3
int a = 10, b = 3;
float result = (float)a / b; // 3.333...
强制转换可能导致数据丢失,应谨慎使用。
布尔类型
C99标准引入的布尔类型(_Bool
或bool
)本质上仍然是一种整数类型,其核心特点是:
- 存储形式:实际仍以整数值存储(通常1字节)
- 取值规范:
0
表示假(false
),任何非0
值表示真(true
) - 类型安全:相比传统方式,提供了更明确的语义标记
布尔类型的底层实现:
// C标准中的定义(通常实现方式)
#define bool _Bool
#define true 1
#define false 0
布尔变量在内存中仍占用至少1字节空间,但逻辑上只应包含0或1。
C99前的真假表示方法
在C99标准引入<stdbool.h>
之前,开发者使用多种方式表示布尔值:
1. 直接使用整数
int is_ready = 1; // 真
int is_empty = 0; // 假
2. 自定义宏定义
#define TRUE 1
#define FALSE 0
typedef int BOOL;
BOOL flag = TRUE;
3. 枚举类型
typedef enum { false, true } bool;
bool file_exists = true;
4. 位字段(结构体位域)
struct {
unsigned int is_active : 1; // 只使用1位
} status;
status.is_active = 1;
布尔类型标准头文件及基本用法:
#include <stdbool.h>
该头文件提供:
bool
:布尔类型别名(实际为_Bool
)true
:值为1的常量false
:值为0的常量
bool is_raining = true;
bool is_sunny = false;
if (is_raining) {
printf("Take an umbrella\n");
}
内存占用:
printf("%zu\n", sizeof(bool)); // 通常是1(但标准只要求至少1)
与旧代码的兼容
// 新旧布尔类型混用时的安全写法
#ifdef __STDC_VERSION__
#include <stdbool.h>
#else
typedef enum { false, true } bool;
#endif
typedef别名
typedef:为现有类型创建别名, 提高代码可读性和可移植性
typedef unsigned char BYTE;
typedef struct {
int x;
int y;
} Point;
BYTE data = 255;
Point p1 = {10, 20};
常量的定义
在C语言中,常量是程序运行期间不可改变的值。C语言提供了两种主要方式来定义常量:使用预处理指令#define
和使用const
关键字。
常量是程序中固定不变的值,与变量相对。使用常量的好处包括:
- 提高代码可读性(如
PI
比3.14159
更易理解) - 便于统一修改(只需修改一处定义)
- 避免意外修改导致错误
#define
宏定义常量
- 预处理阶段替换:在编译前由预处理器进行文本替换
- 无类型检查:只是简单的文本替换,不进行类型验证
- 不分配内存:因为只是文本替换,没有存储概念
- 作用域:从定义处到文件末尾,或直到
#undef
- 常见用途:定义数值常量、字符串常量、条件编译
#include <stdio.h>
#define PI 3.14159
#define MAX_SIZE 100
#define WELCOME_MSG "Hello, World!"
int main() {
double area = PI * 5 * 5; // 编译前会被替换为 3.14159 * 5 * 5
printf("%s\n", WELCOME_MSG);
printf("Max elements: %d\n", MAX_SIZE);
return 0;
}
const
常量
- 编译期处理:由编译器处理,是真正的语言特性
- 类型安全:有明确的类型,编译器会进行类型检查
- 分配内存:通常分配只读存储空间(取决于实现)
- 作用域:遵循C语言的作用域规则(块作用域、文件作用域等)
- 常见用途:需要类型检查的常量、数组大小、函数参数
#include <stdio.h>
const double PI = 3.14159;
const int MAX_SIZE = 100;
const char WELCOME_MSG[] = "Hello, World!";
int main() {
const int local_const = 42; // 局部常量
double area = PI * 5 * 5; // 使用方式与变量相同
printf("%s\n", WELCOME_MSG);
printf("Max elements: %d\n", MAX_SIZE);
printf("Local const: %d\n", local_const);
return 0;
}
#define
与 const
的关键区别
特性 | #define | const |
---|---|---|
处理阶段 | 预处理阶段(文本替换) | 编译阶段 |
类型检查 | 无类型 | 有类型,编译器会检查 |
内存分配 | 不分配内存 | 通常分配只读内存 |
作用域 | 文件作用域(可被#undef取消) | 遵循C语言作用域规则 |
调试可见性 | 调试器中不可见(已被替换) | 调试器中可见 |
数组大小定义 | 可用于静态数组大小 | C89中不能用于静态数组大小 |
指针使用 | 不能定义指向常量的指针 | 可以定义指向常量的指针 |
复合类型 | 不能定义结构体等复合类型常量 | 可以定义复合类型常量 |
常量的高级用法
#define
的高级用法
// 带参数的宏
#define MAX(a,b) ((a) > (b) ? (a) : (b))
// 字符串化
#define STR(x) #x
printf("%s\n", STR(Hello)); // 输出 "Hello"
// 符号连接
#define CONCAT(a,b) a##b
int CONCAT(var,1) = 10; // 相当于 int var1 = 10;
const
的高级用法
// 指针常量
const char *p1 = "Hello"; // 指向常量的指针
char *const p2 = "World"; // 常量指针
// 结构体常量
const struct Point {
int x;
int y;
} origin = {0, 0};
// 常量数组
const int days[] = {31, 28, 31, 30, 31};
注意事项
宏的副作用:
#define SQUARE(x) x * x int result = SQUARE(2+3); // 展开为 2+3 * 2+3 = 11,不是25 // 应改为: #define SQUARE(x) ((x) * (x))
C与C++的区别: 在C++中,
const
常量默认有内部链接(可通过extern
改变), C++中const
可以用于数组大小定义存储位置:
const
变量通常存储在只读数据段(取决于实现),某些嵌入式系统中可能存储在Flash而非RAM
现代C的最佳实践——对于C99及以上版本的项目:
// 现代C推荐方式
const double PI = 3.141592653589793;
const int MAX_USERS = 1000;
// 必须使用宏的情况
#define DEBUG_MODE 1
#ifdef DEBUG_MODE
// 调试专用代码
#endif
- 优先使用
const
获得类型安全,仅在不适合使用const
时才用#define
- 对宏定义使用全大写命名以便识别,为宏参数添加括号避免优先级问题
常见运算符及应用
C语言中的运算符是用于执行各种操作(如算术运算、逻辑判断、位操作等)的符号。它们在程序中广泛使用,用于处理数据、控制流程和实现复杂逻辑。
算术运算符
算术运算符用于执行基本的数学运算。
运算符 | 描述 | 示例 | 结果(假设 a = 10, b = 3 ) |
---|---|---|---|
+ | 加法 | a + b | 13 |
- | 减法 | a - b | 7 |
* | 乘法 | a * b | 30 |
/ | 除法 | a / b | 3 (整数除法,取商) |
% | 取模(求余数) | a % b | 1 (余数为1) |
++ | 自增(前/后自增) | a++ 或 ++a | a = 11 |
-- | 自减(前/后自减) | b-- 或 --b | b = 2 |
注意:
/
和%
的行为取决于操作数类型:- 如果操作数是整数,则
/
执行整数除法。 - 如果操作数是浮点数,则
/
执行浮点除法。
- 如果操作数是整数,则
- 自增和自减运算符:
- 前置:先修改变量值,再使用。
- 后置:先使用变量值,再修改。
关系运算符
关系运算符用于比较两个值,并返回布尔值(真或假)。C语言中用非零表示真,零表示假。
运算符 | 描述 | 示例 | 结果(假设 a = 10, b = 3 ) |
---|---|---|---|
== | 等于 | a == b | 0 (假) |
!= | 不等于 | a != b | 1 (真) |
> | 大于 | a > b | 1 (真) |
< | 小于 | a < b | 0 (假) |
>= | 大于等于 | a >= b | 1 (真) |
<= | 小于等于 | a <= b | 0 (假) |
逻辑运算符
逻辑运算符用于组合多个条件表达式。
运算符 | 描述 | 示例 | 结果(假设 a = 1, b = 0 ) |
---|---|---|---|
&& | 逻辑与 | a && b | 0 (假) |
|| | 逻辑或 | a || b | 1 (真) |
! | 逻辑非 | !a | 0 (假) |
注意:
&&
和||
具有短路特性:- 对于
&&
,如果第一个条件为假,则不计算第二个条件。 - 对于
||
,如果第一个条件为真,则不计算第二个条件。
- 对于
位运算符
位运算符直接对整数的二进制位进行操作。
运算符 | 描述 | 示例 | 结果(假设 a = 5 (0101), b = 3 (0011) ) |
---|---|---|---|
& | 按位与 | a & b | 1 (0001 ) |
| | 按位或 | a | b | 7 (0111 ) |
^ | 按位异或 | a ^ b | 6 (0110 ) |
~ | 按位取反 | ~a | -6 (补码表示) |
<< | 左移 | a << 1 | 10 (1010 ) |
>> | 右移 | a >> 1 | 2 (0010 ) |
注意:
- 左移相当于将数值乘以2的幂次。
- 右移相当于将数值除以2的幂次(对于无符号数)。
赋值运算符
赋值运算符用于将一个值赋给变量。
运算符 | 描述 | 示例 | 等价形式 |
---|---|---|---|
= | 简单赋值 | a = b | a = b |
+= | 加法赋值 | a += b | a = a + b |
-= | 减法赋值 | a -= b | a = a - b |
*= | 乘法赋值 | a *= b | a = a * b |
/= | 除法赋值 | a /= b | a = a / b |
%= | 取模赋值 | a %= b | a = a % b |
<<= | 左移赋值 | a <<= b | a = a << b |
>>= | 右移赋值 | a >>= b | a = a >> b |
&= | 按位与赋值 | a &= b | a = a & b |
|= | 按位或赋值 | a |= b | a = a | b |
^= | 按位异或赋值 | a ^= b | a = a ^ b |
条件运算符
条件运算符是唯一的三目运算符,用于根据条件选择不同的值。
语法:condition ? value_if_true : value_if_false
int a = 10, b = 20;
int max = (a > b) ? a : b; // 如果 a > b,max = a;否则 max = b
printf("Max: %d\n", max); // 输出:Max: 20
其他运算符
逗号运算符 (
,
):- 用于将多个表达式连接在一起,整个表达式的值是最后一个表达式的值。
- 示例:
int a = (1, 2, 3); // a = 3
取地址运算符 (
&
) 和 指针解引用运算符 (*
):- 用于操作内存地址。
- 示例:
int x = 10; int *p = &x; // p 存储 x 的地址 printf("%d\n", *p); // 输出:10
sizeof 运算符:
- 返回变量或数据类型的大小(以字节为单位)。
- 示例:
int size = sizeof(int); // size = 4(通常为4字节)
成员访问运算符:
.
:用于访问结构体或联合体的成员。->
:用于通过指针访问结构体或联合体的成员。- 示例:
struct Point { int x, y; }; struct Point p = {10, 20}; printf("%d\n", p.x); // 输出:10 struct Point *ptr = &p; printf("%d\n", ptr->y); // 输出:20
运算符优先级
C语言中的运算符具有不同的优先级和结合性,这决定了表达式的计算顺序。
优先级 | 运算符 | 结合性 |
---|---|---|
1 | () [] -> . | 从左到右 |
2 | ! ~ ++ -- | 从右到左 |
3 | * / % | 从左到右 |
4 | + - | 从左到右 |
5 | << >> | 从左到右 |
6 | < <= > >= | 从左到右 |
7 | == != | 从左到右 |
8 | & | 从左到右 |
9 | ^ | 从左到右 |
10 | | | 从左到右 |
11 | && | 从左到右 |
12 | ` | |
13 | ?: | 从右到左 |
14 | = += -= 等 | 从右到左 |
15 | , | 从左到右 |
建议: 在复杂的表达式中,使用括号明确优先级,以提高代码可读性和避免错误。
程序控制语句
C语言中的程序控制语句用于控制程序的执行流程,包括条件判断、循环和跳转等。这些控制语句使得程序可以根据不同的条件执行不同的代码块,或者重复执行某些操作,从而实现复杂的功能。
- 条件语句:
if
、if-else
、if-else if-else
和switch
。 - 循环语句:
for
、while
和do-while
。 - 跳转语句:
break
、continue
、goto
和return
。
合理使用这些控制语句,可以使你的代码更加清晰、高效和易于维护。同时,建议避免过度使用 goto
,以保持代码的可读性和结构化风格。
条件语句
if
语句: 用于在满足某个条件时执行特定代码块。
int x = 10;
if (x > 5) {
printf("x is greater than 5\n");
}
if-else
语句: 用于在条件为真时执行一个代码块,否则执行另一个代码块。
int x = 3;
if (x > 5) {
printf("x is greater than 5\n");
} else {
printf("x is less than or equal to 5\n");
}
if-else if-else
语句: 当需要检查多个条件时,可以使用if-else if-else
结构。
int score = 85;
if (score >= 90) {
printf("Grade: A\n");
} else if (score >= 75) {
printf("Grade: B\n");
} else {
printf("Grade: C\n");
}
switch
语句:用于基于变量的值执行多个代码块之一。
- 每个
case
后面必须有break
,否则会继续执行后续的case
代码(称为“贯穿”)。 default
是可选的,用于处理没有匹配的case
的情况。
int day = 3;
switch (day) {
case 1:
printf("Monday\n");
break;
case 2:
printf("Tuesday\n");
break;
case 3:
printf("Wednesday\n");
break;
default:
printf("Invalid day\n");
}
循环语句
for
循环: 用于重复执行一段代码固定次数或基于某个条件。
for (int i = 1; i <= 5; i++) {
printf("%d ", i);
}
// 输出:1 2 3 4 5
while
循环: 在条件为真时重复执行代码块。
int i = 1;
while (i <= 5) {
printf("%d ", i);
i++;
}
// 输出:1 2 3 4 5
do-while
循环:至少执行一次循环体,然后在条件为真时继续执行。
int i = 1;
do {
printf("%d ", i);
i++;
} while (i <= 5);
// 输出:1 2 3 4 5
跳转语句
break
:立即退出当前循环或switch
语句。
for (int i = 1; i <= 10; i++) {
if (i == 5) {
break; // 退出循环
}
printf("%d ", i);
}
// 输出:1 2 3 4
continue
:用于跳过当前迭代的剩余部分,直接进入下一次迭代。
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 跳过 i == 3 的迭代
}
printf("%d ", i);
}
// 输出:1 2 4 5
goto
:用于无条件跳转到指定标签处。虽然不推荐频繁使用,但在某些情况下(如错误处理)可能有用。
int x = 10;
if (x > 5) {
goto end;
}
printf("This will not be printed.\n");
end:
printf("Jumped to the end.\n");
// 输出:Jumped to the end.
return
: 用于从函数中返回值,并结束函数的执行。
int add(int a, int b) {
return a + b;
}
stream和文件I/O
C语言的标准输入输出功能由stdio.h
头文件提供的一系列函数支持。这些函数提供了与用户进行交互的能力,允许程序读取用户的输入或将信息输出给用户
流(stream)
C语言的I/O设计基于流(stream)的概念,为各种设备提供了统一的接口.
流(Stream): 抽象数据源/目的地:流是对输入输出设备的抽象。主要有两种基本类型:
- 文本流:由字符组成的序列
- 二进制流:由原始字节组成的序列
常见预定义流:stdin:标准输入流(键盘)
, stdout:标准输出流(屏幕)
, stderr:标准错误流(屏幕)
常用的流(stream)处理函数:
打开流
FILE *fopen(const char *path, const char *mode);
用来打开一个文件并返回一个指向FILE
结构体的指针。mode
参数决定了打开文件的方式(读、写等)。
关闭流
int fclose(FILE *fp);
关闭由fopen()
打开的文件,释放资源。
读取和写入流
int fprintf(FILE *stream, const char *format, ...);
类似于printf()
,但允许指定不同的流。size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
从流中读取数据到内存块中。size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
将内存块中的数据写入流。
其他操作
int fgetc(FILE *stream);
和int fputc(int c, FILE *stream);
分别用于从流中读取字符和向流中写入字符。char *fgets(char *s, int size, FILE *stream);
和int fputs(const char *s, FILE *stream);
分别用于从流中读取一行字符串和向流中写入一行字符串。int fseek(FILE *stream, long offset, int whence);
改变流的位置指示器。
这里给出一个简单的例子,展示如何使用流来读写文件:
#include <stdio.h>
int main() {
// 打开文件以写模式
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
// 向文件写入数据
fprintf(file, "Hello, World!\n");
// 关闭文件
fclose(file);
// 重新打开文件以读模式
file = fopen("example.txt", "r");
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
// 创建缓冲区
char buffer[100];
// 从文件读取数据
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}
// 关闭文件
fclose(file);
return 0;
}
printf() 函数
printf() 函数 用于格式化输出数据到终端或指定的输出流, int printf(const char *format, ...);
#include <stdio.h>
int main() {
int num = 5;
printf("The number is %d\n", num);
return 0;
}
在这个例子中,%d
是一个占位符,表示将要插入一个整数类型的值。
说明符 | 用途 | 示例 |
---|---|---|
%d | 十进制整数 | 123 |
%u | 无符号十进制 | 456u |
%f | 浮点数 | 3.14159 |
%c | 单个字符 | 'A' |
%s | 字符串 | "text" |
%p | 指针地址 | &variable |
%x | 十六进制(小写) | 0x1a3 |
%X | 十六进制(大写) | 0X1A3 |
%% | 百分号本身 | % |
printf()函数向标准输出(stdout)写入数据,默认情况下就是终端屏幕。它本质上是对标准输出流的操作。在底层,printf() 使用了 stdout 这个预定义的 FILE* 流指针。
fprintf()
fprintf():类似于 printf(),但允许指定不同的流
FILE *file = fopen("example.txt", "w");
if (file != NULL) {
fprintf(file, "Hello, file!\n");
fclose(file);
}
scanf() 函数
scanf() 函数用于从标准输入(通常是键盘)读取数据并根据指定的格式存储到变量中:int scanf(const char *format, ...);
#include <stdio.h>
int main() {
int age;
printf("Enter your age: ");
scanf("%d", &age);
printf("Your age is %d\n", age);
return 0;
}
注意这里需要使用变量的地址(&age
),因为scanf()
需要知道在哪里存储输入的数据。
格式化字符串的安全性:当使用printf()
和scanf()
时,确保提供的格式字符串与实际参数匹配,否则可能导致未定义行为。
scanf()函数从标准输入(stdin)读取数据,默认情况下是从键盘读取。同样地,它也是对标准输入流的操作,使用了 stdin 这个预定义的 FILE* 流指针。
fscanf()
fscanf():类似于 scanf(),但可以从指定的流中读取数据
FILE *file = fopen("example.txt", "r");
if (file != NULL) {
char buffer[100];
fscanf(file, "%s", buffer);
printf("Read from file: %s\n", buffer);
fclose(file);
}
行 I/O 函数
fgets(char *str, int n, FILE *stream)
- 用途:从指定流中读取最多
n-1
个字符(或直到遇到换行符\n
)到缓冲区str
,并在末尾添加一个空字符\0
。 - 优点:相比
gets()
,它允许指定缓冲区大小,从而避免了缓冲区溢出的风险。char buffer[100]; if (fgets(buffer, sizeof(buffer), stdin) != NULL) { printf("You entered: %s", buffer); }
- 用途:从指定流中读取最多
fputs(const char *str, FILE *stream)
- 用途:将字符串写入指定流中。与
puts()
不同的是,它不会自动添加换行符。 - 优点:适用于需要精确控制输出的情况。
fputs("Hello, World!\n", stdout);
- 用途:将字符串写入指定流中。与
gets(char *str)
和puts(const char *str)
gets(char *str)
- 用途:从标准输入读取一行文本并存储到指定的缓冲区
str
中,直到遇到换行符为止。不推荐使用。 - 安全问题:没有提供缓冲区大小限制,容易导致缓冲区溢出攻击,非常不安全。已经被C11标准弃用。
- 替代方案:使用
fgets(str, size, stdin)
代替gets(str)
。
- 用途:从标准输入读取一行文本并存储到指定的缓冲区
puts(const char *str)
- 用途:向标准输出打印一个字符串,并自动追加一个换行符。
- 局限性:不如
fputs()
灵活,因为它总是添加换行符且只能输出到标准输出。puts("Hello, World!");
getline()
和getdelim()
(POSIX标准)- 这些函数不是ANSI C标准的一部分,但被广泛支持,特别是在Unix/Linux系统中。
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
- 动态分配内存来读取整行输入,适合处理未知长度的输入行。
ssize_t getdelim(char **lineptr, size_t *n, int delimiter, FILE *stream);
- 类似于
getline()
,但是可以指定分隔符而不是默认的换行符。
- 类似于
安全注意事项
- 避免使用
gets()
:由于缺乏对缓冲区大小的检查,可能导致缓冲区溢出,这是严重的安全隐患。应始终使用fgets()
作为替代。 - 注意缓冲区大小:当使用
fgets()
时,确保传递正确的缓冲区大小参数以防止溢出。例如,如果缓冲区大小为100,则应该调用fgets(buffer, 100, stdin)
。 - 错误处理:无论是使用
fgets()
还是其他I/O函数,都应该检查返回值,以便正确处理可能发生的错误情况。例如,fgets()
在到达文件末尾或发生错误时会返回NULL
。
字符 I/O 函数
字符I/O函数在C语言中用于处理单个字符的读写操作,它们提供了比格式化输入输出(如printf()
和scanf()
)更细粒度的控制。这些函数特别适用于需要逐字符处理文本的应用场景。
fgetc(FILE *stream)
:从指定流中读取一个字符,并返回其值作为int
类型(以便可以区分EOF)。如果到达文件末尾或发生错误,则返回EOF
。#include <stdio.h> int main() { FILE *file = fopen("example.txt", "r"); if (file == NULL) { printf("无法打开文件\n"); return 1; } int ch; while ((ch = fgetc(file)) != EOF) { putchar(ch); // 输出到标准输出 } fclose(file); return 0; }
在这个例子中,程序逐字符读取文件内容并输出到屏幕。
fputc(int c, FILE *stream)
:将一个字符写入指定流中,并返回写入的字符。如果发生错误,则返回EOF
。#include <stdio.h> int main() { FILE *file = fopen("output.txt", "w"); if (file == NULL) { printf("无法打开文件\n"); return 1; } fputc('H', file); fputc('i', file); fputc('\n', file); // 添加换行符 fclose(file); return 0; }
这段代码向文件写入了字符串"Hi\n"。
getchar(void)
:从标准输入(通常是键盘)读取一个字符,并返回其值作为int
类型。等价于fgetc(stdin)
。#include <stdio.h> int main() { printf("请输入一个字符: "); int ch = getchar(); printf("你输入的字符是: %c\n", ch); return 0; }
putchar(int c)
:将一个字符写入到标准输出(通常是屏幕),并返回写入的字符。等价于fputc(c, stdout)
。#include <stdio.h> int main() { char message[] = "Hello, World!"; for (int i = 0; message[i] != '\0'; ++i) { putchar(message[i]); } putchar('\n'); // 添加换行符 return 0; }
ungetc(int c, FILE *stream)
:将一个字符推回到流中,使得下一次读取该流时首先读取这个字符。注意,大多数实现只允许一个字符被推回。#include <stdio.h> int main() { char ch; printf("请输入一个字符: "); ch = getchar(); ungetc(ch, stdin); // 将字符推回到输入流中 ch = getchar(); // 再次读取相同的字符 printf("你输入的字符是: %c\n", ch); return 0; }
文件I/O操作
在C语言中,所有文件操作都是基于流(stream)的概念进行的。流是一个抽象的概念,代表一个数据序列,无论是从文件读取还是向文件写入。每个流都与一个FILE*
类型的指针相关联,这个指针指向内部的文件控制块,包含了文件的状态和位置等信息。
打开文件:要对文件进行操作,首先需要打开文件。这可以通过fopen()
函数完成:
#include <stdio.h>
// 打开文件
FILE *fopen(const char *path, const char *mode);
path
: 文件路径。mode
: 文件打开模式,如"r"(只读)、"w"(写入,文件不存在则创建,存在则清空)等。模式 描述 "r"
只读模式。打开一个已存在的文本文件用于读取数据,文件指针位于文件开头。如果文件不存在,则函数返回 NULL
并设置错误标志。适用于只读操作。"w"
写模式。创建一个新的文本文件或覆盖一个已存在的文件进行写入操作,文件指针位于文件开头。如果文件存在,其内容会被清空。如果文件不存在,则创建新文件。 "a"
追加模式。打开一个文本文件用于在文件末尾追加数据,文件指针位于文件末尾。如果文件不存在,则创建新文件。尝试在其他位置写入会导致数据追加到文件末尾。 "r+"
读写模式。打开一个已存在的文本文件用于读取和写入数据,文件指针位于文件开头。如果文件不存在,则函数返回 NULL
并设置错误标志。适合需要同时读取和修改文件的应用。"w+"
读写模式(覆盖)。创建一个新的文本文件或覆盖一个已存在的文件进行读取和写入操作,文件指针位于文件开头。如果文件存在,其内容会被清空。如果文件不存在,则创建新文件。 "a+"
读写模式(追加)。打开一个文本文件用于在文件末尾追加数据并允许读取,文件指针位于文件末尾。即使使用 fseek()
移动文件指针,所有写操作仍然会在文件末尾进行。如果文件不存在,则创建新文件。
关闭文件:当不再需要访问文件时,应该关闭文件以释放资源:
int fclose(FILE *fp); // 成功时返回0,失败时返回EOF
常用文件I/O函数
读写文本文件
fgetc(FILE *stream)
和fputc(int c, FILE *stream)
- 分别用于从文件中读取一个字符和向文件中写入一个字符。
fgets(char *str, int n, FILE *stream)
和fputs(const char *str, FILE *stream)
fgets()
用于从文件中读取一行文本,最多读取n-1
个字符。fputs()
用于向文件写入一个字符串,不自动添加换行符。
读写二进制文件
fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
- 从文件中读取数据到内存块中。
size
指定每个元素的大小,nmemb
指定要读取的元素数量。
- 从文件中读取数据到内存块中。
fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
- 将内存块中的数据写入文件。参数含义同
fread()
。
- 将内存块中的数据写入文件。参数含义同
文件定位
fseek(FILE *stream, long offset, int whence)
- 改变文件的位置指示器。
whence
可以是SEEK_SET
(文件开头)、SEEK_CUR
(当前位置)、SEEK_END
(文件末尾)。
- 改变文件的位置指示器。
ftell(FILE *stream)
- 返回文件位置指示器的当前值,即从文件开头到当前位置的字节数。
rewind(FILE *stream)
- 将文件位置指示器重置到文件开头。
错误检测
feof(FILE *stream)
- 检测是否到达文件末尾。
ferror(FILE *stream)
- 检测是否存在文件错误。
写入文本文件示例:
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
fprintf(file, "Hello, World!\n");
fclose(file);
return 0;
}
读取文本文件示例:
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
char buffer[100];
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}
fclose(file);
return 0;
}
读写二进制文件示例:假设我们有一个结构体数组需要保存到文件中,并从文件中读取回来:
#include <stdio.h>
typedef struct {
int id;
char name[50];
} Person;
int main() {
// 写入二进制文件
FILE *file = fopen("people.dat", "wb");
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
Person people[] = {{1, "Alice"}, {2, "Bob"}};
fwrite(people, sizeof(Person), 2, file);
fclose(file);
// 读取二进制文件
file = fopen("people.dat", "rb");
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
Person person;
while (fread(&person, sizeof(Person), 1, file) == 1) {
printf("ID: %d, Name: %s\n", person.id, person.name);
}
fclose(file);
return 0;
}