martes, 27 de mayo de 2014

Apuntadores

El correcto entendimiento y uso de los punteros es critico para una fructífera programación en C. Hay tres razones fundamentales para decir esto. La primera es que los apuntadores proporcionan los medios por los cuales las funciones pueden modificar sus argumentos de llamada. Segundo, se utilizan para soportar  las rutinas de asignación dinámica de C. Tercero, el uso de apuntadores puede mejorar la eficacia de la mayoría de las funciones, instrucciones y rutinas.  
Ademas de ser una de las características mas poderosas, los punteros son también la característica mas peligrosa y poderosa de C. Por ejemplo, los no inicializados o apuntadores descontrolados, pueden provocar fallas en el sistema 

Definición de apuntadores

Un apuntador es una variable que contiene una dirección de memoria. Normalmente, esa dirección es la posición de otra variable de memoria. Si una variable contiene la dirección de otra variable, entonces se dice que la primer variable "apunta" a la segunda. 

Variables tipo apuntador 

Si una variable va a contener un puntero, entonces tiene que declararse como tal. Una declaración de apuntador consiste en un tipo base, un * (asterisco) y el nombre de la variable. La forma general para declarar una variable apuntador es: 

tipo *nombre ; 

donde tipo es cualquier tipo valido en C y nombre es el nombre de la variable apuntador. 
El tipo base del apuntador define el tipo de variables a las que puede apuntar. Técnicamente, cualquier tipo de apuntador puede apuntar a cualquier lugar de la memoria. Sin embargo, toda la aritmética de apuntadores esta hecha en relación a su tipo base, por lo que es importante declarar correctamente el apuntador. 

Los operadores de apuntadores 

Existen 2 operadores esenciales de apuntadores: & y *. El & es un operador monario que devuelve la dirección de memoria de su operando(hay que recordar que un operador monario solo necesita un operando). Por ejemplo 

m = &cuenta

pone en m la dirección de la variable cuenta. Esta dirección es la posición interna de la variable en la computadora. La dirección no tiene ninguna relación con el valor cuenta. Se puede pensar en el operador & como devolviendo "la dirección de". Por tanto, la declaración de asignación anterior significaría "m recibe la dirección de cuenta". 
Para entender mejor la asignación anterior, suponga que la variable cuenta utiliza la posición de memoria 2000 para guardar su valor. Entonces, despumes de la asignación anterior, m tiene el valor de 2000. 

El segundo operador de punteros es el asterisco *, que es el complemento de &. Es también un operador monario que devuelve el valor de la variable localizada en la dirección que sigue. Por ejemplo, si m contiene la dirección de memoria de la variable cuenta, entonces 

q = *m ; 

pone el valor de cuenta en la variable q. Esta variable tendrá el valor de 100 porque ese es el valor que esta almacenado en la posición 2000, que es la dirección de memoria que se guardo en m. Se puede pensar en * como "en la dirección". En este caso, la sentencia anterior significa "q recibe el valor de la dirección m". 
A veces, el hecho de que el operador * también signifique la multiplicación y  & represente la operación Y a nivel de bits confunde a los que van empezando en la programación pero es importante resaltar que estos no tienen relación con los antes mencionados. Tanto & como * tienen mayor prioridad que todos los operadores aritméticos.
Las variables de tipo apuntador deben apuntar siempre al tipo de datos correcto. Por ejemplo, cuando se declara un apuntador de tipo int , el compilador asume que cualquier dirección que mantenga apunta a una variable entera. Debido a que C permite asignar cualquier dirección a una variable apuntado, el siguiente fragmento de código compila sin mensajes de error, pero no produce el resultado deseado.  

void main (void)
{
     float x,y; 
     int *p; 

     p = &x ; 
     y = *p;   
}

Esto no asignara el valor de x a y. Debido a que p se declara como un puntero a entero, solo se transfieren 2 bytes de información a y, y no los bytes que normalmente forman un numero en coma flotante.  


Inicializacion de apuntadores


Después de declarar un puntero pero antes de asignarle cualquier valor, este ya contiene otro valor desconocido. Si se intenta utilizar el apuntador antes de darle un valor, probablemente el programa fallara y no solo el programa, también podría fallar el sistema operativo. 
Para evitar este tipo d errores,se recomienda siempre asignarle un valor al apuntador que se utilizara durante el programa. 


Apuntadores y Funciones 

