Herramientas de usuario

Herramientas del sitio


principios

Ejemplo de programa en C

El programa de más abajo muestra los primeros n números de la serie de Fibonacci. El programa se encuentra almacenado en el archivo fib.c:

// fib.c: Calcula los primeros n numeros de fibonacci
 
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char *argv[]) {
  int n= atoi(argv[1]);
  int prev= 0;
  printf("f0= 0\n");
  int curr= 1;
  int next;
  printf("f1= 1\n");
  for (int i= 2; i<n; i++) {
    next= prev+curr;
    printf("f%d= %d\n", i, next);
    prev= curr;
    curr= next;
  }
  return 0;
}

Para poder ejecutarlo se requiere compilarlo previamente con el comando gcc (el compilador de C). Esto se hace en Linux en una ventana de comandos (o shell de comandos) mediante:

$ gcc -std=c99 fib.c -o fib
$

Que no aparezca ningún mensaje de diagnóstico significa que el programa está bien escrito. La opción -std=c99 especifica que la versión de C requerida es C99, un estándar que data de 1999 y que usaremos en este curso. La opción -o fib le indica al compilador el nombre del archivo en donde debe almacenar la versión binaria del mismo programa codificada en lenguaje de máquina. Usualmente no se usa una extensión para los archivos en lenguaje de máquina.

El programa se ejecuta por ejemplo en el shell de comandos con:

$ ./fib 8
f0= 0
f1= 1
f2= 1
f3= 2
f4= 3
f5= 5
f6= 8
f7= 13
$

Ejercicio: copie este mismo programa en un archivo fib.c en algún computador con Linux. Compílelo y ejecútelo exactamente como se indica más arriba.

Formato de un programa en C

  • Un archivo fuente escrito en el lenguaje C debe llevar la extensión '.c'.
  • A partir de C99 un comentario se inicia con '//' y se extiende hasta el final de la línea.
  • El estilo antiguo de los comentarios (antes de C99) sigue siendo válido y se inician con /* y terminan con */. Pueden extenderse por varias líneas. Algunos ejemplos en estos apuntes usan ese antiguo estilo.
  • Las líneas con #include cumplen el mismo propósito que los import de Python. Indican el nombre de un archivo de encabezados. Por ejemplo stdio.h se necesita para poder usar la función printf y stdlib.h para usar la función atoi. La extensión .h viene de header (encabezado). No se preocupe, el compilador sabrá donde encontrar los archivos de encabezado.
  • En el archivo se define la función main, pero se pueden definir múltiples funciones, una a continuación de la otra.
  • Al ejecutar un programa la primera función que se invoca es siempre la función main.
  • El compilador ignora los espacios en blanco, tabs y cambios de línea. Agréguelos para hacer más legible el programa. Al contrario de Python la indentación no le indica al compilador la estructura del programa. Veremos que son las llaves { } las que definen la estructura del programa. Pero por legibilidad del programa, es importante que la estructura que entregan las llaves sea consistente con la estructura que sugiere la indentación.

El sistema de tipos de C

El lenguaje C posee tipos estáticos. Esto significa que para cada variable hay que indicar explícitamente su tipo, el que acota los valores que puede tomar esa variable. Si una variable es de tipo int entonces solo puede almacenar un entero como 0, 3, -5, etc. No podrá almacenar el real 3.14 o el string “hola que tal”. Al contrario, Python posee tipos dinámicos y por lo tanto una variable puede almacenar cualquier tipo de valor. Algunos tipos muy usados en C son:

  • int para los números enteros: 0, 3, -5, etc.
  • char para enteros pequeños que usualmente corresponden a la codificación en ascii de un caracter: 'a', '4', '*', '\n', etc. La notación 'a' significa la codificación en ascii de la letra a y corresponde al entero 97. El caracter especial \n corresponde al cambio de línea (line feed) y en ascii se representa con el entero 10.
  • char* para strings: “hola que tal”, “123”, “+-*”, etc.
  • double para números reales: 3.14, 2.71, 1.0, -300.2, 10.3e6, etc.
  • void se usa para indicar que una función no entrega ningún valor.

En C también existen los arreglos. Por ejemplo int[] es el tipo de los arreglos de C y char*[] es el tipo de los arreglos de strings.

Los tipos dinámicos de Python le otorgan flexibilidad y comodidad. En cambio los tipos estáticos de C (y también Java) lo hacen eficiente en tiempo de ejecución y además hace que los programas en C sean más fáciles de entender y modificar.

Formato de una función en C

Su formato es: tipo nombre ( parámetros ) { instrucciones }

  • tipo es el tipo de los valores que retorna la función. Usualmente int, char, char*, double o void.
  • parámetros indica los parámetros que recibe la función separados por coma. Cada parámetro viene en el formato: tipo variable.
  • instrucciones son 0, 1, 2 o más instrucciones. Para ejecutar una función, C ejecuta secuencialmente las instrucciones.

