C语言详解
目录
- C语言简介
- C语言的历史
- C语言的特点
- C语言的基本语法
- 程序结构
- 注释
- 标识符与关键字
- 数据类型和变量
- 基本数据类型
- 枚举类型
- 类型修饰符
- 变量声明与初始化
- 常量
- 操作符
- 算术操作符
- 关系操作符
- 逻辑操作符
- 位操作符
- 赋值操作符
- 其他操作符
- 运算符优先级与结合性
- 控制结构
- 条件语句
- 循环语句
- 跳转语句
- 函数
- 函数声明与定义
- 函数调用
- 参数传递
- 函数返回值
- 递归
- 内联函数
- 函数指针
- 指针
- 指针的声明与初始化
- 指针的使用
- 指针运算
- 指针与数组
- 多级指针
- 指针与函数
- 指针的常见应用
- 指针安全性
- 数组和字符串
- 数组
- 字符串
- 结构体和联合体
- 结构体(struct)
- 联合体(union)
- 枚举(enum)
- 内存管理
- 静态内存分配
- 动态内存分配
- 内存管理注意事项
- 内存泄漏与检测
- 预处理器
- 宏定义
- 条件编译
- 文件包含
- 预定义宏
- 宏与内联函数的比较
- 文件操作
- 文件指针
- 打开文件
- 关闭文件
- 读写操作
- 文件定位
- 错误处理
- 标准库
- 常用标准库头文件
- 常用函数示例
- 高级主题
- 位域
- 内联汇编
- 多文件项目管理
- 静态与动态链接库
- C语言与C++的关系
- 调试与优化
- 常用调试工具
- 调试技巧
- 性能优化方法
- 常见错误及其排查
- C语言的应用
- 操作系统
- 嵌入式系统
- 编译器
- 数据库系统
- 图形和游戏开发
- 驱动程序
- 高性能计算
- 网络编程
- 系统工具
- 虚拟机和容器
- 总结
C语言简介
C语言是一种通用的、过程式的编程语言,由Dennis Ritchie于1972年在贝尔实验室开发。C语言设计简洁,功能强大,尤其适用于系统编程,如操作系统、嵌入式系统和编译器等。C语言不仅影响了后来的许多编程语言(如C++、Java、Python),也是学习其他语言的基础。
C语言的用途
- 系统编程:操作系统(如Unix、Linux)、驱动程序等。
- 嵌入式系统:微控制器、物联网设备。
- 编译器与解释器:许多编程语言的编译器实现。
- 高性能计算:科学计算、图形处理、游戏引擎。
- 应用软件:数据库系统(如MySQL)、图形应用等。
C语言的历史
- 1972年:Dennis Ritchie在贝尔实验室开发了C语言,用于重写Unix操作系统。
- 1978年:Brian Kernighan和Dennis Ritchie合著《The C Programming Language》一书,广泛传播C语言。
- 1983年:K&R C标准发布,奠定了C语言的基础。
- 1989年:ANSI(美国国家标准协会)标准化C语言,发布ANSI C标准。
- 1990年:ISO(国际标准化组织)采纳ANSI C,发布ISO C标准。
- 1999年:C99标准,引入了许多新特性,如内联函数、布尔类型、单行注释等。
- 2011年:C11标准,进一步增强了语言特性和安全性。
- 2018年:C18标准,主要是对C11的修订和错误修正。
C语言的特点
- 简洁高效:提供低级内存操作能力,适合系统编程,生成的代码执行效率高。
- 可移植性强:程序可以在不同平台上编译运行,只需少量修改。
- 灵活性:支持多种编程范式,如过程式编程和模块化编程。
- 丰富的运算符:包括算术、逻辑、位操作等,提供强大的数据处理能力。
- 标准库丰富:提供大量的标准函数,简化编程任务。
- 广泛应用:用于操作系统、嵌入式系统、驱动程序、编译器等领域。
- 低级操作能力:支持指针、位操作等,可以直接操作内存和硬件。
C语言的缺点
- 安全性较低:缺乏内置的内存安全机制,容易出现缓冲区溢出、悬挂指针等问题。
- 语法复杂:尤其是指针和内存管理,对初学者有一定难度。
- 缺乏面向对象支持:C语言是过程式语言,缺乏面向对象编程的直接支持。
- 标准库相对有限:与现代高级语言相比,标准库功能较少,需要手动实现许多功能。
C语言的基本语法
程序结构
一个C程序通常由以下部分组成:
- 预处理指令:以
#
开头,如#include
、#define
等,用于指示编译器在编译前进行相应的处理。 - 全局变量和函数声明:在
main
函数之前或之后声明和定义全局变量和其他函数。 - 主函数:
int main()
,程序执行的入口。 - 其他函数定义:程序中调用的其他函数的实现部分。
注释
C语言支持两种类型的注释:
- 单行注释:以
//
开头,注释从//
开始直到行末。// 这是一个单行注释
- 多行注释:以
/*
开始,以*/
结束,可以跨多行。/* 这是一个 多行注释 */
标识符与关键字
- 标识符:用于命名变量、函数、数组等。由字母、数字和下划线组成,且不能以数字开头。
- 例子:
myVar
,_count
,MAX_SIZE
- 例子:
- 关键字:C语言预定义的保留字,具有特定的意义,不能用作标识符。共有32个关键字(根据C标准不同可能略有变化)。
- 例子:
int
,return
,if
,else
,while
,for
,struct
,typedef
- 例子:
数据类型和变量
基本数据类型
C语言提供了多种基本数据类型,用于定义变量的类型和大小。
整型(Integer)
int
:通常为4字节,表示有符号整数。short
:通常为2字节,表示较小的有符号整数。long
:通常为4或8字节,表示较大的有符号整数。unsigned
:修饰符,表示无符号整数。int a = 10; unsigned int b = 20; short c = 5; long d = 100000L;
字符型(Character)
char
:通常为1字节,表示单个字符。unsigned char
,signed char
:分别表示无符号和有符号字符。char ch = 'A'; unsigned char uch = 'B';
浮点型(Floating-point)
float
:通常为4字节,表示单精度浮点数。double
:通常为8字节,表示双精度浮点数。long double
:通常为12或16字节,表示扩展精度浮点数。float f = 3.14f; double d = 3.141592653589793; long double ld = 3.14159265358979323846L;
布尔型(Boolean)(C99引入)
_Bool
:原生布尔类型。bool
:通过包含<stdbool.h>
头文件使用。#include <stdbool.h> bool flag = true;
枚举类型
枚举类型用于定义一组命名的整数常量,提高代码的可读性。
enum Weekday {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
enum Weekday today = Wednesday;
// 手动指定值
enum Colors {
RED = 1,
GREEN = 3,
BLUE = 5
};
类型修饰符
类型修饰符用于改变基本数据类型的性质,提供更多的选项。
signed
:表示有符号类型(默认)。unsigned
:表示无符号类型。short
:表示短类型。long
:表示长类型。
组合使用修饰符可以创建不同的类型。
unsigned int u = 100;
long long ll = 100000L;
unsigned short us = 50;
变量声明与初始化
- 声明变量:指定变量的类型和名称。
int a; float b; char c;
- 初始化变量:在声明的同时为变量赋值。
int a = 10; float b = 3.14f; char c = 'A';
- 多变量声明:在一行中声明多个同类型变量。
int x = 1, y = 2, z = 3;
常量
常量是值固定不变的变量,可以通过以下方式定义:
- 使用
const
关键字:定义只读变量。const int MAX = 100; // MAX = 200; // 错误,无法修改
- 使用宏定义:通过
#define
定义常量。#define PI 3.14159 #define MAX_SIZE 1000
类型转换
C语言支持显式和隐式的类型转换。
- 隐式转换:编译器自动进行类型转换。
int a = 10; float b = a; // 隐式转换为float
- 显式转换(类型强制转换):通过指定目标类型进行转换。
double pi = 3.14159; int int_pi = (int)pi; // 显式转换为int,int_pi = 3
转换规则:
- 整型转换为浮点型时,保持数值的近似。
- 浮点型转换为整型时,向零取整。
- 有符号与无符号类型转换时,可能导致数值变化。
变量作用域与生命周期
- 作用域(Scope):变量在程序中可见和可访问的范围。
- 全局变量:在所有函数外部声明,作用域为整个文件。
- 局部变量:在函数内部声明,作用域仅限于函数或代码块内部。
- 块作用域:在代码块(如
{}
)内部声明,作用域仅限于该块。
- 生命周期(Lifetime):变量在程序运行期间存在的时间。
- 静态变量:在程序整个生命周期内存在,初始值在编译时确定。
- 自动变量:在声明它们的块开始时创建,在块结束时销毁。
- 动态变量:通过动态内存分配函数(如
malloc
)创建,需要手动释放。
操作符
C语言提供了丰富的操作符,用于执行各种操作。操作符的使用对于编写高效和可读的代码至关重要。
算术操作符
用于执行基本的数学运算。
操作符 | 描述 | 示例 |
---|---|---|
+ | 加法 | a + b |
- | 减法 | a - b |
* | 乘法 | a * b |
/ | 除法 | a / b |
% | 取模 | a % b |
注意:
- 除法运算符
/
在整型之间执行整数除法,结果为商的整数部分。int a = 7, b = 3; int c = a / b; // c = 2 float d = (float)a / b; // d = 2.3333
- 取模运算符
%
仅适用于整数类型。int a = 10, b = 3; int c = a % b; // c = 1
关系操作符
用于比较两个值的关系,结果为布尔值(真或假)。
操作符 | 描述 | 示例 |
---|---|---|
== | 等于 | a == b |
!= | 不等于 | a != b |
> | 大于 | a > b |
< | 小于 | a < b |
>= | 大于等于 | a >= b |
<= | 小于等于 | a <= b |
示例:
int a = 5, b = 10;
if (a < b) {
printf("a is less than b\n");
}
逻辑操作符
用于组合多个布尔表达式,返回布尔结果。
操作符 | 描述 | 示例 |
---|---|---|
&& | 逻辑与 | a && b |
! | 逻辑非 | !a |
示例:
int a = 1, b = 0;
if (a && !b) {
printf("a is true and b is false\n");
}
位操作符
用于对整数类型的二进制位进行操作。
操作符 | 描述 | 示例 |
---|---|---|
& | 按位与 | a & b |
| | 按位或 | `a |
^ | 按位异或 | a ^ b |
~ | 按位取反 | ~a |
<< | 左移 | a << 2 |
>> | 右移 | a >> 2 |
示例:
int a = 5; // 二进制: 0101
int b = 3; // 二进制: 0011
printf("a & b = %d\n", a & b); // 输出 1 (0001)
printf("a | b = %d\n", a | b); // 输出 7 (0111)
printf("a ^ b = %d\n", a ^ b); // 输出 6 (0110)
printf("~a = %d\n", ~a); // 输出 -6 (补码表示)
printf("a << 1 = %d\n", a << 1); // 输出 10 (1010)
printf("a >> 1 = %d\n", a >> 1); // 输出 2 (0010)
赋值操作符
用于给变量赋值或更新变量的值。
操作符 | 描述 | 示例 |
---|---|---|
= | 简单赋值 | a = b |
+= | 加后赋值 | a += b 等同于 a = a + b |
-= | 减后赋值 | a -= b |
*= | 乘后赋值 | a *= b |
/= | 除后赋值 | a /= b |
%= | 取模后赋值 | a %= b |
<<= | 左移后赋值 | a <<= 2 |
>>= | 右移后赋值 | a >>= 2 |
&= | 按位与后赋值 | a &= b |
|= | 按位或后赋值 | `a |
^= | 按位异或后赋值 | a ^= b |
示例:
int a = 5;
a += 3; // a = 8
a *= 2; // a = 16
其他操作符
sizeof
:获取数据类型或变量的大小(以字节为单位)。int a = 10; printf("Size of int: %lu bytes\n", sizeof(int)); // 通常输出4 printf("Size of a: %lu bytes\n", sizeof(a)); // 通常输出4
- 三元条件运算符
?:
:用于简化if-else
语句。int a = 5, b = 10; int max = (a > b) ? a : b; // max = 10
- 逗号运算符``,`:在一个表达式中依次执行多个操作。
int a, b, c; a = (b = 3, c = 4, b + c); // a = 7
运算符优先级与结合性
C语言中的运算符有不同的优先级和结合性,决定了表达式中操作符的计算顺序。
运算符优先级(部分):
优先级 | 操作符 | 描述 |
---|---|---|
1 | () [] -> . |
括号、数组、指针成员访问 |
2 | ! ~ ++ -- + - |
逻辑非、位非、递增递减、一元加减 |
3 | * / % |
乘除取模 |
4 | + - |
加减 |
5 | << >> |
位移 |
6 | < <= > >= |
关系 |
7 | == != |
相等 |
8 | & |
按位与 |
9 | ^ |
按位异或 |
10 | ` | ` |
11 | && |
逻辑与 |
12 | ` | |
13 | ?: |
条件运算符 |
14 | `= += -= *= /= %= <<= >>= &= ^= | =` |
15 | , |
逗号 |
结合性(Associativity):
- 大部分运算符是左结合,即从左到右计算。
- 一些运算符是右结合,如赋值运算符
=
和条件运算符?:
。
示例:
int a = 5, b = 10, c = 15;
int result = a + b * c; // result = 5 + (10 * 15) = 155
建议:
使用括号()
明确表达式的计算顺序,增强代码的可读性和可维护性。
int result = (a + b) * c; // 强制先计算a + b
控制结构
C语言提供了多种控制结构,用于控制程序的执行流程,包括条件语句、循环语句和跳转语句。
条件语句
用于根据条件的真假来决定执行哪部分代码。
if语句
当条件为真时执行某段代码。
if (condition) {
// 条件为真时执行的代码
}
示例:
int a = 10;
if (a > 5) {
printf("a is greater than 5\n");
}
if-else语句
根据条件的真假执行不同的代码块。
if (condition) {
// 条件为真时执行的代码
} else {
// 条件为假时执行的代码
}
示例:
int a = 3;
if (a % 2 == 0) {
printf("a is even\n");
} else {
printf("a is odd\n");
}
else-if链
根据多个条件依次判断,执行第一个满足条件的代码块。
if (condition1) {
// 条件1为真时执行
} else if (condition2) {
// 条件2为真时执行
} else if (condition3) {
// 条件3为真时执行
} else {
// 所有条件为假时执行
}
示例:
int score = 85;
if (score >= 90) {
printf("Grade: A\n");
} else if (score >= 80) {
printf("Grade: B\n");
} else if (score >= 70) {
printf("Grade: C\n");
} else {
printf("Grade: D\n");
}
switch语句
基于表达式的值选择执行的代码块,适用于多分支选择。
switch (expression) {
case constant1:
// 执行代码
break;
case constant2:
// 执行代码
break;
default:
// 默认执行代码
}
注意:
break
语句用于跳出switch
,防止“贯穿”。- 如果缺少
break
,会继续执行下一个case
,直到遇到break
或switch
结束。
示例:
char grade = 'B';
switch (grade) {
case 'A':
printf("Excellent\n");
break;
case 'B':
printf("Good\n");
break;
case 'C':
printf("Fair\n");
break;
default:
printf("Invalid grade\n");
}
循环语句
用于重复执行某段代码,直到满足终止条件。
for循环
适用于已知循环次数的情况。
for (初始化; 条件; 更新) {
// 循环体
}
示例:
for (int i = 0; i < 5; i++) {
printf("i = %d\n", i);
}
while循环
适用于条件控制的循环,适合循环次数不确定的情况。
while (condition) {
// 循环体
}
示例:
int i = 0;
while (i < 5) {
printf("i = %d\n", i);
i++;
}
do-while循环
至少执行一次循环体,适用于需要先执行再判断条件的情况。
do {
// 循环体
} while (condition);
示例:
int i = 0;
do {
printf("i = %d\n", i);
i++;
} while (i < 5);
跳转语句
用于在循环或代码块中控制程序的执行流程。
- break:跳出最近的循环或
switch
语句。for (int i = 0; i < 10; i++) { if (i == 5) { break; // 跳出循环 } printf("%d ", i); } // 输出: 0 1 2 3 4
- continue:跳过本次循环,进入下一次循环。
for (int i = 0; i < 10; i++) { if (i % 2 == 0) { continue; // 跳过偶数 } printf("%d ", i); } // 输出: 1 3 5 7 9
- return:从函数返回,终止函数的执行。
int add(int a, int b) { return a + b; // 返回a + b的值 }
- goto:无条件跳转到指定标签(不推荐使用,容易导致代码混乱)。
注意:int main() { int a = 0; goto label; a = 5; // 这行代码不会被执行 label: printf("Jumped to label\n"); return 0; } // 输出: Jumped to label
goto
语句不推荐使用,因为它会降低代码的可读性和可维护性,容易引入错误。
循环控制示例
嵌套循环
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
printf("i = %d, j = %d\n", i, j);
}
}
// 输出:
// i = 1, j = 1
// i = 1, j = 2
// i = 1, j = 3
// i = 2, j = 1
// i = 2, j = 2
// i = 2, j = 3
// i = 3, j = 1
// i = 3, j = 2
// i = 3, j = 3
无限循环
while (1) {
// 永远执行的循环
}
// 或
for (;;) {
// 永远执行的循环
}
终止条件
int i = 0;
while (1) {
if (i >= 5) {
break;
}
printf("i = %d\n", i);
i++;
}
函数
函数是C语言的基本组成单元,用于封装可复用的代码块,提高代码的模块化和可维护性。
函数声明与定义
函数声明(函数原型)
在使用函数之前,需要声明函数的名称、返回类型和参数类型。通常在文件顶部或头文件中声明。
返回类型 函数名(参数类型1, 参数类型2, ...);
示例:
int add(int a, int b); // 函数声明
函数定义
函数的实现部分,包括函数体。
返回类型 函数名(参数类型1 参数1, 参数类型2 参数2, ...) {
// 函数体
return 返回值;
}
示例:
int add(int a, int b) {
return a + b;
}
函数调用
在程序中调用已定义或已声明的函数,通过传递实参来执行函数的代码。
函数名(实参1, 实参2, ...);
示例:
#include <stdio.h>
// 函数声明
int add(int a, int b);
int main() {
int sum = add(5, 3); // 调用函数
printf("Sum: %d\n", sum);
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
参数传递
C语言函数参数传递方式主要有两种:值传递和指针传递。
值传递
函数接收参数的副本,原变量不受影响。
#include <stdio.h>
void modify(int x) {
x = 10; // 修改的是x的副本
}
int main() {
int a = 5;
modify(a);
printf("a = %d\n", a); // 输出 a = 5
return 0;
}
指针传递
通过指针传递变量的地址,函数可以修改原变量。
#include <stdio.h>
void modify(int *x) {
*x = 10; // 修改的是x指向的值
}
int main() {
int a = 5;
modify(&a);
printf("a = %d\n", a); // 输出 a = 10
return 0;
}
函数返回值
函数可以返回值,也可以不返回值(使用void
类型)。
有返回值的函数
int add(int a, int b) {
return a + b;
}
int main() {
int sum = add(3, 4);
printf("Sum: %d\n", sum); // 输出 Sum: 7
return 0;
}
无返回值的函数
void greet() {
printf("Hello!\n");
}
int main() {
greet(); // 输出 Hello!
return 0;
}
递归
函数调用自身的编程技术,适用于解决分治问题,如斐波那契数列、阶乘计算等。
示例:计算阶乘
#include <stdio.h>
int factorial(int n) {
if (n <= 1) {
return 1; // 基本情况
}
return n * factorial(n - 1); // 递归调用
}
int main() {
int num = 5;
printf("Factorial of %d is %d\n", num, factorial(num)); // 输出 Factorial of 5 is 120
return 0;
}
注意事项:
- 确保递归有终止条件,否则会导致栈溢出。
- 递归深度过大可能导致性能问题。
内联函数
C99引入的inline
关键字,建议编译器将函数代码直接插入到调用点,以减少函数调用的开销。
inline int add(int a, int b) {
return a + b;
}
int main() {
int sum = add(3, 4);
return 0;
}
注意:
- 编译器可能会根据优化策略决定是否实际内联。
- 适用于小型、频繁调用的函数。
函数指针
函数指针用于存储函数的地址,可以通过指针调用函数,适用于回调函数、事件驱动编程等。
声明与初始化
#include <stdio.h>
// 函数声明
void greet() {
printf("Hello!\n");
}
int main() {
// 声明一个指向函数的指针,函数无参数且返回void
void (*funcPtr)() = greet;
// 通过指针调用函数
funcPtr(); // 输出 Hello!
return 0;
}
函数指针作为参数
#include <stdio.h>
// 定义一个函数类型
typedef void (*FuncPtr)();
// 函数接受函数指针作为参数
void execute(FuncPtr f) {
f();
}
void greet() {
printf("Hello from execute!\n");
}
int main() {
execute(greet); // 输出 Hello from execute!
return 0;
}
应用场景:
- 回调函数:如排序函数的比较函数。
- 事件驱动编程:如GUI程序的事件处理。
- 实现多态行为。
示例:使用函数指针进行排序
#include <stdio.h>
#include <stdlib.h>
// 比较函数类型
int compare(const void *a, const void *b) {
return (*(int*)a - *(int*)b);
}
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
// 使用qsort进行排序,传入比较函数
qsort(arr, n, sizeof(int), compare);
// 打印排序后的数组
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
// 输出: 1 2 5 5 6 9
return 0;
}
指针
指针是C语言中强大而灵活的特性,用于存储变量的内存地址。掌握指针对于理解C语言的高级特性和高效编程至关重要。
指针的声明与初始化
指针声明
指针声明语法:
类型 *指针名;
示例:
int *p; // 指向int类型的指针
float *f; // 指向float类型的指针
char *c; // 指向char类型的指针
指针初始化
通过取地址运算符&
获取变量的地址并赋值给指针。
int a = 10;
int *p = &a; // p指向a的地址
指针的使用
解引用
通过解引用操作符*
访问指针指向的值。
int a = 10;
int *p = &a;
int value = *p; // value = 10
修改指针指向的值
int a = 10;
int *p = &a;
*p = 20; // a的值被修改为20
指针运算
指针可以进行加减运算,但需要注意指针类型的大小。
- 指针加法:
p + 1
表示指针向前移动一个类型大小的内存地址。 - 指针减法:
p - 1
表示指针向后移动一个类型大小的内存地址。 - 指针减指针:
p2 - p1
表示两个指针之间相隔的元素数量。
示例:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 指向arr[0]
int *q = p + 2; // 指向arr[2], 即3
printf("%d\n", *q); // 输出 3
int diff = q - p; // diff = 2
printf("Difference: %d\n", diff);
指针与数组
数组名实际上是指向数组首元素的指针,可以通过指针访问数组元素。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等同于 int *p = &arr[0];
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i)); // 输出 1 2 3 4 5
}
注意:
- 数组名是常量指针,不能进行赋值操作。
int arr1[5], arr2[5]; int *p = arr1; p = arr2; // 合法,p指向arr2 // arr1 = arr2; // 错误,数组名不可赋值
多级指针
指针的指针,用于指向指针变量的地址。
int a = 10;
int *p = &a;
int **pp = &p;
printf("a = %d\n", a); // 输出 10
printf("*p = %d\n", *p); // 输出 10
printf("**pp = %d\n", **pp); // 输出 10
指针与函数
指针可以作为函数参数,实现通过函数修改外部变量。
#include <stdio.h>
void swap(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}
int main() {
int a = 5, b = 10;
printf("Before swap: a = %d, b = %d\n", a, b);
swap(&a, &b); // 传递变量地址
printf("After swap: a = %d, b = %d\n", a, b);
// 输出:
// Before swap: a = 5, b = 10
// After swap: a = 10, b = 5
return 0;
}
指针的常见应用
- 动态内存分配:通过指针操作堆内存。
- 数据结构实现:如链表、树、图等。
- 函数参数传递:通过指针传递,实现函数修改外部变量。
- 数组与字符串操作:通过指针遍历和处理数组、字符串。
- 回调函数:通过函数指针实现回调机制。
指针安全性
指针使用不当可能导致严重的错误,如内存泄漏、悬挂指针、野指针等。以下是一些指针安全性的建议:
- 初始化指针:声明指针时初始化为
NULL
或有效地址。int *p = NULL;
- 避免悬挂指针:释放内存后将指针设为
NULL
。free(p); p = NULL;
- 检查指针是否为NULL:在解引用前检查指针是否为空。
if (p != NULL) { printf("%d\n", *p); }
- 避免越界访问:确保指针在合法范围内访问内存。
- 使用智能指针(在C++中更常见):自动管理指针生命周期,避免内存泄漏。
示例:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *)malloc(sizeof(int) * 5); // 分配内存
if (p == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
p[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", p[i]);
}
// 输出: 1 2 3 4 5
free(p); // 释放内存
p = NULL; // 避免悬挂指针
return 0;
}
数组和字符串
数组
数组是相同类型元素的集合,通过索引访问。C语言支持一维数组和多维数组。
声明与初始化
- 一维数组:
int arr[5]; // 声明一个包含5个int的数组 int arr2[5] = {1, 2, 3}; // 部分初始化,未初始化的元素默认为0
- 多维数组:
int matrix[3][4]; // 三行四列的二维数组 int cube[2][3][4]; // 三维数组
注意:
- 数组大小必须为常量表达式(编译时确定)。
- 索引从0开始,到
size - 1
。
访问元素
通过索引访问数组元素。
int arr[5] = {1, 2, 3, 4, 5};
arr[0] = 10; // 设置第一个元素为10
int x = arr[2]; // 获取第三个元素的值,x = 3
越界访问: C语言不进行数组越界检查,越界访问可能导致未定义行为,需谨慎操作。
多维数组
通过多个索引访问多维数组元素。
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
printf("%d\n", matrix[1][2]); // 输出 6
数组与指针
数组名是指向数组首元素的指针,可以通过指针遍历数组。
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 等同于 int *p = &arr[0];
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i)); // 输出 10 20 30 40 50
}
return 0;
}
字符串
字符串是字符数组,以空字符\0
结尾,用于存储和处理文本数据。
声明与初始化
- 字符串字面量:
自动在末尾添加空字符char str1[] = "Hello";
\0
。str1
数组大小为6。 - 指定大小的字符串:
其余未初始化的元素默认为char str2[10] = "World";
\0
。 - 逐字符初始化:
char str3[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
注意:
- 确保数组大小足够容纳字符串及终止符。
- 字符串字面量不可修改,尝试修改可能导致未定义行为。
字符串操作函数
需要包含<string.h>
头文件,常用函数包括:
strlen
:获取字符串长度(不包括\0
)。size_t len = strlen(str1);
strcpy
:复制字符串。strcpy(dest, src);
strncpy
:复制指定长度的字符串,防止缓冲区溢出。strncpy(dest, src, n);
strcat
:连接字符串。strcat(dest, src);
strncat
:连接指定长度的字符串。strncat(dest, src, n);
strcmp
:比较字符串。int cmp = strcmp(str1, str2);
strncmp
:比较指定长度的字符串。int cmp = strncmp(str1, str2, n);
strchr
:查找字符在字符串中的第一次出现。char *ptr = strchr(str, 'e');
strstr
:查找子字符串在字符串中的第一次出现。char *ptr = strstr(str, "ell");
示例:
#include <stdio.h>
#include <string.h>
int main() {
char str1[20] = "Hello";
char str2[] = "World";
// 连接字符串
strcat(str1, " ");
strcat(str1, str2); // str1 = "Hello World"
printf("%s\n", str1);
// 获取长度
printf("Length of str1: %lu\n", strlen(str1));
// 比较字符串
int cmp = strcmp(str1, str2);
if (cmp > 0) {
printf("str1 is greater than str2\n");
} else if (cmp < 0) {
printf("str1 is less than str2\n");
} else {
printf("str1 is equal to str2\n");
}
return 0;
}
字符串的内存管理
- 动态分配字符串:使用指针和动态内存分配函数(如
malloc
)管理字符串。#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { char *str = (char *)malloc(20 * sizeof(char)); if (str == NULL) { printf("Memory allocation failed\n"); return 1; } strcpy(str, "Dynamic String"); printf("%s\n", str); free(str); // 释放内存 return 0; }
- 避免缓冲区溢出:在复制和连接字符串时,确保目标缓冲区足够大,使用安全的函数如
strncpy
和strncat
。#include <stdio.h> #include <string.h> int main() { char src[] = "This is a long string"; char dest[10]; // 使用strncpy避免溢出 strncpy(dest, src, sizeof(dest) - 1); dest[sizeof(dest) - 1] = '\0'; // 确保字符串终止 printf("%s\n", dest); // 输出 "This is a" return 0; }
结构体和联合体
结构体(struct)
结构体用于将不同类型的数据组合在一起,形成一个新的复合数据类型。
定义与声明
- 定义结构体:
struct Person { char name[50]; int age; float height; };
- 声明结构体变量:
struct Person p1; // p1是一个Person类型的变量
- 使用typedef简化声明:
typedef struct { char name[50]; int age; float height; } Person; Person p2; // p2是一个Person类型的变量
初始化
- 在声明时初始化:
struct Person p1 = {"Alice", 30, 5.5f};
- 逐成员初始化:
struct Person p2; strcpy(p2.name, "Bob"); p2.age = 25; p2.height = 6.0f;
访问成员
通过点运算符.
访问结构体成员。
printf("Name: %s\n", p1.name);
printf("Age: %d\n", p1.age);
printf("Height: %.2f\n", p1.height);
结构体数组
存储多个结构体变量的数组。
struct Person people[3] = {
{"Alice", 30, 5.5f},
{"Bob", 25, 6.0f},
{"Charlie", 28, 5.8f}
};
for (int i = 0; i < 3; i++) {
printf("Person %d: %s, %d, %.2f\n", i + 1, people[i].name, people[i].age, people[i].height);
}
// 输出:
// Person 1: Alice, 30, 5.50
// Person 2: Bob, 25, 6.00
// Person 3: Charlie, 28, 5.80
结构体与指针
通过指针访问结构体成员,使用箭头运算符->
。
#include <stdio.h>
#include <string.h>
struct Person {
char name[50];
int age;
float height;
};
int main() {
struct Person p = {"Dave", 35, 5.9f};
struct Person *ptr = &p;
// 使用箭头运算符访问成员
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Height: %.2f\n", ptr->height);
return 0;
}
// 输出:
// Name: Dave
// Age: 35
// Height: 5.90
联合体(union)
联合体与结构体类似,但所有成员共用同一块内存,节省空间。适用于需要在不同时间存储不同类型数据的场景。
定义与声明
- 定义联合体:
union Data { int i; float f; char str[20]; };
- 声明联合体变量:
union Data data;
使用
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i: %d\n", data.i);
data.f = 3.14f;
printf("data.f: %.2f\n", data.f);
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
// 注意:最后赋值的成员会覆盖前面的值
printf("After assigning str, data.i: %d\n", data.i); // 未定义行为
return 0;
}
// 输出(示例):
// data.i: 10
// data.f: 3.14
// data.str: C Programming
// After assigning str, data.i: 1214606444 // 示例值,实际输出取决于系统
注意:
- 联合体的所有成员共用同一块内存,因此最后赋值的成员会影响其他成员的值。
- 访问未初始化的成员会导致未定义行为。
联合体与结构体的区别
特性 | 结构体(struct) | 联合体(union) |
---|---|---|
内存分配 | 每个成员都有独立的内存空间 | 所有成员共用同一块内存 |
大小 | 所有成员中最大成员的大小之和 | 最大成员的大小 |
用途 | 需要同时存储多个不同类型数据 | 需要在不同时间存储不同类型数据,节省内存 |
枚举(enum)
枚举类型用于定义一组命名的整数常量,增强代码的可读性。
定义与使用
#include <stdio.h>
enum Day {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
int main() {
enum Day today = Wednesday;
printf("Day number: %d\n", today); // 输出 3
return 0;
}
手动指定值
enum Colors {
RED = 1,
GREEN = 3,
BLUE = 5
};
int main() {
enum Colors color = GREEN;
printf("Color value: %d\n", color); // 输出 3
return 0;
}
枚举与类型兼容性
枚举成员实际上是整数,可以与整型进行比较和运算。
enum Color { RED, GREEN, BLUE };
enum Color c = RED;
if (c == 0) {
printf("Color is RED\n");
}
内存管理
内存管理是C语言中重要的概念,涉及程序如何分配、使用和释放内存。合理的内存管理可以提高程序的效率和稳定性,避免内存泄漏和其他问题。
静态内存分配
在编译时分配内存,适用于全局变量、局部变量等。
全局变量
在所有函数外部声明,生命周期为整个程序运行期间。
int globalVar = 100;
int main() {
// 使用全局变量
printf("Global Var: %d\n", globalVar);
return 0;
}
局部变量
在函数内部声明,生命周期为函数调用期间。
int main() {
int localVar = 50;
printf("Local Var: %d\n", localVar);
return 0;
}
动态内存分配
在运行时通过指针申请和释放内存,适用于需要灵活内存管理的场景,如处理可变大小的数据结构。
malloc
malloc
函数用于分配指定字节数的内存,返回void *
类型指针,需要进行类型转换。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5); // 分配5个int的内存
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5
free(ptr); // 释放内存
ptr = NULL; // 避免悬挂指针
return 0;
}
calloc
calloc
函数用于分配内存,并初始化为0。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)calloc(5, sizeof(int)); // 分配5个int的内存,并初始化为0
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 0 0 0 0 0
free(ptr);
ptr = NULL;
return 0;
}
realloc
realloc
函数用于重新调整已分配内存的大小,保留原有数据。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 初始化数组
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 重新分配为10个int
ptr = (int *)realloc(ptr, sizeof(int) * 10);
if (ptr == NULL) {
printf("Memory reallocation failed\n");
return 1;
}
// 初始化新分配的内存
for (int i = 5; i < 10; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 10; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5 6 7 8 9 10
free(ptr);
ptr = NULL;
return 0;
}
free
free
函数用于释放动态分配的内存,避免内存泄漏。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5
free(ptr); // 释放内存
ptr = NULL; // 避免悬挂指针
return 0;
}
内存管理注意事项
- 避免内存泄漏:每次
malloc
或calloc
后,都应有对应的free
。未释放的内存会导致程序占用越来越多的内存。int *p = (int *)malloc(sizeof(int) * 10); // 使用p free(p); // 释放内存
- 避免悬挂指针:释放内存后,应将指针设置为
NULL
,防止指针指向已释放的内存。free(p); p = NULL;
- 检查分配是否成功:
malloc
和calloc
可能返回NULL
,需检查内存分配是否成功。int *p = (int *)malloc(sizeof(int) * 10); if (p == NULL) { // 处理分配失败 }
- 避免重复释放:同一块内存只应释放一次,重复释放会导致未定义行为。
free(p); // free(p); // 错误,第二次释放同一指针
- 避免越界访问:确保访问指针指向的内存在有效范围内,防止访问非法内存。
内存泄漏与检测
内存泄漏是指程序中动态分配的内存未被释放,导致内存无法再次利用,最终耗尽系统内存。
检测工具
- Valgrind:Linux下的内存调试工具,能够检测内存泄漏、未初始化内存使用等问题。
valgrind --leak-check=full ./your_program
- AddressSanitizer:GCC和Clang提供的内存错误检测工具。
gcc -fsanitize=address -g your_program.c -o your_program ./your_program
示例:内存泄漏
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *)malloc(sizeof(int) * 10);
if (p == NULL) {
return 1;
}
// 使用内存
for (int i = 0; i < 10; i++) {
p[i] = i;
}
// 忘记释放内存
return 0;
}
使用Valgrind检测:
valgrind --leak-check=full ./a.out
输出:
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2BBAF: malloc (vg_replace_malloc.c:299)
==12345== by 0x4005ED: main (leak_example.c:6)
解决方法:确保每次malloc
或calloc
后都有对应的free
。
预处理器
C语言的预处理器在编译之前对源代码进行处理,执行宏替换、条件编译等任务。
宏定义
宏是一种预处理器指令,用于在编译前进行文本替换。宏定义通过#define
指令实现,分为对象宏和函数宏。
对象宏
对象宏是简单的文本替换,不带参数。
#define PI 3.14159
#define MAX_SIZE 100
示例:
#include <stdio.h>
#define PI 3.14159
int main() {
float radius = 5.0f;
float area = PI * radius * radius;
printf("Area: %.2f\n", area);
return 0;
}
// 输出: Area: 78.54
函数宏
函数宏带参数,类似于函数,但在编译时进行文本替换。
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
示例:
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
int a = 5, b = 10;
printf("Square of %d: %d\n", a, SQUARE(a)); // 输出 Square of 5: 25
printf("Max: %d\n", MAX(a, b)); // 输出 Max: 10
return 0;
}
注意事项:
- 使用括号包裹参数和整个宏定义,避免运算优先级导致的错误。
#define SQUARE(x) ((x) * (x))
- 避免宏副作用,如参数中包含自增、自减操作。
#define SQUARE(x) ((x) * (x)) int a = 3; int result = SQUARE(a++); // 展开为 ((a++) * (a++)), 导致未定义行为
宏嵌套与替换
宏可以嵌套使用,预处理器会逐层展开。
#define ADD(a, b) ((a) + (b))
#define MUL(a, b) ((a) * (b))
#define COMBINE(a, b, c) (ADD(a, b) + MUL(a, c))
示例:
#include <stdio.h>
#define ADD(a, b) ((a) + (b))
#define MUL(a, b) ((a) * (b))
#define COMBINE(a, b, c) (ADD(a, b) + MUL(a, c))
int main() {
int result = COMBINE(1, 2, 3); // 展开为 ((1) + (2)) + ((3) * (3)) = 12
printf("Result: %d\n", result);
return 0;
}
// 输出: Result: 12
条件编译
条件编译用于根据特定条件选择性地编译代码块,适用于跨平台编程和调试。
基本语法
#if condition
// 条件为真时编译的代码
#elif condition2
// 条件2为真时编译的代码
#else
// 所有条件为假时编译的代码
#endif
示例:
#include <stdio.h>
#define DEBUG 1
int main() {
#if DEBUG
printf("Debug mode is ON\n");
#else
printf("Debug mode is OFF\n");
#endif
return 0;
}
// 输出: Debug mode is ON
使用#ifdef
和#ifndef
#ifdef
:检查宏是否已定义。#ifdef DEBUG // 调试代码 #endif
#ifndef
:检查宏是否未定义。#ifndef RELEASE // 非发布代码 #endif
示例:
#include <stdio.h>
#define VERSION 2
int main() {
#if VERSION == 1
printf("Version 1\n");
#elif VERSION == 2
printf("Version 2\n");
#else
printf("Unknown Version\n");
#endif
return 0;
}
// 输出: Version 2
文件包含
使用#include
指令包含其他文件,常用于引入头文件。
- 使用尖括号
<>
包含系统头文件,编译器在标准系统目录中查找。#include <stdio.h>
- 使用双引号
""
包含用户自定义头文件,编译器在当前目录或指定路径中查找。#include "myheader.h"
示例:
假设有一个头文件math_utils.h
:
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
对应的实现文件math_utils.c
:
// math_utils.c
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
主程序main.c
:
#include <stdio.h>
#include "math_utils.h"
int main() {
int sum = add(5, 3);
int product = multiply(5, 3);
printf("Sum: %d\n", sum); // 输出 Sum: 8
printf("Product: %d\n", product); // 输出 Product: 15
return 0;
}
编译:
gcc -o main main.c math_utils.c
./main
预定义宏
预定义宏在编译时由编译器自动定义,用于获取编译环境的信息。
__FILE__
:当前文件名。__LINE__
:当前行号。__DATE__
:编译日期。__TIME__
:编译时间。__func__
:当前函数名(C99引入)。
示例:
#include <stdio.h>
void printInfo() {
printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
printf("Function: %s\n", __func__);
}
int main() {
printInfo();
return 0;
}
// 假设文件名为example.c,输出示例:
// File: example.c
// Line: 5
// Function: printInfo
宏与内联函数的比较
- 宏:
- 预处理时进行文本替换,可能导致意想不到的副作用。
- 不进行类型检查。
- 无法调试。
- 内联函数(C99引入):
- 在编译时决定是否内联,提供类型检查。
- 更安全,易于调试。
- 适用于简单、频繁调用的函数。
示例:
#include <stdio.h>
// 宏定义
#define SQUARE_MACRO(x) ((x) * (x))
// 内联函数
inline int square_inline(int x) {
return x * x;
}
int main() {
int a = 5;
// 使用宏
int macro_result = SQUARE_MACRO(a++);
printf("Macro result: %d, a: %d\n", macro_result, a);
// 输出: Macro result: 30, a: 6
a = 5; // 重置a
// 使用内联函数
int inline_result = square_inline(a++);
printf("Inline result: %d, a: %d\n", inline_result, a);
// 输出: Inline result: 25, a: 6
return 0;
}
// 解释:
// SQUARE_MACRO(a++) 展开为 ((a++) * (a++)), 导致a自增两次,结果为5 * 6 = 30。
// square_inline(a++) 内联为 (a++ * a++), 但由于内联函数的参数在函数调用时只评估一次,结果为5 * 5 = 25,a只自增一次。
结论:内联函数比宏更安全、更可靠,推荐使用内联函数替代函数宏。
文件操作
C语言通过标准库提供文件读写功能,适用于读写文件、处理数据持久化等任务。文件操作涉及文件指针、打开/关闭文件、读写数据等。
文件指针
文件指针是FILE
类型的指针,表示打开的文件。需要包含<stdio.h>
头文件。
FILE *fp;
打开文件
使用fopen
函数打开文件,返回文件指针。
fp = fopen("filename", "mode");
模式:
"r"
:只读模式,文件必须存在。"w"
:只写模式,若文件存在则清空,不存在则创建。"a"
:追加模式,写入数据时追加到文件末尾。"r+"
:读写模式,文件必须存在。"w+"
:读写模式,若文件存在则清空,不存在则创建。"a+"
:读写模式,写入数据时追加到文件末尾,不存在则创建。
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
fprintf(fp, "Hello, File!\n"); // 写入文件
fclose(fp); // 关闭文件
return 0;
}
关闭文件
使用fclose
函数关闭文件,释放资源。
fclose(fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
fprintf(fp, "Hello, File!\n");
fclose(fp); // 关闭文件
return 0;
}
读写操作
写入文件
fprintf
:格式化写入,类似于printf
。fprintf(fp, "Name: %s, Age: %d\n", name, age);
fputs
:写入字符串。fputs("Hello, World!\n", fp);
fwrite
:写入二进制数据。fwrite(data, sizeof(char), size, fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
// 使用fprintf
fprintf(fp, "Name: %s, Age: %d\n", "Alice", 30);
// 使用fputs
fputs("This is a test line.\n", fp);
// 使用fwrite
char data[] = "BinaryData";
fwrite(data, sizeof(char), sizeof(data), fp);
fclose(fp);
return 0;
}
读取文件
fscanf
:格式化读取,类似于scanf
。fscanf(fp, "%s %d", name, &age);
fgets
:读取一行字符串。fgets(buffer, sizeof(buffer), fp);
fread
:读取二进制数据。fread(data, sizeof(char), size, fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("input.txt", "r");
if (fp == NULL) {
printf("Failed to open file for reading\n");
return 1;
}
char name[50];
int age;
// 使用fscanf
fscanf(fp, "%s %d", name, &age);
printf("Name: %s, Age: %d\n", name, age);
// 使用fgets读取剩余内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
return 0;
}
文件定位
通过fseek
、ftell
和rewind
函数定位文件指针的位置。
fseek
移动文件指针到指定位置。
fseek(fp, offset, whence);
- offset:偏移量。
- whence:参考位置,常用值:
SEEK_SET
:文件开头。SEEK_CUR
:当前位置。SEEK_END
:文件末尾。
ftell
返回当前文件指针的位置。
long pos = ftell(fp);
rewind
将文件指针移动到文件开头。
rewind(fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("Failed to open file\n");
return 1;
}
// 移动文件指针到第10字节
fseek(fp, 10, SEEK_SET);
// 获取当前文件指针位置
long pos = ftell(fp);
printf("Current position: %ld\n", pos);
// 读取剩余内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
return 0;
}
错误处理
在文件操作中,需要处理可能出现的错误,如文件无法打开、读取失败等。
- 检查文件指针是否为NULL:
if (fp == NULL) { // 处理错误 }
- 使用
feof
检测文件结束:if (feof(fp)) { // 文件结束 }
- 使用
ferror
检测文件错误:if (ferror(fp)) { // 处理文件错误 }
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
perror("Error opening file"); // 打印错误信息
return 1;
}
// 读取文件内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
if (ferror(fp)) {
printf("Error reading file\n");
}
fclose(fp);
return 0;
}
标准库
C语言提供了丰富的标准库,简化编程任务。标准库包含多个头文件,每个头文件提供一组相关的函数和宏。
常用标准库头文件
<stdio.h>
提供输入输出功能,包括文件操作、控制台输入输出等。
- 常用函数:
printf
、scanf
、fopen
、fclose
、fprintf
、fscanf
、fgets
、fputs
、fread
、fwrite
等。
<stdlib.h>
提供内存分配、程序控制、转换函数等。
- 常用函数:
malloc
、calloc
、realloc
、free
、exit
、atoi
、atof
、abs
、rand
、srand
等。
<string.h>
提供字符串处理函数,用于操作C风格字符串。
- 常用函数:
strlen
、strcpy
、strncpy
、strcat
、strncat
、strcmp
、strncmp
、strchr
、strstr
等。
<math.h>
提供数学函数,用于执行数学运算。
- 常用函数:
sqrt
、pow
、sin
、cos
、tan
、log
、exp
、abs
等。
<ctype.h>
提供字符处理函数,用于判断字符类型和转换字符大小写。
- 常用函数:
isalpha
、isdigit
、isspace
、toupper
、tolower
等。
<time.h>
提供时间和日期函数,用于处理时间和日期相关的操作。
- 常用函数:
time
、localtime
、gmtime
、strftime
、clock
等。
<stdbool.h>
提供布尔类型支持(C99引入)。
- 类型:
bool
,值为true
或false
。
<stddef.h>
定义了一些常用的类型和宏,如size_t
、NULL
。
常用函数示例
字符串函数
#include <stdio.h>
#include <string.h>
int main() {
char str1[20] = "Hello";
char str2[] = "World";
char dest[50];
// 连接字符串
strcat(str1, " ");
strcat(str1, str2); // str1 = "Hello World"
printf("Concatenated String: %s\n", str1);
// 获取长度
printf("Length: %lu\n", strlen(str1));
// 复制字符串
strcpy(dest, str1);
printf("Copied String: %s\n", dest);
// 比较字符串
int cmp = strcmp(str1, str2);
if (cmp > 0) {
printf("str1 is greater than str2\n");
} else if (cmp < 0) {
printf("str1 is less than str2\n");
} else {
printf("str1 is equal to str2\n");
}
return 0;
}
数学函数
#include <stdio.h>
#include <math.h>
int main() {
double x = 16.0;
double y = 2.0;
double z = 3.14 / 2;
double sqrt_val = sqrt(x); // 计算平方根
double pow_val = pow(y, 3); // 计算y的3次方
double sin_val = sin(z); // 计算正弦值
printf("sqrt(16) = %.2f\n", sqrt_val);
printf("pow(2, 3) = %.2f\n", pow_val);
printf("sin(π/2) = %.2f\n", sin_val);
return 0;
}
// 输出:
// sqrt(16) = 4.00
// pow(2, 3) = 8.00
// sin(π/2) = 1.00
内存函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char str1[] = "Hello, World!";
size_t len = strlen(str1);
// 动态分配内存并复制字符串
char *copy = (char *)malloc(len + 1); // +1 为 '\0'
if (copy == NULL) {
printf("Memory allocation failed\n");
return 1;
}
strcpy(copy, str1);
printf("Copied String: %s\n", copy);
// 释放内存
free(copy);
copy = NULL;
return 0;
}
字符处理函数
#include <stdio.h>
#include <ctype.h>
int main() {
char ch = 'a';
if (isalpha(ch)) {
printf("%c is an alphabet\n", ch);
}
ch = '1';
if (isdigit(ch)) {
printf("%c is a digit\n", ch);
}
ch = ' ';
if (isspace(ch)) {
printf("Character is a whitespace\n");
}
ch = 'A';
char lower = tolower(ch);
printf("Lowercase of %c is %c\n", ch, lower);
return 0;
}
// 输出:
// a is an alphabet
// 1 is a digit
// Character is a whitespace
// Lowercase of A is a
数组、字符串与指针的关系
- 数组名与指针:
- 数组名是指向数组首元素的常量指针。
- 不能修改数组名指针的指向。
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // 合法 // arr = p + 1; // 错误,数组名不可赋值
- 指针运算:
- 可以通过指针遍历数组元素。
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); }
- 字符串与指针:
- 字符串是字符数组,也可以通过指针操作。
char str[] = "Hello"; char *p = str; while (*p != '\0') { printf("%c ", *p); p++; } // 输出: H e l l o
结构体和联合体
结构体(struct)
结构体用于将不同类型的数据组合在一起,形成一个新的数据类型。适用于表示实体对象,如人、学生、商品等。
定义与声明
- 定义结构体:
struct Person { char name[50]; int age; float height; };
- 声明结构体变量:
struct Person p1;
- 使用typedef简化结构体类型:
typedef struct { char name[50]; int age; float height; } Person; Person p2;
初始化
- 在声明时初始化:
struct Person p1 = {"Alice", 30, 5.5f};
- 逐成员初始化:
struct Person p2; strcpy(p2.name, "Bob"); p2.age = 25; p2.height = 6.0f;
- 使用typedef后的初始化:
Person p3 = {"Charlie", 28, 5.8f};
访问成员
通过点运算符.
访问结构体成员,使用箭头运算符->
通过指针访问。
// 使用点运算符
printf("Name: %s\n", p1.name);
printf("Age: %d\n", p1.age);
printf("Height: %.2f\n", p1.height);
// 使用箭头运算符
struct Person *ptr = &p1;
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Height: %.2f\n", ptr->height);
结构体数组
存储多个结构体变量的数组。
struct Person people[3] = {
{"Alice", 30, 5.5f},
{"Bob", 25, 6.0f},
{"Charlie", 28, 5.8f}
};
for (int i = 0; i < 3; i++) {
printf("Person %d: %s, %d, %.2f\n", i + 1, people[i].name, people[i].age, people[i].height);
}
// 输出:
// Person 1: Alice, 30, 5.50
// Person 2: Bob, 25, 6.00
// Person 3: Charlie, 28, 5.80
结构体与指针
通过指针访问结构体成员,使用箭头运算符->
。
#include <stdio.h>
#include <string.h>
struct Person {
char name[50];
int age;
float height;
};
int main() {
struct Person p = {"Dave", 35, 5.9f};
struct Person *ptr = &p;
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Height: %.2f\n", ptr->height);
return 0;
}
// 输出:
// Name: Dave
// Age: 35
// Height: 5.90
联合体(union)
联合体与结构体类似,但所有成员共用同一块内存,节省空间。适用于需要在不同时间存储不同类型数据的场景。
定义与声明
- 定义联合体:
union Data { int i; float f; char str[20]; };
- 声明联合体变量:
union Data data;
使用
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i: %d\n", data.i);
data.f = 3.14f;
printf("data.f: %.2f\n", data.f);
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
// 注意:最后赋值的成员会覆盖前面的值
printf("After assigning str, data.i: %d\n", data.i); // 未定义行为
return 0;
}
// 输出(示例):
// data.i: 10
// data.f: 3.14
// data.str: C Programming
// After assigning str, data.i: 1214606444 // 示例值,实际输出取决于系统
注意:
- 联合体的所有成员共用同一块内存,因此最后赋值的成员会影响其他成员的值。
- 访问未初始化的成员会导致未定义行为。
联合体与结构体的区别
特性 | 结构体(struct) | 联合体(union) |
---|---|---|
内存分配 | 每个成员都有独立的内存空间 | 所有成员共用同一块内存 |
大小 | 所有成员中最大成员的大小之和 | 最大成员的大小 |
用途 | 需要同时存储多个不同类型数据 | 需要在不同时间存储不同类型数据,节省内存 |
枚举(enum)
枚举类型用于定义一组命名的整数常量,增强代码的可读性。
定义与使用
#include <stdio.h>
enum Day {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
int main() {
enum Day today = Wednesday;
printf("Day number: %d\n", today); // 输出 3
return 0;
}
手动指定值
enum Colors {
RED = 1,
GREEN = 3,
BLUE = 5
};
int main() {
enum Colors color = GREEN;
printf("Color value: %d\n", color); // 输出 3
return 0;
}
枚举与类型兼容性
枚举成员实际上是整数,可以与整型进行比较和运算。
enum Color { RED, GREEN, BLUE };
enum Color c = RED;
if (c == 0) {
printf("Color is RED\n");
}
内存管理
内存管理是C语言中重要的概念,涉及程序如何分配、使用和释放内存。合理的内存管理可以提高程序的效率和稳定性,避免内存泄漏和其他问题。
静态内存分配
在编译时分配内存,适用于全局变量、局部变量等。
全局变量
在所有函数外部声明,生命周期为整个程序运行期间。
int globalVar = 100;
int main() {
// 使用全局变量
printf("Global Var: %d\n", globalVar);
return 0;
}
局部变量
在函数内部声明,生命周期为函数调用期间。
int main() {
int localVar = 50;
printf("Local Var: %d\n", localVar);
return 0;
}
动态内存分配
在运行时通过指针申请和释放内存,适用于需要灵活内存管理的场景,如处理可变大小的数据结构。
malloc
malloc
函数用于分配指定字节数的内存,返回void *
类型指针,需要进行类型转换。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5); // 分配5个int的内存
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5
free(ptr); // 释放内存
ptr = NULL; // 避免悬挂指针
return 0;
}
calloc
calloc
函数用于分配内存,并初始化为0。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)calloc(5, sizeof(int)); // 分配5个int的内存,并初始化为0
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 0 0 0 0 0
free(ptr);
ptr = NULL;
return 0;
}
realloc
realloc
函数用于重新调整已分配内存的大小,保留原有数据。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 初始化数组
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 重新分配为10个int
ptr = (int *)realloc(ptr, sizeof(int) * 10);
if (ptr == NULL) {
printf("Memory reallocation failed\n");
return 1;
}
// 初始化新分配的内存
for (int i = 5; i < 10; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 10; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5 6 7 8 9 10
free(ptr);
ptr = NULL;
return 0;
}
free
free
函数用于释放动态分配的内存,避免内存泄漏。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5
free(ptr); // 释放内存
ptr = NULL; // 避免悬挂指针
return 0;
}
内存管理注意事项
- 避免内存泄漏:每次
malloc
或calloc
后,都应有对应的free
。未释放的内存会导致程序占用越来越多的内存。int *p = (int *)malloc(sizeof(int) * 10); // 使用p free(p); // 释放内存
- 避免悬挂指针:释放内存后,应将指针设置为
NULL
,防止指针指向已释放的内存。free(p); p = NULL;
- 检查分配是否成功:
malloc
和calloc
可能返回NULL
,需检查内存分配是否成功。int *p = (int *)malloc(sizeof(int) * 10); if (p == NULL) { // 处理分配失败 }
- 避免重复释放:同一块内存只应释放一次,重复释放会导致未定义行为。
free(p); // free(p); // 错误,第二次释放同一指针
- 避免越界访问:确保访问指针指向的内存在有效范围内,防止访问非法内存。
内存泄漏与检测
内存泄漏是指程序中动态分配的内存未被释放,导致内存无法再次利用,最终耗尽系统内存。
检测工具
- Valgrind:Linux下的内存调试工具,能够检测内存泄漏、未初始化内存使用等问题。
valgrind --leak-check=full ./your_program
- AddressSanitizer:GCC和Clang提供的内存错误检测工具。
gcc -fsanitize=address -g your_program.c -o your_program ./your_program
示例:内存泄漏
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *)malloc(sizeof(int) * 10);
if (p == NULL) {
return 1;
}
// 使用内存
for (int i = 0; i < 10; i++) {
p[i] = i;
}
// 忘记释放内存
return 0;
}
使用Valgrind检测:
valgrind --leak-check=full ./a.out
输出:
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2BBAF: malloc (vg_replace_malloc.c:299)
==12345== by 0x4005ED: main (leak_example.c:6)
解决方法:确保每次malloc
或calloc
后都有对应的free
。
预处理器
C语言的预处理器在编译之前对源代码进行处理,执行宏替换、条件编译等任务。
宏定义
宏是一种预处理器指令,用于在编译前进行文本替换。宏定义通过#define
指令实现,分为对象宏和函数宏。
对象宏
对象宏是简单的文本替换,不带参数。
#define PI 3.14159
#define MAX_SIZE 100
示例:
#include <stdio.h>
#define PI 3.14159
int main() {
float radius = 5.0f;
float area = PI * radius * radius;
printf("Area: %.2f\n", area);
return 0;
}
// 输出: Area: 78.54
函数宏
函数宏带参数,类似于函数,但在编译时进行文本替换。
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
示例:
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
int a = 5, b = 10;
printf("Square of %d: %d\n", a, SQUARE(a)); // 输出 Square of 5: 25
printf("Max: %d\n", MAX(a, b)); // 输出 Max: 10
return 0;
}
注意事项:
- 使用括号包裹参数和整个宏定义,避免运算优先级导致的错误。
#define SQUARE(x) ((x) * (x))
- 避免宏副作用,如参数中包含自增、自减操作。
#define SQUARE(x) ((x) * (x)) int a = 3; int result = SQUARE(a++); // 展开为 ((a++) * (a++)), 导致未定义行为
宏嵌套与替换
宏可以嵌套使用,预处理器会逐层展开。
#define ADD(a, b) ((a) + (b))
#define MUL(a, b) ((a) * (b))
#define COMBINE(a, b, c) (ADD(a, b) + MUL(a, c))
示例:
#include <stdio.h>
#define ADD(a, b) ((a) + (b))
#define MUL(a, b) ((a) * (b))
#define COMBINE(a, b, c) (ADD(a, b) + MUL(a, c))
int main() {
int result = COMBINE(1, 2, 3); // 展开为 ((1) + (2)) + ((3) * (3)) = 12
printf("Result: %d\n", result);
return 0;
}
// 输出: Result: 12
条件编译
条件编译用于根据特定条件选择性地编译代码块,适用于跨平台编程和调试。
基本语法
#if condition
// 条件为真时编译的代码
#elif condition2
// 条件2为真时编译的代码
#else
// 所有条件为假时编译的代码
#endif
示例:
#include <stdio.h>
#define DEBUG 1
int main() {
#if DEBUG
printf("Debug mode is ON\n");
#else
printf("Debug mode is OFF\n");
#endif
return 0;
}
// 输出: Debug mode is ON
使用#ifdef
和#ifndef
#ifdef
:检查宏是否已定义。#ifdef DEBUG // 调试代码 #endif
#ifndef
:检查宏是否未定义。#ifndef RELEASE // 非发布代码 #endif
示例:
#include <stdio.h>
#define VERSION 2
int main() {
#if VERSION == 1
printf("Version 1\n");
#elif VERSION == 2
printf("Version 2\n");
#else
printf("Unknown Version\n");
#endif
return 0;
}
// 输出: Version 2
文件包含
使用#include
指令包含其他文件,常用于引入头文件。
- 使用尖括号
<>
包含系统头文件,编译器在标准系统目录中查找。#include <stdio.h>
- 使用双引号
""
包含用户自定义头文件,编译器在当前目录或指定路径中查找。#include "math_utils.h"
示例:
假设有一个头文件math_utils.h
:
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
对应的实现文件math_utils.c
:
// math_utils.c
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
主程序main.c
:
#include <stdio.h>
#include "math_utils.h"
int main() {
int sum = add(5, 3);
int product = multiply(5, 3);
printf("Sum: %d\n", sum); // 输出 Sum: 8
printf("Product: %d\n", product); // 输出 Product: 15
return 0;
}
编译:
gcc -o main main.c math_utils.c
./main
预定义宏
预定义宏在编译时由编译器自动定义,用于获取编译环境的信息。
__FILE__
:当前文件名。__LINE__
:当前行号。__DATE__
:编译日期。__TIME__
:编译时间。__func__
:当前函数名(C99引入)。
示例:
#include <stdio.h>
void printInfo() {
printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
printf("Function: %s\n", __func__);
}
int main() {
printInfo();
return 0;
}
// 假设文件名为example.c,输出示例:
// File: example.c
// Line: 5
// Function: printInfo
宏与内联函数的比较
- 宏:
- 预处理时进行文本替换,可能导致意想不到的副作用。
- 不进行类型检查。
- 无法调试。
- 内联函数(C99引入):
- 在编译时决定是否内联,提供类型检查。
- 更安全,易于调试。
- 适用于简单、频繁调用的函数。
示例:
#include <stdio.h>
// 宏定义
#define SQUARE_MACRO(x) ((x) * (x))
// 内联函数
inline int square_inline(int x) {
return x * x;
}
int main() {
int a = 5;
// 使用宏
int macro_result = SQUARE_MACRO(a++);
printf("Macro result: %d, a: %d\n", macro_result, a);
// 输出: Macro result: 30, a: 6
a = 5; // 重置a
// 使用内联函数
int inline_result = square_inline(a++);
printf("Inline result: %d, a: %d\n", inline_result, a);
// 输出: Inline result: 25, a: 6
return 0;
}
// 解释:
// SQUARE_MACRO(a++) 展开为 ((a++) * (a++)), 导致a自增两次,结果为5 * 6 = 30。
// square_inline(a++) 内联为 (a++ * a++), 但由于内联函数的参数在函数调用时只评估一次,结果为5 * 5 = 25,a只自增一次。
结论:内联函数比宏更安全、更可靠,推荐使用内联函数替代函数宏。
文件操作
文件指针
文件指针是FILE
类型的指针,表示打开的文件。需要包含<stdio.h>
头文件。
FILE *fp;
打开文件
使用fopen
函数打开文件,返回文件指针。
fp = fopen("filename", "mode");
模式:
"r"
:只读模式,文件必须存在。"w"
:只写模式,若文件存在则清空,不存在则创建。"a"
:追加模式,写入数据时追加到文件末尾。"r+"
:读写模式,文件必须存在。"w+"
:读写模式,若文件存在则清空,不存在则创建。"a+"
:读写模式,写入数据时追加到文件末尾,不存在则创建。
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
fprintf(fp, "Hello, File!\n"); // 写入文件
fclose(fp); // 关闭文件
return 0;
}
关闭文件
使用fclose
函数关闭文件,释放资源。
fclose(fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
fprintf(fp, "Hello, File!\n");
fclose(fp); // 关闭文件
return 0;
}
读写操作
写入文件
fprintf
:格式化写入,类似于printf
。fprintf(fp, "Name: %s, Age: %d\n", name, age);
fputs
:写入字符串。fputs("Hello, World!\n", fp);
fwrite
:写入二进制数据。fwrite(data, sizeof(char), size, fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
// 使用fprintf
fprintf(fp, "Name: %s, Age: %d\n", "Alice", 30);
// 使用fputs
fputs("This is a test line.\n", fp);
// 使用fwrite
char data[] = "BinaryData";
fwrite(data, sizeof(char), sizeof(data), fp);
fclose(fp);
return 0;
}
读取文件
fscanf
:格式化读取,类似于scanf
。fscanf(fp, "%s %d", name, &age);
fgets
:读取一行字符串。fgets(buffer, sizeof(buffer), fp);
fread
:读取二进制数据。fread(data, sizeof(char), size, fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("input.txt", "r");
if (fp == NULL) {
printf("Failed to open file for reading\n");
return 1;
}
char name[50];
int age;
// 使用fscanf
fscanf(fp, "%s %d", name, &age);
printf("Name: %s, Age: %d\n", name, age);
// 使用fgets读取剩余内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
return 0;
}
文件定位
通过fseek
、ftell
和rewind
函数定位文件指针的位置。
fseek
移动文件指针到指定位置。
fseek(fp, offset, whence);
- offset:偏移量。
- whence:参考位置,常用值:
SEEK_SET
:文件开头。SEEK_CUR
:当前位置。SEEK_END
:文件末尾。
ftell
返回当前文件指针的位置。
long pos = ftell(fp);
rewind
将文件指针移动到文件开头。
rewind(fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("Failed to open file\n");
return 1;
}
// 移动文件指针到第10字节
fseek(fp, 10, SEEK_SET);
// 获取当前文件指针位置
long pos = ftell(fp);
printf("Current position: %ld\n", pos);
// 读取剩余内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
return 0;
}
错误处理
在文件操作中,需要处理可能出现的错误,如文件无法打开、读取失败等。
- 检查文件指针是否为NULL:
if (fp == NULL) { // 处理错误 }
- 使用
feof
检测文件结束:if (feof(fp)) { // 文件结束 }
- 使用
ferror
检测文件错误:if (ferror(fp)) { // 处理文件错误 }
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
perror("Error opening file"); // 打印错误信息
return 1;
}
// 读取文件内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
if (ferror(fp)) {
printf("Error reading file\n");
}
fclose(fp);
return 0;
}
标准库
常用标准库头文件
<stdio.h>
提供输入输出功能,包括文件操作、控制台输入输出等。
- 常用函数:
printf
、scanf
、fopen
、fclose
、fprintf
、fscanf
、fgets
、fputs
、fread
、fwrite
等。
<stdlib.h>
提供内存分配、程序控制、转换函数等。
- 常用函数:
malloc
、calloc
、realloc
、free
、exit
、atoi
、atof
、abs
、rand
、srand
等。
<string.h>
提供字符串处理函数,用于操作C风格字符串。
- 常用函数:
strlen
、strcpy
、strncpy
、strcat
、strncat
、strcmp
、strncmp
、strchr
、strstr
等。
<math.h>
提供数学函数,用于执行数学运算。
- 常用函数:
sqrt
、pow
、sin
、cos
、tan
、log
、exp
、abs
等。
<ctype.h>
提供字符处理函数,用于判断字符类型和转换字符大小写。
- 常用函数:
isalpha
、isdigit
、isspace
、toupper
、tolower
等。
<time.h>
提供时间和日期函数,用于处理时间和日期相关的操作。
- 常用函数:
time
、localtime
、gmtime
、strftime
、clock
等。
<stdbool.h>
提供布尔类型支持(C99引入)。
- 类型:
bool
,值为true
或false
。
<stddef.h>
定义了一些常用的类型和宏,如size_t
、NULL
。
常用函数示例
字符串函数
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello";
char dest[20];
// 复制字符串
strcpy(dest, src);
printf("Copied String: %s\n", dest);
// 连接字符串
strcat(dest, ", World!");
printf("Concatenated String: %s\n", dest);
// 获取长度
size_t len = strlen(dest);
printf("Length: %lu\n", len);
// 查找字符
char *ptr = strchr(dest, 'W');
if (ptr != NULL) {
printf("Found 'W' at position: %ld\n", ptr - dest);
}
return 0;
}
// 输出:
// Copied String: Hello
// Concatenated String: Hello, World!
// Length: 13
// Found 'W' at position: 7
数学函数
#include <stdio.h>
#include <math.h>
int main() {
double x = 16.0;
double y = 2.0;
double z = 3.14 / 2;
double sqrt_val = sqrt(x); // 计算平方根
double pow_val = pow(y, 3); // 计算y的3次方
double sin_val = sin(z); // 计算正弦值
printf("sqrt(16) = %.2f\n", sqrt_val);
printf("pow(2, 3) = %.2f\n", pow_val);
printf("sin(π/2) = %.2f\n", sin_val);
return 0;
}
// 输出:
// sqrt(16) = 4.00
// pow(2, 3) = 8.00
// sin(π/2) = 1.00
内存函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char str1[] = "Hello, World!";
size_t len = strlen(str1);
// 动态分配内存并复制字符串
char *copy = (char *)malloc(len + 1); // +1 为 '\0'
if (copy == NULL) {
printf("Memory allocation failed\n");
return 1;
}
strcpy(copy, str1);
printf("Copied String: %s\n", copy);
// 释放内存
free(copy);
copy = NULL;
return 0;
}
字符处理函数
#include <stdio.h>
#include <ctype.h>
int main() {
char ch = 'a';
if (isalpha(ch)) {
printf("%c is an alphabet\n", ch);
}
ch = '1';
if (isdigit(ch)) {
printf("%c is a digit\n", ch);
}
ch = ' ';
if (isspace(ch)) {
printf("Character is a whitespace\n");
}
ch = 'A';
char lower = tolower(ch);
printf("Lowercase of %c is %c\n", ch, lower);
return 0;
}
// 输出:
// a is an alphabet
// 1 is a digit
// Character is a whitespace
// Lowercase of A is a
数组、字符串与指针的关系
- 数组名与指针:
- 数组名是指向数组首元素的常量指针。
- 不能修改数组名指针的指向。
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // 合法 // arr = p + 1; // 错误,数组名不可赋值
- 指针运算:
- 可以通过指针遍历数组元素。
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); }
- 字符串与指针:
- 字符串是字符数组,也可以通过指针操作。
char str[] = "Hello"; char *p = str; while (*p != '\0') { printf("%c ", *p); p++; } // 输出: H e l l o
结构体和联合体
结构体(struct)
结构体用于将不同类型的数据组合在一起,形成一个新的数据类型。适用于表示实体对象,如人、学生、商品等。
定义与声明
- 定义结构体:
struct Person { char name[50]; int age; float height; };
- 声明结构体变量:
struct Person p1;
- 使用typedef简化结构体类型:
typedef struct { char name[50]; int age; float height; } Person; Person p2;
初始化
- 在声明时初始化:
struct Person p1 = {"Alice", 30, 5.5f};
- 逐成员初始化:
struct Person p2; strcpy(p2.name, "Bob"); p2.age = 25; p2.height = 6.0f;
- 使用typedef后的初始化:
Person p3 = {"Charlie", 28, 5.8f};
访问成员
通过点运算符.
访问结构体成员,使用箭头运算符->
通过指针访问。
// 使用点运算符
printf("Name: %s\n", p1.name);
printf("Age: %d\n", p1.age);
printf("Height: %.2f\n", p1.height);
// 使用箭头运算符
struct Person *ptr = &p1;
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Height: %.2f\n", ptr->height);
结构体数组
存储多个结构体变量的数组。
struct Person people[3] = {
{"Alice", 30, 5.5f},
{"Bob", 25, 6.0f},
{"Charlie", 28, 5.8f}
};
for (int i = 0; i < 3; i++) {
printf("Person %d: %s, %d, %.2f\n", i + 1, people[i].name, people[i].age, people[i].height);
}
// 输出:
// Person 1: Alice, 30, 5.50
// Person 2: Bob, 25, 6.00
// Person 3: Charlie, 28, 5.80
结构体与指针
通过指针访问结构体成员,使用箭头运算符->
。
#include <stdio.h>
#include <string.h>
struct Person {
char name[50];
int age;
float height;
};
int main() {
struct Person p = {"Dave", 35, 5.9f};
struct Person *ptr = &p;
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Height: %.2f\n", ptr->height);
return 0;
}
// 输出:
// Name: Dave
// Age: 35
// Height: 5.90
联合体(union)
联合体与结构体类似,但所有成员共用同一块内存,节省空间。适用于需要在不同时间存储不同类型数据的场景。
定义与声明
- 定义联合体:
union Data { int i; float f; char str[20]; };
- 声明联合体变量:
union Data data;
使用
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i: %d\n", data.i);
data.f = 3.14f;
printf("data.f: %.2f\n", data.f);
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
// 注意:最后赋值的成员会覆盖前面的值
printf("After assigning str, data.i: %d\n", data.i); // 未定义行为
return 0;
}
// 输出(示例):
// data.i: 10
// data.f: 3.14
// data.str: C Programming
// After assigning str, data.i: 1214606444 // 示例值,实际输出取决于系统
注意:
- 联合体的所有成员共用同一块内存,因此最后赋值的成员会影响其他成员的值。
- 访问未初始化的成员会导致未定义行为。
联合体与结构体的区别
特性 | 结构体(struct) | 联合体(union) |
---|---|---|
内存分配 | 每个成员都有独立的内存空间 | 所有成员共用同一块内存 |
大小 | 所有成员中最大成员的大小之和 | 最大成员的大小 |
用途 | 需要同时存储多个不同类型数据 | 需要在不同时间存储不同类型数据,节省内存 |
枚举(enum)
枚举类型用于定义一组命名的整数常量,增强代码的可读性。
定义与使用
#include <stdio.h>
enum Day {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
int main() {
enum Day today = Wednesday;
printf("Day number: %d\n", today); // 输出 3
return 0;
}
手动指定值
enum Colors {
RED = 1,
GREEN = 3,
BLUE = 5
};
int main() {
enum Colors color = GREEN;
printf("Color value: %d\n", color); // 输出 3
return 0;
}
枚举与类型兼容性
枚举成员实际上是整数,可以与整型进行比较和运算。
enum Color { RED, GREEN, BLUE };
enum Color c = RED;
if (c == 0) {
printf("Color is RED\n");
}
内存管理
内存管理是C语言中重要的概念,涉及程序如何分配、使用和释放内存。合理的内存管理可以提高程序的效率和稳定性,避免内存泄漏和其他问题。
静态内存分配
在编译时分配内存,适用于全局变量、局部变量等。
全局变量
在所有函数外部声明,生命周期为整个程序运行期间。
int globalVar = 100;
int main() {
// 使用全局变量
printf("Global Var: %d\n", globalVar);
return 0;
}
局部变量
在函数内部声明,生命周期为函数调用期间。
int main() {
int localVar = 50;
printf("Local Var: %d\n", localVar);
return 0;
}
动态内存分配
在运行时通过指针申请和释放内存,适用于需要灵活内存管理的场景,如处理可变大小的数据结构。
malloc
malloc
函数用于分配指定字节数的内存,返回void *
类型指针,需要进行类型转换。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5); // 分配5个int的内存
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5
free(ptr); // 释放内存
ptr = NULL; // 避免悬挂指针
return 0;
}
calloc
calloc
函数用于分配内存,并初始化为0。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)calloc(5, sizeof(int)); // 分配5个int的内存,并初始化为0
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 0 0 0 0 0
free(ptr);
ptr = NULL;
return 0;
}
realloc
realloc
函数用于重新调整已分配内存的大小,保留原有数据。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 初始化数组
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 重新分配为10个int
ptr = (int *)realloc(ptr, sizeof(int) * 10);
if (ptr == NULL) {
printf("Memory reallocation failed\n");
return 1;
}
// 初始化新分配的内存
for (int i = 5; i < 10; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 10; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5 6 7 8 9 10
free(ptr);
ptr = NULL;
return 0;
}
free
free
函数用于释放动态分配的内存,避免内存泄漏。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5
free(ptr); // 释放内存
ptr = NULL; // 避免悬挂指针
return 0;
}
内存管理注意事项
- 避免内存泄漏:每次
malloc
或calloc
后,都应有对应的free
。未释放的内存会导致程序占用越来越多的内存。int *p = (int *)malloc(sizeof(int) * 10); // 使用p free(p); // 释放内存
- 避免悬挂指针:释放内存后,应将指针设置为
NULL
,防止指针指向已释放的内存。free(p); p = NULL;
- 检查分配是否成功:
malloc
和calloc
可能返回NULL
,需检查内存分配是否成功。int *p = (int *)malloc(sizeof(int) * 10); if (p == NULL) { // 处理分配失败 }
- 避免重复释放:同一块内存只应释放一次,重复释放会导致未定义行为。
free(p); // free(p); // 错误,第二次释放同一指针
- 避免越界访问:确保访问指针指向的内存在有效范围内,防止访问非法内存。
内存泄漏与检测
内存泄漏是指程序中动态分配的内存未被释放,导致内存无法再次利用,最终耗尽系统内存。
检测工具
- Valgrind:Linux下的内存调试工具,能够检测内存泄漏、未初始化内存使用等问题。
valgrind --leak-check=full ./your_program
- AddressSanitizer:GCC和Clang提供的内存错误检测工具。
gcc -fsanitize=address -g your_program.c -o your_program ./your_program
示例:内存泄漏
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *)malloc(sizeof(int) * 10);
if (p == NULL) {
return 1;
}
// 使用内存
for (int i = 0; i < 10; i++) {
p[i] = i;
}
// 忘记释放内存
return 0;
}
使用Valgrind检测:
valgrind --leak-check=full ./a.out
输出:
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2BBAF: malloc (vg_replace_malloc.c:299)
==12345== by 0x4005ED: main (leak_example.c:6)
解决方法:确保每次malloc
或calloc
后都有对应的free
。
预处理器
宏定义
宏是一种预处理器指令,用于在编译前进行文本替换。宏定义通过#define
指令实现,分为对象宏和函数宏。
对象宏
对象宏是简单的文本替换,不带参数。
#define PI 3.14159
#define MAX_SIZE 100
示例:
#include <stdio.h>
#define PI 3.14159
int main() {
float radius = 5.0f;
float area = PI * radius * radius;
printf("Area: %.2f\n", area);
return 0;
}
// 输出: Area: 78.54
函数宏
函数宏带参数,类似于函数,但在编译时进行文本替换。
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
示例:
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
int a = 5, b = 10;
printf("Square of %d: %d\n", a, SQUARE(a)); // 输出 Square of 5: 25
printf("Max: %d\n", MAX(a, b)); // 输出 Max: 10
return 0;
}
注意事项:
- 使用括号包裹参数和整个宏定义,避免运算优先级导致的错误。
#define SQUARE(x) ((x) * (x))
- 避免宏副作用,如参数中包含自增、自减操作。
#define SQUARE(x) ((x) * (x)) int a = 3; int result = SQUARE(a++); // 展开为 ((a++) * (a++)), 导致未定义行为
宏嵌套与替换
宏可以嵌套使用,预处理器会逐层展开。
#define ADD(a, b) ((a) + (b))
#define MUL(a, b) ((a) * (b))
#define COMBINE(a, b, c) (ADD(a, b) + MUL(a, c))
示例:
#include <stdio.h>
#define ADD(a, b) ((a) + (b))
#define MUL(a, b) ((a) * (b))
#define COMBINE(a, b, c) (ADD(a, b) + MUL(a, c))
int main() {
int result = COMBINE(1, 2, 3); // 展开为 ((1) + (2)) + ((3) * (3)) = 12
printf("Result: %d\n", result);
return 0;
}
// 输出: Result: 12
条件编译
条件编译用于根据特定条件选择性地编译代码块,适用于跨平台编程和调试。
基本语法
#if condition
// 条件为真时编译的代码
#elif condition2
// 条件2为真时编译的代码
#else
// 所有条件为假时编译的代码
#endif
示例:
#include <stdio.h>
#define DEBUG 1
int main() {
#if DEBUG
printf("Debug mode is ON\n");
#else
printf("Debug mode is OFF\n");
#endif
return 0;
}
// 输出: Debug mode is ON
使用#ifdef
和#ifndef
#ifdef
:检查宏是否已定义。#ifdef DEBUG // 调试代码 #endif
#ifndef
:检查宏是否未定义。#ifndef RELEASE // 非发布代码 #endif
示例:
#include <stdio.h>
#define VERSION 2
int main() {
#if VERSION == 1
printf("Version 1\n");
#elif VERSION == 2
printf("Version 2\n");
#else
printf("Unknown Version\n");
#endif
return 0;
}
// 输出: Version 2
文件包含
使用#include
指令包含其他文件,常用于引入头文件。
- 使用尖括号
<>
包含系统头文件,编译器在标准系统目录中查找。#include <stdio.h>
- 使用双引号
""
包含用户自定义头文件,编译器在当前目录或指定路径中查找。#include "math_utils.h"
示例:
假设有一个头文件math_utils.h
:
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
对应的实现文件math_utils.c
:
// math_utils.c
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
主程序main.c
:
#include <stdio.h>
#include "math_utils.h"
int main() {
int sum = add(5, 3);
int product = multiply(5, 3);
printf("Sum: %d\n", sum); // 输出 Sum: 8
printf("Product: %d\n", product); // 输出 Product: 15
return 0;
}
编译:
gcc -o main main.c math_utils.c
./main
预定义宏
预定义宏在编译时由编译器自动定义,用于获取编译环境的信息。
__FILE__
:当前文件名。__LINE__
:当前行号。__DATE__
:编译日期。__TIME__
:编译时间。__func__
:当前函数名(C99引入)。
示例:
#include <stdio.h>
void printInfo() {
printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
printf("Function: %s\n", __func__);
}
int main() {
printInfo();
return 0;
}
// 假设文件名为example.c,输出示例:
// File: example.c
// Line: 5
// Function: printInfo
宏与内联函数的比较
- 宏:
- 预处理时进行文本替换,可能导致意想不到的副作用。
- 不进行类型检查。
- 无法调试。
- 内联函数(C99引入):
- 在编译时决定是否内联,提供类型检查。
- 更安全,易于调试。
- 适用于简单、频繁调用的函数。
示例:
#include <stdio.h>
// 宏定义
#define SQUARE_MACRO(x) ((x) * (x))
// 内联函数
inline int square_inline(int x) {
return x * x;
}
int main() {
int a = 5;
// 使用宏
int macro_result = SQUARE_MACRO(a++);
printf("Macro result: %d, a: %d\n", macro_result, a);
// 输出: Macro result: 30, a: 6
a = 5; // 重置a
// 使用内联函数
int inline_result = square_inline(a++);
printf("Inline result: %d, a: %d\n", inline_result, a);
// 输出: Inline result: 25, a: 6
return 0;
}
// 解释:
// SQUARE_MACRO(a++) 展开为 ((a++) * (a++)), 导致a自增两次,结果为5 * 6 = 30。
// square_inline(a++) 内联为 (a++ * a++), 但由于内联函数的参数在函数调用时只评估一次,结果为5 * 5 = 25,a只自增一次。
结论:内联函数比宏更安全、更可靠,推荐使用内联函数替代函数宏。
文件操作
文件指针
文件指针是FILE
类型的指针,表示打开的文件。需要包含<stdio.h>
头文件。
FILE *fp;
打开文件
使用fopen
函数打开文件,返回文件指针。
fp = fopen("filename", "mode");
模式:
"r"
:只读模式,文件必须存在。"w"
:只写模式,若文件存在则清空,不存在则创建。"a"
:追加模式,写入数据时追加到文件末尾。"r+"
:读写模式,文件必须存在。"w+"
:读写模式,若文件存在则清空,不存在则创建。"a+"
:读写模式,写入数据时追加到文件末尾,不存在则创建。
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
fprintf(fp, "Hello, File!\n"); // 写入文件
fclose(fp); // 关闭文件
return 0;
}
关闭文件
使用fclose
函数关闭文件,释放资源。
fclose(fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
fprintf(fp, "Hello, File!\n");
fclose(fp); // 关闭文件
return 0;
}
读写操作
写入文件
fprintf
:格式化写入,类似于printf
。fprintf(fp, "Name: %s, Age: %d\n", name, age);
fputs
:写入字符串。fputs("Hello, World!\n", fp);
fwrite
:写入二进制数据。fwrite(data, sizeof(char), size, fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
printf("Failed to open file for writing\n");
return 1;
}
// 使用fprintf
fprintf(fp, "Name: %s, Age: %d\n", "Alice", 30);
// 使用fputs
fputs("This is a test line.\n", fp);
// 使用fwrite
char data[] = "BinaryData";
fwrite(data, sizeof(char), sizeof(data), fp);
fclose(fp);
return 0;
}
读取文件
fscanf
:格式化读取,类似于scanf
。fscanf(fp, "%s %d", name, &age);
fgets
:读取一行字符串。fgets(buffer, sizeof(buffer), fp);
fread
:读取二进制数据。fread(data, sizeof(char), size, fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("input.txt", "r");
if (fp == NULL) {
printf("Failed to open file for reading\n");
return 1;
}
char name[50];
int age;
// 使用fscanf
fscanf(fp, "%s %d", name, &age);
printf("Name: %s, Age: %d\n", name, age);
// 使用fgets读取剩余内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
return 0;
}
文件定位
通过fseek
、ftell
和rewind
函数定位文件指针的位置。
fseek
移动文件指针到指定位置。
fseek(fp, offset, whence);
- offset:偏移量。
- whence:参考位置,常用值:
SEEK_SET
:文件开头。SEEK_CUR
:当前位置。SEEK_END
:文件末尾。
ftell
返回当前文件指针的位置。
long pos = ftell(fp);
rewind
将文件指针移动到文件开头。
rewind(fp);
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("Failed to open file\n");
return 1;
}
// 移动文件指针到第10字节
fseek(fp, 10, SEEK_SET);
// 获取当前文件指针位置
long pos = ftell(fp);
printf("Current position: %ld\n", pos);
// 读取剩余内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
return 0;
}
错误处理
在文件操作中,需要处理可能出现的错误,如文件无法打开、读取失败等。
- 检查文件指针是否为NULL:
if (fp == NULL) { // 处理错误 }
- 使用
feof
检测文件结束:if (feof(fp)) { // 文件结束 }
- 使用
ferror
检测文件错误:if (ferror(fp)) { // 处理文件错误 }
示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
perror("Error opening file"); // 打印错误信息
return 1;
}
// 读取文件内容
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
if (ferror(fp)) {
printf("Error reading file\n");
}
fclose(fp);
return 0;
}
标准库
常用标准库头文件
<stdio.h>
提供输入输出功能,包括文件操作、控制台输入输出等。
- 常用函数:
printf
、scanf
、fopen
、fclose
、fprintf
、fscanf
、fgets
、fputs
、fread
、fwrite
等。
<stdlib.h>
提供内存分配、程序控制、转换函数等。
- 常用函数:
malloc
、calloc
、realloc
、free
、exit
、atoi
、atof
、abs
、rand
、srand
等。
<string.h>
提供字符串处理函数,用于操作C风格字符串。
- 常用函数:
strlen
、strcpy
、strncpy
、strcat
、strncat
、strcmp
、strncmp
、strchr
、strstr
等。
<math.h>
提供数学函数,用于执行数学运算。
- 常用函数:
sqrt
、pow
、sin
、cos
、tan
、log
、exp
、abs
等。
<ctype.h>
提供字符处理函数,用于判断字符类型和转换字符大小写。
- 常用函数:
isalpha
、isdigit
、isspace
、toupper
、tolower
等。
<time.h>
提供时间和日期函数,用于处理时间和日期相关的操作。
- 常用函数:
time
、localtime
、gmtime
、strftime
、clock
等。
<stdbool.h>
提供布尔类型支持(C99引入)。
- 类型:
bool
,值为true
或false
。
<stddef.h>
定义了一些常用的类型和宏,如size_t
、NULL
。
常用函数示例
字符串函数
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello";
char dest[20];
// 复制字符串
strcpy(dest, src);
printf("Copied String: %s\n", dest);
// 连接字符串
strcat(dest, ", World!");
printf("Concatenated String: %s\n", dest);
// 获取长度
size_t len = strlen(dest);
printf("Length: %lu\n", len);
// 查找字符
char *ptr = strchr(dest, 'W');
if (ptr != NULL) {
printf("Found 'W' at position: %ld\n", ptr - dest);
}
return 0;
}
// 输出:
// Copied String: Hello
// Concatenated String: Hello, World!
// Length: 13
// Found 'W' at position: 7
数学函数
#include <stdio.h>
#include <math.h>
int main() {
double x = 16.0;
double y = 2.0;
double z = 3.14 / 2;
double sqrt_val = sqrt(x); // 计算平方根
double pow_val = pow(y, 3); // 计算y的3次方
double sin_val = sin(z); // 计算正弦值
printf("sqrt(16) = %.2f\n", sqrt_val);
printf("pow(2, 3) = %.2f\n", pow_val);
printf("sin(π/2) = %.2f\n", sin_val);
return 0;
}
// 输出:
// sqrt(16) = 4.00
// pow(2, 3) = 8.00
// sin(π/2) = 1.00
内存函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char str1[] = "Hello, World!";
size_t len = strlen(str1);
// 动态分配内存并复制字符串
char *copy = (char *)malloc(len + 1); // +1 为 '\0'
if (copy == NULL) {
printf("Memory allocation failed\n");
return 1;
}
strcpy(copy, str1);
printf("Copied String: %s\n", copy);
// 释放内存
free(copy);
copy = NULL;
return 0;
}
字符处理函数
#include <stdio.h>
#include <ctype.h>
int main() {
char ch = 'a';
if (isalpha(ch)) {
printf("%c is an alphabet\n", ch);
}
ch = '1';
if (isdigit(ch)) {
printf("%c is a digit\n", ch);
}
ch = ' ';
if (isspace(ch)) {
printf("Character is a whitespace\n");
}
ch = 'A';
char lower = tolower(ch);
printf("Lowercase of %c is %c\n", ch, lower);
return 0;
}
// 输出:
// a is an alphabet
// 1 is a digit
// Character is a whitespace
// Lowercase of A is a
数组、字符串与指针的关系
- 数组名与指针:
- 数组名是指向数组首元素的常量指针。
- 不能修改数组名指针的指向。
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // 合法 // arr = p + 1; // 错误,数组名不可赋值
- 指针运算:
- 可以通过指针遍历数组元素。
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); }
- 字符串与指针:
- 字符串是字符数组,也可以通过指针操作。
char str[] = "Hello"; char *p = str; while (*p != '\0') { printf("%c ", *p); p++; } // 输出: H e l l o
结构体和联合体
结构体(struct)
结构体用于将不同类型的数据组合在一起,形成一个新的数据类型。适用于表示实体对象,如人、学生、商品等。
定义与声明
- 定义结构体:
struct Person { char name[50]; int age; float height; };
- 声明结构体变量:
struct Person p1;
- 使用typedef简化结构体类型:
typedef struct { char name[50]; int age; float height; } Person; Person p2;
初始化
- 在声明时初始化:
struct Person p1 = {"Alice", 30, 5.5f};
- 逐成员初始化:
struct Person p2; strcpy(p2.name, "Bob"); p2.age = 25; p2.height = 6.0f;
- 使用typedef后的初始化:
Person p3 = {"Charlie", 28, 5.8f};
访问成员
通过点运算符.
访问结构体成员,使用箭头运算符->
通过指针访问。
// 使用点运算符
printf("Name: %s\n", p1.name);
printf("Age: %d\n", p1.age);
printf("Height: %.2f\n", p1.height);
// 使用箭头运算符
struct Person *ptr = &p1;
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Height: %.2f\n", ptr->height);
结构体数组
存储多个结构体变量的数组。
struct Person people[3] = {
{"Alice", 30, 5.5f},
{"Bob", 25, 6.0f},
{"Charlie", 28, 5.8f}
};
for (int i = 0; i < 3; i++) {
printf("Person %d: %s, %d, %.2f\n", i + 1, people[i].name, people[i].age, people[i].height);
}
// 输出:
// Person 1: Alice, 30, 5.50
// Person 2: Bob, 25, 6.00
// Person 3: Charlie, 28, 5.80
结构体与指针
通过指针访问结构体成员,使用箭头运算符->
。
#include <stdio.h>
#include <string.h>
struct Person {
char name[50];
int age;
float height;
};
int main() {
struct Person p = {"Dave", 35, 5.9f};
struct Person *ptr = &p;
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("Height: %.2f\n", ptr->height);
return 0;
}
// 输出:
// Name: Dave
// Age: 35
// Height: 5.90
联合体(union)
联合体与结构体类似,但所有成员共用同一块内存,节省空间。适用于需要在不同时间存储不同类型数据的场景。
定义与声明
- 定义联合体:
union Data { int i; float f; char str[20]; };
- 声明联合体变量:
union Data data;
使用
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i: %d\n", data.i);
data.f = 3.14f;
printf("data.f: %.2f\n", data.f);
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
// 注意:最后赋值的成员会覆盖前面的值
printf("After assigning str, data.i: %d\n", data.i); // 未定义行为
return 0;
}
// 输出(示例):
// data.i: 10
// data.f: 3.14
// data.str: C Programming
// After assigning str, data.i: 1214606444 // 示例值,实际输出取决于系统
注意:
- 联合体的所有成员共用同一块内存,因此最后赋值的成员会影响其他成员的值。
- 访问未初始化的成员会导致未定义行为。
联合体与结构体的区别
特性 | 结构体(struct) | 联合体(union) |
---|---|---|
内存分配 | 每个成员都有独立的内存空间 | 所有成员共用同一块内存 |
大小 | 所有成员中最大成员的大小之和 | 最大成员的大小 |
用途 | 需要同时存储多个不同类型数据 | 需要在不同时间存储不同类型数据,节省内存 |
枚举(enum)
枚举类型用于定义一组命名的整数常量,增强代码的可读性。
定义与使用
#include <stdio.h>
enum Day {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
};
int main() {
enum Day today = Wednesday;
printf("Day number: %d\n", today); // 输出 3
return 0;
}
手动指定值
enum Colors {
RED = 1,
GREEN = 3,
BLUE = 5
};
int main() {
enum Colors color = GREEN;
printf("Color value: %d\n", color); // 输出 3
return 0;
}
枚举与类型兼容性
枚举成员实际上是整数,可以与整型进行比较和运算。
enum Color { RED, GREEN, BLUE };
enum Color c = RED;
if (c == 0) {
printf("Color is RED\n");
}
内存管理
内存管理是C语言中重要的概念,涉及程序如何分配、使用和释放内存。合理的内存管理可以提高程序的效率和稳定性,避免内存泄漏和其他问题。
静态内存分配
在编译时分配内存,适用于全局变量、局部变量等。
全局变量
在所有函数外部声明,生命周期为整个程序运行期间。
int globalVar = 100;
int main() {
// 使用全局变量
printf("Global Var: %d\n", globalVar);
return 0;
}
局部变量
在函数内部声明,生命周期为函数调用期间。
int main() {
int localVar = 50;
printf("Local Var: %d\n", localVar);
return 0;
}
动态内存分配
在运行时通过指针申请和释放内存,适用于需要灵活内存管理的场景,如处理可变大小的数据结构。
malloc
malloc
函数用于分配指定字节数的内存,返回void *
类型指针,需要进行类型转换。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5); // 分配5个int的内存
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5
free(ptr); // 释放内存
ptr = NULL; // 避免悬挂指针
return 0;
}
calloc
calloc
函数用于分配内存,并初始化为0。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)calloc(5, sizeof(int)); // 分配5个int的内存,并初始化为0
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 0 0 0 0 0
free(ptr);
ptr = NULL;
return 0;
}
realloc
realloc
函数用于重新调整已分配内存的大小,保留原有数据。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 初始化数组
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 重新分配为10个int
ptr = (int *)realloc(ptr, sizeof(int) * 10);
if (ptr == NULL) {
printf("Memory reallocation failed\n");
return 1;
}
// 初始化新分配的内存
for (int i = 5; i < 10; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 10; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5 6 7 8 9 10
free(ptr);
ptr = NULL;
return 0;
}
free
free
函数用于释放动态分配的内存,避免内存泄漏。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int) * 5);
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i + 1;
}
// 打印数组
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
// 输出: 1 2 3 4 5
free(ptr); // 释放内存
ptr = NULL; // 避免悬挂指针
return 0;
}
内存管理注意事项
- 避免内存泄漏:每次
malloc
或calloc
后,都应有对应的free
。未释放的内存会导致程序占用越来越多的内存。int *p = (int *)malloc(sizeof(int) * 10); // 使用p free(p); // 释放内存
- 避免悬挂指针:释放内存后,应将指针设置为
NULL
,防止指针指向已释放的内存。free(p); p = NULL;
- 检查分配是否成功:
malloc
和calloc
可能返回NULL
,需检查内存分配是否成功。int *p = (int *)malloc(sizeof(int) * 10); if (p == NULL) { // 处理分配失败 }
- 避免重复释放:同一块内存只应释放一次,重复释放会导致未定义行为。
free(p); // free(p); // 错误,第二次释放同一指针
- 避免越界访问:确保访问指针指向的内存在有效范围内,防止访问非法内存。
内存泄漏与检测
内存泄漏是指程序中动态分配的内存未被释放,导致内存无法再次利用,最终耗尽系统内存。
检测工具
- Valgrind:Linux下的内存调试工具,能够检测内存泄漏、未初始化内存使用等问题。
valgrind --leak-check=full ./your_program
- AddressSanitizer:GCC和Clang提供的内存错误检测工具。
gcc -fsanitize=address -g your_program.c -o your_program ./your_program
示例:内存泄漏
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *)malloc(sizeof(int) * 10);
if (p == NULL) {
return 1;
}
// 使用内存
for (int i = 0; i < 10; i++) {
p[i] = i;
}
// 忘记释放内存
return 0;
}
使用Valgrind检测:
valgrind --leak-check=full ./a.out
输出:
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2BBAF: malloc (vg_replace_malloc.c:299)
==12345== by 0x4005ED: main (leak_example.c:6)
解决方法:确保每次malloc
或calloc
后都有对应的free
。
高级主题
位域
位域允许在结构体中定义按位划分的成员,节省内存,适用于嵌入式编程和硬件接口编程。
定义与使用
#include <stdio.h>
struct Flags {
unsigned int flag1 : 1;
unsigned int flag2 : 2;
unsigned int flag3 : 3;
};
int main() {
struct Flags f = {1, 2, 5};
printf("Flag1: %u\n", f.flag1); // 输出 1
printf("Flag2: %u\n", f.flag2); // 输出 2
printf("Flag3: %u\n", f.flag3); // 输出 5
// 注意:位域的大小限制
f.flag1 = 2; // 仅1位,存储结果为0(溢出)
printf("Flag1 after overflow: %u\n", f.flag1); // 输出 0
return 0;
}
// 输出:
// Flag1: 1
// Flag2: 2
// Flag3: 5
// Flag1 after overflow: 0
注意事项:
- 位域成员的位宽必须为正整数。
- 位域的布局和顺序可能因编译器和平台不同而不同。
- 不建议在跨平台代码中依赖位域的具体布局。
内联汇编
C语言支持嵌入汇编代码,允许程序员直接在C代码中编写汇编指令,实现底层操作。
语法(GCC扩展):
#include <stdio.h>
int main() {
int a = 10, b = 20, c;
__asm__("addl %%ebx, %%eax;"
: "=a" (c) // 输出
: "a" (a), "b" (b) // 输入
);
printf("c = %d\n", c); // 输出 c = 30
return 0;
}
注意:
- 内联汇编是编译器特定的扩展,代码不可移植。
- 需要了解目标平台的汇编语言和寄存器。
- 现代编译器优化可能会与内联汇编代码冲突,需谨慎使用。
多文件项目管理
大型C项目通常由多个源文件和头文件组成,需要良好的项目结构和编译管理。
项目结构示例
project/
├── main.c
├── math_utils.c
├── math_utils.h
└── Makefile
示例文件
math_utils.h:
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
math_utils.c:
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
main.c:
#include <stdio.h>
#include "math_utils.h"
int main() {
int sum = add(5, 3);
int product = multiply(5, 3);
printf("Sum: %d\n", sum); // 输出 Sum: 8
printf("Product: %d\n", product); // 输出 Product: 15
return 0;
}
Makefile:
CC = gcc
CFLAGS = -Wall -g
all: main
main: main.o math_utils.o
$(CC) $(CFLAGS) -o main main.o math_utils.o
main.o: main.c math_utils.h
$(CC) $(CFLAGS) -c main.c
math_utils.o: math_utils.c math_utils.h
$(CC) $(CFLAGS) -c math_utils.c
clean:
rm -f *.o main
编译:
make
./main
输出:
Sum: 8
Product: 15
静态与动态链接库
静态链接库
静态链接库在编译时将库代码嵌入到可执行文件中。
创建静态库:
gcc -c math_utils.c
ar rcs libmath_utils.a math_utils.o
使用静态库:
#include <stdio.h>
#include "math_utils.h"
int main() {
int sum = add(5, 3);
int product = multiply(5, 3);
printf("Sum: %d\n", sum);
printf("Product: %d\n", product);
return 0;
}
编译:
gcc -o main main.c -L. -lmath_utils
动态链接库
动态链接库在运行时加载,适用于共享库和插件。
创建动态库:
gcc -fPIC -c math_utils.c
gcc -shared -o libmath_utils.so math_utils.o
使用动态库:
#include <stdio.h>
#include "math_utils.h"
int main() {
int sum = add(5, 3);
int product = multiply(5, 3);
printf("Sum: %d\n", sum);
printf("Product: %d\n", product);
return 0;
}
编译:
gcc -o main main.c -L. -lmath_utils
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main
注意:
- 动态库的路径需要在运行时通过环境变量(如
LD_LIBRARY_PATH
)指定。 - 静态库和动态库在使用和分发上有不同的优缺点。
C语言与C++的关系
C++是C语言的超集,兼容C语言的绝大部分特性,并增加了面向对象编程的支持。了解C++可以扩展C语言的应用范围,但两者在语法和特性上有明显差异。
兼容性
- C语言的C++代码通常可以直接编译和运行。
- C++引入了关键字、类、模板等C语言不具备的特性。
示例:
C语言代码
#include <stdio.h>
int main() {
printf("Hello, C!\n");
return 0;
}
C++代码
#include <iostream>
using namespace std;
int main() {
cout << "Hello, C++!" << endl;
return 0;
}
编译:
gcc -o hello_c hello_c.c
g++ -o hello_cpp hello_cpp.cpp
调试与优化
常用调试工具
- GDB(GNU Debugger):功能强大的命令行调试器,支持断点、单步执行、变量查看等功能。
gcc -g your_program.c -o your_program gdb ./your_program
- Valgrind:内存调试工具,检测内存泄漏和未初始化内存使用。
valgrind --leak-check=full ./your_program
- AddressSanitizer:GCC和Clang提供的内存错误检测工具。
gcc -fsanitize=address -g your_program.c -o your_program ./your_program
- IDE调试器:如Visual Studio、CLion、Code::Blocks等提供图形化调试功能。
调试技巧
- 使用断点:在关键代码行设置断点,暂停程序执行,检查变量状态。
(gdb) break main.c:10 (gdb) run
- 单步执行:逐行执行代码,观察程序的执行流程和变量变化。
(gdb) step (gdb) next
- 查看变量:检查变量的值和状态。
(gdb) print variable
- 堆栈跟踪:查看函数调用堆栈,定位问题源头。
(gdb) backtrace
- 使用日志:在关键位置添加打印语句,记录程序运行情况。
printf("Debug: a = %d\n", a);
性能优化方法
- 算法优化:选择更高效的算法,降低时间复杂度。
- 减少不必要的计算:缓存中间结果,避免重复计算。
// 优化前 for (int i = 0; i < n; i++) { printf("%d ", arr[i] * 2); } // 优化后 for (int i = 0; i < n; i++) { int temp = arr[i] * 2; printf("%d ", temp); }
- 使用内联函数:减少函数调用开销。
inline int add(int a, int b) { return a + b; }
- 内存访问优化:优化数据结构布局,利用缓存局部性。
- 编译优化选项:使用编译器优化选项,如
-O2
、-O3
等。gcc -O2 -o optimized_program your_program.c
常见错误及其排查
- 内存泄漏:通过工具(如Valgrind)检测未释放的内存,确保每次
malloc
都有对应的free
。 - 悬挂指针:释放内存后将指针设为
NULL
,避免指针指向已释放的内存。 - 数组越界:确保访问数组元素在合法范围内,避免访问非法内存。
- 未初始化变量:在使用变量前初始化,避免未定义行为。
int a = 0; // 初始化
- 类型错误:确保变量类型与操作匹配,避免类型转换错误。
- 指针错误:确保指针指向有效内存,避免野指针和悬挂指针。
示例:数组越界
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", arr[5]); // 错误,越界访问
return 0;
}
// 解决方法:确保索引在0到size - 1范围内。
C语言的应用
C语言因其高效性和灵活性,在许多领域有广泛应用,包括但不限于:
操作系统
- Unix/Linux:大部分内核代码和系统工具使用C语言编写。
- Windows:Windows的部分核心组件和驱动程序使用C语言编写。
嵌入式系统
- 微控制器编程:如Arduino、STM32等使用C语言开发固件。
- 物联网设备:如传感器、智能家居设备使用C语言编写嵌入式软件。
编译器
- GCC:GNU Compiler Collection中的C编译器。
- Clang:LLVM项目中的C语言编译器。
数据库系统
- MySQL:开源关系型数据库管理系统,核心部分使用C语言编写。
- PostgreSQL:开源对象关系型数据库系统,核心部分使用C语言编写。
图形和游戏开发
- OpenGL:跨平台的图形API,使用C语言进行底层开发。
- 游戏引擎:如Unreal Engine的部分组件使用C++(C的超集)编写。
驱动程序
- 设备驱动:操作系统中用于控制硬件设备的软件组件,使用C语言编写。
高性能计算
- 科学计算:如数值模拟、分子动力学等使用C语言编写高效的算法。
- 图像处理:如图像滤波、视频编码等使用C语言实现高性能算法。
网络编程
- 服务器软件:如Apache、Nginx等使用C语言编写高性能的网络服务器。
- 网络协议实现:如TCP/IP协议栈的实现部分使用C语言编写。
系统工具
- 文本编辑器:如Vim、Emacs的部分组件使用C语言编写。
- 文件管理工具:如
cp
、mv
、rm
等Unix命令使用C语言编写。
虚拟机和容器
- Docker:容器化平台,部分底层组件使用C语言编写。
- 虚拟机监控程序:如QEMU使用C语言编写。
具体应用示例
操作系统中的C语言应用
示例:Linux内核中的C代码片段
// kernel/sched/core.c
void schedule(void) {
struct task_struct *prev, *next;
// 获取当前任务
prev = current;
// 选择下一个任务
next = pick_next_task();
if (prev != next) {
// 切换任务
switch_to(prev, next);
}
}
嵌入式系统中的C语言应用
示例:Arduino控制LED
// Arduino代码,使用C++语言的Arduino库(C的超集)
#define LED_PIN 13
void setup() {
pinMode(LED_PIN, OUTPUT); // 设置LED_PIN为输出模式
}
void loop() {
digitalWrite(LED_PIN, HIGH); // 打开LED
delay(1000); // 延迟1秒
digitalWrite(LED_PIN, LOW); // 关闭LED
delay(1000); // 延迟1秒
}
编译器中的C语言应用
示例:简单的C语言编译器
// 简单的C语言编译器框架,实际编译器功能复杂
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: simplecc <source_file.c>\n");
return 1;
}
// 读取源代码文件
FILE *fp = fopen(argv[1], "r");
if (fp == NULL) {
printf("Failed to open source file\n");
return 1;
}
// 简单的编译流程
// 解析、语义分析、优化、生成目标代码
fclose(fp);
printf("Compilation successful\n");
return 0;
}
注意:实际编译器需要实现词法分析、语法分析、语义分析、优化和代码生成等复杂功能。
总结
C语言作为一门经典的编程语言,以其高效、灵活和可移植性在计算机科学领域占据重要地位。它不仅适用于系统编程、嵌入式系统等底层开发领域,还为许多高级编程语言和现代技术奠定了基础。通过深入学习C语言的基础和高级特性,掌握指针和内存管理、结构体和联合体等核心概念,以及熟悉标准库和预处理器的使用,你将能够编写高效、稳定和可维护的代码。
关键学习点
- 基础语法:变量声明、数据类型、操作符、控制结构。
- 函数与指针:理解函数声明与定义、指针的使用和指针运算。
- 数据结构:掌握数组、字符串、结构体、联合体和枚举。
- 内存管理:学习动态内存分配、释放及内存安全。
- 文件操作:了解文件的打开、读写和关闭操作。
- 标准库:熟悉常用的C标准库函数,提升编程效率。
- 高级主题:探索位域、多文件项目管理、静态与动态链接库、内联汇编等高级概念。
- 调试与优化:掌握调试工具和优化方法,提高代码质量和性能。
学习建议
- 实践编程:通过编写实际项目和解决编程问题,巩固理论知识。
- 阅读经典书籍:如《C程序设计语言》、《C Primer Plus》,深入理解C语言的精髓。
- 参与社区:加入在线社区和论坛,交流经验,获取帮助。
- 使用调试工具:熟练使用GDB、Valgrind等工具,提升调试能力。
- 编写高质量代码:遵循最佳实践,编写可读、可维护和高效的代码。
- 持续学习:C语言不断发展,关注最新标准和技术,保持知识的更新。