Una característica algo confusa pero muy útil en C son los apuntadores a funciones. Incluso aunque una función no es una variable, tiene una posición física en memoria que se le puede asignar a un apuntador. La dirección de la función es el punto de entrada de dicha función, por lo que se puede usar un apuntador para llamar a la función. 
Para poder entender como funcionan los apuntadores a funciones, se tiene que entender un poco como se compila y se llama a una función en C. Primeramente, cuando se compila cada función, el código fuente se transforma en un código objeto y se establece un punto de entrada. Cuando se llama a la función mientras se ejecuta el programa, se hace una llamada en lenguaje maquina a ese punto de entrada. Por tanto, si un apuntador contiene la dirección del punto de entrada a la función, puede utilizarse para llamar a la función. 
La dirección de la función se obtiene utilizando el nombre de la función sin paréntesis ni argumentos, esto es parecido a la forma de obtener la dirección de un arreglo cuando se utiliza solo en nombre de este, sin indices. 

Cuando C pasa argumentos a funciones, los pasa por valor, es decir, si el parámetro es modificado dentro de la función, una vez que termina la función el valor pasado de la variable permanece inalterado. 
Hay muchos casos que se quiere alterar el argumento pasado a la función y recibir el nuevo valor una vez que la función ha terminado. Para hacer lo anterior se debe usar una llamada por referencia, en C se puede simular pasando un puntero al argumento. Con esto se provoca que la computadora pase la dirección del argumento a la función. 


Apuntadores y Arreglos 

Los apuntadores pueden estructurarse en arreglos como cualquier otro tipo de dato. La declaración para un arreglo de apuntadores a enteros (int) de tamaño 10 es: 

int *x[10]; 

Para asignar la dirección de una variable entera llamada var al tercer elemento del arreglo de apuntadores, se escribe 

x[2] = &var ; 

Para encontrar el valor de var, se escribe 

*x[2] 

Si se quiere pasar un arreglo de apuntadores a una función, se puede utilizar el mismo método que se utiliza para otros arreglos: llamar simplemente a la función con el nombre del arreglo sin indices. Por ejemplo, una función que reciba el arreglo x  seria como esta: 

void mostrar_arreglos (int *q[])
{
     int t ; 
     for (t=0 ; t<10 ; t++)
      printf ("%d", *q[t]);
}

Hay que recordar que la variable q no es un apuntador a enteros, sino un apuntador a un arreglo de apuntadores a enteros.


Un nombre de un arreglo es un índice a la dirección de comienzo del arreglo. Es decir, el nombre de un arreglo es un puntero al arreglo. 
Sin embargo los apuntadores y los arreglos son diferentes: 


  • Un apuntador es una variable. 

               Se puede hacer ap = a y ap++. 


  • Un arreglo NO ES una variable. 


                Hacer a = ap y a++ ES ILEGAL. 



Apuntadores y Arreglos Multidimensionales 


Cuando se pasa una arreglo bidimensional a una función se debe especificar el número de columnas, el número de renglones es irrelevante. 
La razón de lo anterior, es nuevamente los apuntadores. C requiere conocer cuántas son las columnas para que pueda brincar de renglón en renglón en la memoria. 

Considerando que una función deba recibir int a[5][35], se puede declarar el argumento de la función como: 

f( int a[][35] ) { ..... } 
f( int (*a)[35] ) { ..... } 

En el último ejemplo se requieren los paréntesis (*a) ya que [ ] tiene una precedencia más alta que *.


int (*a)[35]; 

declara un apuntador a un arreglo de 35 enteros 

Si hacemos a+2, nos estaremos refiriendo a la dirección del primer elemento que se encuentran en el tercer renglón de la matriz supuesta, 

int *a[35] 

declara un arreglo de 35 apuntadores a enteros. 


Apuntadores y Cadenas 

char *nomb[10];
char anomb[10][20]; 
En donde es válido hacer nomb[3][4] y anomb[3][4]. 
anomb es un arreglo verdadero de 200 elementos de dos dimensiones tipo char. 
El acceso de los elementos anomb en memoria se hace bajo la 
siguiente fórmula: 

20*renglon + columna + dirección_base 

En cambio nomb tiene 10 apuntadores a elementos. 

NOTA: si cada apuntador en nomb indica un arreglo de 20 elementos entonces y solamente entonces 200 chars estarán disponibles (10 elementos). 


CHAR *NOMB[10];

Tiene la ventaja de que cada apuntador puede apuntar a arreglos de diferente longitud. 



Apuntadores y Estructuras 


Los apuntadores a estructuras se definen fácilmente y en una forma directa. 
Un ejemplo de código de una relación de apuntadores y estructuras es la siguiente: 


main() 

 struct COORD { float x,y,z; } punto; 
 struct COORD *ap_punto; 

 punto.x = punto.y = punto.z = 1; 
 ap_punto = &punto; /* Se asigna punto al 
apuntador */ 
 ap_punto->x++; /*Con el operador -> se accesan los miembros*/ 
 ap_punto->y+=2; /* de la estructura apuntados por ap_punto */ 
 ap_punto->z=3; 




