单片机/MCU论坛
直播中

jf_85110202

未满1年用户 168经验值
擅长:嵌入式技术 开发板 仪表仪器 单片机
私信 关注
[文章]

《DNESP32S3使用指南-IDF版_V1.6》第二章 常用的C语言知识点

第二章常用的C语言知识点

本章将为大家介绍常用的C语言知识点。对于已经熟练掌握C语言的开发者,可以选择跳过本节内容;而对于基础相对薄弱的开发者,我们强烈建议深入学习本章内容,以便为后续的ESP32开发打下坚实基础。由于C语言是一门博大精深的编程语言,不可能在章节内全面讲解。因此,本节将专注于复习与ESP32开发密切相关的几个核心C语言知识点,旨在帮助大家更好地学习并编写ESP32代码。通过本章的学习,您将能够回顾和巩固在ESP32开发中常用的C语言特性,从而提升您的编程技能,为后续的项目开发做好准备。
本章将分为如下几个小节:
5.1 位操作
5.2 define宏定义
5.3 ifdef条件编译
5.4 extern外部申明
5.5 typedef类型别名
5.6 struct结构体
5.7 指针


5.1 位操作
位操作是直接在整数的二进制位上进行操作的一种技术。C语言提供了多种位操作符,这些操作符允许程序员对整数的二进制位进行读取、设置、清除或翻转。位操作在处理底层硬件、优化代码、处理二进制数据等方面非常有用C语言支持如下6位操作:
运算符
含义
运算符
含义
&
按位与
~
按位取反
|
按位或
<<
左移
^
按位异或
>>
右移
5.1.1 六种位操作
以下是C语言中的一些位操作符及其描述:
1, 按位与(&):如果两个相应的二进制位都为1,则结果为1,否则为0
int a = 60;            /* 60 =        0011 1100 */
int b = 13;            /* 13 =        0000 1101 */
int c = a & b;         /* c =         0000 1100 */
2,按位或(|):如果两个相应的二进制位中至少有一个为1,则结果为1,否则为0
int a = 60;            /* 60 =        0011 1100 */
int b = 13;            /* 13 =        0000 1101 */
int c = a | b;         /* c =         0011 1101 */
3,按位异或(^):如果两个相应的二进制位不同,则结果为1,否则为0
int a = 60;            /* 60 =        0011 1100 */
int b = 13;            /* 13 =        0000 1101 */
int c = a ^ b;         /* c =         0011 0001 */
4,按位取反(~):将整数的二进制位翻转。
int a = 60;            /* 60 =        0011 1100 */
int b = ~a;           /* b =         1100 0011 */
5,左移(<<):将整数的二进制位向左移动指定的位数,右侧用0填充。
int a = 60;            /* 60 =        0011 1100 */
int b = a << 2;        /* b =         0111 1000 = 240 */
6,右移(>>):将整数的二进制位向右移动指定的位数,左侧根据整数的符号位填充01
int a = 60;            /* 60 =        0011 1100 */
int b = a >> 2;        /* b =         0000 1111 = 15 */
使用位操作可以执行许多有用的任务,例如检查一个数的特定位是否为1,设置或清除一个数的特定位,快速地进行整数乘法和除法,等等。然而,使用位操作时需要特别小心,因为错误的操作可能会导致不可预测的结果
5.2 define宏定义
C语言中,#define是预处理指令的一部分,用于定义宏。宏可以是一个简单的常量、一个带有参数的表达式或是一个代码块。使用宏可以在编译前替换代码中的特定部分,从而实现代码的重用、简化和提高可读性。
1. 无参数宏定义
无参数宏定义是最简单的形式,它只是一个标识符和一个值的组合。
#define PI 3.14159
在代码中,每次出现PI,预处理器都会将其替换为3.14159
2. 带参数宏定义
带参数宏定义允许宏接受一个或多个参数,并返回一个表达式。
#define SQUARE(x) ((x) * (x))
这里,SQUARE是一个宏,它接受一个参数x,并返回x的平方。注意,宏中的参数应该用括号括起来,以避免因为运算符优先级导致的问题。
3. 宏定义的代码块
虽然不常见,但宏定义还可以包含多个语句。这通常用于实现复杂的操作或内联函数。
#define SWAP(a, b) do {   
    typeof(a) temp = a;   
    a = b;   
    b = temp;   
} while (0)
SWAP宏交换两个变量的值。由于宏是文本替换,因此需要用do ... while(0)来确保宏内部的多条语句被当作一个单独的语句块处理。
4. #undef
可以使用#undef指令来取消一个宏的定义。
#define PI 3.14159  
/* ... 使用PI... */  
#undef PI  
/* 之后PI不再是一个宏 */
5. 宏定义的注意事项
l 宏定义只是简单的文本替换,没有类型检查或作用域限制。
l 宏可能会因为参数的运算符优先级导致预期之外的行为,所以使用时要特别小心。
l 宏定义可能会导致代码膨胀,因为每个宏的使用都会导致相同代码的重复插入。
l 避免在宏中使用复杂的表达式或逻辑,因为这会增加代码阅读和维护的难度。
l 使用宏时要谨慎,以避免出现意外的副作用或难以调试的错误。
5.3 ifdef条件编译
C语言中,#ifdef是预处理指令的一部分,用于条件编译。条件编译允许你在编译时根据特定的条件来决定是否包含某些代码段。这对于编写跨平台或可配置的代码非常有用。
#ifdef检查是否定义了一个宏(使用#define指令)。如果宏已经定义,那么#ifdef和紧随其后的#endif之间的代码将被包含在编译中。如果宏没有定义,那么这部分代码将被忽略。
下面是一个简单的例子:
#define FEATURE_A  
  
