Tips de Programación

By Abner Roa | Created: November 22, 2023 | Last updated: December 18, 2024 | Read Time: 19 minutes

Prof. Raphael A. Finkel

Original article: https://www.cs.uky.edu/~raphael/programming.html

Se espera que los alumnos de las clases de CS de los niveles junior y senior conozcan algunas nociones básicas sobre C, Unix y herramientas de desarrollo de software. En esta página se detallan algunos de estos conocimientos y se proponen algunos ejercicios.

C

variables globales, cadenas, búferes, asignación dinámica, enteros, disposición de structs, punteros, salida, parámetros de línea de comandos, características del lenguaje, múltiples archivos fuente, enlace de múltiples archivos objeto, depuración

  • C permite declarar variables fuera de cualquier procedimiento. Estas variables se denominan variables globales.
    • Una variable global se asigna una vez cuando se inicia el programa y permanece en memoria hasta que el programa termina.
    • Una variable global es visible para todos los procedimientos del mismo archivo.
    • Puede hacer que una variable global declarada en el fichero A.c sea visible para todos los procedimientos de otros ficheros B.c, C.c, D.c, ... declarándola con el modificador extern en los ficheros B.c, C.c, D.c, ... como en este ejemplo:
extern int theVariable
    • Si tiene muchos archivos que comparten la variable, debe declararla extern en un archivo de cabecera foo.h (descrito a continuación) y utilizar #include foo.h en los archivos B.c, C.c, D.c, ..... Debes declarar la variable exactamente en un fichero sin el modificador extern o nunca será asignada.
    • No es una buena idea usar demasiadas variables globales, porque no puedes localizar los lugares en los que se accede a ellas. Pero hay situaciones en las que las variables globales te permiten evitar pasar muchos parámetros a las funciones.
  • Las cadenas son punteros a matrices de caracteres terminadas en cero.
    • Se declara una cadena como char *nombreVariable.
    • Si tu cadena es constante, puedes asignarle un literal de cadena:
char *myString = "This is a sample string";
    • Una cadena vacía sólo tiene el terminador nulo:
myString = ""; // empty string, length 0, containing null
    • Un puntero nulo no es un valor de cadena válido:
myString = NULL; // invalid string
    • Puede utilizar este valor para indicar el final de una matriz de cadenas:
argv[0] = "progName";
argv[1] = "firstParam";
argv[2] = "secondParam";
argv[3] = NULL; // terminator
    • Si tu cadena se computa en tiempo de ejecución, necesitas reservar suficiente espacio para contenerla. El espacio debe ser suficiente para contener el null al final:
char *myString;
myString = (char *) malloc(strlen(someString) + 1); // allocate space
strcpy(myString, someString); // copy someString to myString
    • Para evitar fugas de memoria, debes devolver eventualmente el espacio que asignas con malloc utilizando free. Su parámetro debe ser el principio del espacio devuelto por malloc:
free((void *) myString);
    • Para mantener tu código limpio y legible, deberías llamar a free() en el mismo procedimiento en el que llamas a malloc(); puedes llamar a otros procedimientos entre esos dos puntos para manipular tu cadena.
    • Si está copiando cadenas, debe tener mucho cuidado de no copiar nunca más bytes de los que puede contener la estructura de datos de destino. Los desbordamientos de búfer son la causa más común de fallos de seguridad en los programas. En particular, considere el uso de strncpy() y strncat() en lugar de strcpy() y strcat().
    • Si está utilizando C++, necesita convertir sus objetos de cadena en cadenas de estilo C antes de pasarlos en una llamada al sistema.
string myString // This declaration only works in C++
...
someCall(myString.c_str())
    • Desafortunadamente, c_str() devuelve una cadena inmutable. Si necesita una cadena mutable, puede copiar los datos utilizando strcpy() (como se indica más arriba) o puede convertir el tipo:
someCall(const_cast<char *>(myString.c_str()))
    • Casting no es tan seguro como copiar, porque someCall() podría modificar la cadena, lo que confundiría a cualquier parte del programa que asuma que myString es constante, que es el comportamiento habitual de las cadenas C++.
  • Un buffer es una región de memoria que actúa como contenedor de datos. Aunque los datos puedan tener una interpretación (como un array de structs con muchos campos), los programas que leen y escriben buffers suelen tratarlos como arrays de bytes. Una matriz de bytes no es lo mismo que una cadena, aunque ambas se declaren char * o char [].
    • Puede que no contengan caracteres ASCII y que no estén terminados en cero.
    • No se puede utilizar strlen() para averiguar la longitud de los datos en un búfer (porque el búfer puede contener bytes nulos). En su lugar, debe averiguar la longitud de los datos por el valor de retorno de la llamada al sistema (normalmente read) que generó los datos.
    • No puede utilizar strcpy(), strcat(), o rutinas relacionadas en buffers de bytes; en su lugar, necesita utilizar memcpy() o bcopy().
    • Puedes escribir un buffer de 123 bytes en un fichero usando un código como este:
char *fileName = "/tmp/foo"
#define BUFSIZE 4096
char buf[BUFSIZE]; // buffer containing at most BUFSIZE bytes
...
int outFile; // file descriptor, a small integer
int bytesToWrite; // number of bytes still to be written
char *outPtr = buf;
...
if ((outFile = creat(fileName, 0660)) < 0) { // failure
	// see file permissions to understand 0660
	perror(fileName); // print cause
	exit(1); // and exit
}
bytesToWrite = 123; // initialization; 123 is just an example
while ((bytesWritten = write(outFile, outPtr, bytesToWrite)) < bytesToWrite) {
	// not all bytes have been written yet
	if (bytesWritten < 0) { // failure
		perror("write");
		exit(1);
	}
	outPtr += bytesWritten;
	bytesToWrite -= bytesWritten;
}
    • Para conseguir que el compilador asigne espacio a los buffers, debes declarar el buffer con un tamaño que el compilador pueda calcular, como en
#define BUFSIZE 1024
char buf[BUFSIZE];
    • Si declaras el buffer sin tamaño:
char buf[];
    • entonces tiene un tamaño desconocido y C no asigna ningún espacio. Esto es aceptable si buf es un parámetro formal (es decir, aparece en la cabecera de un procedimiento); el parámetro real (proporcionado por el invocador) tiene un tamaño. Pero no es aceptable si buf es una variable. Si no conoces el tamaño del buffer en tiempo de compilación, deberías usar código como este
char *buf = (char *) malloc(bufferSize);
    • donde bufferSize es el resultado en tiempo de ejecución de algún cálculo.
  • Puedes asignar y desasignar memoria dinámicamente.
    • Instancias individuales de cualquier tipo:
typedef ... myType;
myType *myVariable = (myType *) malloc(sizeof(myType));
// you can now access *myVariable.
...
free((void *) myVariable);
    • De nuevo, es una buena práctica de programación invocar free() en la misma rutina en la que se llama a malloc().
    • Matrices unidimensionales de cualquier tipo:
myType *myArray = (myType *) malloc(arrayLength * sizeof(myType));
// myArray[0] .. myArray[arrayLength - 1] are now allocated.
...
free((void *) myArray);
    • Las matrices bidimensionales se representan mediante una matriz de punteros, cada uno de los cuales apunta a una matriz:
myType **myArray = (myType **) malloc(numRows * sizeof(myType *));
int rowIndex;
for (rowIndex = 0; rowIndex < numRows; rowIndex += 1) {
	myArray[rowIndex] = (myType *) malloc(numColumns * sizeof(myType));
}
// myArray[0][0] .. myArray[0][numColumns-1] .. myArray[numRows-1][numColumns-1]
// are now allocated.  You might want to initialize them.
...
for (rowIndex = 0; rowIndex < numRows; rowIndex += 1) {
	free((void *) myArray[rowIndex]);
}
free((void *) myArray);
    • Si usas C++, no mezcles new/delete con malloc/free para la misma estructura de datos. La ventaja de new/delete para las instancias de clase es que llaman automáticamente a los constructores, que pueden inicializar los datos, y a los destructores, que pueden finalizar los datos. Cuando usas malloc/free, debes inicializar y finalizar explícitamente.
  • Enteros
    • C normalmente representa enteros en 4 bytes. Por ejemplo, el número 254235 se representa como el número binario 00000000,00000011,11100001,00011011.
    • Por otro lado, el texto ASCII representa los números como cualquier otro carácter, con un byte por dígito utilizando una codificación estándar. En ASCII, el número 254235 se representa como 00110010, 00110101, 00110110, 00110010, 00110011, 00110101.
    • Si necesitas escribir un archivo de enteros, generalmente es más eficiente tanto en espacio como en tiempo escribir las versiones de 4 bytes que convertirlos a cadenas ASCII y escribirlas. A continuación se muestra cómo escribir un único número entero en un fichero abierto:
