嵌入式系统与嵌入式 C 语言(2)

目录

[1. 指针](#1. 指针)

[1.1 指针的定义](#1.1 指针的定义)

[2. 结构体](#2. 结构体)

[​​​​​​​2.1 基本结构体](#2.1 基本结构体)

[2.2 匿名结构体](#2.2 匿名结构体)

[3. 基本语句](#3. 基本语句)


1. 指针

​​​​​​​1.1 指针的定义

指针是一种存储变量内存地址的特殊变量,通过指针可间接访问和修改目标变量。定义格式为数据类型 *指针名,其中 "数据类型" 指定指针可指向的变量类型,"*" 标识指针类型。

  1. 指针定义示例

|-----------------|---------------------------|
| 定义语句 | 说明 |
| int *p; | 定义指向 int 类型变量的指针 p |
| char *str_ptr; | 定义指向 char 类型变量的指针 str_ptr |
| float *f_ptr; | 定义指向 float 类型变量的指针 f_ptr |
| int **pp; | 定义二级指针(指向 int * 类型指针的指针) |
| void *ptr; | 定义通用指针(可指向任意类型,需强制转换) |

​​​​​​​2.指针初始化注意事项

指针必须初始化(指向合法变量地址),否则会成为 "野指针"(指向未知内存),导致程序崩溃;

初始化方式:通过&(取地址符)获取变量地址,如int a = 10; int *p = &a;;

空指针:int *p = NULL;(指向地址 0,明确表示不指向有效变量,不可解引用)。

  1. 指针的核心操作
  2. 特殊指针

|--------|-----------------|-----------------------------------------------|-------------------------------------------------------------------------------------------|
| 操作类型 | 操作符/格式 | 功能说明 | 示例代码 |
| 取地址 | &变量名 | 获取变量的内存地址,赋值给指针 | inta=20;int*p=&a;//p存储a的地址 |
| 解引用 | *指针名 | 通过指针地址间接访问 / 修改目标变量的值 | *p = 30; // 修改 a 的值为 30printf ("% d", *p); // 输出 30(即 a 的值) |
| 指针算术运算 | p++/p--/p+n/p-n | 指针按 "所指类型字节数" 移动(如 int 指针每次移动 4 字节),用于遍历数组等场景 | int arr[3] = {10,20,30};int *p = arr;p++; // 指向 arr [1]printf ("% d", *p); // 输出 20 |
| 指针赋值 | 指针名 = 地址 | 修改指针存储的地址,让指针指向另一个同类型变量 | int b = 40;p = &b; //p 转而指向 bprintf ("% d", *p); // 输出 40 |

void 指针(通用指针):

格式:void *ptr;,可指向任意类型变量,但使用前需强制转换,如int *p = (int*)ptr;;

用途:作为函数参数接收任意类型指针(如memcpy函数)。

指针数组:

格式:数据类型 *数组名[长度];,数组元素均为指针,如int *arr[5];(5 个 int * 指针的数组);

用途:存储多个字符串(如char *strs[] = {"apple", "banana"};)。

二级指针:

格式:数据类型 **指针名;,存储指针的地址,如int a = 10; int *p = &a; int **pp = &p;;

用途:传递指针变量到函数(如修改指针的指向)。

  1. 指针的作用

间接修改变量:解决函数 "传值调用" 无法修改外部变量的问题(如swap函数交换两个变量值);

高效处理数组:替代数组名访问元素,简化数组遍历和修改(如指针遍历数组比下标更灵活);

动态内存分配:结合malloc/calloc/free实现动态数组,按需分配内存(如根据用户输入确定数组长度);

实现复杂数据结构:作为链表、树、图等动态数据结构的核心(存储节点地址,连接数据元素)。

2. 结构体

​​​​​​​2.1 基本结构体

在 C 语言中,结构体(Struct)是一种用户自定义的数据类型,用于将不同类型的数据组合在一起,形成一个新的复合数据类型。它允许我们将相关的数据元素(可能具有不同的数据类型)封装在一起,方便管理和操作。

|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 类别 | 详细说明 |
| 定义 | 基本格式: struct 结构体名 { 数据类型 成员1; 数据类型 成员2; // ... 其他成员}; 示例: struct Student { char name[20]; int age; float score; }; |
| 作用 | 1. 封装不同类型的数据,形成逻辑整体(如表示 "学生" 包含姓名、年龄等属性) 2. 简化复杂数据的管理,提高代码可读性 3. 便于作为函数参数整体传递,避免多个变量单独传递 4. 支持自定义复合数据类型,扩展 C 语言的数据表示能力 |
| 基本操作 | 1. 申明变量:struct Student stu1; 2. 初始化:struct Student stu1 = {"张三", 18, 95.5}; 3. 成员访问:通过点运算符 .,如 stu1.age = 19; 4. 指针访问:通过箭头运算符 ->,如 p->score = 90;(p 为结构体指针) |
| 高级操作 | 1. 结构体数组:struct Student class[50];(存储多个同类型结构体) 2. 结构体嵌套:结构体成员可以是另一个结构体,如 struct Person { char name[20]; struct Address addr; }; 3. 作为函数参与:传值或传指针(推荐传指针提高效率) |
| 使用场景 | 1. 表示复杂实体(如学生、汽车、坐标点等) 2. 数据集合管理(如链表节点、数组元素) 3. 函数返回多个关联值(通过结构体指针输出) 4. 模拟面向对象思想中的 "类"(结构体 + 函数指针实现简单 |

2.2 匿名结构体

匿名结构体(Anonymous Struct)------ 即没有显式名称的结构体。匿名结构体可以嵌套在其他结构体中,也可以通过 typedef 直接定义为一种类型,从而 "隐藏" 其名称

匿名结构体(隐形结构体)的特点

没有结构体名称,只能在定义时直接使用

通常用于嵌套在其他结构体中,简化代码

可以通过 typedef 定义一个别名来使用

匿名结构体的基本用法

cpp 复制代码
#include <stdio.h>

// 使用typedef定义匿名结构体的别名

typedef struct {

    char brand[20];

    int price;

} Car;  // 现在可以用Car代替这个匿名结构体

int main() {

    Car myCar = {"Toyota", 250000};

    printf("汽车品牌: %s\n", myCar.brand);

    printf("汽车价格: %d\n", myCar.price);

    return 0;

}

3. 基本语句

  • 基本语句
    1. if语句

|--------------|----------------|----------------------------------|------------------------------------------------------------------|
| 语句类型 | 具体语句 | 作用说明 | 使用示例 |
| 申明语句 | 变量声明 | 定义变量,指定数据类型和名称,分配内存空间 | int a; float score = 95.5; char name[20]; |
| | 函数声明 | 告知编译器函数的名称、参数类型和返回值类型,便于跨文件调用 | int add(int x, int y); void printInfo(char* str); |
| | 类型声明(typedef) | 为已有数据类型创建别名,简化复杂类型的使用 | typedef struct { int x; int y; } Point; typedef int* IntPtr; |
| 赋值语句 | 简单赋值 | 将右值赋给左值变量,更新变量的值 | a = 10; sum = x + y; strcpy(name, "Alice"); |
| | 复合赋值 | 结合算术 / 位运算和赋值,简化代码 | a += 5;(等价于a = a + 5) b *= 3;(等价于b = b * 3) |
| 控制语句 | 条件语句(if-else) | 根据条件执行不同代码块 | if (score >= 60) printf("及格"); else printf("不及格"); |
| | 多分支语句(switch) | 根据表达式值匹配 case 分支执行,适合多条件判断 | switch(grade) { case 'A': printf("优秀"); break; default: break; } |
| | 循环语句(for) | 已知循环次数时使用,包含初始化、循环条件和迭代表达式 | for (int i = 0; i < 10; i++) { printf("%d ", i); } |
| | 循环语句(while) | 未知循环次数时使用,仅包含循环条件 | while (count < 5) { count++; printf("%d", count); } |
| | 循环语句(do-while) | 至少执行一次循环体,再判断循环条件 | do { sum += i; i++; } while (i <= 100); |
| | 跳转语句(break) | 终止当前循环或 switch 语句,跳出代码块 | for (...) { if (i == 5) break; } |
| | 跳转语句(continue) | 跳过当前循环剩余部分,直接进入下一次循环 | for (...) { if (i % 2 == 0) continue; printf("%d", i); } |
| | 跳转语句(goto) | 无条件跳转到同一函数内的标签位置,谨慎使用(易破坏代码结构) | if (error) goto fail; ... fail: printf("错误"); |
| 函数相关 | 函数调用语句 | 执行函数体,传递参数并获取返回值 | int result = add(3, 5); printf("Hello"); |
| | 返回语句(return) | 结束函数执行,返回值给调用者(无返回值函数用return;) | return x + y; if (invalid) return; |
| 复合语句 | 块语句({}) | 将多条语句组合为一个整体,形成独立作用域 | if (a > b) { int temp = a; a = b; b = temp; } |
| 其他语句 | 空语句(;) | 不执行任何操作,用于语法需要但无需实际逻辑的场景 | for (i = 0; i < 10; i++);(仅循环计数,无循环体) |
| | 表达式语句 | 由表达式加分号构成,执行表达式并丢弃结果(如函数调用、自增操作) | |

if语句是 C 语言条件分支结构 的核心,通过判断 "条件表达式的真假" 决定代码块的执行逻辑,在嵌入式开发中承担 "硬件状态识别、事件触发判断、参数校验" 的关键角色 ------ 例如判断按键是否按下、串口是否接收数据、传感器值是否超限等。

  1. 基础语法结构

if语句有三种典型形式,适配不同的条件判断场景:

​​​​​​​2. 单分支结构(满足条件则执行)

|-----------------------------------|
| if (条件表达式) { // 条件为真(非0)时执行的代码块 } |

​​​​​​​ 3. 双分支结构(二选一执行)

|--------------------------------------------------|
| if (条件表达式) { // 条件为真时执行 } else { // 条件为假(0)时执行 } |

​​​​​​​ 4. 多分支结构(多条件匹配)

|---------------------------------------------------------------------------|
| if (条件1) { // 条件1为真执行 } else if (条件2) { // 条件2为真执行 } else { // 所有条件均假执行 } |

​​​​​​​5. switch 语句

switch语句是 C 语言多分支条件结构 的核心,通过 "常量匹配 " 快速定位执行分支,在嵌入式开发中承担 "多命令解析、外设模式切换、状态机控制" 的关键角色 ------ 例如解析串口多字节命令、切换传感器采样模式、实现设备状态流转等。

与if语句的 "条件表达式判断" 不同,switch语句更适合 "离散常量值的多分支匹配 ",当分支数量≥3 时,其执行效率(通过跳转表实现)通常高于if-else if链。

​​​​​​​ 6. 基础语法结构

switch语句由 "开关表达式 + case 分支 + default 兜底" 三部分组成,语法框架固定:

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| switch (开关表达式) { // 表达式必须是整数类型(int、char、枚举等) case 常量1: // 常量1:整数/字符/枚举常量,不可重复 // 常量1匹配时执行的代码块 break; // 跳出switch语句(核心,防止穿透) case 常量2: // 常量2匹配时执行的代码块 break; // 可添加多个case分支 default: // 所有case均不匹配时执行(可选但推荐) // 默认处理代码块 break; } |

嵌入式语法约束

  • 开关表达式必须是 "整数类型 "(int、char、enum、uint8_t等),不可为浮点型(如float temp)或字符串;
  • case后的 "常量" 必须是编译期确定的值(如0x01、'A'、枚举常量),不可为变量(如case x:);
  • 每个case分支的常量值必须唯一,不可重复。

难点解析:switch 语句的执行逻辑是 "匹配 case 后顺序执行",必须通过 break 语句跳出,否则会发生 "穿透现象"(即执行完当前 case 后继续执行下一个 case)。

  1. for 循环

for语句是 C 语言循环结构 的核心,通过 "初始化→条件判断→迭代更新 " 的固定流程实现可控循环,在嵌入式开发中承担 "批量数据处理、定时延时、外设参数配置、缓冲区操作" 的关键角色 ------ 例如批量发送串口数据、实现软件延时、配置多个 GPIO 引脚、遍历传感器采样数组等。

与while语句的 "单一条件控制" 不同,for语句将循环的 "初始化、判断、更新" 三要素整合在同一行,更适合 "已知循环次数或有明确迭代逻辑" 的场景,代码结构更紧凑、可读性更强。

​​​​​​​2. 基础语法结构

for语句由 "初始化表达式、循环条件、迭代表达式" 三部分组成,语法框架固定且灵活:

|--------------------------------------------------|
| for (初始化表达式; 循环条件; 迭代表达式) { // 循环体:条件为真时执行的代码块 } |

各部分功能解析

  • 初始化表达式:循环开始前执行一次,用于初始化循环计数器、指针等(可省略,需在循环外初始化);
  • 循环条件:每次循环前判断,结果为真(非 0)则执行循环体,为假(0)则退出循环(可省略,默认恒真,易形成死循环);
  • 迭代表达式:每次循环体执行后执行,用于更新循环计数器、指针偏移等(可省略,需在循环体内手动更新)。

嵌入式语法特性

  1. 三要素均可省略,但分号;必须保留(如for(;;)等价于while(1),为嵌入式常用死循环写法);
  2. 初始化表达式可声明循环变量(C99 及以上标准支持,如for(int i=0; i<10; i++));
  3. 迭代表达式可包含多个操作(用逗号分隔,如for(i=0,j=10; i<j; i++,j--))。
    1. while 循环

while语句是 C 语言条件循环结构 的核心,通过 "先判断、后执行 " 的逻辑实现动态循环,在嵌入式开发中承担 "硬件状态等待、中断标志检测、动态数据处理、死循环主程序" 的关键角色 ------ 例如等待 ADC 转换完成、检测串口接收标志、处理不定长数据、实现主程序无限循环等。

与for语句的 "三要素整合" 不同,while语句仅通过 "单一循环条件" 控制循环,更适合 "循环次数未知、依赖硬件状态变化" 的场景,代码逻辑更聚焦于 "条件判断" 本身。

​​​​​​​3. 基础语法结构

while语句由 "循环条件" 和 "循环体" 两部分组成,语法框架简洁直观:

|-----------------------------------------|
| while (循环条件) { // 循环体:条件为真(非0)时执行的代码块 } |

核心执行流程

  • 执行循环条件判断(表达式结果为真 / 假);
  • 若条件为真,执行循环体代码;
  • 循环体执行完成后,返回步骤 1 重新判断条件;
  • 若条件为假,退出while循环,执行后续代码。

嵌入式语法特性

  1. 循环条件必须是 "可求值的表达式"(如寄存器位判断、变量比较),结果为整数类型(0 为假,非 0 为真);
  2. 循环体可为单条语句(可省略大括号)或代码块(必须加大括号,推荐统一使用);
  3. 若循环条件恒为真(如while(1)),则形成死循环(嵌入式主程序的标准写法)。
  • C语言设计基础
    1. 一个项目是由多个模块程序组成的。

一个完整的项目是由很多模块文件组成的在C语言中模块文件是以工程项目的形式出现的工程项目文件模块程序和包含文件两部分.

1、模块程序包括C程序和汇编程序(*.asm,*.c)

2、包含文件包括C语言和汇编语言的包含文件(*.h,*.inc)

​​​​​​​2. 主函数main()

C程序是由函数构成,一个C源程序至少包含一个函数,一个C应用程序有且只有一个叫main()的函数,即主函数。主函数通过直接书写或通过调用其它函数来完成有关功能

主函数应该简洁和流程清晰,C应用程序总是从main()函数开始执行,不管其物理位置放在何处

函数分为库函数(C编译环境提供的函数,一般在安装目录的C51目录下,调用前应先用#include<库文件名>指令把相应库文件调入)和自定义的函数(需要自己编写,直接调用即可)

​​​​​​​3. 死循环

main()函数中必须有一个死循环,项目要实现的功能都在死循环里完成,没有死循环程序会跑飞。

cpp 复制代码
void main(void)

{
  Init()//初始化

  while(1)

  {

   项目实现的功能

  }

}
  1. C语言变量和程序的调用
    1. 变量的调用

在一个程序中定义的变量要在另一个程序中使用,必须要在调用的程序中用extern声明

​​​​​​​2. 程序的调用

在一个程序中设计的程序或函数要在另一个程序中使用,有两种放方法

1、在调用程序的开头用extern 声明成外部的,即可使用

2、采用包含文件的方法。

  • C条件编译语句

在 C 语言中,条件编译(Conditional Compilation)是一种在编译阶段根据特定条件选择性地编译代码片段的机制。它允许开发者在同一个源文件中包含不同的代码分支,通过编译选项控制哪些代码会被实际编译,哪些会被忽略。

条件编译的主要作用

跨平台兼容性:针对不同操作系统(如 Windows、Linux)或硬件平台编写适配代码,编译时自动选择对应版本

调试与发布版本区分:在调试版本中保留调试打印、断言等代码,发布版本中自动移除

代码复用:在同一套代码中实现不同功能模块,通过条件编译选择启用哪些模块

版本控制:根据软件版本号编译不同功能,实现版本间的功能差异

避免头文件重复包含:通过#ifndef等指令防止头文件被多次包含导致的重复定义错误

常用条件编译语句及用法

  1. #ifdef / #ifndef / #else / #endif

作用:根据宏是否定义来选择编译代码块

语法:

cpp 复制代码
// 形式1:如果宏已定义,则编译代码块1

#ifdef 宏名

    代码块1  // 宏定义时编译

#else

    代码块2  // 宏未定义时编译(可选)

#endif

// 形式2:如果宏未定义,则编译代码块

#ifndef 宏名

    代码块  // 宏未定义时编译

#else

    代码块  // 宏定义时编译(可选)

#endif

示例:

#define DEBUG  // 定义DEBUG宏

int main() {

#ifdef DEBUG

    printf("调试模式:程序开始执行\n");  // 会被编译

#else

    printf("发布模式:程序开始执行\n");  // 不会被编译

#endif

    return 0;

}

​​​​​​​2.  #if / #elif / #else / #endif

作用:根据表达式的值(编译时可计算)选择编译代码块,支持多分支判断

语法:

#if 常量表达式1

    代码块1  // 表达式1为真时编译

#elif 常量表达式2

    代码块2  // 表达式2为真时编译(可选)

#else

    代码块3  // 所有表达式为假时编译(可选)

#endif

示例:

#define VERSION 2

int main() {

#if VERSION == 1

    printf("版本1:基础功能\n");

#elif VERSION == 2

    printf("版本2:基础功能+高级功能\n");  // 会被编译

#else

    printf("未知版本\n");

#endif

    return 0;

}

​​​​​​​4. 头文件保护(防止重复包含)

这是条件编译最常见的应用之一,几乎所有头文件都会使用:

cpp 复制代码
// 文件名:student.h

#ifndef STUDENT_H  // 如果STUDENT_H未定义

#define STUDENT_H  // 定义STUDENT_H



struct Student {

    char name[20];

    int age;

};

#endif  // 结束条件编译

当该头文件被多次包含时,第一次会定义STUDENT_H,后续包含会因STUDENT_H已定义而跳过内容,避免结构体等重复定义。

如何定义宏

在代码中定义:使用#define 宏名(如#define DEBUG)

在编译命令中定义:通过编译器参数(如 GCC 的-D选项),无需修改代码

bash

gcc main.c -DDEBUG -o main # 编译时定义DEBUG宏

gcc main.c -DVERSION=3 -o main # 定义带值的宏

条件编译是 C 语言中处理代码多版本、多平台适配的重要工具,合理使用可以显著提高代码的灵活性和可维护性。

  • 头文件编写方法
    1. 头文件的编写方法

在单片机中需要用到大量的自己编写的头文件,也称包含文件。头文件的编写一般采用一下格式

cpp 复制代码
#ifndef    xxxx

#define   xxxx

#endif

示例:

#ifndef    __USART_H

#define   __USART_H

#endif

命名文件为usart.h

​​​​​​​2. 头文件的包含

在程序中要使用头文件,必须包含头文件头,头文件的包含有两种方法

1:#include <头文件名>

2:#include "头文件名"

采用第一种方法时头文件必须在KEIL的编译器目录里,采用第二种方法时,头文件必须在工作目录里

相关推荐
235164 小时前
【LeetCode】146. LRU 缓存
java·后端·算法·leetcode·链表·缓存·职场和发展
weixin_307779135 小时前
使用Python高效读取ZIP压缩文件中的UTF-8 JSON数据到Pandas和PySpark DataFrame
开发语言·python·算法·自动化·json
柳安忆5 小时前
【论文阅读】Sparks of Science
算法
web安全工具库6 小时前
从课堂笔记到实践:深入理解Linux C函数库的奥秘
java·数据库·算法
爱编程的鱼7 小时前
C# 变量详解:从基础概念到高级应用
java·算法·c#
HalvmånEver8 小时前
红黑树实现与原理剖析(上篇):核心规则与插入平衡逻辑
数据结构·c++·学习·算法·红黑树
哆啦刘小洋8 小时前
T:堆的基本介绍
算法
AresXue8 小时前
你是否也在寻找二进制和字符串的高效转换算法?
算法
Swift社区9 小时前
从 0 到 1 构建一个完整的 AGUI 前端项目的流程在 ESP32 上运行
前端·算法·职场和发展