Problemas con apuntadores 

Nada puede dar mas problemas que un apuntador descontrolado. Los punteros son un arma de doble filo. Dan una potencia tremenda y son necesarios para muchos programas. Al mismo tiempo, cuando un apuntador accidentalmente contiene un valor erróneo, puede ser la falla mas difícil de detectar. 
Un error de un apuntador erróneo es difícil de encontrar porque el problema no es el puntero en si mismo. El problema es que cada vez que se realiza una operación utilizando un apuntador, se esta leyendo o escribiendo en algún lugar desconocido de la memoria. Si se lee de el, lo peor que puede ocurrir es que se obtenga basura. Sin embargo, si se escribe en el, puede que se este escribiendo en otras partes de códigos o datos. Esto puede no evidenciarse hasta mas tarde en la ejecución del programa y puede llevar a buscar el error en un lugar incorrecto. Es posible que no haya nada que nos indique que el problema es el puntero

El error mas común cuando se trabaja con apuntadores es el puntero no inicializado . Aquí hay un ejemplo de un puntero no inicializado: 

void mains (void)
{
       int x, *p; 
       x = 10; 
        *p = x; 
}
Este programa asigna el valor de 10 a alguna posición de la memoria desconocida. Como el apuntador p nunca ha recibido un valor, contiene basura. Este tipo de problema a menudo pasa inadvertido. La solución para este tipo de problemas es asegurarse siempre que el apuntador este apuntando a algo valido antes de utilizarlo. 
El segundo erro mas común se produce por un desconocimiento del uso de apuntadores. Por ejemplo: 

void main (void)
{
    int x, *p; 

    x = 10 ;
    p = x ;
    printf ("%d", *p);
}

La llamada a printf () no imprime el valor x en la pantalla, que es 10. Imprime algún valor desconocido porque la asignación

p = x ;  

esta mal. La sentencia asigna el valor 10 al puntero p, que se supone contendría una dirección y no un valor. La corrección a este problema es escribir lo siguiente: 

p = &x ; 

Recursividad

La recursividad es un concepto fundamental en matemáticas y en computación. Es una alternativa diferente para implementar estructuras de repetición (ciclos). Los módulos se hacen llamadas recursivas. 
 Se puede usar en toda situación en la cual la solución pueda ser expresada como una secuencia de movimientos, pasos o transformaciones gobernadas por un conjunto de reglas no ambiguas. 
La recursividad es el proceso de definir algo en términos de sí mismo, en otras palabras, que las funciones pueden llamarse a sí mismas. Cuando en el cuerpo de la función hay una llamada a la propia función, entonces se dice que es recursiva. Una función recursiva no hace una nueva copia de la función, sólo proporciona nuevos parámetros. 


Función Recursiva


Las funciones recursivas se componen de: 

  • Caso base: una solución simple para un caso particular (puede haber más de un caso base). La secuenciación, iteración condicional y selección son estructuras válidas de control que pueden ser consideradas como enunciados. 

  • Caso recursivo: una solución que involucra volver a utilizar la función original, con parámetros que se acercan más al caso base. Los pasos que sigue el caso recursivo son los siguientes: 
  1. El procedimiento se llama a sí mismo 
  2. El problema se resuelve, resolviendo el mismo problema pero de tamaño menor 
  3. La manera en la cual el tamaño del problema disminuye asegura que el caso base eventualmente se alcanzará 

Ventajas 

  • Se pueden generar soluciones en un código más pequeño. 
  • Se le puede dar solución a problemas complejos. 
  • Algunos problemas complejos que no se pueden resolver fácilmente con iteraciones se pueden resolver con recursividad. 
  • No es necesario definir una secuencia de pasos exacta para dar solución a un problema. 

Desventajas 

  • Se necesitan muchas variables para darle solución al problema 
  • La memoria se puede llegar a saturar debido a la gran cantidad de información que se almacena 
  • Una simple llamada puede generar un gran número de estas. 
  • Es menos eficiente que la solución iterativa, ya que lleva llamadas suplementarias a las funciones. 

ESTRUCTURAS (Struct) & Archivos

Un registro o estructura es un tipo de dato estructurado y definido por el usuario que permite almacenar datos de diferente tipo en una sola variable; dichos datos pueden ser simples (caracteres, números enteros o de coma flotante etc) o compuestos  (vectores, estructuras, listas, etc). A los datos del registro se les denomina campos, elementos o miembros. Una estructura esta formada por variables que tienen relación entre si. También se llaman registros a los grupos de datos en la terminología de las bases de datos. 

Conceptos Básicos

  • Campo. Permite representar un atributo de alguna entidad (Edad, Nombre, Semestre, Teléfono, Carrera) 
  • Registro. Es un conjunto de campos que forman los atributos de cierta entidad (Alumno) 
  • Archivo. Es un conjunto de registros almacenados en un dispositivo externo  


