C语言中的动态数组和静态数组对比

8
以下代码使用malloc函数创建数组。但我知道只需使用int array[size]就可以更简单地实现。我认为这是静态数组。但是使用malloc函数是否可以创建动态数组?我在网上找到了这段代码... 究竟发生了什么,静态数组和动态数组(以及静态内存和堆内存之间)有什么区别。在运行时可以更改动态数组的大小吗?或者...我不太确定...如果有人能解释一下,我会很感激 :)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int size;
    int i;
    printf("Choose size of array: ");
    scanf("%d",&size);

    /*---MALLOC/FREE---*/
    int *m_array = (int *)malloc((size+1)*sizeof(int));
    memset(m_array,'\0',size+1);

    for(i=0; i<size ; i++)
    { 
        m_array[i]=i;
        printf("%d ",m_array[i]);
    }
    printf("\n");
    free(m_array);

    return 0;
}

1
int *m_array 是一个指针(Pointer),而不是一个数组(Array)。 - Michi
1
int array[size] 是一个可变长数组。此外,您可能会对realloc函数感兴趣。 - user3386109
以下代码使用malloc函数创建一个数组。使用malloc函数可以向操作系统请求一定量的内存,如果成功获取到内存,则将指针指向该内存地址。 - Michi
@ Michi,C标准规定malloc创建一个数组。 - M.M
@Michi 我的意思是使用 malloc... - Cache
@M.M 不,C标准并没有说malloc创建数组。 - Johann Gerell
3个回答

46

有几种不同类型的数组,具体取决于它们如何声明和在哪里声明。

固定长度数组

固定长度数组必须在编译时确定其大小。一旦定义,就无法更改固定长度数组的大小。

固定长度数组可以通过以下方式之一进行声明:

T a[N];
T a[N] = { /* initializer list */ };
char_type a[N] = "string literal";
T a[]  = { /* initializer list */ };
char_type a[]  = "string literal";

在前三种情况下,N必须是一个常量表达式,其值必须在编译时已知。在前三种情况下,数组的大小取自N;在后两种情况下,它取自初始化列表中的元素数或字符串字面值的大小。
固定长度数组的初始内容取决于其存储期限和是否提供了初始化程序。
如果数组具有静态存储期(意味着它在任何函数体外的文件作用域中声明,或者带有static关键字),并且没有提供初始化程序,则所有数组元素都将初始化为0(对于标量)或NULL(对于指针)。如果T是聚合类型,例如结构体或数组类型,则聚合的每个成员都将初始化为0或NULL。联合类型也将被清零。
如果数组具有自动存储期(意味着它在没有static关键字的函数或块内部声明),并且没有提供初始化程序,则数组的内容是不确定的-基本上是垃圾。
如果数组使用初始化列表声明(无论存储期如何),则数组元素的初始值与初始化程序相对应。如果初始化程序中的元素比数组少(例如,N为10,但您只初始化了前5个元素),则剩余的元素将初始化为静态存储期的数组。也就是说,给定声明
int a[10] = {0, 1, 2};

如果数组没有显式初始化,那么它的初始内容为{0, 1, 2, 0, 0, 0, 0, 0, 0, 0}
包含字符串值的定长数组可以使用字符串字面量进行初始化。C语言支持“宽字符”字符串,因此char_type可以是charwchar_t。对于常规初始化器列表,规则相同,只是N(如果指定)必须至少比字符串长度多1,以便考虑到字符串终止符。
这意味着:
char a[10] = "test"; 

将被初始化为{'t', 'e', 's', 't', 0, 0, 0, 0, 0, 0}

char a[] = "test";

将被初始化为{'t','e','s','t',0}

静态存储期数组的存储方式使它们在程序加载时立即可用,并且直到程序退出才被释放。这通常意味着它们存储在类似于.data.bss之类的内存段中(或者对于您的系统使用的任何可执行文件格式的等效处理方式)。

自动存储期数组的存储方式是在块或函数进入时分配和在块或函数退出时释放(实际上,不管它们是否被限制在函数内的较小范围内,它们可能会在函数进入时分配)- 这通常转换为堆栈,虽然它不必须如此。

变长数组

变长数组是在C99中添加的 - 它们的行为与固定长度数组大多数相似,除了它们的大小是在运行时确定的; N不必是编译时常量表达式:

int n;
printf( "gimme the array size: ");
scanf( "%d", &n );
T a[n]; // for any type T

与其名称所暗示的相反,在变长数组定义后,您无法更改其大小。 "变长"只是意味着大小在编译时不固定,并且可以从定义到定义发生变化。
由于它们的大小直到运行时才确定,因此不能在文件作用域或使用static关键字声明变长数组,也不能使用初始化程序列表声明变长数组。 VLAs的空间管理方式完全取决于实现;它可能(通常也是)来自堆栈,但据我所知,也可能来自其他地方。
动态数组实际上并不是如我们所说的"数组",至少在我们用来管理它们的对象的数据类型方面不是。 动态数组是在运行时使用malloccallocrealloc之一分配的,而该存储保持到调用free为止。
T *p = malloc( sizeof *p * N ); // where N may be either a compile-time or
                                // run-time expression
