解析“指针对齐”
——以OpenCV库函数为例

2019-04-28 05:58刘硕
电子技术与软件工程 2019年3期
关键词:指针表达式字节

文/刘硕

1 内存与指针

1.1 字节

字节是内存的基本单位。一字节有八位,在内存中字节从上到下按照由低到高的顺序编号(如图1)。

1.2 字节在内存中的结构

对于内存来说,“数据”仅仅是每一个字节中的八个高低电平位的组合;而对于高级语言(如C++)来说,“数据”代表的是“对象”。由于对象的“类型”不同,一个对象储存在一个或多个字节中。例如在64位系统中,一般情况下char类型对象占1字节,而int类型的对象要占4个字节。在C++中,读取一个T类型对象在内存中占多少个字节是通过sizeof(T)完成的(如前所述可以得出sizeof(int)等于4)。

1.3 C++中的指针

指针是对象在内存中的地址,它的值是对应字节的编号。如果我们在C++中定义一个指向T类型对象的指针P(为了避免对指针的操作和对指针值的操作混淆,我们把指针P的值记为valueP)。那么P的含义是“内存中第valueP个字节开始,到第valueP+sizeof(T)个字节,这一段连续的内存中保存着一个T类型的对象”。

由此我们得到第一个结论:对于内存来说,数据的长度是统一的,始终是一字节;对于高级语言来说,对象的长度是根据对象的类型(如char、int)变化的,是sizeof(T)个字节。

“在某些架构下,从一个不被对象大小均匀分割的地址中读取多字节对象是不可能(比如从32位整形中读取4比特)。在像x86这样的架构下,CPU通过多次读取并从这些读取中获取你的值来自动处理这种情况,但代价是显著降低了性能”——《学习OpenCV3(中文版)》为了提高效率,有必要对申请内存产生的原始指针进行处理,使指针的值能被对象长度整除(如图2)。

1.4 读取内存中的字节

根据指针与字节编号的关系,我们可以很自然的想到:在解引用指针时,指针所指对象的类型规定了本次解引用需要要一次读取几个内存单元(字节)。比如在解引用char*类型的指针时需要读取一个字节的数据,而解引用int*类型指针时,需要一次读取四个字节的数据。由此产生了一个不容忽视的问题——指针类型转换时,类型大小的问题。

·较高的指针类型转换成较低的指针类型(int* -> char*):这种转换是安全的。只不过这样解引用指针时,读取的字节数会由原来的四个转变成一个。

·较低的指针类型转换成较高的指针类型(char* ->int*):这种转换是危险的。因为这样解引用指针的时候,读取的字节个数会由原来的一个,转变成四个。我们不能保证多被读取的字节中是否存储着有其他用途的数据,对这样得到的内存加以修改,很可能引发程序运行异常。

2 内存分配与指针对齐

2.1 申请一段连续的内存

在C/C++环境下使用动态内存分配时,malloc()函数返回的值是申请到的内存资源中,第一个字节的编号,即指向一段连续的内存资源首地址的指针P。如果我们用申请得到的内存资源来存放n个T类型的对象,不能保证valueP可以被sizeof(T)整除。由此我们需要进行“指针对齐”

2.2 将指针对齐

指针对齐操作alignPtr()

(T*)(((size_t)P + n-1) & -n);

· P是malloc()返回的指针,即得到的内存资源首地址,也是需要进行对齐操作的指针。

· T是我们要存放的数据的类型(显然T*就是ptr的类型)

· n是T类型数据所占的字节数

图1

·“(size_t)P”这个表达式的值就是P指针的值,即valueP

·表达式的结果就是对齐之后的指针

·OpenCV源码:

template static inline _Tp*alignPtr(

_Tp* ptr, intn=(int)sizeof(_Tp)){

CV_DbgAssert((n & (n - 1)) == 0); // n is a power of 2

return (_Tp*)(((size_t)ptr + n-1) & -n);}

情况一:分配的内存第一个字节的编号(分配内存首地址)是对象长度的整数倍。

假设编号(首地址)为0000 0100 B ,要在这段内存中存放一些长度为4的对象。则(size_t)P + n-1)等于:

0000 0100 B

+0000 0100 B

-0000 0001 B

=0000 0111 B

当n=0000 0100 B时,-n是1111 1100 B(取反加一),于是(size_t)P + n-1)&-n就是低二位置零,使得0000 0111 B变成0000 0100 B.

