大家好,我是良许,一个深耕嵌入式 12 年的老工程师,前世界 500 强高工。
我花了 3 个月时间,写了一个 C 语言电子书,以非常通俗的语言跟大家讲解 C 语言,把复杂的技术讲得连小学生都能听得懂,绝不是 AI 生成那种晦涩难懂的电子垃圾。
点击此处免费领取 C 语言电子书
C 语言电子书目录如下:
1.2.1 编程语言是什么?
语言的本质:沟通的桥梁
在我们的日常生活中,语言是人与人之间沟通的工具。中文、英文、法文等自然语言让我们能够表达思想、传递信息、交流感情。同样地,编程语言就是人与计算机之间沟通的工具。就像我们用中文告诉朋友"帮我买一杯咖啡"一样,我们用编程语言告诉计算机"帮我计算1加1等于几"。
但是,计算机和人不同。人类的大脑非常智能,即使我们说话不够准确,或者表达有歧义,朋友也能理解我们的意思。比如你说"买个东西",朋友会根据上下文和你的表情猜出你要买什么。但计算机却是一个"死脑筋",它只能按照非常精确、明确的指令来工作。你必须告诉它每一个步骤该怎么做,不能有任何模糊的地方。
编程语言的发展层次
如果我们把编程语言按照抽象程度来分类,可以分为三个层次:
按照执行方式分类:编译型语言与解释型语言
编程语言还可以按照执行方式分为两大类,这就像看书有两种方式一样:
编译型语言的好处是运行速度很快,因为计算机直接执行机器语言,不需要中间的翻译过程。但是缺点是每次修改程序后都需要重新编译,而且编译后的程序只能在特定的操作系统上运行,移植到其他系统需要重新编译。
解释型语言的好处是编写和调试很方便,修改程序后可以立即运行,而且程序可以在任何安装了解释器的系统上运行。但是缺点是运行速度相对较慢,因为需要边翻译边执行,而且运行时必须安装相应的解释器。
按照编程方式分类:面向过程与面向对象
编程语言还可以按照编程思想分为不同类型:
面向过程的思维方式比较直观,适合解决流程比较明确的问题。比如计算器程序:输入数据→进行运算→输出结果,这是一个清晰的流程。对于我们学习嵌入式开发来说,面向过程的思维方式更贴近硬件的工作方式,也更容易理解程序的执行过程。
面向对象的思维方式更适合构建复杂的大型软件系统,因为它能更好地组织和管理代码,让程序更容易维护和扩展。
1.2.2 什么是程序?
程序的本质:指令的序列
程序,简单来说,就是一系列指令的有序集合,告诉计算机要做什么以及怎么做。这就像一本菜谱,详细地告诉厨师每一个步骤:先洗菜,再切菜,然后热锅,接着下油,最后炒菜。程序也是这样,它一步一步地告诉计算机:先读取数据,再进行计算,然后判断结果,最后输出答案。
让我们用一个生活中的例子来理解程序。假设你要教一个完全不会做饭的女朋友煮蛋炒饭,你需要给出非常详细的步骤:
打开冰箱,取出2个鸡蛋拿一个碗,把鸡蛋打散热锅,倒入适量油把蛋液倒入锅中,快速搅拌鸡蛋半熟时,倒入米饭翻炒3分钟加入适量盐和酱油继续翻炒1分钟关火,装盘
这个做饭的过程就是一个"程序",每一步都是一条"指令"。程序必须足够详细和准确,不能有遗漏或模糊的地方,否则执行者(无论是女朋友还是计算机)就不知道该怎么办。
从程序到进程:程序的运行状态
很多同学容易混淆"程序"和"进程"这两个概念。让我用一个简单的比喻来解释:
同样地,当我们双击一个程序图标时,操作系统就会创建一个进程来执行这个程序。进程包括了程序的代码、程序运行所需的内存空间、CPU的执行状态等等。
任务与多任务
在现代计算机中,我们经常听到"任务"这个词。任务(Task)其实就是进程的另一种说法,特别是在嵌入式系统中,我们更习惯用"任务"这个词。
实际上,计算机的CPU在任意时刻只能执行一个指令,但它执行得非常快,可以在不同的任务之间快速切换。比如它可能用0.01秒处理音乐播放器,然后用0.01秒处理浏览器,再用0.01秒处理文字处理软件。因为切换得非常快,用户感觉就像是多个程序在同时运行。
程序的不同类型
根据功能和用途的不同,程序可以分为很多类型:
1.2.3 程序与算法的关系
经典公式:程序 = 数据结构 + 算法
在计算机科学领域,有一个非常著名的公式:程序 = 数据结构 + 算法。这个公式是由瑞士计算机科学家尼古拉斯·沃思(Niklaus Wirth)提出的,它精确地概括了程序的本质。
让我们用一个生活中的例子来理解这个公式。想象你要组织一次同学聚会:
没有通讯录(数据结构),你不知道要联系谁;没有组织方法(算法),你不知道怎么办聚会;只有把两者结合起来,才能成功组织一次聚会(完成程序的功能)。
什么是数据结构?
数据结构的定义:数据结构是指数据元素之间的关系,以及对这些数据进行操作的方法。简单来说,就是数据怎么存放、怎么组织的问题。
让我们用几个生活中的例子来理解不同的数据结构:
比如,你要存储一个班级所有学生的成绩,可以用数组:成绩 = 85, 成绩 = 92, 成绩 = 78...。数组的特点是查找某个位置的数据很快(直接根据编号找到柜子),但如果要在中间插入或删除数据就比较麻烦(需要移动后面所有的数据)。
链表的特点是插入和删除数据很方便(只需要改变指针的指向),但查找某个特定数据需要从头开始一个一个地找,就像要吃糖葫芦中间的某颗糖,必须从第一颗开始数。
栈在程序中有很多用途,比如保存函数调用的信息。当程序调用一个函数时,会把当前的状态"压入"栈中;当函数执行完毕时,再从栈中"弹出"之前的状态。
队列常用于处理需要排队等待的任务,比如打印机的打印任务、操作系统的任务调度等。
树结构非常适合表示有层次关系的数据,比如文件系统(文件夹包含子文件夹和文件)、组织架构图等。
什么是算法?
算法的定义:算法是解决特定问题的一系列明确、有限的步骤。它回答的是"怎么做"的问题。
我们讲的算法更侧重"逻辑算法",并非"数学型算法",比如PID算法、滤波算法,数学型算法通常需要硕士、博士以上学历(算法工程师)。
让我们通过几个具体的例子来理解算法:
查找算法 - 在电话簿中找人: 假设你要在一本按姓名排序的电话簿中找到"张三"的电话号码,你可能会用以下几种方法:
顺序查找:从第一页开始,一页一页地翻,直到找到张三。这种方法简单但可能很慢。二分查找:因为电话簿是按字母顺序排列的,你可以翻到中间的一页,看看是在"张"之前还是之后,然后继续在相应的一半中查找。这样每次都能排除一半的页面,查找速度快很多。
数据结构与算法如何结合成程序?
理解了数据结构和算法的概念后,我们来看看它们是如何结合成一个完整的程序的。
以学生成绩管理系统为例:
第一步:确定数据结构 首先,我们需要决定如何存储学生信息。每个学生有姓名、学号、各科成绩等信息,我们可以设计这样的数据结构:
plaintext
struct Student {
char name[50]; // 姓名
int id; // 学号
float scores[5]; // 五科成绩
float average; // 平均分
};
然后,我们需要存储所有学生的信息,可以用数组:
plaintext
struct Student students[100]; // 最多100个学生
int student_count = 0; // 当前学生数量
第二步:设计算法 接下来,我们需要设计各种操作的算法:
添加学生算法:查找学生算法:计算平均分算法:
第三步:组合成程序 最后,我们把数据结构和算法组合起来,形成完整的程序:
plaintext
#include
// 数据结构定义
struct Student {
char name[50];
int id;
float scores[5];
float average;
};
struct Student students[100];
int student_count = 0;
// 算法实现
float calculate_average(float scores[]) {
float sum = 0;
for(int i = 0; i < 5; i++) {
sum += scores[i];
}
return sum / 5;
}
void add_student() {
if(student_count >= 100) {
printf("学生数量已满!\n");
return;
}
// 输入学生信息
printf("请输入学生姓名:");
scanf("%s", students[student_count].name);
printf("请输入学号:");
scanf("%d", &students[student_count].id);
printf("请输入5科成绩:");
for(int i = 0; i < 5; i++) {
scanf("%f", &students[student_count].scores[i]);
}
// 计算平均分
students[student_count].average =
calculate_average(students[student_count].scores);
student_count++;
printf("学生信息添加成功!\n");
}
// 主程序
int main() {
int choice;
while(1) {
printf("1. 添加学生\n2. 查找学生\n3. 退出\n");
printf("请选择:");
scanf("%d", &choice);
switch(choice) {
case 1:
add_student();
break;
case 2:
// 查找学生的代码...
break;
case 3:
return 0;
}
}
}
通过这个例子,我们可以清楚地看到:
1.2.4 如何从零生产一个程序?
程序诞生的完整过程
很多初学者认为编程就是坐在电脑前敲代码,但实际上,从零开始制作一个程序就像建造一座房子一样,需要经过设计、施工、装修、验收等多个阶段。编程只是其中的一个环节,让我们来详细了解程序诞生的整个过程。
- 第一阶段:编程(Programming)- 用代码描述解决方案
什么是编程? 编程就是用计算机能理解的语言来描述解决问题的方法。这就像用中文写作文一样,你心里有想法,但需要用文字把想法表达出来。编程也是这样,你知道怎么解决问题,但需要用编程语言把解决方法"写"出来。
编程的具体过程
让我们用一个简单的例子来理解编程过程。假设我们要编写一个程序,计算圆的面积:
步骤1:分析问题
步骤2:设计解决方案
步骤3:编写代码
plaintext
#include
int main() {
float radius, area;
const float PI = 3.14159;
// 提示用户输入
printf("请输入圆的半径:");
// 读取用户输入
scanf("%f", &radius);
// 计算面积
area = PI * radius * radius;
// 输出结果
printf("圆的面积是:%.2f\n", area);
return 0;
}
- 第二阶段:编译(Compilation)- 翻译成计算机语言
为什么需要编译? 我们写的C语言代码就像用中文写的说明书,但计算机只能理解机器语言(0和1组成的代码)。编译就是把中文说明书翻译成计算机能理解的"外星语"的过程。
编译的详细过程
编译过程其实包含几个步骤,就像翻译一本书需要经过初稿、校对、润色等多个环节:
这是编译的第一步,预处理器会处理所有以#开头的指令。比如:
就像写作文前先准备好所有需要的资料和素材。
编译器把预处理后的C语言代码翻译成汇编语言。汇编语言比机器语言容易理解一些,但仍然很接近硬件。这就像把中文先翻译成英文,为进一步翻译做准备。
汇编器把汇编语言翻译成机器语言,生成目标文件(.obj或.o文件)。这就像把英文翻译成计算机能理解的"外星语"。
链接器把多个目标文件和系统库文件组合成一个完整的可执行文件。这就像把翻译好的各个章节装订成一本完整的书。
编译工具的使用
在实际开发中,我们通常使用集成开发环境(IDE)来简化编译过程:
命令行编译: 如果你使用GCC编译器,编译过程可能是这样的:
plaintext
gcc -o circle_area circle_area.c
这条命令告诉GCC编译器:把circle_area.c编译成名为circle_area的可执行文件。
IDE编译: 如果你使用开发环境如Dev-C++、Code::Blocks等,通常只需要按F9键或点击"编译并运行"按钮,IDE会自动完成整个编译过程。
编译过程中可能遇到的问题
这就像写作文时的错别字或语法错误。比如忘记写分号、括号不匹配等。编译器会告诉你错误的位置,你需要修改后重新编译。
这通常是因为找不到某个函数的定义,或者缺少必要的库文件。就像写书时引用了某个资料,但在参考文献中找不到这个资料。
警告不会阻止编译,但提醒你代码中可能存在问题。就像老师批改作文时的建议,虽然不是错误,但最好改正。
- 第三阶段:执行(Execution)- 程序开始工作
什么是程序执行? 编译完成后,我们得到了一个可执行文件,但它还只是静静地躺在硬盘上。程序执行就是让这个"沉睡"的程序"苏醒"过来,开始工作。
执行过程的详细步骤
当你双击可执行文件时,操作系统会把程序从硬盘加载到内存中。这就像把一本书从书架上取下来,打开准备阅读。
操作系统会为程序分配内存空间,包括:
代码段:存储程序的指令数据段:存储全局变量和静态变量堆:用于动态分配内存栈:用于存储局部变量和函数调用信息
操作系统会为程序创建一个进程,分配一个进程ID(PID),并在进程表中记录相关信息。这就像给每个正在做菜的厨师分配一个工作台和工具。
CPU开始执行程序的指令。对于我们的圆面积计算程序:
首先执行printf("请输入圆的半径:");,在屏幕上显示提示信息然后执行scanf("%f", &radius);,等待用户输入用户输入数据后,执行area = PI * radius * radius;进行计算最后执行printf("圆的面积是:%.2f\n", area);显示结果
- 调试(Debugging)- 发现和修复错误
程序很少能一次性完美运行,通常需要经过调试过程来发现和修复错误: