您的位置:首页 > 理论基础 > 数据结构算法

老基础的一些总结 大小端 内存对齐 分配

2011-04-13 15:00 295 查看
网络字节序与主机字节序

不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序 
最常见的有两种
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址

LE little-endian 
最符合人的思维的字节序 
地址低位存储值的低位 
地址高位存储值的高位 
怎么讲是最符合人的思维的字节序,是因为从人的第一观感来说 
低位值小,就应该放在内存地址小的地方,也即内存地址低位 
反之,高位值就应该放在内存地址大的地方,也即内存地址高位 

BE big-endian 
最直观的字节序 
地址低位存储值的高位 
地址高位存储值的低位 
为什么说直观,不要考虑对应关系 
只需要把内存地址从左到右按照由低到高的顺序写出 
把值按照通常的高位到低位的顺序写出 
两者对照,一个字节一个字节的填充进去 

例子:在内存中双字0x01020304(DWORD)的存储方式 

内存地址 
4000 4001 4002 4003 
LE 04 03 02 01 
BE 01 02 03 04 

例子:如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为
      big-endian  little-endian
0x0000  0x12      0xcd
0x0001  0x23      0xab
0x0002  0xab      0x34
0x0003  0xcd      0x12
x86系列CPU都是little-endian的字节序. 

网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。

为了进行转换 bsd socket提供了转换的函数 有下面四个
htons 把unsigned short类型从主机序转换到网络序
htonl 把unsigned long类型从主机序转换到网络序
ntohs 把unsigned short类型从网络序转换到主机序
ntohl 把unsigned long类型从网络序转换到主机序

在使用little endian的系统中 这些函数会把字节序进行转换 
在使用big endian类型的系统中 这些函数会定义成空宏

同样 在网络程序开发时 或是跨平台开发时 也应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug.

注:
1、网络与主机字节转换函数:htons ntohs htonl ntohl (s 就是short l是long h是host n是network)
2、不同的CPU上运行不同的操作系统,字节序也是不同的,参见下表。
处理器    操作系统    字节排序
Alpha    全部    Little endian
HP-PA    NT    Little endian
HP-PA    UNIX    Big endian
Intelx86    全部    Little endian <-----x86系统是小端字节序系统
Motorola680x()    全部    Big endian
MIPS    NT    Little endian
MIPS    UNIX    Big endian
PowerPC    NT    Little endian
PowerPC    非NT    Big endian  <-----PPC系统是大端字节序系统
RS/6000    UNIX    Big endian
SPARC    UNIX    Big endian
IXP1200 ARM核心    全部    Little endian 

下面是一个检验本机字节序的简便方法:

//判断本机的字节序
//返回true表为小段序。返回false表示为大段序
bool am_little_endian ()
{
 unsigned short i=1;
 return (int)*((char *)(&i)) ? true : false;
}
int main()
{
  if(am_little_endian())
 {
           printf("本机字节序为小段序!/n");
 }
 else
 {
          printf("本机字节序为大段序!/n");
 }
        return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

字节对齐

 

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。对齐的实现通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。对齐的算法:由于各个平台和编译器的不同,现以本人使用的32位x86平台为例子,来讨论编译器对struct数据结构中的各成员如何进行对齐的。

设结构体如下定义:

struct A{

int a;

char b;

short c;

};

结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐。所以使用sizeof(strcut A)值为8。

现在把该结构体调整成员变量的顺序。

struct B{

char b;

int a;

short c;

};

这时候同样是总共7个字节的变量,但是sizeof(struct B)的值却是12。

下面我们使用预编译指令#pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。

#pragma pack (2) /*指定按2字节对齐*/

struct C{

char b;

int a;

short c;

};

#pragma pack () /*取消指定对齐,恢复缺省对齐*/

sizeof(struct C)值是8。

修改对齐值为1:

#pragma pack (1) /*指定按1字节对齐*/

struct D{

char b;

int a;

short c;

};

#pragma pack () /*取消指定对齐,恢复缺省对齐*/

sizeof(struct D)值为7。

对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。这里面有四个概念值:

1.数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。

2.指定对齐值:#pragma pack (value)时的指定对齐值value。

3.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。这样就不能理解上面的几个例子的值了。

例子分析:

分析例子A;

struct A{

int a;

char b;

short c;

};

假设A从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4(16位芯片为2)。第一个成员变量a的自身对齐值是4,指定或者默认指定对齐值也为4,所以其有效对齐值为4,所以其存放地址0x0000到0x000
bf3c
3这四个连续的字节空间中,符合0x0000%4=0.第二个成员变量b,其自身对齐值为1,所以有效对齐值也为1,所以可以存放在起始地址为0x0004这个字节空间中,复核0x0004%1=0,且紧靠第一个变量。第三个变量c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0006到0x0007这两个字节空间中,符合0x0006%2=0。所以从0x0000到0x0007存放的都是A内容。再看数据结构A的自身对齐值为其变量中最大对齐值(这里是a)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求,0x0007到0x0000=8字节,8%4=0。所以A从0x0000到0x0007共有8个字节,sizeof(struct A)=8;

分析例子B;

struct B{

char b;

int a;

short c;

};

假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000,符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是a)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求,0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B共有12个字节,sizeof(struct B)=12;

