嵌入式C语言自我修养之数据存储

本文是个人阅读学习《嵌入式C语言自我修养》王利涛 后做出了一些列的总结,方便个人查阅。它所面向的读者应该已经具备了一些嵌入式C语言编程经验,并想提高一些C语言编程技巧。

1.1数据类型与存储

类型,是一组数值及对该数组数值进行各种操作的集合。同一种类型的数据,在不同的处理器平台下,存储方式可能不一样。不同类型的数据,在同一处理器平台下,存储方式和运算规则也可能不一样。

1.1.1 大端模式与小端模式

在计算机中,位(bit)是最小的存储单位,在一个DDRSDRAM内存电路中,通常使用一个电容器来表示:充电时高电位表示1,放电时低电位表示0。8个bit组成一字节(Byte),字节是计算机最基本的存储单位,也是最小的寻址单元,计算机通常以字节为单位进行寻址。在一个32位的计算机系统中,通常4字节组成一个字(Word),字是软件开发者常用的存储单位。

我们使用C语言提供的int关键字,可以定义一个整型变量。

> int i = 0x12345678

编译器根据变量i的类型,在内存中分配4字节大小的在储空间来存储i变量的值0x12345678。一个数据在内存中有2种存储方式:高地址在储高字节数据,低地址存储低字节数器;或者高地址存储低字节数据,而低地址则存储高字节数据,不同字节的数据在内存中的存储顺序被称为字节序。根据字节序的不同,我们一般将存储模式分为大端模式和小端模式。

嵌入式C语言自我修养之数据存储_第1张图片
不同架构的处理器,存储模式一般也不同。ARM、X86、DSP一般都采用小端模式,而 IBM、Sun、PowerPC 架构的处理器一般都采用大端模式。如何判断程序运行的当前平台是大端模式还是小端模式呢?很简单,我们编写一个程序测试一下就可以知道。

#include 

int main(void)
{
	int a = 0x11223344;
	char b;
	b =a;
	if(b == 0x44)
		printf("Little endian!\n");
	else
		printf("Big endian!\n");
	return 0;
}

如果打印的是a = 0x11则是小端模式,如果打印的是a = 0x88则是大端模式。

作为一名嵌入式工程师,掌握大端模式与小端模式的存储方式很有必要。我们在驱动开发中配置各种寄存器,经常需要对某个寄存器的几个比特位进行读写操作。不同存储模式的嵌入式设备互联及网络数据传输,也需要考虑大小端模式,在处理网络数据时需要自己实现数据的大小端转换。如果你写的程序代码要在不同架构的嵌入式平台上运行(如ARM、PowerPC),还是要考虑到大小端模式的转换的。

在一个嵌入式系统软件中,如何实现大小端存储模式的转换呢?我们可以定义一个宏,将高、低地址上的数据互换,即可完成大小端存储模式的转换。

#define swap_endian_u16(A)        \           ((A & 0xFF00 >> 8) | (A & 0x00FF << 8))

1.1.2 有符号数和无符号数

天有阴晴,月有圆缺,人分男女,数分正负,我们生活在一个二的世界。C 语言为了能表示负数,引入了有符号数和无符号数的概念,在声明数据类型时分别使用关键字signed和unsigned 修
饰。我们定义的变量如果没有使用 signed或unsigned 显式修饰,默认是 signed型的有符号数。

一个字符型的有符号数,最高的位 bit7 是符号位:0表示正数,1表示负数,其余的比特位用来表示大小。而一个字符型的无符号数,所有的比特位都用来表示数的大小。因此有符号数和无符号数能表示的数值范围是不一样的,对于一个字符型数据而言,有符号数能表示的数值范围为[-128,127],而无符号数的数值范围为[0,255]。我们使用printf()函数打印数据时可以使用%d和%u格式符分别格式化打印有符号数和无符号数。