Por ejemplo el formato para main, exceptuando las instrucciones, es:

int main(int argc, char *argv[]) {
  ... etc. ...
}

La función main recibe los parámetros argc y argv. Tome esto como un receta por ahora. El tipo de argc es int y el de argv es char*[] (arreglo de strings). Durante el curso se explicará esto.

C posee instrucciones simples como la asignación e instrucciones compuestas como los ciclos, los if/then/else y los grupos de instrucciones encerradas con llaves { }.

Instrucciones simples

Las instrucciones simples siempre terminan con un punto y coma. Una instrucción puede abarcar varias líneas o también se pueden colocar varias instrucciones en una misma línea, porque recuerde que los cambios de línea son ignorados por el compilador. Sin embargo por legibilidad se recomienda colocar a lo más una sola instrucción por línea.

En el programa de ejemplo observamos 4 categorías de instrucciones simples: declaración de variables, asignación, invocación de funciones y retorno de función.

Declaración de variables

Su sintaxis es: tipo nombre = expresión ;

Tipo especifica el tipo de los valores que almacena la variable. Nombre es el identificador de la variable. La parte = expresión es opcional y especifica el valor inicial de la variable. En expresión se pueden usar los operadores aritméticos típicos (+ - * /) e incluso invocar otras funciones (siempre y cuando no retornen void).

En el ejemplo se declara la variable prev de tipo int con valor inicial 0:

int prev = 0;

Se observa que para la variable next no se indica un valor inicial:

int next;

En tal caso hay un valor inicial desconocido. Usar esta variable antes de asignarle un valor concreto es típicamente un error y algunos compiladores hacen un esfuerzo por diagnosticar el problema.

Es posible declarar varias variables en una misma instrucción separando las variables con una coma. Por ejemplo se pudo declarar prev, curr y next en una sola instrucción:

int prev = 0, curr= 1, next;

Yo suelo declarar varias variables en una sola instrucción, aunque algunos expertos opinan que es menos legible. Después veremos que tiene sus riesgos.

Otro ejemplo de declaración es la de la variable n de tipo int:

int n= atoi(argv[1]);

El valor inicial es la expresión atoi(argv[1]). La variable argv es un arreglo de strings que contiene los parámetros especificados en el shell de comandos. Es decir es el “8” que viene a continuación de ./fib en la línea de comandos. En este caso es solo el string “8”. Por razones que se explicarán más tarde en el curso, este se encuentra en el elemento con índice 1. Si hubiesen más parametros, se ocuparían los índices 2, 3, etc. El número concreto de parámetros + 1 se almacena en la variable entera argc. Por lo tanto argv[1] es el string “8”. La función atoi convierte el string “8” al entero 8, que se convierte en el valor inicial de n.

Ejercicio: ¿Como declararía una variable que almacena el número PI?

Asignación

Sintaxis: variable = expresión ;

Sirve para cambiar el valor de una variable. En el ejemplo:

next= prev+curr;

El lado derecho del igual puede ser una expresión complicada con múltiples operadores e invocaciones de otras funciones. Recuerde que las instrucciones simples siempre terminan con un ;. El cambio de línea no le dice nada al compilador.

Invocación de una función

Algunas funciones son de tipo void, es decir no retornan ningún valor y por lo tanto no tiene sentido invocarlas como parte de una asignación. Para invocarlas se usa esta sintaxis:

nombre ( argumento_1, argumento_2, … ) ;

En donde argumento_1, argumento_2, … son expresiones. Por ejemplo:

printf("f0= 0\n");
printf("f%d= %d\n", i, next);

Acá la función se llama printf (print formatted) y cumple el mismo propósito que print en Python. Es una función especial en C porque recibe un número variable de argumentos. En el primer ejemplo recibe un solo argumento que corresponde al string que se desea desplegar literalmente en pantalla. En el segundo ejemplo printf recibe 3 argumentos. El primer parámetro de printf es siempre el formato. Debe ser un string que se despliega casi literalmente. Digo casi, porque la función examina el string buscando el caracter %, dándole un significado especial: el primer %d se reemplaza por el valor de la variable i y el segundo %d se reemplaza por el valor de next. Entonces cuando i es 6 y next es 8 se reemplaza el primer %d por 6 y el segundo %d por 8 desplegando f6= 8.

En realidad en C la invocación de función es válida porque es válida cualquier instrucción con la sintaxis: expresión ;

De hecho la asignación también es un caso particular de esa sintaxis porque = se considera un operador como + y *.

Retorno de función

Para terminar la ejecución de una función se usa la instrucción return, que usa la siguiente sintaxis:

return expresión ;