对比观察得到在这种情况下,表达式的值即P的值。

情况二:分配的内存第一个字节的编号(分配内存首地址)不是是对象长度的整数倍。

假设编号(首地址)为0000 0110 B,要在这段内存中存放一些长度为4的对象,则(size_t)ptr + n-1)等于:

0000 0110 B

+0000 0100 B

-0000 0001 B

=0000 1001 B

当n=0000 0100 B时,-n是1111 1100 B(取反加一),于是(size_t)P + n-1)&-n就是去掉低二位,使得0000 1001 B变成0000 1000 B.

对比观察可以发现这种情况下,表达式的值是P的值加一个不大于n的整数,且表达式的值可以被n整除。由“不可整除”到“可以整除”这个过程叫做“对齐”

图2:一个T类型占4个字节,打√的编号可以被4整除

图3

通过“对齐表达式”得到的指针所指的内存,是我们真正写入数据的起点,而我们申请的内存是从P开始分配的。释放内存时,也应该从P开始释放,如何保证P和表达式的值之间的字节能够被释放呢?只需要在调用malloc()函数时,多申请sizeof(void *)个字节,之后将P存入这几个字节即可,销毁时把P从这几个字节中读取出来供free()函数使用。

2.3 将数据写入内存

假设有X个T类型对象需要写入内存,每个对象长度是sizeof(T),那么调用malloc()时,应该是malloc(sizeof(T)*x+sizeof(void*)),这样得到的字节数量正好是x个对象和一个指针需要的空间,但是因为指针对齐时是会舍弃几个字节不用的,这几个字节的数量大于零且小于对象的长度,所以还要再多申请存放一个对象所需要的字节,以补充因舍弃而减少的内存空间。故调用malloc()时,参数为

Sizeof(T)*x+sizeof(void*)+sizeof(T)

*从用”sizeof(void*)”来预留一个指针所需要的字节数来看,指针的长度始终是固定的,而指针所指向的对象长度是随着对象的类型变化的。

3 实例应用

OpenCV源码:

·udata是调用malloc()之后得到的资源中的第一个字节编号。是一个指向资源首地址的指针,是一个未对齐的指针

·将几个T类型的对象放入这片连续的内存,每个T类型的对象所在的地址都被一个指针所指。这些指针组成了一个指针数组adata。第一个T类型对象的地址是adata[0],第二个T类型对象的地址是adata[1]...以此类推。在我们想用T类型对象的时候可以解引用指针 *adata[index]。此外还可以用adata[-1]这个位置存储udata的值,以便销毁时利用。

·adata的值应该是用udata对齐后的值。因为udata是uchar*类型,而adata是uchar**类型(C++中数组名自动转换成指向数组首地址的指针),直接带入alignPtr()中进行对齐会有类型错误,所以需要强制类型转换以满足利用udata产生adata。强制转换不会改变udata的值、长度。

执行fastMalloc()之后会返回adata,这时内存与指针的关系如图3所示。

根据刚刚的分析udata和pdata之间有几个(或者没有)字节被闲置,不能保证是否为adata[-1]提供了足够的字节以存放udata。因此在调用alignPtr()时,传入的第一个参数是(uchar**)udata + 1。“(uchar**)”优先级比“+”要高,所以这个操作是“对指向指针的指针加一”。“指针加一”的操作表示“指针当前值+指针所指的对象所占的字节数”。就是说指针对齐的时候,给udata预留了sizeof(void *)个字节,保证adata前面至少有存放一个指针的空间。

4 关于指针的性质

4.1 指针的长度

根据计算机CPU的架构而定,简单说,32位计算机的指针是32个bit组成,也就是4字节;64位计算机的指针长度是64bit组成,也就是8字节。

4.2 指针的操作

“指针加一”和“指针的值”加一是两种情况。假设有一个指向T类型对象的指针P,P的值是valueP。P+1这个操作等价于valueP+sizeof(T)。而valueP+1是令P指针指向“当前所指的字节的”下一个字节。

猜你喜欢
指针表达式字节
No.8 字节跳动将推出独立出口电商APP
一个混合核Hilbert型积分不等式及其算子范数表达式
表达式转换及求值探析
No.10 “字节跳动手机”要来了?
浅析C语言运算符及表达式的教学误区
简谈MC7字节码
基于改进Hough变换和BP网络的指针仪表识别
ARM Cortex—MO/MO+单片机的指针变量替换方法