Definición de una estructura 

Una estructura define una plantilla con la que posteriormente se puede declarar una variable. Una de las características de las estructuras es que hay que definirías antes de usarlas  en la declaración de las variables. En la definición no se declara ni reservando memoria para ninguna variable, solo se construye una estructura con determinadas características, para después poder declarar una variable de ese tipo.  
Para crear una estructura, primero comenzamos por definir el tipo de estructura, para ello se procede de manera parecida a la definición de una variable, con algunas modificaciones; en lenguaje C para definir una estructura, se utiliza la palabra reservada struct, normalmente seguida por un nombre y la apertura de las llaves; después se define el tipo y nombre para cada uno de los campos. Se permite cualquier definición de tipo habitual, incluso punteros y otras estructuras. Cada definición de campo acabara con un punto y coma. Por ultimo se cierran las llaves. Si hay campos del mismo tipo, se pueden declarar en la misma linea, separados por comas. 
A continuación se muestra un ejemplo de definición de una estructura: 

struct  identificador_estructura 
          tipo_dato_1  campo_1, campo_2 
          tipo_dato_2  campo_3, campo_4 
          tipo_dato_3  campo_5, campo_6 
}

Se ha definido el tipo de dato, el nombre de la estructura, pero aun no hay ninguna  variable declarada con este nuevo tipo. En lenguaje C es necesario incluir un carácter (;) después de cerrar las llaves. Para definir una variable de tipo registro se declara de la misma forma que una variable: 

identificador_estructura identificador_variable; 

struct  info
          char campo_1, campo_2 
          int    campo_3, campo_4 
          float campo_5, campo_6 
}

main ( )
{
       info estudiante; 
}

donde: 
  • info es el nombre del registro 
  • estudiante es el nombre de la variable
También es posible definir variables estructuras sin tipo especifico, para ello basta con omitir el identificador del tipo estructura en la definición de la estructura, dando solo el identificador de la variable estructura. De este modo la nueva variable va asociada al tipo creado. En ese caso, quizá no sea necesario dar un nombre a la estructura, con la siguiente sintaxis: 

struct  identificador_estructura 
          tipo_dato_1  campo_1, campo_2 
          tipo_dato_2  campo_3, campo_4 
          tipo_dato_3  campo_5, campo_6 
} nombre_variable; 

Quedando así definida la variable nombre_variable, de tipo de dato struct. En este caso, no se pueden crear nuevas variables de ese tipo, por lo que no se recomienda usar este tipo de declaraciones. 


Tipos de datos definidos por el usuario "typedef"

En el lenguaje C podemos dar un nuevo nombre a tipos de datos  que ya existen, a  fin de que estos sean mas afines con aquello que representan y haciendo que el código fuente sea mas claro. Una vez que se ha creado un tipo de dato y un nombre para hacer referencia a el, podemos usar ese identificador en la declaración de variables, como cualquier tipo de dato estándar en C. Para definir un tipo de dato se utiliza la palabra reservada define_tipo "typedef" con el siguiente formato:  

typedef       ident_tipo     ident_tipo_nuevo; 

Ejemplos: 
  • typedef   float kg 
  • typedef   float mts 
La palabra reservada typedef nos sirve para crear "sinónimos" de tipos de datos predefinidos. Una manera fácil de definir estructuras en Lenguaje C es mediante la combinación de la palabra struct y la palabra typedef . Una forma de definición de una estructura es la siguiente: 

 typedef struct 
{
            tipo_dato_1  campo_1, campo_2 
          tipo_dato_2  campo_3, campo_4 
          tipo_dato_3  campo_5, campo_6 
} nombre_variable


Asignación de valores a los campos de una estructura

Una vez declarada una estructura, se pueden asignar valores iniciales a sus campos. Para ello se dan los valores iniciales escribiendo entre llaves los valores de sus campos en el mismo orden en que se declararon estos, al igual que hacemos con los arreglos. Pero ahora cada dato puede tener un tipo diferente. 
La sintaxis es la siguiente: 

struct fecha 
{
       int dia; 
       char mes [10];
       int año;  
} ; 

struct fecha fec_ant = { 15, "Abril", 2008 }; 

Acceso a los campos de un registro o estructura (struct)

Para acceder a cada campo de una estructura se usa el operador  punto ".", precedido por el nombre de la estructura y seguido del nombre del campo. Dicho operador, que sirve para representar la dependencia de cada uno de los campos con su estructura, es necesario, ya que al declararse varias estructuras del mismo tipo, debemos distinguir los correspondientes campos de unas y otras. Cada campo de una estructura designado mediante este operador se comporta como si se tratase de una variable del mismo tipo que el campo. Podemos realizar todas las operaciones habituales de las variables.  

