数组存储与指针学习笔记(一)数据类型与存储、数据对齐、数据移植、typedef

一、数据类型与存储

  类型,是一组数值及对该组数值进行各种操作的集合。同一种类型的数据,在不同的处理器平台下,存储方式可能不一样。不同类型的数据,在同一个处理器平台下,存储方式和运算规则也可能不一样。很多人都说指针是C语言的灵魂,我认为存储才是C语言的精髓和灵魂。
  认识一下ANSI C关键字 和 C99/C11新增关键字。关键字里,除了控制程序结构的一些关键字,绝大部分都与数据类型和存储相关。
在这里插入图片描述
在这里插入图片描述

1.1 大端模式与小端模式

  在计算机中,位(bit)是最小的存储单位,通常使用一个电容器来表示:充电时高电位表示1,放电时低电位表示0。8个bit组成一字节(Byte),字节是计算机最基本的存储单位,也是最小的寻址单元,计算机通常以字节为单位进行寻址。字(Word)代表计算机处理指令或数据的二进制数位数,是计算机进行数据存储和数据处理的运算的单位。在一个32位的计算机系统中,通常4字节组成一个字(Word),字是软件开发者常用的存储单位。(注意:一个字并不都是占4字节,通常由系统硬件(总线,CPU命令字位数等)有关,在16位的系统中(比如8086微机) 1字 (word)= 2字节(byte)= 16(bit),在32位的系统中(比如win32) 1字(word)= 4字节(byte)=32(bit),在64位的系统中(比如win64)1字(word)= 8字节(byte)=64(bit))。

  • 字节序
      不同字节的数据在内存中的存储顺序被称为字节序。根据字节序的不同,我们一般将存储模式分为大端模式和小端模式。

    • 大端模式:高地址存储高字节数据,低地址存储低字节数据。
    • 小端模式:高地址存储低字节数据,低地址存储高字节数据。
      在这里插入图片描述
        常用的处理器中,ARM、X86、DSP一般都采用小端模式,而IBM、Sun、PowerPC架构的处理器一般都采用大端模式。
        如何判断程序运行的当前平台是大端模式还是小端模式呢?很简单,我们只需要将一个整型变量赋值给字符型变量,通常会发生"截断",将会给低8位的字节赋值到字符型变量,通过打印就可以判断是大端模式还是小端模式。
    #include <stdio.h>
    
    int main(void)
    {
          
          
    	int a = 0x11223344;
    	char b;
    	b = a;
    	if(b==0x44)
    		printf("Little endian\r\n");
    	else
    		printf("Big endian\r\n");
    	return 0;
    }
    
  • 位序
      位序指在一字节的存储中,各个比特位的存储顺序。以十六进制数据0x78=01111000(B)为例,其在内存中可能有2种存储方式。一般情况下字节序和位序是一一对应的。小端模式下,低端地址存储低字节数据,在一字节中,bit0地址也用来存储这个字节的bit0位。大端模式则相反,bit0用来存储一字节的高比特位。
    在这里插入图片描述
      一般来讲,小端模式低地址存储低字节数据,比较符合人类的思维习惯;而大端模式则更适合计算机的处理习惯:不需要考虑地址和数据的对应关系,以字节为单位,把数据从左到右,按照由低到高的地址顺序直接读写即可。大端模式一般用在网络字节序、各种编解码中。
      作为一名嵌入式工程师,掌握大端模式与小端模式的存储方式很有必要。在开发移植过程当中,如配置寄存器、网络数据传输、移植网络等,需要考虑大小端模式的转换。在一个嵌入式软件中,如何实现大小端模式的转换过程,示例代码如下:

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

1.2 有符号数和无符号数

  C语言为了能表示负数,引入了有符号数和无符号数的概念,在声明数据类型时分别使用关键字signed和unsigned修饰。我们定义的变量如果没有使用signed或unsigned显式修饰,默认是signed型的有符号数。
  一个字符型的有符号数,最高的位bit7是符号位:0表示正数,1表示负数,其余的比特位用来表示大小。而一个字符型的无符号数,所有的比特位都用来表示数的大小。有符号数和无符号数能表示的数值范围是不一样的,对于一个字符型数据而言,有符号数能表示的数值范围为[-128,127],而无符号数的数值范围为[0,255]。可以使用%d和%u格式符分别格式化打印有符号数和无符号数。
  一个存储在物理内存中的数据,可以被看作一个有符号数,也可以被看作一个无符号数,就看你怎么去解析它:你使用格式符%d打印,printf()函数就把它看作一个有符号数;你使用无符号格式符%u打印,printf()函数就把它看成了另外一个数。
正所谓“看山是山,看水是水;看山不是山,看水不是水”。如下程序所示
在这里插入图片描述在这里插入图片描述

  •   无符号数在计算机内存中存储时,所有的比特位都用来表示数的大小,没有原码、补码之说。
  •   有符号数,则采用补码形式存储,一个有符号数有原码、反码、补码之说。
反码 = 符号位保持不变,所有的数据位取反。
补码 = 反码 + 1。
正数的补码 = 原码
负数补码 = 反码 + 1

  提问:为啥采用补码存储,而不全使用原码?
  答:1、解决了0的编码问题。如果所有的数据都使用原码编码,那么+0和-0的编码分别为00000000和10000000,一个数用两个编码表示,编码就出现了问题。采用补码则可以避免这个问题,+0和-0都使用00000000表示,空下的编码10000000就可以多表示一个数:-128。需要注意的是,-128这个数只有补码,没有原码和反码
  2、它可以将减法运算转换为加法运算,省去了CPU减法逻辑电路的实现,CPU只需要实现全加器、求补电路即可同时支持加法运算和减法运算。如下例子所示
  正常计算下在这里插入图片描述
  我们将其改为加法运算:7+(-3),则省去了减法电路,直接使用加法电路运算即可。
在这里插入图片描述
  有符号数在运算过程中,符号位也是参与运算的,和其他数据位的计算遵循相同的计算法则和进位处理。用补码表示的数据相加,当最高位有进位时,进位直接被丢弃。

1.3 数据溢出

  每一种数据类型都有它能表示的数值范围。

  • 无符号数溢出时会进行取模运算,继续“周期轮回”。
      例如一个unsigned char类型的数据,它能表示的数据范围为[0,255],当其循环到255最大值时继续加1,这个数就变成了0,开始新的一轮循环,周而复始。
    在这里插入图片描述程序运行结果如下:
    在这里插入图片描述

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

  因此,数据溢出可能会导致程序的运行结果与预期不一致。
  防范数据溢出方法:

  • 1、两个有符号数相加的情况。如果两个正数相加的和小于0,说明运算过程中发生了数据溢出。同理,如果两个负数相加的和大于0,也说明数据发生了溢出。
  • 2、无符号数的相加,如果两个数的和小于其中任何一个加数,此时我们也可以判断数据在计算过程中发生了溢出现象。

1.4 数据类型转换

  数据类型转换分为两种:一种是隐式类型转换,一种是强式类型转换。如果程序员在程序中没有对类型进行强式类型转换,则编译器在编译程序时就会自动进行隐式类型转换。
  一个C程序中发生隐式类型自动转换,主要是以下几种情况。

  • 算术运算、逻辑运算、赋值表达式中运算符两侧数据类型不相同时。
  • 函数调用过程中,传递的实参和形参类型不匹配时。
  • 函数返回值类型与函数声明的类型不匹配时。
      编译器遇到以上情况,就会对数据类型进行自动转换,即隐式类型转换。转换规则一般按照从低精度向高精度、从有符号数向无符号数方向转换。在这里插入图片描述  一个有符号数和无符号数比较大小时,编译器会将它们两个都转换为无符号数。
      强制类型转换的过程中需要注意一个问题,数据的值在转换过程中可能会发生改变:在将一个char型数据转换为int型数据时,值保持不变,但存储格式发生了变化,将char型数据保存在32位中的低8位地址空间,其余的高24位使用符号位填充。将一个有符号数转换为无符号数时,数据的存储格式不会发生变化,但是值会发生改变,因为此时有符号数的符号位变成了无符号数的数据位。

二、数据对齐

2.1 为什么非要地址对齐

  数据对齐原则,就是C语言中各种基本数据类型要按照自然边界对齐:一个char型的变量按1字节对齐,一个short型的整型变量按sizeof(short int)字节对齐,一个int型的整型变量要按sizeof(int)字节对齐。每种数据类型的对齐字节数一般也被称为对齐模数。
  为什么非要地址对齐呢?这主要是由CPU硬件决定的。不同处理器平台对存储空间的管理不同,为了简化CPU电路设计,有些CPU在设计时简化了地址访问,只支持边界对齐的地址访问,因此编译器也会根据处理器平台的不同,选择合适的地址对齐方式,以保证CPU能正常访问这些存储空间。

2.2 结构体对齐

   C语言的基本数据类型不仅要按照自然边界对齐,复合数据类型(如结构体、联合体等)也要按照各自的对齐原则对齐。

  • 结构体内各成员按照各自数据类型的对齐模数对齐。
  • 结构体整体对齐方式:按照最大成员的size或其size的整数倍对齐。
      因为结构体内各个成员都要按照自身数据类型的对齐模数对齐,所以在结构体内部难免会有“空洞”产生,导致结构体的大小也不一样。结构体之所以要对齐,根本原因就是为了加快CPU访问内存的速度,在具体实现上,一般都采用每种数据类型的默认对齐模数sizeof(type)对齐。
      如果在结构体里内嵌其他结构体,那么结构体作为其中一个成员也要按照自身类型的对齐模数对齐。结构体自身的对齐模数是该结构体中最大成员的size,或者其size的整数倍。

2.3 联合体对齐

  联合体也有自己的对齐原则。

  • 联合体的整体大小:最大成员对齐模数或对齐模数的整数倍。
  • 联合体的对齐原则:按照最大成员的对齐模数对齐。

  在C程序编译过程中,无论是基本数据类型还是复合数据类型,编译器在为各个变量分配地址空间时,会按照大家各自的默认对齐模数进行地址对齐。除此之外,我们也可以通过#pragma预处理命令或GNU C编译器的aligned/packed属性声明来显式指定对齐方式。

