硬件嵌入式学习路线大总结(一):C语言与linux。内功心法——从入门到精通,彻底打通你的任督二脉!

嵌入式工程师学习路线大总结(一)

引言:C语言——嵌入式领域的“屠龙宝刀”!

兄弟们,如果你想在嵌入式领域闯出一片天地,C语言就是你手里那把最锋利的“屠龙宝刀”!它不像Python那样优雅,不像Java那样“全能”,但它在嵌入式领域,就是绝对的王者

为什么?因为嵌入式系统资源有限,对性能和实时性要求极高。C语言以其贴近硬件、执行效率高、内存控制灵活的特点,完美契合了嵌入式开发的这些需求。操作系统内核、设备驱动、底层固件、实时操作系统(RTOS)……这些核心组件,几乎清一色都是用C语言编写的。

所以,想要成为一名合格的嵌入式工程师,C语言的功底必须炉火纯青!它不是你学完语法就能放下的工具,而是需要你不断深入、反复打磨的“内功心法”!

你可能会问:

  • C语言那么多知识点,怎么学才能学透?

  • 指针、内存管理这些“拦路虎”,怎么才能彻底搞懂?

  • 刷牛客力扣的算法题,C语言怎么才能写得又快又好?

  • 怎么才能把C语言学到“嵌入式级别”?

今天,我就带你彻底理清C语言的学习脉络,从最基础的概念到最核心的进阶技巧,再到如何用C语言征服算法面试题,帮你彻底打通C语言的“任督二脉”!

第一阶段:C语言基础——构建你的“地基”!(建议2-4周)

“万丈高楼平地起”,C语言的基础就是你嵌入式大厦的“地基”。这个阶段,你要把基本概念搞得清清楚楚,明明白白,不能有半点含糊!

1.1 数据类型、变量与常量:数据的“身份证”

  • 核心概念:理解C语言中各种数据类型(int, char, float, double, void 等)的存储大小、取值范围和用途。

  • 变量:如何声明、初始化和使用变量。理解变量的作用域(局部变量、全局变量)。

  • 常量:字面常量、const 修饰的常量、宏常量(#define)。

  • 逻辑分析:每种数据类型在内存中占据多少字节?为什么会有带符号和无符号类型?数据溢出是怎么回事?

数据类型

字节数(典型)

取值范围(典型)

用途

char

1

-128 ~ 127 或 0 ~ 255

字符、小整数

short

2

-32768 ~ 32767

短整数

int

4

-2,147,483,648 ~ 2,147,483,647

整数,最常用

long

4 或 8

更大范围整数

大整数

float

4

约 pm3.4times1038

单精度浮点数

double

8

约 pm1.7times10308

双精度浮点数,精度更高

代码示例:数据类型、变量与常量

#include <stdio.h> // 引入标准输入输出库,用于printf函数// 1. 全局变量:在所有函数之外定义,整个程序生命周期内都存在,默认初始化为0
int global_int_var = 100; // 2. 宏常量:在预处理阶段进行文本替换,没有类型,不占用内存空间
#define MAX_VALUE 255 
#define PI 3.1415926// 3. 枚举常量:一组具名整数常量,提高代码可读性
enum Color {RED,   // 默认从0开始GREEN, // 1BLUE   // 2
};int main() {// 局部变量:在函数内部定义,只在函数执行期间存在,未初始化时值为随机(垃圾值)int local_int_var; // 未初始化,值不确定local_int_var = 50; // 赋值// 使用const修饰的常量:具有类型,占用内存空间,但其值不能被修改const float GRAVITY = 9.8f; // f表示float类型字面量// 字符类型char my_char = 'A';char ascii_val = 65; // 'A' 的ASCII码// 浮点类型float temperature = 25.5f;double big_number = 1.23456789012345;printf("--- 数据类型、变量与常量示例 ---\n");printf("全局整数变量 global_int_var: %d\n", global_int_var);printf("局部整数变量 local_int_var: %d\n", local_int_var);printf("const浮点常量 GRAVITY: %.2f\n", GRAVITY); // %.2f表示保留两位小数printf("宏常量 MAX_VALUE: %d\n", MAX_VALUE);printf("宏常量 PI: %f\n", PI);printf("字符变量 my_char: %c (ASCII: %d)\n", my_char, my_char);printf("ASCII值变量 ascii_val: %c (ASCII: %d)\n", ascii_val, ascii_val);printf("浮点变量 temperature: %.1f\n", temperature);printf("双精度浮点变量 big_number: %.15lf\n", big_number); // .15lf表示保留15位小数,long float// 枚举常量使用enum Color current_color = RED;printf("枚举常量 RED 的值为: %d\n", current_color); // 输出 0// 尝试修改const常量,会引发编译错误// GRAVITY = 10.0f; // 编译错误: assignment of read-only variable 'GRAVITY'printf("\n--- 变量作用域示例 ---\n");{ // 这是一个新的代码块,块内定义的变量只在块内可见int block_var = 10;printf("块内变量 block_var: %d\n", block_var);// 块内可以访问外部变量printf("块内访问局部变量 local_int_var: %d\n", local_int_var);} // 块结束,block_var被销毁// printf("块外访问 block_var: %d\n", block_var); // 编译错误: 'block_var' undeclaredreturn 0; // 程序正常退出
}

1.2 运算符与表达式:数据的“加工厂”

  • 核心概念:算术运算符(+, -, *, /, %)、关系运算符(==, !=, >, <, >=, <=)、逻辑运算符(&&, ||, !)、位运算符(&, |, ^, ~, <<, >>)、赋值运算符(=, += 等)、条件运算符(? :)、逗号运算符(,)、sizeof 运算符。

  • 优先级与结合性:理解运算符的优先级和结合性,避免写出有歧义的代码。

  • 类型转换:隐式类型转换和强制类型转换。

  • 逻辑分析:不同运算符如何影响数据?位运算在嵌入式中有什么特殊用途?为什么要注意整数除法和浮点数比较?

(这里想象一个表格,列出C语言常见运算符的优先级和结合性,从高到低)

代码示例:运算符与表达式

#include <stdio.h>int main() {int a = 10, b = 3;int result;printf("--- 算术运算符 ---\n");result = a + b; // 加法printf("a + b = %d\n", result); // 13result = a - b; // 减法printf("a - b = %d\n", result); // 7result = a * b; // 乘法printf("a * b = %d\n", result); // 30result = a / b; // 整数除法,结果截断小数部分printf("a / b = %d\n", result); // 3result = a % b; // 取模,余数printf("a %% b = %d\n", result); // 1printf("\n--- 关系运算符 ---\n");printf("a == b : %d\n", a == b); // 0 (假)printf("a != b : %d\n", a != b); // 1 (真)printf("a > b  : %d\n", a > b);  // 1 (真)printf("a < b  : %d\n", a < b);  // 0 (假)printf("a >= b : %d\n", a >= b); // 1 (真)printf("a <= b : %d\n", a <= b); // 0 (假)printf("\n--- 逻辑运算符 ---\n");// C语言中,非0为真,0为假int x = 5, y = 0;printf("x && b : %d\n", x && b); // 1 (真,5 && 3)printf("x || y : %d\n", x || y); // 1 (真,5 || 0)printf("!y     : %d\n", !y);     // 1 (真,!0)printf("!x     : %d\n", !x);     // 0 (假,!5)printf("\n--- 位运算符 (嵌入式重点) ---\n");// 假设 a = 10 (0000 1010), b = 3 (0000 0011)result = a & b; // 按位与: 0000 0010 (2)printf("a & b = %d\n", result);result = a | b; // 按位或: 0000 1011 (11)printf("a | b = %d\n", result);result = a ^ b; // 按位异或: 0000 1001 (9)printf("a ^ b = %d\n", result);result = ~a;    // 按位非: 1111 0101 (-11,补码表示)printf("~a = %d\n", result);result = a << 1; // 左移1位: 0001 0100 (20) (相当于乘以2)printf("a << 1 = %d\n", result);result = a >> 1; // 右移1位: 0000 0101 (5) (相当于除以2)printf("a >> 1 = %d\n", result);printf("\n--- 赋值运算符 ---\n");int c = 20;c += 5; // c = c + 5;printf("c += 5 : %d\n", c); // 25c -= 3; // c = c - 3;printf("c -= 3 : %d\n", c); // 22c *= 2; // c = c * 2;printf("c *= 2 : %d\n", c); // 44c /= 4; // c = c / 4;printf("c /= 4 : %d\n", c); // 11c %= 3; // c = c % 3;printf("c %%= 3 : %d\n", c); // 2printf("\n--- 自增/自减运算符 ---\n");int i = 5;printf("i++ : %d (i变为%d)\n", i++, i); // 先使用i的值(5),再i自增(6)i = 5;printf("++i : %d (i变为%d)\n", ++i, i); // 先i自增(6),再使用i的值(6)int j = 10;printf("j-- : %d (j变为%d)\n", j--, j); // 先使用j的值(10),再j自减(9)j = 10;printf("--j : %d (j变为%d)\n", --j, j); // 先j自减(9),再使用j的值(9)printf("\n--- 条件运算符 (三目运算符) ---\n");int max = (a > b) ? a : b; // 如果a>b为真,max=a;否则max=bprintf("max(a, b) = %d\n", max); // 10printf("\n--- sizeof 运算符 ---\n");printf("sizeof(int) = %zu bytes\n", sizeof(int));printf("sizeof(float) = %zu bytes\n", sizeof(float));printf("sizeof(double) = %zu bytes\n", sizeof(double));printf("sizeof(char) = %zu bytes\n", sizeof(char));printf("sizeof(a) = %zu bytes\n", sizeof(a)); // sizeof可以用于变量名,返回变量类型的大小printf("\n--- 逗号运算符 ---\n");// 逗号运算符从左到右依次计算表达式,并返回最右边表达式的值int val = (printf("First expression\n"), 10 + 20, printf("Second expression\n"), 5 * 5);printf("逗号运算符结果: %d\n", val); // 25// 常用在for循环中同时初始化/更新多个变量printf("\n--- 类型转换 ---\n");float f_val = (float)a / b; // 强制类型转换,先将a转换为float,再进行浮点除法printf("(float)a / b = %f\n", f_val); // 10.0 / 3 = 3.333333return 0;
}

1.3 控制流语句:程序的“指挥棒”

  • 核心概念

    • 分支语句if-else if-elseswitch-case

    • 循环语句whiledo-whilefor

    • 跳转语句breakcontinuegoto

  • 逻辑分析:每种控制流语句的执行流程?如何选择合适的语句?breakcontinue 的区别?goto 的优缺点及在嵌入式中的有限使用场景?

(这里想象三个流程图:if-else、for循环、switch-case 的基本流程)

代码示例:控制流语句

#include <stdio.h>int main() {int score = 85;printf("--- if-else if-else 分支语句 ---\n");if (score >= 90) {printf("优秀!\n");} else if (score >= 70) {printf("良好!\n");} else if (score >= 60) {printf("及格!\n");} else {printf("不及格!\n");}int day = 3;printf("\n--- switch-case 分支语句 ---\n");switch (day) {case 1:printf("星期一\n");break; // 跳出switch语句case 2:printf("星期二\n");break;case 3:printf("星期三\n");// 注意:这里没有break,会“穿透”到下一个casecase 4:printf("星期四\n");break;default: // 所有case都不匹配时执行printf("未知日期\n");break;}printf("\n--- while 循环语句 ---\n");int i = 0;while (i < 5) {printf("while循环:i = %d\n", i);i++; // 每次循环i自增}printf("\n--- do-while 循环语句 ---\n");// do-while 循环至少执行一次,即使条件一开始就不满足int j = 5;do {printf("do-while循环:j = %d\n", j);j++;} while (j < 5); // 条件不满足,只执行一次printf("\n--- for 循环语句 ---\n");// for(初始化; 条件; 每次迭代后操作)for (int k = 0; k < 3; k++) {printf("for循环:k = %d\n", k);}printf("\n--- break 和 continue 语句 ---\n");printf("break 示例:\n");for (int m = 0; m < 10; m++) {if (m == 5) {printf("遇到5,跳出循环!\n");break; // 跳出当前最近的循环}printf("m = %d\n", m);}printf("continue 示例:\n");for (int n = 0; n < 5; n++) {if (n == 2) {printf("遇到2,跳过本次循环的剩余部分!\n");continue; // 跳过本次循环的剩余部分,直接进入下一次迭代}printf("n = %d\n", n);}printf("\n--- goto 语句 (慎用,但在嵌入式中偶有奇效) ---\n");// goto 语句可以无条件跳转到程序中的任何标签位置// 标签是一个标识符,后面跟一个冒号int val = 10;if (val > 5) {goto JUMP_POINT; // 跳转到JUMP_POINT标签}printf("这行代码不会被执行。\n"); // 这行代码会被跳过JUMP_POINT: // 标签定义printf("跳转到这里了!val = %d\n", val);// 典型的goto使用场景:错误处理或跳出多层嵌套循环// 示例:跳出多层循环 (比使用多个break更简洁)printf("\n--- goto 跳出多层循环示例 ---\n");for (int x = 0; x < 3; x++) {for (int y = 0; y < 3; y++) {if (x == 1 && y == 1) {printf("在内层循环中找到条件,跳出所有循环。\n");goto END_LOOPS;}printf("x = %d, y = %d\n", x, y);}}
END_LOOPS:printf("所有循环已结束。\n");return 0;
}

1.4 函数:代码的“模块化”利器

  • 核心概念:函数的定义、声明、调用。参数传递(值传递、地址传递)。返回值。

  • 函数原型:为什么需要函数原型?

  • 递归函数:理解递归的原理和应用场景。

  • 逻辑分析:函数如何提高代码复用性?值传递和地址传递的区别和应用?递归的栈开销和效率问题?

(这里想象一个函数调用栈的示意图,展示函数调用时栈帧的压入和弹出)

代码示例:函数

#include <stdio.h>// 函数声明 (Function Prototype): 告诉编译器函数的名称、参数类型和返回类型
// 这样,即使函数定义在main函数后面,编译器也能知道如何调用它
int add(int a, int b); // 声明一个名为add的函数,接收两个int参数,返回int
void print_message(const char* msg); // 声明一个名为print_message的函数,接收一个const char*参数,无返回值
long factorial(int n); // 声明一个计算阶乘的递归函数int main() {printf("--- 函数示例 ---\n");// 1. 函数调用:通过函数名和参数列表来执行函数int num1 = 10;int num2 = 20;int sum_result = add(num1, num2); // 调用add函数,进行值传递printf("调用 add(%d, %d) = %d\n", num1, num2, sum_result);print_message("Hello from function!"); // 调用print_message函数// 2. 递归函数调用int n = 5;long fact_result = factorial(n);printf("%d的阶乘是: %ld\n", n, fact_result);// 3. 值传递与地址传递 (概念,地址传递将在指针部分详细讲解)// 值传递:函数接收的是参数的副本,函数内部对参数的修改不会影响原始变量// 地址传递:函数接收的是参数的地址,通过地址可以修改原始变量的值 (使用指针实现)int val = 10;printf("\n--- 值传递示例 ---\n");printf("调用 change_value_by_value 前,val = %d\n", val);void change_value_by_value(int x); // 声明change_value_by_value(val); // 传递val的副本printf("调用 change_value_by_value 后,val = %d (未改变)\n", val);// 4. 函数指针 (高级用法,用于回调函数、状态机等)printf("\n--- 函数指针示例 ---\n");// 声明一个函数指针变量 ptr_add,它可以指向任何接收两个int参数并返回int的函数int (*ptr_add)(int, int); ptr_add = add; // 将add函数的地址赋值给函数指针int fp_result = ptr_add(5, 7); // 通过函数指针调用函数printf("通过函数指针调用 add(5, 7) = %d\n", fp_result);// 匿名函数 (C语言本身不支持,但可以通过宏或特定库模拟)// 嵌入式中常用函数指针实现回调机制,例如中断服务函数、定时器回调等。return 0;
}// 函数定义 (Function Definition): 实现函数的具体逻辑
int add(int a, int b) {// a和b是形参,是num1和num2的副本int sum = a + b;return sum; // 返回计算结果
}void print_message(const char* msg) {printf("消息: %s\n", msg);
}// 递归函数定义:计算n的阶乘 (n! = n * (n-1)!)
long factorial(int n) {// 递归终止条件:当n为0或1时,阶乘为1if (n == 0 || n == 1) {return 1;} else {// 递归调用:n * (n-1)的阶乘return n * factorial(n - 1);}
}// 值传递示例函数的定义
void change_value_by_value(int x) {x = 100; // 这里修改的是x的副本,不影响main函数中的valprintf("在函数内部,x = %d\n", x);
}