identificador_variable.nombre_ campo

El tratamiento de cada uno de estos campos depende de su tipo (si es un entero lo asignaremos y trataremos como entero, si un vector, como un vector, si es string como string)
Ejemplo: 

struct persona
{
     long edad; 
     char nombre [40];
      char  sexo ; 
}
struct Persona usuario;  


struct fecha 
{
     int dia; 
     char mes [10];
      int año ; 
} fec_act = { 10, "Mayo", 1990} ; 

strcpy (usuario.nomb, "Hugo Jara"); 
usuario.edad = 20 ; 
usuario.sexo = M ; 


Archivos 

En el Lenguaje de programación C, un archivo puede ser cualquier cosa, desde un archivo de disco a una terminal o una impresora. Se asocia una secuencia con un archivo especifico realizando una operación de apertura. Una vez que el archivo esta abierto, la información puede ser intercambiada entre este y el programa. 
No todos los archivos tienen ñas misma posibilidades. Por ejemplo, un archivo de disco permite el acceso directo mientras que un teclado no. Esto saca a la luz un punto importante sobre el sistema E/S de C: todas las secuencias son iguales, pero no todos los archivos lo son. 
Si el archivo permite el acceso directo, abrir ese archivo también inicia el indicador de posición del archivo al comienzo del mismo. Según se leen o se escriben los caracteres en el archivo, el indicador de posición se incrementa asegurando la progresión sobre el mismo. 
Cada secuencia que se asocia con un archivo tiene una estructura de control del tipo FILE. Esta estructura se define en el archivo de cabecera STDIO.H (directivas al pre-procesador)


Elementos básicos del sistema de archivos 

El sistema de archivos ANSI esta compuesto por varias funciones interrelacionadas. Las mas comunes se encuentran en la siguiente tabla. 


              Nombre                                          Función 
           fopen ( )                                             Abre un  archivo 
           fclose ( )                                             Cierra un archivo 
           putc    ( )                                             Escribe un carácter en un archivo 
           fputc   ( )                                             Escribe un carácter en un archivo
           getc    ( )                                             Lee un carácter de un archivo 
           fprintf ( )                                             Tiene la misma función que printf ( ) en sintaxis                                                                             normal
           fscanf ( )                                             Misma función que scanf ( ) en sintaxis normal  
           ferror ( )                                             Devuelve el valor de cierto si se produce un error 
           rewind ( )                                           Coloca el localizador de posición del archivo al inicio                                                                           de este
           fflush ( )                                              Vacia un archivo
     

El puntero de un archivo 

El puntero de un archivo es el hilo común que unifica el sistema entrada/salida con buffer. Un puntero a un archivo es un puntero a una información que define varias cosas sobre el, incluyendo el nombre, el estado y la posición actual del archivo. En esencia, el puntero a un archivo identifica un archivo en disco especifico y utiliza la secuencia asociada para dirigir el funcionamiento de las funciones entrada/salida con el buffer. Es una variable de tipo puntero al tipo FILE. Un programa necesita utilizar punteros a archivos para leer o escribir sobre los mismos. La sintaxis para obtener una variable de este tipo es la siguiente: 

FILE *fp ; 

Apertura de un archivo 

La función fopen ( ) abre una secuencia para que pueda ser utilizada y la asocia a un archivo. Después, devuelve el puntero al archivo asociado con ese archivo. A menudo, el archivo es un archivo en disco. La función fopen ( ) tiene la siguiente sintaxis: 
    FILE *fopen (const char nombre_del_archivo  modo ) ; 
donde nombre_del_archivo es un puntero a una cadena de caracteres que representan un nombre valido del archivo y puede incluir una especificación de directorio. modo, determina como debe ser abierto el archivo. 
Como se muestra en la siguiente tabla, un archivo puede abrirse en modo texto o en modo binario.  

Modo                                Significado  

r                                        Abre un archivo de texto para lectura 
w                                       Crea un archivo de texto para escritura
a                                        Abre un archivo de texto para añadir 
rb                                      Abre un archivo binario para lectura 
wb                                    Crea un archivo binario para escritura
ab                                     Abre un archivo binario para añadir 
r+                                     Abre un archivo de texto para lectura/escritura 
w+                                    Crea un archivo de texto para lectura/escritura 
a+                                     Añade o crea un archivo de texto para lectura/escritura 
r+b                                   Abre un archivo binario para lectura/escritura
w+b                                 Crea un archivo binario para lectura/escritura
a+b                                  Añadir en un archivo binario en modo lectura/escritura

