Breviario de Lenguaje C

Datos y Tipos de Datos

Valores y Objectos

No confundir object en C con los objetos de POO (por ejemplo de Java). NO es lo mismo.

  • Un valor es un dato, la codificación binaria de un número (entero, coma flotante), una letra (o un texto, una cadena alfanumérica).

  • Un objecto según el estándar de C es “region of data storage in the execution environment, the contents of which can represent values”. Es decir, una zona de memoria donde el programa va a guardar un valor.

  • La manera más sencilla de usar objectos es el uso de variables, pero no la única.

  • Los objectos se pueden usar en el lado izquierdo de una asignación para modificar el valor que guardan. Se entiende cualquier forma de expresión cuyo resultado sea un objeto. Esto se denomina “left value”.

  • Cuando un objecto se sitúa en el lado derecho de una asignación se utiliza su valor, esto se denomina “right value

Variables y tipos.

  • Una variable es un objecto que tiene un identificador y un tipo. Es decir, se declara.

  • Un tipo implica un tamaño (no siempre fijado unívocamente en el estándar) y una codificación.

Declaración

Se escribe tipo e identificador. Ejemplos:

int a;
int b;
int x, y;

Opcionalmente se puede inicializar cualquier declaración:

int a = 3;
int b;
int x = -2, y = 2;

Tipos comunes (simples)

  • Enteros (con signo): char, short, int, long int, (long long int en C99).

  • Enteros (sin signo): unsigned char, unsigned short, unsigned int, unsigned long int, (unsigned long long int en C99).

  • Coma flotante: float, double

  • Alfanuméricos: char

Constantes Literales

Las constantes literales se escriben tal cual mediante su valor. Aplican las siguientes reglas:

  • Los números sin punto decimal positivos o negativos son constantes literales enteras (int). Ejemplos: 3 -5 10 -324

  • Números escritos con punto decimal son constantes literales en coma flotante (double). Ejemplos: 3.14 -1.414

  • Idem para números en notación científica, es indiferente escribir e ó E. Ejemplos: 1e-5 3.14E5 1.1e-4 -1.414e+3

  • Los caracteres (letras) individuales son constantes alfanuméricas simples (char). Se escriben con comilla simple y recta. Ejemplos: 'a' '4' ';' ' ' '?'

  • Algunos caracteres especiales se escriben mediante una secuencia especial que empieza por barra invertida (backslash). Ejemplo: '\n' '\t' '\''

  • Las cadenas alfanuméricas se escriben con comillas doble recta. Ejemplos: "Hola Mundo" "Introduce un dato\n" "Entre \"comillas\" dobles". Es obligatorio cerrar la cadena alfanuméricoa en la misma línea de código fuente.

Casos Especiales

  • Se pueden poner sufijos para cambiar el tipo del literal, por ejemplo: 3.14f es de tipo float, 34U es unsigned int, 12L es long int.

  • Se pueden escribir constantes enteras en octal o hexadecimal prefijando el número con 0 ó 0x respectivamente. Ejemplos: 010 es 8, 0x10 es 16.

  • Si se escriben dos cadenas alfanuméricas seguidas, sin nada más que espacio entre ellas se juntan en la misma constante. Esto es útil para escribir cadenas alfanuméricas largas. Por ejemplo:

"Hola "
"Mundo"

Es equivalente a "Hola Mundo"

Expresiones

Operandos y operaciones

  • Operandos: constantes, variables, resultados de otras operaciones.

  • Operaciones: Atención al tipo de los operandos y del resultado.

Aritméticas: + - * / %

  • Operandos numéricos, resultado numérico del mismo tipo.

  • Cuando tienen distinto tipo se “promociona” el de menor rango.

  • La división entera tiene dos operaciones: cociente y resto, respectivamente / y %.

Ejemplos: El resultado de 1 / 2 es 0, 1 % 2 es 1, 7 / 2 es 3.

Operaciones booleanas: ! (negación) && (y lógico) || (ó lógico)

  • Operandos numéricos, resultado int. Cualquier operando se convierte en verdadero si es distinto de cero y en falso si es cero. El resultado siempre es 1 (verdadero) o 0 (falso).

Ejemplos: El resultado de 1 && 2 es 1, 0 || 0.4 es 1, ! 22 es 0.