int main() {  
    #ifdef FEATURE_A  
        /* 这段代码将被编译,因为FEATURE_A已经定义 */  
        printf("Feature A is enabled.n");  
    #else  
        /* 这段代码不会被编译 */  
        printf("Feature A is disabled.n");  
    #endif  
  
    #ifndef FEATURE_B  
        /* 这段代码将被编译,因为FEATURE_B没有被定义 */
        printf("Feature B is not defined.n");  
    #else  
        /* 这段代码不会被编译 */  
        printf("Feature B is defined.n");  
    #endif  
  
    return 0;  
}
在这个例子中,FEATURE_A被定义了,所以#ifdef FEATURE_A和#endif之间的代码会被编译。而FEATURE_B没有被定义,所以#ifndef FEATURE_B和#endif之间的代码会被编译。
除了#ifdef,还有其他的条件编译指令:
l #ifndef:如果宏没有定义,则包含代码。
l #if:用于检查宏是否定义以及它的值是否为真(非零)。
l #elif:与#if和#else结合使用,用于检查多个条件。
l #else:如果前面的#if或#ifdef条件不满足,则包含代码。
l #endif:标记条件编译块的结束。
这些指令通常在源代码的顶部使用,以根据特定的配置或平台条件包含或排除代码段。例如,你可能想要在不同的操作系统上使用不同的系统调用,或者在调试和发布版本中包含或排除调试代码。
5.4 extern外部申明
C语言中,extern关键字用于声明一个变量或函数,而不是定义它。extern告诉编译器,变量的定义或函数的实现在其他地方,可能是在另一个源文件中。这允许程序的不同部分共享同一个变量或函数,而无需在每个文件中都重复定义它。
1,变量外部声明
当你想在一个源文件中使用另一个源文件中定义的变量时,你需要使用extern来声明这个变量。例如,假设你有一个名为variables.c的文件,它定义了一个名为globalVar的全局变量:
/* variables.c */  
#include   
  
int globalVar = 100; /* 定义全局变量 */
现在,如果你想在另一个源文件main.c中使用这个变量,你可以这样声明它:
/* main.c */  
#include   
  
extern int globalVar; /* 外部声明全局变量 */  
  
int main()
{  
    printf("The value of globalVar is: %dn", globalVar);  
    return 0;  
}
在这个例子中,main.c并没有定义globalVar,而是使用了extern关键字来声明它。这告诉编译器,globalVar的定义存在于其他地方,并且在链接阶段,链接器会找到这个定义。
2,函数外部声明
同样,当你想在一个源文件中调用另一个源文件中定义的函数时,你需要使用extern来声明这个函数。例如,假设你有一个名为functions.c的文件,它定义了一个名为myFunction的函数:
/* functions.c */
#include   
  
void myFunction() {  
    printf("This is my function!n");  
}
然后,在main.c中,你可以这样声明并使用这个函数:
/* main.c */  
#include   
  
extern void myFunction(); /* 外部声明函数*/  
  