三、数据的可移植性

  我们可以使用sizeof关键字去查看int类型的数据在内存中的大小,在不同的编译环境下编译上面的程序并运行,你会发现运行结果可能不一样。在一个跨平台的程序中,有时候我们会需要一个固定大小的存储空间,或者一个固定长度的数据类型。
  现在的操作系统一般都支持多种CPU架构、多种处理器平台。操作系统为了实现跨平台运行,一般都会考虑数据的可移植性,如大小端存储模式、数据对齐、字长等。我们在编程时,可以把程序中与系统、平台相关的部分隔离封装在一个单独的头文件或配置文件中,整个程序的可移植部分和不可移植部分也就变得泾渭分明,更加方便后续的管理、维护和升级。

四、Linux内核中的size_t类型

  Linux内核中定义了很多变量,使用了各种不同的数据类型,总的来说,可以分为3类。

  • C语言基本数据类型:int、char、short。
  • 长度确定的数据类型:long。
  • 特定内核对象的数据类型:pid_t、size_t。
      数据类型size_t一般使用#define宏定义,后面使用一个_t的后缀表示Linux内核中在某些地方特定使用的数据类型。
    在这里插入图片描述
      size_t数据类型一般用在表示长度、大小等无关正负的场合,如数组索引、数据复制长度、大小等。使用size_t不仅仅是考虑到数据类型的可移植性,size_t的另一个优点是其大小并非是固定的,而是用来表征针对某平台的最大长度。当我们使用无符号型的size_t用来表示一个地址或者数据复制的长度时,根本不用担心它表示的数值范围够不够用。

五、typedef

5.1 typedef的基本用法

  使用typedef关键字,可以给student声明一个别名student_t和一个结构体指针类型student_ptr,然后可以直接使用student_t类型去定义一个结构体变量,不用再写struct,这样会显得代码更加简捷。
在这里插入图片描述  程序的运行结果如下:
在这里插入图片描述  typedef除了与结构体结合使用,还可以与数组结合使用。定义一个数组,通常使用int array[10];即可。我们也可以使用typedef先声明一个数组类型,然后使用这个类型去定义一个数组。声明了一个数组类型array_t,然后使用该类型定义一个数组array,这个array效果其实就相当于int array[10]
在这里插入图片描述  typedef还可以与指针结合使用PCHAR的类型是char*,我们使用PCHAR类型去定义一个变量str,其实就是一个char*类型的指针。
在这里插入图片描述  typedef还可以和函数指针结合使用。定义一个函数指针,我们通常采用下面的形式。
在这里插入图片描述  在实际编程中,typedef还可以与枚举结合使用。枚举与typedef的结合使用方法和结构体类似:可以使用typedef为枚举类型color声明一个新名称color_t,然后使用这个类型就可以直接定义一个枚举变量。

5.2 typedef的优势

  • 可以让代码更加清晰简捷。
    在这里插入图片描述

  • 增加代码的可移植性。
    在这里插入图片描述
      如果我们在代码中想使用一个32位的固定长度的无符号类型数据,则可以使用上面的方式声明一个U32的数据类型,在程序中你就可以放心大胆地使用U32。当将代码移植到不同的平台时,直接修改这个声明就可以了。

  • 比宏定义更好用。
      C语言的预处理指令#define用来定义一个宏,而typedef则用来声明一种类型的别名。typedef和宏相比,不是简单的字符串替换,而是可以使用该类型同时定义多个同类型对象。
    在这里插入图片描述

  • 让复杂的指针声明更加简捷。
       我们可以使用typedef优化一下:先声明一个函数指针类型func_ptr_t,接着定义一个数组,就会更加清晰简捷,可读性它增加了不少。在这里插入图片描述   typedef也是一个存储类关键字。typedef在语法上是一个存储类关键字。和常见的存储类关键字(如auto、register、static、extern)一样,在修饰一个变量时,不能同时使用一个以上的存储类关键字,否则编译会报错。
    在这里插入图片描述

5.3 typedef的作用域

   和宏的全局性相比,typedef作为一个存储类关键字,是有作用域的。使用typedef声明的类型和普通变量一样,都遵循作用域规则,包括代码块作用域、文件作用域等。
在这里插入图片描述
宏定义在预处理阶段就已经替换完毕,是全局性的,只要保证引用它的地方在定义之后就可以了。而使用typedef声明的类型则和普通变量一样,都遵循作用域规则。
在这里插入图片描述

5.4 typedef的适用范围

一般来讲,当遇到以下情形时,使用typedef可能会比较合适,否则可能会适得其反。

  • 创建一个新的数据类型。
  • 跨平台的指定长度的类型,如U32/U16/U8。
  • 与操作系统、BSP、网络字宽相关的数据类型,如size_t、pid_t等。
  • 不透明的数据类型,需要隐藏结构体细节,只能通过函数接口访问的数据类型。

猜你喜欢

转载自blog.csdn.net/qq_41866091/article/details/130576158