Operaciones de comparación: == != < > <= >=

  • Operandos cualquier tipo (el mismo), resultado int (booleano).

  • Tienen más prioridad que las booleanas, pero mejor usar paréntesis

  • Cuidado con omitir operandos. Ejemplo: 1 < x < 10 es siempre 1 (verdadero). Lo correcto es: 1 < x && x < 10.

Operaciones de bit: & (y bitwise) | (ó bitwise) ^ (xor bitwise) ~ (negación bitwise) << >> (desplazamientos)

  • Operandos enteros, resultado entero.

  • Aplica a cada bit la operación correspondiente.

Ejemplos:

  • 1 << 3 es 8, 12 | 11 es 15, 12 & 11 es 8.

  • x & (1 << 3) El resultado es el valor de x con un 0 en el tercer bit empezando por la derecha.

  • x | (1 << 3) El resultado es el valor de x con un 1 en el tercer bit empezando por la derecha.

  • ~ x + 1 El resultado es el valor de -x (si x es un entero con signo).

Uso del paréntesis ( )

  • Sirven para priorizar unas operaciones sobre otras.

  • Incluso cuando no sean necesarios contribuyen a hacer más claras las operaciones al hacer explícito el orden de las operaciones.

Asignación =

La asignación es una operación que tiene:

  • Un efecto, cambiar el valor del objecto a la izquierda del = (el left value).

  • Un resultado, el propio valor que se asigna. Normalmente este resultado no se usa, se descarta.

Ejemplo: x = 3 efecto: la variable x modifica su valor a 3 perdiendo el valor anterior, resultado: 3 (el mismo valor asignado).

Asignación combinada: += -= *= /= %= &= |= ^=

La asignación combinada es una operación aritmética o de bit seguida de una asignación.

Ejemplo: x += 3 es lo mismo que x = x + 3

Operador ,

  • Esta operación evalua las expresiones a cada lado, su resultado es el resultado de la derecha.

Ejemplo: 3,4 es 4, por tanto cuidado con usar , en vez de . para escribir números en coma flotante.

Operador ternario (1) ? (2) : (3)

  • Evalua el primer operador y toma como resultado la expresión (2) si (1) es verdadero o el resultado de la expresión (3) en caso contrario.

Ejemplo: a < b ? a : b es el valor mínimo de a y b.

Sentencias

En un programa en C existen:

  1. Declaraciones

  2. Sentencias

Las declaraciones definen nuevos identificadores: variables, funciones, tipos de datos… No son código que se ejecute, salvo quizás alguna inicialización.

Mientras que las sentencias sí se ejecutan, son las ordenes que el ordenador realiza según el programa que se ha escrito. Todas las sentencias en los programas de C deben estar escritas dentro de una función.

Una sentencia puede ser:

  1. Una expresión terminada con un punto y coma.

  2. Se incluyen como expresiones las llamadas a función (el paréntesis de llamada a una función se considera un operador).

  3. Una sentencia compuesta delimitada por una apertura y un cierre de llaves (sin punto y coma final).

  4. Una sentencia condicional (if, switch).

  5. Una sentencia repetitiva o bucle (for, while, do … while).

Sentencias if y switch

Sintaxis if

if (cond) sentencia1
[ else sentencia2 ]
  • Cuando se anidan el else se asocia al if más cercano que sea posible.

Sintaxis switch

switch ( entero )
{
    case CTE_LITERAL1 :
        break;
    case CTE_LITERAL2 :
        break;
    default:
        break;
}
  • Se producen dos saltos el primero hasta la etiqueta que coincide y el segundo del break hasta la siguiente sentencia.

  • Las sentencias break son opcionales. Las etiquetas no son ejecutables en ningún caso.

Bucles (TBD)

TODO: Pendiente de escribir

Entrada y Salida de Datos

Canales de entrada/salida

Cuando se arranca un programa en la terminal se crean tres canales estándares: entrada, salida y errores. Por defecto:

  • El canal de entrada estándar toma la información del teclado (y la lleva al programa).

  • Lo que se escribe en los canales estándares de salida y errores se muestra en la terminal en forma de texto.

Nota: Al usar el teclado la información que el usuario escribe sólo se envía al canal de entrada cuando se pulsa enter. Pero, además, la propia pulsación de enter es un salto de línea y se envía como tal al canal de entrada.

Salida con formato

