¡Esta es una revisión vieja del documento!
El lenguaje de Programación C
Este es un ejemplar amorosamente wikificado por ~peron de El Lenguaje de Programación C, 2da Edición, de B. Kerningham y D. Ritchie, adaptado para hackers del Tercer Mundo.
Capítulos
Capitulo 4: Funciones y la estructura del programa
Las funciones dividen tareas grandes de computación en varias más pequeñas, y permiten la posibilidad de construir sobre lo que otros ya han hecho, en lugar de comenzar desde cero. Las funciones apropiadas ocultan los detalles de operación de las partes del programa que no necesitan saber acerca de ellos, así que dan claridad a la totalidad y facilitan la penosa tarea de hacer cambios.
El lenguaje C se diseñó para hacer que las funciones fueran eficientes y fáciles de usar; los programas escritos en C se componen de muchas funciones pequeñas en lugar de sólo algunas grandes. Un programa puede residir en uno o más archivos fuente, los cuales pueden compilarse por separado y cargarse junto con funciones de biblioteca previamente compiladas. No trataremos aquí tales procesos, puesto que los detalles varían de un sistema a otro.
La declaración y definición de funciones es el área donde el estándar ANSI ha hecho los cambios más visibles a C. Tal como mencionam os en el capítulo 1, ahora es posible declarar los tipos de los argumentos cuando se declara una función. La sintaxis de la definición de funciones también cambia, de modo que las declaraciones y las definiciones coincidan. Esto hace posible que el compilador pueda detectar muchos más errores de lo que podía anteriormente. Además, cuando los argumentos se declaran con propiedad, se realizan automáticamente las conversiones convenientes.
El estándar clarifica las reglas sobre el alcance de los nombres; en particular, requiere que sólo haya una definición de cada objeto externo. La inicialización es más general: los arreglos y las estructuras automáticas ahora se pueden inicializar.
El preprocesador de C también se ha mejorado. Las nuevas facilidades del Procesador incluyen un conjunto más completo de directivas para la compilación condicional, una forma de crear cadenas entrecomilladas a partir de argumentos de macros y un mejor control sobre el proceso de expansión de macros.
4.1 Conceptos básicos de funciones
Para comenzar, diseñemos y escribamos un programa que imprim a cada línea de su entrada que contenga un “patró n” o cadena de caracteres en particular.
(Este es un caso especial del programa grep de UNIX.) Por ejem plo, al buscar el patrón de letras “ould.” en el conjunto de líneas
Ah Love! could you and I with Fate conspire To grasp this sorry Scheme of Things entire, Would not we shatter it to bits -- and then Re-mould it nearer to the Heart's Desire!
producirá la salida
Ah Love! could you and I with Fate conspire Would not we shatter it to bits -- and then Re-mould it nearer to the Heart's Desire!
El trabajo se ajusta ordenadamente en tres partes:
while {hay otra línea) if {la línea contiene el patrón) imprímela
Aunque ciertamente es posible poner el código de todo esto en main, una mejor forma es aprovechar la estructura haciendo de cada parte una función separada. Es más fácil trabajar con tres piezas pequeñas que con una grande, debido a que los detalles irrelevantes se pueden ocultar dentro de las funciones, y minimizar así el riesgo de interacciones no deseadas. Los fragmentos incluso se pueden emplear en otros programas.
“Mientras hay otra línea” es getline, función que ya escribimos en el capítulo 1, e “imprímela” es printf, que alguien ya nos proporcionó. Esto significa que sólo necesitamos escribir una rutina para decidir si la línea contiene una ocurrencia del patrón.
Podemos resolver ese problema escribiendo una función strindex(s,t), que regresa la posición o índice en la cadena s en donde comienza la cadena t, o -1 si s no contiene t. Debido a que los arreglos en C principian en la posición cero, los índices serán cero o positivos, y así un valor negativo como -1 es conveniente para señalar una falla. Cuando posteriormente se necesite una coincidencia de patrones más elaborada, sólo se debe reemplazar strindex; el resto del código puede permanecer igual. (La biblioteca estándar provee una función strstr que es semejante a strindex, excepto en que regresa un apuntador en lugar de un índice.)
Una vez definido todo este diseño, llenar los detalles del programa es simple. Aquí está en su totalidad, de modo que se puede ver cómo las piezas quedan juntas. Por ahora, el patrón que se buscará es una cadena literal, lo cual no es el mecanismo más general. Regresaremos en breve a una discusión sobre cómo inicializar arreglos de caracteres, y en el capítulo 5 mostraremos cómo hacer que el patrón de caracteres sea un parámetro fijado cuando se ejecuta el programa.
También hay una versión ligeramente diferente de getline, que se podrá comparar con la del capítulo 1.
# include <stdio.h> #define MAXILINE 1000 /* longitud máxima por línea de entrada */ int getline(char line[], int max) int strindex(char source[], char searchfor[]); char pattern[] = "ould"; /* patrón a buscar */ /* encontrar todas las líneas que coincidan con el patrón */ main() { char line[MAXLINE]; int found = 0; while (getline(line, MAXLINE) > 0) if (strindex(line, pattern) >= 0) { printf("%s", line); found++; } return found; }
/* getline: pone linea en s, retorna longitud */ int getline(char s[], int lim) { int c, i; i = 0; while (--lim > 0 && (c=getchar()) != EOF && c != '\n') s[i++] = c; if (c == '\n') s[i++] = c; s[i] = '\0'; return i; }
/* strindex: retorna el índice de t en s, -1 si no hay */ int strindex(char s[], char t[]) { int i, j, k; for (i = 0; s[i] != '\0'; i++) { for (j=i, k=0; t[k]!='\0' && s[j]==t[k]; j++, k++) ; if (k > 0 && t[k] == '\0') return i; } return -1; }
Cada definición de función tiene la forma
tipo-regresado nombre-de-función(declaraciones de argumentos) { declaraciones y proposiciones }
Varias partes pueden omitirse; una función mínima “dummy” es
nada() {}
que no hace ni regresa nada. Una función hacer-nada, como ésta, es algunas veces útil para reservar lugar al desarrollar un programa. Si el tipo que regresa se omite, se supone int.
Un programa es sólo un conjunto de definiciones de variables y funciones. La comunicación entre funciones es por argumentos y valores regresados por las funciones, y a través de variables externas. Las funciones pueden presentarse en cualquier orden dentro del archivo fuente, y el programa fuente se puede dividir en varios archivos, mientras las funciones no se dividan.
La proposición return es el mecanismo para que la función que se llama regrese un valor a su invocador. Al return le puede seguir cualquier expresión:
return expresión
La expresión se convertirá al tipo de retorno de la función si es necesario. Con frecuencia se utilizan paréntesis para encerrar la (expresión), pero son optativos.
La función que llama tiene la libertad de ignorar el valor regresado. Incluso, no hay necesidad de un a expresión después de return; en tal caso, ningún valor regresa al invocador. También el control regresa, sin valor, cuando la ejecución “cae al final” de la función al alcanzar la llave cerrada derecha }. No es ilegal,
aunque probablemente un signo de problemas, el que una función regrese un valor desde un lugar y ninguno desde otro. En cualquier caso, si una función no regresa explícitamente un valor, su “valor” es ciertamente basura.
El programa de búsqueda del patrón regresa un estado desde main, el número de coincidencias encontradas. Este valor está disponible para ser empleado por el medio ambiente que llamó al programa.
El mecanismo de cómo compilar y cargar un programa en C que reside en varios archivos fuente varía de un sistema a otro. En el sistema UNIX, por ejemplo, la orden cc mencionada en el capítulo 1 hace el trabajo. Suponiendo que las tres funciones se almacenan en tres archivos llamados main.c, getline.c, y strindex.c. Entonces la orden
cc main.c getline.c strindex.c
compila los tres archivos, sitúa el código objeto resultante en los archivos main.o, getline.o, y strindex.o, y después los carga todos dentro de un archivo ejecutable llamado a.out. Si existe un error - digamos en main.c - dicho archivo puede volverse a compilar por sí mismo y el resultado cargado con los archivos objeto previos, con la orden.
cc main.c getline.o strindex.o
cc emplea la convención “.c” contra “.o” para distinguir los archivos fuente de los archivos objeto.
- Ejercicio 4-1. Escriba la función
strrindex(s,t), que regresa la posición de la ocurrencia de más a la derecha detens, o-1si no hay alguna.
4.2 Funciones que regresan valores no enteros
Basta ahora los ejemplos de funciones han regresado o ningún valor (void) o un int. ¿Qué pasa si una función debe regresar algo de otro tipo? Muchas funciones numéricas como sqrt, sin y cos regresan double; otras funciones especializadas regresan tipos diferentes. Para ilustrar cómo tratar con esto, escribamos y usemos la función atof(s), que convierte la cadena s a su valor equivalente de punto flotante de doble precisión. La función atoi es una extensión de atoi, de la que mostramos versiones en los capítulos 2 y 3. Maneja signo y punto decimal optativos, y presencia o ausencia de parte entera o fraccionaria. Nuestra versión
no es una rutina de conversión de alta calidad; tomaría más espacio del que podemos dedicarle. La biblioteca estándar incluye un atof; el header <math.h> la declara.
Primero, atof por sí misma debe declarar el tipo del valor que regresa, puesto que no es int. El nombre del tipo precede al nombre de la función:
#include <ctype.h> /* atof: convierte la cadena s a double */ double atof(char s[]) { double val, power; int i, sign; for (i = 0; isspace(s[i]); i++) /* saltea espacio en blanco */ ; sign = (s[i] == '-') ? -1 : 1; if (s[i] == '+' || s[i] == '-') i++; for (val = 0.0; isdigit(s[i]); i++) val = 10.0 * val + (s[i] - '0'); if (s[i] == '.') i++; for (power = 1.0; isdigit(s[i]); i++) { val = 10.0 * val + (s[i] - '0'); power *= 10; } return sign * val / power; }
Segundo, e igualmente importante, la rutina que llama debe indicar que atof regresa un valor que no es int. Una forma de asegurar esto es declarar atof explícitamente en la rutina que la llama. La declaración se muestra en esta primitiva calculadora (apenas adecuada para un balance de chequera), que lee un número por
línea, precedido en forma optativa por un signo, y lo acumula, imprimiendo la suma actual después de cada entrada:
#include <stdio.h> #define MAXLINE 100 /* calculadora rudimentaria * / main() { double sum, atof(char []); char line[MAXLINE]; int getline(char line[], int max); sum = 0; while (getline(line, MAXLINE) > 0) printf("\t%g\n", sum += atof(line)); return 0; }
La declaración
double sum, atof(char []);
señala que sum es una variable double, y que atof es una función que toma un argumento char[] y regresa un double.
La función atof se debe declarar y definir consistentemente. Si atof en sí misma y la llamada a ella en main tienen tipos inconsistentes dentro del mismo archivo fuente, el error será detectado por el compilador. Pero si (como es probable) atof fuera compilada separadamente, la falta de consistencia no se detectaría, atof regresaría un valor double que main trataría como int, y se producirían resultados incongruentes.
A la luz de lo que hemos mencionado acerca de cómo deben coincidir las declaraciones con las definiciones, esto podría ser sorprendente. La razón de que ocurra una falta de coincidencia es que, si no existe el prototipo de una función, ésta es declarada implícitam ente la primera vez que aparece en una expresión, como
sum += atof(line)
Si en una expresión se encuentra un nombre que no ha sido declarado previamente y está seguido por paréntesis izquierdo, se declara por contexto, de modo que se supone que es el nombre de una función que regresa un int, y nada se supone acerca de sus argumentos. Aún más, si la declaración de una función no incluye argumentos como en
double atof();
también es tomada de modo que no se supone nada acerca de los argumentos de atof; se desactiva toda revisión de parámetros. Este significado especial de la lista de argumentos vacía se hace para permitir que los programas en C viejos se compilen con los nuevos compiladores. Pero es una mala táctica usar esto con programas nuevos. Si la función toma argumentos, declárelos; si no los toma, use void.
Dado atof, propiamente declarado, podemos escribir atoi (convierte una cade
na a int) en términos de él:
/* atoi: convierte la cadena s a entero usando atof */ int atoi(char s[]) { double atof(char s[]); return (int) atof(s); }
Nótese la estructura de las declaraciones y la proposición return. El valor de la expresión en
return expresión;
se convierte al tipo de la función antes de que se tome el return. Por lo tanto, el valor de atof, un double, se convierte automáticamente a int cuando aparece en este return, puesto que la función atoi regresa un int. Sin embargo, esta operación potencialmente descarta información, de manera que algunos compiladores lo previenen. El cast establece explícitamente lo que la operación intenta y suprime las advertencias.
- Ejercicio 4-2. Extienda
atofpara que maneje notación científica de la forma123.45e-6donde un número de punto flotante puede ir seguido poreoEy opcionalmente un exponente con signo. □
4.3 Variables Externas
4.4 Reglas y Alcance
4.5 Archivo de encabezamiento header
4.6 Variables estáticas
4.7 Variables tipo registro
4.8 Estructura de bloques
4.9 Inicialización
4.10 Recursividad
4.11 El preprocesador de C
4.1.1 Inclusión de archivos
4.1.2 Substitución de macros
4.1.3 Inclusión condicional
Capitulo 5: Apuntadores y arreglos
5.1 Apuntadores y Direcciones
5.2 Apuntadores y argumentos de funciones
5.3 Apuntadores y arreglos
5.4 Aritmética de direcciones
5.5 Apuntadores a caracteres, y funciones
5.6 Arreglos de apuntadores; apuntadores a apuntadores
5.7 Arreglos muitidimensionales
5.8 Inicialización de arreglos de apuntadores
5.9 Apuntadores vs. arreglos muitidimensionales
5.10 Argumentos en la línea de órdenes
5.11 Apuntadores a funciones
5.12 Declaraciones complicadas
Capítulo 6: Estructuras
Una estructura es una colección de una o más variables, de tipos posiblemente diferentes, agrupadas bajo un solo nombre para manejo conveniente. (Las estructuras se conocen como “ records” en algunos otros lenguajes, principalm ente Pascal.) Las estructuras ayudan a organizar datos complicados, en particular dentro de programas grandes, debido a que permiten que a un grupo de variables relacionadas se les trate como una unidad en lugar de como entidades separadas.
Un ejemplo tradicional de estructura es el registro de una nómina: un empleado está descrito por un conjunto de atributos, como nombre, domicilio, número del seguro social, salario, etc. Algunos de estos atributos pueden, a su vez, ser estructuras: un nombre tiene varios componentes, como los tiene un domicilio y aún un salario. Otro ejemplo, más típico para C, procede de las gráficas: un punto es un par de coordenadas, un rectángulo es un par de puntos, y otros casos semejantes.
El principal cambio realizado por el estándar ANSI es la definición de la asignación de estructuras: las estructuras se pueden copiar y asignar, pasar a funciones y ser regresadas por funciones. Esto ha sido manejado por muchos compiladores durante varios años, pero las propiedades están ahora definidas en forma precisa. Las estructuras y los arreglos automáticos ahora también se pueden inicializar.
6.1 Conceptos básicos sobre estructuras
6.2 Estructuras y funciones
6.3 Arreglos de estructuras
6.4 Apuntadores a estructuras
6.5 Estructuras autorreferenciadas
6.6 Búsqueda en tablas
6.7 Typedef
6.8 Uniones
6.9 Campos de bits
Capítulo 7:
Las operaciones de entrada y salida no son en si parle del lenguaje C, por lo que hasta ahora no las hemos destacado. Sin embargo, los programas interactúan con su medio ambiente en formas mucho más complicadas de las que hemos mostrado antes. En este capítulo describiremos la biblioteca estándar, un conjunto de funciones que proporcionan entrada y salida, manipulación de cadenas, manejo de memoria, rutinas matemáticas y una variedad de otros servicios para programas en C, aunque haremos hincapié en la entrada y salida.
El estándar ANSI define de manera precisa estas funciones de biblioteca, de modo que pueden existir en forma compatible en cualquier sistema en donde exista C. Los programas que restringen su interacción con el sistema a las facilidades provistas por la biblioteca estándar pueden ser llevados de un sistema a otro sin cambios.
Las propiedades de las funciones de biblioteca están especificadas en más de una docena de headers; ya hemos visto algunos, incluyendo <stdio.h>, <string.h> y <ctype.h>. No presentaremos aquí la totalidad de la biblioteca, puesto que estamos más interesados en escribir programas en C que los usan. La biblioteca se describe en detalle en el apéndice B.
7.1 Entrada y salida estándar
7.2 Salida con formato - printf
Tabla 7-1. Conversiones básicas de Printf
7.3 Listas de argumentos de longitud variable
7.4 Entrada con formato — scanf
Tabla 7-2 - Conversiones básicas de scanf
| Caracter | Dato de entrada:tipo de argumento |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
|
7.5 Acceso a archivos
7.6 Manejo de errores—stderr y exit
7.7 Entrada y salida de líneas
7.8 Otras funciones
La biblioteca estándar proporciona una amplia variedad de funciones. Esta sección es una breve sinopsis de las más útiles. En el apéndice B pueden encontrarse más detalles y muchas otras funciones.
7.8.1 Operaciones sobre cadenas
7.8.2 Prueba y conversión de clases de caracteres
7.8.3 Ungete
7.8.4 Ejecución de órdenes
7.8.5 Administración del almacenamiento
7.8.6 Funciones matemáticas
7.8.7 Generación de números aleatorios
Capítulo 8: La interfaz con el sistem a UNIX
El sistema operativo UNIX proporciona sus servicios a través de un conjunto de llamadas al sistema, que consisten en funciones que están dentro del sistema operativo y que pueden ser invocadas por programas del usuario. Este capitulo describe cómo emplear algunas de las más importantes llamadas al sistema desde programas en C. Si el lector usa UNIX, esto debe serle directamente útil, debido a que algunas veces es necesario emplear llamadas al sistema para tener máxima eficiencia, o para tener acceso a alguna facilidad que no esté en la biblioteca.
Incluso, si se emplea C en un sistema operativo diferente el lector debería ser capaz de adentrarse en la programación estudiando estos ejemplos; aunque los detalles varían, se encontrará un código semejante en cualquier sistema. Puesto que la biblioteca de C ANSI está en muchos casos modelada con base en las facilidades de UNIX , este código puede ayudar también a su entendimiento.
El capítulo está dividido en tres partes fundamentales: entrad a/salida, sistema de archivos y asignación de almacenamiento. Las primeras dos partes suponen una modesta familiaridad con las características externas de los sistemas UNIX.
El capítulo 7 tuvo que ver con una interfaz de entrada/salida uniforme entre sistemas operativos. En cualquier sistema las rutinas de la biblioteca estándar se tienen que escribir en términos de las facilidades proporcionadas por el sistema anfitrión. En las secciones de este capítulo describiremos las llamadas al sistema UNIX para entrada y salida, y mostraremos cómo puede escribirse parte de la biblioteca estándar con ellas.
8.1 Descriptores de Archivos
8.2 E/S de bajo nivel — read y write
8.3 Open, creat, close, unlink
8.4 Acceso aleatorio — lseek
8.5 Ejemplo - Una realización de fopen y getc
8.6 Ejemplo — listado de directorios
8.7 Ejemplo - Asignador de memoria
Apéndice A: Manual de Referencia
A1: Introducción
Este manual describe al lenguaje C tal como se especifica en Draft Proposed American National Standard for Information Systems — Programming Language C, documento número X3J11/88-001, con fecha 11 de enero de 1988. Este borrador no es el estándar final, y todavía es posible que ocurran algunos cambios en el lenguaje. Así pues, este manual no describe la definición final del lenguaje. Más aún es una interpretación del borrador propuesto del estándar, no el estándar en sí, aunque se ha tenido cuidado de hacerlo una guía confiable.
En su mayor parte, este manual sigue la línea amplia del borrador estándar, que a su vez sigue la de la primera edición de este libro, aunque la organización difiere en el detalle. Excepto por renombrar algunas producciones y porque no se formalizan las definiciones de los componentes léxicos o del preprocesador, la gramática dada aquí para el lenguaje es equivalente a la del borrador actual.
En este manual, el material comentado se encuentra sangrado y escrito en un tipo más pequeño, como este. A menudo estos comentarios resaltan las formas en las que el estándar ansí de C difiere del lenguaje definido por la primera edición de este libro, o de refinamientos introducidos posteriormente en varios compiladores.
Apéndice B: Biblioteca Estándar
Apéndice C: Resúmen de Modificaciones
Desde la publicación de la primera edición de este libro, la definición del lenguaje C ha sufrido modificaciones. Casi todas fueron extensiones al lenguaje original, y fueron diseñadas cuidadosamente para permanecer compatibles con la práctica existente; algunas repararon ambigüedades de la descripción original, y otras representan modificaciones de la práctica existente. Muchas de las nuevas características se anunciaron en los documentos que acompañan a los compiladores disponibles de AT&T, y posteriormente se han adoptado por otros proveedores de compiladores del lenguaje C. Recientemente, el comité ANSI incorporó más de esos cambios estandarizando el lenguaje, y también introdujo otras modificaciones significativas. Su reporte fue en parte anticipado por algunos compiladores comerciales aún antes de la publicación del estándar formal.
Este apéndice resume las diferencias entre el lenguaje definido por la primera edición de este libro, y lo esperado como la definición del estándar final. Trata solamente al lenguaje en sí, no a su entorno ni a su biblioteca; aunque esas son partes importantes del estándar, hay poco con qué compararlas, puesto que en la primera edición no se intentó definirlas.
- El preprocesamiento está definido más cuidadosamente en el Estándar que en la primera edición, y está extendido: está explícitamete basado en tokens (símbolos); existen nuevos operadores para la concatenación de tokens (# # ) y creación de cadenas (#); hay nuévas líneas de control como #elif y #pragm a; está explícitamente permitida la redeclaración de macros por la misma secuencia de tokens; ya no se reemplazan los parámetros que están dentro de cadenas. La separación de líneas por \ está permitida en cualquier lugar, no sólo en definiciones de cadenas y macros. Véase §A12.
- El significado mínimo el más pequeño de todos los identificadores internos se incrementó a 31 caracteres; permitido para identificadores con liga externo permanece en 6 letras, sin importar sin son mayúsculas o minúsculas (muchas implantaciones proporcionan más).
- Las secuencias trigráficas introducidas por ?? permiten la representación de caracteres que no se encuentran en algunos conjuntos. Están definidos los escapes para
#\'[']{}¡\. Véase §A12.1. Obsérvese que la introducción de trigrafos puede cambiar el significado
de cadenas que contengan la secuencia ??.
- Se introdujeron nuevas palabras reservadas (void, const, volatile, signed. enum). La palabra reservada entry, que nunca se puso en uso, fue retirada.
- Se definen nuevas secuencias de escape para uso dentro de constantes de carácter y cadenas literales. El efecto de seguir
\con un carácter que no sea parte de una secuenciade escape aprobada está indefinido. Véase §A2.5.2.
