复合/字符串字面值存储在内存中的哪里?

9

我了解到:

复合字面量是C99的一个特性,可以用于创建没有名称的数组。考虑以下示例:

int *p = (int []){3, 0, 3, 4, 1};

p指向一个包含3, 0, 3, 41的五个元素数组的第一个元素。

实际上,我想知道,由于这个数组没有名称,它是否会存储在内存中?
换句话说,在以下情况下:

char* str = "hello"

字符串 "hello" 将存储在内存中的哪里?


4
@haccks:什么?你刚刚声明了一个 int *p 的指针...(注意:原文中的 "just" 意为 "刚刚",不是只有的意思。) - user529758
一旦您获得了一个指针,它是如何分配的就不再重要了。您可以以同样的方式使用它。由malloc()返回的数组也没有名称。 - Barmar
@H2CO3;我的意思是,这种情况下在块作用域中不需要名称的原因是我将CL分配给指针p吗? - haccks
3
char * p = "some words";中的字符串字面量也没有“名称”,但是你会认为这个字符串不是“存储在内存中”的吗? - Crowman
2
@H2CO3,抱歉,但复合字面量没有作用域,也没有对象。作用域是标识符的属性,而不是对象的属性。你可能指的是“生命周期”。这有点更加复杂,基本上这样一个对象(像任何自动对象一样)的生命周期在执行时结束,当封闭块不再可达时。 “可达”可以意味着正常的控制流,但也包括像gotolongjmp这样的跳转。 - Jens Gustedt
显示剩余20条评论
4个回答

9
使用指针算术运算。因此
p[0], p[1], ...

或者

*p, *(p + 1), ...

以下是需要翻译的内容:

事实上,C语言中有很好的原始类型字面量,如intchar,甚至还有字符串字面量。因此,我们可以很容易地表达出这样的东西:

int length(char *s);
int len = length("Hello, World!");

在C99中,新增了复合字面量的概念来处理“数组字面量”和“结构体字面量”。因此,我们现在可以这样说:
int sum(int a[], int n);
int total = sum((int []){ 17, 42 }, 2);

这里使用复合字面量来表示“数组字面量”。
实际上,我想知道,由于它没有名称,这个数组是否会存储在内存中?
是的,在内存中。
我认为你的困惑源于此。变量p是有名字的,而(int []){3, 0, 3, 4, 1}没有。恰好,p的值是(int []){3, 0, 3, 4, 1}的地址。当然,(int []){3, 0, 3, 4, 1}在内存中;它将在你的可执行文件的数据段中。你只是没有任何名称可以引用它。

1
你是什么意思?它将在可执行文件的数据段中。你还期望什么? - jason
5
这个答案表示复合字面值将位于数据段,这通常是不正确的。它具有自动存储期限,因此通常位于堆栈上。(实现可能会将初始值存储在数据段中,以便可以轻松地初始化每个实例,但对象本身通常位于堆栈上,除非优化使其可以以其他方式进行管理。) - Eric Postpischil

5

C 2011 (N1570) 6.5.2.5 5规定:

如果复合字面量出现在函数体的外部,则对象具有静态存储期;否则,它具有与包含块相关联的自动存储期。

因此,您展示的复合字面量具有自动存储期。C标准没有指定这些对象在内存中的位置。将内存组织起来是C实现的工作。通常情况下,具有自动存储期的对象是在堆栈上创建的,但是实现可能通过其他方式管理这些对象。

特别地,假设您在其他地方记录了 p 的值并递归调用包含此复合字面量的程序。当程序再次初始化 p 时,将存在第二个复合字面量实例。它们实际上是不同的对象,并且C标准要求它们的地址不同。因此,如果打印这两个指针,它们将具有不同的值。但是,如果优化器能够确定您不会这样做,并且永远不会比较指向不同复合字面量实例的两个指针,并且没有其他可以区分它们的可观察行为(由C标准定义),则C实现可以自由使用一个实际的复合字面量实例,而不是每次都创建新实例。在这种情况下,编译器可以将复合字面量保留在数据节中,而不是在堆栈上。

以下是演示两个相同复合字面量实例具有不同地址的代码:

#include <math.h>
#include <stdio.h>


void foo(int *q)
{
    int *p = (int []) { 2, 3 };
    if (!q)
        foo(p);
    else
        printf("p = %p, q = %p.\n", (void *) p, (void *) q);
}


int main(void)
{
    foo(0);
    return 0;
}

字符串字面量是不同的。C 2011(N1570)6.4.5 6说:

在第7个翻译阶段,对于由一个或多个字符串字面量产生的多字节字符序列,将追加值为零的字节或代码。78)然后使用多字节字符序列来初始化具有静态存储期和长度的数组,该数组刚好足够包含该序列。

