| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> 嵌入式 C语言 补充 -> 正文阅读 |
|
[C++知识库]嵌入式 C语言 补充 |
文章目录前言下面的内容很多都是做一些整理和搬运,借鉴了别人的文章,然后稍微整理、汇总一下,方便自己查看。 C语言结构体中__packed 和位段的理解1、__packedtypedef __packed struct 对齐
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。 比如8位机,那么上述结构体占用1+4+2+8=15byte。 在16位机里,变量就按照2字节对齐,比如成员a,虽然是char类型,地址在0x80000000本身只占1字节,但是下一个成员b却不能使用0x80000001这个地址,而必须使用而必须使用0x80000002,这就是按字长对齐,以上结构体占用的空间也就是2+4+2+8=16字节。 在32位机中,如果a在0x80000000的话,b只能放在0x80000004,因为这里的字长是4个字节。以上结构体占用空间4+4+4+8=20字节也就是说总有一些字节是浪费掉的,这样做的目的很简单,就是因为在大多数计算机体系结构中,对内存操作时按整字存取才能达到最高效率,相当于是以空间换取时间。 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。 比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数 据。显然在读取效率上下降很多。
在MDK中加上
2、位段
其中冒号表示啥意思? C语言中,这叫 “位段”,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。 利用位段能够用较少的位数存储数据, 位域通过一个结构声明来建立:该结构声明为每个字段提供标签,并确定该字段的宽度。例如,下面的声明建立了个4个1位的字段:
根据该声明, prnt包含4个1位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:
由于每个字段恰好为1位,所以只能为其赋值1或0。变量prnt被储存在int大小的内存单元中,但是在本例中只使用了其中的4位。 结构体结构体内存对齐问题先看一个结构体:
在32位编译系统下这一个结构体的字节数是多少呢?是1+4+1+2=8字节吗?不是的,实际结果为12字节。为什么呢?因为编译器会对不足4字节的变量空间自动补齐为4个字节(这就是内存对齐),以提高CPU的寻址效率(32位CPU以4个字节步长寻址的)。 内存对齐是编译器的“管辖范围”。编译器为程序中的每个”数据单元“安排在适当的位置上,以便于能快速的找到每个“数据单元”。对于32bit的CPU,其寻址的步长为4个字节(即unsigned int 字节长度),这就是常说的“4字节对齐”。同理,对于64bit的CPU,就有“8字节对齐”。本文以32位的CPU为例。 请看下面代码:
运行结果为: 可见,正好印证了上述的说法,补齐之后结构体成员a1、a2、a3的地址之间正好相差4个字节,a3与a4之间相差两个字节也是因为在其中多留出了1个空白字节。该程序的运行结果可形象地描述为下图: a1只占用一个字节,为了内存对齐保留了三个空白字节 a3和a4加起来共3字节,为了内存对齐保留了1个空白字节。这就是编译器存储变量时做的见不得人的”手脚“,以方便其雇主——CPU能更快地找到这些变量。 共用体 union结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。 作用:
举个例子我们看一看TI的寄存器封装是怎么做的: 所有的寄存器被封装成联合体类型的,联合体里边的成员是一个
或者直接操控整个寄存器:
如果不是工作于芯片原厂,寄存器的封装应该离我们很远。但我们可以学习使用这种方法,然后用于我们的实际应用开发中。 下面就看一种实际应用: 示例代码:
assert()
断言函数,用于在调试过程中捕捉程序的错误。 函数原型
assert() 会对表达式
assert() 的用法和机制
本例用来计算两个数相除的结果,由于被除数不能为 0,所以我们加入了 assert() 来检测错误。 NDEBUG 宏
这意味着,一旦定义了 NDEBUG 是”No Debug“的意思,也即“非调试”。有的编译器(例如 Visual Studio)在发布(Release)模式下会定义 NDEBUG 宏,在调试(Debug)模式下不会定义定义这个宏;有的编译器(例如 Xcode)在发布模式和调试模式下都不会定义 NDEBUG 宏,这样当我们以发布模式编译程序时,就必须自己在编译参数中增加 NDEBUG 宏,或者在包含 <assert.h> 头文件之前定义 NDEBUG 宏。 调试模式是程序员在测试代码期间使用的编译模式,发布模式是将程序提供给用户时使用的编译模式。在发布模式下,我们不应该再依赖 assert() 宏,因为程序一旦出错,assert() 会抛出一段用户看不懂的提示信息,并毫无预警地终止程序执行,这样会严重影响软件的用户体验,所以在发布模式下应该让 assert() 失效。 修改上面的代码,在包含 <assert.h> 之前定义 NDEBUG 宏:
当以发布模式编译这段代码时,assert() 就会失效。如果希望继续以调试模式编译这段代码,去掉 NDEBUG 宏即可。
C typedef为基本数据类型定义新的类型名也就是说,系统默认的所有基本类型都可以利用 typedef 关键字来重新定义类型名,示例代码如下所示:
这样做的好处就是我们在跨平台移植的时候只需要修改一下 比如
为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称以结构体为例,下面我们定义一个名为 Point 的结构体:
在调用这个结构体时,我们必须像下面的代码这样来调用这个结构体:
在这里,结构体 struct Point 为新的数据类型,在定义变量的时候均要向上面的调用方法一样有保留字 struct,而不能像 int 和 double 那样直接使用 Point 来定义变量。现在,我们利用 typedef 定义这个结构体,如下面的代码所示:
在上面的代码中,实际上完成了两个操作:
为了加深对 typedef 的理解,我们再来看一个结构体例子,如下面的代码所示:
从表面上看,上面的示例代码与前面的定义方法相同,所以应该没有什么问题。但是编译器却报了一个错误,为什么呢?莫非 C 语言不允许在结构中包含指向它自己的指针? 其实问题并非在于 struct 定义的本身,大家应该都知道,C 语言是允许在结构中包含指向它自己的指针的,我们可以在建立链表等数据结构的实现上看到很多这类例子。那问题在哪里呢?其实,根本问题还是在于 typedef 的应用。 在上面的代码中,新结构建立的过程中遇到了 pNext 声明,其类型是 pNode。这里要特别注意的是,pNode 表示的是该结构体的新别名。于是问题出现了,在结构体类型本身还没有建立完成的时候,编译器根本就不认识 pNode,因为这个结构体类型的新别名还不存在,所以自然就会报错。因此,我们要做一些适当的调整,比如将结构体中的 pNext 声明修改成如下方式:
为数组定义简洁的类型名称它的定义方法很简单,与为基本数据类型定义新的别名方法一样,示例代码如下所示:
为指针定义简洁的名称对于指针,我们同样可以使用下面的方式来定义一个新的别名:
对于上面这种简单的变量声明,使用 typedef 来定义一个新的别名或许会感觉意义不大,但在比较复杂的变量声明中,typedef 的优势马上就体现出来了,如下面的示例代码所示:
对于上面变量的声明,如果我们使用 typdef 来给它定义一个别名,这会非常有意义,如下面的代码所示:
实际应用在IAP编程当中,我们要用到跳转到应用程序段: 就用到了
#define用 #define 定义标识符的一般形式为:
#define 和 #include 一样,也是以“#”开头的。凡是以“#”开头的均为预处理指令,#define也不例外。 #define又称宏定义,标识符为所定义的宏名,简称宏。标识符的命名规则与前面讲的变量的命名规则是一样的。#define 的功能是将标识符定义为其后的常量。一经定义,程序中就可以直接用标识符来表示这个常量。是不是与定义变量类似?但是要区分开!变量名表示的是一个变量,但宏名表示的是一个常量。可以给变量赋值,但绝不能给常量赋值。 宏所表示的常量可以是数字、字符、字符串、表达式。其中最常用的是数字。 简单用法:
C 标准库 - <string.h>
经常要用到一些对字符串的操作相关的函数 库函数下面是头文件 string.h 中定义的函数: 关于STM32 __IO 的变量定义这个IO 是指静态 这个 _IO 是指静态 volatile uint32_t 是指32位的无符号整形变量uint32_t 是指32位的无符号整形变量
写一段测试代码如下:
设置优化级别中级 运行后test会被直接取值为3,只有最后一个语句被编译 如果使用
则所有语句都会被编译。test先后被设置成1、2、3 由此可以看出这个作用在IO操作,寄存器操作,特殊变量,多线程变量读写都是很重要。 归纳一下就是:
C 语言中 static 的作用(1)先来介绍它的第一条也是最重要的一条:隐藏。当我们同时编译多个文件时,所有未加 static 前缀的全局变量和函数都具有全局可见性。为理解这句话,我举例来说明。我们要同时编译两个源文件,一个是 a.c,另一个是 main.c。 下面是 a.c 的内容: a.c 文件代码
下面是 main.c 的内容: main.c 文件代码
程序的运行结果是:
你可能会问:为什么在 a.c 中定义的全局变量 a 和函数 msg 能在 main.c 中使用?前面说过,所有未加 static 前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。此例中,a 是全局变量,msg 是函数,并且都没有加 static 前缀,因此对于另外的源文件 main.c 是可见的。 如果加了 static,就会对其它源文件隐藏。例如在 a 和 msg 的定义前加上 static,main.c 就看不到它们了。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static 可以用作函数和变量的前缀。 (2)static 的第二个作用是保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。 共有两种变量存储在静态存储区: **全局变量 ** 和 static 变量 只不过和全局变量比起来,static 可以控制变量的可见范围,说到底 static 还是用来隐藏的。 如果我们在函数内部定义一个static的变量 (3)static 的第三个作用是默认初始化为 0。其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是 0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置 0,然后把不是 0 的几个元素赋值。如果定义成静态的,就省去了一开始置 0 的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加 \0 太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是 \0 。 (3)static 的第三个作用是默认初始化为 0。C语言中const关键字const用来定义常量,如果一个变量被const修饰,那么它的值就不能再被改变,我想一定有人有这样的疑问,C语言中不是有#define吗,干嘛还要用const呢,我想事物的存在一定有它自己的道理,所以说const的存在一定有它的合理性,与预编译指令相比,const修饰符有以下的优点:
下面我们从几个方面来说一下const的用法: 一、修饰局部变量
这两种写法是一样的,都是表示变量n的值不能被改变了,需要注意的是,用const修饰变量时,一定要给变量初始化,否则之后就不能再进行赋值了。 接下来看看
如果没有 二、常量指针与指针常量常量指针是指针指向的内容是常量,可以有一下两种定义方式。
需要注意的是一下两点: 1、常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的引用来改变变量的值的。
2、常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。
指针常量是指指针本身是个常量,不能在指向其他的地址,写法如下:
需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。
区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果const在星号的左边,则为常量指针,如果const在星号的右边则为指针常量。如果我们将星号读作‘指针’,将const读作‘常量’的话,内容正好符合。
指向常量的常指针 是以上两种的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。
三、修饰函数的参数根据常量指针与指针常量,const修饰函数的参数也是分为三种情况 1、防止修改指针指向的内容
其中 strSource 是输入参数,strDestination 是输出参数。给 strSource 加上 const 修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。 2、防止修改指针指向的地址
指针p1和指针p2指向的地址都不能修改。 3、以上两种的结合 指针和内容都不能改 四、修饰函数的返回值如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。 例如函数
如下语句将出现编译错误:
正确的用法是
五、修饰全局变量全局变量的作用域是整个文件,我们应该尽量避免使用全局变量,因为一旦有一个函数改变了全局变量的值,它也会影响到其他引用这个变量的函数,导致除了bug后很难发现,如果一定要用全局变量,我们应该尽量的使用const修饰符进行修饰,这样防止不必要的人为修改,使用的方法与局部变量是相同的。 UNICODE、UNICODE、TEXT、T、T、_TEXT、TEXT宏
unsigned int 和 int 浅析首先来看一下: 1字节(Byte) = 8位(bit)
其中
无符号整型 (unsigned int)在stm32中为例, 有符号整型 ((signed)int)
例子:
输出:
a=65534 65534对应的二进制数字为 1111 1111 1111 1110 由于负数在计算机中以补码形式存在。所以 1111 1111 1111 1110 减一为 1111 1111 1111 1101 再取**反(反码)**为 1000 0000 0000 0010 而 1000 0000 0000 0010 就是 -2 补码有符号数在计算机中存储,用数的最高位存放符号, 正数为0, 负数为1 例如:有符号数 1000 0011,其最高位1代表负,其真正数值是 -3,而不是形式值131(无符号数1000 0011转换成十进制等于131) 原码: 原码就是符号位加上真值的绝对值,即用第一个二进制位表示符号(正数该位为0,负数该位为1),其余位表示值。 反码:
补码:
例如:
为什么要设置补码有人会有疑惑为什么要用反码,补码,不直接用原码呢? 先搞清楚一点反码补码原码是针对二进制数而言,计算机若用原码相加减,正数加正数不会出错,然而正数和负数原码相加就会出错。
如果用原码表示,让符号位也参与计算,显然对于减法来说,结果是不正确的。这也就是为何计算机内部不使用原码表示一个数。
这样计算的话就是正确的了 引进补码的作用是为了让计算机更方便做减法 说白了,补码反码就是为了简化减法而来的,将减号化为负数,再将负数化为补码求加法 跟正数没关系 ,不管是正整数还是正小数,原码,反码,补码都全部相同。 负数时的有符号整型和无符号整型的转换当执行一个运算时(如这里的a>b),如果它的一个运算数是有符号的而另一个数是无符号的,那么C语言会隐式地将有符号 参数强制类型为无符号数,并假设这两个数都是非负的,来执行这个运算。 我们知道,整数在计算机中通常是以补码的形式存在的,而-1的补码(用4个字节储存)为1111,1111,1111,1111。而C语言对于强制类型转换是怎么处理的呢?对大多数C语言的实现,处理同样字长的有符号数和无符号数之间的相互转换的一般规则是:数值可能会改变,但是位模式不变。也就是说,将unsigned int强制类型转换成int,或将int转换成unsigned int底层的位表示保持不变。 也就是说,即使是-1转换成unsigned int之后,它在内存中的表示还是没有改变,即1111,1111,1111,1111。我们知道在计算机的底层,数据是没有类型可言的,所有的数据非0即1。数据类型只有在高层的应用程序才有意义,也就是说,同样的储存表示对于应用程序而言可能对应着不同的数据 例如1111,1111,1111,1111对于有符号数而言它表示-1,但对于无符号数而言,它表示UMax
但是它们的底层存储都是一样的。现在你应该明白为什么-1转换成无符号数之后,就成了UMax了吧。 参考关于“#ifdef __cplusplus” 和 " extern “C” 的问题
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
|
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 | -2024/12/29 4:05:16- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |
数据统计 |