这篇文章是我的第一篇编程教程,也是给自己学的东西的一点总结,由于篇幅和精力,作为一篇普适性的文章,涉及的C语言都是很基础的。
1. C语言简介
C语言是一种通用、高级且面向过程的编程语言,由贝尔实验室的丹尼斯・里奇于20世纪70年代开发。它具有高效、灵活的特点,可直接访问硬件,代码执行速度快。C语言语法简洁,拥有丰富的数据类型和运算符,广泛应用于系统软件、嵌入式开发、游戏、驱动程序等领域。C语言还是许多现代编程语言的基础,对计算机科学发展影响深远,是学习编程和理解计算机底层原理的重要工具。
2. 基本语法
代码结构与注释
C语言的代码结构通常由函数组成。每个C语言程序的执行都从main()
函数开始(实际上并不是从main开始的,这里只是方便理解,可以自己想一下原因)
代码示例:
1 |
|
注释:
- 单行注释:
//
后面的内容为注释 - 多行注释:
/*
和*/
之间的内容为注释
注释里面的代码只是方便人理解,不会被编译器编译
数据类型与变量
C语言支持多种基本数据类型,其中最常用的是:
int
:整数类型,通常用于表示整数值。float
:单精度浮点类型,用于表示小数。double
:双精度浮点类型,表示更高精度的小数。char
:字符类型,通常用于存储单个字符。
声明变量:C语言需要在使用变量之前声明它的类型。声明语法如下:
1 | int a; // 声明一个整数变量 |
常量与宏定义
常量:常量是在程序运行期间不能改变的值。在C语言中,可以通过const
关键字定义常量。
1 | const int maxAge = 100; // 定义常量maxAge |
宏定义:宏定义使用#define
预处理指令,可以在程序中定义常量值或宏函数。宏的作用是在编译时进行简单的文本替换,不会考虑类型,所以要注意可能会由于类型报错。
1 |
完整示例:
1 |
|
基本输入输出(printf
和scanf
)
输出函数printf:用于向屏幕输出信息。其基本用法如下:
1 | printf("文本内容 %d\n", 变量名); // %d表示输出整数 |
常用格式说明符:
%d
:输出整数%f
:输出浮点数%c
:输出字符%s
:输出字符串
示例:
1 | int age = 25; |
输入函数scanf:用于从键盘输入数据。它会根据指定的格式将输入的值存储到变量中。
1 | scanf("格式字符串", &变量名); // scanf是向变量地址写入,所以要加&取地址符 |
1 | char str[10]; |
1 | int num[5]; |
3. 运算符与表达式
算术运算符
运算符 | 描述 | 示例 |
---|---|---|
+ |
加法 | a + b |
- |
减法 | a - b |
* |
乘法 | a * b |
/ |
除法 | a / b |
% |
取余(模运算) | a % b |
示例:
1 | int a = 10, b = 3; |
注意:
/
运算符在整数除法中会丢弃小数部分。那么整数除以整数怎么计算小数部分呢?把其中一个数变成浮点数即可,可以强制转换
比如
1
2
3
4
5int a = 3,b = 2;
float c1 = a / b; // c1 = 1.0;
float c2 = (float)a / b; // c2 = 1.5
float c2 = a / (float)b; // 或者这种
int c3 = (float)a / b; // c3 = 1 这种也会丢失小数,因为c3是int型,要注意%
运算符仅适用于整数类型,返回除法的余数。
关系运算符
关系运算符用于比较两个值,返回一个布尔值(true
或false
)。它们用于判断条件。
运算符 | 描述 | 示例 |
---|---|---|
== |
等于 | a == b |
!= |
不等于 | a != b |
> |
大于 | a > b |
< |
小于 | a < b |
>= |
大于或等于 | a >= b |
<= |
小于或等于 | a <= b |
注意,判断等于是两个等号,写一个等号是赋值语句,这种初学时容易犯错
示例:
1 | int a = 10, b = 3; |
逻辑运算符
逻辑运算符用于对条件进行逻辑运算,返回布尔值。
运算符 | 描述 | 示例 |
---|---|---|
&& |
逻辑与 | a > b && b > 0 |
|| |
逻辑或 | a || b |
! |
逻辑非 | !(a > b) |
示例:
1 | int a = 10, b = 3; |
位运算符
位运算符用于对整数类型的二进制位进行操作。它们常用于底层编程、加密等领域。
运算符 | 描述 | 示例 |
---|---|---|
& |
按位与 | a & b |
| |
按位或 | a | b |
^ |
按位异或 | a ^ b |
~ |
按位取反 | ~a |
<< |
左移 | a << 2 |
>> |
右移 | a >> 2 |
示例:
1 | int a = 5, b = 3; // 二进制:a = 0101, b = 0011 |
位移操作:
- 左移:将二进制数的所有位向左移动,右边补零。
- 右移:将二进制数的所有位向右移动,左边补符号位。
示例:
1 | int a = 5; // 二进制:0101 |
条件运算符
条件运算符是一种简化的if-else
语句。它的基本格式是:
condition ? expression1 : expression2;
condition
为真时取expression1
的返回值,否则取expression2
的返回值
示例:
1 | int a = 10, b = 3; |
1 | int max(int a,int b,int c){ |
三元运算符是一个很不错的方式,在未来的学习中会经常用到,比如flutter和react的条件渲染可以通过三元运算符达到一个快速实现
运算符优先级与结合性
运算符优先级决定了表达式中各个运算符的计算顺序。优先级高的运算符先计算。
常见优先级:
()
,[]
,.
,->
(数组下标、函数调用、结构体成员访问)*
,/
,%
(乘法、除法、取余)+
,-
(加法、减法)==
,!=
,>
,<
,>=
,<=
(比较运算符)&&
,||
(逻辑与、逻辑或)=
,+=
,-=
,*=
,/=
,%=
(赋值运算符)
结合性:
- 大多数运算符是从左到右结合的(例如
+
,-
,*
)。 - 赋值运算符(
=
)是从右到左结合的。
示例:
1 | int a = 10, b = 5, c = 3; |
4. 控制结构
条件语句
条件语句用于根据某个条件的真假来决定是否执行某些代码。在C语言中,常见的条件语句有if
、else
和switch
。
if
语句
if
语句根据一个条件表达式的结果来决定是否执行某段代码。如果条件为真,执行对应的代码块。
语法:
1 | if (condition) { |
示例:
1 | int a = 10; |
if-else
语句
if-else
语句提供了一个可选的代码块,当条件为假时执行else
后面的代码。
语法:
1 | if (condition) { |
示例:
1 | int a = 3; |
if-else if-else
语句
当有多个条件时,可以使用else if
来进行多个条件的判断。
语法:
1 | if (condition1) { |
示例:
1 | int a = 7; |
switch
语句
switch
语句根据变量的值匹配多个可能的选项,并执行对应的代码块。switch
通常用于值的离散比较,而不适用于范围判断。
语法:
1 | switch (expression) { |
示例:
1 | int day = 3; |
循环语句
循环语句允许你重复执行某段代码,直到满足某个条件为止。常见的循环语句有for
、while
和do-while
。
for
循环
for
循环是一种最常见的循环结构,通常用于已知循环次数的情况。
语法:
1 | for (initialization; condition; increment) { |
- initialization:初始化语句,在循环开始时执行一次。
- condition:循环条件,每次循环前判断,如果为真,继续执行;如果为假,退出循环。
- increment:每次循环结束后执行,通常用于更新循环变量。
示例:
1 | for (int i = 1; i <= 5; i++) { |
while
循环
while
循环是基于条件判断的循环,只要条件为真,就继续执行循环体。
语法:
1 | while (condition) { |
示例:
1 | int i = 1; |
do-while
循环
do-while
循环与while
循环类似,不同之处在于它至少执行一次循环体,然后再判断条件。
语法:
1 | do { |
示例:
1 | int i = 1; |
跳转语句
跳转语句用于改变程序的执行流程。常见的跳转语句有break
、continue
和return
。
break
语句
break
语句用于跳出当前循环或switch
语句。它立即终止循环或switch
,并继续执行循环或switch
语句后的代码。
示例:
1 | for (int i = 1; i <= 10; i++) { |
continue
语句
continue
语句用于跳过当前循环的剩余部分,直接进入下一次循环。
示例:
1 | for (int i = 1; i <= 5; i++) { |
return
语句
return
语句用于终止当前函数的执行,并可以选择性地返回一个值。它通常用于从函数中返回计算结果。
示例:
1 | int add(int a, int b) { |
5. 函数
在C语言中,函数是程序的基本构建块之一。通过函数,我们可以将代码分块,提升程序的模块化、可读性和可维护性。函数可以接收输入(参数),并返回结果(返回值)。在本节中,我们将介绍C语言中函数的定义、声明、参数传递以及一些进阶概念。
函数的定义与声明
C语言的函数由函数名、返回类型、参数列表和函数体组成。函数定义的基本语法如下:
语法:
1 | return_type function_name(parameter1, parameter2, ...) { |
- 返回类型(return_type):表示函数的返回值类型。如果函数没有返回值,使用
void
。 - 函数名(function_name):标识函数的名称。
- 参数列表(parameter1, parameter2, …):函数接收的输入参数,可以是多个,使用逗号分隔。没有参数时,使用空的圆括号
()
。 - 函数体:包含函数的具体操作代码,通常是由一系列语句构成。
示例:
1 |
|
函数的参数与返回值
函数可以通过参数接收外部数据,也可以通过返回值将计算结果传递给调用者。C语言中的函数参数传递是通过值传递的方式进行的,即传递的是参数值的副本。
参数传递
C语言中,函数参数传递有两种方式:值传递和引用传递(通过指针)。
值传递:调用函数时,传递给函数的参数是其值的副本,函数对参数的修改不会影响到原始数据。
示例:
1
2
3
4
5
6
7
8
9
10void changeValue(int a) {
a = 10; // 修改的是a的副本
}
int main() {
int num = 5;
changeValue(num);
printf("num = %d\n", num); // 输出:num = 5,原值未改变
return 0;
}引用传递(通过指针):可以通过传递变量的地址(指针),在函数内部直接修改原始数据。
示例:
1
2
3
4
5
6
7
8
9
10void changeValue(int *a) {
*a = 10; // 修改的是a指向的地址内容
}
int main() {
int num = 5;
changeValue(&num); // 传递num的地址
printf("num = %d\n", num); // 输出:num = 10,值被修改
return 0;
}
返回值
一个函数可以返回一个值,表示计算结果。返回值的类型由函数的返回类型决定。如果函数没有返回值,可以使用void
。
示例:
1 |
|
递归函数
递归是函数自己调用自己的一种编程技巧。递归函数通常用于解决可以被分解为相似子问题的问题。递归函数必须有一个基准条件,用于终止递归调用。
示例:计算阶乘
1 |
|
递归调用过程:
1 | factorial(5) = 5 * factorial(4) |
作用域与生命周期
C语言中的作用域和生命周期决定了变量在程序中的有效范围和存在时间。
局部变量:在函数内部声明的变量,只有在函数执行时有效。它们在函数调用时创建,执行完后销毁。
示例:
1
2
3
4
5
6
7
8
9
10void myFunction() {
int a = 5; // 局部变量
printf("a = %d\n", a); // 输出:a = 5
}
int main() {
myFunction();
// printf("a = %d\n", a); // 错误:a在main函数中不可访问
return 0;
}全局变量:在所有函数外部声明的变量,可以在程序的任何地方访问。全局变量在程序启动时创建,在程序结束时销毁。
示例:
1
2
3
4
5
6
7
8
9
10int globalVar = 10; // 全局变量
void printGlobalVar() {
printf("globalVar = %d\n", globalVar); // 输出:globalVar = 10
}
int main() {
printGlobalVar();
return 0;
}
函数指针
函数指针是一个指向函数的指针变量,可以用来动态调用函数。它允许我们将函数作为参数传递给其他函数,或在运行时决定调用哪个函数。
声明函数指针:
1 | return_type (*function_ptr)(parameter_types); |
示例:
1 |
|
6. 数组与字符串
一维数组与多维数组
一维数组:相同类型元素的线性集合
1 | // 声明与初始化 |
多维数组(以二维为例):
1 | int matrix[2][3] = { |
字符数组与字符串操作
字符数组:
1 | char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; |
字符串函数(需包含string.h
):
函数 | 描述 |
---|---|
strlen() |
字符串长度 |
strcpy() |
字符串复制 |
strcat() |
字符串连接 |
strcmp() |
字符串比较 |
1 | char s1[20] = "Hello"; |
数组的指针表示
数组名本质是指向首元素的指针:
1 | int arr[3] = {10, 20, 30}; |
7. 指针
指针基础
定义与使用:
1 | int var = 10; |
指针与数组
1 | int arr[3] = {1, 2, 3}; |
指针与函数
指针作为参数:
1 | void swap(int *a, int *b) { |
动态内存管理
基本操作:
1 |
|
常见错误:
- 忘记释放内存(内存泄漏)
- 使用已释放的内存
- 越界访问
8. 结构体与联合体
结构体定义与使用
1 | struct Student { |
结构体指针与动态分配
1 | struct Student *pStu = (struct Student*)malloc(sizeof(struct Student)); |
联合体的定义与应用
联合体所有成员共享同一内存空间:
1 | union Data { |
典型应用:类型转换、节省内存
9. 文件操作
文件打开与关闭
1 | FILE *fp; |
文件读写操作
格式化读写:
1 | // 写入 |
二进制读写:
1 | struct Student stu; |
文件指针与错误处理
1 | // 移动文件指针 |
10. 内存管理
栈与堆的区别
特性 | 栈 | 堆 |
---|---|---|
分配方式 | 自动分配/释放 | 手动分配/释放 |
大小限制 | 较小(MB级) | 较大(受系统限制) |
访问速度 | 更快 | 较慢 |
碎片问题 | 无 | 可能有 |
内存泄漏检测
常见检测方法:
- 使用工具(Valgrind、Dr. Memory)
- 重载malloc/free函数
- 维护分配/释放日志
预防措施:
1 | // 分配后立即初始化 |
内存分配技巧
批量分配优于多次小分配
1
2
3
4
5
6
7// 不好
for (int i=0; i<100; i++) {
arr[i] = malloc(sizeof(int));
}
// 更好
int *block = malloc(100 * sizeof(int));对齐分配提高访问效率
1
2
alignas(16) int aligned_arr[100];自定义内存池用于高频操作
1
2void* mem_pool_alloc(size_t size);
void mem_pool_free(void* ptr);
完整的内存管理示例:
1 |
|
11.项目实战
完成上述的学习之后一定要完成一个项目实战,便于发现自己的缺漏点以及总结知识
示例: