En la programación real, a menudo ocurre que el espacio de memoria necesario depende de los datos de entrada reales y no se puede determinar de antemano. Para este tipo de problema, es difícil resolverlo con arreglos estáticos. Para solucionar el problema anterior, el lenguaje C proporciona algunas funciones de gestión de memoria. Estas funciones de gestión de memoria, combinadas con punteros, pueden asignar dinámicamente espacio de memoria según sea necesario para construir arreglos dinámicos, y también pueden recuperar el espacio que ya no se utiliza para usos futuros, lo que proporciona un medio para utilizar eficazmente los recursos de memoria.
Los arreglos dinámicos se definen en contraposición a los arreglos estáticos. La longitud de un arreglo estático está predefinida; una vez que se le da un tamaño, no se puede cambiar durante todo el programa. Los arreglos dinámicos no son así; pueden reespecificar su tamaño según las necesidades del programa. El espacio de memoria de un arreglo dinámico se asigna desde el montón (heap), es decir, se asigna dinámicamente. El espacio de almacenamiento se le asigna mediante la ejecución de código. Solo se asigna cuando el programa ejecuta estas sentencias. El programador es responsable de liberar la memoria.
¿Por qué usar arreglos dinámicos?
En la programación real, a menudo ocurre que el espacio de memoria necesario depende de los datos de entrada reales y no se puede determinar de antemano. Para este tipo de problema, es difícil resolverlo con arreglos estáticos. Para solucionar el problema anterior, el lenguaje C proporciona algunas funciones de gestión de memoria. Estas funciones de gestión de memoria, combinadas con punteros, pueden asignar dinámicamente espacio de memoria según sea necesario para construir arreglos dinámicos, y también pueden recuperar el espacio que ya no se utiliza para usos futuros, lo que proporciona un medio para utilizar eficazmente los recursos de memoria.
Comparación entre arreglos dinámicos y arreglos estáticos
Para los arreglos estáticos, su creación es muy conveniente, no necesitan liberarse después de usarlos y es sencillo referenciarlos; sin embargo, el hecho de que no se pueda cambiar su tamaño después de crearlos es su punto débil fatal.
Para los arreglos dinámicos, su creación es más problemática, y después de usarlos deben ser liberados por el propio programador; de lo contrario, pueden ocasionar graves fugas de memoria. Sin embargo, su uso es muy flexible, ya que pueden asignar dinámicamente el tamaño según las necesidades del programa.
Cómo construir arreglos dinámicos
Principios a seguir
Al solicitar, se solicita capa por capa, desde la capa exterior hacia la capa interior;
Al liberar, se libera capa por capa, desde la capa interior hacia la capa exterior.
Punteros necesarios para la construcción
Para construir un arreglo dinámico unidimensional, se necesita un puntero unidimensional;
Para uno bidimensional, se necesitan punteros unidimensionales y bidimensionales;
Para uno tridimensional, se necesitan punteros unidimensionales, bidimensionales y tridimensionales;
Y así sucesivamente.
Funciones necesarias para la construcción
| Prototipo de la función | Retorno | Descripción de la función |
| void *malloc(unsigned int size); | Éxito: devuelve la dirección inicial del espacio asignado. Fracaso: devuelve un puntero nulo. | Solicita al sistema un espacio de montón de size bytes. |
| void *calloc(unsigned int num, unsigned int size); | Éxito: devuelve la dirección inicial del espacio asignado. Fracaso: devuelve un puntero nulo. | Solicita un espacio de montón de num elementos de size bytes según el tipo. |
| void free(void *p); | Sin valor de retorno | Libera el espacio de montón al que apunta p. |
| void *realloc(void *p,unsigned int size); | Éxito: devuelve la dirección inicial del nuevo espacio asignado. Fracaso: devuelve un puntero nulo. | Cambia el espacio de montón al que apunta p a size. |
Nota:
-
(1) Se especifica como tipo void *, lo que no significa que la función no tenga valor de retorno después de la llamada, sino que devuelve la dirección de un nodo, y el tipo de esta dirección es void (sin tipo o tipo indeterminado), es decir, la dirección inicial de un área de almacenamiento cuyo tipo concreto no se puede determinar; solo se determina al usarlo según los valores de cada campo. Se puede convertir a otros tipos mediante una conversión forzada. Por ejemplo: double *pd=NULL; pd=(double *)calloc(10,sizeof(double)); Esto significa solicitar al sistema 10 espacios de almacenamiento continuos de tipo double, y usar el puntero pd para apuntar a la dirección inicial de este espacio continuo. Además, se usa (double) para convertir el tipo de retorno de calloc(), con el fin de poder asignar la dirección de los datos de tipo double al puntero pd.
-
(2) El propósito de usar sizeof es calcular el número de bytes que ocupa un tipo, de modo que se adapte a diferentes compiladores.
(3) Dado que la asignación dinámica no siempre tiene éxito, es necesario añadir un segmento de manejo de excepciones para que el programa no se detenga y el usuario no se quede sin saber qué hacer. Normalmente se utiliza un segmento de manejo de excepciones como este: if(p==NULL) /* oif(!p)*/ { printf("动estado申solicitardentroalmacenarfracaso!\n"); exit(1); //Excepciones退出 }
-
(4) Los archivos de cabecera de estas cuatro funciones están todos incluidos en
. -
(5) El espacio de montón asignado no tiene nombre; solo se puede acceder a él a través del puntero devuelto.
-
(6) Nunca se debe usar free en bloques de almacenamiento que no hayan sido asignados dinámicamente. Tampoco se debe liberar la misma área de memoria dos veces con free. Por ejemplo: free(p); free(p);
(7) Al llamar a free(), la memoria a la que apunta el puntero pasado se libera, pero el valor del puntero en la función que llama puede permanecer igual, porque p se pasa a la función como parámetro formal. Estrictamente hablando, el valor del puntero liberado no es válido, porque ya no apunta al área de memoria solicitada. Cualquier uso posterior del mismo puede causar problemas.
Diferencia entre malloc y calloc
Para el área de memoria asignada con malloc, si no se ha utilizado antes, es posible que cada bit sea 0; por el contrario, si esta parte del espacio de memoria ha sido asignada, liberada y reasignada previamente, puede contener todo tipo de datos residuales. Es decir, un programa que usa la función malloc() puede funcionar normalmente al principio (cuando el espacio de memoria aún no ha sido reasignado), pero después de un tiempo (cuando el espacio de memoria ya ha sido reasignado) puede presentar problemas. Por lo tanto, antes de usarlo, se debe inicializar (se puede usar la función memset para inicializarlo a 0). Sin embargo, el espacio asignado al llamar a la función calloc() ya se ha inicializado a 0 en el momento de la asignación. Cuando eliges entre la función calloc() y la función malloc(), debes considerar si necesitas inicializar el espacio de memoria asignado, y así elegir la función correspondiente.
Método de construcción específico
Tomemos como ejemplo el arreglo entero tridimensional array[n1][n2][n3].
Primero sigue el principio de solicitar capa por capa, desde la capa exterior hacia la capa interior:
El puntero más externo es array, que es un puntero tridimensional, y apunta a array[], que es un puntero bidimensional. Por lo tanto, para asignar memoria a array
la solicitud de memoria debe ser:
array=(int***)calloc(n1,sizeof(int**));
El puntero del siguiente nivel es array[], que es un puntero bidimensional, y apunta a array[][], que es un puntero unidimensional. Por lo tanto, para asignar memoria a array[]
la solicitud de memoria debe ser:
for(i=0;i<n1;i++)
{
array[i]=(int**)calloc(n2,sizeof(int*));
}
El puntero más interno es array[][], que es un puntero unidimensional, y apunta a array[][][], que es una constante entera. Por lo tanto, para asignar memoria a array[][], la solicitud debe ser:
for(i=0;i<n1;i++)
{
for(j=0;j<n2;j++)
{
array[i][j]=(int*)calloc(n3,sizeof(int));
}
}
Por supuesto, puedes combinarlos todos juntos como:
int i,j,k;
int n1,n2,n3;
int ***array;
scanf("%d%d%d",&n1,&n2,&n3);
array=(int***)calloc(n1,sizeof(int**));
for(i=0;i<n1;i++)
{
array[i]=(int**)calloc(n2,sizeof(int*));
for(j=0;j<n2;j++)
{
array[i][j]=(int*)calloc(n3,sizeof(int));
for(k=0;k<n3;k++)
{
array[i][j][k]=i+j+k+1;
}
}
}
Por último, no olvides liberar esta memoria; esto debe seguir el principio de liberar capa por capa, desde la capa interior hacia la capa exterior.
El proceso de análisis puede consultarse en la respuesta anterior; no se repetirá aquí. Solo se proporciona el código:
for(i=0;i<n1;i++)
{
for(j=0;j<n2;j++)
{
free(array[i][j]);//释放第三维指针
}
}
for(i=0;i<n1;i++)
{
free(array[i]);//释放第二维指针
}
free(array);//释放第一维指针
El proceso de creación de otras dimensiones, como la cuarta dimensión, es prácticamente el mismo; no se repetirá aquí.