write(outFile, &myInteger, sizeof(myInteger))
    • Puede ver los bytes individuales de un entero convirtiéndolo en una estructura de cuatro bytes:
int IPAddress; // stored as an integer, understood as 4 bytes
typedef struct {
	char byte1, byte2, byte3, byte4;
} IPDetails_t;
IPDetails_t *details = (IPDetails_t *) (&IPAddress);
printf("byte 1 is %o, byte 2 is %o, byte 3 is %o, byte 4 is %o\n",
	details->byte1, details->byte2, details->byte3, details->byte4);
    • Los enteros multibyte pueden representarse de forma diferente en las distintas máquinas. Algunas (como la Sun SparcStation) ponen primero el byte más significativo; otras (como la Intel i80x86 y sus descendientes) ponen primero el byte menos significativo. Si está escribiendo datos enteros que podrían ser leídos en otras máquinas, convierta los datos al orden de bytes de "red" mediante htons() o htonl(). Si está leyendo datos enteros que podrían haber sido escritos en otras máquinas, convierta los datos del orden "red" a su orden de bytes local mediante ntohs() o ntohl().
  • Puede predecir la disposición en memoria de los structs y el valor que devolverá sizeof(). Por ejemplo,
struct foo {
	char a; // uses 1 byte
		// C inserts a 3-byte pad here so b can start on a 4-byte boundary
	int b; // uses 4 bytes
	unsigned short c; // uses 2 bytes
	unsigned char d[2]; // uses 2 bytes
};
    • Por lo tanto, sizeof(struct foo) devuelve 12. Esta previsibilidad (para una arquitectura dada) es la razón por la que algunos llaman a C un "lenguaje ensamblador portátil". Es necesario predecir la disposición de las estructuras cuando se generan datos que deben seguir un formato específico, como la cabecera de un paquete de red.
  • En C se pueden declarar punteros a cualquier tipo y asignarles valores que apunten a objetos de ese tipo.
    • En particular, C permite construir punteros a enteros:
int someInteger;
int *intPtr = &someInteger; // declares a pointer-valued variable and assigns an appropriate pointer value
someCall(intPtr); // passes a pointer as an actual parameter
someCall(&someInteger); // has the same effect as above
    • Un procedimiento de la biblioteca C que toma un puntero a un valor lo más probable es que modifique ese valor (se convierte en un parámetro "out" o "in out"). En el ejemplo anterior, es muy probable que someCall modifique el valor del entero someInteger.
    • Puedes construir un puntero a un array de enteros y usarlo para recorrer ese array.
#define ARRAY_LENGTH 100
int intArray[ARRAY_LENGTH];
int *intArrayPtr;
...
int sum = 0;
for (intArrayPtr = intArray; intArrayPtr < intArray+ARRAY_LENGTH; intArrayPtr += 1) {
	sum += *intArrayPtr;
} 
    • Puedes construir un puntero a un array de structs y usarlo para recorrerlo.
#define ARRAY_LENGTH 100
typedef struct {int foo, bar;} pair_t; // pair_t is a new type
pair_t structArray[ARRAY_LENGTH]; // structArray is an array of ARRAY_LENGTH pair_t elements
pair_t *structArrayPtr; // structArrayPtr points to a pair_t element
...
int sum = 0;
for (structArrayPtr = structArray; structArrayPtr < structArray+ARRAY_LENGTH; structArrayPtr += 1) {
	sum += structArrayPtr->foo + structArrayPtr->bar;
} 
    • Cuando añades un entero a un puntero, el puntero avanza en esa cantidad de elementos, sin importar el tamaño de los elementos. El compilador conoce el tamaño y hace lo correcto.
  • Salida
    • Puede formatear la salida con printf o su variante, fprintf.
    • La cadena de formato usa %d, %s, %f para indicar que un entero, cadena, o real debe ser colocado en la salida.
    • La cadena de formato usa \t y \n para indicar tabulador y nueva línea.
    • Ejemplo:
printf("I think that the number %d is %s\n", 13, "lucky");
    • Mezclar printf(), fprintf(), y cout puede no imprimir elementos en el orden esperado. Utilizan zonas de almacenamiento independientes ("buffers") que imprimen cuando están llenas.
  • La rutina main() toma parámetros de función que representan parámetros de línea de comandos.
    • Una forma común de escribir la rutina main es esta:
int main(int argc; char *argv[]);
    • Aquí, argc es el número de parámetros, y argv es un array de cadenas, es decir, un array de punteros a arrays de caracteres terminados en cero.
    • Por convención, el primer elemento de argv es el nombre del programa.
int main(int argc; char *argv[]);
printf("I have %d parameters; my name is %s, and my first parameter is %s\n", 
	argc, argv[0], argv[1]);
  • Funciones útiles del lenguaje
    • Puedes incrementar un entero o hacer que un puntero apunte al siguiente objeto utilizando el operador ++. Normalmente es mejor colocar este operador después de la variable: miInt++. Si pones el ++ antes de la variable, entonces la variable se incrementa antes de ser evaluada, lo cual es raramente lo que quieres.
    • Puedes construir una asignación donde la variable del lado izquierdo participe como la primera parte de la expresión del lado derecho:
myInt -= 3; // equivalent to myInt = myInt - 3
myInt *= 42; // equivalent to myInt = myInt * 42
myInt += 1;  // equivalent to and maybe preferable to myInt++
    • Puede expresar números en decimal, octal (anteponiendo el dígito 0, como en 0453) o hexadecimal (anteponiendo 0x, como en 0xffaa).
    • Puede tratar un número entero como un conjunto de bits y realizar operaciones a nivel de bits:
myInt = myInt | 0444; // bitwise OR; 0444 is in octal
myInt &= 0444; // bitwise AND with an assignment shorthand
myInt = something ^ whatever; // bitwise XOR
    • C y C++ disponen de expresiones condicionales. En lugar de escribir
if (a < 7)
	a = someValue
else
	a = someOtherValue;
    • puede escribir
a = a < 7 ? someValue : someOtherValue;
    • Las asignaciones devuelven el valor del lado izquierdo, por lo que puedes incluir una asignación en expresiones más amplias como las condicionales. Pero debes seguir la convención de que tales asignaciones siempre están rodeadas de paréntesis para indicar tanto a quien lea tu código como al compilador que realmente te refieres a una asignación, no a una prueba de igualdad. Por ejemplo, escriba
if ((s = socket(...)) == -1)
    • no
if (s = socket(...) == -1)
    • La segunda versión es más difícil de leer y, en este caso, incorrecta, porque el operador de igualdad == tiene mayor precedencia que el operador de asignación =.
  • Los programas que no son trivialmente cortos deberían descomponerse en varios archivos fuente, cada uno con un nombre que termine en .c (para programas C) o .cpp (para programas C++).
    • Intente agrupar en el mismo archivo las funciones que manipulan las mismas estructuras de datos o tienen propósitos relacionados.
    • Todos los tipos, funciones, variables globales y constantes manifiestas que se necesiten en más de un fichero fuente deben declararse también en un fichero de cabecera, con un nombre que termine en .h.
    • Excepto para las funciones en línea, no declare cuerpos de función (o cualquier cosa que haga que el compilador genere código o asigne espacio) en el fichero de cabecera.
    • Cada fichero fuente debe hacer referencia a los ficheros de cabecera que necesite con una línea #include.
    • Nunca #incluya un archivo .c.
  • Cuando tienes múltiples ficheros fuente, necesitas enlazar todos los ficheros objeto compilados junto con cualquier librería que tu programa necesite.
    • El método más sencillo es utilizar el compilador C, que conoce las bibliotecas C:
gcc *.o -o myProgram
    • Este comando pide al compilador que enlace todos los archivos objeto con la biblioteca C (que está implícitamente incluida) y coloque el resultado en el archivo miPrograma, que se convierte en ejecutable.
    • Si tu programa necesita otras bibliotecas, debes especificarlas después de tus archivos objeto, porque el enlazador sólo recoge rutinas de las bibliotecas que ya sabe que necesita, y enlaza los archivos en el orden que especifiques. Así que si necesitas una librería como libxml2, tu orden de enlazado debería ser algo como esto:
gcc *.o -lxml2 -o myProgram
    • El compilador sabe cómo buscar en varios directorios estándar la versión actual de libxml2.
  • Depuración de programas C
    • Si obtiene un fallo de segmentación, lo más probable es que tenga un índice fuera de rango, un puntero no inicializado o un puntero nulo.
    • Puedes poner sentencias print en tu programa para ayudarte a localizar un error.
    • Es probable que la depuración tenga más éxito si utiliza gdb (descrito más adelante) para averiguar dónde está el error.
    • Los programas que se ejecutan durante mucho tiempo deben tener cuidado de liberar toda la memoria que asignan, o eventualmente se quedarán sin memoria. Para depurar fugas de memoria puedes considerar estos artículos sobre depuración de fugas de memoria en C y fugas de memoria en C++.

UNIX

archivos estándar, comandos, llamadas al sistema, permisos de archivos

  • Por convención, todo proceso se inicia con tres archivos estándar abiertos: entrada estándar, salida estándar y error estándar, asociados a los descriptores de archivo 0, 1 y 2. La entrada estándar suele estar conectada al teclado.
    • La entrada estándar suele estar conectada al teclado. Todo lo que escriba irá al programa.
    • La salida estándar suele estar conectada a la pantalla. Todo lo que el programa emite se hace visible.
    • El error estándar también suele estar conectado a la pantalla.
    • Puedes utilizar el intérprete de órdenes para invocar programas de modo que la salida estándar de un programa esté directamente vinculada ("canalizada") a la entrada estándar de otro programa:
ls | wc
    • Puede utilizar el intérprete de comandos para invocar programas de modo que la entrada y/o salida estándar esté vinculada a un archivo:
ls > lsOutFile
wc < lsOutFile
sort -u < largeFile > sortedFile 
    • En general, los programas no saben ni les importa si el shell ha reordenado el significado de sus archivos estándar.
  • Comandos Unix
    • Los comandos no son más que los nombres de archivos ejecutables. La variable de entorno PATH indica al shell dónde buscarlos. Normalmente, esta variable tiene un valor como /bin:/usr/bin:/usr/local/bin:..
    • Para ver dónde encuentra el shell un programa concreto, por ejemplo, vim, diga dónde vim.
  • Las llamadas al sistema y a bibliotecas siguen algunas convenciones importantes.
    • El valor de retorno de la llamada normalmente indica si la llamada tuvo éxito (normalmente el valor es 0 o positivo) o falló (normalmente el valor es -1).
    • Compruebe siempre el valor de retorno de las llamadas a bibliotecas. Cuando una llamada al sistema falla, la función perror() puede imprimir cuál fue el error (al error estándar):
int fd;
char *filename = "myfile";
if ((fd = open(filename, O_RDONLY)) < 0) {
	perror(filename); // might print "myfile: No such file or directory"
}
    • Una página de manual para una llamada al sistema o rutina de biblioteca puede listar un tipo de datos que no define, como size_t o time_t o O_RDONLY. Estos tipos se definen normalmente en los ficheros de cabecera mencionados en la página del manual; necesitas incluir todos esos ficheros de cabecera en tu programa C.
  • Los permisos de fichero en Unix se expresan normalmente con números octales.
    • En el ejemplo de creat() anterior, 0660 es un número octal (eso es lo que significa el 0 inicial), que representa el 110.110.000 binario. Este número octal concede permisos de lectura y escritura, pero no permisos de ejecución, al propietario y al grupo del archivo, pero ningún permiso a otros usuarios.
    • Los permisos se establecen al crear un archivo mediante el parámetro de la llamada creat().
    • El comando ls -l muestra los permisos de los archivos.
    • Puedes cambiar los permisos de un fichero de tu propiedad utilizando el programa chmod.
    • Todos tus procesos tienen una característica llamada umask, normalmente representada como un número octal. Cuando un proceso crea un fichero, los bits de la umask se eliminan de los permisos especificados en la llamada a creat(). Así que si tu umask es 066, entonces otros no pueden leer o escribir archivos que crees, porque 066 representa permisos de lectura y escritura para tu grupo y para otras personas. Puedes inspeccionar y modificar tu umask usando el programa umask, que normalmente invocas en el script de inicio de tu shell (dependiendo de tu shell, ~/.login o ~/.profile).