同理,分析上面例子C:

#pragma pack (2) /*指定按2字节对齐*/

struct C{

char b;

int a;

short c;

};

#pragma pack () /*取消指定对齐,恢复缺省对齐*/

第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.

同理,分析上面例子D:

#pragma pack (1) /*指定按1字节对齐*/

struct D{

char b;

int a;

short c;

};

#pragma pack () /*取消指定对齐,恢复缺省对齐*/

 第一个变量b的自身对齐值为1,指定对齐值为1,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;第二个变量,自身对齐值为4,指定对齐值为1,所以有效对齐值为1,所以顺序存放在0x0001、0x0002、0x0003、0x0004四个连续字节中,符合0x0001%1=0。第三个变量c的自身对齐值为2,所以有效对齐值为1,顺序存放在0x0005、0x0006中,符合0x0005%1=0。所以从0x0000到0x00006共七字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为1。又7%1=0,C只占用0x0000到0x0006的八个字节。所以sizeof(struct D)=7.

另:    

        需要字节对齐当然有设计者的考虑了,原来这样有助于加快计算机的存取速度,否则就得多花指令周期了。所以,编译器通常都会对结构体进行处理,让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上。正是因为如此两个数中间就可能需要加入填充字节,所以结构体占的内存空间就增长了。    

        其实字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则:     

        1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;    

        2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;例如上面例子B结构体变量的地址空间。  

        3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。例如上面例子B结构体变量的地址空间。  

        至于涉及到结构体嵌套的问题,你也可以用上述方法总结的,只不过你把被嵌套的结构体在原地展开就行了,不过在计算偏移地址的时候被嵌套的结构体是不能原地展开的必须当作整体。 

        上述三条建议是做编译器的工程师总结出来的,在这里只是借用而已。

 

 

 

 

 

 

 

 

 

 

 

 

 

指针变量也是传值调用的(C语言)

先看看下面一段代码:

#include <stdio.h>
#include <stdlib.h>

void F(int *pi)
{
    pi = (int *)malloc(sizeof(int));
}

main()
{
    int *pi = NULL;
    F(pi);
    printf("%d/n", pi == NULL);
}

如果你指望函数F能帮你改变pi的值,那你就错了,运行上面这段代码,你会发现输出是1。刚开始我也很诧异,我不是传了指针进去吗,为什么没能保留改变的结果呢?

别着急,再看看下面一段代码:

#include <stdio.h>
#include <stdlib.h>

void F(int *pi)
{
    *pi = 5;
}

main()
{
    int i = 0;
    F(&i);
    printf("%d/n", i);
}

这次运行代码,i的值被函数F改变了。对比一下两段代码,你会发现:第二段代码中传给函数F的实参是一个整型变量地址的拷贝,函数F拿到了这个地址,改变的是这个地址指向的变量。这样当然可以成功,地址和其拷贝的值是一样的,所以按照这两个地址找到的变量自然也是一样的。第一段代码则完全不同,虽然传给函数F的实参也是一个整型变量地址的拷贝,但函数F试图通过改变这个拷贝来改变原来的地址值,这当然行不通,因为指针和它的拷贝是两个变量。指针也是变量,它在函数调用过程中也是传值调用的。其实第一段代码和下面这段代码效果是类似的:

#include <stdio.h>
#include <stdlib.h>

void F(int i)
{
    i = 5;
}

main()
{
    int i = 0;
    F(i);
    printf("%d/n", i);
}

这段代码大家一看就知道函数F不会改变i的值,因为主函数传给F的只是变量i的一份拷贝,改变拷贝的值并不会影响原变量的值。其实这段代码和第一段有什么区别呢,不过一个是整型变量,一个是指针变量,它们都是传值调用的。

那么如何修改第一段代码好让它符合我们的要求呢?其实只需让函数F简单地返回地址:

#include <stdio.h>
#include <stdlib.h>

int *F()
{
    return (int *)malloc(sizeof(int));
}

main()
{
    int *pi = NULL;
    pi = F();
    printf("%d/n", pi == NULL);
}

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息