int main()
{  
    myFunction(); /* 调用函数*/  
    return 0;  
}
在这个例子中,main.c中并没有定义myFunction,而是通过extern关键字进行了声明。在编译和链接阶段,链接器会找到myFunction的定义,并将其与main.c中的调用关联起来。
3,注意事项
l extern只能用于声明变量或函数,不能用于定义。定义会分配内存空间,而声明不会。
l 当你在一个源文件中定义了一个变量或函数,并想在另一个源文件中使用它时,你需要在第二个源文件中使用extern进行声明。
l 外部声明必须在使用变量或函数之前进行。
l 在链接阶段,链接器会查找所有extern声明的定义,如果找不到,就会出现链接错误。
l 如果多个源文件定义了同一个extern变量,那么链接器会将其视为错误,因为同一个变量只能有一个定义。但是,多个源文件可以包含同一个extern函数的声明,因为这个函数可能在不同的地方有不同的实现。
5.5 typedef类型别名
typedef用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。例如在编写程序时经常使用到的uint8_tuint16_tuint32_t等都是由typedef定义的类型别名,其定义如下:
typedef unsigned char                uint8_t;
typedef unsigned short int        uint16_t;
typedef unsigned int                uint32_t;
这么一来就可以在编写程序代码的时候使用uint8_t等代替unsigned char等,极大地提高的代码的可读性可编写代码的效率。
5.6 struct结构体
struct用于定义结构体,结构体就是一堆变量的集合,结构体中的成员变量的作用一般都是相互关联的,定义结构体的形式如下:
struct 结构体名
{
    成员变量1的定义;
    成员变量2的定义;
    ......
};
例如:
struct lcd_device_struct
{
    uint16_t width;
    uint16_t height;
};
如上举例的结构体定义,一堆描述LCD屏幕的变量的集合,其中包含了LCD屏幕的宽度和高度。
结构体变量的定义如下:
struct lcd_device_struct lcd_device;
如上,就定义了一个名为lcd_device的结构体变量,那么怎么访问这个结构体变量中的成员变量呢?如下:
lcd_device.width = 240;
printf("LCD Height: %dn", lcd_device.height);
如上就展示了结构体变量中成员变量的访问操作。
任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义就可以达到增加变量的目的。
    理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可以提高你的代码的可读性。
使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作用就远远不止这个了,同时,VSCode中用结构体来定义外设也不仅仅只是这个作用,这里我们只是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲解结构体的一些其他知识。
5.7 指针
指针是一个值指向地址的变量(或常量),其本质是指向一个地址,从而可以访问一片内存区域。在编写ESP32代码的时候,或多或少都要用到指针,它可以使不同代码共享同一片内存数据,也可以用作复杂的链接性的数据结构的构建,比如链表,链式二叉树等,而且,有些地方必须使用指针才能实现,比如内存管理等。
申明指针我们一般以p开头,如:
char * p_str = “This is a test!;
如上,就定义一个名为p_str的指针变量,并将p_str指针指向了字符串This is a string!”保存在内存中首地址,对于ESP32来说,此时p_str的值就是一个32位的数,这个数就是一个内存地址,这个内存地址就是上述字符串保存在内存中的首地址。
通过p_str指针就可以访问到字符串This is a string!”,那具体是如何访问的呢?前面说p_str保存的是一个内存地址,那么就可以通过这个内存地址去内存中读取数据,通过*p_str就可以访问地址为p_str的内存数据,*(p_str + 1)可以访问下一个内存地址中的数据。
知道了如何访问内存中的数据,但是读取到的数据要如何解析呢?这就有p_str指针的类型决定了。在这个例子中p_str是一个char类型的指针,那么访问*p_str就是访问地址为p_str,大小为sizeof(char)(一般为一个字节)的一段内存数据,在这个例子中就可以读取到字符T”, 读取*(p_str + 1)就是h”,以此类推。
为了更加直观的演示,我们试着编写如下代码并观察输出结果的变化:
/**
* @Brief       程序入口
* @param      
* @retval      
*/
void app_main(void)
{
    esp_err_t ret;
    uint8_t temp = 0x88;                                                /* 定义变量temp */
    uint8_t *p_num = &temp;                                                /* 定义指针p_num,指向temp的地址 */
   
    ret = nvs_flash_init();                                                /* 初始化NVS */
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
            ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    printf("temp:0X%Xrn", temp);                              /* 打印temp的值 */
    printf("*p_num:0X%Xrn", *p_num);                         /* 打印*p_num的值 */
    printf("p_num:0X%xrn", (unsigned int)(long)p_num);        /* 打印p_num的值 */
    printf("&p_num:0X%xrn", (unsigned int)(long)&p_num);        /* 打印&p_num的值 */
}
此代码的输出结果为:
file:///C:UsersALIENTEKAppDataLocalTempksohtml1792wps1.jpg
5.7.1 终端输出结果
①: p_num:是uint8_t类型指针,指向temp变量的地址,其值等于temp变量的地址。
②:*p_num:取p_num指向的地址所存储的值,即temp的值。
③:p_num:指针的地址。
④:&p_num:取p_num指针的地址,即指针自身的地址。
以上,就是指针的简单使用和基本概念说明,指针的详细知识和使用范例大家可以百度学习,网上有非常多的资料可供参考。指针是C语言的精髓,在后续的代码中我们将会大量用到各种指针,大家务必好好学习和了解指针的使用。

更多回帖

发帖
×
20
完善资料,
赚取积分