1.5 数组:同类型数据的“集合体”

  • 核心概念:一维数组、多维数组的声明、初始化和访问。数组在内存中的连续存储。

  • 数组与指针:数组名即首元素地址的特殊性(这是理解指针的关键一步)。

  • 字符串:字符数组与字符串(以 \0 结尾)。

  • 逻辑分析:数组的内存访问效率?为什么数组下标从0开始?如何避免数组越界?

(这里想象一个数组在内存中连续存储的示意图)

代码示例:数组

#include <stdio.h>
#include <string.h> // 引入字符串处理函数库,用于strlen, strcpy等int main() {printf("--- 数组示例 ---\n");// 1. 一维数组的声明与初始化// 方式一:指定大小并初始化int numbers[5] = {10, 20, 30, 40, 50}; // 方式二:不指定大小,编译器根据初始化列表推断大小int scores[] = {90, 85, 92, 78}; // 大小为4printf("numbers数组元素:\n");// 访问数组元素:通过下标,下标从0开始for (int i = 0; i < 5; i++) {printf("numbers[%d] = %d\n", i, numbers[i]);}printf("\nscores数组元素:\n");// sizeof(scores) 获取整个数组的字节大小// sizeof(scores[0]) 获取单个元素的大小// sizeof(scores) / sizeof(scores[0]) 计算数组元素个数for (int i = 0; i < sizeof(scores) / sizeof(scores[0]); i++) {printf("scores[%d] = %d\n", i, scores[i]);}// 2. 数组的内存连续性printf("\nnumbers数组的内存地址:\n");for (int i = 0; i < 5; i++) {printf("numbers[%d] 的地址: %p\n", i, (void*)&numbers[i]); // %p 用于打印地址}// 可以看到地址是连续递增的,每个int之间相差4个字节// 3. 多维数组 (以二维数组为例)// 声明一个3行4列的二维数组int matrix[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};printf("\nmatrix二维数组元素:\n");for (int row = 0; row < 3; row++) {for (int col = 0; col < 4; col++) {printf("%2d ", matrix[row][col]);}printf("\n");}// 4. 字符数组与字符串// 字符串在C语言中是特殊的字符数组,以空字符 '\0' 结尾char greeting[] = "Hello"; // 自动在末尾添加 '\0',实际大小为6char name[10] = {'W', 'o', 'r', 'l', 'd', '\0'}; // 显式添加 '\0'char city[20]; // 声明一个足够大的字符数组printf("\ngreeting: %s\n", greeting);printf("name: %s\n", name);printf("greeting的长度 (不包含\\0): %zu\n", strlen(greeting)); // strlen计算字符串长度// 字符串拷贝:不能直接用 = 赋值,要用 strcpy 或 strncpystrcpy(city, "Shanghai"); // 将"Shanghai"拷贝到city数组printf("city: %s\n", city);// 字符串连接:strcatstrcat(greeting, " C!"); // 将" C!"连接到greeting后面printf("连接后的greeting: %s\n", greeting); // "Hello C!"// 字符串比较:strcmpif (strcmp(greeting, "Hello C!") == 0) {printf("greeting 和 \"Hello C!\" 相同。\n");} else {printf("greeting 和 \"Hello C!\" 不同。\n");}// 5. 数组越界 (非常危险,可能导致程序崩溃或不可预测行为)// C语言不检查数组越界,访问越界内存是未定义行为// numbers[5] = 100; // 这是一个越界访问,numbers的有效下标是0-4// printf("尝试越界访问 numbers[5] = %d\n", numbers[5]);return 0;
}

第二阶段:C语言进阶——深入理解“内功心法”的精髓!(建议3-6周)

这个阶段是C语言学习的核心和难点,特别是指针和内存管理。学透了它们,你才能真正掌握C语言的“精髓”,为后续的嵌入式开发打下坚实的基础!

2.1 指针的奥秘:C语言的“灵魂”与“魔杖”

  • 核心概念

    • 什么是指针? 指针变量存储的是内存地址。

    • 指针的声明与初始化int *ptr;

    • 取地址运算符 &:获取变量的内存地址。

    • 解引用运算符 *:通过地址访问内存中的值。

    • 指针的类型:指针指向的数据类型决定了其解引用时访问的字节数。

    • 指针算术:指针的加减运算(按类型大小移动)。

    • 空指针 NULL:指向地址0的指针,表示不指向任何有效内存。

    • 野指针:指向不确定或无效内存地址的指针,非常危险。

    • void* 指针:通用指针,可以指向任何类型的数据,但不能直接解引用,需要强制类型转换。

    • 指针与数组:数组名就是首元素的地址,但数组名是常量指针。

    • 指针与字符串:字符串常量是字符数组的地址。

    • 指针与函数:函数参数的地址传递,函数指针(回调函数)。

    • 多级指针:指向指针的指针。

  • 逻辑分析

    • 为什么说指针是C语言的灵魂?它赋予了C语言直接操作内存的能力。

    • 指针的危险性在哪里?野指针、空指针解引用、内存泄漏等。

    • 如何安全地使用指针?初始化、检查NULL、及时释放内存。

    • 指针与数组的“异同”?它们在很多场景下可以互换,但本质不同。

(这里想象一个指针概念图:一个变量,它的内存地址,一个指针变量,它存储着那个变量的地址,以及解引用操作)

(这里想象一个数组和指针的对比图,展示数组名和指针变量的区别)

代码示例:指针的奥秘

#include <stdio.h>
#include <stdlib.h> // For NULLint main() {printf("--- 指针基础概念 ---\n");int num = 10;int *ptr_num; // 声明一个指向int类型的指针变量ptr_numptr_num = &num; // 使用&运算符获取num的地址,并赋值给ptr_numprintf("变量num的值: %d\n", num);printf("变量num的地址: %p\n", (void*)&num); // %p 用于打印地址printf("指针ptr_num存储的地址: %p\n", (void*)ptr_num);printf("通过指针解引用访问num的值: %d\n", *ptr_num); // 使用*运算符解引用指针// 修改通过指针修改变量的值*ptr_num = 20;printf("通过指针修改后,num的值: %d\n", num); // num的值变为20printf("\n--- 指针的类型与指针算术 ---\n");int arr[] = {10, 20, 30, 40, 50};int *p = arr; // 数组名arr就是首元素的地址,p指向arr[0]printf("p指向的元素: %d\n", *p); // 10printf("p的地址: %p\n", (void*)p);p++; // 指针加1,p会向后移动sizeof(int)个字节,指向下一个int元素printf("p++ 后指向的元素: %d\n", *p); // 20printf("p++ 后p的地址: %p\n", (void*)p);char char_arr[] = {'A', 'B', 'C', 'D', '\0'};char *cp = char_arr;printf("cp指向的字符: %c\n", *cp); // Aprintf("cp的地址: %p\n", (void*)cp);cp++; // 指针加1,cp会向后移动sizeof(char)个字节,指向下一个char元素printf("cp++ 后指向的字符: %c\n", *cp); // Bprintf("cp++ 后cp的地址: %p\n", (void*)cp);printf("\n--- 空指针与野指针 ---\n");int *null_ptr = NULL; // 空指针,不指向任何有效内存// printf("*null_ptr = %d\n", *null_ptr); // 危险!解引用空指针会导致程序崩溃 (段错误)int *wild_ptr; // 野指针,未初始化,指向未知地址// printf("*wild_ptr = %d\n", *wild_ptr); // 危险!解引用野指针会导致程序崩溃或不可预测行为// 避免野指针:声明时初始化为NULL,或指向有效地址int *safe_ptr = NULL;int data = 100;safe_ptr = &data; // 指向有效地址printf("safe_ptr指向的值: %d\n", *safe_ptr);printf("\n--- void* 指针 (通用指针) ---\n");int i_val = 123;float f_val = 45.6f;void *v_ptr; // void* 可以指向任何类型v_ptr = &i_val;printf("void* 指向 int: %d\n", *(int*)v_ptr); // 必须强制类型转换为int*才能解引用v_ptr = &f_val;printf("void* 指向 float: %f\n", *(float*)v_ptr); // 必须强制类型转换为float*才能解引用printf("\n--- 指针与数组 (深入理解) ---\n");int my_array[3] = {100, 200, 300};int *ptr_array = my_array; // 数组名作为指针printf("my_array[0] = %d, *ptr_array = %d\n", my_array[0], *ptr_array);printf("my_array[1] = %d, *(ptr_array + 1) = %d\n", my_array[1], *(ptr_array + 1));printf("my_array[2] = %d, ptr_array[2] = %d\n", my_array[2], ptr_array[2]); // 指针也可以用数组下标方式访问// 数组名是常量指针,不能被赋值// my_array = ptr_array; // 编译错误printf("\n--- 指针与字符串 ---\n");char *str_ptr = "Hello, Pointer!"; // 字符串字面量存储在只读数据区printf("字符串指针: %s\n", str_ptr);// str_ptr[0] = 'h'; // 运行时错误,不能修改字符串字面量char str_array[] = "Hello, Array!"; // 字符数组,可修改printf("字符数组: %s\n", str_array);str_array[0] = 'h';printf("修改后的字符数组: %s\n", str_array);printf("\n--- 多级指针 (指向指针的指针) ---\n");int value = 777;int *p1 = &value;    // p1 指向 valueint **p2 = &p1;      // p2 指向 p1int ***p3 = &p2;     // p3 指向 p2printf("value = %d\n", value);      // 777printf("*p1 = %d\n", *p1);          // 777printf("**p2 = %d\n", **p2);        // 777printf("***p3 = %d\n", ***p3);      // 777// 通过多级指针修改原始值***p3 = 888;printf("通过p3修改后,value = %d\n", value); // 888printf("\n--- 指针与函数 (地址传递) ---\n");void swap(int *x, int *y); // 声明一个交换两个整数值的函数int val_a = 10, val_b = 20;printf("交换前: val_a = %d, val_b = %d\n", val_a, val_b);swap(&val_a, &val_b); // 传递变量的地址printf("交换后: val_a = %d, val_b = %d\n", val_a, val_b);return 0;
}// 交换两个整数值的函数定义 (通过指针实现地址传递)
void swap(int *x, int *y) {int temp = *x; // 解引用x,获取x指向的值*x = *y;       // 解引用y,获取y指向的值,并赋值给x指向的位置*y = temp;     // 将temp的值赋值给y指向的位置
}

2.2 内存管理:程序的“生命线”

  • 核心概念

    • 内存分区

      • 栈区(Stack):局部变量、函数参数、函数返回地址等,由编译器自动分配和释放,速度快,但空间有限。

      • 堆区(Heap):动态内存分配(malloc/calloc/realloc/free),由程序员手动管理,空间大,但速度相对慢,容易产生内存泄漏和碎片。

      • 全局/静态区(Global/Static Data Segment):全局变量、静态变量,程序启动时分配,程序结束时释放。

      • 代码区(Text Segment):存放可执行代码,通常是只读的。

    • 动态内存分配malloc (分配指定大小内存)、calloc (分配并清零)、realloc (重新调整大小)、free (释放内存)。

    • 内存泄漏:分配的内存未被释放,导致内存占用持续增加。

    • 内存溢出:程序尝试访问超出其分配范围的内存。

    • 内存碎片:频繁分配和释放小块内存导致内存空间不连续。

  • 逻辑分析

    • 为什么需要动态内存?在编译时无法确定所需内存大小的场景。

    • mallocfree 必须配对使用,否则后果很严重!

    • 如何避免内存泄漏?“谁分配,谁释放”原则,错误处理中的内存释放。

    • 嵌入式中内存管理的重要性?资源有限,内存泄漏可能导致系统崩溃。

(这里想象一个C程序内存布局图:代码区、数据区(全局/静态)、堆区、栈区)

代码示例:内存管理

#include <stdio.h>
#include <stdlib.h> // 引入动态内存分配函数库:malloc, free, calloc, realloc
#include <string.h> // 用于字符串操作// 全局变量:存储在全局/静态区,程序启动时分配,程序结束时释放
int global_data = 100;
static int static_global_data = 200; // 静态全局变量void demonstrate_memory_areas() {// 局部变量:存储在栈区,函数调用时分配,函数返回时释放int local_var = 10;char local_arr[20] = "Hello Stack!";// 静态局部变量:存储在全局/静态区,只初始化一次,生命周期与程序相同static int static_local_var = 30;printf("--- 内存分区示例 ---\n");printf("代码区(函数地址):%p\n", (void*)demonstrate_memory_areas); // 函数本身在代码区printf("全局变量 global_data 地址:%p\n", (void*)&global_data);printf("静态全局变量 static_global_data 地址:%p\n", (void*)&static_global_data);printf("局部变量 local_var 地址:%p\n", (void*)&local_var);printf("局部数组 local_arr 地址:%p\n", (void*)local_arr);printf("静态局部变量 static_local_var 地址:%p\n", (void*)&static_local_var);// 注意:栈区地址通常是递减的(向下增长),堆区地址通常是递增的(向上增长)// 但这取决于具体的系统和编译器实现
}int main() {demonstrate_memory_areas();printf("\n--- 动态内存分配 (堆区) ---\n");// 1. malloc:分配指定字节数的内存,返回void*指针,不初始化内容int *ptr_int = (int *)malloc(sizeof(int)); // 分配一个int大小的内存if (ptr_int == NULL) { // 检查是否分配成功,非常重要!fprintf(stderr, "内存分配失败!\n");return 1; // 返回非零表示程序异常退出}*ptr_int = 500; // 写入数据printf("malloc分配的int值:%d (地址:%p)\n", *ptr_int, (void*)ptr_int);// 2. calloc:分配指定数量和大小的内存,并初始化所有位为0int *ptr_array = (int *)calloc(5, sizeof(int)); // 分配5个int大小的内存,并清零if (ptr_array == NULL) {fprintf(stderr, "内存分配失败!\n");free(ptr_int); // 释放之前分配的内存return 1;}printf("calloc分配的数组(初始值):");for (int i = 0; i < 5; i++) {printf("%d ", ptr_array[i]); // 应该都是0}printf("\n");// 3. realloc:重新调整已分配内存的大小// 假设我们现在需要10个int的空间int *new_ptr_array = (int *)realloc(ptr_array, 10 * sizeof(int));if (new_ptr_array == NULL) {fprintf(stderr, "内存重新分配失败!\n");free(ptr_int);free(ptr_array); // realloc失败时,原ptr_array仍然有效return 1;}ptr_array = new_ptr_array; // 更新指针,旧的ptr_array可能已失效printf("realloc后数组大小变为10,前5个元素:");for (int i = 0; i < 5; i++) {printf("%d ", ptr_array[i]); // 前5个元素内容不变}printf("\n");// 4. free:释放动态分配的内存,将其归还给系统free(ptr_int); // 释放单个int内存ptr_int = NULL; // 最佳实践:释放后将指针置为NULL,避免野指针free(ptr_array); // 释放数组内存ptr_array = NULL; // 最佳实践:释放后将指针置为NULLprintf("动态内存已释放。\n");printf("\n--- 内存泄漏示例 (错误示范) ---\n");int *leaked_ptr = (int *)malloc(sizeof(int));if (leaked_ptr == NULL) { /* handle error */ }*leaked_ptr = 999;// 这里没有调用 free(leaked_ptr);// 当函数结束时,leaked_ptr这个局部变量会销毁,但它指向的内存没有被释放// 这块内存就“丢失”了,直到程序结束才被操作系统回收,造成内存泄漏。printf("故意制造内存泄漏:leaked_ptr指向的内存未释放。\n");printf("\n--- 内存溢出示例 (错误示范) ---\n");char buffer[10]; // 大小为10的字符数组// strcpy(buffer, "This is a very long string that will cause buffer overflow.");// 上面这行代码会尝试向buffer写入超过10个字节的数据,导致缓冲区溢出// 这可能覆盖相邻内存,导致程序崩溃、数据损坏或安全漏洞。printf("避免缓冲区溢出:使用strncpy并注意大小限制。\n");strncpy(buffer, "Short", sizeof(buffer) - 1); // 拷贝时限制大小,并为'\0'留出空间buffer[sizeof(buffer) - 1] = '\0'; // 确保字符串以空字符结尾printf("安全拷贝后的buffer: %s\n", buffer);return 0;
}

