第二章 常用的C语言知识点
1)实验平台:正点原子DNESP32S3开发板
2)章节摘自【正点原子】ESP32-S3利用指南—IDF版 V1.6
3)购买链接:https://detail.tmall.com/item.htm?&id=768499342659
4)全套实验源码+手册+视频下载地点:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
本章将为大家介绍常用的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,左移(> 2; /* b = 0000 1111 =15 */[/code]利用位操作可以实行许多有用的任务,例如检查一个数的特定位是否为1,设置或清除一个数的特定位,快速地举行整数乘法和除法,等等。然而,利用位操作时必要特别警惕,因为错误的操作大概会导致不可猜测的结果。
5.2 define宏定义
在C语言中,#define是预处理指令的一部分,用于定义宏。宏可以是一个简单的常量、一个带有参数的表达式或是一个代码块。利用宏可以在编译前替换代码中的特定部分,从而实当代码的重用、简化和提高可读性。
- 无参数宏定义
无参数宏定义是最简单的形式,它只是一个标识符和一个值的组合。
- int a = 60; /* 60 = 0011 1100 */
- int b = a << 2; /* b = 0111 1000 =240 */
复制代码 在代码中,每次出现PI,预处理器都会将其替换为3.14159。
2. 带参数宏定义
带参数宏定义允许宏担当一个或多个参数,并返回一个表达式。- int a = 60; /* 60 = 0011 1100 */
- int b = a >> 2; /* b = 0000 1111 =15 */
复制代码 这里,SQUARE是一个宏,它担当一个参数x,并返回x的平方。注意,宏中的参数应该用括号括起来,以避免因为运算符优先级导致的问题。
3. 宏定义的代码块
固然不常见,但宏定义还可以包含多个语句。这通常用于实现复杂的操作或内联函数。SWAP宏交换两个变量的值。由于宏是文本替换,因此必要用do ... while(0)来确保宏内部的多条语句被看成一个单独的语句块处理。
4. #undef
可以利用#undef指令来取消一个宏的定义。- int a = 60; /* 60 = 0011 1100 */
- int b = a << 2; /* b = 0111 1000 =240 */ /* ... 利用PI... */ #undef PI/* 之后PI不再是一个宏 */
复制代码
- 宏定义的注意事项
l 宏定义只是简单的文本替换,没有类型检查或作用域限制。
l 宏大概会因为参数的运算符优先级导致预期之外的举动,所以利用时要特别警惕。
l 宏定义大概会导致代码膨胀,因为每个宏的利用都会导致相同代码的重复插入。
l 避免在宏中利用复杂的表达式或逻辑,因为这会增加代码阅读和维护的难度。
l 利用宏时要谨慎,以避免出现不测的副作用或难以调试的错误。
5.3 ifdef条件编译
在C语言中,#ifdef是预处理指令的一部分,用于条件编译。条件编译允许你在编译时根据特定的条件来决定是否包含某些代码段。这对于编写跨平台或可设置的代码非常有用。
#ifdef检查是否定义了一个宏(利用#define指令)。假如宏已经定义,那么#ifdef和紧随其后的#endif之间的代码将被包含在编译中。假如宏没有定义,那么这部分代码将被忽略。
下面是一个简单的例子:- #define SWAP(a, b) do { \
- typeof(a) temp = a; \
- a = b; \
- b = temp; \
- } while (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的全局变量:- #define PI 3.14159
- /* ... 使用PI... */
- #undef PI
- /* 之后PI不再是一个宏 */
复制代码 现在,假如你想在另一个源文件main.c中利用这个变量,你可以这样声明它:- #define FEATURE_A
-
- int main() {
- #ifdef FEATURE_A
- /* 这段代码将被编译,因为FEATURE_A已经定义 */
- printf("FeatureA is enabled.\n");
- #else
- /* 这段代码不会被编译 */
- printf("FeatureA is disabled.\n");
- #endif
-
- #ifndef FEATURE_B
- /* 这段代码将被编译,因为FEATURE_B没有被定义 */
- printf("FeatureB is not defined.\n");
- #else
- /* 这段代码不会被编译 */
- printf("FeatureB is defined.\n");
- #endif
-
- return 0;
- }
复制代码 在这个例子中,main.c并没有定义globalVar,而是利用了extern关键字来声明它。这告诉编译器,globalVar的定义存在于其他地方,而且在链接阶段,链接器会找到这个定义。
2,函数外部声明
同样,当你想在一个源文件中调用另一个源文件中定义的函数时,你必要利用extern来声明这个函数。例如,假设你有一个名为functions.c的文件,它定义了一个名为myFunction的函数:- /* variables.c */
- #include <stdio.h>
-
- int globalVar = 100; /* 定义全局变量 */
复制代码 然后,在main.c中,你可以这样声明并利用这个函数:- /* main.c */
- #include <stdio.h>
-
- extern int globalVar; /* 外部声明全局变量 */
-
- int main()
- {
- printf("Thevalue of globalVar is: %d\n", globalVar);
- 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_t、uint16_t和uint32_t等都是由typedef定义的类型别名,其定义如下:- /* functions.c */
- #include <stdio.h>
-
- void myFunction() {
- printf("Thisis my function!\n");
- }
复制代码 这么一来就可以在编写程序代码的时候利用uint8_t等代替unsigned char等,极大地提高的代码的可读性可编写代码的效率。
5.6 struct结构体
struct用于定义结构体,结构体就是一堆变量的聚集,结构体中的成员变量的作用一般都是相互关联的,定义结构体的形式如下:- /* main.c */
- #include <stdio.h>
-
- extern void myFunction(); /* 外部声明函数*/
-
- int main()
- {
- myFunction(); /* 调用函数*/
- return 0;
- }
复制代码 例如:- typedef unsigned char uint8_t;
- typedef unsigned short int uint16_t;
- typedef unsigned int uint32_t;
复制代码 如上举例的结构体定义,一堆描述LCD屏幕的变量的聚集,此中包含了LCD屏幕的宽度和高度。
结构体变量的定义如下:- struct 结构体名
- {
- 成员变量1的定义;
- 成员变量2的定义;
- ......
- };
复制代码 如上,就定义了一个名为lcd_device的结构体变量,那么怎么访问这个结构体变量中的成员变量呢?如下:- struct lcd_device_struct
- {
- uint16_t width;
- uint16_t height;
- };
复制代码 如上就展示了结构体变量中成员变量的访问操作。
任何时候,我们只必要修改结构体成员变量,往结构体中间加入新的成员变量,而不必要修改函数定义就可以到达修改入口参数同样的目的了。这样的利益是不用修改任何函数定义就可以到达增加变量的目的。
理解了结构体在这个例子中间的作用吗?在以后的开发过程中,假如你的变量定义过多,假如某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样大概可以提高你的代码的可读性。
利用结构体组合参数,可以提高代码的可读性,不会以为变量定义杂乱。当然结构体的作用就远远不止这个了,同时,VSCode中用结构体来定义外设也不仅仅只是这个作用,这里我们只是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。背面一节我们还会解说结构体的一些其他知识。
5.7 指针
指针是一个值指向地点的变量(或常量),其本质是指向一个地点,从而可以访问一片内存地区。在编写ESP32代码的时候,或多或少都要用到指针,它可以使不同代码共享同一片内存数据,也可以用作复杂的链接性的数据结构的构建,比如链表,链式二叉树等,而且,有些地方必须利用指针才能实现,比如内存管理等。
申明指针我们一般以p开头,如:- struct lcd_device_struct lcd_device;
复制代码 如上,就定义一个名为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”,以此类推。
为了更加直观的演示,我们试着编写如下代码并观察输出结果的变革:- lcd_device.width = 240;
- printf("LCD Height: %d\n", lcd_device.height);
复制代码 此代码的输出结果为:
图5.7.1 终端输出结果
①: p_num:是uint8_t类型指针,指向temp变量的地点,其值即是temp变量的地点。
②:*p_num:取p_num指向的地点所存储的值,即temp的值。
③:p_num:指针的地点。
④:&p_num:取p_num指针的地点,即指针自身的地点。
以上,就是指针的简单利用和基本概念阐明,指针的详细知识和利用范例大家可以百度学习,网上有非常多的资料可供参考。指针是C语言的英华,在后续的代码中我们将会大量用到各种指针,大家务必好好学习和相识指针的利用。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |