Manejo de strings en C

Una cadena o string en C no es más que una secuencia de cero o más caracteres (tipo char para los byte string o tipo wchar_t para los wide string) acabada en un carácter nulo '\0'. La longitud del string se considera el número de caracteres sin incluir el carácter nulo.

Los string se pueden declarar de tres formas (y por tanto alojarse en diferentes regiones de memoria):

  • como literales (puntero a literal): char *st_literal = "esto es un string literal";
  • como array de caracteres: char st_array[50];
  • como puntero a carácter: char *st_puntero;

Uno de los primeros problemas que pueden surgir es que cuando se asigna un string automáticamente (literal), este es constante, se aloja en una zona de memoria de solo lectura y por tanto no se puede cambiar.

El CERT en su recomendación STR30-C 'Do not attempt to modify string literals' alerta del problema de modificar string literales.

Vamos a ver un ejemplo:

/* Strings */ 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
int main() 
{ 
 char *s1 = "esto es un string literal"; 
 char s2[] = "esto es un array de caracteres"; 
 char *s3;  
 
 s3 = (char *) malloc (strlen("esto es un puntero a un string")+1); 
 strcpy(s3, "esto es un puntero a un string"); 
 
 printf("%s\n", s1); 
 printf("%s\n", s2); 
 printf("%s\n", s3); 
 
 free(s3); 
  
 exit(EXIT_SUCCESS); 
} 

Aunque las tres formas asignan una frase a una variable, cada una es tratada de una forma diferente por el compilador. La primera variable s1 es un literal (realmente un puntero a un literal). Si intentamos modificar posteriormente s1 el comportamiento es indefinido, probablemente una violación de segmento ya que la zona donde se almacenan los literales es una zona de memoria de solo lectura para el programa. Intentar liberarlo con free() también producirá un error.

s1[0] = 'E'; //Segmentation fault 
free(s1); //Invalid pointer 

Aunque estas dos instrucciones compilaran sin errores, las dos fallarán en tiempo de ejecución.

En caso de usar literales es recomendable hace la variable constante:
const char *s1 = "esto es un string literal";
De esta forma tendremos un error en tiempo de compilación si intentamos modificar la constante y un aviso en caso de intentar liberarla con free().

En cambio estas mismas instrucciones son perfectamente válidas para s3 ya que la cadena a la que apunta s3 se aloja en el montón (heap) :

s3[0] = 'E'; //Cambia la primera letra de s3 
free(s3); //Libera la memoria  

Por otro lado en el array de caracteres s2[] podemos modificar su contenido, pero no liberarlo con free() lo que nos daría un warnning en tiempo de compilación y un error de acceso a memoria si ejecutamos el programa. Tampoco podemos cambiar el tamaño de s2, lo que quiere decir que podría contener una cadena de igual tamaño a la que tenía en origen o más pequeña.

Si intentamos asignarle un valor mayor que el de origen a un array de caracteres, tendríamos un warnning en tiempo de compilación (si la asignación tiene lugar en tiempo de compilación) y desbordamiento de buffer en ejecución (CERT STR31-C Guarantee that storage for strings has sufficient space for character data and the null terminator).

Haciendo las cosas más sencillas

Tener diferentes formas de hacer las cosas esta bien, pero no significa que tengamos que emplear todas al mismo tiempo. Podemos unificar nuestras asignaciones de strings con el fin de evitar estos errores y hacer las cosas más sencillas.

Hay dos enfoques básicos para administrar cadenas en programas C: el primero es mantener cadenas en matrices asignadas estáticamente; el segundo es asignar dinámicamente memoria según sea necesario - CERT STR01-C. 'Adopt and implement a consistent plan for managing strings'

Por tanto, una forma de unificar es declarar todas los strings de forma manual utilizando memoria dinámica. Esto es lo que sería utilizar malloc() para cada asignación y liberarlas con free().

Podemos utilizar dos funciones que van a asignar la memoria por nosotros: asprintf() y strdup():

/* Strings Manuales*/ 
#define _GNU_SOURCE // para incluir asprintf 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
int main() 
{ 
    char *s1 = strdup("como un literal, pero es un puntero"); 
    char *s2;  
 
    asprintf(&s2, "esto es un puntero a string");  
     
    printf("%s\n", s1); 
    printf("%s\n", s2); 
 
    free (s1); 
    free (s2); 
     
    exit(EXIT_SUCCESS); 
} 

Las dos funciones asignan la memoria necesaria en el heap y nos devuelven un puntero. Al ser una asginación manual tenemos que hacer la liberación con free() de cada variable.

Es posible modificar las variables cuanto queramos:

/* Strings Manuales - Reasignación*/ 
#define _GNU_SOURCE // para incluir asprintf 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
int main() 
{ 
    char *s1 = strdup("como un literal, pero es un puntero"); 
    char *s2;  
 
    asprintf(&s2, "esto es un puntero a string");  
     
    printf("%s\n", s1); 
    printf("%s\n", s2); 
 
    asprintf(&s1, "string s1 modificada");  
    asprintf(&s2, "string s2 modificada");  
     
    printf("%s\n", s1); 
    printf("%s\n", s2); 
     
    free (s1); 
    free (s2); 
     
    exit(EXIT_SUCCESS); 
} 

Hemos modificado s1 y s2. Pero tenemos que tener en cuenta que tanto strdup() como asprintf() lo que hacen es una nueva asignación de memoria, es decir no liberan el espacio anterior ni lo sobreescriben. Si damos un vistazo a esta última aplicación con valgrind veremos que las dos asignaciones iniciales no se liberaron:

david@sglan-pc7:~/Proyects/testc/str30c$ valgrind --leak-check=yes ./literales

==8741== Memcheck, a memory error detector
==8741== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==8741== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==8741== Command: ./literales
==8741==
como un literal, pero es un puntero
esto es un puntero a string
string s1 modificada
string s2 modificada
==8741==
==8741== HEAP SUMMARY:
==8741== in use at exit: 64 bytes in 2 blocks
==8741== total heap usage: 8 allocs, 6 frees, 1,430 bytes allocated
==8741==
==8741== 28 bytes in 1 blocks are definitely lost in loss record 1 of 2
==8741== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==8741== by 0x4EC461F: vasprintf (vasprintf.c:73)
==8741== by 0x4EA1153: asprintf (asprintf.c:35)
==8741== by 0x1087B8: main (literales.c:30)
==8741==
==8741== 36 bytes in 1 blocks are definitely lost in loss record 2 of 2
==8741== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==8741== by 0x4ED99B9: strdup (strdup.c:42)
==8741== by 0x10879C: main (literales.c:27)
==8741==
==8741== LEAK SUMMARY:
==8741== definitely lost: 64 bytes in 2 blocks
==8741== indirectly lost: 0 bytes in 0 blocks
==8741== possibly lost: 0 bytes in 0 blocks
==8741== still reachable: 0 bytes in 0 blocks
==8741== suppressed: 0 bytes in 0 blocks
==8741==
==8741== For counts of detected and suppressed errors, rerun with: -v
==8741== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

Efectivamente necesitamos liberar con free() cada asignación antes de reasignarla (las cadenas anteriores siguen existiendo solo que hemos perdido el puntero que las apuntaba).

Macro para automatizar strings

Para hacer las cosas más fáciles y menos propensas a error, vamos a automatizar las reasignaciones de asprintf() con una macro que libere la anterior antes de crear la nueva cadena:

/* Strings Manuales - con macro */ 
#define _GNU_SOURCE // para incluir asprintf  
#include <stdio.h> 
#include <stdlib.h> 
 
/* Gestionamos cadenas de texto con asprintf que asigna dinamicamente 
    el espacio usando malloc.  
    Si devuelve -1 es que no hay memoria, salimos con exit */ 
#define AString(my_string, ...) { 					\ 
    char *tempor_string = (my_string); 				\ 
    int res = asprintf(&(my_string), __VA_ARGS__); 	\ 
    if(res == -1) exit(EXIT_FAILURE); 				\ 
    free(tempor_string); 							\ 
} 
 
int main() 
{ 
    char *c1 = NULL; // siempre iniciamos las cadenas a NULL 
     
    AString(c1, "string manual dinámico"); 
    AString(c1, "%s - Se puede añadir o modificar la cadena", c1); 
 
    printf ("c1:%p = %s\n", c1, c1); 
 
    free(c1); 
     
    exit(EXIT_SUCCESS); 
} 

Aprovechamos en la macro para comprobar la salida de asprint(), si devuelve -1 es que no hay memoria, en este caso salimos con EXIT_FAILURE.

Siempre es buena política inicializar las variables, en este caso también nos sirve para no dar error en free() cuando todavía no tiene nada.

De esta forma podemos modificar el string todo lo que queramos y solo tenemos que liberarlo cuando no vayamos a necesitarlo más. Como vemos con valgrind, la memoria se ha ido liberando correctamente:

david@sglan-pc7:~/Proyects/testc/strings$ valgrind --leak-check=yes ./strings

==12818== Memcheck, a memory error detector
==12818== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12818== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==12818== Command: ./strings
==12818==
c1:0x522d200 = string manual dinámico - Se puede añadir o modificar la cadena
==12818==
==12818== HEAP SUMMARY:
==12818== in use at exit: 0 bytes in 0 blocks
==12818== total heap usage: 5 allocs, 5 frees, 1,313 bytes allocated
==12818==
==12818== All heap blocks were freed -- no leaks are possible
==12818==
==12818== For counts of detected and suppressed errors, rerun with: -v
==12818== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Liberación automática

Podemos dar un giro de tuerca más a nuestra asignación dinámica, haciendo la liberación automática. Para ello vamos a usar un atributo que tiene GCC, que permite ejecutar una función cuando una variable sale de ámbito: __attribute__

Vamos a crearla con una macro para no tener que repetirla en cada string que queramos hacer automático:

/* 
 * aString.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
 * 
 * 
 * Strings Manuales - con macro y liberación automática  
 * 
*/ 
 
#define _GNU_SOURCE // para incluir asprintf 
#include <stdio.h> 
#include <stdlib.h> 
 
/* Gestionamos cadenas de texto con asprintf que asigna dinamicamente 
    el espacio usando malloc. 
    Si devuelve -1 es que no hay memoria, salimos con exit */ 
#define AString(my_string, ...) ({ \ 
    char *tempor_string = (my_string); \ 
    int res = asprintf(&(my_string), __VA_ARGS__); \ 
    if(res == -1) exit(EXIT_FAILURE); \ 
    free(tempor_string); \ 
}) 
 
/* Definimos _AUTO_FREE_ para que llame a una función cuando la variable 
    salga de ámbito. En este caso llamamos a free_buffer para liberar la 
    memoria con free */ 
#define _AUTO_FREE_ __attribute__((cleanup(free_buffer))) = NULL 
 
typedef char * String; 
 
void free_buffer(char **buffer) 
{ 
    printf("Liberando buffer: %p\n", *buffer); 
    free(*buffer); 
    *buffer = NULL; 
} 
 
void fun_strings_autofree(void); 
 
int main() 
{ 
    printf("Dentro de main\n"); 
    fun_strings_autofree(); 
    printf("bye, bye\n"); 
 
    exit(EXIT_SUCCESS); 
} 
 
void fun_strings_autofree() { 
    String c2 _AUTO_FREE_; 
    String c3 _AUTO_FREE_; 
 
    AString(c2, "Esto es otra prueba"); 
    AString(c2, "%s - También se puede añadir o modificar la cadena", c2); 
 
    AString(c3, "Más datos para nuestros strings"); 
    AString(c3, "Ahora más pequeña"); 
 
    printf ("c2:%p = %s\n", c2, c2); 
    printf ("c3:%p = %s\n", c3, c3); 
 
} 

Podemos ver como se liberan los string cuando se sale de la función fun_strings_autofree() donde hemos creado dos strings con la macro _AUTO_FREE_ con la que añadimos la función free_buffer() a la variable y que se ejecutará cuando esta sale de ámbito.

Por si acaso un vistazo a valgrind nos confirma que todo se libero correctamente:

david@sglan-pc7:~/Proyects/testc/strings$ valgrind --leak-check=yes ./strings 

==14024== Memcheck, a memory error detector
==14024== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==14024== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==14024== Command: ./strings
==14024==
Dentro de main
c2:0x522d640 = Esto es otra prueba - También se puede añadir o modificar la cadena
c3:0x522d8a0 = Ahora más pequeña
Liberando buffer: 0x522d8a0
Liberando buffer: 0x522d640
bye, bye
==14024==
==14024== HEAP SUMMARY:
==14024== in use at exit: 0 bytes in 0 blocks
==14024== total heap usage: 9 allocs, 9 frees, 1,567 bytes allocated
==14024==
==14024== All heap blocks were freed -- no leaks are possible
==14024==
==14024== For counts of detected and suppressed errors, rerun with: -v
==14024== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Todo esto puede valer para un uso básico de strings, pero si vamos a trabajar de forma intensa con texto es recomdable usar una librería string, como bstring o la que nos obrece Glib. Generalmente estas librerías nos ofrecen una estructura string que contiene la cadena de texto y su longitud y también diferentes funciones para trabajar con la estructura string. En próximas entregas examinaremos el uso de alguna de estas librerías.

Modificado por última vez enViernes, 14 Agosto 2020 22:04
(3 votos)
Etiquetado como :

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.