C(https://github.com/eteran/c-vector/blob/master/vector.h)中向量的流行的基于宏的通用实现使用以下内存布局。
+------+----------+---------+
| size | capacity | data... |
+------+----------+---------+
^
| user's pointer
这提供了一个非常方便的API,用户可以通过简单地声明所需类型的指针来获取向量。
float *vf = NULL;
VEC_PUSH_BACK(vf, 3.0);
int *vi = NULL;
size_t sz = VEC_CAPACITY(vi);
在内部,磁带库会像这样访问大小和容量
#define VEC_CAPACITY(vec) \
((vec) ? ((size_t *)(vec))[-1] : (size_t)0)
但这不是对严格混叠的侵犯吗?
答案 0 :(得分:4)
该库处理内存的方式不会不违反严格的别名。
尽管在C标准中没有提到名称,但严格的别名基本上意味着您无法像访问另一类型的对象一样访问一种类型的对象。这些规则在6.5节第6和第7段中有详细说明:
6 用于访问其存储值的对象的有效类型是该对象的声明类型(如果有)。 87)如果 值存储到没有声明类型的对象中 通过具有非字符类型的左值,然后 左值的类型成为对象的有效类型 该访问以及不修改存储的后续访问 值。如果将值复制到没有 使用memcpy或memmove声明的类型,或者被复制为一个数组 字符类型,则为 该访问以及不修改该值的后续访问 是从中复制值的对象的有效类型,如果 它有一个。对于所有其他未声明对象的访问 类型,对象的有效类型就是 用于访问的左值。
7 对象只能由具有以下类型之一的左值表达式访问其存储值: 88)
- 与对象的有效类型兼容的类型
- 与对象的有效类型兼容的类型的限定版本,
- 一种类型,它是与对象的有效类型相对应的有符号或无符号类型,
- 一种类型,它是与对象的有效类型的限定版本相对应的有符号或无符号类型,
- 一种聚集或联合类型,其成员中包括上述类型之一(递归地包括 子集合或包含的联盟的成员),或
- 一种字符类型。
87)分配的对象没有声明的类型。
88)的意图 此列表用于指定对象可能会或 可能没有别名。
例如,以下内容违反了严格的别名:
float x = 3.14;
unsigned int *i = (unsigned int *)&x;
printf("value of x: %f, representation of x: %08x\n", x, *i);
因为它试图读取float
,就好像它是int
。
向量库的工作方式不会尝试这样做。
让我们看一下库如何创建向量:
#define vector_grow(vec, count) \
do { \
if(!(vec)) { \
size_t *__p = malloc((count) * sizeof(*(vec)) + (sizeof(size_t) * 2)); \
assert(__p); \
(vec) = (void *)(&__p[2]); \
vector_set_capacity((vec), (count)); \
vector_set_size((vec), 0); \
} else { \
size_t *__p1 = &((size_t *)(vec))[-2]; \
size_t *__p2 = realloc(__p1, ((count) * sizeof(*(vec))+ (sizeof(size_t) * 2))); \
assert(__p2); \
(vec) = (void *)(&__p2[2]); \
vector_set_capacity((vec), (count)); \
} \
} while(0)
假设它是这样命名的:
int *v = NULL;
vector_grow(v, 10);
由于v
为NULL,因此输入了宏的if
部分。它为10 int
和2 size_t
分配空间。在malloc
之后,__p
所指向的内存没有类型。然后将其分配给vec
:
(vec) = (void *)(&__p[2]);
首先,将__p
定义为size_t *
,因此&__p[2]
在2个类型为size_t
的对象之后创建一个指向位置的指针,并将该指针转换为{{1} },并将其分配给void *
。此时,已分配的内存还没有类型。接下来的vec
被称为:
vector_set_capacity
这首先将#define vector_set_capacity(vec, size) \
do { \
if(vec) { \
((size_t *)(vec))[-1] = (size); \
} \
} while(0)
转换为vec
的原始类型size_t *
,并索引元素-1。这是有效的,因为__p
与((size_t *)(vec))[-1]
相同。现在,这里写入类型为__p[1]
的值,因此从size_t
开始的sizeof(size_t)
字节包含类型为__p[1]
的对象。
与size_t
类似:
vector_set_size
#define vector_set_size(vec, size) \
do { \
if(vec) { \
((size_t *)(vec))[-2] = (size); \
} \
} while(0)
与((size_t *)(vec))[-2]
相同,并且在其中写入还会创建类型为__p[0]
的对象。
所以现在的内存看起来像这样:
size_t
现在,当用户使用+--------+----------+---------+
| size_t | size_t | untyped |
+--------+----------+---------+
^ ^ ^
| | |
__p[0] __p[1] __p[2]==vec
时,它将执行以下操作:
vector_push_back
与写入任何分配的内存空间的工作原理相同。
因此,由于vec[vector_size(vec)] = (value);
和__p[0]
仅通过__p[1]
访问,因此没有严格的别名冲突。
然而,是问题的一件事是对齐。从size_t *
返回的内存已适当对齐以处理任何类型的数据。但是,在不使用malloc
的情况下在此分配的内存中创建其他对象时,这些对象可能未正确对齐。
让我们以一个struct
和int
均为2字节大小的系统为例,并假设从size_t
返回的内存块的偏移量为0。现在我们创建类型为malloc
的向量,其大小至少为8个字节。创建矢量后,第一个long long
位于偏移量0,第二个位于偏移量2。这很好,因为每个偏移量都是大小的倍数。但是,这意味着矢量数据从偏移量4开始。这不是8的倍数,因此类型size_t
的对象在此处将未对齐。
可以通过创建long long
的并集和两个max_align_t
的结构来解决对齐问题:
size_t
然后将union vector_meta {
struct {
size_t size;
size_t capacity;
}
max_align_t align[2];
};
创建如下:
vec
您将通过以下方式访问大小和容量:
union vector_meta *__p = malloc((count) * sizeof(*(vec)) + (sizeof(union vector_meta)));
assert(__p);
(vec) = (void *)(&__p[1]);
这可以确保将元数据标头后的内存正确对齐以进行任何使用,并且可以安全地访问((union vector_meta *)vec)[-1].size
((union vector_meta *)vec)[-1].capacity
和size
字段。
答案 1 :(得分:2)
没有别名问题,因为对象开头的两个单元格始终以size_t
的身份访问。
但是,库存在对齐问题。假定从malloc
获得的指针偏移了2 * sizeof (size_t)
个字节,对于任何对象类型,该指针仍然合适对齐。
这在主流体系结构上很可能是正确的,但这不是标准定义的保证。解决该问题的一种方法是定义一些可以调整的常量,例如:
#define VEC_HEADER_SIZE (2*sizeof(size_t)) // redefine if insufficient for alignment
然后可以使用(size_t *)((char *)(vec)-VEC_HEADER_SIZE)
获得两个单元头,然后可以使用[0]和[1]对其进行索引,以获取两个size_t
单元。
答案 2 :(得分:1)
标准中可能导致此类代码出现问题的部分不是“严格的别名规则”,而是指针算法的规范。 +
和-
在指针上的行为仅在原始指针和结果都指向“相同数组对象”内或“略过”“相同数组对象”的情况下定义,但标准相当含糊关于什么“数组对象”是由从另一种类型的指针强制转换的指针标识的。
给出例如
struct foo { int length; int dat[10]; };
void test(struct foo *p, int index)
{
if (index < p->length) p->dat[index]++;
return p->length;
}
该标准不要求实施允许以下可能性:index
可能为-1,p->dat-1
可能会产生p->length
的地址,因此p->length
可能会在if
和return
之间递增。下标的定义方式,但是代码等同于:
struct foo { int length; int dat[10]; };
void test(struct foo *p, int index)
{
int *pp = p->dat;
if (index < p->length) pp[index]++;
return p->length;
}
依次相当于:
struct foo { int length; int dat[10]; };
void test(struct foo *p, int index)
{
int *pp = (int*)&p->dat;
if (index < p->length) pp[index]++;
return p->length;
}
开始看起来与您正在执行的操作非常相似。适用于低级内存管理的实现应该不会有麻烦地处理此类代码,但是标准不会尝试禁止专门针对不涉及低级内存管理的任务的实现做出假设,这些假设会导致它们不适合完成任务。