El valor retornado es el resultado de evaluar la expresión. No es necesario que sea la última instrucción de la función, pero cuando se ejecuta return, las instrucciones que vienen a continuación no se ejecutarán. Por eso nunca verá instrucciones después del return, a menos que el return sea una de las ramas de un if.

Instrucciones compuestas

Se forman a partir de una o más instrucciones (simples o compuestas). Pueden ser ciclos while, for, o condicionales if/then/else o agrupación de instrucciones con { }.

Ciclo while

La sintaxis es: while (condición) instrucción

Es como el while de Python: se evalúa la condición, si es verdadera se ejecuta la instrucción. Esto se repite hasta que la condición sea falsa. Por ejemplo:

int a= 1;
while ( a < 100 )
  a = a * 2;

El último valor que tomará a es 128, porque cuando a sea mayor o igual que 100, ya no se ejecutará la instrucción a=a*2;

Observe que la indentación no indica cuantas instrucciones están dentro del while. Siempre es una sola instrucción. En el siguiente ejemplo la instrucción a=a+1 está fuera del ciclo:

int a= 1;
while ( a < 100 )
  a = a * 2;
  a = a + 1;  // ¡No haga esto por favor!

El valor final de a será 129. La asignación a=a+1 se ejecuta una sola vez después de que a sea 128. Este código está mal escrito porque está mal indentado. La indentación no refleja la estructura entendida por el compilador. Dependiendo de las opciones de compilación, el compilador podría entregar una advertencia del problema, pero no está obligado a hacerlo. Ud. no lo haga. La indentación correcta del programa es:

int a= 1;
while ( a < 100 )
  a = a * 2;
a = a + 1;  // ¡Bien!

Esta es una ventaja de Python. Dado que la indentación señala a Python cuál es la estructura del programa no puede haber inconsistencia.

Agrupación de instrucciones

Sintaxis: { instrucción instrucción … etc. }

Se usa para colocar varias instrucciones en instrucciones compuestas que aceptan solo una instrucción. Para ejecutar una agrupación, sus instrucciones se ejecutan secuencialmente en el orden en que aparecen. Por ejemplo si realmente se necesita que a=a+1 esté dentro del while se debe agrupar las 2 asignaciones con las llaves:

int a= 1;
while ( a < 100 ) {
  a = a * 2;
  a = a + 1;  // ¡Bien!
}

Ejercicio: indique en donde se usa agrupación de instrucciones en el programa para fibonacci.

La indentación usada es importante, aunque al compilador le de lo mismo. Esta es la indentación de Kernighan y es la que Ud. debe usar obligatoriamente en este curso: el símbolo { debe ir al final de la línea del while y el } en una línea aparte, en la misma columna de la w del while. Los siguientes estilos de indentación serán penalizados con décimas menos en controles y tareas:

// ¡No use estos estilos de indentación!
while ( a < 100 )        while ( a < 100 )      while ( a < 100 ) {
{                          {                      a = a * 2;
  a = a * 2;               a = a * 2;             a = a + 1; }
  a = a + 1;               a = a + 1;
}                          }

El estilo de la izquierda sí se usa ampliamente y es legible. El estilo del medio casi no se usa. El estilo de la derecha es horrible y merece la reprobación. El principio de la indentación es hacer los programas más legibles. Pueden haber muchos estilos bonitos y legibles, pero si cada alumno usa estilos distintos, será más difícil que el equipo docente entienda sus programas y por lo tanto podrían recibir una calificación injusta porque no se entendió su programa. Por ello urge acordar un solo estilo y este será el de Kernighan.

El ciclo for

Sintaxis: for ( inicio; condición ; expresión ) instrucción

Se usa mucho para hacer ciclos. Es equivalente a:

{
  inicio;
  while ( condición ) {
    instrucción
    expresión;
  }
}

Ejemplo:

  for (int i= 2; i<n; i++)
    next= prev+curr;

Ejercicio: reescriba la instrucción equivalente usando while.

Ejecución condicional

Sintaxis 1: if ( condición ) instrucción

Significado: instrucción se ejecuta cuando condición es verdadera. Si es falsa, no se ejecuta ninguna instrucción.

Sintaxis 2: if ( condición ) instrucción_1 else instrucción_2

Significado: instrucción_1 se ejecuta cuando la condición es verdadera. En tal caso no se ejecuta instrucción_2. Si la condición es falsa, solo se ejecuta instrucción_2.

Ejemplo:

int a= 15, b= 35;
if (a<b)
  b = b-a;
else
  a = a-b;

Ejercicio: ¿Qué debe hacer cuando debe ejecutar varias instrucciones cuando la condición es verdadera?

Trabajo personal