...
free( p );

可以使用 realloc 库函数来重新调整动态数组的大小,如下所示:

/**
 * If realloc fails, it will return NULL and leave the original array in 
 * place.  We assign the result to a temporary variable so we don't risk
 * losing our only reference to that memory. 
 */
T *tmp = realloc( p, sizeof *p * new_size );  
if ( tmp )                                    
  p = tmp;                                    

虽然数组元素本身的内存来自堆(或其他动态内存池),但指针变量 p 的内存将根据其存储期限 (staticauto) 从 .bss.data 段或堆栈中分配。

使用 mallocrealloc 分配的内存未被初始化,该内存的内容将为不确定值。使用 calloc 分配的内存将被初始化为零。

数组 vs. 指针

在某些时候,有人会告诉你:“数组只是一个指针”。那个人是不正确的。

当你声明一个数组(无论是定长还是变长),会为该数组的元素和 仅此而已 分配足够的存储空间,不会为任何元数据(如数组长度或指向第一个元素的指针)分配存储空间。给定以下声明:

T a[N];

那么存储看起来会像这样:
    +---+
 a: |   | a[0]
    +---+
    |   | a[1]
    +---+
    |   | a[2]
    +---+
     ...
    +---+ 
    |   | a[N-1]
    +---+

除了数组元素本身(或者更准确地说,对象a是数组的元素),没有单独的对象a,表达式a不能作为赋值的目标。
但是……
表达式a[i]被定义为*(a+i);也就是说,给定指针值a,从该地址偏移i个元素(而不是字节!),然后解引用结果。但如果a不是指针,那怎么行呢?
像这样——除非它是sizeof或一元&运算符的操作数,或者是在声明中用作数组初始化程序的字符串文字,类型为“N个元素的T数组”的表达式将被转换(“衰减”)为类型为“指向T的指针”的表达式,并且表达式的值将是数组的第一个元素的地址。
这有几个含义:
- 表达式a、&a和&a[0]将产生相同的值(数组的第一个元素的地址),但表达式的类型将不同(T*、T(*)[N]和T*); - 下标运算符[]对数组表达式和指针表达式都适用(实际上,它被定义为在指针表达式上工作); - 当你将数组表达式传递给函数时,你实际上传递的是指针值,而不是整个数组;
对于动态数组,情况是不同的。给定以下代码行:
T *p = malloc( sizeof *p * N );

那么你的存储看起来会像这样:
   +---+
p: |   | ---+
   +---+    |
    ...     |
     +------+
     |
     V
   +---+
   |   | p[0]
   +---+
   |   | p[1]
   +---+
    ...   
   +---+
   |   | p[N-1]
   +---+

在这种情况下,p是一个单独的对象,与数组不同。因此,&p不会给你与p&p[0]相同的值,并且它的类型将是T **而不是T (*)[N]。另外,由于p只是一个指针变量,所以你可以给它赋一个新值(尽管如果你在没有首先使用free释放它指向的内存的情况下这样做,你将创建一个内存泄漏)。
类似地,sizeof p不会像sizeof a一样工作;它只会返回指针变量的大小,而不是指针指向的分配内存的大小。

2
非常棒的答案!但是对于语句“除了N(如果指定),必须至少比字符串长度多1来考虑字符串终止符”有一个小问题。那不是技术上正确的。如果指定了N,则它必须大于或等于字符串的长度。如果N等于字符串的长度,则结果是一个没有NUL终止符的字符数组。例如,a[4] =“test”;是C规范允许的,参见第6.7.9/14段。 - user3386109

2

静态数组在编译时分配内存,内存分配在堆栈上。而动态数组在运行时分配内存,内存从堆中分配。

这是静态整数数组,即在运行时之前分配固定内存。

int arr[] = { 1, 3, 4 };

这是动态整数数组,即在运行时分配内存。

int* arr = new int[3]; 

Int *arr 不是一个数组,它是一个指针。 - Michi
我知道。动态内存以指针形式分配。它的名称为arr,它不是一个数组。 - Muhammad Noman
指针中没有分配内存。指针指向那里。 - Michi
我和你说的是同样的事情。动态内存以指针形式分配,指针指向数据所在的位置,无论它是分布式还是顺序形式。 - Muhammad Noman
静态数组不使用堆栈...也就是自动数组。 - M.M
@Muhammad Noman:new 是来自 C++,但问题是关于 C 的。 - John Boe

1
您正在使用大小为size+1的动态数组,并向其中添加元素(从0到size),在返回0之前释放空间。所以在您的情况下,int *m_array是一个指向int的指针。在第13行中,您所做的是声明:
(int *m_array) =...

和分配:

...(int *)malloc((size+1)*sizeof(int));

由于它是动态数组,你正在堆上分配并保留它,直到它被释放(这就是为什么你在最后有free(m_array))。如果它是静态的,你可以这样初始化数组:

int m_array[size];

如果不是自动变量,它会被分配在静态存储区,并且在程序结束时被释放。在C语言中,您无法更改静态数组的大小,因此需要使用动态数组。当您想要更改动态数组的大小时,可以使用realloc


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