#include 
int main(void)
{
	signed int a = -1;
	int b = 0xffffffff;
	printf("a = %d\n",a);   //有符号数打印
	printf("a = %u\n",a);	//无符号数打印
	
	printf("b = %d\n",b);   //有符号数打印
	printf("b = %u\n",b);	//无符号数打印
	return 0}

打印出来的效果如下图

a = -1
a = 4294967295
b = -1
b = 4294967295

对于我们定义的变量,编译器在编译程序时会根据变量的类型对数据进行编码,分配合适的存储空间,把数据存储在内存中。当 printf()函数解析数据时,如果你使用和该数据类型相匹配的打印格式,则可以正确打印这个数据的值:如果你使用其他打印格式打印,则 printf0函数可能就把它解析成另外一个值了。总而言之,它在内存里就是一串二进制数据 0 和 1,关键看如何去解析它。

1.1.3 数据溢出

每一种数据类型都有它能表示的数据范围。一个有符号字符型变量,它能表示的数值范围为[-128,127],如果我们把130赋值给这个有符号型的字符变量,则会发生什么情况?

#include 
int main(void)
{
	char i;
	for(i = 0; i < 130; i++)
	printf("*");
	return 0;
}

编译运行上面的程序,你会发现程序陷入了死循环,一直在不断打印。当我们给一个变量赋一个超出其能表示范围的数时,就会发生数据溢出,如上面的示例代码所示,导致程序运行出现异常。当数据溢出时,到底发生了什么状况,导致上面的程序陷入了死循环呢?
一般来讲,无符号数溢出时会进行取模运算,继续“周期轮回”。例如,一个 unsigned char类型的数据,它能表示的数据范围为[0.255],当其循环到255 最大值时继续加1,这个数就变成了0,开始新的一轮循环,周而复始。

#include 
int main(void)
{
	unsigned char c = 255;
	printf("c= %u\n",c);
	c++;
	printf("C %u\n",c);
	return 0;
}

程序运行如下。

c = 255
c = 0

而对于有符号数,当发生数据溢出时,由于 C 语言的语法宽松性,不对数据类型做安全性检查,因此也不会触发异常,但是会产生一个未定义行为。
什么是未定义行为呢?通俗点理解,就是遇到这种情况时,C 语言标准也没有规定该如何操作,各家编译器在处理这种情况时也就没有了参考标准,各自按照自己的方式处理,编译器都不算错误。这也导致了当有符号数发生溢出时,运行结果是不确定的,在不同的编译器环境下编译运行,结果可能不一样。

#include 
int main(void)
{
	signed char c2 = 127;
	printf("c2 = %d\n",c2);
	c2++;
	printf("c2 = %d\n",c2);
	return 0}

大家可以尝试在不同的编译器环境下编译运行上面的程序,你会发现大部分结果都是-128,也就是说大部分编译器都默认采用了与无符号数一样的轮回处理。但是如果有一家编译器比较特殊,编译运行后的结果是 0,你也不能算错。
数据溢出可能会导致程序的运行结果和你预期的不一样,有时候甚至会改变程序的运行路径,因此在实际编程中,我们要时刻注意数据溢出的问题。

如何防范数据溢出呢?方法其实很简单,我们先看看两个有符号数相加的情况。如果两个正数相加的和小于 0,说明运算过程中发生了数据溢出。同理,如果两个负数相加的和大于 0,也说明数据发生了溢出。

#include 
int main(void)
{
	char a= 125;
	char b= 30;
	char c=a+b;
	if(c<0)
		printf("data overflow!\n");
	else
		printf("%d\n",c);
	return 0;
}

对于无符号数的相加,如果两个数的和小于其中任何一个加数,此时我们也可以判断数据在计算过程中发生了溢出现象。

int main(void)
{
	unsigned char a = 255;
	unsigned char b = 255;
	unsigned char c;
	c = a+b;
	if(c < a || c < b)
		printf("data overflow!\n");
	else
		printf("c = %u\n",c );
	return 0;
}

你可能感兴趣的:(c语言,开发语言)