第一部分:C语言基础概念详解
1. C语言的起源与特点
C语言诞生于20世纪70年代初,最初用于开发UNIX操作系统。它的设计目标是使得代码更高效且能够直接操作硬件,因此C语言也被称为“低级语言的高级化”。以下是C语言的几个主要特点:
- 高效性:C语言编写的程序运行速度非常快,尤其适合底层开发,如操作系统、驱动程序等。
- 可移植性:C语言代码具有良好的跨平台移植性,只需少量修改就可以在不同平台上编译运行。
- 灵活性:C语言允许直接访问内存,可以进行精细的内存操作(如指针操作)。
- 丰富的运算符:C语言提供了丰富的运算符集,能够进行各种类型的数据运算和逻辑运算。
2. 程序的基本结构
每个C语言程序都由以下几个主要部分组成:
- 预处理指令:使用
#include来引入标准库,如stdio.h,为输入输出提供函数。 - 主函数
main():每个C程序的入口点是main()函数,程序的执行从main()开始。其返回类型通常是int,表示程序的执行状态(0通常表示成功)。
一个简单的C语言程序如下:
|
|
3. 变量和数据类型
C语言是一种静态类型语言,这意味着在使用变量之前,必须声明变量的类型。主要的数据类型有:
- 整数类型 (
int):用于存储整型数值,通常占用4字节。- 例:
int a = 10;
- 例:
- 浮点类型 (
float,double):用于存储小数,float通常占用4字节,double占用8字节。- 例:
float pi = 3.14;
- 例:
- 字符类型 (
char):用于存储单个字符,占用1字节。- 例:
char letter = 'A';
- 例:
C语言允许使用修饰符来改变基本类型的特性,如:
short:表示短整型,减少内存占用;long:表示长整型,增加数值范围;unsigned:表示无符号类型,仅存储非负数。
例如:
|
|
4. 常量
常量是指在程序运行期间其值不可改变的量。C语言通过 const关键字来声明常量。例如:
|
|
5. 基本输入输出
- 输出函数
printf():用于向屏幕输出信息,常见的占位符有:%d:输出整数;%f:输出浮点数;%c:输出字符;%s:输出字符串。
|
|
- 输入函数
scanf():用于从键盘读取输入,常见的格式符号与printf()相似,使用&符号来获取变量的地址。
|
|
6. 运算符
C语言的运算符种类繁多,主要分为以下几类:
- 算术运算符:用于基本的数学运算,包括:
+:加法;-:减法;*:乘法;/:除法(整数除法时,结果为整数);%:取模运算(只适用于整数)。
例如:
|
|
- 关系运算符:用于比较两个值,返回布尔结果(
1表示真,0表示假)。==:等于;!=:不等于;>:大于;<:小于;>=:大于等于;<=:小于等于。
例如:
|
|
- 逻辑运算符:用于处理逻辑关系。
&&:逻辑与,只有两个表达式都为真时,结果为真;||:逻辑或,任一表达式为真,结果为真;!:逻辑非,将表达式的结果取反。
例如:
|
|
7. 条件控制结构
C语言中的条件控制结构用于控制程序执行的流程,主要包括:
- if语句:用于条件判断,当条件成立时执行特定代码块。
- else语句:在
if条件不成立时执行。 - else if语句:用于多条件判断。
例如:
|
|
8. 循环控制结构
C语言提供了三种常用的循环结构:
- for循环:常用于已知循环次数的情况。
|
|
- while循环:在条件成立时重复执行代码块,适用于未知循环次数的情况。
|
|
- do…while循环:先执行一次代码块,然后在条件成立时继续循环。
|
|
9. 函数与作用域
- 函数是C语言中的重要组成部分,用于将代码逻辑封装成可重用的代码块。函数的声明和定义包括返回类型、函数名、参数列表和函数体。例如:
|
|
在调用函数时,参数会传递到函数内,函数执行完毕后将返回一个值。例如:
|
|
- 局部变量与全局变量:C语言中的变量分为局部变量和全局变量。局部变量是在函数或代码块内部声明的变量,只能在其定义的范围内使用;全局变量是在函数外部声明的,整个程序都可以访问。
第二部分:数组与字符串的详细介绍
在上一部分中,我们讲到了C语言的基本概念、控制结构和函数等内容。现在,我们将深入探讨C语言中的数组与字符串,它们是编程中用于处理多个数据的一些关键工具。
1. 数组概念
数组是存储相同数据类型元素的连续内存块。它使得我们可以用同一个变量名来处理一组值,而不是为每个值都声明一个独立的变量。数组的元素通过下标(索引)进行访问,索引从 0开始。
一维数组
- 一维数组可以看作是一个线性的数据集合。其语法为:
|
|
例如,声明一个存储5个整数的数组:
|
|
我们可以通过下标来给数组元素赋值和访问元素:
|
|
也可以使用循环来遍历数组:
|
|
数组初始化
数组可以在声明时直接初始化,也可以在运行时赋值:
- 静态初始化(在声明时指定初始值):
|
|
- 部分初始化时,未指定的元素会自动被初始化为
0:
|
|
多维数组
C语言支持多维数组,常用的有二维数组。二维数组可以看作是一个矩阵,即行列形式的数据结构。声明语法如下:
|
|
例如:
|
|
二维数组的元素可以通过两个下标进行访问:
|
|
二维数组的遍历通常使用嵌套循环:
|
|
在上述例子中,matrix[2][3]是一个2行3列的数组,遍历时通过嵌套的 for循环访问每个元素。
2. 字符串
字符串在C语言中被视为字符数组。C语言中没有专门的字符串数据类型,因此字符串通常是一个以空字符 \0 结尾的字符数组。该空字符标志着字符串的结束,且不会被显示输出。
字符串的声明与初始化
字符串可以通过字符数组来声明,并且可以直接初始化为字符串常量:
|
|
注意,这里的 str数组长度为6,因为 "Hello"有5个字符,加上末尾的空字符 \0总共6个字节。我们也可以手动初始化字符串:
|
|
字符串输入输出
可以使用 printf()输出字符串,也可以使用 scanf()读取字符串。特别地,scanf()读取字符串时,不需要写 &符号,因为字符串本质上是指针(稍后我们会详细讨论指针)。
- 输出字符串:
|
|
- 输入字符串:
|
|
不过,使用 scanf()时有一个重要问题:它只能读取空格之前的内容,遇到空格会停止输入。如果需要读取包含空格的字符串,可以使用 gets()函数(已废弃,不推荐)或 fgets()函数:
|
|
常用字符串处理函数
C语言的 string.h库提供了一系列处理字符串的函数,如:
strlen():计算字符串长度。strcpy():复制字符串。strcat():拼接字符串。strcmp():比较两个字符串。
示例:
|
|
3. 数组与字符串的常见操作
数组与指针
在C语言中,数组名实际上是一个指向数组第一个元素的指针。我们可以通过指针运算来访问数组元素:
|
|
字符串的常见问题
- 数组越界:如果字符串数组的大小不足以容纳字符串和结束符
\0,可能会导致内存越界的问题。因此在声明字符串数组时要确保有足够的空间。 - 空字符问题:字符串末尾必须有一个
\0,否则可能导致程序在输出或操作字符串时出现错误或崩溃。
4. 字符串与数组的区别
尽管字符串是以字符数组的形式存在,但处理方式上仍有细微的区别:
- 字符数组可以包含任意字符,不必以
\0结尾; - 字符串必须以
\0结尾,printf等函数遇到\0时才会停止输出。
第三部分:指针详解
在前面的部分,我们讨论了数组和字符串,并简要提到了指针的概念。现在我们将深入讨论指针,这是C语言中最强大、但也最难掌握的概念之一。指针在C语言中的作用非常重要,尤其是在处理内存、数组、函数参数和动态分配内存时。
1. 指针的基本概念
指针是存储另一个变量的地址的变量。也就是说,指针指向存储在内存中的一个特定位置,而这个位置存储了某个值。通过指针,可以间接地访问和修改存储在该位置的值。
指针声明
在C语言中,指针通过类型名和一个星号(*)声明。它的基本语法为:
|
|
data_type:指针所指向的变量的类型,例如int,float等。pointer_name:指针变量的名称。
例如:
|
|
指针的地址和解引用
- &运算符:取地址运算符,返回变量的内存地址。
- *运算符:解引用运算符,返回指针指向的变量的值。
例如:
|
|
在这个例子中,p存储了 a的地址,而 *p则访问 a的值。
2. 指针与数组的关系
在C语言中,数组名实际上就是一个指向数组第一个元素的指针。因此,指针和数组之间有非常密切的关系。
例如:
|
|
这里,p指向 arr[0],p + 1指向 arr[1],通过解引用 *(p+1)可以访问数组的第二个元素。
总结指针与数组的关系:
arr实际上是一个指向arr[0]的指针;arr[i]等价于*(arr + i);- 指针可以通过指针运算遍历数组。
3. 指针与函数
在C语言中,指针与函数有着紧密的关系。通过指针,我们可以实现以下两种主要功能:
- 函数参数传递:通过传递指针,可以使得函数内部对传递参数的修改在函数外部生效。
- 指向函数的指针:通过指针来引用和调用函数。
通过指针传递参数
C语言中函数的参数传递是值传递,也就是说,函数接收到的是参数的副本。如果希望函数修改原始参数,可以通过指针来传递参数的地址。
例如:
|
|
在这个例子中,swap函数通过指针修改了 x和 y的值,使得函数外的 x和 y被交换。
函数指针
指针不仅可以指向变量,还可以指向函数。一个函数指针可以存储某个函数的地址,并通过这个指针调用该函数。
函数指针的声明语法如下:
|
|
例如,声明一个指向返回 int类型、接受两个 int参数的函数指针:
|
|
我们可以将一个函数的地址赋给函数指针,并通过指针调用该函数:
|
|
4. 指针的分类
指针在C语言中有多个分类,每种指针类型在不同的场景下使用。以下是几种常见的指针类型:
空指针(NULL指针)
空指针是指向**“无效"地址**的指针。通常将一个指针赋值为 NULL来表明它不指向任何有效的内存地址。在实际编程中,空指针常用于指针初始化或检查指针是否有效。
|
|
使用空指针可以避免一些未初始化指针带来的错误。
悬空指针
悬空指针是指一个指针指向了被释放或不存在的内存地址。它是造成内存访问错误的常见原因。为了避免悬空指针,在释放动态分配的内存后,应该将指针设为 NULL。
|
|
野指针
野指针指的是指向未知地址或非法内存地址的指针。野指针的行为是不可预料的,可能导致程序崩溃或产生未定义的行为。在使用指针时,要确保指针指向合法的内存区域。
5. 指针与动态内存管理
C语言提供了一套标准库函数来进行动态内存分配,它们包括:
malloc():分配指定大小的内存块,返回指向该内存块的指针。如果分配失败,返回NULL。calloc():与malloc()类似,但会初始化分配的内存为0。free():释放之前用malloc()或calloc()分配的内存块。
例如:
|
|
在这个例子中,malloc函数分配了足够存储5个整数的内存空间,并通过指针 p访问和操作这些内存。
6. 指针的常见错误
- 未初始化的指针:指针在使用前必须初始化。如果未初始化就使用指针,可能会导致程序崩溃或产生不确定的行为。
- 内存泄漏:使用
malloc或calloc动态分配内存后,必须使用free释放内存,否则会导致内存泄漏。 - 解引用空指针:解引用空指针会导致程序崩溃。在解引用指针之前,必须确保指针指向有效的内存区域。
第四部分:结构体、联合体与指针的结合使用
在上一部分中,我们深入讨论了C语言中的指针。现在我们继续探讨结构体和联合体,并进一步学习它们如何与指针结合使用。在C语言中,结构体和联合体是非常重要的数据类型,常用于定义复杂的数据结构,例如链表、树等。
1. 结构体(Struct)
结构体是一种用户自定义的数据类型,它允许将不同类型的变量组合成一个单一的实体。结构体特别适合用于描述复杂的对象或记录,例如描述一名学生的学号、姓名和成绩等信息。
结构体的声明
结构体的声明格式如下:
|
|
例如,定义一个表示学生信息的结构体:
|
|
结构体变量的定义与访问
定义结构体变量时,可以使用结构体名称。例如:
|
|
访问结构体的成员时使用点运算符(.):
|
|
通过这种方式,我们可以操作和访问结构体中的各个成员。
结构体初始化
结构体变量可以在声明时初始化:
|
|
结构体数组
与基本类型数组类似,我们可以定义结构体数组来存储多个结构体变量。例如,定义一个存储多名学生信息的结构体数组:
|
|
2. 结构体与指针
指针可以与结构体结合使用,以更高效地操作结构体,尤其是在函数参数传递和动态内存分配时。
结构体指针的声明
结构体指针的声明与普通指针相同,只是类型为结构体类型。例如,声明一个指向 Student结构体的指针:
|
|
我们可以让指针 p指向某个结构体变量的地址:
|
|
通过指针访问结构体成员
通过指针访问结构体成员时,不能直接使用点运算符,而是使用箭头运算符(->)。例如:
|
|
这种方式避免了结构体的大量复制,特别是在传递结构体作为函数参数时,指针更加高效。
结构体指针与动态内存分配
结构体指针可以与 malloc()函数结合,动态分配结构体内存。例如:
|
|
分配完成后,可以像普通结构体指针一样操作和访问结构体成员,使用完成后需要记得释放内存:
|
|
3. 联合体(Union)
联合体与结构体类似,也是一种自定义的数据类型,但它允许多个成员共用同一块内存空间。联合体中的所有成员共享同一个内存地址,因此在某一时刻,联合体只能存储一个成员的值。
联合体的声明
联合体的声明格式与结构体类似:
|
|
例如:
|
|
联合体的使用
定义联合体变量时,可以像结构体一样操作。因为所有成员共享同一块内存,修改一个成员的值会覆盖其他成员的值:
|
|
联合体的大小
由于联合体的所有成员共享同一个内存,因此联合体的大小等于它所有成员中最大类型的大小。例如:
|
|
4. 结构体与联合体的比较
| 特性 | 结构体 | 联合体 |
|---|---|---|
| 内存分配 | 每个成员都有独立的内存空间 | 所有成员共享同一块内存 |
| 使用场景 | 需要同时存储多个不同类型的数据 | 需要节省内存且一次只使用一个成员 |
| 成员访问 | 可以同时访问多个成员 | 只能访问最后一次赋值的成员 |
| 大小 | 总大小等于所有成员的大小之和 | 大小等于最大成员的大小 |
5. 嵌套结构体
在C语言中,结构体可以嵌套,即一个结构体的成员可以是另一个结构体。例如:
|
|
通过这种方式,可以创建更复杂的数据结构。例如:
|
|
6. 指向结构体数组的指针
指针不仅可以指向单个结构体变量,还可以指向结构体数组。例如:
|
|
可以通过指针和下标一起操作结构体数组:
|
|
7. 位域(Bit Fields)
位域是一种特殊的结构体成员,用于控制某个成员占用的比特位数。位域用于节省内存空间,通常在需要精确控制内存分配的情况下使用,例如嵌入式系统中。
位域的声明
位域声明时,需要指定成员占用的位数:
|
|
在这个例子中,a和 b各占用4位,总共占用1个字节。
第五部分:文件操作与预处理器指令
在前面四部分的基础上,本部分将讨论文件操作和预处理器指令,它们是C语言编程中非常实用且重要的功能。文件操作使得程序可以读写外部文件,而预处理器指令则帮助程序员更灵活地管理代码和进行条件编译。
1. 文件操作
文件操作是指通过程序与外部文件进行交互,比如读取文件内容或向文件中写入数据。C语言中,文件操作主要通过标准库函数来实现,这些函数定义在 stdio.h头文件中。
文件指针
在C语言中,文件通过文件指针进行操作。FILE类型是一个结构体,表示文件流,而 FILE *则是文件指针,用来引用和操作文件。
|
|
打开和关闭文件
使用 fopen()函数来打开文件,并返回一个 FILE *指针。如果文件打开失败,fopen()会返回 NULL。其基本语法如下:
|
|
filename:文件名。mode:文件打开模式,有以下几种常见模式:"r":以只读模式打开文件,文件必须存在。"w":以写入模式打开文件,若文件不存在则创建,若文件存在则清空文件内容。"a":以追加模式打开文件,文件必须存在。"r+":以读写模式打开文件,文件必须存在。"w+":以读写模式打开文件,若文件不存在则创建,若文件存在则清空文件内容。
例如:
|
|
当操作完文件后,必须使用 fclose()函数关闭文件,以释放系统资源:
|
|
读写文件
C语言提供了多种函数用于文件的读写操作,包括单字符、字符串和格式化数据的读写。
字符读写
fgetc():从文件中读取一个字符。fputc():将一个字符写入文件。
|
|
字符串读写
fgets():从文件中读取一行字符串,最多读取n-1个字符(n是指定的大小),并在末尾添加'\0'。fputs():向文件写入一个字符串。
|
|
格式化读写
fprintf():将格式化的数据写入文件。fscanf():从文件中读取格式化的数据。
|
|
二进制文件操作
对于需要精确控制数据格式的场景,C语言提供了二进制文件的读写函数:
fread():从文件中读取二进制数据。fwrite():向文件中写入二进制数据。
|
|
文件定位
文件定位函数用于在文件中移动读写指针:
fseek():将文件指针移动到指定位置。ftell():返回文件指针的当前偏移量。rewind():将文件指针重置到文件开头。
|
|
2. 预处理器指令
预处理器指令是由**#**号开头的指令,它们在编译器编译代码之前执行预处理工作。常见的预处理器指令包括文件包含、宏定义、条件编译等。
#include:文件包含
#include指令用于将外部文件的内容包含到当前文件中。常见的有两种包含形式:
- 尖括号形式:用于包含标准库文件。
|
|
- 双引号形式:用于包含用户自定义的头文件。
|
|
#define:宏定义
宏定义用于定义常量或简单的函数。在预处理阶段,所有宏的使用都会被替换为定义的内容。
- 定义常量:
|
|
使用 PI时,编译器会将其替换为 3.14159。
- 宏函数:
|
|
使用 SQUARE(5)时,编译器会将其替换为 ((5) * (5))。
条件编译
条件编译指令用于控制代码的编译行为,允许编译器根据条件选择性地编译某些代码。
#if、#elif、#else、#endif:根据条件选择性地编译代码。
|
|
#ifdef和#ifndef:判断某个宏是否被定义。
|
|
#undef:取消宏定义
使用 #undef可以取消某个宏的定义:
|
|
#pragma:特殊指令
#pragma指令是编译器特定的指令,用于设置编译器的特性。例如:
|
|
3. 宏与内联函数的区别
- 宏在预处理阶段展开,不执行类型检查,可能会导致意外的错误。
- 内联函数在编译时替换成函数代码,执行类型检查,通常比宏更安全。
例如:
|
|