因此,字符串字面量表示具有静态存储期的对象。即使递归调用包含它的函数,也只有一个实例。


嗨,Eric!我有一个问题。假设我声明:int *p = (int []){3, 0, 3, 4, 1};,然后尝试修改数组 p[2] = 34;,这可能吗?我相信这会导致分段错误,因为我正在尝试更改常量数组。 - Grijesh Chauhan
@EricPostpischil 所以 char* s = "hello";char* s = (char []){'h', 'e', 'l', 'l', 'o', '\0'}; 有很大的不同吗? - Grijesh Chauhan
2
@GrijeshChauhan:是的。为了简单起见,您可以将后者写成 char *s = (char []){"hello"}; - Eric Postpischil
@EricPostpischil “char str[7] = "grijesh";” 是合法的还是未定义行为?使用“gcc -Wall -pedantic”编译没有警告或错误。然而,如果我像这样定义:char name[7] = {'1', '2', '3', '4', '5', '6', '7', '\0'}; 就会导致警告:“数组初始化器中有过多的元素”。 - Grijesh Chauhan
在C语言中,char str[3] = "abc";等同于char str[3] = {'a','b','c'};。在C语言中,这种用法是被禁止的。个人认为应该有一个定义好的前缀来表示字符串字面值不需要终止的空字节,因为有些情况下字符串的长度会以其他方式知道,而空字节最多只会浪费空间[如果字符串是结构体的一部分,则可能没有空间用于空字节]。 - supercat
显示剩余2条评论

4

就像在C语言中将字符串赋值给char指针一样,您同样可以将一个整数数组赋值给类型为int*的p:
当您声明:int *p = (int []){3, 0, 3, 4, 1};时,可以假设它被存储在内存中,类似于:

  p          23    27   31   35   36  37
+----+      +----+----+----+----+----+----+
| 23 |      | 3  | 0  |  3 | 4  | 1  | ?  |    
+----+      +----+----+----+----+----+----+  
              ▲    ▲    ▲    ▲    ▲     ▲
              |    |    |    |    |     // garbage value 
              p    p+1  p+2  p+3  p+4

基本上,数组分配连续的内存。您可以按照以下方式访问数组的元素:
p[0] == 3
p[1] == 0
p[2] == 3
p[3] == 4
p[4] == 1

注意:

我们使用char* str = "hello";来表示字符串。但是在C语言中,字符串字面值的类型是char[N]而不是char*。但是在大多数表达式中,char[N]可以转化为char*

要点:

当你声明一个数组时,例如:

int p[] = {3, 0, 3, 4, 1};

那么这里的 p 类型是 int[5]&p 是一个指向数组的指针,类型是:int(*)[5]

而在声明中:

int *p = (int []){3, 0, 3, 4, 1};

p指向第一个元素,类型为int*&p的类型为int**。(这类似于将字符串分配给char指针)。

在第一种情况下,对于0 <= i <= 4p[i] = 10;是合法的。但在第二种情况下,你正在写入只读内存-非法内存操作-段错误。

要点:

以下声明也是有效的:

int *p = (int *){3, 0, 3, 4, 1};

问:实际上我想知道,由于这个数组没有名称,它会被存储在内存中吗?

当然,数组是存储在内存中的,但它只是由p指向(p不是数组的名称),没有其他名称与之对应。例如,如果您执行以下操作:

int *p = (int []){3, 0, 3, 4, 1};
int i = 10;
p = &i;

现在您已经丢失了数组的地址,这与以下情况非常相似:

char* s = "hello";
char c = 'A';
s = &c;

现在你失去了"hello"的地址。

常量的内存来自静态段,在声明时分配。任何int数组或char字符串字面值都会被存储在那里。当您的声明运行时,地址被分配给指针变量。但是常量没有名称,只有值。在这两种情况下,数组和字符串"hello"成为可执行文件中数据部分的一部分(您可以反汇编以查找其中的值)。


1
@haccks 常量的内存来自静态段,当您声明时。任何int数组或char字符串文字都会被存储在那里。当您的声明运行时,地址被分配给指针变量。但常量没有任何名称-只有值。 - Grijesh Chauhan
2
哦,我应该猜到那是什么意思了。我忘记我们在大学里叫它什么了,但那是我最喜欢的课程(自动机是另一门我也很喜欢的课程)。不,我不是老师。 - jason
2
@haacks:我同意。但是它如此美丽。 :( - jason
1
@haccks 我以A+的成绩通过了 :) - Grijesh Chauhan
1
@Grijesh Chauhan:我的荣幸。 - jason
显示剩余17条评论

0

两种方法:

&((int[]){3,0,3,4,1})[3]

并且

((int[]){3,0,3,4,1})+3

请注意,如果字面值在函数内部,则当退出封闭块时,对其的指针将无效。


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