C语言中指向unsigned char数组的指针为何c与*c地址相同且无法直接用c[i]访问
核心问题现象
给定如下测试代码:
typedef unsigned char mac_address_t[6]; mac_address_t* c = malloc(sizeof(mac_address_t)); printf("%p \n", c); printf("%p \n", (*c));
实际运行时两次printf输出的内存地址完全一致;已知&(*c)[0]的打印结果和上述两个值也完全相同。这里存在两个核心疑惑:
- 为什么指针
c和解引用得到的*c打印出的地址值完全一样 - 既然二者地址相同,为什么不能直接用
c[i]的形式访问数组内的单字节元素
为什么c和*c打印的地址值完全相同
这个现象的本质是两个表达式的类型不同,但存储的地址数值一致,拆解底层类型逻辑如下:
- 首先明确自定义类型的本质:
mac_address_t是长度为6的unsigned char数组类型,本身不是指针。因此mac_address_t* c的实际类型是unsigned char (*)[6]——即指向「一整个长度为6的char数组块」的指针,它存储的内存值就是这个数组块的起始地址。 - 对
c执行解引用*c,得到的是位于该地址处、类型为unsigned char[6]的数组左值。根据C语言的数组退化规则:当数组类型的表达式出现在大多数场景(比如作为可变参数传入printf这类参数类型不固定的函数)时,会自动转换为指向该数组首元素的指针,类型为unsigned char*,转换后的指针数值同样是数组块的起始地址。
也就是说,虽然两个表达式最终打印出来的地址数值完全重合,但二者的指针类型有本质区别:
c是指向整个数组块的指针,做指针运算时的步长是6个字节- 退化后的
*c是指向单字节char的指针,做指针运算时的步长是1个字节
这也是为什么&(*c)[0]的打印值和二者完全一致:它本身就是直接取数组首元素的地址,和前两个表达式指向的物理内存位置完全相同。
为什么不能直接用c[i]访问数组元素
C语言中的下标运算a[b]本质等价于*(a + b),地址偏移量的计算完全由操作数的指针类型决定,规则是:偏移量 = 下标值 × 指针类型的步长。
如果直接对c使用下标运算c[i]:
因为c的类型是unsigned char (*)[6],步长为6字节,计算地址偏移时会按照“每个步长对应一整个6字节mac数组”的规则计算:
c[0]等价于*(c + 0),本质是拿到c指向的那个6字节数组本身(退化为char*的首地址)c[1]等价于*(c + 1),会直接从当前数组的起始位置向后偏移6个字节,完全跨过当前申请的整个数组块,访问到相邻的未知内存,完全不符合逐字节访问数组元素的预期。
正确的数组元素访问方式
如果要访问指向的mac地址数组的第k个元素,常见的合法写法包括:
- 先解引用拿到数组本身,再对数组取下标:
(*c)[k] - 利用二维下标的等价规则:
c[0][k](本质和上一种写法的执行逻辑完全等价) - 强制转换为逐字节的字符指针后再做偏移:
*((unsigned char*)c + k)
常见认知误区
- 只要打印的地址值相同,两个指针的行为就可以完全互换:实际上指针类型决定了运算规则、步长,哪怕两个指针存储的数值完全一样,类型不同时的行为可能天差地别
- 地址重合就可以直接用下标访问元素:跨类型的指针如果步长不一致,直接用下标访问会出现非预期的内存越界
内容的提问来源于stack exchange,提问作者damooo
相关产品推荐
相关产品推荐

