有人编写了以下C程序并询问为什么gcc允许“更改数组的基址”。他知道代码很糟糕但仍然想知道。我发现这个问题很有趣,因为C中数组和指针之间的关系是微妙的(看看在数组上使用地址运算符!“为什么有人这样做?”),令人困惑,因而经常被误解。这个问题被删除了,但我想我再次提出问题,并提出适当的背景,并且 - 正如我所希望的那样 - 是一个合适的答案。这是原始编程。
static char* abc = "kj";
void fn(char**s)
{
*s = abc;
}
int main()
{
char str[256];
fn(&str);
}
它使用gcc(带警告),链接和运行进行编译。这里发生了什么?我们可以通过获取地址,将其转换为指向指针(在所有数组几乎都是C中的指针,不是它们之后)并分配给它来更改数组的基址吗?
答案 0 :(得分:5)
它无法工作(甚至理论上),因为数组不是指针:
int arr[10]
:
使用的内存量为sizeof(int)*10
字节
arr
和&arr
的值必须相同
arr
指向有效的内存地址,但不能设置为指向另一个内存地址
int* ptr = malloc(sizeof(int)*10)
:
使用的内存量为sizeof(int*) + sizeof(int)*10
字节
ptr
和&ptr
的值不一定相同(事实上,它们大多不同)
ptr
可以设置为指向有效和无效的内存地址,可以多次使用
答案 1 :(得分:4)
该程序不会更改"基地址"数组。它甚至没有尝试过。
传递给fn的是内存中256个字符的块的地址。它在数字上与str
在其他表达式中衰减的指针相同,只是输入不同。这里,数组确实保持一个数组 - 将地址运算符应用于数组是数组不会衰减到指针的实例之一。例如,递增&str
会在数值上增加256.这对于多维数组很重要,正如我们所知,实际上是C中的一维数组数组。增加&#34的第一个索引; 2维"数组必须将地址提前到下一个" chunk"或"行"。
现在抓住了。就fn而言,您传递的地址指向包含另一个地址的位置。那不是真的;它指向一系列字符。打印该字节序列被解释为指针显示' A'字节值,65或0x41。
然而,fn,认为指向的内存包含一个地址,用地址覆盖它,其中" kj"住在记忆中。由于在str中分配了足够的内存来保存地址,因此分配成功并在该位置产生可用的地址。
应该注意的是,这当然不能保证有效。失败的最常见原因应该是对齐问题 - 我认为str
不需要为指针值正确对齐。标准要求函数的参数必须与参数声明分配兼容。任意指针类型不能彼此分配(需要通过void指针或者转换)。
编辑: david.pfx指出(即使使用正确的强制转换)代码会调用未定义的行为。标准要求通过兼容的左值(包括引用)访问对象最新公共草案第6.5 / 7节中的指针)。正确投射并使用gcc -fstrict-aliasing -Wstrict-aliasing=2 ...
进行编译时,gcc会警告"类型惩罚"。理由是编译器应该可以自由地假设不兼容的指针不会修改相同的内存区域;这里不需要假设fn改变了str的内容。这使编译器能够优化远程重载(例如,从存储器到寄存器),否则这将是必要的。这将在优化中发挥作用;一个可能的示例,其中调试会话将无法重现错误(即,如果正在调试的程序将在没有优化的情况下进行编译以用于调试目的)。话虽如此,如果非优化编译器会在这里产生意想不到的结果,我会感到惊讶,所以我让答案的其余部分保持原样.--
我已插入一些调试printfs来说明正在进行的操作。这里可以看到一个实例:http://ideone.com/aL407L。
#include<stdio.h>
#include<string.h>
static char* abc = "kj";
// Helper function to print the first bytes a char pointer points to
void printBytes(const char *const caption, const char *const ptr)
{
int i=0;
printf("%s: {", caption);
for( i=0; i<sizeof(char *)-1; ++i)
{
printf("0x%x,", ptr[i]);
}
printf( "0x%x ...}\n", ptr[sizeof(char *)-1] );
}
// What exactly does this function do?
void fn(char**s) {
printf("Inside fn: Argument value is %p\n", s);
printBytes("Inside fn: Bytes at address above are", (char *)s);
// This throws. *s is not a valid address.
// printf("contents: ->%s<-\n", *s);
*s = abc;
printf("Inside fn: Bytes at address above after assignment\n");
printBytes(" (should be address of \"kj\")", (char *)s);
// Now *s holds a valid address (that of "kj").
printf("Inside fn: Printing *s as string (should be kj): ->%s<-\n", *s);
}
int main() {
char str[256];
printf("size of ptr: %zu\n", sizeof(void *));
strcpy(str, "AAAAAAAA"); // 9 defined bytes
printf("addr of \"kj\": %p\n", abc);
printf("str addr: %p (%p)\n", &str, str);
printBytes("str contents before fn", str);
printf("------------------------------\n");
// Paramter type does not match! Illegal code
// (6.5.16.1 of the latest public draft; incompatible
// types for assignment).
fn(&str);
printf("------------------------------\n");
printBytes("str contents after fn (i.e. abc -- note byte order!): ", str);
printf("str addr after fn -- still the same! --: %p (%p)\n", &str, str);
return 0;
}
答案 2 :(得分:2)
您在这里只是未定义的行为。
函数的参数被声明为指向指针的指针。传递给它的参数是指向256-char的数组指针。该标准允许一个指针与另一个指针之间的转换,但由于指向的对象不是指向char的指针,因此取消引用指针是Undefined Behavior。
n1570 S6.5.3.2 / 4:
如果为指针分配了无效值,则一元*运算符的行为为 未定义。
推测Undefined Behavior将如何在不同的实现上发挥作用是徒劳的。这是完全错误的。
为了清楚起见,UB就在这一行:
*s=abc;
指针s
未指向正确类型的对象(char*
),因此使用*
为UB。