文章目录
前言
下面的内容很多都是做一些整理和搬运,借鉴了别人的文章,然后稍微整理、汇总一下,方便自己查看。
C语言结构体中__packed 和位段的理解
1、__packed
typedef __packed struct
对齐
typedef struct
{
char a; //1byte
int b; //4byte
char c[2]; //2byte
double d; //8byte
}
现代计算机中内存空间都是按照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数 据。显然在读取效率上下降很多。
#pragma pack(1)
:使结构体按照1字节对齐
#pragma pack()
:取消指定对齐,恢复缺省对齐
在MDK中加上__packed
关键字,可以得到非字节对齐的紧凑结构,如果编译器不支持__packed
关键字,可以将其定义为
#define __packed
2、位段
struct __packed data
{
unsigned a : 2;
unsigned b : 6;
unsigned c : 4;
unsigned d : 4;
int i;
} data;
其中冒号表示啥意思?
C语言中,这叫 “位段”,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。
利用位段能够用较少的位数存储数据, :
后面的数字用来限定成员变量占用的位数。
位域通过一个结构声明来建立:该结构声明为每个字段提供标签,并确定该字段的宽度。例如,下面的声明建立了个4个1位的字段:
struct
{
unsigned int autfd : 1; //用来限定成员变量占用的位数
unsigned int bldfc : 1;
unsigned int undin : 1;
unsigned int itals : 1;
}prnt;
根据该声明, prnt包含4个1位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:
prnt.itals = 0:
prnt.undin = 1;
由于每个字段恰好为1位,所以只能为其赋值1或0。变量prnt被储存在int大小的内存单元中,但是在本例中只使用了其中的4位。
结构体
结构体内存对齐问题
先看一个结构体:
typedef struct Test
{
char a1;
int a2;
char a3;
short a4;
} Test_T;
在32位编译系统下这一个结构体的字节数是多少呢?是1+4+1+2=8字节吗?不是的,实际结果为12字节。为什么呢?因为编译器会对不足4字节的变量空间自动补齐为4个字节(这就是内存对齐),以提高CPU的寻址效率(32位CPU以4个字节步长寻址的)。
内存对齐是编译器的“管辖范围”。编译器为程序中的每个”数据单元“安排在适当的位置上,以便于能快速的找到每个“数据单元”。对于32bit的CPU,其寻址的步长为4个字节(即unsigned int 字节长度),这就是常说的“4字节对齐”。同理,对于64bit的CPU,就有“8字节对齐”。本文以32位的CPU为例。
请看下面代码:
#include <stdio.h>
typedef struct Test
{
char a1;
int a2;
char a3;
short a4;
} Test_T;
int main(void)
{
Test_T T;
printf("\nsizeof(T) = %d\n", sizeof(T));
printf("a1地址:%d\n", (unsigned int)&T.a1);
printf("a2地址:%d\n", (unsigned int)&T.a2);
printf("a3地址:%d\n", (unsigned int)&T.a3);
printf("a4地址:%d\n", (unsigned int)&T.a4);
return 0;
}
运行结果为:
可见,正好印证了上述的说法,补齐之后结构体成员a1、a2、a3的地址之间正好相差4个字节,a3与a4之间相差两个字节也是因为在其中多留出了1个空白字节。该程序的运行结果可形象地描述为下图:
a1只占用一个字节,为了内存对齐保留了三个空白字节
a3和a4加起来共3字节,为了内存对齐保留了1个空白字节。这就是编译器存储变量时做的见不得人的”手脚“,以方便其雇主——CPU能更快地找到这些变量。
共用体 union
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
作用:
-
为了方便看懂代码
比如说想写一个3 * 3的矩阵,可以这样写:struct Matrix { union { struct { float _f11, _f12, _f13, _f21, _f22, _f23, _f31, _f32, _f33; }; float f[3][3]; }_matrix; }; struct Matrix m;
这两个东西共同使用相同的空间,所以没有空间浪费,在需要整体用矩阵的时候可以用
m._matrix.f
(比如说传参,或者是整体赋值等);需要用其中的几个元素的时候可以用m._matrix._f11
那样可以避免用m.f[0][0]
(这样不大直观,而且容易出错)。 -
实现多种数据类型之间转换
-
测试大小端模式(大小端去百度)
所谓的大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中所谓的小端模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中
-
寄存器的定义,实现整体的访问和单项的访问
struct register { char a; char b; char c; char d; }; union Register { struct register; int whole; };
这样就能实现单项和整体的访问,特别是引入位域操作等相关结构以后,能够实现每一个bit的访问。
-
数据组合、拆分
比如,可以方便直观的把0x12345678拆分成0x78、0x56、0x34、0x12 -
传输浮点数据
union f_data { float f; struct { unsigned char byte[4]; }; }
类似的,使用这样子的方法可以用于传输浮点数,更具体地不再展开,网络上有很多这一块的资料。感兴趣的朋友可以自己操作验证验证。
举个例子
我们看一看TI的寄存器封装是怎么做的:
所有的寄存器被封装成联合体类型的,联合体里边的成员是一个32bit
的整数及一个结构体,该结构体以位域的形式体现。这样就可以达到直接操控寄存器的某些位了。比如,我们要设置PA0
引脚的GPAQSEL1
寄存器的[1:0]
两位都为1,则我们只操控两个bit
就可以很方便的这么设置:
GpioCtrlRegs.GPAQSEL1.bit.GPIO0 = 3
或者直接操控整个寄存器:
GpioCtrlRegs.GPAQSEL1.all |=0x03
如果不是工作于芯片原厂,寄存器的封装应该离我们很远。但我们可以学习使用这种方法,然后用于我们的实际应用开发中。
下面就看一种实际应用:管理一些状态变量
。
示例代码:
union sys_status
{
uint32 all_status;
struct
{
bool status1: 1; // FALSE / TRUE
bool status2: 1; //
bool status3: 1; //
bool status4: 1; //
bool status5: 1; //
bool status6: 1; //
bool status7: 1; //
bool status8: 1; //
bool status9: 1; //
bool status10: 1; //
// ...
}bit;
};
assert()
<assert.h> – 宏函数
断言函数,用于在调试过程中捕捉程序的错误。
函数原型
void assert (int expression);
assert() 会对表达式expression
进行检测:
- 如果
expression
的结果为 0(条件不成立),那么断言失败,表明程序出错,assert() 会向标准输出设备(一般是显示器)打印一条错误信息,并调用 abort() 函数终止程序的执行。 - 如果
expression
的结果为非 0(条件成立),那么断言成功,表明程序正确,assert() 不进行任何操作。
assert() 的用法和机制
#include <stdio.h>
#include <assert.h>
int main(){
int m, n, result;
scanf("%d %d", &m, &n);
assert(n != 0); //写作 assert(n) 更加简洁
result = m / n;
printf("result = %d\n", result);
return 0;
}
本例用来计算两个数相除的结果,由于被除数不能为 0,所以我们加入了 assert() 来检测错误。
NDEBUG 宏
#ifdef NDEBUG
#define assert(e) ((void)0)
#else
#define assert(e) \
((void) ((e) ? ((void)0) : __assert (#e, __FILE__, __LINE__)))
#endif
这意味着,一旦定义了NDEBUG
宏,assert() 就无效了。
NDEBUG 是”No Debug“的意思,也即“非调试”。有的编译器(例如 Visual Studio)在发布(Release)模式下会定义 NDEBUG 宏,在调试(Debug)模式下不会定义定义这个宏;有的编译器(例如 Xcode)在发布模式和调试模式下都不会定义 NDEBUG 宏,这样当我们以发布模式编译程序时,就必须自己在编译参数中增加 NDEBUG 宏,或者在包含 <assert.h> 头文件之前定义 NDEBUG 宏。
调试模式是程序员在测试代码期间使用的编译模式,发布模式是将程序提供给用户时使用的编译模式。在发布模式下,我们不应该再依赖 assert() 宏,因为程序一旦出错,assert() 会抛出一段用户看不懂的提示信息,并毫无预警地终止程序执行,这样会严重影响软件的用户体验,所以在发布模式下应该让 assert() 失效。
修改上面的代码,在包含 <assert.h> 之前定义 NDEBUG 宏:
#define NDEBUG
#include <stdio.h>
#include <assert.h>
int main()
{
int m, n, result;
scanf("%d %d", &m, &n);
assert(n);
result = m / n;
printf("result = %d\n", result);
return 0;
}
当以发布模式编译这段代码时,assert() 就会失效。如果希望继续以调试模式编译这段代码,去掉 NDEBUG 宏即可。
在代码中显式地增加 NDEBUG 宏比较麻烦,因为当以调试模式编译代码时还得再去掉它,更加科学的做法是在 IDE 的编译参数中添加 NDEBUG 宏。不同的 IDE 添加宏的方式不同,这里不再深入探讨。(可以去看看keil5里面怎么添加全局的宏)
C typedef
为基本数据类型定义新的类型名
也就是说,系统默认的所有基本类型都可以利用 typedef 关键字来重新定义类型名,示例代码如下所示:
typedef unsigned int COUNT;
这样做的好处就是我们在跨平台移植的时候只需要修改一下 typedef
就可以了
比如 size_t
在 VC++2010
的 crtdefs.h 文件中的定义如下所示:
#ifndef _SIZE_T_DEFINED
#ifdef _WIN64
typedef unsigned __int64 size_t;
#else
typedef _W64 unsigned int size_t;
#endif
#define _SIZE_T_DEFINED
#endif
为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
以结构体为例,下面我们定义一个名为 Point 的结构体:
struct Point
{
double x;
double y;
double z;
};
在调用这个结构体时,我们必须像下面的代码这样来调用这个结构体:
struct Point oPoint1 = {100,100,0};
struct Point oPoint2;
在这里,结构体 struct Point 为新的数据类型,在定义变量的时候均要向上面的调用方法一样有保留字 struct,而不能像 int 和 double 那样直接使用 Point 来定义变量。现在,我们利用 typedef 定义这个结构体,如下面的代码所示:
typedef struct tagPoint
{
double x;
double y;
double z;
} Point;
在上面的代码中,实际上完成了两个操作:
- 定义了一个新的结构类型
- 使用 typedef 为这个新的结构起了一个别名,叫 Point
为了加深对 typedef 的理解,我们再来看一个结构体例子,如下面的代码所示:
typedef struct tagNode
{
char *pItem;
pNode pNext;
} * pNode;
从表面上看,上面的示例代码与前面的定义方法相同,所以应该没有什么问题。但是编译器却报了一个错误,为什么呢?莫非 C 语言不允许在结构中包含指向它自己的指针?
其实问题并非在于 struct 定义的本身,大家应该都知道,C 语言是允许在结构中包含指向它自己的指针的,我们可以在建立链表等数据结构的实现上看到很多这类例子。那问题在哪里呢?其实,根本问题还是在于 typedef 的应用。
在上面的代码中,新结构建立的过程中遇到了 pNext 声明,其类型是 pNode。这里要特别注意的是,pNode 表示的是该结构体的新别名。于是问题出现了,在结构体类型本身还没有建立完成的时候,编译器根本就不认识 pNode,因为这个结构体类型的新别名还不存在,所以自然就会报错。因此,我们要做一些适当的调整,比如将结构体中的 pNext 声明修改成如下方式:
typedef struct tagNode
{
char *pItem;
struct tagNode *pNext;
} * pNode;
为数组定义简洁的类型名称
它的定义方法很简单,与为基本数据类型定义新的别名方法一样,示例代码如下所示:
typedef int INT_ARRAY_100[100];
INT_ARRAY_100 arr;
为指针定义简洁的名称
对于指针,我们同样可以使用下面的方式来定义一个新的别名:
typedef char* PCHAR;
PCHAR pa;
对于上面这种简单的变量声明,使用 typedef 来定义一个新的别名或许会感觉意义不大,但在比较复杂的变量声明中,typedef 的优势马上就体现出来了,如下面的示例代码所示:
int *(*a[5])(int, char *);
对于上面变量的声明,如果我们使用 typdef 来给它定义一个别名,这会非常有意义,如下面的代码所示:
// PFun是我们创建的一个类型别名
typedef int *(*PFun)(int,char*);
// 使用定义的新类型来声明对象,等价于int*(*a[5])(int,char*);
PFun a[5];
实际应用
在IAP编程当中,我们要用到跳转到应用程序段:
就用到了 typedef void (*app_func) (void);
typedef void (*app_func) (void); //定义一个函数类型的参数
app_func application;
uint32_t app_address;
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(uint32_t appxaddr)
{
//printf("%x\r\n", *(vu32 *)appxaddr);
//printf("%x\r\n", *(vu32 *)0x08010000);
//检测栈顶的地址是否合法,判断用户代码的堆栈地址是否落在0x2000000~0x2001ffff区间
if (((*(vu32 *)appxaddr) & 0x2FFE0000U) == 0x20000000U)
{
app_address = *(__IO uint32_t*) (appxaddr + 4U); //ApplicationAddress + 4 对应的是app中断向量表的第二项,复位地址
application = (app_func) app_address;// (iapfun) 把地址强转为函数指针
/* initialize user application's stack pointer */
__set_MSP(*(__IO uint32_t*) appxaddr);//设置主函数栈指针 将用户代码的栈顶地址设为栈顶指针
/* jump to user application */
application(); //跳转到APP 调用函数,实际失去app复位地址去执行复位操作---设置程序指针为复位地址
}
}
#define
用 #define 定义标识符的一般形式为:
#define 标识符 常量 //注意, 最后没有分号
#define 和 #include 一样,也是以“#”开头的。凡是以“#”开头的均为预处理指令,#define也不例外。
#define又称宏定义,标识符为所定义的宏名,简称宏。标识符的命名规则与前面讲的变量的命名规则是一样的。#define 的功能是将标识符定义为其后的常量。一经定义,程序中就可以直接用标识符来表示这个常量。是不是与定义变量类似?但是要区分开!变量名表示的是一个变量,但宏名表示的是一个常量。可以给变量赋值,但绝不能给常量赋值。
宏所表示的常量可以是数字、字符、字符串、表达式。其中最常用的是数字。
简单用法:
/*信息输出*/
#define NQ_DEBUG_ON 1 //DEBUG开关
#define NQ_PRINTF(fmt, arg...) \
do \
{ \
if (NQ_DEBUG_ON) \
printf(fmt, ##arg); \
} while (0)
C 标准库 - <string.h>
string .h 头文件定义了一个变量类型、一个宏和各种操作字符数组的函数。
来自:菜鸟教程
经常要用到一些对字符串的操作相关的函数
库函数
下面是头文件 string.h 中定义的函数:
关于STM32 __IO 的变量定义
这个IO 是指静态 这个 _IO 是指静态 volatile uint32_t 是指32位的无符号整形变量uint32_t 是指32位的无符号整形变量
#define __IO volatile /*!< Defines 'read / write' permissions */
volatile主要就是为了避免编译过程中编译器对代码的主动优化
写一段测试代码如下:
u8 test;
test = 1;
test = 2;
test = 3;
设置优化级别中级
运行后test会被直接取值为3,只有最后一个语句被编译
如果使用 volatile
volatile u8 test;
test = 1;
test = 2;
test = 3;
则所有语句都会被编译。test先后被设置成1、2、3
由此可以看出这个作用在IO操作,寄存器操作,特殊变量,多线程变量读写都是很重要。
归纳一下就是:
volatile
变量可变允许除了程序之外的比如硬件来修改他的内容- 访问该数据任何时候都会直接访问该地址处内容,不会被编译器优化
C 语言中 static 的作用
(1)先来介绍它的第一条也是最重要的一条:隐藏。
当我们同时编译多个文件时,所有未加 static 前缀的全局变量和函数都具有全局可见性。为理解这句话,我举例来说明。我们要同时编译两个源文件,一个是 a.c,另一个是 main.c。
下面是 a.c 的内容:
a.c 文件代码
char a = 'A'; // global variable
void msg()
{
printf("Hello\n");
}
下面是 main.c 的内容:
main.c 文件代码
int main(void)
{
extern char a; // extern variable must be declared before use
printf("%c ", a);
(void)msg();
return 0;
}
程序的运行结果是:
A Hello
你可能会问:为什么在 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的变量 static int i = 0
,那么离开这个函数后,这个变量 i
的数值是不会改变的,也就是和全局变量一样,不会因为下一次再进去这个函数而被重新赋值为0
(3)static 的第三个作用是默认初始化为 0。
其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是 0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置 0,然后把不是 0 的几个元素赋值。如果定义成静态的,就省去了一开始置 0 的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加 \0 太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是 \0 。
(3)static 的第三个作用是默认初始化为 0。
C语言中const
关键字const用来定义常量,如果一个变量被const修饰,那么它的值就不能再被改变,我想一定有人有这样的疑问,C语言中不是有#define吗,干嘛还要用const呢,我想事物的存在一定有它自己的道理,所以说const的存在一定有它的合理性,与预编译指令相比,const修饰符有以下的优点:
- 预编译指令只是对值进行简单的替换,不能进行类型检查
- 可以保护被修饰的东西,防止意外修改,增强程序的健壮性
- 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
下面我们从几个方面来说一下const的用法:
一、修饰局部变量
const int n=5; int const n=5;
这两种写法是一样的,都是表示变量n的值不能被改变了,需要注意的是,用const修饰变量时,一定要给变量初始化,否则之后就不能再进行赋值了。
接下来看看 const
用于修饰常量静态字符串,例如:
const char* str="fdsafdsa";
如果没有 const
的修饰,我们可能会在后面有意无意的写 str[4]=’x’
这样的语句,这样会导致对只读内存区域的赋值,然后程序会立刻异常终止。有了 const
,这个错误就能在程序被编译的时候就立即检查出来,这就是 const
的好处。让逻辑错误在编译期被发现。
二、常量指针与指针常量
常量指针是指针指向的内容是常量,可以有一下两种定义方式。
const int * n; int const * n;
需要注意的是一下两点:
1、常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的引用来改变变量的值的。
int a=5; const int* n=&a; a=6;
2、常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。
int a=5; int b=6; const int* n=&a; n=&b;
指针常量是指指针本身是个常量,不能在指向其他的地址,写法如下:
int *const n;
需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。
int a=5; int *p=&a; int* const n=&a; *p=8;
区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果const在星号的左边,则为常量指针,如果const在星号的右边则为指针常量。如果我们将星号读作‘指针’,将const读作‘常量’的话,内容正好符合。
int const *n;
是常量指针,int *const n;
是指针常量。
指向常量的常指针
是以上两种的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。
const int* const p;
三、修饰函数的参数
根据常量指针与指针常量,const修饰函数的参数也是分为三种情况
1、防止修改指针指向的内容
void StringCopy(char *strDestination, const char *strSource);
其中 strSource 是输入参数,strDestination 是输出参数。给 strSource 加上 const 修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。
2、防止修改指针指向的地址
void swap ( int *const p1 , int *const p2 )
指针p1和指针p2指向的地址都不能修改。
3、以上两种的结合
指针和内容都不能改
四、修饰函数的返回值
如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
例如函数
const char *GetString(void);
如下语句将出现编译错误:
char *str = GetString();
正确的用法是
const char *str = GetString();
五、修饰全局变量
全局变量的作用域是整个文件,我们应该尽量避免使用全局变量,因为一旦有一个函数改变了全局变量的值,它也会影响到其他引用这个变量的函数,导致除了bug后很难发现,如果一定要用全局变量,我们应该尽量的使用const修饰符进行修饰,这样防止不必要的人为修改,使用的方法与局部变量是相同的。
UNICODE、UNICODE、TEXT、T、T、_TEXT、TEXT宏
https://www.cnblogs.com/ini_always/archive/2011/05/20/2050517.html
unsigned int 和 int 浅析
首先来看一下:
1字节(Byte) = 8位(bit)
1024 bytes | = | 1 KB |
---|---|---|
1024 KB | = | 1 MB |
1024 MB | = | 1 GB |
1024 GB | = | 1 TB |
1024 TB | = | 1 PB |
其中 无符号整型 (unsigned int)
和 有符号整型 ((signed)int)
都是32位的,也就是4个字节
无符号整型 (unsigned int)
:存储数据范围为: [ 0 , 2 32 ] \left [ 0,2^{32} \right ] [0,232]有符号整型 ((signed)int)
:存储数据范围为: [ − 2 31 , 2 31 ] \left [ -2^{31}, 2^{31} \right ] [−231,231]
无符号整型 (unsigned int)
在stm32中为例, unsigned int
最高位是数据位用来存放数据的
有符号整型 ((signed)int)
int类型默认是有符号的,所以int实际上是signed int ,我们通常省略signed
有符号整型 ((signed)int)
最高位为符号位
- 1:负
- 0:正
看一下补码、反码、原码的概念
例子:
#include <stdio.h>
main()
{
short int a = 65534, b, c, d;
b = a + 1;
c = a + 2;
d = a + 3;
printf("a=%d\nb=%d\nc=%d\nd=%d\n", a, b, c, d);
}
输出:
//溢出位(实际上不存在)
a = -2 // 1111 1111 1111 1110
b = -1 // 1111 1111 1111 1111
c = 0 //0001 0000 0000 0000 0000
d = 1 //0001 0000 0000 0000 0001
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),其余位表示值。
反码:
- 正数的反码与其原码相同
- 负数的反码是对其原码逐位取反,但符号位除外
补码:
- 正数的补码就是其本身
- 负数的补码是在其反码的基础上+1
例如:
[+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
[-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
为什么要设置补码
有人会有疑惑为什么要用反码,补码,不直接用原码呢?
先搞清楚一点反码补码原码是针对二进制数而言,计算机若用原码相加减,正数加正数不会出错,然而正数和负数原码相加就会出错。
- 计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
如果用原码表示,让符号位也参与计算,显然对于减法来说,结果是不正确的。这也就是为何计算机内部不使用原码表示一个数。
- 计算十进制的表达式: 1-1=0
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原
这样计算的话就是正确的了
引进补码的作用是为了让计算机更方便做减法
说白了,补码反码就是为了简化减法而来的,将减号化为负数,再将负数化为补码求加法 跟正数没关系 ,不管是正整数还是正小数,原码,反码,补码都全部相同。
负数时的有符号整型和无符号整型的转换
当执行一个运算时(如这里的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
因为没有符号位,就像正数的反码补码都是它本身一样,1111,1111,1111,1111的原码依然是它本身,且最高位也不表示符号位
但是它们的底层存储都是一样的。现在你应该明白为什么-1转换成无符号数之后,就成了UMax了吧。
参考
关于“#ifdef __cplusplus” 和 " extern “C” 的问题
#ifdef __cplusplus
extern "C" {
#endif
//一段代码
#ifdef __cplusplus
}
#endif
https://blog.csdn.net/u012234115/article/details/43272441
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
百度百科