通过指针访问结构体成员感到困惑

3

我是C语言的新手,当通过指针引用结构体成员时,我对结果感到困惑。请看以下代码示例。第一次引用tst->number时发生了什么?我错过了什么基本的东西吗?

#include <stdio.h>
#include <stdlib.h>

typedef struct {
   int number;
} Test;

Test* Test_New(Test t,int number) {
    t.number = number;
    return &t;
}    

int main(int argc, char** argv) {    
    Test test;
    Test *tst = Test_New(test,10);
    printf("Test.number = %d\n",tst->number);
    printf("Test.number = %d\n",tst->number);
    printf("Test.number = %d\n",tst->number);
}

输出结果为:
Test.number = 10
Test.number = 4206602
Test.number = 4206602
8个回答

14
当你将test传递到Test_New函数中时,你是按值传递的,因此在Test_New函数的函数作用域中会在堆栈上创建一个本地副本。由于你返回变量的地址,一旦函数返回,堆栈就无用了,但你返回了一个指向旧堆栈上结构体的指针!因此,你可以看到第一次调用返回了正确的值,因为没有覆盖你的堆栈值,但后续调用(都使用堆栈)会覆盖你的值并给出错误的结果。
要正确执行此操作,请重写Test_New函数以接受指针,并将指向结构体的指针传递到函数中。
Test* Test_New(Test * t,int number) {
    t->number = number;
    return t;
}

int main(int argc, char ** argv)  {
   Test test;
   Test * tst = Test_New(&test,10);

   printf("Test.number = %d\n",tst->number);
   printf("Test.number = %d\n",tst->number);
   printf("Test.number = %d\n",tst->number);

}

GCC默认情况下会产生这个警告,即使没有额外的编译器选项。 - Gordon Wrigley

3
与结构体无关,返回局部变量的地址始终是不正确的。将局部变量的地址放入全局变量中或将其存储在使用malloc分配的堆上对象中也通常是不正确的。通常,如果您需要返回一个对象的指针,您需要要么让其他人为您提供指针,要么您需要使用malloc分配空间,以返回指针。在这种情况下,您的函数的API的一部分必须指定谁负责在不再需要对象时调用free。

1
只是扩展了BlodBath的答案,考虑在执行此操作时内存中会发生什么。
当您进入主函数时,将创建一个新的自动Test结构 - 在堆栈上,因为它是自动的。所以您的堆栈看起来像 |主要返回地址|从环境中复制到栈上的argc|从环境中复制到栈上的argv地址|通过定义Test test创建->测试号码|
其中“->”表示指向堆栈最后使用元素的堆栈指针。
现在,调用Test_new(),并更新堆栈如下:
主要返回地址|从环境中复制到栈上的argc|从环境中复制到栈上的argv地址|通过定义Test test创建->测试号码| Test_new的返回地址|测试编号的副本|复制到栈中,因为C始终使用按值调用-> 10 |
当你返回&t时,你得到的是哪个地址?答案是:栈上数据的地址。但是当你返回时,栈指针会被减少。当你调用printf时,栈上的那些单词会被重新使用,但是你的地址仍然指向它们。恰好在栈中该位置的数字被解释为一个地址,指向的值为4206602,但这只是纯粹的巧合;事实上,这有点倒霉,因为运气应该是导致段错误,让你知道实际上出了什么问题。

1

Test tTest_New() 中被声明为局部变量。您试图返回局部变量的地址。由于局部变量一旦函数退出就会被销毁,内存将被释放,这意味着编译器可以在保留局部变量的位置上放置其他值。

在程序中第二次尝试访问该值时,内存位置可能已经分配给不同的变量或进程。因此,您得到了错误的输出。

对您来说更好的选择是通过引用而不是值从 main() 传递结构。


1

你返回的是在方法 Test_New 中声明的指针 t 的地址,而不是你传入该方法的指针 test 的地址。也就是说,test 是按值传递的,你应该传递它的指针。

当你调用 Test_New 时,会创建一个名为 t 的新的 Test 结构体,并将 t.number 设置为等于 test.number 的值(你还没有初始化它)。然后你将 t.number 设置为你传递给该方法的参数 number,最后返回指向 t 的地址。但是 t 是一个局部变量,在方法结束时就会超出作用域。因此,你返回了一个指向不存在的数据的指针,这就是为什么你最终得到了垃圾数据的原因。

Test_New 的声明更改为

Test* Test_New(Test* t,int number) {
    t->number = number;
    return t;
}

并通过调用它

Test *tst = Test_New(&test,10);

只要一切按照你的期望进行,一切都会顺利进行。


1
问题在于您没有将引用传递到 Test_New,而是传递了一个值。然后,您返回的是 局部变量 的内存位置。请考虑以下代码,它演示了您的问题:
#include <stdio.h>

typedef struct {
} Test;

void print_pass_by_value_memory(Test t) {
  printf("%p\n", &t);
}

int main(int argc, char** argv) {
  Test test;
  printf("%p\n", &test);
  print_pass_by_value_memory(test);

  return 0;
}

这个程序在我的机器上的输出是:
0xbfffe970
0xbfffe950

0

你已经通过值将测试内容传递给了Test_New。也就是说,当你调用Test_New时,在堆栈上分配了一个新的Test结构的副本。从函数返回的是这个Test的地址。

当你第一次使用tst->number时,会检索到10的值,因为尽管该堆栈已被解除,但该内存没有被其他任何使用所占用。然而,一旦调用了第一个printf,堆栈内存就会被重新使用,但tst仍然指向该内存。因此,后续使用tst->number检索在该内存中留下的printf。

在函数签名中使用Test &t。


0
你可以这样做,使它变得更容易:
typedef struct test {
   int number;
} test_t;

test_t * Test_New(int num)
{
   struct test *ptr;

   ptr = (void *) malloc(sizeof(struct test));
   if (! ptr) {
     printf("Out of memory!\n");
     return (void *) NULL;
   }

   ptr->number = num;

   return ptr;
}

void cleanup(test_t *ptr)
{
    if (ptr)
     free(ptr);
}

....

int main(void)
{
    test_t *test, *test1, *test2;

    test = Test_New(10);
    test1 = Test_New(20);
    test2 = Test_new(30);

    printf(
        "Test (number) = %d\n"
        "Test1 (number) = %d\n"
        "Test2 (number) = %d\n",
        test->number, test1->number, test2->number);
    ....

    cleanup(test1);
    cleanup(test2);
    cleanup(test3);

    return 0;
}

正如您所见,为test_t的几个完全不同的实例分配空间非常容易,例如,如果您需要保存其中一个的现有状态以便稍后还原...或出于任何其他原因。

当然,除非有某些原因要保持它本地...但我真的想不出来。


在分配类型结构时,我通常将malloc()的值转换为void,以避免破坏已经崩溃的编译器。一些人可能会觉得这有点奇怪。 - Tim Post
你还需要将“number”定义为int32_t或int64_t,具体取决于它的预期值。否则,根据你输入的内容,你可能会得到与最初提出问题时相同的结果 :) - Tim Post

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接