C语言从入门到放弃
- 1. 介绍
- 1.1 特点
- 1.2 历史与发展
- 1.3 应用领域
- 2. 安装
- 2.1 编译器安装
- 2.2 编辑器安装
- 3. 第一个程序
- 1. 包含头文件
- 2. 主函数定义
- 3. 打印语句
- 4. 返回值
- 4. 基础语法
- 4.1 注释
- 4.1.1 单行注释
- 4.1.2 多行注释
- 4.2 关键字
- 4.2.1 C语言标准
- 4.2.2 C89/C90关键字(32)
- 1. 数据类型关键字(12个)
- 2. 控制语句关键字(12个)
- 3. 存储类型关键字(5个)
- 4. 其他关键字(3个)
- 4.2.3 C99 新增关键字
- 4.2.4 C11 新增关键字
- 4.2.5 C17移除关键字
- 4.2.6 C23新增关键字
- 4.3 常量
- 1. 使用预处理器指令 `#define`
- 2. 使用 `const` 关键字
- 3. 枚举类型 `enum`
- 4.4 变量
- 1. 变量的基本概念
- 2. 变量的声明与定义
- 3. 数据类型
- 4. 初始化
- 5. 存储类别
- 6. 变量的作用域和可见性
- 4.5 字面量
- 1. 整数字面量(Integer Literals)
- 2. 浮点数字面量(Floating-point Literals)
- 3. 字符字面量(Character Literals)
- 4. 字符串字面量(String Literals)
- 5. 布尔字面量(Boolean Literals)
- 4.6 格式化输出
- 4.7 ASCII
- 4.8 转义序列
- 4.9 sizeof
- 4.10 数据类型
- 1. 基本数据类型
- 整型(Integer Types)
- 浮点型(Floating-point Types)
- 布尔型(Boolean Type)
- 枚举类型(Enumeration Type)
- 2. 派生数据类型
- 指针(Pointer Types)
- 数组(Array Types)
- 结构体(Structure Types)
- 联合体(Union Types)
- 3. 空类型(Void Type)
- 4.11 输入输出函数
- 1. 格式化输出函数
- 示例
- 2. 格式化输入函数
- 3. 非格式化输入输出函数
- 4. 文件输入输出函数
- 4.12 运算符
- 1. 算术运算符
- 2. 关系运算符
- 3. 逻辑运算符
- 4. 位运算符
- 5. 赋值运算符
- 6. 条件运算符
- 7. 其他运算符
- 8. 运算符优先级表
- 9. 短路运算
- 10. 类型转换
- 5. 控制结构
- 1. 顺序结构
- 2. 选择结构(条件语句)
- 2.1 `if` 语句
- 2.2 `switch` 语句
- 3. 循环结构
- 3.1 `for` 循环
- 3.2 `while` 循环
- 3.3`do...while` 循环
- 4. 其他语句
- 6. 数组
- 6.1 一维数组
- 1. 定义和声明
- 2. 初始化
- 3. 访问元素
- 6.2 多维数组
- 1. 二维数组
- 2. 三维及更高维数组
- 6.3 数组的操作
- 1. 遍历数组
- 6.4 动态数组
- 6.5 字符串与字符数组
- 7. 字符串
- 7.1 字符串的声明与初始化
- 7.2 访问字符串中的字符
- 7.3 字符串的输入输出
- 7.4 字符串处理函数
- `strlen`
- `strcpy`
- `strncpy`
- `strcat`
- `strncat`
- `strcmp`
- `strncmp`
- `strchr`
- `strstr`
- 7.5 动态字符串
- 8. 函数
- 8.1 函数的定义
- 8.2 函数声明与定义
- 1. 函数声明(原型)
- 2. 函数定义
- 3. 参数传递
- 4. 返回值
- 5. 函数的调用
- 6. 函数的嵌套与递归
- 7. 标准库函数
- 8. 函数指针
- 9. 指针
- 9.1 指针的基本概念
- 9.2 访问指针指向的数据
- 9.3 指针运算
- 9.4 指针与数组
- 9.5 动态内存分配
- 9.6 函数参数传递
- 9.7 指针数组与多级指针
- 9.8 指针与字符串
- 9.9 函数指针
- 9.10 指针的安全性与注意事项
- 10. 结构体
- 10.1 结构体的定义
- 10.2 结构体变量的声明与初始化
- 10.3 访问结构体成员
- 10.4 结构体数组
- 10.5 结构体作为函数参数
- 10.6 嵌套结构体
- 10.7 动态分配结构体
- 10.8 匿名结构体与typedef
- 11. 联合体
- 11.1 联合体的基本概念
- 11.2 联合体变量的声明与初始化
- 11.3 访问联合体成员
- 11.4 联合体的内存布局
- 11.5 联合体与结构体的比较
- 11.6 匿名联合体与typedef
- 11.7 联合体的应用场景
- 11.8 注意事项
- 11.9 结合枚举使用联合体
- 12. 头文件
- 1. 头文件的作用
- 2. 创建自定义头文件
- 3. 包含头文件
- 4. 实现与声明分离
- 5. 使用头文件的优点
- 6. 注意事项
- 13. 文件
- 1. 文件指针
- 2. 打开文件
- 3. 关闭文件
- 4. 文件读取
- 5. 文件写入
- 6. 文件定位
- 7. 文件检测
- 8. 文件复制示例
- 14. 数据库
- 1. 使用MySQL C API
- 2. 使用PostgreSQL C API (libpq)
- 3. 使用SQLite
1. 介绍
C语言是一种广泛使用的高级编程语言,由Dennis M. Ritchie(丹尼斯·里奇,C语言之父)在20世纪70年代初于贝尔实验室开发。它最初是为设计UNIX操作系统而创建的,但后来因其高效性、灵活性和跨平台特性而成为世界上最流行的编程语言之一。
1.1 特点
- 简洁且强大:C语言提供了一套精简的关键字集,并允许程序员直接操作内存(通过指针),这使得它可以非常高效地执行任务。
- 移植性强:C 程序可以在不同平台上编译运行,只需做少量甚至不做修改。这种跨平台的能力使C成为了编写操作系统、嵌入式系统和其他底层软件的理想选择。
- 丰富的库函数:标准C库提供了大量的预定义函数,用于文件 I/O、字符串处理、数学运算等常用功能,简化了开发过程。
- 支持结构化编程:它鼓励使用函数来组织代码,增强了程序的模块化和可读性。
- 指针支持:C中的指针允许直接访问和操纵内存地址,这对于实现复杂的数据结构(如链表、树)以及优化性能至关重要。
- 编译型语言:C是一种编译型语言,这意味着源代码在执行前需要被编译成机器码。编译后的程序通常比解释型语言更快。
1.2 历史与发展
- 起源:C 语言源于 B 语言,后者又是 BCPL 的后继者。Ritchie 在 B 语言的基础上添加了类型系统和其他改进,从而创造了 C 语言
- 标准化:ANSI 和 ISO 分别于1989年和1990年发布了 C 语言的标准版本(即 C89 或 ANSI C)。随后有多个修订版发布,包括 C99、C11 和最新的 C18。
1.3 应用领域
- 系统编程:由于其低级别的特性和对硬件的良好控制,C 被广泛应用于操作系统内核、驱动程序和网络协议栈等领域。
- 嵌入式系统:从微控制器到复杂的实时控制系统,C 都是非常流行的选择,因为它可以生成高效的代码并且占用较少资源。
- 游戏开发:许多游戏引擎和框架都是用 C 或 C++ 编写的,因为它们能够提供必要的性能优势。
- 数据库管理系统:像 MySQL 这样的数据库也是用 C 实现的,以确保高性能的数据管理和查询处理能力。
2. 安装
2.1 编译器安装
MinGW(Minimalist GNU for Windows)是一个用于 Windows 操作系统的开源编译器工具链,它基于 GNU 工具集(包括 GCC 编译器、GDB 调试器等),但专门为 Windows 环境进行了优化和定制。
2.2 编辑器安装
Visual Studio Code(简称 VS Code)是一款由微软开发的源代码编辑器,支持多种编程语言,并且可以通过插件扩展其功能。
安装扩展:Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code、C/C++、Code Runner
3. 第一个程序
#include <stdio.h>int main()
{printf("Hello World\n");return 0;
}
1. 包含头文件
#include <stdio.h>
:这行预处理指令告诉编译器包含标准输入输出库(Standard Input Output library)的声明。stdio.h
提供了诸如printf
和scanf
等函数的原型声明,使得我们可以在程序中使用这些函数。
2. 主函数定义
int main()
:这是程序的入口点——主函数。根据 C 标准,main
函数应该返回一个整数类型 (int
),通常用来向操作系统报告程序的执行状态。返回值0
表示程序成功结束。
3. 打印语句
printf("Hello World\n");
:这行代码调用printf
函数来输出字符串"Hello World"
到标准输出(通常是控制台)。\n
是换行符,表示在输出完文本后移动光标到下一行的开始位置。
4. 返回值
return 0;
:这条语句明确地指定了当main
函数结束时返回给操作系统的值为0
,这通常被解释为程序成功完成。如果程序遇到错误,可以返回非零值(例如1
),以指示发生了某种问题。
4. 基础语法
4.1 注释
注释用于向代码中添加解释性文本,这些文本不会被编译器编译为机器码,因此不会影响程序的执行。注释的主要目的是提高代码的可读性和可维护性,帮助开发者理解代码的功能和逻辑。
4.1.1 单行注释
单行注释是从 //
开始直到该行结束的所有内容都被视为注释。
#include <stdio.h>int main()
{// 控制台输出:Hello Worldprintf("Hello World\n");return 0;
}
4.1.2 多行注释
多行注释是用 /*
开始,并以 */
结束。在这两个标记之间的所有内容都被视为注释,可以跨越多行。多行注释适合用于较长的解释或暂时禁用一段代码。
#include <stdio.h>/*C语言入口程序main函数
*/
int main()
{printf("Hello World\n");return 0;
}
4.2 关键字
关键字是具有特殊含义和用途的保留字,它们不能用作标识符(如变量名、函数名等)。这些关键字定义了 C 语言的基本结构和语法。
4.2.1 C语言标准
序号 | 标准名称 | 发布时间 |
---|---|---|
1 | C89/C90 标准 | 1990 |
2 | C99 标准 | 1999 |
3 | C11 标准 | 2011 |
4 | C17 标准 | 2018 |
5 | C23 标准 | 2024 |
4.2.2 C89/C90关键字(32)
1. 数据类型关键字(12个)
这些关键字用于声明变量或函数的数据类型。
关键字 | 说明 |
---|---|
char | 声明字符型变量或函数返回值类型。 |
short | 声明短整型变量或函数返回值类型。 |
int | 声明整型变量或函数返回值类型。 |
long | 声明长整型变量或函数返回值类型。 |
signed | 明确声明有符号数类型(如 signed int )。 |
unsigned | 定义无符号数类型(如 unsigned int )。 |
float | 声明单精度浮点型变量或函数返回值类型。 |
double | 声明双精度浮点型变量或函数返回值类型。 |
struct | 定义结构体类型。 |
union | 定义共用体类型,允许不同类型的成员共享同一块内存。 |
enum | 定义枚举类型。 |
void | 表示没有类型;用于定义函数返回值为空或指针指向未知类型的数据。 |
2. 控制语句关键字(12个)
这些关键字用于控制程序流程,包括循环、条件分支等。
-
循环控制(5个)
for
:定义for
循环。do
:与while
组合使用,形成do-while
循环。while
:定义while
循环。break
:终止循环或switch
语句。continue
:结束当前循环迭代并继续下一次迭代。
-
条件语句(3个)
if
:条件判断语句。else
:与if
结合使用,形成条件分支。goto
:无条件跳转到指定标签处,不推荐频繁使用。
-
开关语句(3个)
switch
:控制多分支选择语句。case
:在switch
语句中定义一个分支。default
:在switch
语句中定义默认分支。
-
返回语句(1个)
return
:返回从函数调用的结果给调用者。
3. 存储类型关键字(5个)
这些关键字用于指定变量的存储类型和作用域。
关键字 | 说明 |
---|---|
auto | 声明自动变量,默认情况下局部变量即为自动变量,很少使用。 |
extern | 声明外部变量或函数,表明其定义位于其他文件中。 |
register | 建议编译器将变量存储在寄存器中以加快访问速度,现代编译器通常忽略此建议。 |
static | 定义静态变量或函数,限制其作用域和生命周期。 |
typedef | 创建新的类型名作为现有类型的别名。 |
4. 其他关键字(3个)
这些关键字用于特定目的,如声明常量、查询类型大小以及防止优化。
关键字 | 说明 |
---|---|
const | 定义常量,表示该值在程序运行期间不可改变。 |
sizeof | 运算符,返回数据类型的大小(以字节为单位)。 |
volatile | 告诉编译器不要对变量进行优化,因为它的值可能会被外部因素改变。 |
4.2.3 C99 新增关键字
关键字 | 说明 |
---|---|
_Bool | 定义布尔类型(true 或 false )。通常通过 <stdbool.h> 头文件中的宏定义为 bool 类型。 |
complex | 用于复数类型(需要包含 <complex.h> )。 |
imaginary | 用于虚数类型(需要包含 <complex.h> ),但较少使用。 |
inline | 提示编译器将函数内联展开以提高性能。 |
restrict | 用于指针声明,表示该指针是访问对象的唯一方式,帮助编译器优化代码。 |
4.2.4 C11 新增关键字
关键字 | 说明 |
---|---|
_Alignas | 指定数据类型的对齐方式。 |
_Alignof | 查询类型的对齐要求(类似于 sizeof ,但用于对齐属性)。 |
_Atomic | 定义原子类型,确保操作不会被打断,常用于多线程编程中。 |
_Bool | 虽然 _Bool 最初是在 C99 中引入的,但在 C11 中继续保留。 |
_Complex | 同样是从 C99 继承而来,用于复数类型。 |
_Generic | 支持泛型选择表达式,允许根据参数类型选择不同的实现。 |
_Imaginary | 从 C99 继承而来,用于虚数类型,但较少使用。 |
_Noreturn | 标记函数不会返回(例如 exit 函数),帮助编译器优化代码。 |
_Static_assert | 编译时断言,如果条件不满足则产生编译错误,增强代码健壮性。 |
_Thread_local | 定义线程局部存储变量,即每个线程拥有该变量的一个独立副本。 |
4.2.5 C17移除关键字
_Imaginary
:在C17中,_Imaginary
关键字被标记为过时,并且不再推荐使用。复数类型现在主要通过_Complex
来表示。
4.2.6 C23新增关键字
关键字 | 说明 |
---|---|
_Alignof | 查询类型的对齐要求(虽然不是新引入,但得到了进一步的支持和澄清)。 |
_Atomic | 定义原子类型,确保操作不会被打断,常用于多线程编程中(得到进一步支持)。 |
_BitInt | 支持固定宽度的整数类型,允许定义特定位数的整数类型。 |
_Decimal32 | 支持 IEEE 754-2008 规范中的 32 位十进制浮点数类型。 |
_Decimal64 | 支持 IEEE 754-2008 规范中的 64 位十进制浮点数类型。 |
_Decimal128 | 支持 IEEE 754-2008 规范中的 128 位十进制浮点数类型。 |
_Generic | 支持泛型选择表达式(得到进一步支持和扩展)。 |
_Imaginary | 虽然在 C17 中被标记为过时,但在 C23 中重新考虑其使用情况。 |
_Noreturn | 标记函数不会返回(例如 exit 函数),帮助编译器优化代码(得到进一步支持)。 |
_Static_assert | 编译时断言,如果条件不满足则产生编译错误(得到进一步支持)。 |
_Thread_local | 定义线程局部存储变量,即每个线程拥有该变量的一个独立副本(得到进一步支持)。 |
bool | 定义布尔类型(通过 <stdbool.h> 头文件引入,得到进一步支持)。 |
char8_t | 表示 UTF-8 编码的字符类型,增强了对 Unicode 的支持。 |
char16_t | 表示 UTF-16 编码的字符类型,增强了对 Unicode 的支持。 |
char32_t | 表示 UTF-32 编码的字符类型,增强了对 Unicode 的支持。 |
4.3 常量
常量是指在程序执行期间其值不能被修改的量。C语言提供了多种定义常量的方式,包括使用预处理器指令、关键字 const
以及枚举类型。
1. 使用预处理器指令 #define
这是最传统的定义常量的方法,通过预处理器指令 #define
来创建宏定义。
示例
#define PI 3.141592653589793
#define MAX_SIZE 100
优点:
- 简单直接,适用于所有数据类型。
- 可以定义复杂的表达式或字符串。
缺点:
- 没有类型检查,容易导致错误。
- 不是真正意义上的变量,调试时可能难以跟踪。
2. 使用 const
关键字
从 C99 开始,推荐使用 const
关键字来定义常量。这种方式更安全,因为编译器可以进行类型检查,并且可以在运行时分配内存。
示例
const double PI = 3.141592653589793;
const int MAX_SIZE = 100;
优点:
- 提供类型安全和更好的调试支持。
- 可以用于局部和全局作用域。
- 支持数组和其他复杂类型的初始化。
缺点:
- 对于一些非常量表达式(如函数返回值),不能直接用作数组维度等编译时常量。
3. 枚举类型 enum
枚举类型提供了一种定义整型常量集合的方法,通常用于表示一组相关的符号名称。
示例
enum Weekday {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
};
优点:
- 清晰地表达了相关常量之间的关系。
- 编译器自动为每个枚举成员分配唯一的整数值。
- 改善代码的可读性和维护性。
缺点:
- 只能用于整数类型。
4.4 变量
变量是程序中用于存储数据的命名位置。每个变量都有一个特定的类型,这决定了它所占用的内存大小和可以进行的操作。
1. 变量的基本概念
- 标识符:变量名必须是一个有效的标识符。标识符是由字母(a-z, A-Z)、数字(0-9)和下划线(_)组成,并且不能以数字开头。
- 作用域:变量的作用域决定了它可以在代码的哪些部分访问。常见的作用域有全局作用域(文件级别)和局部作用域(函数内部或块内)。
- 生命周期:变量的生命周期指的是它在程序运行期间存在的时间段。静态变量和全局变量在整个程序运行期间都存在,而自动变量仅在其定义的作用域内有效。
2. 变量的声明与定义
声明
声明告诉编译器变量的名称和类型,但不一定分配内存空间。全局变量的声明通常出现在所有函数之外。
extern int globalVar; // 全局变量的外部声明
定义
定义不仅声明了变量的类型和名称,还会为该变量分配内存空间。定义通常伴随着初始值的赋值。
int globalVar = 10; // 全局变量的定义并初始化
局部变量
局部变量是在函数或代码块内部声明的,其作用域仅限于该函数或代码块。
void myFunction() {int localVar = 5; // 局部变量
}
3. 数据类型
C语言基本数据类型:
- 整型 (
int
,short
,long
,long long
):用于表示整数。 - 浮点型 (
float
,double
,long double
):用于表示实数。 - 字符型 (
char
):用于表示单个字符。 - 布尔型 (
_Bool
或bool
):用于表示逻辑值(从 C99 开始引入,通常通过<stdbool.h>
头文件中的宏定义)。 - 指针 (
*
):用于存储地址。 - 枚举 (
enum
):用于定义一组命名的整数值。 - 结构体 (
struct
) 和 联合体 (union
):用于组合不同类型的数据成员。
4. 初始化
变量可以在定义时进行初始化,也可以稍后通过赋值语句来设置初始值。
int x = 10; // 定义并初始化
char ch = 'A'; // 定义并初始化
double d = 3.14; // 定义并初始化
如果不显式地初始化变量,那么它的值将是未定义的(对于自动变量),或者为零(对于静态变量和全局变量)。
5. 存储类别
C语言提供了几种不同的存储类别来控制变量的存储方式和生命周期:
- 自动 (
auto
):默认情况下,局部变量都是自动变量,它们在函数调用时创建,在函数返回时销毁。 - 静态 (
static
):静态变量存在于整个程序的执行过程中,即使定义它们的函数已经返回。静态局部变量只初始化一次,而静态全局变量只能在定义它们的文件中访问。 - 寄存器 (
register
):建议编译器将变量存储在寄存器中以加快访问速度,但这只是一个提示,现代编译器可能会忽略此关键字。 - 外部 (
extern
):用于引用其他地方定义的变量,特别是在多文件项目中共享全局变量。
6. 变量的作用域和可见性
- 全局变量:在所有函数之外声明的变量具有全局作用域,可以在整个文件甚至多个文件之间访问(如果正确声明的话)。
- 局部变量:在函数或代码块内部声明的变量仅在该函数或代码块内可见。
- 块作用域:由大括号
{}
包围的代码区域内的变量只能在该区域内访问。
示例代码
#include <stdio.h>// 全局变量声明
int globalVar = 10;void myFunction() {// 局部变量声明static int staticVar = 0; // 静态局部变量int autoVar = 5; // 自动局部变量staticVar++;printf("Static Variable: %d\n", staticVar);printf("Auto Variable: %d\n", autoVar);
}int main() {// 局部变量声明int localMainVar = 20;printf("Global Variable: %d\n", globalVar);myFunction();myFunction(); // 注意静态变量的行为return 0;
}
4.5 字面量
字面量(literals)是指直接出现在代码中的固定值,它们不需要通过变量名来引用。字面量可以是整数、浮点数、字符或字符串。
1. 整数字面量(Integer Literals)
整数字面量用于表示整数值。它们可以使用十进制、八进制或十六进制表示法。
- 十进制:默认情况下,整数是十进制的。
- 八进制:以
0
开头(例如017
表示八进制的 15)。 - 十六进制:以
0x
或0X
开头(例如0x1A
表示十六进制的 26)。 - 后缀:可以添加
u
或U
表示无符号类型,l
或L
表示长整型(long
),ll
或LL
表示长长整型(long long
)。这些后缀可以组合使用,例如100uL
。
示例
int a = 10; // 十进制
int b = 012; // 八进制 (等同于十进制 10)
int c = 0xA; // 十六进制 (等同于十进制 10)
unsigned long d = 100uL;
2. 浮点数字面量(Floating-point Literals)
浮点数字面量用于表示实数。它们可以包含小数点和指数部分,并且可以用科学计数法表示。
- 小数形式:包括一个小数点(例如
3.14
)。 - 指数形式:使用
e
或E
来表示指数(例如6.022e23
表示 (6.022 \times 10^{23}))。 - 后缀:可以添加
f
或F
表示float
类型,l
或L
表示long double
类型。
示例
double pi = 3.14159;
float f = 1.234f;
long double ld = 1.234L;
3. 字符字面量(Character Literals)
字符字面量用于表示单个字符,用单引号括起来。
- 普通字符:例如
'A'
。 - 转义序列:用于表示特殊字符,如换行
\n
、制表符\t
等。 - 多字节字符:可以使用 Unicode 编码表示非ASCII字符(依赖于编译器支持)。
示例
char letter = 'A';
char newline = '\n';
char copyright = '\u00A9'; // Unicode for ©
4. 字符串字面量(String Literals)
字符串字面量用于表示一系列字符,用双引号括起来。它们实际上是字符数组,并以空字符 \0
结尾。
- 普通字符串:例如
"Hello, World!"
。 - 拼接字符串:多个相邻的字符串字面量会自动拼接成一个字符串。
- 宽字符字符串:使用前缀
L
表示宽字符字符串(wchar_t
类型),适用于多字节字符集。
示例
const char* greeting = "Hello, ";
const char* name = "World!";
const char* message = "Hello, " "World!"; // 拼接字符串
const wchar_t* wideString = L"你好,世界!";
5. 布尔字面量(Boolean Literals)
从 C99 开始,C语言支持布尔类型 _Bool
(通常通过 <stdbool.h>
头文件中的宏定义为 bool
),其字面量为 true
和 false
。
示例
#include <stdbool.h>
bool isTrue = true;
bool isFalse = false;
4.6 格式化输出
格式化输出主要用于控制输出数据的格式,使得输出结果更加整齐和易于理解。最常用的格式化输出函数是 printf
和 fprintf
。其中,printf
用于向标准输出(通常是屏幕)打印格式化的字符串,而 fprintf
可以将格式化的字符串输出到指定的文件流中。
格式化输出的基本语法
格式化输出函数的基本语法如下:
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
format
:是一个格式字符串,包含普通字符和格式说明符。...
:代表一个或多个额外的参数,这些参数与格式说明符一一对应。
常见的格式说明符
格式说明符以百分号 %
开头,并跟随一个或多个字符来指定如何格式化相应的参数。以下是一些常见的格式说明符及其用法:
类型 | 说明符 | 示例 |
---|---|---|
整数 | %d 或 %i | 十进制整数 (int ) |
%u | 无符号十进制整数 (unsigned int ) | |
%o | 八进制整数 | |
%x 或 %X | 小写/大写的十六进制整数 | |
浮点数 | %f | 浮点数 (float , double ) |
%e 或 %E | 科学计数法表示的浮点数 | |
%g 或 %G | 根据值自动选择 %f 或 %e /%E | |
字符 | %c | 单个字符 (char ) |
字符串 | %s | 字符串 (const char* ) |
指针 | %p | 指针地址 |
百分号 | %% | 输出百分号本身 |
格式修饰符
除了基本的格式说明符外,还可以使用修饰符来进一步控制输出格式:
- 宽度:指定输出字段的最小宽度。如果实际内容短于指定宽度,则会用空格填充。可以在宽度前加
0
来用零填充。 - 精度:对于浮点数和字符串,可以指定小数点后的位数或最大长度。
- 标志:如
-
表示左对齐,+
强制显示正负号,空格(
示例
printf("%10d\n", 42); // 输出右对齐的整数,占10个字符宽
printf("%05d\n", 25); // 输出左补零的整数,占5个字符宽
printf("%.2f\n", 3.14159); // 输出保留两位小数的浮点数
printf("%-10s\n", "hello"); // 输出左对齐的字符串,占10个字符宽
格式化输出示例
下面是一些具体的例子,展示了如何使用 printf
进行格式化输出:
#include <stdio.h>int main() {int age = 25;double gpa = 3.876;char grade = 'A';// 简单输出printf("Age: %d\n", age);printf("GPA: %.2f\n", gpa);printf("Grade: %c\n", grade);// 使用宽度和精度printf("Formatted Age: %5d\n", age); // 右对齐,宽度为5printf("Formatted GPA: %6.2f\n", gpa); // 右对齐,宽度为6,精度为2printf("Formatted Grade: %3c\n", grade); // 右对齐,宽度为3// 使用标志printf("Signed Age: %+d\n", age); // 强制显示正负号printf("Left-aligned GPA: %-6.2f\n", gpa); // 左对齐,宽度为6,精度为2// 组合使用printf("Student Info: Age=%03d, GPA=%.1f, Grade=%c\n", age, gpa, grade);return 0;
}
文件输出
要将格式化的输出发送到文件而不是标准输出,可以使用 fprintf
函数。以下是 fprintf
的一个简单例子:
#include <stdio.h>int main() {FILE *file = fopen("output.txt", "w");if (file == NULL) {perror("Error opening file");return 1;}int age = 25;double gpa = 3.876;char grade = 'A';fprintf(file, "Age: %d\n", age);fprintf(file, "GPA: %.2f\n", gpa);fprintf(file, "Grade: %c\n", grade);fclose(file);return 0;
}
4.7 ASCII
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是一种字符编码标准,最初设计用于在计算机和通信设备之间传输文本。ASCII码为每个英文字母、数字、标点符号以及一些控制字符分配了一个唯一的7位二进制数。
ASCII 码的基本概念
- 7位编码:标准ASCII码使用7位来表示字符,这意味着它可以表示 (2^7 = 128) 种不同的字符。
- 扩展ASCII:为了支持更多的字符(如国际字符),出现了各种扩展ASCII码,它们使用8位,可以表示 (2^8 = 256) 种字符。
标准ASCII码表(0-127)
扩展ASCII码(128-255)
扩展ASCII码增加了额外的字符,包括非英语字符和其他符号。不同系统可能有不同的扩展ASCII实现。
ASCII 码的应用
-
字符串处理:在编程中,ASCII码常用于字符串处理和字符比较。例如,在C语言中,字符实际上是用其对应的ASCII码值来表示的。
-
文件格式:许多简单的文本文件格式(如
.txt
文件)都是基于ASCII码的,确保了跨平台的兼容性。 -
网络协议:早期的网络协议(如HTTP、SMTP等)也广泛使用ASCII码进行数据传输。
C语言中的ASCII码
在C语言中,字符和整数可以互换使用,因为字符实际上是用其对应的ASCII码值来表示的。例如:
#include <stdio.h>int main() {char ch = 'A'; // 定义一个字符变量printf("Character: %c, ASCII Value: %d\n", ch, ch); // 输出字符及其ASCII码值int asciiValue = 65; // 直接使用ASCII码值printf("ASCII Value: %d, Character: %c\n", asciiValue, asciiValue); // 输出ASCII码值及其对应的字符return 0;
}
4.8 转义序列
转义序列(escape sequences)用于表示那些不能直接通过键盘输入或不容易用普通字符表示的特殊字符。这些序列以反斜杠 \
开头,后面跟着一个或多个字符,用来指示编译器将它们解释为特殊的控制字符或非打印字符。
常见的转义序列
序列 | 说明 | ASCII 码 (十进制) |
---|---|---|
\a | 警报(响铃) | 7 |
\b | 退格符(删除前一个字符) | 8 |
\f | 换页符 | 12 |
\n | 换行符(newline) | 10 |
\r | 回车符(将光标移回到当前行的开头) | 13 |
\t | 水平制表符(tab) | 9 |
\v | 垂直制表符 | 11 |
\\ | 反斜杠字符 \ | 92 |
\' | 单引号 ' | 39 |
\" | 双引号 " | 34 |
\? | 问号 ? | 63 |
八进制和十六进制转义序列
除了上述标准转义序列外,C语言还支持使用八进制和十六进制值来表示任意字符。
- 八进制:以
\
开头,后跟1到3位八进制数字(0-7)。例如,\101
表示字母A
(ASCII码为65)。 - 十六进制:以
\x
开头,后跟任意数量的十六进制数字(0-9, a-f, A-F)。例如,\x41
同样表示字母A
。
示例
char ch1 = '\101'; // 使用八进制表示 'A'
char ch2 = '\x41'; // 使用十六进制表示 'A'
Unicode 转义序列(宽字符)
对于多字节字符集或Unicode字符,可以使用以下格式:
- 通用字符名:如
\u
后跟四位十六进制数,或\U
后跟八位十六进制数。例如,\u00A9
表示版权符号 ©。
示例
wchar_t wc = L'\u00A9'; // 定义宽字符版权符号
使用示例
以下是一些如何在代码中使用转义序列的例子:
#include <stdio.h>int main() {printf("Hello\tWorld!\n"); // 输出 "Hello" 和 "World!" 之间有一个制表符,并换行printf("Backspace\bexample\n"); // 输出 "Backspaceexample",因为 \b 删除了前一个空格printf("Path: C:\\Windows\\System32\n"); // 输出路径字符串,其中 \\ 表示单个反斜杠printf("Quote: \"Hello, World!\"\n"); // 输出带双引号的字符串printf("Alert: \a\n"); // 发出警报声(可能在某些终端不起作用)// 使用八进制和十六进制转义序列printf("Octal: %c\n", '\101'); // 输出 'A'printf("Hexadecimal: %c\n", '\x41'); // 输出 'A'// 使用 Unicode 转义序列wprintf(L"Copyright symbol: %lc\n", L'\u00A9');return 0;
}
注意事项
- 转义序列的作用范围:转义序列通常只在一个字符常量或字符串字面量内有效。如果需要在其他上下文中使用这些特殊字符,则需要使用相应的函数或方法。
- 跨平台兼容性:虽然大多数转义序列在不同平台上都有一致的行为,但某些特定于操作系统的控制字符(如
\r\n
在 Windows 中表示换行)可能会有所不同。编写跨平台代码时应尽量使用标准转义序列。 - 避免混淆:确保正确使用转义序列,尤其是在处理文件路径、URL 或其他包含反斜杠的字符串时,以免引起意外的解析错误。
4.9 sizeof
sizeof
是一个运算符,用于查询数据类型、变量或表达式在内存中所占的字节数。它可以了解不同类型的数据占用多少空间,这对于优化内存使用和理解程序行为非常重要。
语法
sizeof(type)
sizeof(variable)
sizeof(expression)
type
:可以是任何有效的C数据类型,如int
、float
、char
等。variable
:可以是任何已声明的变量。expression
:可以是任意合法的C表达式。
返回值
sizeof
返回一个 size_t
类型的值,表示对象或类型的大小(以字节为单位)。size_t
是一种无符号整数类型,通常定义在 <stddef.h>
头文件中。
示例
查询基本数据类型的大小
#include <stdio.h>int main() {printf("Size of char: %zu bytes\n", sizeof(char));printf("Size of int: %zu bytes\n", sizeof(int));printf("Size of float: %zu bytes\n", sizeof(float));printf("Size of double: %zu bytes\n", sizeof(double));printf("Size of long: %zu bytes\n", sizeof(long));printf("Size of long long: %zu bytes\n", sizeof(long long));return 0;
}
输出结果可能会因平台不同而有所差异,但典型的输出如下:
Size of char: 1 bytes
Size of int: 4 bytes
Size of float: 4 bytes
Size of double: 8 bytes
Size of long: 8 bytes
Size of long long: 8 bytes
查询数组的大小
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};printf("Size of array arr: %zu bytes\n", sizeof(arr)); // 整个数组的大小printf("Size of single element in arr: %zu bytes\n", sizeof(arr[0])); // 单个元素的大小return 0;
}
查询指针的大小
#include <stdio.h>int main() {int *ptr;printf("Size of pointer ptr: %zu bytes\n", sizeof(ptr)); // 指针的大小return 0;
}
查询结构体的大小
#include <stdio.h>struct Point {int x;int y;
};int main() {struct Point p = {10, 20};printf("Size of struct Point: %zu bytes\n", sizeof(struct Point));printf("Size of variable p: %zu bytes\n", sizeof(p));return 0;
}
结合表达式使用
#include <stdio.h>int main() {int a = 10;int b = 20;printf("Size of expression (a + b): %zu bytes\n", sizeof(a + b)); // 表达式的类型决定大小return 0;
}
特点和注意事项
-
编译时计算:
sizeof
是在编译时计算的,因此它的结果是一个常量表达式,可以在编译期间确定。 -
不适用于动态分配的内存:
sizeof
不能用于确定通过malloc
或calloc
动态分配的内存块的大小,因为这些函数返回的是指针,而sizeof
只会给出指针本身的大小,而不是指向的内存区域的大小。 -
结构体填充(Padding):为了保证结构体成员对齐,编译器可能会在结构体成员之间添加填充字节。因此,结构体的实际大小可能大于其所有成员大小之和。
-
空结构体:根据 C 标准,空结构体(即没有成员的结构体)的大小是未定义的。某些编译器可能会将空结构体的大小设为1字节,以便它可以有唯一的地址。
-
联合体(Union):联合体的大小等于其最大成员的大小,因为所有成员共享同一块内存。
-
格式化输出:当使用
printf
输出sizeof
的结果时,推荐使用%zu
格式说明符,因为它与size_t
类型相匹配。
4.10 数据类型
数据类型用于定义变量可以存储的数据种类。每种数据类型都有特定的内存大小和可以进行的操作。C语言提供了多种基本数据类型以及一些复合数据类型。
1. 基本数据类型
整型(Integer Types)
char
:通常用于表示字符,但实际上是一个小整数类型。它占用1个字节。short
:短整型,占用2个字节。int
:整型,默认情况下占用4个字节(取决于平台)。long
:长整型,占用4或8个字节(取决于平台)。long long
:长长整型,占用8个字节。
类型 | 说明 | 典型大小 (字节) | 取值范围 |
---|---|---|---|
char | 字符/小整数 | 1 | -128 到 127 或 0 到 255 |
short | 短整型 | 2 | -32,768 到 32,767 |
int | 整型 | 4 | -2,147,483,648 到 2,147,483,647 |
long | 长整型 | 4 或 8 | 根据平台 |
long long | 长长整型 | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
浮点型(Floating-point Types)
float
:单精度浮点数,占用4个字节。double
:双精度浮点数,占用8个字节。long double
:扩展精度浮点数,占用8或16个字节(取决于平台)。
类型 | 说明 | 典型大小 (字节) | 取值范围 |
---|---|---|---|
float | 单精度浮点数 | 4 | ±1.18×10^-38 到 ±3.4×10^38 |
double | 双精度浮点数 | 8 | ±2.23×10^-308 到 ±1.80×10^308 |
long double | 扩展精度浮点数 | 8 或 16 | 根据平台 |
布尔型(Boolean Type)
从 C99 开始引入布尔类型 _Bool
,通常通过 <stdbool.h>
头文件中的宏定义为 bool
,其取值为 true
和 false
。
#include <stdbool.h>
bool isTrue = true;
枚举类型(Enumeration Type)
枚举类型是一组命名的整数值,常用于定义一组相关的符号常量。
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
2. 派生数据类型
指针(Pointer Types)
指针是存储内存地址的变量。它们可以指向任何数据类型,并且可以通过解引用操作访问或修改所指向的值。
int *ptr; // 指向 int 类型的指针
数组(Array Types)
数组是一系列相同类型的元素的集合,所有元素都存储在连续的内存位置。
int numbers[5]; // 包含5个整数的数组
结构体(Structure Types)
结构体是一种用户自定义的数据类型,允许将不同类型的数据组合在一起。
struct Point {int x;int y;
};
联合体(Union Types)
联合体也是一种用户自定义的数据类型,但与结构体不同的是,联合体的所有成员共享同一块内存区域。
union Data {int i;float f;char str[20];
};
3. 空类型(Void Type)
void
类型表示没有可用的值。它主要用于声明函数返回类型为无返回值的函数,或者用于指针类型来表示通用指针。
void func(void); // 不接受参数也不返回值的函数
void *ptr; // 指向任意类型的指针
数据类型的修饰符
为了进一步指定数据类型的特性,C语言提供了一些修饰符:
signed
和unsigned
:分别表示有符号和无符号类型。例如,unsigned int
表示一个非负整数。short
和long
:用于调整整数类型的大小。例如,long int
表示长整型。long long
:用于表示更长的整数类型。
4.11 输入输出函数
输入输出(I/O)操作是通过标准库函数来实现的。这些函数提供了从终端、文件或其他数据源读取和写入数据的能力。最常用的输入输出函数包括格式化输入输出函数(如 printf
和 scanf
)、非格式化输入输出函数(如 puts
和 gets
),以及其他一些用于特定目的的函数。
1. 格式化输出函数
printf
- 用途:向标准输出(通常是终端)打印格式化的字符串。
- 原型:
int printf(const char *format, ...);
- 返回值:成功时返回输出字符的数量;如果发生错误,则返回一个负数。
示例
#include <stdio.h>int main() {int age = 25;double gpa = 3.876;char grade = 'A';printf("Age: %d\n", age);printf("GPA: %.2f\n", gpa);printf("Grade: %c\n", grade);return 0;
}
fprintf
- 用途:将格式化的字符串输出到指定的文件流中。
- 原型:
int fprintf(FILE *stream, const char *format, ...);
- 返回值:同
printf
。
示例
#include <stdio.h>int main() {FILE *file = fopen("output.txt", "w");if (file == NULL) {perror("Error opening file");return 1;}int age = 25;double gpa = 3.876;char grade = 'A';fprintf(file, "Age: %d\n", age);fprintf(file, "GPA: %.2f\n", gpa);fprintf(file, "Grade: %c\n", grade);fclose(file);return 0;
}
2. 格式化输入函数
scanf
- 用途:从标准输入(通常是键盘)读取格式化的输入。
- 原型:
int scanf(const char *format, ...);
- 返回值:成功时返回成功读取并赋值的项目数量;如果到达文件结束或遇到匹配失败,则返回
EOF
或小于预期的数值。
示例
#include <stdio.h>int main() {int age;double gpa;char grade;printf("Enter your age, GPA, and grade: ");scanf("%d %lf %c", &age, &gpa, &grade);printf("You entered: Age=%d, GPA=%.2f, Grade=%c\n", age, gpa, grade);return 0;
}
fscanf
- 用途:从指定的文件流中读取格式化的输入。
- 原型:
int fscanf(FILE *stream, const char *format, ...);
- 返回值:同
scanf
。
示例
#include <stdio.h>int main() {FILE *file = fopen("input.txt", "r");if (file == NULL) {perror("Error opening file");return 1;}int age;double gpa;char grade;fscanf(file, "%d %lf %c", &age, &gpa, &grade);printf("Read from file: Age=%d, GPA=%.2f, Grade=%c\n", age, gpa, grade);fclose(file);return 0;
}
3. 非格式化输入输出函数
puts
- 用途:向标准输出写入字符串,并自动添加换行符。
- 原型:
int puts(const char *s);
- 返回值:成功时返回非负值;如果发生错误,则返回
EOF
。
示例
#include <stdio.h>int main() {puts("Hello, World!");return 0;
}
fgets
- 用途:从指定的文件流中读取一行字符,直到遇到换行符、文件结束符或达到指定的最大字符数(包括终止空字符
\0
)。 - 原型:
char *fgets(char *str, int n, FILE *stream);
- 返回值:成功时返回指向字符串的指针;如果到达文件结束或发生错误,则返回
NULL
。
示例
#include <stdio.h>int main() {char line[100];printf("Enter a line of text: ");fgets(line, sizeof(line), stdin);printf("You entered: %s", line);return 0;
}
getchar
- 用途:从标准输入读取单个字符。
- 原型:
int getchar(void);
- 返回值:读取的字符作为无符号字符转换为
int
类型;如果到达文件结束或发生错误,则返回EOF
。
putchar
- 用途:向标准输出写入单个字符。
- 原型:
int putchar(int c);
- 返回值:写入的字符作为无符号字符转换为
int
类型;如果发生错误,则返回EOF
。
示例
#include <stdio.h>int main() {int ch;printf("Enter a character: ");ch = getchar();printf("You entered: ");putchar(ch);putchar('\n');return 0;
}
4. 文件输入输出函数
除了标准输入输出外,C语言还提供了用于文件处理的函数,例如:
fopen
:打开文件。fclose
:关闭文件。fread
和fwrite
:二进制文件读写。feof
:检查文件结束符。
示例:使用 fopen
和 fclose
#include <stdio.h>int main() {FILE *file = fopen("example.txt", "r");if (file == NULL) {perror("Error opening file");return 1;}// 进行文件操作...fclose(file);return 0;
}
注意事项
-
格式化输入的安全性:当使用
scanf
系列函数时,应小心处理输入数据,以防止缓冲区溢出等问题。例如,使用宽度限制来限制字符串的最大长度。char name[50]; scanf("%49s", name); // 限制最大读取长度为49个字符
-
刷新输出缓冲区:有时需要确保输出立即显示,特别是在等待用户输入之前。可以使用
fflush(stdout)
来强制刷新输出缓冲区。printf("Press Enter to continue..."); fflush(stdout); // 强制刷新输出缓冲区 getchar(); // 等待用户输入
-
避免使用不安全的函数:
gets
函数由于存在缓冲区溢出的风险已被弃用,建议使用fgets
替代。
4.12 运算符
运算符用于执行各种操作,如算术、比较、逻辑判断等。根据功能和用途的不同,C语言中的运算符可以分为多个类别。
1. 算术运算符
用于执行基本的数学运算。
运算符 | 描述 | 示例 |
---|---|---|
+ | 加法 | a + b |
- | 减法 | a - b |
* | 乘法 | a * b |
/ | 除法 | a / b |
% | 取模(求余) | a % b |
++ | 自增(前/后置) | ++a 或 a++ |
-- | 自减(前/后置) | --a 或 a-- |
示例
#include <stdio.h>int main() {int a = 10, b = 3;printf("a + b = %d\n", a + b);printf("a - b = %d\n", a - b);printf("a * b = %d\n", a * b);printf("a / b = %d\n", a / b); // 整数除法printf("a %% b = %d\n", a % b); // 求余return 0;
}
2. 关系运算符
用于比较两个值之间的关系。
运算符 | 描述 | 示例 |
---|---|---|
== | 等于 | a == b |
!= | 不等于 | a != b |
> | 大于 | a > b |
< | 小于 | a < b |
>= | 大于或等于 | a >= b |
<= | 小于或等于 | a <= b |
示例
#include <stdio.h>int main() {int a = 10, b = 5;printf("Is a equal to b? %s\n", (a == b) ? "Yes" : "No");printf("Is a greater than b? %s\n", (a > b) ? "Yes" : "No");return 0;
}
3. 逻辑运算符
用于组合多个条件表达式。
运算符 | 描述 | 示例 |
---|---|---|
&& | 逻辑与 | a && b |
` | ` | |
! | 逻辑非 | !a |
示例
#include <stdio.h>int main() {int a = 10, b = 5, c = 8;if (a > b && a > c) {printf("a is the largest.\n");}if (!(a < b)) {printf("a is not less than b.\n");}return 0;
}
4. 位运算符
用于直接对整数的二进制表示进行操作。
运算符 | 描述 | 示例 |
---|---|---|
& | 按位与 | a & b |
` | ` | 按位或 |
^ | 按位异或 | a ^ b |
~ | 按位取反 | ~a |
<< | 左移 | a << n |
>> | 右移 | a >> n |
示例
#include <stdio.h>int main() {unsigned int a = 60; // 0011 1100unsigned int b = 13; // 0000 1101printf("a & b = %u\n", a & b); // 0000 1100 = 12printf("a | b = %u\n", a | b); // 0011 1101 = 61printf("a ^ b = %u\n", a ^ b); // 0011 0001 = 49printf("~a = %u\n", ~a); // 1100 0011 = 255 - 60 = 195 (带符号时为负数)printf("a << 2 = %u\n", a << 2); // 1111 0000 = 240printf("a >> 2 = %u\n", a >> 2); // 0000 1111 = 15return 0;
}
5. 赋值运算符
用于将值赋给变量。
运算符 | 描述 | 示例 |
---|---|---|
= | 简单赋值 | a = b |
+= | 加法赋值 | a += b |
-= | 减法赋值 | a -= b |
*= | 乘法赋值 | a *= b |
/= | 除法赋值 | a /= b |
%= | 取模赋值 | a %= b |
<<= | 左移赋值 | a <<= b |
>>= | 右移赋值 | a >>= b |
&= | 按位与赋值 | a &= b |
^= | 按位异或赋值 | a ^= b |
` | =` | 按位或赋值 |
示例
#include <stdio.h>int main() {int a = 10;a += 5; // 相当于 a = a + 5;printf("a after addition assignment: %d\n", a);a -= 3; // 相当于 a = a - 3;printf("a after subtraction assignment: %d\n", a);return 0;
}
6. 条件运算符
也称为三元运算符,提供了一种简短的if-else语句形式。
运算符 | 描述 | 示例 |
---|---|---|
?: | 如果条件为真则返回第一个表达式的值,否则返回第二个表达式的值 | condition ? expr1 : expr2 |
示例
#include <stdio.h>int main() {int a = 10, b = 5;int max = (a > b) ? a : b;printf("The maximum value is %d\n", max);return 0;
}
7. 其他运算符
-
逗号运算符(
,
):允许在一个表达式中顺序执行多个表达式,并返回最后一个表达式的值。int a = (b = 3, c = 5, b + c);
-
sizeof运算符:用于查询数据类型、变量或表达式在内存中所占的字节数。
size_t size = sizeof(int);
-
指针运算符(
*
和&
):*
用于解引用指针,&
用于获取变量的地址。int a = 10; int *ptr = &a; // 获取 a 的地址 printf("Value of a: %d\n", *ptr); // 解引用 ptr 获取 a 的值
8. 运算符优先级表
优先级 | 运算符 | 结合性 | 类别 |
---|---|---|---|
1 | () 函数调用[] 数组下标-> 指向结构体成员. 结构体成员访问 | 左到右 | 后缀运算符 |
2 | ++ 自增-- 自减(type) 类型转换* 指针解引用& 取地址sizeof _Alignof | 右到左 | 前缀运算符 |
3 | ! 逻辑非~ 按位取反+ 正号- 负号* 指针类型乘法/ 除法% 取模 | 右到左 | 单目运算符 |
4 | << 左移>> 右移 | 左到右 | 移位运算符 |
5 | < 小于<= 小于等于> 大于>= 大于等于 | 左到右 | 关系运算符 |
6 | == 等于!= 不等于 | 左到右 | 相等运算符 |
7 | & 按位与 | 左到右 | 按位运算符 |
8 | ^ 按位异或 | 左到右 | 按位运算符 |
9 | ` | ` 按位或 | 左到右 |
10 | && 逻辑与 | 左到右 | 逻辑运算符 |
11 | ` | ` 逻辑或 | |
12 | ?: 条件运算符 | 右到左 | 条件运算符 |
13 | = 赋值+= 加法赋值-= 减法赋值*= 乘法赋值/= 除法赋值%= 取模赋值<<= 左移赋值>>= 右移赋值&= 按位与赋值^= 按位异或赋值` | =` 按位或赋值 | 右到左 |
14 | , 逗号运算符 | 左到右 | 逗号运算符 |
示例
考虑以下表达式:
int a = 5, b = 3, c = 2;
int result = a + b * c;
根据上述优先级表,*
的优先级高于 +
,因此首先计算 b * c
,然后再将结果加上 a
。
result = 5 + (3 * 2); // result = 5 + 6 = 11
结合性
结合性指的是同优先级运算符之间的求值顺序。大多数二元运算符都是左结合的,意味着它们从左到右求值;而一元运算符、赋值运算符和条件运算符是右结合的,意味着它们从右到左求值。
左结合示例
int a = 5 - 3 - 2; // 先计算 5 - 3 = 2,再计算 2 - 2 = 0
右结合示例
int a = 5;
a = b = 3; // 先计算 b = 3,然后 a = 3
使用括号控制求值顺序
虽然了解运算符优先级很重要,但在编写代码时使用括号来明确指定求值顺序可以提高代码的可读性和维护性。例如:
int result = (a + b) * c; // 明确表示先加后乘
9. 短路运算
短路运算是指在评估逻辑表达式时,如果可以确定整个表达式的值而无需评估所有操作数,则停止进一步的评估。逻辑运算符 &&
(逻辑与)和 ||
(逻辑或)支持短路运算。
逻辑与 (&&
) 的短路行为
对于逻辑与运算符 &&
,如果第一个操作数为假(即0),则整个表达式必定为假,因此不会评估第二个操作数。例如:
if (ptr != NULL && ptr->value > 10) {// 只有当 ptr 不是空指针时,才会检查 ptr->value 是否大于 10
}
在这个例子中,如果 ptr
是 NULL
,那么 ptr->value > 10
这部分就不会被计算,从而避免了访问无效内存的错误。
逻辑或 (||
) 的短路行为
对于逻辑或运算符 ||
,如果第一个操作数为真(即非0),则整个表达式必定为真,因此不会评估第二个操作数。例如:
if (ptr == NULL || ptr->value <= 10) {// 如果 ptr 是空指针,直接返回 true,不访问 ptr->value
}
同样地,在这个例子中,如果 ptr
是 NULL
,那么 ptr->value <= 10
这部分就不会被计算,从而避免了潜在的错误。
短路运算的优势
- 提高性能:由于不必评估所有操作数,程序可以更快地得出结果。
- 避免错误:如上所述,短路运算可以用来安全地处理可能引发错误的操作,比如访问空指针、除以零等。
- 简化代码逻辑:通过短路运算,可以将多个条件合并到一个表达式中,使代码更简洁。
示例代码
使用 &&
的短路运算
#include <stdio.h>int main() {int a = 5;int *ptr = &a;if (ptr != NULL && *ptr > 0) {printf("Value is positive\n");} else {printf("Value is not positive or pointer is NULL\n");}ptr = NULL; // 设置为 NULL 指针if (ptr != NULL && *ptr > 0) {printf("Value is positive\n");} else {printf("Value is not positive or pointer is NULL\n");}return 0;
}
使用 ||
的短路运算
#include <stdio.h>int main() {int a = 5;int *ptr = &a;if (ptr == NULL || *ptr <= 10) {printf("Pointer is NULL or value is less than or equal to 10\n");} else {printf("Value is greater than 10\n");}ptr = NULL; // 设置为 NULL 指针if (ptr == NULL || *ptr <= 10) {printf("Pointer is NULL or value is less than or equal to 10\n");} else {printf("Value is greater than 10\n");}return 0;
}
注意事项
虽然短路运算非常有用,但在使用时也需要注意以下几点:
- 副作用:如果操作数包含有副作用的表达式(如函数调用、自增/自减操作等),那么这些表达式可能不会被执行,这可能导致意外的行为。因此,应确保依赖于这些副作用的代码不会因为短路而被跳过。
- 可读性和意图明确:尽管短路运算可以使代码更紧凑,但过度依赖它可能会降低代码的可读性。始终确保逻辑清晰,并考虑是否有必要添加注释来解释复杂的短路表达式。
10. 类型转换
类型转换是指将一个表达式的值从一种类型转换为另一种类型。这种转换可以是自动的(隐式转换),也可以是由程序员显式指定的(强制转换)。
**隐式转换 **
隐式转换由编译器自动执行,无需程序员干预。它通常发生在以下几种情况下:
- **整数提升 **: 小于
int
的整数类型(如char
,short
)会自动被提升为int
或unsigned int
。 - **算术转换 **: 当不同类型的数值进行运算时,较小的类型会被转换成较大的类型以进行运算。
- 赋值转换: 在赋值操作中,右侧表达式的值会被转换为左侧变量的类型。
- 函数参数和返回值转换: 调用函数时,实际参数会根据形式参数的类型自动转换;函数返回值也会被转换为目标类型。
强制转换
强制转换是程序员通过代码明确指示的一种转换方式。它使用圆括号包裹目标类型来实现,语法如下:
(type) expression;
例如,将浮点数转换为整数:
float f = 3.14;
int i = (int)f; // 强制转换,i 的值将是 3
需要注意的是,强制转换可能会导致数据丢失或精度降低。比如,当将浮点数转换为整数时,小数部分将会被截断。同样地,当一个较大范围的类型被转换为较小范围的类型时,如果原始值超出了新类型的表示范围,结果可能是不确定的。
示例代码
这里有一些关于不同类型转换的例子:
#include <stdio.h>int main() {// 隐式转换char ch = 'A'; // 自动转换为 int 类型int num = ch + 1; // ch 被提升为 int 后与 1 相加// 强制转换float f = 3.14;int i = (int)f; // 显式转换为 int,丢弃了小数部分printf("ch + 1 = %d\n", num); // 输出:66,因为 'A' 的 ASCII 码是 65printf("f as int = %d\n", i); // 输出:3return 0;
}
在这个例子中,展示了字符到整数的隐式转换以及浮点数到整数的显式转换。请注意,在处理类型转换时要小心,以避免潜在的数据丢失或意外行为。
5. 控制结构
程序的控制结构决定了代码的执行流程。控制结构:顺序结构、选择结构(条件语句)、循环结构。
1. 顺序结构
顺序结构是最基本的程序结构,它按照代码出现的顺序依次执行每一条语句。没有跳转或分支逻辑,程序从头到尾逐行执行。
示例
#include <stdio.h>int main() {printf("Step 1\n");printf("Step 2\n");printf("Step 3\n");return 0;
}
在这个例子中,程序将按顺序输出 “Step 1”、“Step 2” 和 “Step 3”。
2. 选择结构(条件语句)
选择结构允许根据条件表达式的真假来决定执行哪一部分代码。常见的选择结构包括 if
语句和 switch
语句。
2.1 if
语句
if
语句用于基于条件表达式的值来选择性地执行代码块。可以与 else
和 else if
结合使用,以处理多个条件分支。
- 基本语法:
if (condition) {// 当 condition 为真时执行的代码块
}
- 带
else
的if
语句:
if (condition) {// 当 condition 为真时执行的代码块
} else {// 当 condition 为假时执行的代码块
}
- 多重条件 (
else if
):
if (condition1) {// 当 condition1 为真时执行的代码块
} else if (condition2) {// 当 condition2 为真时执行的代码块
} else {// 当所有条件都为假时执行的代码块
}
2.2 switch
语句
switch
语句用于基于一个表达式的值来选择多个代码块中的一个执行。它通常用于替代多个 if...else if
语句。
- 基本语法:
switch (expression) {case constant-expression:// 当 expression 等于 constant-expression 时执行的代码块break;case constant-expression:// 另一个 case 块break;// 可以有多个 case 块default:// 如果没有 case 匹配,则执行这里的代码块
}
- 注意事项:
- 每个
case
后面通常跟一个break
语句,以防止“贯穿”到下一个case
。 default
分支是可选的,但推荐使用以确保所有可能的情况都被处理。
- 每个
3. 循环结构
循环结构用于重复执行一段代码,直到满足某个终止条件。常见的循环结构包括 for
、while
和 do...while
。
3.1 for
循环
for
循环是一种预测试循环,它在每次迭代前检查条件,并且可以在初始化和更新部分指定额外的操作。
- 基本语法:
for (initialization; condition; increment/decrement) {// 循环体
}
3.2 while
循环
while
循环也是一种预测试循环,但在每次迭代前只检查一个条件。只要条件为真,循环就会继续执行。
- 基本语法:
while (condition) {// 循环体
}
3.3do...while
循环
do...while
循环是唯一的一种后测试循环,即它会先执行一次循环体,然后再检查条件。因此,即使条件一开始就不成立,循环体也会至少执行一次。
- 基本语法:
do {// 循环体
} while (condition);
4. 其他语句
为了更灵活地控制程序流,C语言还提供了一些辅助性的语句,如 break
、continue
、goto
和 return
。
break
和 continue
break
:立即退出当前最内层的循环或switch
语句。continue
:跳过当前迭代的剩余部分,直接进入下一次迭代。
goto
语句
虽然 goto
语句可以在某些情况下提供灵活性,但由于它容易导致代码变得难以理解和维护,因此一般不推荐使用。不过,在某些特定场景下(如错误处理),它还是有一定用途的。
- 基本语法:
goto label;
...
label: /* 标签 */
// 这里是目标位置
return
语句
return
语句用于从函数中返回,可以返回一个值给调用者(如果函数不是 void
类型)。对于 void
函数,return
语句可以不带参数,仅用于提前终止函数。
- 基本语法:
return value; // 对于非 void 函数
return; // 对于 void 函数
6. 数组
数组是一种数据结构,用于存储相同类型的多个元素。数组中的每个元素可以通过索引(从0开始)来访问。C语言支持一维数组、多维数组,并且可以对数组进行初始化、遍历和操作。
6.1 一维数组
1. 定义和声明
要定义一个一维数组,您需要指定元素类型和数组名,还可以指定数组的大小。
type arrayName[arraySize];
type
:数组中元素的数据类型。arrayName
:数组的名称。arraySize
:数组的大小(即元素的数量),必须是一个正整数或常量表达式。
2. 初始化
可以在声明时初始化数组:
int arr[5] = {1, 2, 3, 4, 5};
如果提供的初始值少于数组大小,则剩余元素将被初始化为0(对于数值类型)或空字符(对于字符类型)。也可以省略数组大小,编译器会根据初始化列表自动确定:
int arr[] = {1, 2, 3, 4, 5}; // 编译器计算大小为5
3. 访问元素
使用索引来访问数组中的元素,索引从0开始:
arr[0] = 10; // 修改第一个元素
printf("%d\n", arr[2]); // 打印第三个元素
6.2 多维数组
1. 二维数组
二维数组可以看作是"数组的数组",通常用于表示表格或矩阵。
type arrayName[rowSize][columnSize];
初始化
int matrix[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}
};
访问元素
matrix[0][0] = 0; // 修改第一行第一列的元素
printf("%d\n", matrix[1][2]); // 打印第二行第三列的元素
2. 三维及更高维数组
虽然不常见,但C语言也支持三维及更高维数组。它们的定义和访问方式与二维数组类似,只是维度更多。
int cube[2][3][4]; // 三维数组
cube[0][1][2] = 10;
6.3 数组的操作
1. 遍历数组
使用循环语句可以方便地遍历数组的所有元素。
使用 for
循环遍历一维数组
#include <stdio.h>int main() {int arr[] = {1, 2, 3, 4, 5};int n = sizeof(arr) / sizeof(arr[0]);for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");return 0;
}
使用嵌套 for
循环遍历二维数组
#include <stdio.h>int main() {int matrix[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};for (int i = 0; i < 3; i++) {for (int j = 0; j < 4; j++) {printf("%d ", matrix[i][j]);}printf("\n");}return 0;
}
指针与数组
数组名本质上是一个指向数组第一个元素的指针。因此,您可以使用指针算术来访问数组元素。
#include <stdio.h>int main() {int arr[] = {1, 2, 3, 4, 5};int *ptr = arr;for (int i = 0; i < 5; i++) {printf("%d ", *(ptr + i)); // 等同于 arr[i]}printf("\n");return 0;
}
6.4 动态数组
在C语言中,静态数组的大小必须在编译时确定。但是,有时我们可能需要在运行时创建大小可变的数组。这时可以使用动态内存分配函数如 malloc
和 calloc
来创建动态数组。
#include <stdio.h>
#include <stdlib.h>int main() {int n;printf("Enter number of elements: ");scanf("%d", &n);// 分配内存int *arr = (int *)malloc(n * sizeof(int));if (arr == NULL) {printf("Memory allocation failed\n");return 1;}// 初始化并打印数组for (int i = 0; i < n; i++) {arr[i] = i + 1;printf("%d ", arr[i]);}printf("\n");// 释放内存free(arr);return 0;
}
6.5 字符串与字符数组
在C语言中,字符串是以空字符 \0
结尾的字符数组。处理字符串时,通常使用标准库中的字符串处理函数(如 strlen
、strcpy
、strcat
等)。
#include <stdio.h>
#include <string.h>int main() {char str[] = "Hello, World!";printf("String length: %lu\n", strlen(str));char dest[50];strcpy(dest, str);printf("Copied string: %s\n", dest);strcat(dest, " Welcome!");printf("Concatenated string: %s\n", dest);return 0;
}
7. 字符串
在C语言中,字符串是以空字符 \0
结尾的字符数组。C语言没有内置的字符串类型,因此所有字符串操作都是通过字符数组和标准库函数来完成的。
7.1 字符串的声明与初始化
声明
要声明一个字符串(即字符数组),可以使用以下语法:
char str[size];
size
:数组的大小,必须足够容纳字符串的所有字符以及终止符\0
。
初始化
可以在声明时直接初始化字符串:
char greeting[] = "Hello, World!";
注意,这里并没有显式指定数组大小,编译器会根据初始化字符串的长度自动计算出正确的大小(包括终止符 \0
)。
也可以逐个字符地初始化:
char name[6] = {'J', 'o', 'h', 'n', '\0'};
或者使用字符串字面量初始化:
char message[] = "Welcome to C programming!";
7.2 访问字符串中的字符
字符串中的每个字符都可以通过索引访问,索引从0开始:
printf("First character: %c\n", message[0]);
message[0] = 'W'; // 修改第一个字符
7.3 字符串的输入输出
输入
可以使用 scanf
函数读取字符串,但需要注意防止缓冲区溢出:
char name[50];
printf("Enter your name: ");
scanf("%49s", name); // 限制最大读取长度为49个字符
为了更安全地读取包含空格的字符串,可以使用 fgets
函数:
char line[100];
printf("Enter a line of text: ");
fgets(line, sizeof(line), stdin);
输出
使用 printf
函数可以方便地输出字符串:
printf("Greeting: %s\n", greeting);
%s
是用于格式化输出字符串的格式说明符。
7.4 字符串处理函数
C标准库提供了许多用于字符串操作的函数,这些函数定义在 <string.h>
头文件中。下面是一些常用的字符串处理函数:
strlen
返回字符串的长度(不包括终止符 \0
)。
#include <stdio.h>
#include <string.h>int main() {char str[] = "Hello";printf("Length of string: %zu\n", strlen(str));return 0;
}
strcpy
将一个字符串复制到另一个字符串中。
char dest[50];
strcpy(dest, "Copy this string");
strncpy
安全版本的 strcpy
,允许指定最多复制的字符数,以避免缓冲区溢出。
char dest[10];
strncpy(dest, "A very long string", sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 确保字符串以 \0 结尾
strcat
将一个字符串追加到另一个字符串的末尾。
char dest[50] = "Hello, ";
strcat(dest, "World!");
strncat
安全版本的 strcat
,允许指定最多追加的字符数。
char dest[50] = "Hello, ";
strncat(dest, "a very long string", 5);
strcmp
比较两个字符串,返回值小于0表示第一个字符串小于第二个,等于0表示相等,大于0表示第一个字符串大于第二个。
char str1[] = "apple";
char str2[] = "banana";
int result = strcmp(str1, str2);
if (result < 0) {printf("str1 is less than str2\n");
} else if (result == 0) {printf("str1 equals str2\n");
} else {printf("str1 is greater than str2\n");
}
strncmp
安全版本的 strcmp
,允许指定最多比较的字符数。
int result = strncmp(str1, str2, 3);
strchr
在一个字符串中查找第一次出现的指定字符,并返回指向该字符的指针;如果未找到,则返回 NULL
。
char *p = strchr("hello world", 'w');
if (p != NULL) {printf("Found 'w' at position: %ld\n", p - "hello world");
}
strstr
在一个字符串中查找子字符串,并返回指向子字符串首字符的指针;如果未找到,则返回 NULL
。
char *p = strstr("hello world", "world");
if (p != NULL) {printf("Found substring at position: %ld\n", p - "hello world");
}
7.5 动态字符串
对于需要动态调整大小的字符串,可以使用动态内存分配函数如 malloc
和 realloc
来创建和调整字符数组的大小。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main() {char *str = (char *)malloc(10 * sizeof(char));strcpy(str, "initial");// 扩展字符串空间str = (char *)realloc(str, 20 * sizeof(char));strcat(str, " additional text");printf("Dynamic string: %s\n", str);// 释放内存free(str);return 0;
}
8. 函数
函数是程序的基本构建块,用于执行特定任务。函数可以提高代码的模块化和可重用性,并使程序更易于维护和理解。C语言中的函数可以接受参数、返回值(或不返回),并且可以通过调用栈机制实现递归调用。
8.1 函数的定义
基本语法
return_type function_name(parameter_list) {// 函数体// 可以有 return 语句返回值
}
return_type
:函数返回的数据类型。如果函数不返回任何值,则使用void
。function_name
:函数的名称,应具有描述性且遵循标识符命名规则。parameter_list
:传递给函数的参数列表。每个参数由类型和名称组成,多个参数之间用逗号分隔。如果没有参数,则可以留空或写为void
。
示例
#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;
}
8.2 函数声明与定义
1. 函数声明(原型)
函数声明告诉编译器函数的名称、返回类型和参数类型,但不包括函数体。通常放在源文件的开头或头文件中。
return_type function_name(parameter_list);
例如:
int max(int a, int b); // 函数声明
2. 函数定义
函数定义包含完整的函数体,实现了函数的实际逻辑。
int max(int a, int b) {return (a > b) ? a : b;
}
3. 参数传递
C语言支持两种参数传递方式:按值传递和按地址传递(通过指针)。
按值传递
按值传递时,函数接收的是实际参数的副本,因此对形参的修改不会影响实参。
void modifyValue(int x) {x = 10; // 修改的是副本,不影响调用者中的变量
}
按地址传递(指针)
通过传递指针,函数可以直接访问和修改调用者中的变量。
void modifyValue(int *x) {*x = 10; // 修改的是调用者中的变量
}int main() {int num = 5;modifyValue(&num);printf("Modified value: %d\n", num); // 输出 10return 0;
}
4. 返回值
函数可以返回一个值给调用者。对于非 void
类型的函数,必须使用 return
语句返回一个与声明类型相匹配的值。
int add(int a, int b) {return a + b; // 返回整数值
}
对于 void
类型的函数,return
语句可以不带参数,仅用于提前终止函数。
void greet() {printf("Hello, World!\n");return; // 提前终止函数
}
5. 函数的调用
函数调用通过函数名和括号内的实参列表来完成。实参与形参的数量和类型必须匹配。
int result = add(3, 7); // 调用 add 函数并存储返回值
6. 函数的嵌套与递归
函数嵌套
虽然C标准不允许函数内部定义另一个函数(即不能直接嵌套函数),但是可以通过函数指针或匿名函数库(如lambda表达式)间接实现类似的功能。
递归
递归是指函数直接或间接地调用自身。递归函数需要有一个或多个基准条件(base case),以防止无限递归。
int factorial(int n) {if (n == 0 || n == 1) {return 1; // 基准条件} else {return n * factorial(n - 1); // 递归调用}
}int main() {printf("Factorial of 5 is %d\n", factorial(5));return 0;
}
7. 标准库函数
C语言提供了丰富的标准库函数,涵盖了输入输出、字符串处理、数学运算等多个方面。常用的库函数包括:
printf
和scanf
:用于格式化输入输出。strlen
、strcpy
、strcat
等:用于字符串操作。malloc
、free
:用于动态内存分配。math.h
中的函数:如sin
、cos
、sqrt
等,用于数学计算。
要使用这些库函数,需包含相应的头文件,如 <stdio.h>
、<string.h>
、<stdlib.h>
和 <math.h>
。
8. 函数指针
函数指针是一个指向函数的指针变量,可以通过它调用函数。这在回调函数、函数表等场景中非常有用。
// 定义函数指针类型
typedef int (*FuncPtr)(int, int);// 使用函数指针调用函数
int add(int a, int b) { return a + b; }int main() {FuncPtr pFunc = add;int result = pFunc(3, 7);printf("Result: %d\n", result);return 0;
}
9. 指针
指针是C语言中一个非常强大且灵活的概念,它允许直接操作内存地址,从而实现高效的内存管理和数据处理。
9.1 指针的基本概念
定义
指针是一个变量,其值为另一个变量的地址(即内存位置)。通过指针可以间接访问和修改该地址中的数据。
声明
要声明一个指针,需要指定指针所指向的数据类型:
type *pointer_name;
type
:指针所指向的数据类型。*
:指针符号,表示这是一个指针变量。pointer_name
:指针变量的名称。
例如,声明一个指向整数的指针:
int *p; // p 是一个指向 int 类型数据的指针
初始化
指针可以通过取地址运算符 &
获取变量的地址并初始化:
int a = 10;
int *p = &a; // p 现在指向变量 a
9.2 访问指针指向的数据
使用解引用运算符 *
可以访问指针所指向的内存位置中的数据:
printf("Value of a: %d\n", *p); // 输出 a 的值
*p = 20; // 修改 a 的值
9.3 指针运算
指针支持算术运算,这在遍历数组或动态分配的内存块时非常有用。
指针加法与减法
增加或减少指针的值会根据指针所指向的数据类型的大小进行调整:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 或者写作 int *p = &arr[0];printf("First element: %d\n", *p);
p++; // 移动到下一个元素
printf("Second element: %d\n", *p);
指针之间的差值
两个指针相减的结果是它们之间元素的数量:
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[0];
int *p2 = &arr[4];
printf("Difference: %td\n", p2 - p1); // 输出 4
9.4 指针与数组
指针和数组紧密相关,数组名本质上是一个指向数组第一个元素的常量指针。因此,可以通过指针来访问数组元素。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;for (int i = 0; i < 5; i++) {printf("Element %d: %d\n", i, *(p + i));
}
9.5 动态内存分配
C语言提供了几个函数用于动态内存分配,如 malloc
、calloc
和 realloc
。这些函数返回一个指向新分配内存的指针。
使用 malloc
#include <stdio.h>
#include <stdlib.h>int main() {int *p = (int *)malloc(5 * sizeof(int)); // 分配5个整数的空间if (p == NULL) {printf("Memory allocation failed\n");return 1;}for (int i = 0; i < 5; i++) {p[i] = i + 1;printf("Element %d: %d\n", i, p[i]);}free(p); // 释放内存return 0;
}
使用 calloc
calloc
不仅分配内存,还会将其初始化为零:
int *p = (int *)calloc(5, sizeof(int)); // 分配并初始化5个整数的空间
使用 realloc
realloc
可以改变之前分配的内存块的大小:
p = (int *)realloc(p, 10 * sizeof(int)); // 扩展到10个整数的空间
9.6 函数参数传递
指针可以用于按地址传递参数,从而允许函数修改调用者中的变量。
void modifyValue(int *x) {*x = 10;
}int main() {int num = 5;modifyValue(&num);printf("Modified value: %d\n", num); // 输出 10return 0;
}
9.7 指针数组与多级指针
指针数组
指针数组是指每个元素都是指针的数组:
char *names[] = {"Alice", "Bob", "Charlie"};
多级指针
多级指针是指向指针的指针:
int a = 10;
int *p = &a;
int **pp = &p;printf("Value of a: %d\n", **pp); // 输出 a 的值
9.8 指针与字符串
在C语言中,字符串是以空字符 \0
结尾的字符数组。可以使用指针来操作字符串。
char str[] = "Hello";
char *p = str;while (*p != '\0') {printf("%c", *p++);
}
printf("\n");
9.9 函数指针
函数指针是一个指向函数的指针,可以通过它调用函数。
// 定义函数指针类型
typedef int (*FuncPtr)(int, int);// 使用函数指针调用函数
int add(int a, int b) { return a + b; }int main() {FuncPtr pFunc = add;int result = pFunc(3, 7);printf("Result: %d\n", result);return 0;
}
9.10 指针的安全性与注意事项
尽管指针功能强大,但不当使用可能导致程序错误甚至崩溃。以下是使用指针时的一些注意事项:
- 避免悬空指针:确保指针始终指向有效的内存地址。
- 检查内存分配是否成功:在使用
malloc
、calloc
或realloc
后,应检查返回的指针是否为NULL
。 - 及时释放内存:使用完动态分配的内存后,应及时调用
free
释放内存,防止内存泄漏。 - 避免指针越界:确保指针运算不会超出分配的内存范围。
10. 结构体
结构体(struct)是一种用户自定义的数据类型,允许将不同类型的数据组合在一起。结构体可以包含多个成员(也称为字段),这些成员可以是不同的数据类型,如整数、浮点数、字符、数组、指针,甚至是其他结构体。通过使用结构体,可以创建复杂的数据结构来表示现实世界中的实体或概念。
10.1 结构体的定义
基本语法
要定义一个结构体类型,需要使用 struct
关键字,并为结构体指定一个标签(名称)。然后,在大括号 {}
内列出结构体的各个成员及其类型。
struct struct_name {type member1;type member2;// 更多成员...
};
示例
定义一个表示学生的结构体:
struct Student {char name[50];int age;float gpa;
};
10.2 结构体变量的声明与初始化
声明结构体变量
可以在定义结构体时同时声明变量,或者在之后单独声明。
// 定义并声明
struct Student student1;// 或者先定义后声明
struct Student student2;
初始化结构体变量
结构体变量可以在声明时初始化,也可以在之后分别给每个成员赋值。
// 初始化时赋值
struct Student student1 = {"Alice", 20, 3.8};// 分别赋值
struct Student student2;
strcpy(student2.name, "Bob");
student2.age = 22;
student2.gpa = 3.6;
10.3 访问结构体成员
使用成员访问运算符 .
来访问结构体中的成员。
printf("Name: %s\n", student1.name);
printf("Age: %d\n", student1.age);
printf("GPA: %.2f\n", student1.gpa);
如果结构体变量是指针,则使用箭头运算符 ->
来访问成员。
struct Student *ptr = &student1;
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
printf("GPA: %.2f\n", ptr->gpa);
10.4 结构体数组
可以创建结构体类型的数组,以管理多个相关结构体实例。
struct Student students[3];// 初始化数组元素
students[0] = (struct Student){"Alice", 20, 3.8};
students[1] = (struct Student){"Bob", 22, 3.6};
students[2] = (struct Student){"Charlie", 21, 3.7};// 访问数组元素
for (int i = 0; i < 3; i++) {printf("Student %d: %s, Age: %d, GPA: %.2f\n", i + 1, students[i].name, students[i].age, students[i].gpa);
}
10.5 结构体作为函数参数
可以将整个结构体作为参数传递给函数,也可以传递指向结构体的指针。传递指针可以避免复制整个结构体,提高效率。
void printStudent(struct Student s) {printf("Name: %s, Age: %d, GPA: %.2f\n", s.name, s.age, s.gpa);
}void updateGPA(struct Student *s, float newGPA) {s->gpa = newGPA;
}int main() {struct Student student = {"Alice", 20, 3.8};printStudent(student); // 按值传递updateGPA(&student, 4.0); // 按地址传递printStudent(student);return 0;
}
10.6 嵌套结构体
结构体可以包含其他结构体作为成员,形成嵌套结构。
struct Address {char street[50];char city[50];char state[50];int zip;
};struct Person {char name[50];int age;struct Address addr; // 嵌套结构体
};int main() {struct Person person = {"Alice", 20, {"123 Main St", "Wonderland", "AL", 12345}};printf("Person: %s, Age: %d, Address: %s, %s, %s %d\n",person.name, person.age, person.addr.street, person.addr.city, person.addr.state, person.addr.zip);return 0;
}
10.7 动态分配结构体
可以使用动态内存分配函数(如 malloc
和 calloc
)来创建结构体实例,并根据需要调整大小。
struct Student *pStudent = (struct Student *)malloc(sizeof(struct Student));if (pStudent != NULL) {strcpy(pStudent->name, "John");pStudent->age = 21;pStudent->gpa = 3.9;printf("Name: %s, Age: %d, GPA: %.2f\n", pStudent->name, pStudent->age, pStudent->gpa);free(pStudent); // 释放内存
} else {printf("Memory allocation failed\n");
}
10.8 匿名结构体与typedef
为了简化代码,可以使用 typedef
创建结构体类型的别名,甚至可以定义匿名结构体。
typedef struct {char name[50];int age;float gpa;
} Student;// 使用 typedef 定义的类型
Student student = {"Alice", 20, 3.8};
11. 联合体
联合体(union)是C语言中的一种用户自定义数据类型,类似于结构体(struct),但它与结构体有一个关键的区别:联合体的所有成员共享同一块内存。这意味着在任何给定时间,联合体只能存储其中一个成员的数据,而不能同时存储多个成员的数据。联合体的大小等于其最大成员的大小加上可能的填充字节。
11.1 联合体的基本概念
定义联合体
使用 union
关键字来定义联合体。联合体可以包含不同类型的数据成员,但这些成员共享相同的内存空间。
union union_name {type member1;type member2;// 更多成员...
};
示例
定义一个表示不同类型数值的联合体:
union NumericValue {int intValue;float floatValue;double doubleValue;
};
11.2 联合体变量的声明与初始化
声明联合体变量
可以在定义联合体时同时声明变量,或者在之后单独声明。
// 定义并声明
union NumericValue value;// 或者先定义后声明
union NumericValue value2;
初始化联合体变量
联合体变量可以在声明时初始化,但只能初始化第一个成员。
// 初始化第一个成员
union NumericValue value = {10}; // 等同于 .intValue = 10
11.3 访问联合体成员
使用成员访问运算符 .
来访问联合体中的成员。需要注意的是,由于所有成员共享同一块内存,访问非最近赋值的成员会导致未定义行为。
value.intValue = 10;
printf("Integer Value: %d\n", value.intValue);value.floatValue = 3.14f;
printf("Float Value: %.2f\n", value.floatValue);
11.4 联合体的内存布局
联合体的总大小等于其最大成员的大小加上可能的填充字节。每个成员都从联合体的起始地址开始,因此它们重叠存储在同一位置。
#include <stdio.h>union ExampleUnion {char ch;int num;double dbl;
};int main() {printf("Size of union: %zu bytes\n", sizeof(union ExampleUnion));return 0;
}
在这个例子中,union ExampleUnion
的大小将取决于 double
类型的大小(通常是8个字节),因为它是三个成员中最大的。
11.5 联合体与结构体的比较
- 结构体:每个成员都有自己独立的内存空间,可以同时存储多个成员的数据。
- 联合体:所有成员共享同一块内存,只能存储其中一个成员的数据。
11.6 匿名联合体与typedef
为了简化代码,可以使用 typedef
创建联合体类型的别名,甚至可以定义匿名联合体。
typedef union {int intValue;float floatValue;double doubleValue;
} NumericValue;// 使用 typedef 定义的类型
NumericValue value = {10};
11.7 联合体的应用场景
联合体主要用于以下几种情况:
- 节省内存:当需要在不同的时间点存储不同类型的数据时,联合体可以有效地节省内存。
- 实现变长数据结构:例如,在网络协议或文件格式中,某些字段的长度可能会根据前一个字段的内容变化。
- 处理不同类型的输入/输出:比如,函数参数或返回值可能是多种类型之一,此时可以使用联合体来表示。
11.8 注意事项
- 未定义行为:访问非最近赋值的成员会导致未定义行为。应始终确保只访问最近赋值的那个成员。
- 类型安全:由于联合体缺乏类型检查,容易导致错误。可以通过编程习惯和注释来避免误用。
- 对齐问题:不同平台和编译器可能会对联合体成员进行不同的对齐处理,这可能导致跨平台兼容性问题。
11.9 结合枚举使用联合体
有时,为了跟踪当前存储的是哪种类型的数据,可以结合枚举和联合体一起使用。这样可以在程序中明确地知道联合体中存储了什么类型的数据。
#include <stdio.h>enum ValueType {INT_TYPE,FLOAT_TYPE,DOUBLE_TYPE
};union Value {int intValue;float floatValue;double doubleValue;
};struct TypedValue {enum ValueType type;union Value value;
};void printTypedValue(struct TypedValue *tv) {switch (tv->type) {case INT_TYPE:printf("Integer Value: %d\n", tv->value.intValue);break;case FLOAT_TYPE:printf("Float Value: %.2f\n", tv->value.floatValue);break;case DOUBLE_TYPE:printf("Double Value: %.2f\n", tv->value.doubleValue);break;}
}int main() {struct TypedValue tv;tv.type = INT_TYPE;tv.value.intValue = 10;printTypedValue(&tv);tv.type = FLOAT_TYPE;tv.value.floatValue = 3.14f;printTypedValue(&tv);return 0;
}
12. 头文件
头文件(header file)用于声明函数、宏定义、类型定义(如结构体和联合体)、全局变量等。它们通常以 .h
为扩展名,并通过 #include
预处理指令包含在源文件中。使用头文件有助于代码的模块化设计,提高代码的可读性和可维护性。
1. 头文件的作用
- 声明函数原型:使得可以在不同文件中调用这些函数。
- 定义宏:通过预处理器指令提供常量或简单的代码替换。
- 定义数据类型:例如结构体、联合体、枚举等复杂的数据类型。
- 声明外部变量:使得多个源文件可以访问同一个全局变量。
2. 创建自定义头文件
创建一个头文件非常简单,只需将需要共享的内容放入一个 .h
文件中即可。
示例:mylib.h
#ifndef MYLIB_H
#define MYLIB_H// 宏定义
#define MAX(a, b) ((a) > (b) ? (a) : (b))// 函数原型声明
int add(int a, int b);
float divide(float a, float b);// 结构体定义
struct Point {int x;int y;
};#endif // MYLIB_H
在这个例子中,我们使用了预处理指令 #ifndef
, #define
, 和 #endif
来防止头文件被多次包含,这被称为“头卫”或“防护宏”。
3. 包含头文件
要使用头文件中的内容,必须在源文件中使用 #include
指令将其包含进来。有两种方式来包含头文件:
-
标准库头文件:使用尖括号
< >
,编译器会在标准库路径中查找。#include <stdio.h>
-
用户自定义头文件:使用双引号
""
,编译器会首先在当前目录中查找,然后在标准库路径中查找。#include "mylib.h"
4. 实现与声明分离
通常情况下,函数的实现放在 .c
文件中,而函数的声明则放在对应的 .h
文件中。这样做的好处是可以隐藏实现细节,并且允许其他源文件只关心接口而不必了解内部实现。
示例:mylib.c
#include "mylib.h"int add(int a, int b) {return a + b;
}float divide(float a, float b) {if (b != 0)return a / b;elsereturn 0; // 或者返回错误码/值
}
5. 使用头文件的优点
- 代码复用:通过将常用的功能封装到头文件中,可以在多个项目中重复使用。
- 模块化编程:促进代码组织和管理,每个模块可以有自己的头文件。
- 提高编译效率:避免重复编译相同的代码,因为只有当头文件更改时才会重新编译依赖它的文件。
- 便于维护:如果需要修改接口,只需要在一个地方进行更改,即头文件中。
6. 注意事项
- 避免循环依赖:确保没有两个头文件互相包含对方,以免造成编译错误。
- 使用防护宏:如前所述,使用防护宏防止头文件被多次包含,这是编写健壮代码的好习惯。
- 合理命名:选择有意义的名字来命名头文件,以便于理解和维护。
- 文档化:为头文件添加注释和文档,说明其用途和如何使用其中的接口。
13. 文件
在C语言中,文件操作是通过标准库函数实现的,这些函数定义在 <stdio.h>
头文件中。文件操作包括打开、关闭、读取、写入和定位文件指针等基本功能。
1. 文件指针
所有文件操作都是通过文件指针(FILE *
类型)进行的。文件指针指向一个与文件关联的数据结构,该结构包含了文件的状态信息。
FILE *fp;
2. 打开文件
使用 fopen
函数可以打开文件,并返回一个文件指针。如果文件无法打开,则返回 NULL
。
基本语法
FILE *fopen(const char *filename, const char *mode);
filename
:要打开的文件名(带路径或不带路径)。mode
:指定文件的操作模式,例如:"r"
:只读方式打开文件。文件必须存在。"w"
:写入方式打开文件。如果文件存在,则会被截断为零长度;如果文件不存在,则会创建新文件。"a"
:追加方式打开文件。如果文件存在,则写入的数据将被添加到文件末尾;如果文件不存在,则会创建新文件。"rb"
,"wb"
,"ab"
:以二进制模式打开文件,分别对应于读、写和追加。"r+"
,"w+"
,"a+"
:允许读写操作。"w+"
和"a+"
模式会在文件不存在时创建新文件。
示例
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {printf("Failed to open file\n");return 1;
}
3. 关闭文件
使用 fclose
函数可以关闭文件,并释放与文件指针关联的资源。
int fclose(FILE *stream);
关闭文件时,fclose
返回 0
表示成功,返回 EOF
表示失败。
示例
fclose(fp);
4. 文件读取
读取字符
使用 fgetc
函数可以逐个字符地从文件中读取数据。
int fgetc(FILE *stream);
示例
char ch;
while ((ch = fgetc(fp)) != EOF) {putchar(ch); // 输出字符
}
读取字符串
使用 fgets
函数可以从文件中读取一行文本。
char *fgets(char *str, int n, FILE *stream);
str
:存储读取内容的缓冲区。n
:最大读取字符数(包括终止符\0
)。
示例
char line[100];
while (fgets(line, sizeof(line), fp) != NULL) {printf("%s", line);
}
读取格式化输入
使用 fscanf
函数可以从文件中读取格式化的输入,类似于 scanf
。
int fscanf(FILE *stream, const char *format, ...);
示例
int num;
fscanf(fp, "%d", &num);
printf("Read number: %d\n", num);
5. 文件写入
写入字符
使用 fputc
函数可以向文件中写入单个字符。
int fputc(int c, FILE *stream);
示例
fputc('A', fp);
写入字符串
使用 fputs
函数可以向文件中写入字符串,但不会自动添加换行符。
int fputs(const char *str, FILE *stream);
示例
fputs("Hello, World!\n", fp);
写入格式化输出
使用 fprintf
函数可以向文件中写入格式化的输出,类似于 printf
。
int fprintf(FILE *stream, const char *format, ...);
示例
fprintf(fp, "Number: %d\n", 42);
6. 文件定位
获取当前文件位置
使用 ftell
函数可以获得文件指针的当前位置。
long ftell(FILE *stream);
设置文件位置
使用 fseek
函数可以设置文件指针的位置。
int fseek(FILE *stream, long offset, int whence);
offset
:相对于whence
的偏移量。whence
:定位基准点,可以是以下值之一:SEEK_SET
:文件开头。SEEK_CUR
:当前位置。SEEK_END
:文件结尾。
示例
fseek(fp, 0, SEEK_SET); // 移动到文件开头
7. 文件检测
检查文件结束
使用 feof
函数可以检查是否到达文件末尾。
int feof(FILE *stream);
检查文件错误
使用 ferror
函数可以检查文件操作中是否发生错误。
int ferror(FILE *stream);
8. 文件复制示例
下面是一个完整的示例,演示如何复制文件内容:
#include <stdio.h>int main() {FILE *source, *destination;char ch;source = fopen("source.txt", "r");if (source == NULL) {printf("Failed to open source file\n");return 1;}destination = fopen("destination.txt", "w");if (destination == NULL) {printf("Failed to create destination file\n");fclose(source);return 1;}while ((ch = fgetc(source)) != EOF) {fputc(ch, destination);}fclose(source);fclose(destination);printf("File copied successfully\n");return 0;
}
14. 数据库
在C语言中,直接操作数据库通常不是通过内置的语言特性来实现的,而是借助于外部库或API与数据库进行交互。这些库提供了函数和数据结构,使开发者能够执行SQL查询、管理连接、处理结果集等操作。
1. 使用MySQL C API
MySQL 提供了一个C API,允许开发人员编写C代码来访问MySQL数据库。要使用这个API,你需要安装MySQL客户端库,并包含相应的头文件。
安装MySQL客户端库
根据你的操作系统,可以通过包管理器或其他方式安装MySQL客户端库。例如,在Ubuntu上:
sudo apt-get install libmysqlclient-dev
示例代码
#include <mysql/mysql.h>
#include <stdio.h>int main() {MYSQL *conn;MYSQL_RES *res;MYSQL_ROW row;char *server = "localhost";char *user = "root";char *password = "password"; /* set me first */char *database = "test";conn = mysql_init(NULL);/* Connect to database */if (!mysql_real_connect(conn, server, user, password, database, 0, NULL, 0)) {fprintf(stderr, "%s\n", mysql_error(conn));return 1;}/* Send SQL query */if (mysql_query(conn, "SELECT DATABASE()")) {fprintf(stderr, "%s\n", mysql_error(conn));return 1;}res = mysql_store_result(conn);/* Fetch and display the result */if ((row = mysql_fetch_row(res)) != NULL)printf("Current database: %s\n", row[0]);/* Clean up */mysql_free_result(res);mysql_close(conn);return 0;
}
编译时需要链接MySQL客户端库:
gcc -o myapp myapp.c `mysql_config --cflags --libs`
2. 使用PostgreSQL C API (libpq)
PostgreSQL 提供了libpq
库,用于从C程序中访问PostgreSQL数据库。
安装PostgreSQL客户端库
根据你的操作系统,可以通过包管理器或其他方式安装PostgreSQL客户端库。例如,在Ubuntu上:
sudo apt-get install libpq-dev
示例代码
#include <libpq-fe.h>
#include <stdio.h>int main() {const char *conninfo = "dbname=test user=postgres password=secret";PGconn *conn;PGresult *res;int nFields;int i, j;/* Make a connection to the database */conn = PQconnectdb(conninfo);/* Check to see that the backend connection was successfully made */if (PQstatus(conn) != CONNECTION_OK) {fprintf(stderr, "Connection to database failed: %s",PQerrorMessage(conn));PQfinish(conn);return 1;}/* Execute a query */res = PQexec(conn, "SELECT * FROM test_table");if (PQresultStatus(res) != PGRES_TUPLES_OK) {fprintf(stderr, "SELECT failed: %s", PQerrorMessage(conn));PQclear(res);PQfinish(conn);return 1;}/* Get number of fields in the result */nFields = PQnfields(res);/* Print out the results */for (i = 0; i < PQntuples(res); i++) {for (j = 0; j < nFields; j++)printf("%s\t", PQgetvalue(res, i, j));printf("\n");}/* Free the results and close the connection */PQclear(res);PQfinish(conn);return 0;
}
编译时需要链接PostgreSQL客户端库:
gcc -o pgapp pgapp.c -lpq
3. 使用SQLite
SQLite 是一个轻量级的关系型数据库管理系统,它不需要单独的服务器进程或系统配置。SQLite非常适合嵌入式应用和个人项目。
安装SQLite库
根据你的操作系统,可以通过包管理器或其他方式安装SQLite库。例如,在Ubuntu上:
sudo apt-get install libsqlite3-dev
示例代码
#include <sqlite3.h>
#include <stdio.h>static int callback(void *NotUsed, int argc, char **argv, char **azColName) {for(int i = 0; i < argc; i++) {printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");}printf("\n");return 0;
}int main(int argc, char* argv[]) {sqlite3 *db;char *zErrMsg = 0;int rc;rc = sqlite3_open("test.db", &db);if( rc ) {fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));return(1);} else {fprintf(stdout, "Opened database successfully\n");}char *sql = "CREATE TABLE COMPANY(""ID INT PRIMARY KEY NOT NULL,""NAME TEXT NOT NULL,""AGE INT NOT NULL,""ADDRESS CHAR(50),""SALARY REAL );";/* Execute SQL statement */rc = sqlite3_exec(db, sql, callback, 0, &zErrMsg);if( rc != SQLITE_OK ){fprintf(stderr, "SQL error: %s\n", zErrMsg);sqlite3_free(zErrMsg);} else {fprintf(stdout, "Table created successfully\n");}sqlite3_close(db);return 0;
}
编译时需要链接SQLite库:
gcc -o sqliteapp sqliteapp.c -lsqlite3