Herramientas de desarrollo de software

editor de texto, depurador, compilador, páginas de manual, make, búsqueda,

  • Utilice un editor de texto para crear, modificar e inspeccionar su programa. Existen varios editores de texto razonables.
    • El editor vim y su interfaz gráfica, gvim, requieren cierto esfuerzo de aprendizaje, pero proporcionan un conjunto de herramientas de muy alta calidad para la edición de archivos de programa, incluyendo resaltado de sintaxis, coincidencia de paréntesis, completado de palabras, sangrado automático, búsqueda por etiqueta (que le mueve rápidamente desde el lugar donde el programa llama a una función hasta el lugar donde se define la función), y búsqueda manual de páginas integrada. Vim está diseñado para su uso con el teclado; no es necesario utilizar el ratón si no se desea. Está disponible gratuitamente para los sistemas operativos Unix, Win32 y Microsoft. Es la versión más desarrollada de la serie de editores que incluye ed, ex, vi y elvis. Puede leer la documentación en línea de vim y obtener asistencia inmediata a través del comando :help de vim.
    • El editor emacs es, en todo caso, más completo que vim. También requiere un esfuerzo considerable para aprenderlo. También está disponible gratuitamente para los sistemas operativos Unix y Microsoft. Puedes encontrar documentación aquí.
    • Existen muchos otros editores de texto, pero en general no ofrecen las dos características más útiles para crear programas: sangría automática y resaltado de sintaxis. Sin embargo, estos editores de texto suelen tener la ventaja de ser más fáciles de aprender, en consonancia con sus limitadas capacidades. Entre estos editores de texto de menor calidad se encuentran (para Unix) pico, gedit y joe y (para Microsoft) notepad y word.
    • Puede que estés familiarizado con un entorno de desarrollo integrado (IDE) como Eclipse, Code Warrior o .NET. Estos entornos suelen tener editores de texto integrados con depuradores y compiladores. Si estás utilizando un IDE de este tipo, tiene sentido utilizar los editores de texto asociados.
  • gdb es un depurador que entiende tus variables y la estructura del programa.
    • Puedes encontrar documentación aquí.
    • Para usar gdb de forma efectiva, necesitas pasar la bandera -g al compilador C o C++.
    • Si su programa miPrograma ha fallado dejando un fichero llamado core, entonces pruebe gdb miPrograma core.
    • También puede ejecutar su programa desde el principio bajo el control de gdb: gdb miPrograma.
    • Todos los comandos para gdb pueden abreviarse con un prefijo único.
    • El comando help es muy útil.
    • El comando where muestra la pila de llamadas, incluyendo los números de línea que muestran dónde está cada rutina. Este es el primer comando que debe probar cuando depure un archivo core.
    • Para imprimir el valor de alguna expresión (puede incluir sus variables y los operadores habituales de C), escriba print expression, como en
print (myInt + 59) & 0444;
    • Para ver su programa, pruebe list myFunction o list myFile.c:38.
    • Para establecer un registro de activación diferente como actual, utilice el comando up (para más reciente) o down (para menos reciente).
    • Puede establecer un punto de interrupción en cualquier línea de cualquier archivo. Por ejemplo, puede decir break foo.p:38 para establecer un punto de interrupción en la línea 38 del archivo foo.p. Cada vez que su programa llegue a esa línea mientras se ejecuta, se detendrá y gdb le pedirá comandos. Puedes mirar variables, por ejemplo, o avanzar por el programa.
    • El siguiente comando avanza una sentencia (llamando y volviendo de cualquier procedimiento si es necesario).
    • El comando step avanza una sentencia, pero si la sentencia implica una llamada a un procedimiento, entra en el procedimiento y se detiene en la primera sentencia.
    • Si introduce el comando set follow-fork-mode child, entonces cuando su programa ejecute la llamada fork(), gdb continuará depurando el hijo, no el padre.
    • Salga de gdb introduciendo el comando quit.
    • Puede que prefiera usar la interfaz gráfica ddd para gdb.
  • Siempre dé a los programas compiladores gcc o g++ la bandera -Wall para activar un alto nivel de advertencias. Del mismo modo, dale a javac la opción -Xlint:all. No utilices un programa que genere advertencias en tiempo de compilación.
  • Puede leer el manual para obtener detalles sobre programas, rutinas de la biblioteca C y llamadas al sistema Unix utilizando el programa man, como en man printf o man gcc.
    • A veces, la función que desea se encuentra en una sección específica del manual de Unix y debe solicitarla explícitamente: man 2 open o man 3 printf. La sección 1 trata de los programas, la sección 2 de las llamadas al sistema, la sección 3 de la biblioteca C y la sección 8 de la administración del sistema. Lo más probable es que no necesites las otras secciones.
    • Puede encontrar si cualquier programa, rutina de la biblioteca C, o llamada al sistema Unix es relevante para algún tema usando la bandera -k, como en man -k print.
  • Use el programa make para organizar recetas para recompilar y revincular su programa cuando cambie un fichero fuente.
    • Consulte este tutorial o este manual para más detalles.
    • Si su programa se compone de varios archivos, puede compilarlos por separado y luego enlazarlos juntos. Compila con la bandera -c, y usa la bandera -o para indicar el archivo de salida. Un makefile razonable podría tener este aspecto:
SOURCES = driver.c input.c output.c
OBJECTS = driver.o input.o output.o
HEADERS = common.h
CFLAGS = -g -Wall

program: $(OBJECTS)
	$(CC) $(CFLAGS) $(OBJECTS) -o program

$(OBJECTS): $(HEADERS)
	
testRun: program
	program < testData

Este makefile usa una definición incorporada de CC y una regla incorporada para convertir ficheros fuente C como driver.c en su fichero objeto. Si modifica sólo input.c, entonces make testRun hará que el compilador reconstruya input.o, luego hará que el compilador vuelva a enlazar los objetos, creando el programa, y luego ejecutará el programa con la entrada estándar redirigida desde el archivo testData.

Si tiene muchos archivos fuente y muchos archivos de cabecera, puede querer usar el programa makedepend para construir automáticamente las reglas Makefile que especifican cómo los archivos fuente dependen de los archivos de cabecera. El ejemplo anterior asume que todos los ficheros fuente dependen de todos los ficheros de cabecera, lo que a menudo no es el caso.

  • El programa grep puede buscar rápidamente una definición o variable, particularmente en archivos de inclusión:
grep "struct timeval {" /usr/include/*/*.h

Ejercicios

Haga estos ejercicios en C.

  • Escribe un programa llamado atoi que abra un fichero de datos nombrado en la línea de comandos y lea de él una única línea de entrada, que debe contener un número entero representado en caracteres ASCII. El programa convierte esa cadena a un entero, multiplica el entero por 3, e imprime el resultado a la salida estándar. El programa no debe utilizar la función atoi(). Debe usar el programa make. Su Makefile debe tener tres reglas: atoi, run (que ejecuta su programa en sus datos de prueba estándar y redirige la salida a un nuevo archivo), y clean (que elimina los archivos temporales). Asegúrese de que su programa se ejecuta correctamente en los datos malos y sale con un mensaje de ayuda si el archivo de datos falta o es ilegible. Recorra su programa iniciándolo con gdb, colocando un punto de interrupción en main(), y usando el comando step repetidamente.
  • Busca la página del manual del programa cat. Codifique su propia versión de cat. Su versión debe aceptar múltiples (o ningún) parámetro de nombre de archivo. No necesita aceptar ningún parámetro de opción.
  • Escriba un programa removeSuffix que reciba un único parámetro: el nombre de un fichero de sufijos. El archivo de sufijos tiene una línea por entrada. Una entrada es una cadena no vacía, que llamamos sufijo, seguida por el signo >, seguida por otra cadena (posiblemente vacía), que llamamos reemplazo. Su programa debe almacenar todos los sufijos y sus reemplazos en una tabla hash. Utilice encadenamiento externo. A continuación, el programa debe leer la entrada estándar. Para cada palabra delimitada por espacios w en la entrada, encuentre el sufijo más largo s que aparezca en w, y modifique w eliminando s e insertando el reemplazo s, creando w'. Escriba una línea por cada palabra modificada, de la forma w>w'. No emita ninguna palabra que no haya sido modificada.

Author profile
Abner Roa

A seasoned writer, Abner Roa excels in weaving compelling stories and detailed analyses across various topics. With a sharp eye for detail, he provides thought-provoking articles that resonate with readers on multiple platforms, from print to digital media.


Related Articles