2.3 结构体、共用体与枚举:数据的“组织者”

  • 核心概念

    • 结构体(struct:将不同类型的数据组合成一个自定义的数据类型。理解结构体成员的访问(. 运算符和 -> 运算符)。

    • 结构体数组:结构体的集合。

    • 结构体指针:指向结构体的指针。

    • 结构体嵌套:结构体成员可以是另一个结构体。

    • 结构体大小与内存对齐:理解编译器如何对结构体成员进行内存对齐,以及这如何影响结构体实际占用的大小(sizeof)。

    • 位域(Bit Fields):在结构体中以位为单位定义成员,节省内存,常用于嵌入式中对寄存器或协议字段的精确控制。

    • 共用体(union:所有成员共享同一块内存空间,同一时间只有一个成员有效。

    • 枚举(enum:定义一组具名的整数常量,提高代码可读性和可维护性。

  • 逻辑分析

    • 结构体和共用体的区别?何时使用?

    • 内存对齐的原理和作用?为什么需要对齐?如何影响 sizeof

    • 位域在嵌入式中的优势?如何节省内存?

(这里想象一个结构体内存布局图,展示成员、填充字节(padding)和总大小)

(这里想象一个共用体内存布局图,展示所有成员共享同一块内存)

代码示例:结构体、共用体与枚举

#include <stdio.h>
#include <string.h> // For strcpy// 1. 结构体 (struct): 将不同类型的数据组合成一个自定义类型
// 定义一个表示学生信息的结构体
struct Student {int id;          // 学号char name[50];   // 姓名float score;     // 成绩
};// 2. 结构体嵌套: 一个结构体作为另一个结构体的成员
struct Date {int year;int month;int day;
};struct Course {char course_name[30];struct Date start_date; // 嵌套Date结构体int credits;
};// 3. 结构体位域 (Bit Fields): 精确控制成员占用的位数,节省内存,常用于嵌入式
// 假设我们需要表示一个设备的配置寄存器
struct DeviceConfig {unsigned int enable : 1;    // 1位,表示启用/禁用unsigned int mode : 2;      // 2位,表示工作模式 (0-3)unsigned int status : 4;    // 4位,表示状态 (0-15)unsigned int reserved : 25; // 剩余位用于填充或保留
};int main() {printf("--- 结构体示例 ---\n");// 结构体变量的声明与初始化struct Student s1; // 声明一个Student类型的变量s1// 访问结构体成员:使用 . 运算符s1.id = 1001;strcpy(s1.name, "张三"); // 字符串拷贝s1.score = 95.5f;printf("学生信息:\n");printf("学号: %d\n", s1.id);printf("姓名: %s\n", s1.name);printf("成绩: %.1f\n", s1.score);// 结构体数组: 存储多个结构体变量struct Student class_students[2];class_students[0] = s1; // 将s1赋值给数组第一个元素class_students[1].id = 1002;strcpy(class_students[1].name, "李四");class_students[1].score = 88.0f;printf("\n班级学生信息:\n");for (int i = 0; i < 2; i++) {printf("学生 %d: ID=%d, Name=%s, Score=%.1f\n", i + 1, class_students[i].id, class_students[i].name, class_students[i].score);}// 结构体指针: 指向结构体的指针struct Student *ptr_s1 = &s1; // ptr_s1指向s1的地址// 通过结构体指针访问成员:使用 -> 运算符printf("\n通过结构体指针访问s1:\n");printf("学号: %d\n", ptr_s1->id);printf("姓名: %s\n", ptr_s1->name);printf("成绩: %.1f\n", ptr_s1->score);printf("\n--- 结构体嵌套示例 ---\n");struct Course c1 = {"C语言基础",{2024, 3, 1}, // 初始化嵌套的Date结构体4};printf("课程名称: %s\n", c1.course_name);printf("开课日期: %d-%02d-%02d\n", c1.start_date.year, c1.start_date.month, c1.start_date.day);printf("学分: %d\n", c1.credits);printf("\n--- 结构体大小与内存对齐 ---\n");printf("sizeof(struct Student) = %zu bytes\n", sizeof(struct Student));printf("sizeof(struct Date) = %zu bytes\n", sizeof(struct Date));printf("sizeof(struct Course) = %zu bytes\n", sizeof(struct Course));// 实际大小可能大于成员大小之和,因为内存对齐printf("\n--- 结构体位域示例 (嵌入式重点) ---\n");struct DeviceConfig config;config.enable = 1;      // 设置enable位为1config.mode = 2;        // 设置mode位为2config.status = 10;     // 设置status位为10config.reserved = 0;    // 保留位清零printf("设备配置: Enable=%u, Mode=%u, Status=%u\n", config.enable, config.mode, config.status);printf("sizeof(struct DeviceConfig) = %zu bytes\n", sizeof(struct DeviceConfig));// 尽管总共只有1+2+4=7位有效数据,但由于位域通常以一个整数类型(如unsigned int)作为底层存储单元,// 并且编译器可能会进行填充,所以实际大小通常是4字节或8字节,而不是1字节。// 在这里,25+4+2+1 = 32位,正好是一个unsigned int的大小,所以通常是4字节。printf("\n--- 共用体 (union): 成员共享同一块内存 ---\n");// 定义一个共用体,可以存储int、float或char数组union Data {int i;float f;char str[20];};union Data data;printf("sizeof(union Data) = %zu bytes\n", sizeof(union Data)); // 大小是最大成员的大小 (str[20] -> 20字节)data.i = 123;printf("设置int后: data.i = %d\n", data.i);// data.f 和 data.str 的内容现在是垃圾值,因为它们共享内存data.f = 45.6f;printf("设置float后: data.f = %f\n", data.f);printf("此时 data.i 的值可能已变乱: %d\n", data.i); // 乱码或不符合预期strcpy(data.str, "Hello Union!");printf("设置string后: data.str = %s\n", data.str);printf("此时 data.i 的值可能已变乱: %d\n", data.i);printf("此时 data.f 的值可能已变乱: %f\n", data.f);printf("\n--- 枚举 (enum): 定义具名整数常量 ---\n");// 定义一个表示一周的枚举enum Weekday {MONDAY = 1, // 可以指定起始值TUESDAY,    // 默认递增,为2WEDNESDAY,  // 3THURSDAY,   // 4FRIDAY,     // 5SATURDAY,   // 6SUNDAY      // 7};enum Weekday today = WEDNESDAY;printf("今天的值是: %d\n", today); // 输出 3if (today == WEDNESDAY) {printf("今天是星期三!\n");}// 枚举的底层是整数,可以进行整数运算,但不建议printf("MONDAY + TUESDAY = %d\n", MONDAY + TUESDAY); // 1 + 2 = 3return 0;
}

2.4 预处理与宏:代码的“魔法师”

  • 核心概念

    • 宏定义(#define:文本替换,无类型检查。理解宏的优缺点,特别是宏函数(带参数的宏)的陷阱(如优先级问题)。

    • 文件包含(#include:将其他文件的内容插入到当前文件中。理解头文件保护(#ifndef/#define/#endif#pragma once)。

    • 条件编译(#if/#ifdef/#ifndef/#elif/#else/#endif:根据条件选择性地编译代码。常用于跨平台、调试代码、功能开关。

  • 逻辑分析

    • 宏和函数的区别?宏的优势(无函数调用开销)和劣势(无类型检查、可能产生副作用)。

    • 头文件保护的重要性?避免重复包含导致编译错误。

    • 条件编译在嵌入式中的广泛应用?(例如,不同芯片平台、不同调试级别)

代码示例:预处理与宏

#include <stdio.h> // 引入标准输入输出库
#include "my_header.h" // 引入自定义头文件,假设存在// 1. 宏定义 (Macro Definition)
// 简单宏:文本替换
#define MAX_BUFFER_SIZE 1024
#define AUTHOR "嵌入式老司机"// 带参数的宏 (宏函数):看起来像函数,但只是文本替换
// 注意宏的陷阱:优先级问题,建议使用括号包裹参数和整个表达式
#define SQUARE(x) ((x) * (x)) // 推荐写法,避免优先级问题
#define ADD(a, b) (a + b)     // 错误示范,可能导致优先级问题// 宏的副作用示例
#define INCREMENT_AND_PRINT(x) (printf("Incrementing %d\n", x++), x)// 2. 条件编译 (Conditional Compilation)
// 根据宏是否定义来选择编译代码
#define DEBUG_MODE // 定义DEBUG_MODE宏,表示处于调试模式#ifdef DEBUG_MODE // 如果定义了DEBUG_MODE宏#define LOG_LEVEL "DEBUG"#define DEBUG_PRINT(fmt, ...) printf("[DEBUG] " fmt, ##__VA_ARGS__) // 可变参数宏
#else // 否则#define LOG_LEVEL "RELEASE"#define DEBUG_PRINT(fmt, ...) // 空宏,在发布模式下不输出调试信息
#endif// 根据宏的值进行条件编译
#define VERSION 2 // 定义版本号#if VERSION == 1#define FEATURE_A_ENABLED
#elif VERSION == 2#define FEATURE_B_ENABLED
#else#error "未知版本号!" // 编译错误,用于强制检查
#endif// 3. 预定义宏 (Predefined Macros)
// 编译器内置的宏,提供编译信息
// __FILE__: 当前源文件名
// __LINE__: 当前行号
// __DATE__: 编译日期
// __TIME__: 编译时间
// __STDC__: 如果编译器遵循ANSI C标准,则为1int main() {printf("--- 宏定义示例 ---\n");printf("最大缓冲区大小: %d\n", MAX_BUFFER_SIZE);printf("作者: %s\n", AUTHOR);int val = 5;printf("SQUARE(%d) = %d\n", val, SQUARE(val)); // 5 * 5 = 25// 宏的优先级陷阱示例// 期望: (10 + 20) * 2 = 60// 实际: 10 + 20 * 2 = 50 (因为宏只是文本替换,没有括号保护)printf("ADD(10, 20) * 2 = %d\n", ADD(10, 20) * 2); // 10 + 20 * 2 = 50// 宏的副作用示例int x = 10;int y = INCREMENT_AND_PRINT(x); // x会自增两次:一次在printf中,一次在宏的返回表达式中printf("y = %d, x = %d\n", y, x); // y=11, x=12 (取决于编译器对逗号表达式的优化)// 建议:避免在宏中使用有副作用的表达式printf("\n--- 条件编译示例 ---\n");printf("当前日志级别: %s\n", LOG_LEVEL);DEBUG_PRINT("这是一个调试信息,当前x = %d\n", x); // 只有在DEBUG_MODE下才会打印#ifdef FEATURE_A_ENABLEDprintf("特性A已启用!\n");#endif#ifdef FEATURE_B_ENABLEDprintf("特性B已启用!\n");#endifprintf("\n--- 预定义宏示例 ---\n");printf("当前文件: %s\n", __FILE__);printf("当前行号: %d\n", __LINE__);printf("编译日期: %s\n", __DATE__);printf("编译时间: %s\n", __TIME__);#ifdef __STDC__printf("编译器遵循ANSI C标准。\n");#elseprintf("编译器不完全遵循ANSI C标准。\n");#endifreturn 0;
}// my_header.h (假设存在,用于演示文件包含)
/*
#ifndef MY_HEADER_H
#define MY_HEADER_H// 可以在头文件中定义宏、声明函数、定义结构体等
#define MY_CONSTANT 123#endif // MY_HEADER_H
*/

2.5 文件I/O:与外部世界交互的“桥梁”

  • 核心概念

    • 文件流FILE* 指针。

    • 文件打开与关闭fopen() (打开文件)、fclose() (关闭文件)。

    • 文件读写

      • 字符I/Ofputc() (写字符)、fgetc() (读字符)。

      • 行I/Ofgets() (读行)、fputs() (写行)。

      • 格式化I/Ofprintf() (格式化写入)、fscanf() (格式化读取)。

      • 块I/Ofread() (读块)、fwrite() (写块),常用于二进制文件。

    • 文件定位fseek() (移动文件指针)、ftell() (获取当前位置)、rewind() (重置到文件开头)。

    • 错误处理:检查 fopen 返回值、feof() (文件结束)、ferror() (读写错误)。

  • 逻辑分析

    • 文本文件和二进制文件的区别?

    • 文件I/O的缓冲机制?

    • 在嵌入式中,文件I/O可能涉及到Flash、SD卡等存储介质的操作,需要理解文件系统。

(这里想象一个文件I/O的流程图:打开文件 -> 读写操作 -> 关闭文件)

代码示例:文件I/O

#include <stdio.h>  // 引入标准输入输出库,用于文件操作
#include <stdlib.h> // 用于EXIT_FAILURE
#include <string.h> // 用于strcpyint main() {FILE *fp = NULL; // 声明一个文件指针,并初始化为NULLconst char *text_filename = "example.txt";const char *binary_filename = "binary_data.bin";printf("--- 文本文件写入示例 ---\n");// "w" 模式:写入模式,如果文件不存在则创建,如果文件存在则清空内容fp = fopen(text_filename, "w"); if (fp == NULL) { // 检查文件是否成功打开fprintf(stderr, "无法打开文件 %s 进行写入!\n", text_filename);return EXIT_FAILURE;}// 1. 写入字符fputc('H', fp);fputc('e', fp);fputc('l', fp);fputc('l', fp);fputc('o', fp);fputc('\n', fp); // 写入换行符// 2. 写入字符串fputs("World from fputs!\n", fp);// 3. 格式化写入int num = 123;float pi = 3.14159f;fprintf(fp, "这是一个格式化写入的数字:%d,圆周率:%.2f\n", num, pi);fclose(fp); // 关闭文件,释放资源printf("文本文件 '%s' 写入完成。\n", text_filename);printf("\n--- 文本文件读取示例 ---\n");// "r" 模式:读取模式,如果文件不存在则返回NULLfp = fopen(text_filename, "r");if (fp == NULL) {fprintf(stderr, "无法打开文件 %s 进行读取!\n", text_filename);return EXIT_FAILURE;}// 1. 逐字符读取printf("逐字符读取:\n");int ch;while ((ch = fgetc(fp)) != EOF) { // 循环读取直到文件结束符EOFputchar(ch); // 打印字符}printf("\n");// 重置文件指针到文件开头,以便再次读取rewind(fp); // 2. 逐行读取printf("逐行读取:\n");char buffer[256]; // 定义一个缓冲区while (fgets(buffer, sizeof(buffer), fp) != NULL) { // 每次读取一行,直到文件结束或错误printf("%s", buffer); // fgets会读取换行符,所以这里不用再加}// 重置文件指针到文件开头,以便再次读取rewind(fp);// 3. 格式化读取printf("\n格式化读取:\n");int read_num;float read_pi;// 注意:fscanf会跳过空白符,直到找到匹配的格式// 这里假设文件内容与写入时一致,且光标在文件开头// 实际应用中,通常会先读取一行,再用sscanf从字符串中解析char line_buffer[256];if (fgets(line_buffer, sizeof(line_buffer), fp) != NULL) { // 读取包含格式化数据的行// 使用sscanf从字符串中解析数据if (sscanf(line_buffer, "这是一个格式化写入的数字:%d,圆周率:%f", &read_num, &read_pi) == 2) {printf("读取到的数字:%d,圆周率:%.2f\n", read_num, read_pi);} else {printf("格式化读取失败或格式不匹配。\n");}}fclose(fp);printf("文本文件 '%s' 读取完成。\n", text_filename);printf("\n--- 二进制文件读写示例 ---\n");// "wb" 模式:写入二进制文件,清空内容fp = fopen(binary_filename, "wb");if (fp == NULL) {fprintf(stderr, "无法打开文件 %s 进行写入!\n", binary_filename);return EXIT_FAILURE;}int data_to_write[] = {10, 20, 30, 40, 50};// fwrite(数据源指针, 每个元素大小, 元素个数, 文件指针)size_t written_items = fwrite(data_to_write, sizeof(int), 5, fp);if (written_items != 5) {fprintf(stderr, "写入二进制文件失败或未完全写入!\n");} else {printf("二进制文件 '%s' 写入 %zu 个整数完成。\n", binary_filename, written_items);}fclose(fp);// "rb" 模式:读取二进制文件fp = fopen(binary_filename, "rb");if (fp == NULL) {fprintf(stderr, "无法打开文件 %s 进行读取!\n", binary_filename);return EXIT_FAILURE;}int data_read[5];// fread(目标缓冲区指针, 每个元素大小, 元素个数, 文件指针)size_t read_items = fread(data_read, sizeof(int), 5, fp);if (read_items != 5) {fprintf(stderr, "读取二进制文件失败或未完全读取!\n");} else {printf("二进制文件 '%s' 读取 %zu 个整数完成:", binary_filename, read_items);for (int i = 0; i < 5; i++) {printf("%d ", data_read[i]);}printf("\n");}// 文件定位:fseek// 将文件指针移动到文件开头偏移量为 2 * sizeof(int) 的位置fseek(fp, 2 * sizeof(int), SEEK_SET); // SEEK_SET表示从文件开头算起int single_int_read;fread(&single_int_read, sizeof(int), 1, fp);printf("使用fseek读取第三个整数:%d\n", single_int_read); // 应该是30// ftell: 获取当前文件指针位置long current_pos = ftell(fp);printf("当前文件指针位置:%ld 字节\n", current_pos); // 应该是 3 * sizeof(int) = 12fclose(fp);printf("二进制文件 '%s' 读取完成。\n", binary_filename);// 错误处理示例:文件不存在fp = fopen("non_existent_file.txt", "r");if (fp == NULL) {perror("打开 'non_existent_file.txt' 失败"); // perror会打印错误信息}return 0;
}

第三阶段:C语言与算法/数据结构——磨砺你的“剑锋”!(建议4-8周)

C语言是刷算法题的利器,也是理解数据结构底层实现的最佳语言。这个阶段,你要把C语言和算法数据结构结合起来,真正磨砺你的“剑锋”,为面试和实际项目打下坚实基础!

3.1 为什么C语言适合刷算法题?

  • 性能优势:C语言编译后执行效率高,对于时间复杂度要求严格的算法题,C语言往往能更容易通过。

  • 内存控制:可以直接操作内存,实现各种复杂的数据结构,避免高级语言的抽象开销。

  • 底层理解:通过C语言实现数据结构和算法,能让你更深入地理解它们的底层工作原理。

  • 面试要求:在嵌入式、操作系统、高性能计算等领域,面试官通常会要求你用C/C++解决算法问题。

3.2 常见数据结构的C语言实现

你需要掌握以下基本数据结构在C语言中的实现:

  • 线性结构

    • 数组:连续存储,随机访问快,插入删除慢。

    • 链表:非连续存储,插入删除快,随机访问慢。包括单向链表、双向链表、循环链表。

    • :LIFO(后进先出),基于数组或链表实现。

    • 队列:FIFO(先进先出),基于数组或链表实现。

  • 树形结构

    • 二叉树:基本概念、遍历(前序、中序、后序)。

    • 二叉搜索树(BST):插入、删除、查找。

  • 哈希表:键值对存储,快速查找,解决冲突。

(这里想象一个单向链表的结构图:节点包含数据和指向下一个节点的指针)

(这里想象一个二叉树的结构图:根节点、左右子节点)

代码示例:单向链表的C语言实现

#include <stdio.h>
#include <stdlib.h> // For malloc, free// 定义链表节点结构体
typedef struct Node {int data;          // 节点存储的数据struct Node *next; // 指向下一个节点的指针
} Node;// 1. 创建新节点
Node* createNode(int value) {Node* newNode = (Node*)malloc(sizeof(Node)); // 动态分配内存if (newNode == NULL) {fprintf(stderr, "内存分配失败!\n");exit(EXIT_FAILURE);}newNode->data = value;newNode->next = NULL; // 新节点初始时指向NULLreturn newNode;
}// 2. 在链表头部插入节点
// 参数:head_ref 是指向头指针的指针,因为可能需要修改头指针本身
void insertAtHead(Node** head_ref, int value) {Node* newNode = createNode(value);newNode->next = *head_ref; // 新节点的next指向原头节点*head_ref = newNode;       // 更新头指针为新节点printf("在头部插入 %d\n", value);
}// 3. 在链表尾部插入节点
void insertAtTail(Node** head_ref, int value) {Node* newNode = createNode(value);if (*head_ref == NULL) { // 如果链表为空,新节点就是头节点*head_ref = newNode;printf("在尾部插入 %d (链表为空)\n", value);return;}Node* current = *head_ref;while (current->next != NULL) { // 遍历到链表尾部current = current->next;}current->next = newNode; // 尾节点的next指向新节点printf("在尾部插入 %d\n", value);
}// 4. 打印链表所有元素
void printList(Node* head) {Node* current = head;printf("链表元素: ");while (current != NULL) {printf("%d -> ", current->data);current = current->next;}printf("NULL\n");
}// 5. 查找元素
Node* findNode(Node* head, int value) {Node* current = head;while (current != NULL) {if (current->data == value) {return current; // 找到}current = current->next;}return NULL; // 未找到
}// 6. 删除指定值的节点 (只删除第一个匹配的)
void deleteNode(Node** head_ref, int value) {Node* current = *head_ref;Node* prev = NULL;// 如果头节点就是要删除的节点if (current != NULL && current->data == value) {*head_ref = current->next; // 更新头指针free(current);             // 释放内存printf("删除头部节点 %d\n", value);return;}// 遍历查找要删除的节点while (current != NULL && current->data != value) {prev = current;current = current->next;}// 如果未找到节点if (current == NULL) {printf("未找到要删除的节点 %d\n", value);return;}// 找到节点,跳过它prev->next = current->next;free(current); // 释放内存printf("删除节点 %d\n", value);
}// 7. 释放整个链表的内存
void freeList(Node** head_ref) {Node* current = *head_ref;Node* next_node;while (current != NULL) {next_node = current->next; // 保存下一个节点的地址free(current);             // 释放当前节点current = next_node;       // 移动到下一个节点}*head_ref = NULL; // 将头指针置为NULL,防止野指针printf("链表内存已释放。\n");
}int main() {Node* head = NULL; // 初始化空链表printf("--- 单向链表操作示例 ---\n");insertAtHead(&head, 10); // 10 -> NULLprintList(head);insertAtTail(&head, 20); // 10 -> 20 -> NULLprintList(head);insertAtHead(&head, 5);  // 5 -> 10 -> 20 -> NULLprintList(head);insertAtTail(&head, 25); // 5 -> 10 -> 20 -> 25 -> NULLprintList(head);Node* found = findNode(head, 10);if (found) {printf("找到元素 %d\n", found->data);} else {printf("未找到元素 10\n");}found = findNode(head, 100);if (found) {printf("找到元素 %d\n", found->data);} else {printf("未找到元素 100\n");}deleteNode(&head, 10); // 删除10printList(head);deleteNode(&head, 5);  // 删除5 (头节点)printList(head);deleteNode(&head, 25); // 删除25 (尾节点)printList(head);deleteNode(&head, 20); // 删除20 (最后一个节点)printList(head);deleteNode(&head, 99); // 尝试删除不存在的节点printList(head);// 再次插入一些节点用于最后的释放insertAtHead(&head, 1);insertAtHead(&head, 2);printList(head);freeList(&head); // 释放所有内存printList(head); // 此时head应为NULLreturn 0;
}

3.3 牛客力扣面试热题100榜单:题型全解总结分析归纳

刷算法题是检验C语言功底和逻辑思维能力的最佳方式。牛客和力扣(LeetCode)是两大刷题平台,其中的“面试热题100榜单”更是经典中的经典。

针对这些题目,你需要掌握的题型解题思路

题型分类

核心思想/常用算法

C语言实现技巧

典型题目示例

数组操作

双指针、滑动窗口、前缀和、排序

数组遍历、指针移动、内存管理

两数之和、盛最多水的容器、三数之和

链表操作

快慢指针、虚拟头节点、递归

结构体与指针、动态内存分配、递归/迭代

两数相加、删除链表的倒数第N个节点、反转链表

字符串处理

双指针、哈希表、KMP、动态规划

字符数组、字符串函数(strlen, strcpy等)、ASCII码

最长回文子串、无重复字符的最长子串、字符串转换整数

栈与队列

模拟、单调栈/队列

数组/链表实现栈/队列、巧用辅助栈/队列

有效的括号、最小栈、滑动窗口最大值

树与图

递归、DFS、BFS、回溯、动态规划

结构体与指针、递归函数、队列实现BFS、栈实现DFS

二叉树的最大深度、翻转二叉树、路径总和

哈希表

键值映射、冲突解决

结构体数组+链表实现哈希表、哈希函数设计

两数之和、最长连续序列、字母异位词分组

排序与查找

冒泡、选择、插入、快排、归并、二分查找

指针交换、递归、分治

排序数组、搜索旋转排序数组、寻找两个正序数组的中位数

动态规划

状态定义、转移方程、边界条件

数组/二维数组DP表、状态压缩

爬楼梯、最长递增子序列、打家劫舍

回溯算法

递归、剪枝、状态恢复

递归函数、全局/局部变量、参数传递

组合总和、全排列、N皇后

位运算

掩码、移位、逻辑运算

整数的二进制表示、巧用位运算优化

只出现一次的数字、2的幂、汉明距离

解题思路与C语言实现技巧:

  1. 理解题意:这是最关键的第一步,确保你完全理解了输入、输出、约束条件和题目要求。

  2. 选择数据结构:根据题目特点选择最合适的数据结构(数组、链表、树、哈希表等)。

  3. 设计算法

    • 暴力解法:先想一个最直观但可能效率不高的解法,确保逻辑正确。

    • 优化思路:思考如何优化时间复杂度或空间复杂度。

      • 双指针:常用于数组、链表、字符串。

      • 滑动窗口:常用于字符串、数组的子序列问题。

      • 分治:将大问题分解为小问题。

      • 贪心:每一步都做出局部最优选择。

      • 动态规划:解决重叠子问题和最优子结构问题。

      • 回溯:用于搜索所有可能的解。

    • 边界条件:考虑空输入、单元素、最大/最小值等特殊情况。

  4. C语言实现细节

    • 内存管理:对于需要动态分配内存的数据结构(如链表、树),务必正确使用 mallocfree,避免内存泄漏。

    • 指针操作:熟练运用指针进行遍历、修改、连接等操作。

    • 函数参数:理解值传递和地址传递,何时需要传递指针的指针(如修改链表头节点)。

    • 宏和条件编译:在调试时可以使用 DEBUG_PRINT 宏,或者通过条件编译来切换不同的实现。

    • 错误处理:对于 malloc 等可能失败的函数,要进行 NULL 检查。

代码示例:两数之和(LeetCode经典题)C语言解法分析

题目描述: 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回答案。

示例: 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

思路分析:

  1. 暴力解法(O(n2)

    • 使用两层循环,外层循环遍历 i,内层循环遍历 j(从 i+1 开始)。

    • 检查 nums[i] + nums[j] 是否等于 target

    • 如果找到,返回 [i, j]

    • 优点:简单直观。

    • 缺点:时间复杂度高,对于大规模数据会超时。

  2. 哈希表解法(O(n)

    • 核心思想:将数组中的元素及其下标存储到哈希表中。

    • 遍历数组 nums,对于每个元素 nums[i],计算它与 target 的差值 complement = target - nums[i]

    • 在哈希表中查找 complement 是否存在:

      • 如果存在,并且 complement 对应的下标不是 i(防止同一个元素重复使用),那么我们就找到了两个数。

      • 如果不存在,将当前元素 nums[i] 及其下标 i 存入哈希表。

    • 优点:时间复杂度降为线性,效率高。

    • 缺点:需要额外的空间存储哈希表。

C语言实现技巧(哈希表解法):

  • C语言标准库没有内置哈希表,需要自己实现一个简易的哈希表,或者使用外部库。

  • 对于面试,通常会要求手写一个简单的哈希表,或者使用数组模拟哈希表(如果数据范围允许)。

  • 这里我们为了简化和演示核心逻辑,可以假设有一个 HashMap 的抽象层,或者直接使用一个数组来模拟(如果题目限制了数据范围较小)。

  • 由于题目没有明确数据范围,我们这里将模拟一个简单的哈希表,使用链地址法解决冲突。

代码示例:两数之和(C语言哈希表模拟实现)

#include <stdio.h>
#include <stdlib.h> // For malloc, free
#include <string.h> // For memset// 定义哈希表节点结构体 (链地址法处理冲突)
typedef struct HashNode {int key;           // 存储数组元素的值int value;         // 存储数组元素的下标struct HashNode *next; // 指向下一个节点的指针
} HashNode;// 定义哈希表结构体
typedef struct HashMap {HashNode **buckets; // 哈希桶数组int capacity;       // 哈希表容量int size;           // 当前存储的键值对数量
} HashMap;// 辅助函数:哈希函数 (简单的取模哈希)
// 注意:对于负数,C语言的%运算符行为可能不同,实际哈希函数需要更健壮
// 这里为了简化,假设key为非负数
unsigned int hash(int key, int capacity) {// 简单的哈希函数,实际应用中需要更复杂的哈希算法以减少冲突// 对于负数,需要处理 abs(key)return (unsigned int)key % capacity;
}// 初始化哈希表
HashMap* createHashMap(int capacity) {HashMap* map = (HashMap*)malloc(sizeof(HashMap));if (map == NULL) {fprintf(stderr, "HashMap内存分配失败!\n");exit(EXIT_FAILURE);}map->capacity = capacity;map->size = 0;// 为桶数组分配内存,并初始化为NULLmap->buckets = (HashNode**)calloc(capacity, sizeof(HashNode*));if (map->buckets == NULL) {fprintf(stderr, "HashMap桶内存分配失败!\n");free(map);exit(EXIT_FAILURE);}return map;
}// 向哈希表插入键值对
// 参数:map - 哈希表指针
//       key - 待插入的键(数组元素值)
//       value - 待插入的值(数组元素下标)
void hashMapPut(HashMap* map, int key, int value) {unsigned int index = hash(key, map->capacity);HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));if (newNode == NULL) {fprintf(stderr, "HashNode内存分配失败!\n");exit(EXIT_FAILURE);}newNode->key = key;newNode->value = value;newNode->next = map->buckets[index]; // 新节点插入到链表头部map->buckets[index] = newNode;map->size++;
}// 从哈希表获取键对应的值
// 参数:map - 哈希表指针
//       key - 待查找的键
// 返回值:如果找到,返回对应的值(下标);否则返回-1(表示未找到)
int hashMapGet(HashMap* map, int key) {unsigned int index = hash(key, map->capacity);HashNode* current = map->buckets[index];while (current != NULL) {if (current->key == key) {return current->value; // 找到键,返回对应的值}current = current->next;}return -1; // 未找到
}// 释放哈希表内存
void freeHashMap(HashMap* map) {if (map == NULL) return;for (int i = 0; i < map->capacity; i++) {HashNode* current = map->buckets[i];while (current != NULL) {HashNode* temp = current;current = current->next;free(temp); // 释放节点}}free(map->buckets); // 释放桶数组free(map);          // 释放哈希表结构体
}/*** @brief 解决LeetCode两数之和问题* * @param nums 整数数组* @param numsSize 数组大小* @param target 目标和* @param returnSize 指向返回数组大小的指针* @return int* 返回包含两个下标的数组,需要调用者free*/
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {// 为了简化,哈希表容量可以根据numsSize估算,例如2倍numsSize// 实际应考虑哈希冲突率和动态扩容int hashMapCapacity = numsSize * 2; HashMap* map = createHashMap(hashMapCapacity);// 结果数组,存储两个下标int* result = (int*)malloc(2 * sizeof(int));if (result == NULL) {fprintf(stderr, "结果数组内存分配失败!\n");freeHashMap(map);exit(EXIT_FAILURE);}*returnSize = 2; // 返回数组的大小固定为2for (int i = 0; i < numsSize; i++) {int complement = target - nums[i]; // 计算补数int complement_index = hashMapGet(map, complement); // 在哈希表中查找补数// 如果找到补数,并且补数的下标不是当前元素的下标(防止同一个元素重复使用)if (complement_index != -1 && complement_index != i) {result[0] = complement_index; // 补数的下标result[1] = i;                // 当前元素的下标freeHashMap(map);             // 找到结果后,释放哈希表内存return result;                // 返回结果}// 如果未找到补数,将当前元素及其下标存入哈希表hashMapPut(map, nums[i], i);}// 如果遍历完整个数组都没有找到,理论上题目保证有解,这里作为安全返回*returnSize = 0; // 表示未找到free(result);freeHashMap(map);return NULL;
}int main() {int nums1[] = {2, 7, 11, 15};int target1 = 9;int returnSize1;int* result1 = twoSum(nums1, sizeof(nums1) / sizeof(nums1[0]), target1, &returnSize1);if (result1 != NULL && returnSize1 == 2) {printf("nums = [2,7,11,15], target = 9 -> 结果: [%d, %d]\n", result1[0], result1[1]);free(result1); // 释放结果数组内存} else {printf("未找到结果。\n");}int nums2[] = {3, 2, 4};int target2 = 6;int returnSize2;int* result2 = twoSum(nums2, sizeof(nums2) / sizeof(nums2[0]), target2, &returnSize2);if (result2 != NULL && returnSize2 == 2) {printf("nums = [3,2,4], target = 6 -> 结果: [%d, %d]\n", result2[0], result2[1]);free(result2);} else {printf("未找到结果。\n");}int nums3[] = {3, 3};int target3 = 6;int returnSize3;int* result3 = twoSum(nums3, sizeof(nums3) / sizeof(nums3[0]), target3, &returnSize3);if (result3 != NULL && returnSize3 == 2) {printf("nums = [3,3], target = 6 -> 结果: [%d, %d]\n", result3[0], result3[1]);free(result3);} else {printf("未找到结果。\n");}return 0;
}

总结与展望:C语言——嵌入式工程师的“基石”与“利刃”!

兄弟们,恭喜你!你已经完成了嵌入式工程师学习路线的第一部分——C语言的内功心法

通过这一部分的学习和实践,你现在应该对C语言有了更深入的理解:

  • 你掌握了C语言的基本语法核心概念,为后续学习打下了坚实的地基。

  • 你深入理解了指针的奥秘内存管理的精髓,这是C语言最强大也最危险的部分,也是嵌入式开发中必须掌握的“杀手锏”。

  • 你学会了如何使用结构体、共用体和枚举来高效组织数据,并理解了内存对齐和位域在嵌入式中的重要性。

  • 你掌握了预处理和宏的用法,以及它们在条件编译、代码复用中的“魔法”。

  • 你了解了文件I/O的基本操作,这是程序与外部存储交互的基础。

  • 你还通过经典的“两数之和”问题,初步感受了如何将C语言与数据结构和算法结合,磨砺你的“剑锋”,为面试和解决实际问题做准备。

C语言,它不仅仅是一门编程语言,它更是一种思维方式,一种让你能够直接与计算机底层“对话”的能力。它是你成为一名合格嵌入式工程师的基石,也是你手中无往不胜的利刃

下一篇文章,我们将进入嵌入式学习路线的第二部分——Linux操作系统学习路线!我们将深入探索Linux的奥秘,从Shell命令到文件系统,从进程管理到用户管理,让你彻底掌握Linux这个嵌入式开发的“主战场”!

敬请期待!如果你觉得这篇文章对你有帮助,让你对C语言有了更深的理解,请务必点赞、收藏、转发

嵌入式工程师学习路线大总结(二):Linux操作系统——你的嵌入式“主战场”与“瑞士军刀”!

引言:Linux——嵌入式工程师的“左膀右臂”!

兄弟们,上一篇咱们把C语言这把“屠龙宝刀”的内功心法彻底搞明白了。现在,你手里有了绝世神兵,但你得知道去哪里“施展拳脚”啊!

在嵌入式领域,这个“主战场”就是Linux操作系统

你可能会疑惑:备实时操嵌入式不是直接跑在裸机上的吗?为什么还要学Linux?

  • Linux命令那么多,怎么才能高效学习和使用?

  • Shell脚本有什么用?不就是写写命令吗?

  • 那些TFTP、NFS服务,GDB调试,听起来就很高大上,我能学会吗?

没错,Linux就是嵌入式工程师的“左膀右臂”!它不仅仅是一个操作系统,更是你开发、调试、部署嵌入式系统的强大平台。从搭建开发环境、编译代码,到烧录固件、远程调试,再到自动化测试,几乎每一个环节都离不开Linux。

今天,我就带你彻底征服Linux,从最基础的命令到高级的Shell脚本,从服务搭建到强大的GDB调试,让你把Linux变成你的“瑞士军刀”!

第一阶段:Linux基础——熟悉你的“武器库”!(建议2-4周)

这个阶段,咱们要先熟悉Linux的“长相”和“脾气”,掌握最基本的命令行操作,就像拿到一把新枪,先得知道怎么上膛、怎么瞄准。

2.1 Linux介绍及环境配置:搭建你的“练兵场”

    • Linux是什么? 一个开源、免费、多用户、多任务的操作系统内核。

    • Linux发行版:理解Ubuntu、Debian、CentOS、Fedora等主流发行版的区别和选择。对于嵌入式开发,通常选择Ubuntu或Debian,因为它们社区活跃,资料丰富。

    • 虚拟机:为什么要在虚拟机里安装Linux?隔离开发环境、方便备份和恢复、不影响宿主机。

    • 命令行(Terminal/Shell):Linux的灵魂,所有的操作都通过命令完成,高效且强大。

    • GCC/G++编译环境:C/C++代码编译的基石。

    • 交叉编译:在宿主机(如x86架构的PC)上编译出能在目标机(如ARM架构的开发板)上运行的代码。这是嵌入式开发的核心概念之一。

    d 等):(

    • 为什么Linux在服务器和嵌入式领域如此流行?稳定性、安全性、开源、可定制性、强大的命令行工具。

    • 交叉编译的必要性:目标板资源有限,无法直接进行编译;架构不同,直接编译出的代码无法运行。

    环境配置步骤(概念性):

    1. 选择发行版:推荐Ubuntu Desktop LTS版本(长期支持版)。

    2. 安装虚拟机软件:VirtualBox或VMware Workstation Player。

    3. 创建虚拟机:分配足够的内存(4GB+)、CPU核心(2核+)、硬盘空间(50GB+)。

    4. 在虚拟机中安装Linux:按照提示一步步安装。

    5. 安装开发工具

      • 打开终端(Terminal)。

      • 更新软件包列表:sudo apt update

      • 安装GCC/G++、make、gdb等基本开发工具:sudo apt install build-essential gdb

      • 安装Git:sudo apt install git

      • 安装Vim/NeoVim或VS Code等编辑器。

      • 安装交叉编译工具链

        • 根据你的目标板(如ARM Cortex-M、ARM Cortex-A)选择合适的工具链。

        • 通常从芯片厂商官网下载,或使用Linaro等提供的预编译工具链。

        • 例如:arm-none-eabi-gcc (裸机ARM), arm-linux-gnueabihf-gcc (带Linux的ARM)。

        • 解压到 /opt 目录,并配置环境变量 PATH

    2.2 Linux Shell命令(基础篇):你的“第一把枪”

    掌握这些基本命令,就像学会了如何使用你的“武器库”里的第一把枪。它们是你日常操作Linux的基石。

    2.2.1 文件与目录操作:管理你的“战场”

    局部命令

    作用

    常用参数

    示例o

    逻辑分析/嵌入式应用

    ls

    列出目录内容

    -l (详细列表), -a (显示隐藏文件), -h (人类可读大小)

    ls -

    查看文件和目录,快速定位代码文件

    cd

    切换目录

    cd .. (上级目录), cd ~ (用户主目录), cd / (根目录)

    cd /home/user/proje

    在文件系统中导航,进入项目目录

    pwd

    显示当前工作目录

    p

    确认当前所在位置

    mkdir

    创建目录

    -p (递归创建父目录)

    mkdir -p build/deb

    创建项目构建目录

    rmdir

    删除空目录

    rmdir empty_dir

    很少用,通常用rm -r

    rm

    删除文件或目录

    -r (递归删除目录), -f (强制删除,不提示)

    rm -rf buil

    清理构建产物,删除旧文件

    cp

    复制文件或目录

    -r (递归复制目录), -i (交互式,覆盖前提示)

    cp main.c buil

    复制源代码、配置文件

    mv

    移动/重命名文件或目录

    mv old_name.c new_name.c

    重命名文件、移动文件到其他目录

    代码示例:文件与目录操作Shell脚本

    #!/bin/bash
    # 文件名: file_ops_demo.sh
    # 这是一个演示Linux文件与目录基本操作的Shell脚本echo "--- 开始文件与目录操作演示 ---"# 1. 创建目录
    echo "1. 创建目录 'my_project' 和 'my_project/src'..."
    mkdir -p my_project/src # -p 确保父目录不存在时也创建
    if [ $? -eq 0 ]; then # 检查上一条命令的退出状态,0表示成__V echo "目录创建成功。"
    else
    A55 echo "目录创建失败。"exit 枚 # 退出脚本
    fi举
    # 2. 进入目录
    echo "2. 进入 'my_project' 目录..."
    cd my_project
    echo "当前工作目录: $(pwd)" # $(pwd) 获取当前目录# 3. 创建一些文件
    echo "3. 创建一些示例文件..."
    echo "int) {/n 0; // 程序 }" > src/main.c # 将字符串写入文件
    echo "Hello World" > README.txt
    touch config.h # 创建一个空文件
    ls -l # 列出当前目录内容# 4. 复制文件
    echo "4. 复制 'src/main.c' 到当前目录..."
    cp src/main.c . # . 表示当前目录
    ls -l# 5. 重命名文件
    echo "5. 重命名 'main.c' 为 'app.c'..."
    mv main.c app.c
    ls -l# 6. 移动文件
    echo "6. 移动 'app.c' 到 'src' 目录..."
    mv app.c src/
    ls -l src/ # 查看src目录内容这
    # 7. 删除文件
    echo "7. 删除 'README.txt'..."
    rm README.txt
    ls -l里
    # 8. 返回上级目录
    echo "8. 返回上级目录..."
    cd ..
    echo "当前工作目录: $(pwd)"# 9. 删除目录 (递归删除非空目录)
    echo "9. 删除 'my_project' 目录及其所有内容..."
    rm -rf my_project # -r 递归,-f 强制,非常危险的命令,慎用!
    if [ $? -eq 0 ]; thenitf( echo "目录 'my_project' 已删除。"
    elseecho "目录 'my_project' 删除失败。"
    fiecho "--- 文件与目录操作演示结束 ---"an -2.2.2 文件内容查看:浏览你的“情报”

    命令

    作用

    常用参数

    示例

    逻辑分析/嵌入式应用

    cat

    连接文件并打印到标准输出

    -n (显示行号)

    cat main.c

    快速查看小文件内容,合并文件

    more

    分页查看文件内容

    空格 (下一页), b (上一页), q (退出)

    more large_log.txt

    适合查看大文件,但只能向前翻

    less

    分页查看文件内容

    箭头键 (上下翻页), q (退出), / (搜索)

    less large_log.txt

    more更强大,可前后翻页,支持搜索

    head

    显示文件开头几行

    -n N (显示前N行)

    head -n 10 log.txt

    快速查看日志文件开头

    tail

    显示文件末尾几行

    -n N (显示后N行), -f (实时跟踪文件末尾)

    tail -f /var/log/syslog

    实时查看日志输出,调试程序输出

    代码示例:文件内容查看Shell脚本- #!/bin/bash # 文件名: file_view_demo.sh # 这是一个演示Linux文件内容查看命令的Shell脚本 echo "--- 开始文件内容查看演示 ---" # 创建一个示例文件 echo "Line 1: This is the first line." > example_log.txt echo "Line 2: This is the second line." >> example_log.txt echo "Line 3: This is the third line." >> example_log.txt echo "Line f_ This is the fourth line." >> example_log.txt echo "Line 5: This is the fifth line." >> example_log.txt echo "Line 6: This is the sixth line." >> example_log.txt echo "Line 7: This is the seventh line." >> example_log.txt echo "Line 8: This is the eighth line." >> example_log.txt echo "Line 9: This is the ninth line." >> example_log.txt echo "Line 10: This is the tenth line." >> example_log.txt echo "Line 11: This is the eleventh line." >> example_log.txt echo "Line 12: This is the twelfth line." >> example_log.txtv echo "已创建 'example_log.txt' 文件。" # 1. 使用 cat echo -e "\n--- 使用 cat 查看文件所有内容 ---" cat example_log.txt # 2. 使用 head echo -e "\n--- 使用 head 查看文件前3行 ---" head -n 3 example_log.txt # 3. 使用 tail echo -e "\n--- 使用 tail 查看文件后2行 ---" tail -n 2 example_log.txt # 4. 实时跟踪文件 (tail -f) echo -e "\n--- 使用 tail -f 实时跟踪文件 (按 Ctrl+C 停止) ---" echo "请在另一个终端中向 'example_log.txt' 追加内容,例如: echo 'New line added' >> example_log.txt" echo "等待 5 秒后自动停止 tail -f..." (sleep 5; kill $$) & # 在后台运行一个子进程,5秒后杀死当前脚本进程 tail -f example_log.txt # 5. 使用 more (需要手动交互) echo -e "\n--- 使用 more 分页查看文件 (按空格翻页,q 退出) ---" # more example_log.txt # 运行时会暂停,这里不自动执行 # 6. 使用 less (更强大的分页查看,需要手动交互) echo -e "\n--- 使用 less 分页查看文件 (按箭头键翻页,q 退出,/ 搜索) ---" # less example_log.txt # 运行时会暂停,这里不自动执行 # 清理示例文件 echo -e "\n清理示例文件 'example_log.txt'..." rm example_log.txt echo "--- 文件内容查看演示结束 ---"n/ 2.2.3 权限管理:保护你的“资产”

    Linux是多用户系统,文件和目录的权限管理至关重要。kase权限类型

    • r (read): 读取权限

    • w (write): 写入权限

    • x (execute): 执行权限(对文件表示可执行,对目录表示可进入) } wh权限对象

      • u (user): 文件所有者

      • g (group): 文件所属组

      • o (others): 其他用户

      • a (all): 所有用户(u+g+o)for

      ls -l 输出解释

      • drwxr-xr-x:第一位 d 表示目录,- 表示文件sco接下来的9位分为三组:所有者权限、所属组权限、其他用户权限。

      • 例如:rwx (可读、可写、可执行), r-x (可读、可执行,不可写), rw- (可读、可写,不可执行)。n"i++)数字权限表示

        • r = 4, w = 2, x = 1

        • 权限组合相加:rwx = 4+2+1=7, rw- = 4+2+0=6, r-x = 4+0+1=5

        • 例如:chmod 755 file 表示所有者rwx,组r-x,其他r-x。数组元素的

          命令

          作用

          常用参数

          示例

          逻辑分析/嵌入式应用

          chmod

          改变文件或目录权限

          u/g/o/a +/-/= r/w/x, 数字模式

          chmod +x script.sh, chmod 755 my_app

          使脚本可执行、控制程序访问权限

          chown

          改变文件或目录所有者

          -R (递归)

          sudo chown user:group file

          改变文件归属,常用于部署

          chgrp

          改变文件或目录所属组

          -R (递归)

          sudo chgrp dev_group project_dir

          改变文件组归属

          代码示例:权限管理Shell脚本数#!/bin/bash # 文件名: permissions_demo.sh # 这是一个演示Linux文件权限管理命令的Shell脚本 echo "--- 开始权限管理演示 ---" # 创建一个示例文件和目录 echo "这是一个测试文件。" > test_file.txt mkdir test_dir echo "已创建 'test_file.txt' 和 'test_dir'。" # 1. 查看初始权限 echo -e "\n1. 初始权限:" ls -l test_file.txt test_dir # 2. 修改文件权限 (chmod) echo -e "\n2. 修改文件权限 (chmod):" echo "2.1. 给所有者添加执行权限: chmod u+x test_file.txt" chmod u+x test_file.txt ls -l test_file.txt echo "2.2. 给组和其他用户移除写权限: chmod go-w test_file.txt" chmod go-w test_file.txt ls -l test_file.txt echo "2.3. 使用数字模式设置权限 (rwx for owner, rx for group, rx for others): chmod 755 test_file.txt" chmod 755 test_file.txt ls -l test_file.txt # 3. 修改目录权限 (chmod) echo -e "\n3. 修改目录权限 (chmod):" echo "3.1. 给目录添加所有者写权限: chmod u+w test_dir" chmod u+w test_dir ls -l test_dir echo "3.2. 设置目录权限为 777 (所有人都可读写执行): chmod 777 test_dir" chmod 777 test_dir ls -l test_dir # 4. 修改文件所有者和所属组 (chown, chgrp) # 注意: chown 和 chgrp 通常需要root权限 (sudo) echo -e "\n4. 修改文件所有者和所属组 (需要sudo权限):" # 假设当前用户是 user,可以尝试将其所有者改为root,组改为root # sudo chown root:root test_file.txt # sudo chown root test_dir # sudo chgrp users test_file.txt # 演示 chown 改变所有者 echo "当前用户: $(whoami)" echo "尝试将 test_file.txt 的所有者改为 root (需要输入密码):" # sudo chown root test_file.txt # ls -l test_file.txt # 演示 chgrp 改变组 echo "尝试将 test_dir 的组改为 users (需要输入密码):" # sudo chgrp users test_dir # ls -l test_dir) echo -e "\n5. 递归修改权限和所有者 (chown -R, chmod -R):" mkdir -p recursive_test/subdir touch recursive_test/file1.txt echo "content" > recursive_test/subdir/file2.txt echo "初始状态:" ls -lR recursive_test; echo "递归设置 recursive_test 及其子内容为 777 权限: chmod -R 777 recursive_test" chmod -R 777 recursive_test ls -lR recursive_test # echo "递归设置 recursive_test 及其子内容的所有者为 root: sudo chown -R root recursive_test" # sudo chown -R root recursive_test # ls -lR recursive_testt # 清理示例文件和目录 echo -e "\n清理示例文件和目录..." rm -rf test_file.txt test_dir recursive_testi echo "--- 权限管理演示结束 ---";如何2.2.4 输入输出重定向:控制你的“信息流”内n在Linux中,一切皆文件。标准输入(stdin)、标准输出(stdout)、标准错误(stderr)都是文件描述符。重定向允许你改变这些默认的输入输出流。存

          符号

          作用

          示例

          逻辑分析/嵌入式应用

          >

          将标准输出重定向到文件(覆盖)

          ls > filelist.txt

          将命令输出保存到文件,而不是打印到屏幕

          >>

          将标准输出重定向到文件(追加)

          echo "new line" >> log.txt

          向日志文件追加内容

          <

          将文件内容作为标准输入

          sort < numbers.txt

          将文件内容作为程序的输入

          2>

          将标准错误重定向到文件(覆盖)

          command 2> error.log

          捕获错误信息,方便调试

          &>>&

          将标准输出和标准错误都重定向到文件(覆盖)

          command &> all_output.log

          同时捕获所有输出

          `

          `

          管道,将前一个命令的标准输出作为后一个命令的标准输入

          `ls -l

          代码示例:输入输出重定向Shell脚本解#!/bin/bash # 文件名: io_redirection_demo.sh # 这是一个演示Linux输入输出重定向的Shell脚本 echo "--- 开始输入输出重定向演示 ---" # 1. 标准输出重定向到文件 (覆盖) echo -e "\n1. 标准输出重定向到文件 (>):" echo "这是第一行内容。" > output.txt echo "这是第二行内容。" >> output.txt # 使用 >> 追加 echo "这是第三行内容。" > output.txt # 再次使用 > 会覆盖 echo "cat output.txt 结果:" cat output.txt # 预期:只显示“这是第三行内容。” # 2. 标准错误重定向到文件 (2>) echo -e "\n2. 标准错误重定向到文件 (2>):" # 尝试删除一个不存在的文件,会产生错误输出 rm non_existent_file.txt 2> error.log echo "cat error.log 结果:" cat error.log # 预期:显示rm的错误信息 # 3. 标准输出和标准错误同时重定向 (&> 或 >& ) echo -e "\n3. 标准输出和标准错误同时重定向 (&> 或 >&):" # 创建一个会同时产生输出和错误的命令 (ls -l; rm another_non_existent_file.txt) &> combined_output.log echo "cat combined_output.log 结果:" cat combined_output.log # 预期:显示ls的输出和rm的错误信息 # 4. 输入重定向 (<) echo -e "\n4. 输入重定向 (<):" echo "Line A" > input.txt echo "Line B" >> input.txt echo "Line C" >> input.txt echo "使用 wc -l < input.txt 统计行数:" wc -l < input.txt # wc -l 从标准输入读取内容并统计行数 # 预期:显示3 # 5. 管道 (|) echo -e "\n5. 管道 (|):" echo "使用 ls -l | grep .txt 查找所有txt文件:" ls -l | grep ".txt" # 将ls -l的输出作为grep的输入 # 预期:显示当前目录下所有.txt文件的详细信息 echo "使用 ps aux | grep bash 查找bash进程:" ps aux | grep "bash" | grep -v "grep" # 查找所有bash进程,并排除grep自身的进程 # 预期:显示与bash相关的进程信息 # 6. 组合使用 echo -e "\n6. 组合使用 (复杂示例):" # 查找 /etc 目录下所有包含 "conf" 字符串的文件,并将结果保存到 find_conf.log find /etc -name "*conf*" 2>/dev/null | tee find_conf.log # 2>/dev/null 将find命令的错误输出(如权限不足)重定向到空设备,不显示 # tee 命令会将标准输入同时输出到标准输出和文件 echo "cat find_conf.log 结果 (部分):" head -n用持 find_conf.log # 只看前5行续 # 清理示例文件 echo -e "\n清理示例文件..." rm -f output.txt error.log combined_output.log input.txt find_conf.log echo "--- 输入输出重定向演示结束 ---"对局/第二阶段:Linux进阶——掌握你的“战场策略”!(建议3-6周)放d掌握了基础命令,你已经能操作Linux了。但要成为一名合格的嵌入式工程师,你还需要掌握更高级的命令和Shell脚本编程,这就像你不仅会用枪,还会制定“战场策略”,自动化完成复杂的任务。_;2.3 Linux Shell命令(进阶篇):你的“高级武器”

          2.3.1 文件搜索与处理:快速定位与修改“情报”

          命令

          作用

          常用参数

          示例

          逻辑分析/嵌入式应用

          find

          在文件系统中搜索文件和目录

          -name (按名称), -type (按类型), -size (按大小), -mtime (按修改时间), -exec (执行命令)

          find . -name "*.c"

          查找项目中的所有C源文件

          grep

          在文件中搜索匹配的文本模式

          -i (忽略大小写), -r (递归搜索), -n (显示行号), -v (反向匹配)

          grep -rn "printf" .

          在整个代码库中查找函数调用、变量定义

          sed

          流编辑器,用于文本转换

          s/old/new/g (替换), d (删除行)

          sed 's/old_func/new_func/g' file.c

          批量修改代码、配置文件

          awk

          强大的文本处理工具,按列处理

          {print $1, $3} (打印第1和第3列)

          `ls -l

          awk '{print $NF}'`

          代码示例:文件搜索与处理Shell脚本

          #!/bin/bash # 文件名: file_search_process_demo.sh # 这是一个演示Linux文件搜索与处理命令的Shell脚本 echo "--- 开始文件搜索与处理演示 ---" # 创建一些示例文件和目录 mkdir -p project/src project/docs echo "int main() { printf(\"Hello World\\n\"); return 0; }" > project/src/app.c echo "This is a test file." > project/docs/notes.txt echo "Another line with printf." >> project/docs/notes.txt echo "Configuration setting: DEBUG_MODE = 1" > project/config.h echo "Configuration setting: RELEASE_MODE = 0" >> project/config.h echo "已创建示例项目结构和文件。" # 1. 使用 find 搜索文件 echo -e "\n1. 使用 find 搜索文件:" echo "1.1. 查找 'project' 目录下所有 .c 文件:" find project -name "*.c" # 预期:project/src/app.c echo "1.2. 查找 'project' 目录下所有文件类型为普通文件 (f) 的文件:" find project -type f -name "*" # 预期:project/src/app.c, project/docs/notes.txt, project/config.h echo "1.3. 查找 'project' 目录下,名称包含 'config' 的文件,并打印其路径和大小:" find project -name "*config*" -exec du -h {} \; # -exec 对每个结果执行命令 # 预期:显示config.h的大小 # 2. 使用 grep 搜索文件内容 echo -e "\n2. 使用 grep 搜索文件内容:" echo "2.1. 在 project/src/app.c 中查找 'printf' 字符串:" grep "printf" project/src/app.c # 预期:显示包含 printf 的行 echo "2.2. 在 'project' 目录下递归查找所有包含 'printf' 的文件,并显示行号:" grep -rn "printf" project/ # 预期:显示 project/src/app.c 和 project/docs/notes.txt 中包含 printf 的行及行号 echo "2.3. 在 'project/config.h' 中查找不包含 'RELEASE_MODE' 的行:" grep -v "RELEASE_MODE" project/config.h # 预期:显示 "Configuration setting: DEBUG_MODE = 1" # 3. 使用 sed 进行文本替换 echo -e "\n3. 使用 sed 进行文本替换 (非原地修改,需重定向或 -i 选项):" echo "原始 config.h 内容:" cat project/config.h echo "将 'DEBUG_MODE' 替换为 'TEST_MODE' (输出到标准输出):" sed 's/DEBUG_MODE/TEST_MODE/g' project/config.h # 预期:显示替换后的内容,但原文件不变 echo "将 'project/config.h' 中的 'DEBUG_MODE' 替换为 'NEW_DEBUG_MODE' (原地修改):" sed -i 's/DEBUG_MODE/NEW_DEBUG_MODE/g' project/config.h echo "修改后的 config.h 内容:" cat project/config.h # 预期:DEBUG_MODE 变为 NEW_DEBUG_MODE # 4. 使用 awk 进行列处理 echo -e "\n4. 使用 awk 进行列处理:" echo "ls -l 的输出:" ls -l project/src/app.c echo "使用 awk 提取 project/src/app.c 的文件名和大小:" ls -l project/src/app.c | awk '{print $9, $5 " bytes"}' # $9是文件名,$5是大小 # 预期:app.c [大小] bytes echo "提取 project/config.h 中所有行的第二个单词:" awk '{print $2}' project/config.h # 预期:setting, setting # 清理示例文件和目录 echo -e "\n清理示例文件和目录..." rm -rf project echo "--- 文件搜索与处理演示结束 ---" }2.3.2 压缩解压:打包你的“成果”

          在嵌入式开发中,经常需要打包和传输固件、日志文件等。

          命令

          作用

          常用参数

          示例

          逻辑分析/嵌入式应用

          tar

          打包/解包文件,常与压缩工具结合

          -c (创建), -x (解压), -v (显示过程), -f (指定文件名)

          tar -cvf archive.tar dir/

          打包整个项目目录

          gzip

          GNU zip压缩工具

          -d (解压)

          gzip file.txt (生成file.txt.gz)

          单文件压缩,常与tar结合

          bzip2

          bzip2压缩工具

          -d (解压)

          bzip2 file.txt (生成file.txt.bz2)

          压缩率比gzip高,速度慢

          xz

          xz压缩工具

          -d (解压)

          xz file.txt (生成file.txt.xz)

          压缩率最高,速度最慢

          zip

          跨平台压缩工具

          -r (递归)

          zip -r archive.zip dir/

          Windows和Linux通用

          unzip

          解压zip文件

          unzip archive.zip

          解压zip文件

          常用组合:

          • tar -zcvf archive.tar.gz dir/ (打包并用gzip压缩)

          • tar -zxvf archive.tar.gz (解压gzip压缩的tar包)

          • tar -jcvf archive.tar.bz2 dir/ (打包并用bzip2压缩)

          • tar -jxvf archive.tar.bz2 (解压bzip2压缩的tar包)tiCotar -Jcvf archive.tar.xz dir/ (打包并用xz压缩)tLEVtar -Jxvf archive.tar.xz (解压xz压缩的tar包)EG//代码示例:压缩解压Shell脚本_ #!/bin/bash # 文件名: compress_decompress_demo.sh # 这是一个演示Linux压缩解压命令的Shell脚本 echo "--- 开始压缩解压演示 ---" # 创建一些示例文件和目录 mkdir -p data/logs data/configs echo "Log entry 1" > data/logs/app.log echo "Log entry 2" >> data/logs/app.log echo "Config A=1" > data/configs/dev.conf echo "Config B=2" > data/configs/prod.conf echo "Hello.txt content." > data/hello.txt echo "已创建示例数据目录和文件。" # 1. 使用 tar 打包 echo -e "\n1. 使用 tar 打包 'data' 目录到 'data.tar':" tar -cvf data.tar data/ ls -lh data.tar # 2. 使用 gzip 压缩 tar 包 echo -e "\n2. 使用 gzip 压缩 'data.tar' 到 'data.tar.gz':" gzip data.tar ls -lh data.tar.gz # 原始的data.tar会被删除 # 3. 解压 gzip 压缩的 tar 包 echo -e "\n3. 解压 'data.tar.gz':" mkdir extract_gz tar -zxvf data.tar.gz -C extract_gz/ # -C 指定解压目录 echo "解压到 'extract_gz' 目录,内容如下:" ls -lR extract_gz # 清理 rm -rf data.tar.gz extract_gz # 4. 使用 bzip2 压缩 echo -e "\n4. 使用 bzip2 压缩 'data' 目录到 'data.tar.bz2':" tar -jcvf data.tar.bz2 data/ ls -lh data.tar.bz2 # 5. 解压 bzip2 压缩的 tar 包 echo -e "\n5. 解压 'data.tar.bz2':" mkdir extract_bz2 tar -jxvf data.tar.bz2 -C extract_bz2/ echo "解压到 'extract_bz2' 目录,内容如下:" ls -lR extract_bz2 # 清理 rm -rf data.tar.bz2 extract_bz2 # 6. 使用 xz 压缩 echo -e "\n6. 使用 xz 压缩 'data' 目录到 'data.tar.xz':" tar -Jcvf data.tar.xz data/ ls -lh data.tar.xz # 7. 解压 xz 压缩的 tar 包 echo -e "\n7. 解压 'data.tar.xz':" mkdir extract_xz tar -Jxvf data.tar.xz -C extract_xz/ echo "解压到 'extract_xz' 目录,内容如下:" ls -lR extract_xz # 清理 rm -rf data.tar.xz extract_xzt # 8. 使用 zip 压缩 echo -e "\n8. 使用 zip 压缩 'data' 目录到 'data.zip':" zip -r data.zip data/ ls -lh data.zipf # 9. 解压 zip 文件 echo -e "\n9. 解压 'data.zip':" mkdir extract_zip unzip data.zip -d extract_zip/ echo "解压到 'extract_zip' 目录,内容如下:" ls -lR extract_zip # 清理 rm -rf data.zip extract_zip # 清理原始数据目录 echo -e "\n清理原始数据目录 'data'..." rm -rf data echo "--- 压缩解压演示结束 ---"

            2.3.3 进程管理:掌控你的“程序生命”

            理解进程管理对于调试、监控和优化嵌入式系统至关重要。

            命令

            作用

            常用参数

            示例

            逻辑分析/嵌入式应用

            ps

            显示当前进程状态

            aux (显示所有用户进程,包括没有控制终端的进程), ef (显示完整格式的进程列表)

            ps aux

            查看系统运行的进程,查找异常进程

            top

            实时显示进程活动和系统资源使用

            q (退出), k (杀死进程)

            top

            实时监控CPU、内存、进程负载,诊断性能问题

            kill

            杀死进程

            -9 (强制杀死), -15 (正常终止)

            kill PID, kill -9 PID

            终止无响应或异常的程序

            nohup

            在后台运行命令,并在退出终端后继续运行

            nohup ./my_app &

            在开发板上运行后台服务,不随SSH断开而停止

            &

            将命令放到后台运行

            ./my_script.sh &

            脚本或程序在后台执行,不阻塞终端

            代码示例:进程管理Shell脚本

            #!/bin/bash
            # 文件名: process_management_demo.sh
            # 这是一个演示Linux进程管理命令的Shell脚本echo "--- 开始进程管理演示 ---"# 1. 启动一个后台模拟进程 (一个无限循环的C程序)
            echo -e "\n1. 启动一个后台模拟进程..."# 创建一个简单的C程序,用于模拟长时间运行的进程
            cat << EOF > dummy_process.c
            #include <stdio.h>
            #include <unistd.h> // For sleepint main() {printf("Dummy process started. PID: %d\n", getpid());fflush(stdout); // 确保立即打印ilenfp);(1)fputcp); printf("Dummy process running...\n"); // 频繁打印会影响性能sleep(1); // 每秒休眠,模拟工作}return 0;tf
            EOF(
            gcc dummy_process.c -o dummy_process
            echo "已编译模拟进程 'dummy_process'。"# 在后台运行模拟进程,并将输出重定向到文件,防止阻塞终端
            nohup ./dummy_process > dummy_process.log 2>&1 &
            DUMMY_PID=$! # 获取后台进程的PID
            echo "模拟进程 'dummy_process' 已在后台启动,PID: $DUMMY_PID"
            sleep 2 # 等待进程启动n
            # 2. 使用 ps 查看进程
            echo -e "\n2. 使用 ps 查看进程 (查找 dummy_process):"
            ps aux | grep "dummy_process" | grep -v "grep" # 排除grep自身进程
            # 预期:显示 dummy_process 的进程信息"
            # 3. 使用 top 监控进程 (需要手动交互,这里不自动执行)
            echo -e "\n3. 使用 top 监控进程 (手动执行,按 'q' 退出):"
            echo "请在终端中输入 'top' 并观察 'dummy_process' 的CPU和内存使用情况。"
            # top-
            # 4. 使用 kill 终止进程
            echo -e "\n4. 使用 kill 终止进程:"
            echo "尝试正常终止进程 (SIGTERM, 默认信号): kill $DUMMY_PID"
            kill $DUMMY_PID
            sleep 1 # 等待进程响应信号-
            # 检查进程是否还在运行
            if ps -p $DUMMY_PID > /dev/null; thenecho "进程 $DUMMY_PID 仍在运行,尝试强制终止 (SIGKILL)."tfil kill -9 $DUMMY_PID # 强制杀死sleep 1nf(sput ps -p $DUMMY_PID > /dev/null; thenecho "进程 $DUMMY_PID 无法被杀死。"nd(f; // 2. 逐 echo "进程 $DUMMY_PID 已被强制终止。"fi
            else uff echo "进程 $DUMMY_PID 已正常终止。"
            fi# 5. 再次启动一个进程,演示 & 后台运行
            echo -e "\n5. 演示 '&' 后台运行:"
            ./dummy_process > dummy_process2.log 2>&1 &
            DUMMY_PID2=$!
            echo "另一个模拟进程已在后台启动,PID: $DUMMY_PID2"
            sleep 2
            ps aux | grep "dummy_process" | grep -v "grep"# 杀死第二个进程
            kill $DUMMY_PID2
            echo "第二个模拟进程已终止。"# 清理示例文件
            echo -e "\n清理示例文件..."
            rm -f dummy_process dummy_process.c dummy_process.log dummy_process2.logecho "--- 进程管理演示结束 ---"eUL2.3.4 用户管理:管理你的“团队成员”

            在多用户或多团队协作的嵌入式开发环境中,用户和组的管理也很重要。Eo_wr命令

            作用

            常用参数

            示例0

            逻辑分析/嵌入式应用

            useradd

            创建新用户

            -m (创建家目录), -s (指定Shell)

            sudo useradd -m -s /bin/bash dev_usem为开发团队成员创建独立账号fw5) {

            usermod

            修改用户信息

            -aG (添加到附加组), -L (锁定用户), -U (解锁用户)

            sudo usermod -aG sudo dev_us%s将用户添加到sudo组,赋予管理权限 %clos

            userdel

            删除用户

            -r (同时删除家目录)

            sudo userdel -r old_usil删除不再需要的用户me文件 %

            passwd

            设置或修改用户密码

            passwd usernaUR修改用户密码 素个数,

            sudo

            以root或其他用户身份执行命令

            sudo apt updad,临时提升权限执行管理任务zeint代码示例:用户管理Shell脚本二个#!/bin/bash # 文件名: user_management_demo.sh # 这是一个演示Linux用户管理命令的Shell脚本 echo "--- 开始用户管理演示 (需要root权限) ---" # 检查是否为root用户 if [ "$(id -u)" -ne 0 ]; then echo "此脚本需要root权限,请使用 'sudo ./user_management_demo.sh' 运行。" exit 1 fi TEST_USER="dev_test_user" TEST_GROUP="dev_group" # 1. 创建组 echo -e "\n1. 创建测试组 '$TEST_GROUP'..." groupadd $TEST_GROUP if [ $? -eq 0 ]; then echo "组 '$TEST_GROUP' 创建成功。" else echo "组 '$TEST_GROUP' 可能已存在或创建失败。" fi # 2. 创建用户 echo -e "\n2. 创建测试用户 '$TEST_USER'..." # -m: 创建家目录 # -s /bin/bash: 指定登录Shell为bash # -g $TEST_GROUP: 指定主组 useradd -m -s /bin/bash -g $TEST_GROUP $TEST_USER if [ $? -eq 0 ]; then echo "用户 '$TEST_USER' 创建成功。" else echo "用户 '$TEST_USER' 可能已存在或创建失败。" fi # 3. 设置用户密码 echo -e "\n3. 设置用户 '$TEST_USER' 的密码 (密码将是 'password'):" echo "$TEST_USER:password" | chpasswd # 通过管道设置密码,避免交互式输入 echo "密码已设置为 'password'。" # 4. 添加用户到附加组 (例如 sudo 组) echo -e "\n4. 将用户 '$TEST_USER' 添加到 'sudo' 组..." usermod -aG sudo $TEST_USER if [ $? -eq 0 ]; then echo "用户 '$TEST_USER' 已添加到 'sudo' 组。" else echo "将用户添加到 'sudo' 组失败。" fi # 5. 查看用户信息 echo -e "\n5. 查看用户 '$TEST_USER' 的信息:" id $TEST_USER # 预期:显示用户ID、主组ID、附加组ID (包括sudo组) echo -e "\n6. 切换用户 (手动演示,按 Ctrl+D 退出):" echo "请尝试在新的终端中,使用 'su - $TEST_USER' 切换到该用户,然后尝试 'sudo ls /root'。" # su - $TEST_USER # 7. 锁定和解锁用户 (安全管理) echo -e "\n7. 锁定用户 '$TEST_USER'..." usermod -L $TEST_USER echo "用户 '$TEST_USER' 已被锁定,无法登录。" # 尝试登录会失败 echo -e "\n8. 解锁用户 '$TEST_USER'..." usermod -U $TEST_USER echo "用户 '$TEST_USER' 已被解锁,可以登录。" # 9. 删除用户和组 echo -e "\n9. 删除测试用户 '$TEST_USER' 和组 '$TEST_GROUP'..." # -r: 同时删除用户的家目录和邮件池 userdel -r $TEST_USER if [ $? -eq 0 ]; then echo "用户 '$TEST_USER' 已删除。" else echo "删除用户 '$TEST_USER' 失败。" fi groupdel $TEST_GROUP if [ $? -eq 0 ]; then echo "组 '$TEST_GROUP' 已删除。" else echo "删除组 '$TEST_GROUP' 失败。" fi echo "--- 用户管理演示结束 ---"

            2.4 Linux Shell脚本编程:自动化你的“战场部署”

            Shell脚本是Linux的“自动化魔法”,它允许你将一系列命令组合起来,形成一个可执行的程序。在嵌入式开发中,Shell脚本是实现自动化编译、部署、测试、日志分析等任务的利器!

            2.4.1 概念与变量
            • 什么是Shell脚本?

              • 一个包含Shell命令的文本文件,以 #!/bin/bash (Shebang) 开头,告诉系统使用哪个解释器执行。

              • 无需编译,直接解释执行。

            • 为什么需要Shell脚本?

              • 自动化:将重复性任务自动化,提高效率。

              • 批处理:一次性执行大量命令。

              • 简化复杂操作:将复杂命令封装成简单脚本。

              • 嵌入式应用:用于自动化交叉编译、固件烧录、设备初始化、日志收集、测试脚本等。

            • 变量

              • 用户定义变量VAR_NAME="value",赋值时等号两边不能有空格,使用时加 $ 前缀($VAR_NAME)。

              • 特殊变量

                • $0:脚本文件名。

                • $1, $2, ...:脚本的第一个、第二个参数。

                • $#:脚本参数的总数量。

                • $@:所有参数,每个参数都是独立的字符串("$@")。

                • $*:所有参数,作为一个单独的字符串("$*")。

                • $?:上一条命令的退出状态码(0表示成功,非0表示失败)。

                • $$:当前Shell进程的PID。

                • $!:上一个后台进程的PID。

            • 基本语句

              • echo:打印字符串到标准输出。

              • read:从标准输入读取用户输入。

            代码示例:Shell脚本概念与变量

            #!/bin/bash
            # 文件名: shell_basics_vars.sh
            # 这是一个演示Shell脚本基本概念和变量的脚本echo "--- Shell脚本基础与变量演示 ---"# 1. 脚本的特殊变量
            echo -e "\n1. 脚本的特殊变量:"
            echo "脚本名称: $0"
            echo "第一个参数 (\$1): $1"
            echo "第二个参数 (\$2): $2"
            echo "所有参数 (\$@): $@" # 推荐使用 "$@",因为它能正确处理包含空格的参数
            echo "所有参数 (\$*): $*"
            echo "参数数量 (\$#): $#"
            echo "当前Shell进程ID (\$S): $$" # 脚本自身的PID# 模拟一个命令执行
            ls non_existent_file.txt > /dev/null 2>&1 # 将输出和错误都重定向到空,避免干扰
            echo "上一条命令的退出状态 (\$?): $?" # 0表示成功,非0表示失败# 2. 用户定义变量
            echo -e "\n2. 用户定义变量:"
            MY_NAME="嵌入式老司机" # 定义字符串变量,赋值时等号两边不能有空格
            MY_AGE=30 *headNULL,防 # 定义整数变量 (Shell默认所有变量都是字符串,但可以进行算术运算)
            PROJECT_PATH="/home/user/my_embedded_project"echo "我的名字是: $MY_NAME"
            echo "我的年龄是: $MY_AGE"
            echo "项目路径是: $PROJECT_PATH"# 变量的引用
            echo "再次引用名字: ${MY_NAME}" # 推荐使用大括号,避免歧义,尤其是在变量名后面紧跟其他字符时# 变量的修改
            MY_AGE=$((MY_AGE + 1)) # 使用 $((...)) 进行算术运算
            echo "一年后我的年龄是: $MY_AGE"# 3. read 命令:从用户读取输入
            echo -e "\n3. read 命令 (从用户读取输入):"
            read -p "请输入你的城市: " CITY # -p 选项用于显示提示信息
            echo "你来自: $CITY"# 4. 变量的默认值
            echo -e "\n4. 变量的默认值:"
            # 如果 MY_DEFAULT_VAR 未设置,则使用 "DefaultValue"
            echo "MY_DEFAULT_VAR 的值: ${MY_DEFAULT_VAR:-DefaultValue}"
            MY_DEFAULT_VAR="ActualValue"
            echo "MY_DEFAULT_VAR 的值 (设置后): ${MY_DEFAULT_VAR:-DefaultValue}"echo "--- 演示结束 ---"# 如何运行此脚本:
            # 保存为 shell_basics_vars.sh
            # chmod +x shell_basics_vars.sh
            # ./shell_basics_vars.sh arg1 "arg 2 with space"
            2.4.2 条件判断与循环语句

            Shell脚本的强大之处在于其控制流能力,让你能编写复杂的逻辑。

            • 条件判断

              • if [ condition ]; then ... fi

              • if [ condition ]; then ... else ... fi

              • if [ condition1 ]; then ... elif [ condition2 ]; then ... else ... fi

              • case ... in ... esac

              • 常用条件表达式

                • 文件测试:-f file (文件是否存在), -d dir (目录是否存在), -r file (文件可读), -w file (文件可写), -x file (文件可执行)。

                • 字符串测试:str1 == str2 (字符串相等), str1 != str2 (字符串不相等), -z str (字符串为空), -n str (字符串非空)。

                • 整数测试:num1 -eq num2 (等于), num1 -ne num2 (不等于), num1 -gt num2 (大于), num1 -lt num2 (小于), num1 -ge num2 (大于等于), num1 -le num2 (小于等于)。

                • 逻辑组合:&& (逻辑与), || (逻辑或)。

            • 循环语句

              • for var in list; do ... done:遍历列表。

              • for (( init; condition; increment )); do ... done:C语言风格的for循环。

              • while [ condition ]; do ... done:当条件为真时循环。

              • until [ condition ]; do ... done:当条件为假时循环。

            代码示例:Shell脚本条件判断与循环

            #!/bin/bash
            # 文件名: shell_control_flow.sh
            # 这是一个演示Shell脚本条件判断和循环语句的脚本echo "--- Shell脚本控制流演示 ---"# 1. if-else if-else 语句
            echo -e "\n1. if-else if-else 语句:"
            read -p "请输入一个数字: " numif (( num > 10 )); then # 使用 ((...)) 进行算术比较哈序列、 echo "$num 大于 10"
            elif (( num == 10 )); thenecho "$num 等于 10"
            elseecho "$num 小于 10"
            fi# 2. 文件测试条件
            echo -e "\n2. 文件测试条件:"
            FILE_TO_CHECK="test_file.txt"
            if [ -f "$FILE_TO_CHECK" ]; then # -f 判断是否为普通文件echo "文件 '$FILE_TO_CHECK' 存在。"
            elseecho "文件 '$FILE_TO_CHECK' 不存在,正在创建..."touch "$FILE_TO_CHECK"echo "文件 '$FILE_TO_CHECK' 已创建。"
            fi

            if [ -d "non_existent_dir" ]; then # -d 判断是否为目录

            echo "目录 'non_existent_dir' 存在。" else echo "目录 'non_existent_dir' 不存在。" fi # 3. 字符串测试条件 echo -e "\n3. 字符串测试条件:" STR1="hello" STR2="world" STR_EMPTY="" if [ "$STR1" == "hello" ]; then # == 判断字符串相等 echo "'$STR1' 等于 'hello'" fi if [ "$STR1" != "$STR2" ]; then # != 判断字符串不相等 echo "'$STR1' 不等于 '$STR2'" fi if [ -z "$STR_EMPTY" ]; then # -z 判断字符串是否为空 echo "字符串 STR_EMPTY 为空。" fi if [ -n "$STR1" ]; then # -n 判断字符串是否非空 echo "字符串 STR1 非空。" fi # 4. case 语句 echo -e "\n4. case 语句:" read -p "请输入一个水果 (apple/banana/orange): " fruit case "$fruit" i循环遍 "apple")ms[iget简 echo "你选择了苹果。"单直观。

            ;; # 两个分号表示一个case块结束 "banana" | "orange") # 多个模式可以用 | 分隔 echo "你选择了香蕉或橘子。" ;; *) # 默认匹配,相当于defaultlem是否存下 echo "未知的水果。" ;; esac # 5. for 循环 (遍历列表) echo -e "\n5. for 循环 (遍历列表):" for item in "file1.txt" "file2.c" "image.jpg"; do要外的空 echo "处理文件: $item" done # 6. for 循环 (C语言风格) echo -e "\n6. for 循环 (C语言风格):" for (( i=0; i<3; i++ )); do echo "计数器 i = $i" done # 7. while 循环 echo -e "\n7. while 循环:" count=0 while [ $count -lt 3 ]; do # -lt 判断小于 echo "while 循环计数: $count" ((count++)) # 算术自增 done # 8. until 循环 (与 while 相反,条件为假时执行) echo -e "\n8. until 循环:" until [ $count -eq 0 ]; do # -eq 判断等于 echo "until 循环计数: $count" ((count--)) done # 清理示例文件 rm -f "$FILE_TO_CHECK"/ echo "--- 演示结束 ---"/

            2.4.3 函数

            Shell脚本中的函数可以将一段逻辑封装起来,提高代码的模块化和复用性。

            • 函数定义

              • function_name() { commands; }

              • function function_name { commands; } (推荐第一种)

            • 参数传递:函数内部通过 $1, $2 等访问传入的参数。

            • 返回值:通过 return 命令返回一个退出状态码(0-255),或者通过 echo 打印结果并用 $(function_name) 捕获。

            代码示例:Shell脚本函数

            #!/bin/bash
            # 文件名: shell_functions.sh
            # 这是一个演示Shell脚本函数的脚本echo "--- Shell脚本函数演示 ---"# 1. 定义一个简单的函数
            # 函数定义:推荐使用 function_name() { ... } 格式
            my_function() {echo "这是我的第一个Shell函数!"
            }# 调用函数
            echo -e "\n1. 调用 my_function:"
            my_function# 2. 定义带参数的函数
            # 函数内部通过 $1, $2 等访问传入的参数
            greet_user() {local name=$1 # 使用 local 声明局部变量,避免污染全局变量local age=$2echo "你好, $name! 你今年 $age 岁了。"
            }echo -e "\n2. 调用 greet_user:"
            greet_user "张三" 25
            greet_user "李四" 30# 3. 定义带返回值的函数 (通过 return 退出状态码)
            # Shell函数的 return 只能返回0-255的整数作为退出状态码
            add_numbers() {local num1=$1local num2=$2local sum=$((num1 + num2))return $sum # 返回计算结果作为退出状态码
            }echo -e "\n3. 调用 add_numbers (通过退出状态码返回):"
            add_numbers 10 5
            result=$? # $? 获取上一条命令的退出状态码
            echo "10 + 5 = $result" # 预期:15# 注意:如果结果大于255,会发生截断
            add_numbers 200 100
            result=$?
            echo "200 + 100 = $result (注意:可能被截断)" # 预期:44 (300 % 256 = 44)# 4. 定义带返回值的函数 (通过 echo 打印结果并捕获)
            # 推荐这种方式返回字符串或大整数结果
            multiply_numbers() {local num1=$1local num2=$2local product=$((num1 * num2))echo "$product" # 打印结果到标准输出
            }echo -e "\n4. 调用 multiply_numbers (通过 echo 捕获结果):"
            product_result=$(multiply_numbers 12 10) # 使用 $() 捕获函数标准输出
            echo "12 * 10 = $product_result" # 预期:120# 5. 函数的递归 (不常用,但可行)
            factorial() {local n=$1     un (( n <= 1 )); thenecho 1ent  else
            != Nf ( local prev_factorial=$(factorial $((n - 1)))echo $((n * prev_factorial))fi
            } 
            echo -e "\n5. 调用 factorial (递归):"
            fact_5=$(factorial 5)
            echo "5! = $fact_5" # 预期:120 
            echo "--- 演示结束 ---"

            第三阶段:Linux服务与调试——搭建你的“后勤保障”!(建议2-3周)

            这个阶段,咱们要学习一些在嵌入式开发中非常实用的Linux服务和强大的调试工具。它们就像你的“后勤保障部队”,能大大提高你的开发效率。

            2.5 Linux TFTP服务搭建及使用:固件下载的“快递员”

            Size// 实际应考虑哈希冲突率和动态扩容int hashMapCapacity = numsSize * 2; HashMap* map = createHashMap(hashMapCapacity);// 结果数组,存储两个下标int* result = (int*)malloc(2 * sizeof(int));if (result == NULL) {fprintf(stderr, "结果数组内存分配失败!\n");freeHashMap(map);exit(EXIT_FAILURE);}*returnSize = 2; // 返回数组的大小固定为2for (int i = 0; i < numsSize; i++) {int complement = target - nums[i]; // 计算补数int complement_index = hashMapGet(map, complement); // 在哈希表中查找补数// 如果找到补数,并且补数的下标不是当前元素的下标(防止同一个元素重复使用)if (complement_index != -1 && complement_index != i) {result[0] = complement_index; // 补数的下标result[1] = i;                // 当前元素的下标freeHashMap(map);             // 找到结果后,释放哈希表内存return result;                // 返回结果}// 如果未找到补数,将当前元素及其下标存入哈希表hashMapPut(map, nums[i], i);}// 如果遍历完整个数组都没有找到,理论上题目保证有解,这里作为安全返回*returnSize = 0; // 表示未找到free(result);freeHashMap(map);return NULL;
            }int main() {int nums1[] = {2, 7, 11, 15};int target1 = 9;int returnSize1;int* result1 = twoSum(nums1, sizeof(nums1) / sizeof(nums1[0]), target1, &returnSize1);if (result1 != NULL && returnSize1 == 2) {printf("nums = [2,7,11,15], target = 9 -> 结果: [%d, %d]\n", result1[0], result1[1]);free(result1); // 释放结果数组内存} else {printf("未找到结果。\n");}int nums2[] = {3, 2, 4};int target2 = 6;int returnSize2;int* result2 = twoSum(nums2, sizeof(nums2) / sizeof(nums2[0]), target2, &returnSize2);if (result2 != NULL && returnSize2 == 2) {printf("nums = [3,2,4], target = 6 -> 结果: [%d, %d]\n", result2[0], result2[1]);free(result2);} else {printf("未找到结果。\n");}int nums3[] = {3, 3};int target3 = 6;int returnSize3;int* result3 = twoSum(nums3, sizeof(nums3) / sizeof(nums3[0]), target3, &returnSize3);if (result3 != NULL && returnSize3 == 2) {printf("nums = [3,3], target = 6 -> 结果: [%d, %d]\n", result3[0], result3[1]);free(result3);} else {printf("未找到结果。\n");}return 0;
            }

总结与展望:C语言——嵌入式工程师的“基石”与“利刃”!

兄弟们,恭喜你!你已经完成了嵌入式工程师学习路线的第一部分——C语言的内功心法

通过这一部分的学习和实践,你现在应该对C语言有了更深入的理解:

  • 你掌握了C语言的基本语法核心概念,为后续学习打下了坚实的地基。

  • 你深入理解了指针的奥秘内存管理的精髓,这是C语言最强大也最危险的部分,也是嵌入式开发中必须掌握的“杀手锏”。

  • 你学会了如何使用结构体、共用体和枚举来高效组织数据,并理解了内存对齐和位域在嵌入式中的重要性。

  • 你掌握了预处理和宏的用法,以及它们在条件编译、代码复用中的“魔法”。

  • 你了解了文件I/O的基本操作,这是程序与外部存储交互的基础。

  • 你还通过经典的“两数之和”问题,初步感受了如何将C语言与数据结构和算法结合,磨砺你的“剑锋”,为面试和解决实际问题做准备。

C语言,它不仅仅是一门编程语言,它更是一种思维方式,一种让你能够直接与计算机底层“对话”的能力。它是你成为一名合格嵌入式工程师的基石,也是你手中无往不胜的利刃

下一篇文章,我们将进入嵌入式学习路线的第二部分——Linux操作系统学习路线!我们将深入探索Linux的奥秘,从Shell命令到文件系统,从进程管理到用户管理,让你彻底掌握Linux这个嵌入式开发的“主战场”!

如果你觉得这篇文章对你有帮助,让你对C语言有了更深的理解,请务必点赞、收藏、转发




 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/87513.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/87513.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MCP server资源网站去哪找?国内MCP服务合集平台有哪些?

在人工智能飞速发展的今天&#xff0c;AI模型与外部世界的交互变得愈发重要。一个好的工具不仅能提升开发效率&#xff0c;还能激发更多的创意。今天&#xff0c;我要给大家介绍一个宝藏平台——AIbase&#xff08;<https://mcp.aibase.cn/>&#xff09;&#xff0c;一个…

修改Spatial-MLLM项目,使其专注于无人机航拍视频的空间理解

修改Spatial-MLLM项目&#xff0c;使其专注于无人机航拍视频的空间理解。以下是修改方案和关键代码实现&#xff1a; 修改思路 输入处理&#xff1a;将原项目的视频文本输入改为单一无人机航拍视频/图像输入问题生成&#xff1a;自动生成空间理解相关的问题&#xff08;无需用户…

攻防世界-Reverse-insanity

知识点 1.ELF文件逆向 2.IDApro的使用 3.strings的使用 步骤 方法一&#xff1a;IDA 使用exeinfo打开&#xff0c;发现是32位ELF文件&#xff0c;然后用ida32打开。 找到main函数&#xff0c;然后F5反编译&#xff0c;得到flag。 tip&#xff1a;该程序是根据随机函数生成…

【openp2p】 学习1:P2PApp和优秀的go跨平台项目

P2PApp下面给出一个基于 RESTful 风格的 P2PApp 管理方案示例,供二次开发或 API 对接参考。核心思路就是把每个 P2PApp 当成一个可创建、查询、修改、启动/停止、删除的资源来管理。 一、P2PApp 资源模型 P2PApp:id: string # 唯一标识name: string # …

边缘设备上部署模型的限制之一——显存占用:模型的参数量只是冰山一角

边缘设备上部署模型的限制之一——显存占用&#xff1a;模型的参数量只是冰山一角 在边缘设备上部署深度学习模型已成为趋势&#xff0c;但资源限制是其核心挑战之一。其中&#xff0c;显存&#xff08;或更广义的内存&#xff09;占用是开发者们必须仔细考量的重要因素。许多…

脑机新手指南(二十一)基于 Brainstorm 的 MEG/EEG 数据分析(上篇)

一、脑机接口与神经电生理技术概述 脑机接口&#xff08;Brain-Computer Interface, BCI&#xff09;是一种在大脑与外部设备之间建立直接通信通道的技术&#xff0c;它通过采集和分析大脑信号来实现对设备的控制或信息的输出。神经电生理信号作为脑机接口的重要数据来源&…

[Linux]内核态与用户态详解

内核态和用户态是针对CPU状态的描述。在内核态可以执行一切特权代码&#xff0c;在用户态只能执行那些受限级别的代码。如果需要调用特权代码需要进行内核态切换。 一、内核态和用户态概况 内核态&#xff1a; 系统中既有操作系统的程序&#xff0c;也有普通用户程序。为了安…

如何查看每个磁盘都安装了哪些软件或程序并卸载?

步骤如下&#xff1a; 1、点击电脑桌面左下角&#xff1a; 2、选择【应用和功能】 3、点击下拉框&#xff0c;选择想要查看的磁盘&#xff0c;下方显示的就是所有C磁盘下安装的软件和程序 卸载方法&#xff1a; 点击对应的应用&#xff0c;然后点击卸载即可&#xff1a;

记录一次莫名奇妙的跨域502(badgateway)错误

这里图片加载不了&#xff0c;原文请访问&#xff1a;原文链接 公司的项目&#xff0c;这几天添加了一个统计功能&#xff0c; 本地测试没太大问题&#xff0c;上线后有一个问题&#xff0c;具体现象描述如下&#xff1a; 统计首页接口大约有5-6个&#xff0c;也就是同时需要…

Linux之线程

Linux之线程 线程之形线程接口线程安全互斥锁条件变量&信号量生产者与消费者模型线程池 线程之形 进程是资源分配的基本单位&#xff0c;而线程是进程内部的一个执行单元&#xff0c;也是 CPU 调度的基本单位。 线程之间共享进程地址空间、文件描述符与信号处理&#xff0…

snail-job的oracle sql(oracle 11g)

官网版本的oracle sql中有自增主键&#xff0c;oracle 11g并不支持&#xff0c;所以改成新建索引和触发器的方式自增主键。&#xff08;tip&#xff1a;snail-job的最新版本1.0.0必须使用JDK17&#xff0c; jdk8会报错&#xff0c;所以最后没用起来&#xff09; /*SnailJob Dat…

Windows VMWare Centos Docker部署Nginx并配置对Springboot应用的访问代理

前置博文 Windows VMWare Centos环境下安装Docker并配置MySqlhttps://blog.csdn.net/u013224722/article/details/148928081 Windows VMWare Centos Docker部署Springboot应用https://blog.csdn.net/u013224722/article/details/148958480 # 将已存在的容器设置为宿主机重启后…

暑期数据结构第一天

暑期数据结构第一天 数据元素与数据对象 数据元素--组成数据的基本单位 与数据的关系&#xff1a;是集合的个体 数据对象--性质相同的数据元素的集合 与数据的关系&#xff1a;集合的子集 逻辑结构 &#xff08;1&#xff09;线性结构&#xff0c;所有结点都最多有一个直…

vsCode 扩展中 package.nls.json 文件的作用国际化支持

package.nls.json 代表英文语言文件 {"command.favourite.addtofavourite": "Add to Favourite","command.favourite.deletefavourite": "Remove from Favourite","command.favourite.moveup": "Move Up" } 在 …

结构型智能科技的关键可行性——信息型智能向结构型智能的转换(提纲)

结构型智能科技的关键可行性 ——信息型智能向结构型智能的转换 1.信息型智能科技概述 1.1传统计算机科技的信息型继承者 1.2 信息型智能环境 1.3信息型智能主体 1.4机器学习创造的智能 1.5信息型智能科技的问题 2.结构型智能科技概述 2.1传统计算机科技向真实生命结构…

Excel 数据合并助手SheetDataMerge智能识别同类数据,销售报表处理提升效率

各位Excel小能手们&#xff01;今天给大家介绍个超厉害的玩意儿——SheetDataMerge&#xff0c;这可是专注Excel数据处理的实用工具&#xff01;它就像个数据小管家&#xff0c;核心功能就是智能合并工作表里的同类数据。 软件下载地址安装包 它有多牛呢&#xff1f;能自动识别…

AIStarter平台使用指南:如何一键卸载已下载的AI项目(最新版操作教程)

如果你正在使用 AIStarter 平台&#xff0c;但不知道如何卸载不再需要的 AI 项目&#xff0c;那么这篇简明教程将为你提供清晰的操作指引。 AIStarter 是由知名创作者“熊哥”打造的一款 AI 工具启动器平台&#xff0c;旨在帮助用户快速部署和运行各类 AI 项目。随着平台不断更…

项目中大表治理方案实践

一、业务背景 目前生产库数据库服务器数据存储达到了13T&#xff0c;其中license_spart表数据量达到了200亿&#xff0c;占用7.5T&#xff0c;空间占用率达到54%。而且这张表每年数据增长量达到30亿。其中有效VALID数据占20亿&#xff0c;无效数据INVALID占180亿。由于业务上有…

快应用(QuickApp)技术解析与UniApp跨端开发生态探秘优雅草卓伊凡

快应用&#xff08;QuickApp&#xff09;技术解析与UniApp跨端开发生态探秘优雅草卓伊凡引言&#xff1a;一场由快应用引发的技术辩论近日&#xff0c;优雅草科技的资深开发者卓伊凡在与甲方的一次项目沟通中&#xff0c;因技术选型问题展开了激烈讨论。甲方对快应用&#xff0…

《Font Awesome 参考手册》

《Font Awesome 参考手册》 引言 Font Awesome 是一个功能丰富的图标库,旨在帮助设计师和开发者快速地在网页上添加图标。它提供了超过700个矢量图标,并且支持响应式设计。本文将为您详细介绍 Font Awesome 的使用方法、图标分类、图标定制以及与 CSS 的结合。 一、Font A…