Programa C
Programa C
Acreditación:
Este documento se encuentra a la espera de ser revisado y reconocido por la
cátedra de Algoritmos y Estructuras de Datos de la Universidad Tecnológica
Nacional Facultad Regional La Plata como documentación oficial de la misma.
Bibliografía de referencia:
- https://www.tutorialspoint.com/cprogramming/index.htm
- https://www.cprogramming.com/tutorial/c-tutorial.html
- https://stackoverflow.com/
- https://www.geeksforgeeks.org/c-language-set-1-introduction/
- Estándar de programación en C ISO/IEC 9899:2011
Historial de versiones
Esta es una guía de uso general de Standard C centrada en el manejo del lenguaje y sus
características. Por tanto, se dan por sabidos conceptos básicos de programación y se
ahonda en la sintaxis propia y el manejo de C.
A lo largo del manual se detallarán las claras diferencias que presenta C respecto a los
lenguajes convencionales dado su bajo nivel, las cuales pueden resultar confusas en
principio.
https://github.com/Zeriel/Aprendiendo-C/
Índice
1. Introducción .........................................................................................................................................1
1.1. Qué es C........................................................................................................................................1
1.2. Compiladores ...............................................................................................................................1
1.3. Librerías ........................................................................................................................................1
1.4. Tipos de Datos ..............................................................................................................................2
1.5. Caracteres especiales ...................................................................................................................3
1.6. Conectores lógicos .......................................................................................................................4
2. Lenguaje C ............................................................................................................................................5
2.1. Definición del programa ...............................................................................................................5
2.2. Operaciones básicas .....................................................................................................................6
2.2.1. Printf() .............................................................................................................................. 6
2.2.2. Scanf()............................................................................................................................... 7
2.2.3. Getchar() y _getch() ......................................................................................................... 7
2.2.4. _clrscr() ............................................................................................................................. 8
3. Variables ...............................................................................................................................................9
3.1. Declaración ...................................................................................................................................9
3.2. Aplicación .....................................................................................................................................9
3.3. Contadores y acumuladores.......................................................................................................10
4. Estructuras de Decisión ......................................................................................................................11
4.1. Si ………………………………………………………………………………………………………………………………………… 11
4.2. Caso ……………………………………………………………………………………………………………………………………. 11
5. Estructuras de Control........................................................................................................................13
5.1. Repetición Incondicional ............................................................................................................13
5.2. Repetición Condicional...............................................................................................................14
5.3. Continue y Break ........................................................................................................................15
6. Arreglos ..............................................................................................................................................17
6.1. Definición ...................................................................................................................................17
6.2. Strings .........................................................................................................................................20
6.3. Arreglo de arreglos .....................................................................................................................21
7. Registros .............................................................................................................................................24
7.1. Definición ...................................................................................................................................24
7.2. Arreglo de Registros ...................................................................................................................25
8. Modularización ...................................................................................................................................28
8.1. Definición ...................................................................................................................................28
8.2. Parámetros .................................................................................................................................29
8.3. Procedimientos y Funciones ......................................................................................................31
8.4. Definición de main según estándares C99/C11 .........................................................................33
8.5. Recursión ....................................................................................................................................34
8.6. Arreglos y registros como parámetro ........................................................................................34
9. Punteros .............................................................................................................................................39
9.1. Definición ...................................................................................................................................39
9.2. Puntero como parámetro...........................................................................................................40
10. Listas .................................................................................................................................................45
10.1. Definición y uso ........................................................................................................................45
10.2. Lista como parámetro ..............................................................................................................47
10.3. Aplicación .................................................................................................................................49
1. Introducción
1.1. Qué es C
C es un lenguaje de programación imperativo desarrollado en 1972 por Dennis Ritchie, sucesor del lenguaje
B. Es considerado de medio nivel ya que, si bien es un lenguaje de alto nivel, fue pensado como intermediario
del lenguaje ensamblador. Por esto, es un lenguaje que produce código de alta eficiencia ya que es fácil de
convertir a lenguaje máquina con unas pocas instrucciones. Además, muchos compiladores incluyen
extensiones para poder escribir código ensamblador dentro del código de C y operar directamente con el
Sistema Operativo y el hardware.
Los usos de C en la actualidad son muy variados. Desde su uso original como lenguaje de desarrollo de
software base (Sistema Operativo y programas que interactúan con este) es también usado para desarrollar
firmware de productos electrónicos que poseen microcontroladores, para crear compiladores de más alto
nivel, para desarrollar software aplicativo, entre otros.
Muchos lenguajes actuales están influenciados total o parcialmente en la estructura de C. Estos lenguajes
son usualmente referidos como “C-like” o de la “Familia C” e incluyen a los derivados de C (C++, Objective-C,
etc), Java, Processing, PHP, y más. Estos suelen compartir la estructura y sintaxis de C, aunque no incluyen los
manejos de bajo nivel.
Hay que tener siempre en mente que C es un lenguaje Case Sensitive, por lo que las mayúsculas y minúsculas
erróneas pueden causar errores en la compilación.
1.2. Compiladores
Existen múltiples compiladores y entornos para programar en lenguaje C, algunos específicos de Standard C
(como GCC) y otros que permiten programación en C y C++ (como el entorno Visual Studio). En este manual se
utilizará el Pelles C para Windows, que sólo admite código C.
1.3. Librerías
Las librerías en C agregan funciones para simplificar las operaciones que, de otra manera, requerirían
codificación más avanzada. Se invocan con la sentencia “#include”.
Por cuestiones de portabilidad, la mayoría de las librerías siguen un estándar general, lo que permite
ejecutar un mismo código en distintos Sistemas Operativos como Windows y Linux. A continuación, se
explican las librerías utilizadas a lo largo del manual:
Todas las librerías tienen la extensión .h, que viene de header. Los programas fuentes de C tienen la
extensión .c.
En C, los tipos de datos son similares a Pascal, salvo por algunas diferencias. Lo que sí es muy notorio y
debe aclararse es que cada tipo de dato, además de su nombre nativo usado en la declaración, posee un
carácter específico que se usa para definir explícitamente el tipo de dato en las funciones de lectura e
impresión en pantalla, por ejemplo.
Int: El tipo entero, su carácter es la letra “d”. No se diferencia del Integer en Pascal.
Char: El tipo carácter, que almacena un carácter individual. Su carácter es la letra “c”.
Float: El tipo flotante, equivalente al real en Pascal. Su carácter es la letra “f”. Para acotar
la cantidad de decimales en un muestreo se utiliza “.xf”, donde “x” es la cantidad de dígitos
decimales.
Estos son los tipos nativos de C. Nótese que faltan los tipos “booleano” y “cadena de caracteres”; esto
es porque nativamente no existen en C.
Para los booleanos, C emplea como condición booleana cualquier entero distinto de 0 para TRUE y 0 para
FALSE. Por tanto, se podría decir que una variable booleana puede definirse mediante una variable entera
que se evalúe respecto a 0. También existen librerías que definen el tipo bool.
Para las cadenas de caracteres, se debe definir un arreglo de tipo char para almacenar cada carácter de
la palabra en cada una de las posiciones del arreglo. Este tipo de “string” es diferenciado del tipo char con
el carácter “s”.
Uno de los detalles más visibles ni bien se comienza a trabajar con C es la necesidad de emplear ciertos
caracteres especiales ya que, a diferencia de otros lenguajes como Pascal, en C se debe aclarar (en la
mayoría de los casos) si estamos haciendo referencia al valor de una variable o a la dirección en donde
está almacenada para poder asignarle un dato.
- Et / Ampersand
El carácter Et (“&”), que de aquí en más lo llamaremos por su nombre inglés ampersand (dado que así
es como lo encontraremos en la mayoría de las bibliografías), es el símbolo que C utiliza para operar con
la dirección de memoria de una variable. Así, si tenemos una variable “x” cargada con algún dato, “&x”
indicaría que estamos operando con la dirección en memoria de “x” y no con el valor que contiene.
- Porcentaje
El símbolo de porcentaje (%) aclara el tipo de dato esperado en las funciones de impresión y lectura, ya
que se debe especificar explícitamente el tipo de la variable que se usa en ambos casos. Este símbolo se
emplea con la letra asignada a cada tipo de dato, por ejemplo, “%d” para especificar que el tipo de la
variable involucrada es entero.
- Asterisco
El asterisco (*) se emplea para definir y operar con variables de tipo puntero, similar, en cierta forma, al
uso del circunflejo (^) en Pascal.
La flecha se forma con un guion y el símbolo de mayor, siendo entonces “->”. Este símbolo es usado en
los punteros a registro (como las listas) para acceder a cada campo del registro que el puntero apunta, por
ejemplo “puntero->campo”.
En Pascal, los conectores lógicos se basan en el habla inglesa y por ende son textuales. En C, por otro
lado, se opta por una forma más genérica de codificación, y por ello se utilizan símbolos para los mismos.
Los operadores/conectores en C son:
Conector Símbolo
Igual ==
Distinto !=
Not !
Menor <
Menor o igual <=
Mayor >
Mayor o igual >=
AND &&
OR ||
program nombrePrograma; //El encabezado del programa con el nombre del mismo
uses librería; //La lista de librerías a invocar
var
//Declaración de variables globales
Begin //El bloque principal del programa
[bloque de código]
end.
Hay que hacer una serie de aclaraciones respecto al código anterior. Como anticipamos, el código en C
carece de un encabezado con su nombre, comenzando directamente por la invocación de librerías y la
declaración de variables globales. Ignoremos por ahora la palabra “int” en el main, se explicará en el
Capítulo 8 de Modularización.
Puede apreciarse a priori que el cuerpo principal se define como el procedimiento main y que su
correspondiente código se halla encerrado en llaves ({}). Las llaves son la forma en que C inicia bloques de
código, siendo la forma de Pascal el par BEGIN-END. También vemos en “return 0;” que el cierre de
sentencias, tanto en Pascal como en C, se realiza con el punto y coma (;).
Por último, podemos ver que la última línea de código es la ya mencionada return 0. Return es la forma
en la que las funciones de C devuelven un valor a quien las invocó. Por convención se suele emplear un
return 0 al final del main con el fin de verificar si la ejecución del programa fue correcta. Esta verificación
se basa en que, si durante la ejecución ocurrió algún error, el valor que retornará main será distinto de
cero.
5
Llamamos operaciones básicas a las funciones que empleamos para leer información del usuario y para
mostrar información en la consola. Mientras que en Pascal estas funciones eran simples de usar, en C
presentan cierta rigidez en cuanto al orden de sus parámetros.
2.2.1. Printf()
Printf es la función en C incluida en la librería <stdio.h> que nos permitirá mostrar información en la
consola, como “write” en Pascal.
Recordando Pascal, el write lleva como parámetro la información a mostrar encerrada en comillas, y de
ser necesario concatenar el valor de una variable, la misma puede separar el texto entre comillas con el
uso de una coma. De manera que la sintaxis del write de Pascal es:
Esta libertad que nos brinda Pascal en el write no se aplica en C. Para poder mostrar información con
printf se debe respetar que primero se coloca el texto entre comillas y luego las variables, no de otra forma.
Para poder identificar dónde concatenar el valor de cada variable, se usan los caracteres de cada tipo de
dato explicados en el Capítulo 1.4.
Entonces, supongamos que la variable “nomVariable” es de tipo entero, su carácter en C es la letra “d” y
se escribiría de la siguiente forma:
El “%d” le dice a la función que en esa ubicación está esperando un dato de tipo entero, el cual será el
parámetro fuera de las comillas. Se pueden concatenar varias variables con este método, siempre que se
tenga en cuenta que la asignación del tipo esperado y la variable es secuencial; es decir, por cada “%tipo”
buscará una variable y se asignará el valor, en el orden de izquierda a derecha.
Si se quisiera dejar una línea en Pascal, bastaría con cambiar la función write por writeln. En C, para dejar
una línea se debe incluir en el texto entre comillas “\n” en donde deseemos generar el corte de línea. Es
posible, aunque engorroso, escribir un texto completo en un solo printf empleando \n para generar los
cortes.
Scanf en C es la función incluida en la librería <stdio.h> que nos permitirá leer información del usuario y
almacenarla en una variable, como read/readln en Pascal.
Esta función es similar a su homóloga en Pascal, con la diferencia que deberemos emplear los caracteres
especiales porcentaje y ampersand; porcentaje para especificar el tipo de dato, y ampersand para
almacenarlo en la dirección de memoria de la variable objetivo.
Si quisiéramos leer un número entero en la variable “num” en Pascal, el readln sería simplemente:
readln(num);
scanf("%d", &num);
Como dijimos, primero especificamos qué tipo de dato queremos almacenar empleando el “%tipo” entre
comillas. Luego, como segundo parámetro, enviamos la dirección de memoria de la variable “num”
empelado el ampersand. Si el tipo especificado en la primera parte no coincide con el tipo de la variable
en la segunda, el compilador dará error.
Hay que hacer una aclaración importante: este ejemplo se cumple para todos los tipos de datos excepto
el string. Como ya dijimos en el capítulo 1.4, los strings son en realidad arreglos de caracteres, y en C un
arreglo está definido por un puntero al primer elemento. Esto se verá en mayor detalle en el Capítulo 6 de
Arreglos y Capítulo 9 de Punteros, por lo que por ahora basta con aclarar que, cuando se quiera leer un
string, no se deberá utilizar el ampersand.
Las funciones getchar y _getch son el equivalente a readkey de la librería “crt” en Pascal. Su función
consiste en detener la consola, para poder visualizar mensajes de salida, hasta que se ingrese una tecla y
se continúe con la ejecución.
La razón por la cual hay dos funciones para el mismo objetivo radica en que la función estándar de C,
getchar(), puede traer problemas al capturar datos ingresados previamente en una lectura de información
si no se tiene cuidado a la hora de la codificación.
Otra diferencia entre ambas funciones es que getchar(), en su funcionamiento normal, espera que se
ingrese un valor por teclado y se presione enter para recibirlo, mientras que getch() toma cualquier tecla
presionada y automáticamente la recibe para continuar con la ejecución.
2.2.4. _clrscr()
De igual manera que con _getch(), la función _clrscr() (clrear screen) está incluida en <conio.h> y no es
portable a otros sistemas fuera de Windows. Su función es idéntica a su par clrscr de Pascal: limpiar la
consola de Windows de todo el texto acumulado hasta el momento.
Como se vio en el Capítulo 1, las variables pueden declarase globalmente fuera del main() o localmente
dentro del mismo. En C la declaración de una variable es al revés que en Pascal; primero se declara el tipo
de la variable y luego el nombre de la misma.
#include <stdio.h>
#include <conio.h>
int num1; //Declaración de un número entero
char letra=a; //Declaración de un carácter y asignación de valor inicial
char palabra[15]; //Declaración de un string
int main(){
float num2, num3; //Declaración de dos números flotantes
return 0;
}
En el ejemplo anterior num1, letra y palabra son variables globales, mientras que num2 y num3 son
variables locales a main. Además, letra se declara con un valor inicial; es posible declarar una variable con
un valor inicial fijo si se le asigna el valor en la misma declaración. La asignación en C es “=”, a diferencia
de Pascal que usa la sintaxis “:=”.
Nótese que “palabra” es un string; como dijimos anteriormente los strings en C son en realidad arreglos
de caracteres, y la forma de definir una variable tipo arreglo es con los corchetes ([]) al final de la variable.
En el Capítulo 6 de Arreglos se verá de manera más detallada.
Cuando declaramos variables locales en el main (o en cualquier módulo) debemos hacerlo al principio
de cualquier otra línea de código, ya que C no permite declarar variables en mitad de ejecución del módulo.
3.2. Aplicación
Ahora que sabemos cómo declarar variables podemos empelar las funciones vistas en el Capítulo 2.2
para crear una aplicación simple, donde se pida datos al usuario y se los trabaje generando una salida.
La aplicación anterior le pide un valor al usuario en num2, la suma con num1 y almacena el resultado en
res.
Con el objetivo de agilizar la programación, C define una manera más rápida de trabajar con contadores
y acumuladores:
cont= cont+1 puede reducirse a cont++. Lo mismo para decrementar con cont--.
acum= acum+valor puede reducirse a acum+=valor.
Esta forma de trabajo puede verse reflejada en todos los lenguajes C-like, como Java o PHP.
10
La estructura SI/SINO no difiere mucho de su versión en Pascal, sólo debemos procurar que cada bloque
booleano esté debidamente encerrado entre paréntesis, ya que de esta manera es como operará
correctamente la condición.
if ([condición]) {
[bloque de código]
}
else {
[Bloque de código]
}
Nótese los paréntesis. Cada evaluación booleana debe estar correctamente delimitada entre paréntesis,
para que el compilador entienda lo que se quiere evaluar. En este caso, se evalúa que, tanto condicion1
como condicion2 sean TRUE, o que condicion3 sea TRUE para ejecutar el bloque. Para la simbología,
recordar la tabla de operadores booleanos del Capítulo 1.6.
4.2. Caso
El caso en C sí difiere bastante de su versión en Pascal en cuanto a sintaxis. La palabra reservada para el
caso es SWITCH y cada opción posible va precedida de un CASE. Además, el cierre de cada opción del caso
debe realizarse manualmente a través de un BREAK al final del bloque. La rama falsa es DEFAULT.
11
El ejemplo anterior lee un número ingresado por el usuario, y devuelve el día de la semana que
corresponde a ese número, o un error si se ingresa un número incorrecto.
El dato a validar en las opciones del caso debe ser un valor constante. Las comparaciones no son
reconocidas.
12
La estructura de repetición incondicional en C es muy particular, y está presente en todos los lenguajes
C-Like. La variable de control de bucles puede declararse en la misma repetición y usarse de manera local.
Nótese el int que empleamos en la variable de control, esto es porque la estamos declarando localmente
para el FOR como explicamos anteriormente.
El valor inicial.
La condición de salida.
La forma en que se modificará la variable de control.
El valor inicial es similar a Pascal, simplemente define el valor que tendrá la variable de control al
momento de iniciarse el bucle. Como ya dijimos, la variable de control puede estar ya declarada o
declararse dentro del FOR.
La condición de salida es una expresión booleana que mantendrá el bucle, siempre y cuando sea TRUE.
Por ejemplo, para una iteración de 10 ciclos se podría emplear “variableControl <= 10” si tenemos en
cuenta que la variable de control comenzó con valor inicial 1.
La forma de modificación es la operación que se aplicará a la variable de control por cada vuelta del
bucle. Para un FOR clásico que incrementa en 1 la variable de control hasta llegar a cierto valor, se puede
usar el incremento “variableControl++”.
El siguiente ejemplo mostrará en pantalla la tabla de multiplicar del 1 al 10 de un número ingresado por
el usuario:
13
int main(){
int num; //Variable local a main
printf("Ingrese valor: "); //Valor a realizar tabla
scanf("%d", &num); //Nótese el uso de caracteres especiales
_clrscr();
for (int i=1; i<=10; i++){ //Repetición de 1 a 10
printf("%d x %d = %d\n", num, i, num*i);
}
_getch(); //Para que no termine la ejecución sin mostrar
return 0;
}
En C puede resultar un poco confuso que el FOR, que es una repetición incondicional, posea una
condición booleana; contrario a Pascal, que sólo podía realizar bucles por incrementos o decrementos de
la variable.
- While
int true=1;
while (true){
[bloque de código]
}
Como anticipamos, la sintaxis del while es casi idéntica a la de pseudocódigo y Pascal, por lo que no se
considera que haya mucho que explicar sobre la misma.
Recordemos que, como explicamos en el Capítulo 1.4, en C no existe el tipo booleano nativamente, sino
que evalúa 0 como FALSE y cualquier otro valor entero como TRUE. Por tanto, en este ejemplo empleamos
una variable entera como nuestro “bool” para generar un bucle infinito en la iteración.
Respecto a las condiciones, se aplican las mismas reglas para los conectores lógicos
14
Antes de mostrar la sintaxis del repetir hasta, explicaremos brevemente su función. En la repetición
condicional mientras, el bloque de código puede no ejecutarse nunca si la condición da FALSE la primera
vez que se evalúa.
Cuando se trabaje con mientras y hasta hay que tener claro que no siempre una repetición mientras se
va a poder reemplazar con un hasta, en tanto que el hasta siempre es reemplazable por un mientras, que
es el caso más general de repetición condicional.
int true=1;
do{
[bloque de código]
}while(true);
Tener en cuenta que, como la condición del do..while va después del cierre de llave, lleva punto y coma.
Ya vimos el uso del break con anterioridad, cuando se explicó caso en el Capítulo 3.2. Ahora veremos
más en detalle tanto esta sentencia como la sentencia continue, en lo que a repetición compete.
El continue y el break son sentencias que pueden usarse dentro de una repetición para alterar la
ejecución normal de cada bucle. Estas herramientas no suelen darse en Algoritmos y Estructuras de Datos,
dado que dificultan entender el concepto de cada repetición al permitir manipularla sin importar las
condiciones de salida. Se explicarán para que se tenga conocimiento de ellas, pero no se usarán en los
ejemplos de este manual.
- Continue
El continue ignora todo el código restante que lo sucede cuando es encontrado por el compilador, y
vuelve al inicio del bucle. Si se realiza en un for, se aplicará la modificación a la variable de control del
mismo.
15
El break permite salir de una iteración en el momento que la sentencia es ejecutada por el compilador,
sin importar el estado de las condiciones de salida del bucle.
Es obvio, habiendo explicado estas sentencias, el por qué podrían entorpecer el entendimiento
conceptual de las estructuras de repetición, lo que no quita que su uso en determinados casos pueda ser
más eficiente. Sin embargo, y como se aclaró al principio, no se usarán estas herramientas en los ejemplos
de este manual.
16
Primero que nada, para trabajar con arreglos en C hay que tener en cuenta que un arreglo es, en realidad,
un puntero de memoria al primer elemento del mismo. Esto quiere decir que la variable tipo arreglo
almacena una dirección de memoria, motivo por el cual, cuando se trabajó con una variable así (los string)
en ejemplos anteriores, no se utilizó el ampersand.
Como ya se explicó, el ampersand se emplea para indicar que queremos operar con la dirección de
memoria de una variable y no con el valor contenido en la misma. En la mayoría de los casos no habría
problemas, pero cuando trabajamos con arreglos en su forma general (es decir, sin usar un índice)
debemos omitir el ampersand ya que la variable, como se aclaró al principio, almacena como valor la
dirección en memoria del primer elemento. La variable tipo arreglo es un puntero.
Cuando se trabaje con campos específicos del arreglo mediante un índice, sí usaremos el ampersand, ya
que sería igual a trabajar con una variable del mismo tipo de dato que el arreglo.
17
#include <stdio.h>
#include <conio.h>
int main(){
[bloque de código]
}
En este ejemplo vemos que, para definir el rango de un arreglo, se coloca dentro de los corchetes el
número de posiciones del mismo. También vemos que, si en lugar de un arreglo queremos definir una
matriz, basta solamente con agregar otro par de corchetes. Así, podemos definir arreglos de n dimensiones
definiendo n pares de corchetes.
Tener en cuenta, cuando se trabaje con arreglos en C, que las posiciones se cuentan desde 0, no desde
1. Así, el arreglo vecNum del ejemplo, el cual tiene tamaño 5, tiene las posiciones 0 a 4.
18
int main(){
for (i=0; i<5; i++){ //Recorro el arreglo
vecNum[i]= 0; //Inicializo con valor 0
}
for (i=0; i<5; i++){ //Realizo la carga del vector
printf("Vector posicion: %d\n", i);
printf("Ingrese valor entero: ");
scanf("%d", &vecNum[i]); //Nótese que en el scan el vector va con & porque use índice
_clrscr();
}
}
En este ejemplo, recorremos dos veces el arreglo “vecNum”, primero para inicializarlo en cero y luego
para cargar valores por consola.
En el scanf, como tipo de dato esperado debemos ingresar el tipo del arreglo, entero (“%d”) en este caso.
Podríamos no haber definido un índice junto al vector y, en su lugar, definirlo localmente dentro de cada
FOR, pero se hizo de esta manera para mantener una semejanza con Pascal y Pseudocódigo. A partir de
ahora, los índices se definirán en el FOR a no ser que su uso sea necesario fuera del mismo.
Haciendo repaso sobre lo que ya dijimos al principio, si queremos operar con el dato almacenado en una
posición del arreglo, se utiliza el ampersand dado que es semejante a trabajar con una variable. Si, en
cambio, queremos trabajar con el arreglo en sí, no se utiliza el ampersand.
A grandes rasgos, y como recordatorio: si se utiliza un arreglo con índice se debe usar ampersand.
Por último, cabe aclarar que los arreglos pueden cargarse manualmente mediante un recorrido por el
mismo, como hicimos en el ejemplo anterior, o crearse con datos precargados. Esto último no requiere el
uso de un FOR, ya que los datos se definen en la declaración del arreglo. La forma de hacer esto es:
En donde a nuestro arreglo vecNum le asignamos de forma predefinida los valores 1, 2 y 3, los cuales se
cargarán secuencialmente en las posiciones 0, 1 y 2 del mismo, respectivamente.
19
Ya dijimos antes que los strings no existen como tales en C y, en su lugar, se utilizan arreglos de
caracteres. Para poder operar con strings (ya sea compararlos, asignarle valor, copiar uno en otro y demás)
debemos empelar la librería <string.h>.
Veremos ahora un ejemplo donde se utilice esta librería para poder dar cierre a los strings de C:
#include <stdio.h>
#include <conio.h>
#include <string.h>
int main(){
strcpy(cadena1, "hola"); //Asignamos un valor a cadena 1
strcpy(cadena2, " mundo"); //Asignamos un valor a cadena 2
strcat(cadena1, cadena2); //Concatenamos ambos para formar "hola mundo"
if (strcmp(cadena1, "hola mundo")==0){
printf("La concatenacion funciono");
_getch();
_clrscr();
}
return 0;
}
La única manera que tenemos de cargar un string sin usar estas funciones es leyendo el dato
directamente del usuario mediante el scanf, el cual sería:
20
No importa si se trabaja con una variable, un campo de registro o un campo de lista, si el tipo de dato
almacenado es un string, se aplica todo lo visto en este capítulo.
El manejo de arreglo de arreglos en C requiere conocimientos sobre el manejo de punteros, los cuales se
verán en detalle el Capítulo 9. Por tanto, lo que veamos en esta sección del manual podrá resultar algo
confusa, por lo que se recomienda volver a repasarla una vez que se haya entendido el uso de punteros.
Se debe definir, entonces, un arreglo de punteros, luego los arreglos que serán apuntados, y finalmente
vincular cada uno de estos arreglos con el puntero correspondiente en el arreglo general. Adicionalmente,
se definirá un puntero auxiliar para acceder al arreglo que queramos recorrer en la i-ésima posición dentro
del arreglo general.
21
Seguramente todo esto parecerá muy confuso, pero quedará más claro una vez lo veamos plasmado en
código. La definición de un arreglo de arreglos entonces será:
En donde el asterisco nos indica que nuestro arreglo de tres posiciones enteras es, en realidad, un arreglo
de tres posiciones que apuntan cada una a un entero.
El siguiente paso es crear cada uno de los arreglos que irán en cada uno de los punteros:
Estos arreglos no necesariamente deben ser del mismo tamaño. Los siguientes arreglos también son
válidos:
Ahora, definimos nuestro puntero auxiliar, con el cual podremos recorrer cada uno de estos arreglos:
Por último, asignamos cada uno de estos arreglos a nuestro arreglo general:
Ya tenemos definida nuestra estructura para el arreglo de arreglos. Si quisiéramos recorrerlo para
mostrar sus datos, bastaría con emplear el siguiente código:
22
//Si los arreglos tuvieran tamaños distintos, cada uno llevaría su propio for
//Como yo los definí a todos iguales, puedo usar el mismo for para recorrerlos a los tres
Nótese el uso que le damos al puntero auxiliar, igualándolo en cada iteración al arreglo actual que apunta
el arreglo general a través del índice i.
Con esto puede darse cierre al tema de arreglo de arreglos. Se aconseja rever esta parte una vez
entendido el Capítulo 9 de Punteros, para un mejor entendimiento de lo que se está haciendo.
23
Los registros en C son, a grandes rasgos, idénticos a los de Pascal. La palabra reservada en el Sistema
para registros es struct y se usa, tanto para definir un registro, como para declarar una variable de dicho
tipo.
Persona = record
apellido: string;
nombre: string;
edad: integer;
end;
Definición de struct en C:
El registro del ejemplo define a una persona con tres campos: dos strings para apellido y nombre, y un
entero para su edad. Como es una definición de un tipo de dato y no un bloque de código, la llave de cierre
lleva un punto y coma.
El acceso a un campo de registro se realiza de la misma manera que en Pascal, con un punto seguido del
nombre del campo:
24
int main(){
strcpy(p.apellido, "Moradillo"); //Cargo un apellido
strcpy(p.nombre, "Federico"); //Cargo un nombre
p.edad= 23; //Cargo una edad
printf("Datos de la persona:\n");
printf("Apellido: %s\n", p.apellido);
printf("Nombre: %s\n", p.nombre);
printf("Edad: %d\n", p.edad);
_getch();
_clrscr();
return 0;
}
En el ejemplo se declaró una variable tipo Persona y se cargó con datos, en este caso los míos.
Para declarar una variable tipo registro, se debe colocar primero el nombre del tipo de registro seguido
del nombre de la variable que estamos declarando.
Tanto nombre como apellido se trabajaron con funciones de la librería <string.h>, mientras que edad se
trabajó de manera normal, accediendo al campo con un punto y asignando el valor directamente.
Al igual que en Pascal, los campos de un registro pueden ser de tipos nativos o de tipos definidos por el
usuario (arreglos, otros registros, listas).
Se pueden definir arreglos que contienen registros para trabajar con situaciones más complejas. La
declaración de un arreglo de registros es tan simple como agregarle corchetes a una declaración normal
de registro:
25
En este ejemplo se declaró una variable vecP de tipo Persona que, además, es un arreglo de 10
posiciones. Es decir, es un vector que contiene a 10 personas.
Como no hay más que explicar, pasamos directamente a un ejemplo práctico donde se cargan 10
personas en el arreglo y luego se las muestra por consola:
#include <stdio.h>
#include <conio.h>
#include <string.h>
int main(){
for (int i=0; i<10; i++){ //El índice para moverme por el arreglo
printf("Persona nº: %d\n", i+1); //Se agrega un +1 porque arranca en 0
printf("Ingrese apellido: ");
scanf("%s", vecP[i].apellido); //Recordar: es un string, no lleva ampersand
printf("Ingrese nombre: ");
scanf("%s", vecP[i].nombre);
printf("Ingrese edad: ");
scanf("%d", &vecP[i].edad); //Es un entero, lleva ampersand
_clrscr();
}
for (int i=0; i<10; i++){ //Recorro el arreglo y muestro los datos
printf("Datos de persona nº: %d\n", i+1);
printf("Apellido: %s\n", vecP[i].apellido);
printf("Nombre: %s\n", vecP[i].nombre);
printf("Edad: %d\n", vecP[i].edad);
_getch();
_clrscr();
}
return 0;
}
26
Finalmente, y como dato más relevante, podemos ver que el acceso a cada campo del registro dentro
del vector es prácticamente idéntico a como se haría en Pascal o pseudocódigo; primero accediendo a la
posición deseada mediante el índice y luego, con el punto, accediendo al campo específico que queremos
operar.
27
En C, los módulos se definen de la misma forma que el main, que no es más que un módulo reconocido
por el compilador como el primero a ejecutar. Una forma estándar de definir módulos es mediante un
prototipo antes del main, que consiste sólo en el encabezado del módulo con sus parámetros y la
definición del cuerpo del módulo debajo de main.
Si bien la forma estándar con prototipo es la más recomendada por considerarse buena práctica, tener
en cuenta que, de omitirse el prototipo, el módulo debe definirse antes del main, como se hace en Pascal
y pseudocódigo.
#include <stdio.h>
#include <conio.h>
int moduloEjemplo(void)
{
[bloque de código del módulo] //La definición del módulo
}
#include <stdio.h>
#include <conio.h>
28
8.2. Parámetros
Los módulos pueden o no recibir parámetros. Hasta ahora los dos módulos que vimos (main y
moduloEjemplo) no recibían ningún parámetro. Existen dos formas de aclarar que un módulo no recibe
parámetros, pero sólo una de ambas es aceptada como estándar en los compiladores modernos.
Utilizando la palabra reservada void entre los paréntesis, podemos aclarar que ese módulo NO recibe
ningún tipo de parámetro y tratar de enviarle uno causará error.
La otra opción es la que venimos empleando con main, que consiste en dejar vacíos los paréntesis. El
problema con esta opción es que, en realidad, no está aclarando que el módulo main() no recibe
parámetros, sino que lo que le está diciendo al compilador es que no se sabe si va a recibir y en qué
cantidad parámetros.
La declaración de un main bajo esta opción, en la gran mayoría de los casos y en la totalidad de los vistos
en este manual, no supondría problemas, dado que nunca realizamos invocaciones a main desde otro
módulo. En su lugar, siempre corremos los ejecutables generados al compilar nuestro código, por lo que
emplear main() y main(void) no tiene ninguna diferencia.
Sin embargo, sí podemos cometer errores si no usamos el void para los módulos que invocaremos desde
main, por lo que se respetará el estándar y correcta definición de un módulo sin parámetros mediante el
uso de void.
Para terminar, aclaramos que la versión con paréntesis vacíos puede servir si se trabaja con un
compilador antiguo de C o a modo de compatibilidad con el mismo, el cual no tiene implementado void
en la lista de parámetros.
En el caso de sí querer enviar parámetros, los mismos se declaran de la misma forma que declaramos
variables:
29
int modulConParametro (int para1, int para2); //Módulo que recibe dos parámetros enteros
int main()
{
int x, y, z;
...
z=modulConparametro(x,y); //Invocación al módulo enviándole x e y como parámetros
...
return 0;
}
En el ejemplo anterior vemos como enviarle parámetros a un módulo, y también como invocarlo. Nótese
la variable “z”, esto es porque nuestro módulo de ejemplo era una función y necesitaba devolver un dato.
En el siguiente Capítulo se verá cómo definir funciones y procedimientos.
A diferencia de Pascal, donde los parámetros se separan por punto y coma en la definición del módulo y
por coma en la invocación, en C tanto en la invocación como en la definición los parámetros se paran por
coma simple.
program ejemploModulo;
uses crt;
Un tema importante respecto a los parámetros de C: sólo existe pasaje por copia. A diferencia de Pascal,
C sólo admite pasajes por copia. Los pasajes por referencia se realizan enviando como parámetro una
30
En realidad, esto es lo que hace Pascal con su palabra reservada var. En C debemos trabajar sin esta
herramienta, haciendo explícito el envío de la dirección de memoria como pasaje.
#include <stdio.h>
#include <conio.h>
void modulReferencia(int par1, int par2, int *resul); //Módulo que recibe resul por
//referencia
int main()
{
int x, y, z;
...
modulReferencia(x, y, &z); //z es enviado con &, por lo que en realidad
//estamos enviando su dirección en memoria
...
return 0;
}
void modulReferencia(int par1, int par2, int *resul) //El parámetro que enviamos era una
{ //dirección, así que la recibimos como
[bloque de código] //puntero
}
En donde la variable que se envía como parámetro lleva el ampersand para enviar su dirección en
memoria en lugar de su valor copiado, mientras que el parámetro referenciado se declara con un asterisco,
que es la forma en la que se emplea un puntero. Los punteros se verán en detalle en el Capítulo 9, pero
basta con saber que para manejar un puntero se debe usar el asterisco.
Al igual que Pascal, C trabaja con procedimientos que pueden o no retornar información en un parámetro
referenciado, y funciones que siempre retornan un tipo de dato nativo cuando son invocadas.
31
Y un procedimiento:
En el siguiente ejemplo, damos cierre a todo lo dado hasta ahora de modularización realizando un
procedimiento y una función que reciban dos valores, los multipliquen y retornen el resultado:
#include <stdio.h>
#include <conio.h>
int mult (int x, int y); //Función que devuelve la multiplicación de dos números enteros
void multReferencia (int x, int y, int *resul); //Procedimiento que hace lo mismo que mult pero
//devuelve el resultado en un parámetro por referencia
int main()
{
int x, y, z;
void multReferencia (int x, int y, int *resul){ //El parámetro que enviamos era una dirección, asique
//la recibimos como puntero
*resul=x*y; //De nuevo, nótese como operamos con resul como un puntero, así modificamos por
} //referencia
32
Este capítulo no es necesario para proseguir con el manual, se dedicará a abarcar la definición de un
main según los estándares vigentes para la correcta programación en C.
Previamente, en el Capítulo 8.1, discutimos brevemente sobre cómo definir a main y concluimos que
utilizaríamos int main(), dado que no presenta mayor problema. Esta definición no es incorrecta, pero,
desde un punto de vista más estricto, no satisface a los estándares vigentes hoy en día, los cuales dan a
int main() como una forma obsoleta de definir el mismo.
Tanto el anterior estándar C99 (ISO/IEC 9899:1999), como el actualmente vigente C11 (ISO/IEC
9899:2011), nos especifican cuatro formas correctas de definir un main:
Punto 1) Es la definición que explicamos como alternativa en el Capítulo 8.1, el cual agrega un void para
especificar la nulidad de parámetros.
El NTMBS es una cadena de bytes distinta de cero que termina con un byte en cero, marcando el final.
Punto 3) Es igual al segundo, pero deja lugar a otros parámetros en caso que sea necesario
Punto 4) Este punto nos permite definir nuestro propio main basándonos en el ambiente de ejecución
sobre el que estemos trabajando, definiendo por ejemplo un void main.
Si bien nosotros seguiremos trabajando en el manual con la forma int main(), es bueno que se esté
informado respecto a lo que indican los actuales estándares.
33
La recursión en C no difiere en nada con la vista tanto en pseudocódigo como en Pascal, basta con
adaptarse al manejo de la sintaxis de C y su comportamiento. Lo único que necesitamos es entender el
concepto de recursión y cómo aplicarlo.
#include<stdio.h>
#include<conio.h>
//MAIN
int main(){
int num;
printf("Calculo de factorial mediante funcion recursiva\n");
printf("Ingrese un numero: ");
scanf("%d", &num);
printf("El factorial de %d es: %d", num, factorial(num));
_getch(); //Invoco a la función recursiva
_clrscr();
return 0;
}
//FUNCION RECURSIVA
int factorial(int x){ //Paso el número que quiero calcular por copia.
if (x==0){ //Primero chequeo el caso base, en el factorial es x=0.
return 1; //El factorial de 0 es 1.
}
else{ //Si no llegue a 0, invoco recursivamente a factorial con x-1
return x*factorial(x-1); //IMPORTANTE: Esta es la llamada recursiva a la
} //función, es decir, una auto invocación.
}
No hay más que agregar respecto a recursión. La lógica no cambia de un lenguaje a otro, sólo la sintaxis
para que el lenguaje pueda entender lo que se quiere lograr.
Ya vimos, en el Capítulo 8.1 de Parámetros, cómo se envían estos a un módulo y como los recibe y opera
el mismo. Veremos ahora como trabajar con tipos estructurados de datos, especialmente los arreglos que
tienen un manejo particular.
34
35
int main(){
int vecNum[5]; //Defino el arreglo
inicializarVecReferencia(vecNum); //Invoco al procedimiento que modifica por referencia
printf("Arreglo modificado por referencia");
_getch();
_clrscr();
for (int i=0; i<5; i++){ //Muestro los valores del arreglo tras la inicialización
printf("Valor del arreglo en posición %d: %d\n", i, vecNum[i]);
_getch();
}
inicializarVecCopia(vecNum); //Invoco al procedimiento que modifica una copia
_clrscr();
printf("Arreglo modificado por copia");
_getch();
_clrscr();
for (int i=0; i<5; i++){ //Vuelvo a mostrar los valores del arreglo
printf("Valor del arreglo en posición %d: %d\n", i, vecNum[i]);
_getch(); //Modifiqué el arreglo con valor 1, pero como era copia
} //no se reflejó fuera del procedimiento
_clrscr();
return 0;
}
Si se ejecuta el código anterior, se verá que el valor del arreglo al inicializarse por referencia es 0, y al
modificarse por copia sigue siendo 0, por lo que realizamos efectivamente un “pasaje por copia”.
36
Por otro lado, tenemos el pasaje de registros. A diferencia de los arreglos, los registros no tienen casos
particulares que evaluar y es mucho más simple su pasaje por copia o referencia.
El pasaje por referencia se realiza, al igual que con los pasajes de tipos nativos, enviando la dirección en
memoria del registro y operándolo como un puntero, con la particularidad que debemos definir el registro.
Como se aclaró en el comentario del código, nótese que al campo DNI del puntero al registro no
accedemos con un punto como hacemos normalmente con registros, ni tampoco con el asterisco como
hacemos cuando se trata de punteros. Esto es así porque tenemos un símbolo reservado especialmente
para los punteros a registro: la flecha “->”. Lógicamente, como una lista no es más que un conjunto de
registros referenciados por puntero, este símbolo también se utilizará con ellas.
Al igual que con arreglo, verificaremos la diferencia entre pasaje por copia y por referencia con el
siguiente ejemplo:
37
int main(){
struct Persona per; //Defino la variable tipo Persona
modificarReferencia(&per); //Invoco al módulo con per por referencia
printf("DNI modificado por referencia: %d", per.dni);
_getch(); //Verificamos que efectivamente el DNI vale 0
_clrscr();
modificarCopia(per); //Invoco al módulo con per por copia
printf("DNI modificado por copia: %d", per.dni);
_getch(); //El DNI sigue valiendo 0, porque fue por copia
_clrscr();
return 0;
}
38
A estas alturas ya hemos hecho un extenso uso de punteros en los módulos dada la forma en que se
envían por referencia parámetros en C, por lo que su concepto y sintaxis deberían estar bastante
familiarizados. Se explicará puntero nuevamente en caso que no haya quedado del todo claro.
Un puntero, como ya sabemos, es una variable cuyo contenido no es un dato, sino una dirección de
memoria que apunta a un dato y con la cual puede modificarse el mismo. Los punteros en C se definen
con el símbolo asterisco (*), y para asignarle la dirección de una variable a un puntero debemos usar el
ampersand para obtener dicha dirección.
El valor de dirección nulo en C no es nil, como sucede en Pascal. En cambio, en C se usa el valor numérico
0 (cero) para definir que un puntero no apunta a ninguna dirección o, lo que es lo mismo, que tiene valor
nulo.
#include<stdio.h>
#include<conio.h>
int main(){
printf("Ejemplo de uso de punteros\n");
printf("Ingrese un numero: ");
scanf("%d", &num); //Lectura normal de un valor
_clrscr();
puntero=# //Asigno a la variable puntero la dirección de num. Nótese el
//uso del ampersand
printf("Ingreso: %d\nLa variable puntero contiene: %d", num, *puntero); //Muestro el contenido
_getch(); //de num y del puntero
_clrscr(); //El puntero se accede con asterisco (*)
*puntero=10; //Modificamos la información de num a través del puntero a su
//dirección de memoria
printf("Ahora la variable 'num' tiene valor: %d", num); //Verificamos que efectivamente se
_getch(); //modificó el valor
_clrscr();
return 0;
}
39
Muchas veces necesitaremos liberar la memoria direccionada por nuestro puntero. Para ello existe la
función nativa “free()”:
Los punteros, al enviarse como parámetros, tienen un comportamiento muy particular que debemos
explicar en profundidad de detalles.
Digamos que enviamos un puntero a una variable entera por parámetro y modificamos el valor
apuntado, es decir, usamos asterisco.
#include <stdio.h>
#include <conio.h>
int main(){
int x=5, *punteroNum; //Declaro un entero y un puntero a entero
punteroNum=&x; //Apunto el puntero a la variable entera.
printf("Valor del puntero: %d\n", *punteroNum); //El puntero muestra 5
_getch();
modificarPuntero(punteroNum); //Modifico el valor de x a través del puntero
printf("Valor del puntero modificado: %d", *punteroNum);
_getch();
return 0;
}
El valor será modificado dado que sería como enviar un parámetro por referencia.
Digamos que enviamos el mismo puntero por parámetro, pero ahora modificamos sin el asterisco ya que
queremos modificar la dirección a la que apunta ese puntero.
40
int main(){
int x=5, *punteroNum; //Declaro un entero y un puntero a entero
punteroNum=&x; //Apunto el puntero a la variable entera.
printf("Valor del puntero: %d\n", *punteroNum); //El puntero muestra 5
_getch();
modificarPuntero(punteroNum); //Modifico la dirección del puntero
printf("Valor del puntero modificado: %d", *punteroNum);
_getch(); //El valor mostrado sigue siendo 5, no cambió
return 0;
}
Este último caso no funcionará, dado que C, como ya aclaramos, no permite pases por referencia, sólo
por copia. Es decir, si enviamos un puntero como parámetro, el dato al que apunta podrá modificarse,
pero el valor que contiene el puntero es un valor duplicado del original y cualquier cambio realizado sobre
él no se reflejará fuera del módulo.
Para el caso de un puntero en particular podría no darse mucha importancia al hecho de no poder
modificar hacia dónde apunta desde un módulo. Pero recordemos que una lista también es un puntero, y
si quisiéramos modificar una lista mediante un módulo enviando su cabecera o raíz, por lo planteado
anteriormente, no sería posible.
Entonces, ¿es imposible modificar un puntero o lista en un módulo en C? No, existen formas de poder
modificar la dirección apuntada por el puntero.
Una de ellas consiste en definir el puntero como variable global, con lo cual no tendríamos que enviarlo
como parámetro y podríamos operarlo desde cualquier módulo en nuestro ejecutable. Esta forma resulta
bastante sencilla, pero tiene como contraparte que el uso de variables globales puede facilitar la confusión
y la aparición de errores al codificar.
El ejemplo anterior podría resolverse entonces con la siguiente redefinición del puntero:
41
int main(){
int x=5; //Declaro un entero
punteroNum=&x; //Apunto el puntero a la variable entera.
printf("Valor del puntero: %d\n", *punteroNum); //El puntero muestra 5
_getch();
modificarPuntero(); //Modifico la dirección del puntero
printf("Valor del puntero modificado: %d", *punteroNum);
_getch(); //El programa falla porque el puntero ahora vale nil
return 0;
}
void modificarPuntero(void){
punteroNum=0; //No uso asterisco para modificar la dirección del puntero
} //Además, uso el nombre global
Si ejecutamos este código, notaremos que el programa falla en la ejecución. Es lógico, estamos queriendo
mostrar un valor apuntado por p, siendo que p vale nil (0) tras la ejecución del módulo. Lo importante en
este ejemplo es que antes p apuntaba a x, y luego paso a apuntar a nil, lo que quiere decir que
efectivamente logramos modificarlo.
Otra forma de modificar un puntero es enviar como parámetro un puntero al puntero, es decir, enviar la
dirección en memoria de nuestro puntero, y recibirla como un puntero que apunta al puntero definido en
main. Este juego de palabras puede ser confuso, pero se entenderá mejor con el siguiente ejemplo:
42
¿Qué hicimos en el ejemplo anterior? Definimos el parámetro con doble asterisco, dado que queremos
enviar la dirección de memoria de un puntero, que es otra dirección de memoria. Asimismo, cuando
invocamos el módulo le pasamos como parámetro la dirección de memoria de nuestro puntero con el
ampersand.
Finalmente, en el módulo, operamos el puntero con un solo asterisco. Si utilizamos un solo asterisco,
estamos operando sobre la dirección del puntero que enviamos por parámetro. Si en cambio utilizamos
dos asteriscos, estaremos modificando la variable x a la que apunta.
43
Se usará en el manual esta segunda forma (puntero de puntero) para trabajar dado que es más eficiente
y refleja efectivamente un “pasaje por referencia” del puntero, tal cual se ve en Algoritmos y Estructuras
de Datos.
44
En Pascal definimos una lista y las variables para un uso básico de la forma:
program ejemploLista;
uses crt;
type
//Defino el registro persona
persona=record
nombre: string;
apellido: string;
anioNacimiento: integer;
end;
//Defino la lista y su nodo
lista=^nodo;
nodo=record
dato: persona;
ps: lista;
end;
var
l:lista; //Variable tipo lista
p:persona; //Dato para cargar
45
En C es:
#include <stdlib.h>
...
...
struct Lista *nuevoNodo = malloc(sizeof(struct Lista)); //Declaro y genero el nuevo nodo
Repasemos este ejemplo. Antes que nada, vemos que hemos incluido una nueva librería a nuestro
código, <stdlib.h>, la cual, como ya explicamos al principio en el Capítulo 1.3, agrega funciones para operar
sobre la memoria dinámica y procesos. Una función incluida en esta librería es malloc() (memory
allocation), la cual se encarga de reservar una cantidad de bits en memoria generando un bloque al que
apuntará nuestro nodo o lista.
Otra función que vemos en el código es sizeof(). Esta función retorna el tamaño en bits del parámetro
que se le envía, en este caso un registro.
Entonces, ¿qué está haciendo C con la línea de código anterior? Aunque parezca algo engorroso, notarán
que si lo analizamos es en realidad bastante sencillo. Leído de derecha a izquierda, primero obtenemos el
tamaño en bits de nuestro registro que es la lista, luego reservamos un bloque de memoria con ese tamaño
exacto y, por último, le indicamos a nuestro puntero que apunte a dicho bloque reservado por malloc. El
resultado final será el mismo que con el new de Pascal, pero queda en evidencia todo lo que ocurre detrás
de dicha función. Como ya vimos con el var de los parámetros, lo que en Pascal se hace de manera sencilla
con una función o palabra clave, nos lleva algo más de trabajo en C dado el bajo nivel sobre el que opera.
Para asignarle valor a un nodo de la lista, trabajamos de la misma manera que los registros cuando se
envían por referencia: se utiliza la flecha “->” para acceder al campo. Entonces, si quisiera crear y cargar
un nuevo nodo de la lista anterior, lo haría de la forma siguiente:
46
Para el ejemplo anterior suponemos que p ya estaba cargado con datos y listo para ser agregado al campo
dato de la lista.
También se podría cargar datos directamente desde el usuario en la lista, si así lo quisiera:
Como se aclara en los comentarios, recordar siempre el uso del ampersand cuando los campos a cargar
no sean arreglos o punteros.
Todo lo que vimos respecto a pasaje como parámetro de punteros en el Capítulo anterior se aplica de la
misma forma a las listas, lo cual resulta lógico ya que ,en definitiva, una lista puede reducirse a una cadena
enlazada de punteros.
Al igual que con punteros, utilizaremos la segunda opción dado que es así como se enseña el manejo de
listas normalmente y porque considero es la manera más eficiente de trabajar con las mismas.
void inicializar(struct Lista **l){ //Envío una lista de tipo “Lista” por referencia
*l=0; //Hago nil la lista
}
47
Como vemos, es prácticamente idéntico a cuando trabajamos con punteros. Veamos ahora un módulo
de inserción al final de la lista, considerando que ya recibimos un registro dato de tipo Persona cargado
con información.
void insertar(struct Lista **l, struct Persona p){ //Envío por referencia la lista y por
//copia un registro
struct Lista *aux; //Defino el aux para moverme por l
struct Lista *nuevo = malloc(sizeof(struct Lista)); //Defino el nuevo nodo
nuevo->dato=p; //Asigno el dato al nuevo nodo, en este caso una Persona
nuevo->ps=0; //El puntero siguiente es nil
if (*l==0){ //Si la lista está vacía, inserto al principio
*l=nuevo;
}
else{ //Sino, me muevo hasta el final y lo inserto
aux=*l; //Asigno un aux para no modificar a la cabeza l
while(aux->ps!=0){ //Recorro aux hasta el final de la lista
aux=aux->ps;
}
aux->ps=nuevo; //Realizo la inserción
}
}
Una aclaración importante: supongamos que tenemos un procedimiento A al cual le pasamos por
referencia nuestra lista, y queremos pasar dicha lista por referencia a un procedimiento interno B. Se
hará de la siguiente forma:
int main(){
struct Lista *L; //Definimos nuestra lista.
procedimientoA (&L); //Envío la dirección de memoria de L.
}
48
De esta forma, se puede enviar por referencia a nuestra lista ya referenciada en nuestro procedimiento
actual.
10.3. Aplicación
Con todo lo visto de lista hasta ahora y de C en general, podemos hacer un cierre de contenido con una
aplicación que nos permita realizar ABM y consulta de una lista determinada. Para ello, tendremos
distintos módulos que nos permitan operar sobre la lista y un módulo principal main con un menú de
opciones que nos permita elegir qué tareas ejecutar. Planteamos para esta aplicación el siguiente
enunciado:
Realizar una aplicación que permita agregar, modificar y eliminar productos de una lista. Un
producto está compuesto por código, nombre y stock del mismo. La lista deberá estar ordenada
por el código de producto. Además, se deberá poder mostrar todos los elementos de la lista,
consultar por uno específico mediante su código, y obtener el total de productos cargados. El
módulo principal deberá contar con un menú de opciones para navegar por la aplicación.
Pasemos a definir uno por uno los módulos del programa y el principal.
49
- Inicializar lista
void inicializarLista (struct Lista **l){
*l=0; //Lista en NIL
}
- Insertar ordenado
void insertarOrdenado(struct Lista **l, struct Producto p){
struct Lista *nuevo=malloc(sizeof(struct Lista)); //Defino el nuevo nodo
struct Lista *ant; //Defino el nodo anterior
struct Lista *act; //Defino el nodo actual
nuevo->dato=p; //Cargo producto en nuevo
nuevo->ps=0; //Inicializo ps de nuevo en NIL
if (*l==0){ //La lista está vacía, no hace falta moverme
*l=nuevo;
}
else{ //La lista no está vacía, busco el punto de inserción
ant=0; //Puntero al nodo anterior
act=*l; //Puntero al nodo actual
while ((act!=0) && (act->dato.codigo < nuevo->dato.codigo)){
ant=act;
act=act->ps;
}
if (ant==0){ //Si ant es NIL, inserto al principio
nuevo->ps= act;
*l=nuevo;
}
else{ //Debo insertar en un punto medio o al final
nuevo->ps=act;
ant->ps=nuevo;
}
}
}
50
51
52
- Buscar producto
void buscarProducto(struct Lista *l){
int cod;
if(l==0){
printf("La lista esta vacia");
_getch();
}
else{
printf("Ingrese codigo del producto: ");
scanf("%d", &cod);
_clrscr();
while((l!=0) && (l->dato.codigo != cod)){
l=l->ps;
}
if(l==0){ //Si es NIL, no encontró un producto de código cod
printf("El producto de codigo %d no existe", cod);
}
else{ //Encontró el producto, lista sus datos
printf("Datos del producto:\n");
printf("--------------------\n");
printf("Codigo: %d\n", l->dato.codigo);
printf("Nombre: %s\n", l->dato.nombre);
printf("Stock: %d\n", l->dato.stock);
}
_getch();
}
_clrscr();
}
53
54
55
Esta aplicación nos sirve tanto para cierre de listas como para cierre general de todo lo visto hasta
ahora en C.
56