Breviario de Lenguaje C
-
Arrays 2D y operador ``[ ]`. <#arrays-2d-y-operador–>`__
`Funciones para manipulación de datos en binario<#funciones-sobre-buffers>`__
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 -324Números escritos con punto decimal son constantes literales en coma flotante (double). Ejemplos:
3.14 -1.414Idem para números en notación científica, es indiferente escribir e ó E. Ejemplos:
1e-5 3.14E5 1.1e-4 -1.414e+3Los 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.14fes de tipo float,34Ues unsigned int,12Les long int.Se pueden escribir constantes enteras en octal o hexadecimal prefijando el número con 0 ó 0x respectivamente. Ejemplos:
010es 8,0x10es 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 < 10es 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 << 3es 8,12 | 11es 15,12 & 11es 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 + 1El 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:
Declaraciones
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:
Una expresión terminada con un punto y coma.
Se incluyen como expresiones las llamadas a función (el paréntesis de llamada a una función se considera un operador).
Una sentencia compuesta delimitada por una apertura y un cierre de llaves (sin punto y coma final).
Una sentencia condicional (if, switch).
Una sentencia repetitiva o bucle (for, while, do … while).
Sentencias if y switch
Sintaxis if
if (cond) sentencia1
[ else sentencia2 ]
Cuando se anidan el
elsese asocia alifmá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
breakhasta la siguiente sentencia.Las sentencias
breakson 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:
%.3fson 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:
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.
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.
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:
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%3dsepararía el texto123456en 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.Modificadores de longitud. Por ejemplo:
%Ldsirve para guardar el valor leído en una variable de tipolong.
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,
pque 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 = # /* 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:
De salida. Es decir, aquellos donde se fija el valor de la variable con un resultado calculado internamente en la función.
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:
Se debe encontrar la declaración de todas las variables y funciones usadas
Pero NO hace falta que estén sus definiciones
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:
Que se encuentre una definición y sólo una para cada variable y función declaradas.
Que se encuentre una función y sólo una con el nombre
mainSi 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:
-Lseguido 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 prefijoliby 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ábveces desplazada respecto deaen 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:
arrayse convierte en un puntero aint.la aritmetica de punteros desplaza en memoria el puntero tantos
intcomo 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]);
El
arrayse convierte en un puntero a una fila compuesta de dos número enteros.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).
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:
Reserva de memoria: el programa indica al sistema operativo que va a utilizar una cantidad de bytes para su uso.
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.
memcpycopia datos de una zona de memoria a otra.freadcopia datos de un archivo a una zona de memoria.fwritecopia 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;
№ /* 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