2014-09-04 28 views
6

有人写了下面的C程序,并问为什么gcc允许“更改数组的基地址”。他知道代码很糟糕,但仍然想知道。我发现这个问题足够有趣,因为C中数组和指针之间的关系很微妙(请看阵列上的地址操作符的使用!“为什么有人会这么做?”),因此容易混淆,因此经常被误解。这个问题被删除了,但我想我会再次提出这个问题,并且有适当的背景和 - 我希望 - 正确地回答它。这是原始编程。我们可以通过使用蛮力的数组指针来更改数组的基地址吗?

static char* abc = "kj"; 

void fn(char**s) 
{ 
    *s = abc; 
} 

int main() 
{ 
    char str[256]; 
    fn(&str); 
} 

它编译与gcc(带警告),链接和运行。这里发生了什么?我们可以通过获取地址来更改数组的基地址,将它转换为指向指针的指针(在所有数组几乎都是C中的指针之后,是不是它们)并将其分配给它?

+0

为后人,[这里](http://stackoverflow.com/questions/25660493/assigning-to-a-char-array-using-assignment-operator-and-double-pointers)是删除的问题 – 2014-09-04 11:34:36

回答

4

该程序不会更改该阵列的“基地址”。它甚至没有尝试。

你传递给fn的是内存中256个字符块的地址。它在数字上与str在其他表达式中衰减的指针相同,只是不同的类型。在这里,数组真的保持一个数组 - 将地址运算符应用到数组是数组不会衰减到指针的一个实例。例如,增量&str会使其数值增加256.这对于多维数组非常重要,正如我们所知,它实际上是C中的一维数组阵列。增加“二维”数组的第一个索引必须将地址提前到下一个“块”或“行”的开始处。

现在赶上。就fn而言,您通过的地址指向包含另一地址的位置。那是不正确的;它指向一系列字符。打印该字节序列解释为指针会显示'A'字节值,即65或0x41。然而,fn认为指向的内存包含一个地址,并用“kj”驻留在内存中的地址覆盖它。由于在str中分配了足够的内存来保存地址,因此分配成功并在该位置生成可用地址。

应该指出,这当然不能保证工作。导致失败的最常见原因应该是对齐问题 - 我认为str不需要为指针值正确对齐。标准规定函数参数必须与参数声明兼容。任意指针类型不能分配给对方(需要通过void指针或者强制转换)。

编辑: david.pfx指出(即使有适当的转换),代码调用未定义的行为。该标准要求通过最新公共草案第6.5/7节中兼容的左值(包括引用的指针)来访问对象。当正确编译和编译gcc -fstrict-aliasing -Wstrict-aliasing=2 ... gcc警告“类型双关”。理由是编译器应该自由地假设不兼容的指针不会修改相同的内存区域;这里不需要假定fn改变str的内容。这使得编译器能够优化不必要的重新加载(例如从内存到注册)。这将对优化起作用;一个调试会话将无法重现错误的可能示例(即,如果正在调试的程序将被编译而不进行用于调试目的的优化)。话虽如此,如果一个非优化编译器会在这里产生意想不到的结果,我会感到惊讶,所以我让剩下的答案保持原样。 -

我插入了一些debug 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

我想知道在6分钟前询问问题时,你是如何及时回答这么长的问题的,而且你的答案也是在6分钟前出现的?只输入程序代码将花费1分多钟。 – 2014-09-04 11:12:55

+1

那么,Ctrl-V需要1/100 ;-)。我在这里发现,当我提出一个问题时,SO让我有机会立即与问题一起写出答案。这种自我Q/A模式是SO的支持格式之一,因此人们可以分享他们的见解(我认为,如果没有问题,你就不能写出答案)。这是我第一次尝试它。 – 2014-09-04 11:20:37

+0

@VladfromMoscow这是从另一个已被删除的问题复制粘贴 – 2014-09-04 11:21:48

5

它不能工作(即使在理论上),因为阵列不是指针:


  • int arr[10]

    使用的内存
    • 量为sizeof(int)*10字节

    • arr&arr的值不必相同

    • arr指向一个有效的存储器地址,但不能被设置为指向另一存储器地址


  • int* ptr = malloc(sizeof(int)*10)

    使用
    • 金额的存储器是sizeof(int*) + sizeof(int)*10字节

    • ptr值和&ptr不必相同(实际上,它们大多是不同的)

    • ptr可以被设置为指向两个有效和无效的内存地址,尽可能多的次数,你会

+0

那么,O-OP(不是我)很困惑的事实,你可以采取数组的地址,解除引用和分配给(在有效地铸造之后)。他预计这会像“(*(&x))= 1”一样改变“价值”,就像其他指针一样。由于这个“价值”是他认为他改变了数组,所以不知何故(不是它的内容)。在我的回答中,我剖析了应用于数组的地址运算符的效果,类型转换后对'fn'中结果参数值的解释以及对所涉及值的影响。 – 2014-09-04 11:28:07

+0

@PeterSchneider:你让我在这里感到困惑。我给你+1,因为我发现这个问题很有趣。然后我注意到你也是回答它的人(从第一个评论到答案),这有点奇怪,我不得不说,但就我而言,这没问题......无论如何,现在你说“O-OP(不是我)感到困惑”。 “O-OP”是什么意思,你为什么称自​​己为“不是我”? – 2014-09-04 11:57:35

+0

我用“回答你自己的问题”的格式来传播智慧;-)。这并不罕见;实际上,它提供了一个选项,用于编辑您的答案以及直到今天我还没有意识到的问题。这使得OP自己的答案首先出现 - 正如我在问题中所解释的那样,它是基于不同用户询问然后删除的答案。那个人应该是“原始海报”或O-OP。这不是我。我是这个主题中的OP。 – 2014-09-04 12:17:21

2

你在这里只是未定义的行为。

该函数的参数被声明为pointer-to-pointer-to-char。传递给它的参数是指向256-char-array的指针。该标准允许一个指针与另一个指针之间的转换,但由于指向的对象不是指向char的指针,因此对指针的解引用是Undefined Behavior。

n1570 S6.5.3.2/4:

如果无效值已被分配给所述指针,一元的行为*运算是 未定义。

推测未定义行为将如何在不同的实现上发挥作用是徒劳的。这显然是错误的。


只要是明确的,则UB是在这条线:

*s=abc; 

指针s没有指向的正确类型(char*)的目的,因此使用的*是UB 。

+0

我不认为这是UB(好吧,gcc让我们通过错误的指针类型,严格地说它会需要一个强制转换,但是让我们在调用'fn()'时假设一个强制类型)。通过的值不是无效的;这是一个有效的数组指针。如果满足对齐要求,则解除引用是明确的。 Cf n1570,6.3.2.3/7:“指向对象类型的指针可能[显式地,P.S.]转换为指向不同对象类型的指针。”然后该段落讨论对齐问题和转换为char *时的行为,但不限制目标指针类型。 – 2014-09-04 12:11:15

+0

还要注意,代码从不取消引用'* s'(例如尝试读取'** s'),这至少在将'abc'分配给'* s'之前,这对于具有存储器保护的系统是非法的和崩溃。 – 2014-09-04 12:20:36

+1

@PeterSchneider:'* s = abc'根据标准是UB。不管你称之为“解除引用”还是不行(我这样做)。请参阅编辑。 – 2014-09-04 13:37:58

相关问题