Debe realizar el siguiente trabajo personal antes de la segunda clase de este curso (clase del jueves): estudie y complete los ejercicios de la parte Learn the Basics de este tutorial del lenguaje C. No haga la última sección Static y tampoco la parte Advanced. Es un tutorial muy corto que va un poco más allá de lo que contiene esta sección. Además la misma página compila y ejecuta las soluciones que Ud. dará para cada ejercicio, ayudándole a consolidar lo aprendido.

Ejemplo con varias funciones: quicksort

Cree el archivo qsort.c con el siguiente contenido:

#include <stdio.h>
#include <stdlib.h>
 
void swap(double v[], int i, int j); // nota 1: encabezado de funcion
 
void quicksort(double a[], int left, int right) {
  if (left>=right)
    return;  // nota 2: retorno de funcion de tipo void
 
  swap(a, left, (left+right)/2); // nota 3: uso previo a definicion
  int last= left;
 
  //  +--+-----------+--------+--------------+
  //  |  |///////////|\\\\\\\\|              |
  //  +--+-----------+--------+--------------+
  //  left        last         i         right
 
  for (int i= left+1; i<=right; ++i) { // nota 4: legibilidad
    if (a[i]<a[left])
      swap(a, ++last, i);
  }
  swap(a, left, last);
 
  quicksort(a, left, last-1);  // nota 5: recursividad
  quicksort(a, last+1, right);
}
 
void swap(double v[], int i, int j) { // nota 6: definicion posterior
  double tmp= v[i];
  v[i]= v[j];
  v[j]= tmp;
}
 
int main(int argc, char *argv[]) {
  int n= argc-1;
  double a[n]; // nota 7: declaracion de arreglo
 
  for (int i= 0; i<n; i++)
    a[i]= atof(argv[i+1]);
 
  quicksort(a, 0, n-1);
 
  for (int i= 0; i<n; i++)
    printf("%g ", a[i]); // nota 8: despliegue de reales
  printf("\n");
 
  return 0; // nota 9: retorno de función no //void//
}

Notas:

  1. Esto se denomina declaración de encabezado de función. Se requiere porque la función swap se usa en la función quicksort antes de su definición. Sin este encabezado el compilador reclamaría. El encabezado es similar a una definición de función, pero la parte { instrucciones } se reemplaza por punto y coma.
  2. Se usa la instrucción return para anticipar el retorno de una función. Como la función es void, no se especifica el valor retornado.
  3. Acá es donde se usa la función swap. Por eso se incluyó la declaración de su encabezado en la nota 1.
  4. Observe que las llaves no son necesarias acá. Pero su uso le entrega legibilidad al programa.
  5. C es recursivo. La invocación recursiva de funciones es igualmente eficiente que cualquier otra función.
  6. Esta es la definición de swap, que fue usada antes en la nota 3, por lo que se necesitó la declaración de su encabezado en la nota 1.
  7. Esa es la sintaxis para declarar un arreglo de n elementos de tipo double. Los arreglos son de tamaño fijo, lo que significa que una vez que se declaran no pueden crecer. El primer elemento es a[0] y el último a[n-1]. ¡Cuidado! No se verifica el correcto uso de los índices. Si Ud. accede a a[n] el resultado puede ser cualquiera. O peor aún, si modifica a[n] el programa podría terminar en segmentation fault.
  8. Use el formato %g para desplegar reales con printf.
  9. La función main debe retornar obligatoriamente un entero. Este se llama el código de retorno del programa y se puede mostrar en el shell con el comando echo $?. Por convención un valor 0 indica que el programa tuvo éxito. Un valor distinto de 0 significa que ocurrió algún problema.

He aquí cómo compilar y un ejemplo de ejecución con el resultado del programa:

$ gcc -std=c99 qsort.c -o qsort
$ ./qsort 3 1 5 7 4
1 3 4 5 7 
$ echo $?
0
$

Ejercicios:

  • Modifique el programa de modo que muestre los números descendentemente (ahora son mostrados ascendentemente).
  • Modifique el programa de modo que cada número se muestre en una línea independiente.
  • Modifique el programa de modo que el código de retorno sea 1 cuando el usuario olvidó especificar los elementos a ordenar (es decir cuando n=0).

Ejercicio final: factorial

  • Escriba en un archivo fact.c un programa que calcule recursivamente el factorial de un entero.
  • El resultado debe ser un número real.
  • Despliegue el resultado en pantalla con printf.

Ejemplo de uso:

$ ./fact 6
720
$ echo $?
0
$

Opcional

En el curso Metodologías de Diseño y Programación deberá estudiar el lenguaje Java. Se puede considerar Java como un sucesor de C y por lo tanto gran parte de lo que estudie en este curso le servirá también para aprender Java. Los tipos y la sintaxis son parecidos. Opcionalmente puede leer este comparativo entre C y Java.

principios.txt · Última modificación: 2022/09/27 10:01 por lmateu