Para escribir en el canal de salida se usa printf. Para llamar a printf hay que pasar como argumento una cadena alfanumérica (con ") donde se indica el texto a escribir.

Para escribir datos de variables o expresiones se utilizan argumentos adicionales, por cada argumento se debe colocar en la cadena de formato un especificador de conversión que determinan cómo se debe convertir el dato pasado como argumento al texto que se va a escribir en la terminal. La conversión se indica con un tipo de dato asociado.

Los siguientes son los especificadores de conversión más habituales:

Especificador

Tipo de Conversión

%d

Número entero en base 10

%f

Números en coma flotante (cualquiera)

%c

Letra

%s

Cadena alfanumérica (texto)

Algunas extensión a estos especificadores de conversión básicos son:

  • Indicar la anchura: se escribe un número entre el % y la letra. Sirve para hacer columnas en la salida.

  • Indicar las cifras decimales en los números en coma flotante: se escribe un punto y el número de cifras. Por ejemplo: %.3f son tres cifras decimales.

  • Escribir un + delante de los números positivos: se escribe un + justo después del %. Por ejemplo: %+d.

  • Para escribir el símbolo de tanto por ciento sin confusión respesto de un especificador de conversión se escriben dos: %%.

  • Hay muchas más: rellenar los números con ceros a la izquierda, conversiones a notación científica, a hexadecimal u octal…

Nota: No es habitual usarlo, pero el retorno de printf es el número de caracteres escrito en el canal.

Entrada con formato

Para leer del canal de entrada estándar se usa scanf. Para llamar a scanf hay que pasar como argumento una cadena alfanumérica (con ") donde se indica el contenido esperado a la entrada. Este argumento se denomina cadena de formato.

Al leer, se va comprobando si lo que se encuentra se corresponde con lo esperado y si no es así, la lectura se interrumpe.

En la cadena de formato se puede encontrar:

  1. Especificadores de conversión. Para los especificadores de conversión se intenta convertir los caracteres de la entrada al tipo indicado hasta que ya no sea posible. Por ejemplo: si el especificador de conversión es %d se intentan leer los caracteres para calcular un número en base 10, cuando algún carácter no pueda formar parte del número se interrumpe la lectura.

  2. Blancos (espacio, tabulador, salto de línea). Cualquier blanco en el formato tiene el mismo efecto: se corresponde con cualquier número de blancos en el canal de entrada. El efecto que se logra es saltar u omitir los blancos al leer el texto en el canal de entrada. Se suele utilizar un simple espacio (y no tabulares o saltos de línea). Se insiste desde el punto de vista de lectura todos los blancos son equivalentes.

  3. Otros caracteres. Cualquier otro carácter tiene que corresponder exactamente con lo que se encuentra en el canal de entrada.

Al leer el canal, scanf procesa los caracteres que encuentra, los transforma en el tipo de dato especificado y guarda el valor hallado en la variable apuntada por la dirección de memoria que se pasa como argumento. Por esta razón la coincidencia debe ser exacta.

Los siguientes son los especificadores de conversión más habituales:

Especificador

Tipo de Conversión

Tipo del argumento

%d

Número entero en base 10

int*

%f

Números en coma flotante

float*

%lf

Números en coma flotante

double*

%c

Letra

char*

%s

Cadena alfanumérica (texto)

char* (array)

Al leer con los especificadores: %d, %f, %lf, %s se saltan los blancos que se puedan encontrar inicialmente. No así con %c, si hubiese un blanco ese es el valor que se lee y guarda en la variable.

Para los datos numéricos la lectura se interrumpe cuando el siguiente carácter no pueda formar parte de la representación del número (un espacio, una coma, una letra…). Para %s la lectura se interrumpe al llegar a un espacio en blanco, el efecto, equivale aproximadamente a leer palabras pero los signos de puntuación sí se agregan a la cadena leída.

Además de estos especificadores es muy útil el especificador corchete que sirve para leer cadenas alfanuméricas mientras los caracteres leídos estén entre los dados entre los corchetes (%[]) o hasta que se encuentre uno que estén entre los corchetes (%[^]).

Hay dos extensiones a estos especificadores:

  1. La anchura: un número que se indica detrás del % y que indica el número máximo de caracteres leídos. Esto sirve para leer columnas de ancho fijo. Por ejemplo: "%3d%3d separaría el texto 123456 en el canal de entrada en dos números: 123 y 456. La anchura debe ser considerada obligatoria en la lectura de cadenas para no desbordar el tamaño de la cadena.

  2. Modificadores de longitud. Por ejemplo: %Ld sirve para guardar el valor leído en una variable de tipo long.

El retorno de scanf es el número de datos leídos correctamente y, por tanto, almacenados en las correspondientes variables.

Cuando no es posible leer un número (se encuentra una letra o cualquier otro carácter que no se corresponde con el número esperado) la lectura se interrumpe y este dato no se cuenta como leido. Por ejemplo, si la entrada es:

-34 a 4.3

y se intenta leer con:

int num; char letra; double x;
int res;

res = scanf("%d%c%lf", &a, &letra, &x);

La variable res tomará el valor 2 porque se leerán los valores para num (-34), para letra (el espacio), pero no se podrá leer el valor de x (porque hay una ‘a’) y, por tanto, no cuenta como leida. De las 3 variables se han leido 2 y ese es el retorno del scanf.

Si se hiciese: res = scanf("%d %c%lf", &a, &letra, &x); el retorno sí sería 3 (se salta el espacio) y la variable x tomaría el valor esperado.

También puede ser EOF si se intenta leer más allá del final del canal de entrada.

Punteros

Definición

Un puntero es:

  • Un tipo de dato que se corresponde con una dirección de memoria. En sistemas operativos de 64 bits son un enteros sin signo de 8 bytes. Una dirección de memoria es el número que permite encontrar un dato concreto en la memoria del ordenador.

  • Una variable de este tipo.

  • Y son una referencia a una variable (objeto en la terminología C)

Además:

  • La frase: “puntero apunta a variable” significa que la dirección de memoria que está guardada en el puntero es la dirección de la variable.

  • La frase: “p es un puntero a entero” significa que en la dirección de memoria guardada en p se supone que hay un variable de tipo entero.

Sintaxis

  • Se coloca un asterisco delante del identificador de la variable.

  • El siguiente código declara un puntero, p que apunta a un entero, int:

int *p;

Operaciones con punteros

Operación indirección

  • Su símbolo es el asterisco (*)

  • Es una operación aplicable sólo a punteros

  • Es una operación unaria (un único operando) por la izquierda (el símbolo de la operación se coloca a la izquierda del operando)

  • Obtiene la variable a la que apunta el puntero

  • El tipo del resultado coincide con el tipo al que apunta el puntero

Operación dirección de

  • Su símbolo es el carácter (&) et ó ampersand

  • Es una operación aplicable sólo a variables (objetos)

  • Es una operación unaria (un único operando) por la izquierda (el símbolo de la operación se coloca a la izquierda del operando)

  • Obtiene la dirección de memoria de una variable

  • El tipo del resultado es un puntero que apunta al tipo de la variable

Ejemplo

int num = 6;
int *p; /* Declaración de un puntero a entero */

p = &num; /* p apunta num */

num = 2 * *p; /* Se multiplica 2 por 6 */

printf("num = %d\n", num); /* Muestra 12 */

*p = 7; /* Se fija a 7 el valor de la variable apuntada por p */

printf("num = %d\n", num); /* Muestra 7 */

Una forma de verlo:

Por definición las operaciones & y * son inversas, es decir: *&num es identicamente igual a num.

Funciones

Sintaxis básica

tipo_retorno id_funcion(Tipo1 par1, Tipo2 par2) {
    /* Implementación */
    return expresion;
}

Importante: tipo_retorno NO puede ser un array (con [ ]).

Paso de parámetros por valor

El paso de todos los parámetros es por valor, siempre.

Es equivalente a hacer: par = expresion_arg, donde el lado derecho de la asignación es, precisamente, el argumento que se pasa a la función en la llamada.

Paso de parámetros por referencia

El paso de un puntero a una función también es por valor, pero como los punteros son referencias, entonces, el paso de un puntero implica:

  • Que se puede acceder (leer o modificar) la dirección de memoria apuntada por el puntero (normalmente una variable).

  • Y, entonces, la variable apuntada por el puntero es un parámetro indirecto, se dice que la variable (como tal) se pasa por referencia dado que se podrá utilizar o modificar su valor.

  • En la práctica sirve para tener parámetros:

  1. De salida. Es decir, aquellos donde se fija el valor de la variable con un resultado calculado internamente en la función.

  2. De entrada / salida. Es decir, aquellos donde se utiliza el valor de la variable para hacer los calculos dentro de la función y, posteriormente, se modifica con un resultado de la función.

Organización del código en C.

Conceptos generales

Por costumbre se entiende que:

  • Los archivos .h se incluyen y contienen las cabeceras o declaraciones de funciones.

  • Los archivos .c se compilan y contienen la implementación de las mismas

Declaración y Definición

  • La declaración de una función es su cabecera

  • La definición de una función es su implementación

  • La definición de una variable es lo que solemos llamar declaración por no ser demasiado estrictos

  • La declaración de una variables, sintácticamente, es su definición precedida por extern

Compilación y translation unit

  • Un archivo .c que se compila de forma individual una vez que ha pasado por el preprocesador se dice que una translation unit. Se distigue de propio archivo .c porque en la translation unit se encuentra todo el código fuente de los archivos de cabeceras que se hayan incluido.

  • Para que la compilación tenga éxito:

    1. Se debe encontrar la declaración de todas las variables y funciones usadas

    2. Pero NO hace falta que estén sus definiciones

    3. No pasa nada porque se repitan declaraciones, pero sólo puede haber una definición de cada cosa.

Enlace del programa

  • Una vez compiladas todas las translation units se procede a enlazar todo para producir el programa. Es decir, para producir un programa (el código objeto ejecutable) se juntan todos los códigos objetos, .o (.obj) de las compilaciones previas.

  • Para producir el programa se debe cumplir:

    1. Que se encuentre una definición y sólo una para cada variable y función declaradas.

    2. Que se encuentre una función y sólo una con el nombre main

    3. Si no es así se producirán errores de link

Uso de librerías

  • En esencia, una librería en C es juntar archivos *.o procedentes de compilaciones de archivos de C relacionados.

  • De hecho, suele ser un archivo comprimido o un tar de estos archivos.

  • Se usan en el momento de enlazar para proporcionar las definciones de las funciones en la librería.

  • Para poder usar las funciones de la librería en otros archivos de C hace falta tener sus declaraciones. Normalmente estas se encuentran en archivos .h que también se suministran junto con la propia librería.

  • Por costumbre se suelen colocar en una carpeta llamada include

Comandos

Para compilar archivos:

gcc -ansi -pedantic -Wall -Wextra -c *.c

La opción -c indica que el archivo sólo se compila no se enlaza.

Para enlazar:

gcc -ansi -pedantic -Wall -Wextra *.o main.c -o programa.out

Se escriben como argumentos del comando los archivos objeto generados previamente (o los .c si no se habían compilado).

Para crear una librería estática:

ar rcs libnombre.a *.o

El comando ar crea un archivo, donde se guardan archivos *.o. Simplemente una agrupación de los archivos objeto.

Para utilizar una librería:

gcc -ansi -pedantic -Wall -Wextra -L. -lnombre main.c -o programa.out

El enlace de librería se hace a través de dos opciones:

  • -L seguido por la(s) ruta(s) donde se deben buscar las librería, por ejemplo, la carpeta de trabajo .

  • -l (ele minúscula) seguido del nombre de la librería. Se puede omitir el prefijo lib y la extensión *.a

Librerías dinámicas

Una librería dinámica (o shared object) es una librería que se carga en tiempo de ejecución. Por contraste con una librería estática:

  • Un ejecutable que enlaza una librería estática extrae de la librería el código objeto necesario y lo incorpora en si mismo.

  • Un ejecutable que enlaza una librería dinámica sólo toma una referencia a la librería y a las funciones que se encuentren en ella. En el momento de arrancar tiene que cargar la librería y encontrar la referencia a las funciones que le hagan falta.

  • Esto último implica que se debe encontrar en la ruta donde se buscan librerías,

  • O debe añadirse a la variable de entorno LD_LIBRARY_PATH.

Arrays

Pendiente

Declaración de arrays muy grandes

Declarar variables locales en main es una buena práctica, no obstante, es posible declarar variables globales si su declaración se escribe fuera del main (y de cualquier otra función). La declaración de variables globales se considera una mala práctica porque se puede acceder (leer su valor o modificarlo) desde cualquier punto del código fuente.

Sin embargo, la declaración de variables globales puede estar justificada (o ser necesaria) en algunos casos. Uno de ellos es la declaración de variables tipo array de gran tamaño. Como las variables locales se colocan en una zona de memoria de tamaño limitado llamada stack declarar estas variables grandes puede ser un problema incluso para compilar el programa.

Por ejemplo:

#define MUCHO 100000

double arrayGrande[MUCHO][MUCHO]; /* Se admite porque es muy grande */

int main() {
    double arrayCuidado[MUCHO][MUCHO]; /* Puede dar problemas */
    return 0;
}

Arrays y punteros

  • Cualquier array es convertible en un puntero que apunta al primer elemento del array.

  • La aritmética de punteros equivale a moverse en un array. Es decir, se cumple que:

a[b] es exactamente lo mismo que *(a+(b)). (Literal en el estándar de C)

Siendo a un array y b una expresión cuyo resultado es un entero.

  • Alternativamente, sumar a + (b) tiene como resultado un puntero que cuya dirección de memoria está b veces desplazada respecto de a en unidades del tipo al que apunta el puntero.

Funciones y arrays

En C, los parámetros de tipo array como tal realmente NO existen. En su lugar, sea cuál sea la sintaxis utilizada, C va a usar un puntero para pasar el array.

Se podría decir que todos los arrays en C se pasan por referencia.

Es decir, las declaraciones de funciones:

void func1(int array[], int tam);
void func2(int array[20], int tam);
void func3(int *array, int tam);

Son equivalentes. Cuando se hace una llamada en la forma:

int mi_array[10];
funcX(mi_array, 10);

Da igual la forma de declara la funcion (func1, func2, o func3) el paso del parámetro siempre consiste en pasar la dirección de memoria del primer elemento de mi_array y, por eso, es necesario pasar un argumento adicional (el 10) para indicar el tamaño del array.

Arrays 2D y operador [ ].

Si ejecutamos el siguiente fragmento de código fuente:

int array[][2] = { 11, 22 }, { 33, 44 }, { 55, 66 };
printf("%d", array[1][1]);

obtenemos 44. Porque:

  • El primer corchete salta una fila y el segundo salta un elemento. El primer corchete nos lleva a la fila { 33, 44 } y el segundo al 44.

  • En los arrays 2D (y superiores) los elementos siguen estando contiguos en memoria, la manera en que el operador indexación funciona es saltando filas al aplicar el primer corchete y saltando elementos al aplicar el segundo corchete.

Punteros a arrays

Recordamos:

int array[] = { 11, 22, 33 };
printf("%d", *(array+1));

Muestra 22 porque:

  1. array se convierte en un puntero a int.

  2. la aritmetica de punteros desplaza en memoria el puntero tantos int como indique el otro sumando (en este caso 1).

Vamos a considerar el siguiente código fuente:

int array[][2] = { 11, 22 }, { 33, 44 }, { 55, 66 };
printf("%d", (array+1)[0]);
  1. El array se convierte en un puntero a una fila compuesta de dos número enteros.

  2. Al sumar 1 se desplaza una fila en memoria, es decir, se desplaza 2 enteros porque esa fila está formada por 2 enteros. Y el puntero apunta a la fila (al primer elemento de la fila, el 33).

  3. Al hacer [0] obtenemos 33, el primer elemento de esa fila.

Arrays de punteros y punteros a arrays

Consideremos estas dos declaraciones:

int* array_p[5]; /* 1 */
int (*p_array)[5]; /* 2 */

La declaración (1) es 1 array de 5 punteros a enteros. Los elementos del array son punteros NO enteros.

La declaración (2) es 1 puntero a un array de 5 enteros. NO hay ningún array, solo hay un puntero y lo importante de ese puntero es que, al sumar +1 al puntero, se saltan 5 enteros en memoria. Este es el tipo al que se transforman las arrays 2D en C.

Cuando el array de la declaración (1) se convierte implicítamente en un puntero se transforma en int**. Veamos:

int array_int[5]; /* array_int -> int* */
int* array_punteros[5]; /* array_punteros -> int** */

El array array_int se convierte en la dirección de memoria al primer elemento como los elementos son int, su dirección es int*.

El array array_punteros se convierte en la dirección de memoria al primer elemento como los elementos son int*, su dirección es int**.

Funciones y arrays (2D o más)

Un array estático bidimensional se convierte en un puntero a una fila, por tanto, para utilizarlo como parámetro de una función existen las siguientes opciones:

Declaración como array (la más sencilla):

void una_funcion(int array[][5], int n_filas);

Importante: NO se puede quitar el 5 (o el número que corresponda) porque el compilador debe saber cuanto ocupa una fila, su tamaño.

Declaración como puntero (la real):

void una_funcion(int (*array)[5], int n_filas);

Es decir, el parámetro es el puntero a la primera fila. Igual que en cualquier otro array, es el puntero al primer elemento del array.

Memoria dinámica (arrays 1D)

Frente a la memoria estática, aquella cuyo tamaño se conoce en tiempo de compilación, la memoria dinámica se gestiona durante la ejecución del programa mediante dos operaciones:

  1. Reserva de memoria: el programa indica al sistema operativo que va a utilizar una cantidad de bytes para su uso.

  2. Liberación de memoria: el programa indica al SO que ya no va a utilizar los bytes que había reservado.

Ambas operaciones se realizan mediante funciones de la librería estándar de C y la memoria usada se gestiona mediante punteros.

Como parámetros y argumentos de estas operaciones se utiliza un tipo de puntero especial llamado genérico, es un tipo de puntero del que se desconce a qué apunta. Representa una dirección de memoria pura, se conoce la dirección, pero no se supone nada sobre el contenido de esa dirección. Se declaran como void*.

Para reservar memoria se puede hacer:

void* dyn_m = malloc(5*sizeof(int));
void* dyn_c = calloc(5, sizeof(int));

En ambos casos se reserva una zona de memoria para su uso posterior. Con malloc se indica el número de bytes. Con calloc se supone que se reserva una zona de memoria para colocar un número de elementos de tamaño dado, en el ejemplo 5 elementos de tamaño int.

Normalmente el puntero void* se convierte en un puntero que apunte a lo que deseamos guardar en esa zona de memoria. Por ejemplo:

int* dyn_m = (int*)malloc(5*sizeof(int));
int* dyn_c = (int*)calloc(5, sizeof(int));

Donde la operación (int) también llamada cast se puede omitir (o da un aviso) según la versión del compilador. Ambos punteros se puede manejar como equivalentes a un array de 5 enteros. Se dice que sonarrays dinámicos*.

Para liberar memoria se hace:

free(dyn_m);
free(dyn_c);

Las funciones malloc, calloc y free están declaradas en el archivo de cabeceras: stdlib.h.

Memoria dinámica (arrays 2D)

Equivalente a un array 2D

El equivalente a una array 2D estática es una variable dinámica donde se impone que tenga el número de filas y columnas correspondientes.

Si queremos el equivalente a int array2d[7][5], hacemos (en 2 pasos):

void *p = malloc(35*sizeof(int));
/* También: void *p = calloc(7, sizeof(int[5])); */
int (*array)[5] = p;
array[2][3] = 33;
/* [...] */
free(array);

Alternativa a un array 2D como un vector de punteros

Alternativamente se puede generar una matriz dinámica como un vector de punteros. Este alternativa tiene como ventaja que no es necesario fijar el tamaño de la fila. Y tiene como inconveniente que exige un mayor número de variables dinámicas: un vector de punteros y una variable dinámica por cada fila.

Para la misma matriz, int array2d[7][5], se hace:

int i;
int **array = calloc(7, sizeof(int*));  /* Vector de 7 punteros */
for (i = 0; i < 7; ++i ) {
  array[i] = calloc(5, sizeof(int)); /* Vector de 5 int */
}

array[2][3] = 33; /* array[2] es el puntero a la segunda fila */

for (i = 0; i < 7; ++i ) {
  free(array[i]); /* Se Libera las filas */
}
free(array); /* Se libera el vector de punteros */

Funciones para manipulación de datos en binario

En esta sección se explican las funciones: memcpy, fread y fwrite.

Todas ellas están relacionadas porque operan sobre datos binarios sin interpretar de ninguna manera su contenido.

  • memcpy copia datos de una zona de memoria a otra.

  • fread copia datos de un archivo a una zona de memoria.

  • fwrite copia datos de una zona de memoria a un archivo.

Cambia el origen y el destino de la información, pero el fondo es el mismo.

Una zona de memoria se corresponde con un objeto (en sentido C), es decir, una variable estática o dinámica. La zona de memoria se define mediante la dirección de memoria del primer byte que ocupa (un puntero void*) y su tamaño en bytes.

Las siguientes expresiones son formas válidas de definir zonas de memoria, se indica con un comentario la expresión para el puntero y la expresión para el tamaño, y, previamente se hacen las declaraciones necesarias:

int numero;
&numero; /* Puntero */
sizeof(numero); /* tamaño */

double vector[10];
vector; /* Puntero */
sizeof(vector); /* tamaño, también: 10*sizeof(double) */

short *v_dyn = calloc(10, sizeof(short));
v_dyn; /* Puntero */
10*sizeof(short) /* tamaño */

typedef struct { int num; char bytes[4]; } mi_struct;
mi_struct datos;
&datos; /* Puntero */
sizeof(mi_struct) /* tamaño, también: sizeof(datos) */

Estas zonas de memoria que se manipulan sin conocer qué tipo de información tienen suelen recibir el nombre de bufferes.

Es común usar este nombre para un array de bytes cuyo uso no va a ser una cadena alfanumérica sino, simplemente, una zona de memoria para usar como medio de trasvase de información. Por ejemplo:

char buffer[128];
buffer; /* Puntero */
128*sizeof(char) /* tamaño, también 128 porque sizeof(char) es 1 */

Uso de las funciones fread y fwrite

Para usar las funciones fread y fwrite necesitamos un archivo ya abierto. Al usar fread y fwrite se suele especificar que la lectura o escritura es en binario. Es decir el archivo se abre con:

FILE *f_leer = fopen(filename, "rb"); /* Para leer, fread */
FILE *f_escribir = fopen(filename, "wb"); /* Para escribir, fwrite */

Y se cierra como se hace usualmente.

Las declaraciones son:

::

size_t fread(void buffer, size_t size, size_t count, FILE *stream); size_t fwrite(const void buffer, size_t size, size_t count, FILE* stream);

Como se puede ver en las declaraciones está previsto leer o escribir arrays de un tipo base cuyo tamaño viene dado por el parámentro size. Cuando se quiera leer una única variable y no un array simplemente se pasa un valor 1 a count.

Para leer desde archivo necesitamos la zona de memoria en donde se va a guardar la información de archivo. Respectivamente para escribir, necesitamos la zona de memoria cuya información se guarda en archivo.

Si usamos las expresiones anteriores y suponemos que tenemos los archivos abiertos como se ha hecho anteriormente, queda de la siguiente manera:

int numero;
fread(&numero, sizeof(int), 1, f_leer);
fwrite(&numero, sizeof(int), 1, f_escribir);

double vector[10];
fread(vector, sizeof(double), 10, f_leer);
fwrite(vector, sizeof(double), 10, f_escribir);

short *v_dyn = calloc(10, sizeof(short));
fread(v_dyn, sizeof(short), 10, f_leer);
fwrite(v_dyn, sizeof(short), 10, f_escribir);

typedef struct { int num; char bytes[4]; } mi_struct;
mi_struct datos;
fread(&datos, sizeof(mi_struct), 1, f_leer);
fwrite(&datos, sizeof(mi_struct), 11, f_escribir);

Uso de la función memcpy

Para usar la función memcpy necesitados dos zonas de memoria, una origen y otra destino. Los datos se van a copiar del origen al destino. Las dos zonas de memoria deben de tener el mismo tamaño o, al menos, la de destino tiene que tener un tamaño mayor.

La declaración es:

void *memcpy(void *dest, const void *src, size_t n);

Como dest y src se pueden usar expresiones como las indicadas anteriormente. Por ejemplo:

typedef struct { char bytes[4]; int num; } mi_struct;
mi_struct datos;
char *buffer;
datos.num = 258;
datos.bytes[0] = 65;
/* [...] Se asigna valor al resto de los campos de la estructura datos */
buffer = malloc(sizeof(datos)); /* Para que tenga el mismo tamaño */
memcpy(buffer, &datos, sizeof(datos)); /* Copia los datos de la estructura en el buffer */

Después de memcpy el valor de buffer[0] debe ser 65, buffer[4] y buffer[5] debe ser 2 y 1 respectivamente porque 258 es 1*2^8+2 y la representación está en little endianess el número se escribe empezando en las potencias menos significativas.

:: .. warning:

Si el tamaño de int no es 4, puede ocurrir que C introduzca `padding` en la estructura y entonces habría un hueco de 4 bytes adicionales entre el campo ``bytes`` y el campo ``num``.
Esto se debe a que C `alinea` los datos en las estructuras para que se coloquen en posiciones de memoria que sean múltiplos de potencias de 2 muy concretas.

struct (TBD)

TODO: Pendiente de escribir