Si se quiere abrir un archivo llamado PRUEBA para escritura, puede escribir : 

 FILE *fp; 
 if  ( ( fp = fopen ("prueba", "w") ) == NULL) 
{
      printf ("No se puede abrir el archivo"); 

La instrucción NULL se define como \0. Si se abre un archivo utilizando este método, 
fopen( ) detecta cualquier erro al abrir el archivo, como puede ocurrir cuando el disco esta protegido contra escritura. Se utiliza el valor nulo para indicar un fallo porque ningún puntero a archivo puede tomar este valor. 
Si se utiliza fopen ( ) para abrir un archivo para escritura, cualquier archivo que ya existiese con ese nombre se borra y se crea uno nuevo y si no existe, se crea uno nuevo. Si se quiere añadir información al final del archivo, se debe utilizar el modo "a". Solo se pueden abrir archivos que ya existen para lectura, si no es así, se muestra un error. Finalmente, si se abre un archivo para realizar operaciones de lectura/escritura, si ya existía no sera borrado. 
El estándar ANSI especifica que pueden ser abiertos, por lo menos, ocho archivos en cualquier momento. Sin embargo, la mayoría de los compiladores y C permiten un numero mayor. 


Cierre de un archivo

La función fclose( ) cierra una secuencia que fue abierta mediante una llamada a fopen ( ). Escribe toda la información que todavía se encuentre en el buffer del disco y realiza un cierre formal del archivo a nivel del sistema operativo. Un error en el cierre de una secuencia puede generar todo tipo de problemas, incluyendo la perdida de datos, destrucción de archivos  y posibles errores intermitentes en el programa. Un fclose ( ) también libera el bloque de control de archivo asociado con la secuencia, dejándolo libre para su reutilizacion . En la mayoría de los casos, existe un limite del sistema operativo al numero de archivos abiertos que pueden haber simultáneamente, por lo que es necesario cerrar los archivos que ya no se estén utilizando. 

La función fclose ( )    tiene este prototipo 

int fclose (FILE *pf)          

donde pf es el puntero al archivo devuelto por la llamada fopen ( ). Si se devuelve un valor cero significa que la operación de cierre ha tenido éxito. Se puede utilizar la función estándar ferror(  ) para determinar y notificar cualquier problema. 
Generalmente, fclose ( ) solo falla cuando un disco se ha retirado antes de tiempo del dispositivo o cuando no queda espacio libre en el disco.




Escribir un registro en un archivo binario


Un registro se escribe en un archivo binario con la función fwrite ( ). 

fwrite(&buffer, bytes_buffer, n, arch); 
donde: 
  • &buffer es una estructura, 
  • bytes_buffer es el tamaño en bytes de la estructura, 
  • n es el número de estructuras a escribir en el archivo 
  • arch es el nombre lógico del archivo. 

Leer un registro de un archivo binario

Un registro se lee de un archivo binario con la función fread ( ).  

fread(&buffer, bytes_buffer, n, arch); 
donde: 
  • &buffer es una estructura, 
  • bytes_buffer es el tamaño en bytes de la estructura, 
  • n es el número de estructuras a escribir en el archivo 
  •  arch es el nombre lógico del archivo.

Tamaño de una estructura


La función sizeof(), regresa el número de bytes que ocupa una  estructura. 
int x; 
x= sizeof(tipo_estructura)

Matrices (Arreglos Bidimensionales)

Un arreglo bidimensional es un conjunto de n elementos del mismo tipo almacenados en memoria contigua en una matriz o tabla. A diferencia de los arreglos unidimensionales que solo requieren de un sub-indice, los arreglos bidimensionales para acceder a cada elemento del arreglo requieren de dos indices declarados en dos pares de corchetes, donde el primer corchete se refiere al tamaño de filas y el segundo al tamaño de las columnas. 

Para declarar una matriz/tabla de 3 filas y 5 columnas se procede de la siguiente forma: 

tipo_dato   identificador_arreglo  [tam_fila] [tam_columna] ; 
int matriz [3] [5] ; 

donde: 
tipo_dato es el tipo de dato de la matriz 
identificador_arreglo es el nombre de la matriz 
tam_fila es el numero total de filas en la matriz 
tam_columna es el numero total de columnas de la matriz 

Inicializacion de matrices 

En el momento de declarar el arreglo, se pueden especificar valores. 

tipo_dato   identificador_arreglo  [tam_fila] [tam_columna] = { valores } ;
int matriz [3] [3] = { 1,2,3,4,5,6,7,8,9 } ;

Lectura en impresión de matrices 

Para la lectura de datos, la computadora requiere de dos ciclos anidados (para ubicar la fila y la columna) y la instrucción leer scanf o leercad gets, almacenando con ello los valores en cada celda de la tabla o matriz. 
En el siguiente ejemplo, se muestra como se pueden almacenar datos en una matriz de nombre mat de 3 filas y 4 columnas, se utiliza la instrucción leer scanf para guardar o leer los datos recibidos: 

for (  z = 0 ; z < 3 ; z ++ )  --------------------------- Recorre cada fila z 
      for ( h = 0 ; h < 4 ; h ++ )------------------------ Recorre cada columna h
        scanf ("%d", &mat[z][h]);--------------------Lee y almacena los datos en fila y columna

Cuando existen dos ciclos anidados, primero se inicializa el ciclo de fuera, posteriormente realiza el que se encuentra dentro y hasta que termine con el regresa al ciclo de fuera, en este caso, primero inicia el ciclo "z" y hasta que termina con todas las columnas "h" de la fila #0 y continua con la fila #1, iniciando de nuevo con la columna (h = 0).


En el siguiente ejemplo, se muestra como se pueden imprimir los datos almacenados en una matriz llamada mat de 3 filas y 4 columnas. Se utiliza la instrucción imprimir (printf) para escribir o mostrar el resultado: 

for (  z = 0 ; z < 3 ; z ++ ) --------------------------- Recorre cada fila z 
    for ( h = 0 ; h < 4 ; h ++ )------------------------ Recorre cada columna h
       printf ("%d", mat[z][h]); ------------------------ Imprime el valor recogido en el ejemplo                                                                          siguiente


Modificación de un elemento de una matriz

Los elementos de una matriz se pueden modificar en cualquier momento durante la ejecución del programa, solo es necesario especificar  el nombre del arreglo bidimensional (matriz), la posición del elemento que deseamos modificar y el nuevo valor que tomará. A continuación se muestra un ejemplo modificando el valor de un elemento de una matriz: 

tipo_dato  identif_arre [fila][columna] = nuevo_valor; 
 int    mat  [2][4]= 7; 

Donde: 
  • nuevo_valor es un dato o el resultado de una llamada a función o de alguna operación lógica o aritmética. En este ejemplo, se modifica el valor de elemento de la segunda fila y la cuarta columna a 7.
La mayoría de las manipulaciones con arreglos utilizan la estructura de control desde (for). Por ejemplo, el siguiente código define todos los elementos en el tercer renglón o fila del arreglo z (la fila 2 almacena los elementos de la tercera fila); donde el único indice que se modifica es el j, que sirve para cambiar el indice de las columnas: 

int z [4][4] ; 
for (j=0 ; j<4 ; j++)
    a[2][j]= 5 ; 

El ciclo desde (for) es equivalente  a los enunciados de asignación siguientes: 

  •  z[2][0] = 5 
  • z [2][1] = 5 
  • z [2][2] = 5 
  • z [2][3] = 5 
lo que significa que a toda la tercera fila de la matriz se le asigna el valor de 5. 

Arreglos & Cadenas

Un arreglo es un tipo de dato estructurado que almacena en una sola variable un conjunto limitad de datos o elementos del mismo tipo. Es un conjunto de localidades de memoria contiguas, donde la dirección mas baja corresponde al primer elemento y la mas alta al ultimo. 
El tipo de elementos almacenados en el arreglo puede ser cualquier tipo de dato. 

Un arreglo se caracteriza por: 
  • Ser una lista de un numero finito de n elementos del mismo tipo 
  • Almacena los elementos del arreglo en memoria contigua 
  • Tener un único nombre de variable que representa a todos los elementos y estos a su vez se diferencian por un indice o sub-indice 
  • Acceder de manera directa o aleatoria a los elementos individuales del arreglo, por el nombre del arreglo y el indice o sub-indice 

Los arreglos se clasifican en: 
  • Unidimensionales  (vectores o listas)
  • Bidimensionales (tablas o matrices) 
  • Multidimensionales (mas de dos dimensiones)

Arreglos unidimensionales (vectores o listas)

Conjunto de n elementos del mismo tipo almacenados en memoria continua en un vector o lista. 

Declaración de un arreglo unidimensional 

tipo_dato identif_arreglo [tam_arreglo] 

donde: 
tipo_dato se refiere al tipo de dato de cada elemento del arreglo 
identif_arreglo es el nombre que representa a todo el arreglo 
tam_arreglo es la cantidad de elementos que contiene el arreglo 



Inicialización de arreglos unidimensionales 

En el momento de declarar el arreglo, se pueden especificar los valores. Ejemplo: 

     tipo_dato identif_arreglo [tam_arreglo] = { valores }; 
        int lista [5] = {10,4,6,7,3};

La asignación de los valores se realiza al declarar el arreglo mediante el operador de asignación ( = )  y los valores contenidos dentro de las llaves. 


Lectura e impresión de un arreglo 

Es fácil procesar los elementos de un arreglo mediante los ciclos repetitivos porque facilitan la lectura, impresión, consulta y modificación de los elementos del arreglo, reduciendo el trabajo a unas cuantas lineas bien empleadas. 
Cuando el compilador ejecuta un ciclo y la instrucción leer scanf  el programa almacena los valores de en la variable arreglo. Para utilizar ciclos repetitivos es necesario el uso de variable de apoyo; el siguiente ejemplo muestra la forma de pedirle 10 números al usuario e imprimirlos después, utilizando ciclos repetitivos for. 

#include <stdio.h> 
 main ()
{
     int lista [10], i ; 
     for ( i = 0 ;  i <10 ; i++)
     { 
              printf ("Dame el elemento");
              scanf ("%d", &lista[i]);  
     }
    
     printf (" Elementos del arreglo lista : ");
     for ( i = 0 ;  i <10 ; i++)
    {
              printf ("%d\n", lista [i] );
    }

system ("pause");

El programa utiliza la estructura de repetición para (for) y leer (scanf) para capturar los elementos  del arreglo con nombre lista, e imprime el arreglo mediante otro ciclo para (for) y el imprimir (printf), donde la instrucción desde recorre todas las posiciones del arreglo. Es importante mencionar que se debe respetar el tipo de dato del arreglo y también especificar la posición del arreglo en la que se quiere guardad el valor. 

Modificación de un elemento del arreglo 

Es posible modificar los elementos de un vector en cualquier momento durante el programa, solo es necesario especificar el nombre del arreglo, la posición y el nuevo valor. A continuación se muestra un ejemplo: 

tipo_dato identif_arreglo [posición] = nuevo_valor ; 
 int lista [3] = 18; 

En donde valor es un dato, el resultado de una llamada a función o de alguna operación lógica o aritmética. En este ejemplo, se asigna el valor de 18 al cuarto elemento del arreglo que se encuentra en la posición 3. 

Ordenación en Arreglos

Su finalidad es la clasificación de datos en arreglos o archivos en un orden ascendente o descendente mediante un criterio que puede ser numérico o alfabético o de algún otro tipo. 
De acuerdo con el tipo de elemento que se quiere, la ordenación puede ser: 
  • Interna: Los datos se encuentran en memoria (arreglos, listas etc) y pueden ser de acceso aleatorio o directo 
  • Externa: Los datos están en un dispositivo de almacenamiento externo (archivo) y su ordenación es mas lenta que la interna 

Ordenamiento por burbuja 

Consiste en comparar pares de elementos del arreglo e intercambiar las posiciones, comenzando por la casilla cero hasta haber acomodado el numero mas grande en la ultima posición. (orden ascendente). Se le llama "burbuja" porque en cada pasada brota el elemento mayor o menor hasta que queden todos los elementos ordenados. 
El problema de este algoritmo de búsqueda es que vuelve a comparar  los ya ordenados desde la posición cero, por lo que es el mas deficiente, sin embargo por su sencillez es el mas utilizado. 
El siguiente código es el ejemplo de la implementación del algoritmo de búsqueda burbuja: 


i <- 1 
mientras i < N haz 
 j <- N 
 mientras j > i haz 
 si arreglo[j] < arreglo[j-1] entonces 
 intercambia(arreglo[j],arreglo[j-1]) 
 j < j - 1 
 i <- i +1 

Arreglos de Caracteres (Cadenas) 

Los arreglos son necesarios para la implementación de las cadenas de caracteres. Una cadena de texto es un conjunto de caracteres. En el lenguaje C no existe el tipo de dato cadena (string) como en otros lenguajes de programación, por lo que se utiliza un arreglo de caracteres, para poder almacenar una cadena: 

                    char cad [ ]= "Hola" ; 

Una cadena de caracteres es un arreglo de caracteres que contiene al final el carácter nulo (\0);  por esta razón es necesario que al declarar los arreglos estos sean de un carácter mas que la cadena mas grande. El compilador inserta automáticamente un espacio nulo al final de la cadena, de modo que la secuencia real seria: 
  
                          char cad [ ]= "Hola" ; 

La cadena se almacenara de la siguiente forma: 


Contenido:       H         O           L          A      \0

Una opción para almacenar una cadena de caracteres es el uso de la palabra reservada scanf (variable)  pero si lo que se quiere es almacenar una cadena con espacios en blanco no lo podemos hacer con ella, sino que debemos utilizar la palabra reservada gets, que se encuentra dentro de la librería string.h; gets  solo se utiliza para leer cadenas de caracteres y scanf   para leer cualquier tipo de variable, de preferencia de tipo numérico. 

Gets 

Introduce una cadena de caracteres del teclado hasta que se encuentre un carácter \n ( nueva linea), dicho carácter no se añade a la cadena. Se agrega al final un carácter de terminacion NULL.