C语言学习笔记
1. C 语言概述
-
1.1 C 语言的历史与发展
- 起源
- C 语言由 Dennis Ritchie 在 1972 年于贝尔实验室开发,目的是为了实现 UNIX 操作系统。
- C 语言是在 B 语言的基础上发展而来的,B 语言是由 Ken Thompson 开发的。
- 发展
- 1978 年,Brian Kernighan 和 Dennis Ritchie 合作出版了《C 程序设计语言》(K&R),这本书成为 C 语言的经典教材,并推广了 C 语言的使用。
- 1983 年,C 语言被国际标准化组织(ISO)采纳,形成了 ISO C 标准。
- 1999 年,发布了 C99 标准,增加了对新的数据类型(如
long long
和bool
)的支持,并引入了变量声明的灵活性。 - 2011 年,发布了 C11 标准,增强了多线程支持、内存管理功能以及对泛型编程的支持。
- 起源
-
1.2 C 语言的特点与应用
- 特点
- 高效性:C 语言接近于计算机硬件,编写的程序执行效率高。
- 灵活性:提供了低级的内存操作能力,允许直接操作硬件。
- 可移植性:C 语言代码可在不同平台上编译运行,只需少量修改。
- 丰富的库支持:标准库提供了大量的函数,可以用于输入输出、字符串处理、数学运算等。
- 结构化编程:支持函数和模块化编程,使程序结构清晰、易于维护。
- 应用
- 系统软件:操作系统(如 UNIX、Linux、Windows)和编译器的开发。
- 嵌入式系统:广泛应用于嵌入式设备开发,如微控制器和嵌入式操作系统。
- 游戏开发:许多游戏引擎使用 C 语言进行开发,以提高性能。
- 网络编程:在网络协议和服务器软件中广泛应用。
- 科学计算与数值分析:用于开发高性能的计算程序。
- 特点
-
1.3 C 语言的编译过程
-
源代码编写
- 使用文本编辑器编写 C 语言源代码,文件扩展名通常为
.c
。
编译过程
- 预处理(Preprocessing):
- 处理以
#
开头的预处理指令,如#include
和#define
。 - 将头文件的内容插入到源代码中,展开宏定义。
- 处理以
- 编译(Compilation):
- 将预处理后的源代码转换为汇编代码。
- 进行语法检查和类型检查,生成中间代码。
- 汇编(Assembly):
- 将汇编代码转换为机器代码(目标文件)。
- 目标文件通常包含未解决的符号(如函数引用)。
- 链接(Linking):
- 将一个或多个目标文件与库文件链接,生成可执行文件。
- 解决未定义的符号,将函数和变量的地址绑定。
示例
- 使用 GCC 编译器的命令行编译过程示例:
gcc -o myprogram myprogram.c
这里
-o
参数指定输出文件名为myprogram
,myprogram.c
是源代码文件。如:
gcc test.c -o test.exe
- 使用文本编辑器编写 C 语言源代码,文件扩展名通常为
-
-
1.4 常用 C 语言开发工具与环境搭建
- 开发环境
- 文本编辑器:可使用任何文本编辑器,如 Vim、Nano、Notepad++、VS Code 等。
- 集成开发环境(IDE):如 Code::Blocks、Dev-C++、Eclipse CDT、Visual Studio 等,这些 IDE 提供了代码高亮、调试和编译功能。
- 编译器
- GCC(GNU Compiler Collection):广泛使用的开源编译器,支持多种平台。
- Clang:由 LLVM 项目开发,提供高效的编译速度和丰富的功能。
- MSVC(Microsoft Visual C++):适用于 Windows 开发的专有编译器。
- 调试工具
- GDB(GNU Debugger):开源调试工具,用于调试 C 语言程序。
- Valgrind:内存调试和泄漏检测工具,帮助查找内存管理问题。
- 设置环境
- Linux 系统:可以直接使用终端安装 GCC 和其他工具。
- Windows 系统:可以使用 MinGW 或 Cygwin 安装 GCC,或者直接安装 Visual Studio。
- macOS 系统:可以使用 Homebrew 安装 GCC 或 Clang。
- 开发环境
2. C 语言基础
C语言是一种广泛使用的程序设计语言,因其高效性和灵活性而受到欢迎。理解C语言的基本概念是编程的基础,下面将详细介绍C语言的基础知识
-
2.1 基本语法
扫描二维码关注公众号,回复: 17470210 查看本文章-
2.1.1 程序结构
-
C语言程序的基本结构包括头文件、主函数和其他函数的定义。一个典型的C语言程序如下所示:
#include <stdio.h> // 引入标准输入输出库 int main() { // 主函数,程序的入口 printf("Hello, World!\n"); // 输出语句 return 0; // 返回值 }
#include <stdio.h>
:预处理指令,用于包含标准输入输出库,以便使用printf
等函数。int main()
:主函数,程序的执行从这里开始。printf
:用于输出字符串到控制台。return 0;
:返回值,表示程序正常结束。
-
-
2.1.2 注释与格式化
-
C语言中的注释用于解释代码,可以提高代码的可读性。C语言支持两种注释形式:
- 单行注释:以
//
开始,直到行末为止。 - 多行注释:以
/*
开始,以*/
结束,可以跨越多行。
// 这是一个单行注释 /* 这是一个多行注释 可以用于更详细的解释 */
在格式化方面,C语言并没有强制要求代码的书写格式,但推荐使用一致的缩进和换行方式,使代码更易读。
- 单行注释:以
-
-
-
2.2 数据类型
C语言中的数据类型决定了变量所能存储的数据类型及其占用的内存大小。主要分为基本数据类型和派生数据类型。
-
2.2.1 基本数据类型(int, float, char, double)
int
:整型,用于表示整数。float
:单精度浮点型,用于表示带小数的数值。char
:字符型,用于表示单个字符。double
:双精度浮点型,用于表示更精确的带小数的数值。
int a = 10; // 整数 float b = 5.5; // 单精度浮点数 char c = 'A'; // 字符 double d = 3.14159; // 双精度浮点数
-
2.2.2 枚举类型
枚举类型用于定义一组命名的整型常量,增加代码的可读性。
enum Color { RED, GREEN, BLUE }; // 定义一个枚举类型 enum Color favoriteColor; favoriteColor = GREEN; // 使用枚举类型
在这个例子中,由于没有显式指定值,
RED
将被赋值为0
,GREEN
为1
,BLUE
为2
。 -
2.2.3 类型修饰符
C语言提供了类型修饰符来改变基本数据类型的特性,包括:
signed
:表示有符号类型(默认)。unsigned
:表示无符号类型,只能存储非负值。short
:表示短整型,通常占用较少的内存(例如2字节)。long
:表示长整型,通常占用更多的内存(例如4或8字节)。
unsigned int u = 10; // 无符号整型 short int s = -5; // 短整型 long int l = 123456; // 长整型
-
-
2.3 变量与常量
-
2.3.1 变量的定义与初始化
变量是程序运行时存储数据的基本单元。在C语言中,定义变量的语法为:
数据类型 变量名;
变量可以在定义的同时进行初始化。
int x; // 定义整型变量x x = 5; // 初始化变量x float y = 3.14; // 定义并初始化浮点型变量y
-
2.3.2 常量的使用
常量是不可改变的量,使用
const
关键字来定义常量。常量在程序执行过程中不会被修改(尽管使用const
关键字声明的常量不允许直接修改,但在某些情况下,可以通过指针进行操作,后续会有补充)。const int MAX_SIZE = 100; // 定义一个整型常量
使用常量可以提高代码的可读性和可维护性,避免魔法数字的使用。
在编程中,“魔法数字”(magic number)指的是在代码中直接使用的数字字面量,这些数字没有上下文或说明,可能使代码难以理解和维护。例如,直接使用 3.14、42 或 60 等数字,而不对其含义进行解释,会让读者难以明白这些数字在程序中的意义。
-
-
2.4 运算符
C语言提供了多种运算符,用于执行不同类型的运算。主要运算符包括算术运算符、关系运算符、逻辑运算符、位运算符和赋值运算符。
-
2.4.1 算术运算符
算术运算符用于执行基本的数学运算。
运算符 描述 示例 +
加法 a + b
-
减法 a - b
*
乘法 a * b
/
除法 a / b
%
取模(余数) a % b
int a = 10, b = 3; int sum = a + b; // 加法 int difference = a - b; // 减法 int product = a * b; // 乘法 int quotient = a / b; // 除法 int remainder = a % b; // 取模
-
2.4.2 关系运算符
关系运算符用于比较两个操作数的关系,返回布尔值(真或假)。
运算符 描述 示例 ==
等于 a == b
!=
不等于 a != b
>
大于 a > b
<
小于 a < b
>=
大于或等于 a >= b
<=
小于或等于 a <= b
if (a > b) { printf("a is greater than b\n"); }
在C语言中,布尔值通常使用整数来表示。在没有 stdbool.h 的情况下,C语言通常使用 0 表示 false,使用非零值(通常是 1)表示 true。从C99标准开始,可以使用 stdbool.h 头文件来定义布尔类型。此时,bool 代表布尔类型,true 和 false 分别代表 1 和 0。
-
2.4.3 逻辑运算符
逻辑运算符用于组合布尔表达式。
运算符 描述 示例 &&
逻辑与 a && b
||
逻辑或 a || b
!
逻辑非 !a
if (a > 0 && b > 0) { printf("Both a and b are positive\n"); }
-
2.4.4 位运算符
位运算符用于对整型数据的位进行操作。
运算符 描述 示例 &
按位与 a & b
|
按位或 a | b
^
按位异或 a ^ b
~
按位取反 ~a
<<
左移 a << 2
>>
右移 a >> 2
int a = 5; // 0000 0101 int b = 3; // 0000 0011 int result = a & b; // 结果是 0000 0001
-
2.4.5 赋值运算符
赋值运算符用于给变量赋值,常用的赋值运算符包括:
运算符 描述 示例 =
赋值 a = b
+=
加法赋值 a += b
-=
减法赋值 a -= b
*=
乘法赋值 a *= b
/=
除法赋值 a /= b
%=
取模赋值 a %= b
-
3. 控制结构
控制结构是编程语言的基本组成部分,用于控制程序的执行流程。主要包括条件语句和循环语句,这些结构帮助程序根据不同的输入和条件采取不同的执行路径,从而实现复杂的逻辑和功能。
-
3.1 条件语句
条件语句根据特定条件的真假来执行不同的代码块。它们用于决策过程,根据用户输入、计算结果或其他条件的不同来执行不同的代码。
-
3.1.1 if 语句
if
语句是最基本的条件控制结构。它通过检查条件表达式的结果来决定执行哪一部分代码。基本语法如下:if (条件) { // 如果条件为 true 执行的代码 } else { // 如果条件为 false 执行的代码 }
示例:
int age = 20; if (age >= 18) { printf("你是成年人。\n"); } else { printf("你是未成年人。\n"); }
可以使用
else if
来处理多个条件:if (age < 13) { printf("你是儿童。\n"); } else if (age < 18) { printf("你是青少年。\n"); } else { printf("你是成年人。\n"); }
注意: 在
if
语句中,条件表达式的结果必须为布尔值(true 或 false)。在 C 语言中,非零值被视为 true,零被视为 false。 -
3.1.2 switch 语句
switch
语句用于根据变量的值选择不同的执行路径,适合处理多个分支的情况。基本语法如下:switch (表达式) { case 值1: // 执行的代码 break; case 值2: // 执行的代码 break; default: // 当没有匹配到的值时执行的代码 }
示例:
int day = 3; switch (day) { case 1: printf("星期一\n"); break; case 2: printf("星期二\n"); break; case 3: printf("星期三\n"); break; default: printf("未知的星期\n"); }
注意: 如果没有
break
语句,程序会继续执行下一个case
,这称为“穿透”,在某些情况下可能有用,但通常会导致意外行为。-
switch
语句可以让你利用穿透特性将多个case
语句组合在一起,以共享相同的执行代码。示例:
int grade = 2; switch (grade) { case 1: case 2: case 3: printf("你是初学者。\n"); break; case 4: case 5: printf("你是中级玩家。\n"); break; case 6: printf("你是高级玩家。\n"); break; default: printf("未知等级。\n"); }
-
switch
语句不直接支持范围计算,但可以通过一些技巧来实现对范围的处理。#include <stdio.h> int main() { int age = 25; // 假设这是要检查的年龄 switch (age / 10) { // 将年龄除以 10 进行分组 case 0: // 0-9岁 printf("儿童\n"); break; case 1: // 10-19岁 printf("青少年\n"); break; case 2: // 20-29岁 printf("年轻人\n"); break; case 3: // 30-39岁 printf("中年人\n"); break; default: // 40岁及以上 printf("中老年人\n"); } return 0; }
小练习,判断下列两段代码中最终的i值和j值分别是多少
int main() { int i = 1; switch (i) { case 1: { int j = 0; case 2: { if (!!j != 1) { i = !!i, i++; printf("j=%d\n", (j = 3, j++)); break; } j = !!j, j++; printf("j=%d\n", j); break; }break; } } printf("i=%d\n", i); return 0; }
int main() { int i = 2; switch (i) { case 1: { int j = 0; case 2: { if (!!j != 1) { i = !!i, i++; printf("j=%d\n", (j = 3, j++)); break; } j = !!j, j++; printf("j=%d\n", j); break; }break; } } printf("i=%d\n", i); return 0; }
第一段:
j=3 i=2
第二段:
j=2 i=2
-
-
-
3.2 循环语句
条件语句可以嵌套使用,即在一个条件语句内部再放置其他条件语句。这使得代码能够处理更复杂的逻辑。
-
3.2.1 for 循环
for
循环是用于已知循环次数的情况。它包括三个部分:初始化、条件和更新。基本语法如下:for (初始化; 条件; 更新) { // 循环体 }
示例:
for (int i = 0; i < 5; i++) { printf("当前值: %d\n", i); }
应用场景: 适合处理数组、列表等数据结构的遍历。
for循环中的条件可以省略,但省略条件不能省略分号
for (;;) { // 无限循环的内容 if (some_condition) { break; // 退出循环 } }
-
3.2.2 while 循环
while
循环在每次循环开始时检查条件,如果条件为 true,则执行循环体。基本语法如下:while (条件) { // 循环体 }
示例:
int count = 0; while (count < 5) { printf("当前计数: %d\n", count); count++; }
应用场景: 当循环次数未知时,适合等待某个条件成立的场景。
示例:
char src[] = "helloworld"; char* i = &src; while (*i++) { printf("%s\n", i - 1); }
小练习,判断下面两份代码分别输出什么
int i = 0; while (i = 1, i++) { if (i == 2) { printf("退出循环"); break; } } printf("%d\n", i);
int i = 0; while (i = 0, i++) { if (i == 1) { printf("退出循环"); break; } } printf("%d\n", i);
A.
退出循环 2,退出循环 1
B.2,1
C.退出循环 2,1
D.2,退出循环 1
答案:C
-
3.2.3 do while 循环
do while
循环与while
循环类似,但do while
循环会在条件检查之前至少执行一次循环体。基本语法如下:do { // 循环体 } while (条件);
示例:
int count = 0; do { printf("当前计数: %d\n", count); count++; } while (count < 5);
应用场景: 适用于至少需要执行一次的场景,例如获取用户输入并进行验证。
-
3.2.4 break 和 continue 的使用
- break 语句用于立即终止循环,跳出循环体。
示例:
for (int i = 0; i < 10; i++) { if (i == 5) { break; // 当 i 为 5 时,跳出循环 } printf("当前值: %d\n", i); }
- continue 语句用于跳过当前循环的剩余部分,并进入下一次循环。
示例:
for (int i = 0; i < 10; i++) { if (i % 2 == 0) { continue; // 如果 i 是偶数,跳过后面的代码 } printf("当前值: %d\n", i); // 只打印奇数 }
-
4. 函数
函数是程序设计中的一个基本概念,用于将特定的操作或逻辑封装起来,以便重用和组织代码。在C语言中,函数的使用能够使程序更加模块化、易于理解和维护。
-
4.1 函数的定义与声明
函数声明是在使用函数之前对其名称、返回类型及参数类型的声明。它告诉编译器这个函数的基本信息,但并不提供具体的实现。
函数定义则是对函数的具体实现,包含了函数的主体和逻辑。
函数声明的语法:
return_type function_name(parameter_type1 parameter_name1, parameter_type2 parameter_name2, ...);
函数定义的语法:
return_type function_name(parameter_type1 parameter_name1, parameter_type2 parameter_name2, ...) { // 函数的具体实现 }
示例:
#include <stdio.h> // 函数声明 int add(int a, int b); // 函数定义 int add(int a, int b) { return a + b; } int main() { int sum = add(5, 10); // 调用函数 printf("Sum: %d\n", sum); return 0; }
其中,函数的声明有多种方式:
在C语言中,函数声明是对函数名称、返回类型和参数类型的说明,目的是让编译器知道函数的基本信息,以便在调用函数之前进行正确的类型检查。函数声明可以有几种不同的形式,以下是详细说明:
4.1.1 基本函数声明
基本的函数声明包括返回类型、函数名称和参数列表。参数列表可以为空,也可以包含一个或多个参数。
语法:
return_type function_name(parameter_type1 parameter_name1, parameter_type2 parameter_name2, ...);
示例:
int add(int a, int b); // 返回类型为int,函数名为add,接受两个int类型的参数
4.1.2 不带参数的函数声明
如果函数不接受任何参数,可以使用
void
关键字表示参数列表为空。这是C语言中表明函数不接收参数的一种方式。语法:
extern return_type function_name(void);
其中
extern
声明关键字可以省略,void
关键字也可以省略示例:
void printMessage(void); // 返回类型为void,表示该函数没有参数
4.1.3 函数声明中的参数类型
在函数声明中,参数名称可以省略,只保留参数类型。这样做的好处是能够更简洁地声明函数,但编写代码的人必须清楚函数的参数类型。
语法:
return_type function_name(parameter_type1, parameter_type2, ...);
示例:
float calculateArea(float, float); // 省略参数名称,仅保留参数类型
4.1.4 函数指针的声明
函数指针可以用来指向具有特定参数和返回类型的函数。声明函数指针时,首先定义返回类型,然后是指针标识符,最后是参数列表。
语法:
return_type (*pointer_name)(parameter_type1, parameter_type2, ...);
示例:
int (*operation)(int, int); // 声明一个指向返回int类型并接受两个int参数的函数的指针
4.1.5 带有数组或指针参数的函数声明
如果函数接受数组或指针作为参数,参数可以声明为数组的指针类型。对于数组参数,通常在声明时省略数组的大小。
语法:
return_type function_name(type_name[]); // 数组形式 // 或 return_type function_name(type_name*); // 指针形式
示例:
void processArray(int arr[], int size); // 数组作为参数 void processPointer(int* ptr, int size); // 指针作为参数
4.1.6 变长参数的函数声明
C语言允许使用变长参数的函数,这类函数可以接收不定数量的参数。通常使用
stdarg.h
库来处理这类参数。声明时使用...
表示参数的可变性。语法:
return_type function_name(parameter_type1, ..., parameter_typeN, ...);
示例:
#include <stdarg.h> void myPrintf(const char* format, ...); // 变长参数的函数声明
4.1.7 函数的类型定义
可以使用
typedef
定义一个函数类型,以简化函数指针的声明。这样在需要多个地方使用相同类型的函数指针时,可以提高可读性。示例:
typedef int (*FuncPtr)(int, int); // 定义一个函数指针类型 FuncPtr add; // 使用定义的类型声明函数指针
-
4.2 函数参数传递
C语言中的函数参数可以通过两种方式传递:值传递和引用传递(指针传递)。
-
4.2.1 值传递
在值传递中,函数接收参数的副本,这意味着在函数内部对参数的修改不会影响原始数据。这种方法的优点是安全,但对于较大的数据结构(如数组或结构体),可能会导致性能问题,因为需要复制数据。
示例:
#include <stdio.h> void modifyValue(int num) { num = num + 10; // 修改副本 } int main() { int original = 5; modifyValue(original); printf("Original: %d\n", original); // 输出仍为 5 return 0; }
-
4.2.2 引用传递(指针)
引用传递通过指针传递参数,允许函数直接修改调用者的变量。这样可以提高效率,特别是在处理大型数据结构时。
示例:
#include <stdio.h> void modifyValue(int* num) { *num = *num + 10; // 通过指针修改原始数据 } int main() { int original = 5; modifyValue(&original); // 传递原始变量的地址 printf("Original: %d\n", original); // 输出为 15 return 0; }
-
-
4.3 递归函数
递归函数是一个直接或间接调用自身的函数。递归通常用于解决具有重复结构的问题,如树遍历和数学计算(例如阶乘和斐波那契数列)。每个递归函数必须有一个基准条件,以防止无限递归。
示例:计算阶乘
#include <stdio.h> int factorial(int n) { if (n == 0) { // 基准条件 return 1; } return n * factorial(n - 1); // 递归调用 } int main() { int num = 5; printf("Factorial of %d: %d\n", num, factorial(num)); // 输出 120 return 0; }
-
4.4 函数指针
函数指针是指向函数的指针,可以用来实现回调机制、动态函数调用等。通过函数指针,可以将函数作为参数传递,或者在运行时决定要调用哪个函数。
定义函数指针的语法:
return_type (*pointer_name)(parameter_type1, parameter_type2, ...);
示例:
#include <stdio.h> // 定义一个函数 int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } // 定义函数指针 typedef int (*operation)(int, int); int main() { operation op; // 函数指针 op = add; // 指向 add 函数 printf("Add: %d\n", op(5, 10)); // 调用 add op = subtract; // 指向 subtract 函数 printf("Subtract: %d\n", op(10, 5)); // 调用 subtract return 0; }
5. 数组
数组是一种数据结构,可以存储固定数量的相同类型的元素。数组的元素可以通过索引进行访问,索引从零开始。
-
5.1 一维数组
定义和初始化
一维数组是线性的数据结构,元素在内存中是连续存储的。可以通过以下方式定义和初始化一维数组:
#include <stdio.h> int main() { // 定义并初始化数组 int arr[5] = { 1, 2, 3, 4, 5}; // 访问数组元素 for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); } return 0; }
数组的特点
- 固定大小:数组在定义时需要指定大小,无法动态改变。
- 元素类型一致:数组中的所有元素必须是相同类型。
常用操作
- 遍历数组:使用循环结构访问每个元素。
- 求和/平均值:可以通过遍历数组实现。
int sum = 0; for (int i = 0; i < 5; i++) { sum += arr[i]; } float average = sum / 5.0;
-
5.2 二维数组
定义和初始化
二维数组可以视为数组的数组,常用于表示矩阵。定义和初始化的方式如下:
#include <stdio.h> int main() { // 定义并初始化二维数组 int matrix[3][3] = { { 1, 2, 3}, { 4, 5, 6}, { 7, 8, 9} }; // 访问二维数组元素 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d ", matrix[i][j]); } printf("\n"); } return 0; }
二维数组的特点
- 行和列:访问方式为
matrix[row][column]
。 - 内存布局:在内存中,二维数组是以行优先的方式存储的。
常用操作
- 矩阵加法:可以通过遍历两个矩阵实现相应位置元素的加法。
- 转置矩阵:可以通过交换行和列来实现。
- 行和列:访问方式为
-
5.3 多维数组
定义和初始化
多维数组是指具有两个以上维度的数组,常用于更复杂的数据结构,如三维数组等。以下是一个三维数组的示例:
#include <stdio.h> int main() { // 定义并初始化三维数组 int array[2][3][4] = { { { 1, 2, 3, 4}, { 5, 6, 7, 8}, { 9, 10, 11, 12} }, { { 13, 14, 15, 16}, { 17, 18, 19, 20}, { 21, 22, 23, 24} } }; // 访问三维数组元素 for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { for (int k = 0; k < 4; k++) { printf("%d ", array[i][j][k]); } printf("\n"); } printf("\n"); } return 0; }
多维数组的特点
- 维度任意:可以定义任意维度的数组,但需要考虑内存使用情况。
- 访问方式:访问元素时需要指定所有维度的索引。
-
5.4 数组与函数的结合
数组可以作为函数的参数传递。C语言中,数组的实际传递是以指针的形式进行的,因此在函数内对数组的修改会影响原数组。
void modifyArray(int arr[], int size) { for (int i = 0; i < size; i++) { arr[i] *= 2; // 将数组中的每个元素乘以2 } }
在主函数中,可以这样调用:
int main() { int myArray[] = { 1, 2, 3, 4, 5}; int size = sizeof(myArray) / sizeof(myArray[0]); modifyArray(myArray, size); // myArray 现在变成 {2, 4, 6, 8, 10} return 0; }
虽然C语言不支持直接返回数组,但可以通过返回指针来实现。通常使用动态内存分配来创建数组。
int* createArray(int size) { int* arr = (int*)malloc(size * sizeof(int)); for (int i = 0; i < size; i++) { arr[i] = i + 1; // 初始化数组 } return arr; }
注意,调用者需要负责手动释放这个动态分配的内存。
int main() { int size = 5; int* myArray = createArray(size); // 使用 myArray free(myArray); // 释放内存 return 0; }
对于多维数组,可以通过在函数参数中明确维度来传递。
void printMatrix(int matrix[][3], int rows) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 3; j++) { printf("%d ", matrix[i][j]); } printf("\n"); } }
在主函数中,可以这样调用:
int main() { int matrix[2][3] = { { 1, 2, 3}, { 4, 5, 6}}; printMatrix(matrix, 2); return 0; }
6. 字符串处理
字符串是由字符组成的数组,以 '\0'
(空字符)结尾。C语言中的字符串处理非常灵活,但也需要注意数组的边界和内存管理。
-
6.1 字符串的定义与初始化
定义字符串
在C语言中,字符串可以通过字符数组或字符指针定义。以下是两种常见的定义方式:
#include <stdio.h> int main() { // 使用字符数组定义字符串 char str1[20] = "Hello, World!"; // 预留足够空间 // 使用字符指针定义字符串 const char *str2 = "Hello, C!"; // 字符串常量 // 输出字符串 printf("%s\n", str1); printf("%s\n", str2); return 0; }
注意事项
- 字符数组需要有足够的空间存放字符串及结束符
'\0'
。 - 字符指针指向的字符串常量是只读的,不能修改。
- 字符数组需要有足够的空间存放字符串及结束符
-
6.2 字符串操作函数
C标准库提供了一些函数用于字符串操作,包含在头文件
string.h
中。-
6.2.1 字符串长度计算
使用
strlen
函数计算字符串的长度(不包括'\0'
)。#include <stdio.h> #include <string.h> int main() { char str[] = "Hello, World!"; size_t length = strlen(str); // 计算字符串长度 printf("Length of string: %zu\n", length); return 0; }
-
6.2.2 字符串拷贝与连接
-
字符串拷贝:使用
strcpy
函数将一个字符串复制到另一个字符串中。 -
字符串连接:使用
strcat
函数将两个字符串连接起来。#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); return 0; }
注意事项
- 使用
strcpy
和strcat
时,确保目标字符串有足够的空间以存放结果。 - 可以使用
strncpy
和strncat
来限制复制和连接的字符数。
- 使用
-
-
6.2.3 字符串比较
使用
strcmp
函数比较两个字符串的内容,返回值指示字符串的关系:- 返回 0:两个字符串相等。
- 返回 > 0:第一个字符串大于第二个字符串。
- 返回 < 0:第一个字符串小于第二个字符串。
#include <stdio.h> #include <string.h> int main() { char str1[] = "Hello"; char str2[] = "Hello, World!"; char str3[] = "hello"; // 比较字符串 int result1 = strcmp(str1, str2); int result2 = strcmp(str1, str3); int result3 = strcmp(str1, str1); printf("Comparison result (str1 vs str2): %d\n", result1); // < 0 printf("Comparison result (str1 vs str3): %d\n", result2); // > 0 printf("Comparison result (str1 vs str1): %d\n", result3); // 0 return 0; }
-
7. 指针
指针是C语言中的一个重要特性,它允许程序直接访问内存地址,极大地增强了程序的灵活性和效率。
-
7.1 指针的定义与使用
指针是一个变量,其值为另一个变量的地址。可以通过取地址运算符
&
获取变量的地址,通过解引用运算符*
访问指针所指向的值。- 7.1.1 指针的定义
int a = 10; // 定义一个整数变量 int *p = &a; // 定义一个指向整数的指针,并将其初始化为a的地址
- 7.1.2 指针的使用
#include <stdio.h> int main() { int a = 10; // 整数变量 int *p = &a; // 指针p指向a的地址 printf("a的值: %d\n", a); // 输出: a的值: 10 printf("p的值: %p\n", (void*)p); // 输出p的地址 printf("*p的值: %d\n", *p); // 输出: *p的值: 10 *p = 20; // 通过指针修改a的值 printf("修改后的a的值: %d\n", a); // 输出: 修改后的a的值: 20 return 0; }
其中,指针还能够改变某些常量的值
#include<stdio.h> int main() { const int a = 10; printf("常量a的值是:%d\n", a); int* p = &a; *p = 100; printf("常量a的值是:%d\n", a); printf("指针p的值是:%d\n", *p);// 打印指针 p 指向的值,即 a 的值 return 0; }
-
7.2 指针与数组的关系
在C语言中,数组名实际上是一个指向数组第一个元素的指针。因此,数组可以通过指针进行操作。
-
7.2.1 数组与指针
#include <stdio.h> int main() { int arr[] = { 1, 2, 3, 4, 5}; // 数组 int *p = arr; // 数组名作为指针 // 通过指针遍历数组 for (int i = 0; i < 5; i++) { printf("arr[%d] = %d\n", i, *(p + i)); // 使用指针访问数组元素 } return 0; }
-
-
7.3 指针与函数
指针可以作为函数参数,使得函数能够直接修改传入的变量。此外,函数也可以返回指针。
-
7.3.1 指针作为函数参数
使用指针作为参数,可以让函数修改调用者的变量。
#include <stdio.h> void modifyValue(int *p) { *p = 100; // 修改指针所指向的值 } int main() { int a = 10; printf("修改前: a = %d\n", a); // 输出: 修改前: a = 10 modifyValue(&a); // 传递a的地址 printf("修改后: a = %d\n", a); // 输出: 修改后: a = 100 return 0; }
-
7.3.2 返回指针
函数可以返回指向动态分配内存的指针。注意,返回指向局部变量的指针是不安全的,因为局部变量在函数返回后会被销毁。
#include <stdio.h> #include <stdlib.h> int* allocateArray(int size) { return (int*)malloc(size * sizeof(int)); // 动态分配内存 } int main() { int *arr = allocateArray(5); // 分配数组 for (int i = 0; i < 5; i++) { arr[i] = i + 1; // 初始化数组 } for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); // 输出: 1 2 3 4 5 } free(arr); // 释放动态分配的内存 return 0; }
-
-
7.4 指针的高级使用
-
7.4.1 指向指针
指针可以指向另一个指针。这种用法在处理多级指针(如指向指针的指针)时非常有用。
#include <stdio.h> int main() { int a = 10; int *p = &a; // 指向a的指针 int **pp = &p; // 指向p的指针 printf("a的值: %d\n", a); // 输出: 10 printf("通过pp访问a: %d\n", **pp); // 输出: 10 **pp = 20; // 通过指向指针的指针修改a的值 printf("修改后的a的值: %d\n", a); // 输出: 20 return 0; }
-
7.4.2 动态内存分配
动态内存分配使用
malloc()
、calloc()
和free()
等函数,可以在运行时分配和释放内存。这对于处理未知大小的数据结构(如链表和动态数组)非常重要。-
7.4.2.1 使用
malloc()
#include <stdio.h> #include <stdlib.h> int main() { int *arr = (int*)malloc(5 * sizeof(int)); // 动态分配数组 if (arr == NULL) { printf("内存分配失败\n"); return 1; // 处理内存分配失败 } for (int i = 0; i < 5; i++) { arr[i] = i + 1; // 初始化数组 } for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); // 输出: 1 2 3 4 5 } free(arr); // 释放内存 return 0; }
-
7.4.2.2 使用
calloc()
calloc()
与malloc()
类似,但它会初始化分配的内存。#include <stdio.h> #include <stdlib.h> int main() { int *arr = (int*)calloc(5, sizeof(int)); // 动态分配并初始化数组 if (arr == NULL) { printf("内存分配失败\n"); return 1; } for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); // 输出: 0 0 0 0 0 (初始值为0) } free(arr); // 释放内存 return 0; }
-
-
8. 结构体与联合体
- 8.1 结构体的定义与使用
- 8.2 嵌套结构体
- 8.3 联合体的定义与使用
- 8.4 结构体与指针
9. 文件操作
- 9.1 文件的打开与关闭
- 9.2 文件的读写操作
- 9.2.1 文本文件
- 9.2.2 二进制文件
- 9.3 错误处理
10. C 语言标准库
- 10.1 常用头文件介绍
- 10.1.1
<stdio.h>
:输入输出库 - 10.1.2
<stdlib.h>
:标准库(内存分配、随机数等) - 10.1.3
<string.h>
:字符串处理函数 - 10.1.4
<math.h>
:数学函数
- 10.1.1
- 10.2 使用标准库函数的注意事项
11. 进阶主题
- 11.1 动态内存管理
- 11.1.1 malloc()、calloc()、realloc()、free()
- 11.2 数据结构
- 11.2.1 链表
- 11.2.2 栈与队列
- 11.2.3 树
- 11.2.4 图
- 11.3 预处理器指令
- 11.3.1 宏定义
- 11.3.2 条件编译
- 11.4 多文件项目与头文件
12. 调试与优化
- 12.1 常见错误与调试技巧
- 12.2 性能优化方法
- 12.3 代码规范与注释
13. 实践项目
- 13.1 简单计算器
- 13.2 学生信息管理系统
- 13.3 猜数字游戏
- 13.4 文件系统模拟
- 13.5 数据结构实现(如链表、栈、队列)
14. 拓展学习
- 14.1 C++ 和其他语言的对比
- 14.2 C 语言在嵌入式系统中的应用
- 14.3 C 语言在操作系统中的应用
- 14.4 学习算法与数据结构的结合
15. 资源推荐
- 15.1 书籍推荐
- 15.2 在线学习平台
- 15.3 社区与论坛