Prólogo
En mis
comentarios, me referí varias veces al libro Diseño e implementación de sistemas operativos, su
primera edición, de Andrew Tanenbaum
, y cómo se representa a C en él. Y estos comentarios siempre han sido de interés. Decidí que era hora de publicar una traducción de esta introducción a C. Sigue siendo relevante. Aunque ciertamente hay quienes no han escuchado sobre el lenguaje de programación
PL / 1 , y tal vez incluso sobre el sistema operativo
Minix .
Esta descripción también es interesante desde un punto de vista histórico y para comprender hasta dónde ha llegado el lenguaje C desde su nacimiento y la industria de TI en general.
Quiero hacer una reserva de inmediato de que mi segundo idioma es el francés:

Pero esto se compensa con 46 años de
experiencia en programación .
Entonces, comencemos, es el turno de Andrew Tanenbaum.
Introducción al lenguaje C (págs. 350 - 362)
El lenguaje de programación C fue creado por Dennis Ritchie de AT&T Bell Laboratories como un lenguaje de programación de alto nivel para desarrollar el sistema operativo UNIX. Actualmente, el lenguaje es ampliamente utilizado en varios campos. C es especialmente popular entre los programadores de sistemas porque le permite escribir programas de manera simple y concisa.
El libro principal que describe el lenguaje C es el libro del lenguaje de programación C (1978) de Brian Kernigan y Dennis Ritchie. Los libros sobre el lenguaje C fueron escritos por Bolon (1986), Gehani (1984), Hancock y Krieger (1986), Harbison y Steele (1984) y muchos otros.
En esta aplicación, intentaremos dar una introducción bastante completa a C, para que aquellos que estén familiarizados con lenguajes de alto nivel como Pascal, PL / 1 o Modula 2 puedan comprender la mayor parte del código MINIX que se proporciona en este libro. Las características de C que no se usan en MINIX no se analizan aquí. Numerosos puntos sutiles omitidos. El énfasis está en leer programas en C, en lugar de escribir código.
A.1 Conceptos básicos de lenguaje C
Un programa en C consiste en un conjunto de procedimientos (a menudo llamados funciones, incluso si no devuelven valores). Estos procedimientos contienen declaraciones, operadores y otros elementos que juntos le dicen a la computadora qué hacer. La Figura A-1 muestra un pequeño procedimiento en el que se declaran tres valores enteros y se les asignan valores. El nombre del procedimiento es principal. El procedimiento no tiene parámetros formales, como lo indica la ausencia de identificadores entre los corchetes detrás del nombre del procedimiento. El cuerpo del procedimiento está encerrado entre llaves ({}). Este ejemplo muestra que C tiene variables y que estas variables deben declararse antes de su uso. C también tiene operadores, en este ejemplo, estos son operadores de asignación. Todas las declaraciones deben terminar con un punto y coma (a diferencia de Pascal, que usa dos puntos entre las declaraciones, no después de ellas).
Los comentarios comienzan con los caracteres "/ *" y terminan con los caracteres "* /" y pueden abarcar varias líneas.
main () { int i, j, k; i = 10; j = i + 015; k = j * j + 0xFF; } . Al. .
El procedimiento contiene tres constantes. Constante 10 en la primera tarea
Es una constante decimal ordinaria. La constante 015 es una constante octal
(igual a 13 en decimal). Las constantes octales siempre comienzan en cero. La constante 0xFF es una constante hexadecimal (igual a 255 decimales). Las constantes hexadecimales siempre comienzan con 0x. Los tres tipos se usan en C.
A.2 Tipos de datos básicos
C tiene dos tipos principales de datos (variables): un entero y un carácter, declarados como int y char, respectivamente. No hay una variable booleana separada. La variable int se usa como una variable booleana. Si esta variable contiene 0, significa falso / falso, y cualquier otro valor significa verdadero / verdadero. C también tiene tipos de coma flotante, pero MINIX no los usa.
Puede aplicar "adjetivos" cortos, largos o sin signo a un tipo int que define un rango de valores (rango dependiente del compilador). La mayoría de los procesadores 8088 usan enteros de 16 bits para int y short int y enteros de 32 bits para int largo. Los enteros sin signo (unsigned int) en el procesador 8088 tienen un rango de 0 a 65535, y no de -32768 a +32767, como es el caso de los enteros ordinarios (int). Un personaje toma 8 bits.
El especificador de registro también está permitido tanto para int como para char, y es una pista para el compilador de que la variable declarada debe colocarse en el registro para que el programa funcione más rápido.
Algunos anuncios se muestran en la fig. A - 2.
int i; short int z1, z2; / * */ char c; unsigned short int k; long flag_poll; register int r; . -2. .
Se permite la conversión entre tipos. Por ejemplo, el operador
flag_pole = i;
permitido incluso si i es de tipo int y flag_pole es largo. En muchos casos
es necesario o útil forzar conversiones entre tipos de datos. Para la conversión forzada, es suficiente poner el tipo de destino entre paréntesis delante de la expresión para la conversión. Por ejemplo:
( (long) i);
instruye a convertir el entero i a largo antes de pasarlo como parámetro al procedimiento p, que espera el parámetro largo.
Al convertir entre tipos, preste atención al signo.
Al convertir un carácter en un número entero, algunos compiladores tratan los caracteres como si estuvieran firmados, es decir, de - 128 a +127, mientras que otros los tratan como
sin signo, es decir, de 0 a 255. En MINIX, expresiones como
i = c & 0377;
que convierte de (carácter) a un entero y luego realiza un AND lógico
(ampersand) con la constante octal 0377. El resultado es que los 8 bits altos
se establecen en cero, lo que obliga a c a considerarse como un número sin signo de 8 bits, en el rango de 0 a 255.
A.3 Tipos compuestos y punteros
En esta sección, veremos cuatro formas de construir tipos de datos más complejos: matrices, estructuras, uniones y punteros. Una matriz es una colección / conjunto de elementos del mismo tipo. Todas las matrices en C comienzan con el elemento 0.
Anuncio
int a [10];
declara que una matriz a con 10 enteros se almacenará en los elementos de la matriz desde [0] a a [9]. En segundo lugar, las matrices pueden tener tres o más dimensiones, pero no se usan en MINIX.
Una estructura es una colección de variables, generalmente de varios tipos. La estructura en C es similar al registro en Pascal. Operador
struct {int i; char c;} s;
declara s como una estructura que contiene dos miembros, el entero i y el carácter c.
Para asignar el miembro i de la estructura s a 6, escriba la siguiente expresión:
si = 6;
donde el operador de punto indica que el elemento i pertenece a la estructura s.
Un sindicato es también un conjunto de miembros, similar a una estructura, excepto que en cualquier momento solo uno de ellos puede estar en un sindicato. Anuncio
union {int i; char c;} u;
significa que puede tener un número entero o carácter, pero no ambos. El compilador debe asignar suficiente espacio para combinar de modo que pueda acomodar el elemento de combinación más grande (desde el punto de vista de la memoria ocupada). Las uniones se usan solo en dos lugares en MINIX (para definir un mensaje como una unión de varias estructuras diferentes, y para definir un bloque de disco como una unión de un bloque de datos, bloque de nodo i, bloque de catálogo, etc.).
Los punteros se utilizan para almacenar direcciones de máquinas en C. Se usan muy, muy a menudo. Se utiliza un asterisco (*) para indicar un puntero en los anuncios. Anuncio
int i, *pi, a [10], *b[10], **ppi;
declara un entero i, un puntero a un entero pi, una matriz a de 10 elementos, una matriz b de 10 punteros a enteros y un puntero a un puntero ppi a un entero.
Las reglas de sintaxis exactas para declaraciones complejas que combinan matrices, punteros y otros tipos son algo complejas. Afortunadamente, MINIX usa solo declaraciones simples.
La Figura A-3 muestra la declaración de una matriz z de estructuras de tabla de estructura, cada una de las cuales tiene
tres miembros, entero i, puntero cp al caracter y caracter c.
struct table { int i; / * */ char *cp, c; } z [20]; . - 3. .
Las matrices de estructuras son comunes en MINIX. Además, la tabla de nombres se puede declarar como una estructura de tabla de estructura que se puede utilizar en declaraciones posteriores. Por ejemplo
register struct table *p;
declara p un puntero a una estructura de tabla de estructura y sugiere guardarlo
en el registro Durante la ejecución del programa, p puede indicar, por ejemplo, z [4] o
a cualquier otro elemento en z, los 20 elementos de los cuales son estructuras de tipo struct table.
Para hacer de p un puntero a z [4], solo escriba
p = &z[4];
donde el comercial como operador unario (monádico) significa "tomar la dirección de lo que sigue". Copie el valor del miembro i a la variable entera n
La estructura señalada por p se puede hacer de la siguiente manera:
n = p->i;
Tenga en cuenta que la flecha se utiliza para acceder a un miembro de la estructura a través de un puntero. Si usamos la variable z, entonces debemos usar el operador de punto:
n = z [4] .i;
La diferencia es que z [4] es una estructura, y el operador de punto selecciona los elementos
de tipos compuestos (estructuras, matrices) directamente. Usando punteros, no seleccionamos un participante directamente. El puntero indica que primero seleccione una estructura y solo luego seleccione un miembro de esta estructura.
A veces es conveniente dar un nombre a un tipo compuesto. Por ejemplo:
typedef unsigned short int unshort;
define unshort como short sin signo (entero corto sin signo). Ahora se puede usar unshort en el programa como tipo principal. Por ejemplo
unshort ul, *u2, u3[5];
declara un entero corto sin signo, un puntero a un entero corto sin signo y
una matriz de enteros cortos sin signo.
A.4. Operadores
Los procedimientos en C contienen declaraciones y declaraciones. Ya hemos visto las declaraciones, por lo que ahora consideraremos los operadores. El propósito de los operadores condicionales y de bucle es esencialmente el mismo que en otros idiomas. La Figura A - 4 muestra varios ejemplos de ellos. Lo único a lo que vale la pena prestar atención es que las llaves se usan para agrupar operadores, y la instrucción while tiene dos formas, la segunda de las cuales es similar a la declaración de repetición de Pascal.
C también tiene una declaración for, pero no parece una declaración for en ningún otro lenguaje. La declaración for tiene la siguiente forma:
for (<>; <>; <>) ;
Lo mismo se puede expresar a través de la declaración while:
<> while(<>) { <>; <> }
Como ejemplo, considere la siguiente declaración:
for (i=0; i <n; i = i+l) a[i]=0;
Este operador establece los primeros n elementos de la matriz a en cero. La ejecución del operador comienza estableciendo i en cero (esto se realiza fuera del bucle). Luego, el operador se repite hasta i <n, mientras realiza la asignación y el aumento de i. Por supuesto, en lugar del operador de asignar un valor al elemento actual de una matriz cero, puede haber un operador compuesto (bloque) encerrado entre llaves.
if (x < 0) k = 3; if (x > y) { i = 2; k = j + l, } if (x + 2 <y) { j = 2; k = j - 1; } else { m = 0; } while (n > 0) { k = k + k; n = n - l; } do { / * while */ k = k + k; n = n - 1; } while (n > 0); . A-4. if while C.
C también tiene un operador similar al operador de caso en Pascal. Esta es una declaración de cambio. Un ejemplo se muestra en la Figura A-5. Dependiendo del valor de la expresión especificada en el interruptor, se selecciona una u otra declaración de caso.
Si la expresión no coincide con ninguna de las declaraciones de caso, se selecciona la declaración predeterminada.
Si la expresión no está asociada con ninguna declaración de caso y la declaración predeterminada está ausente, la ejecución continúa desde la siguiente declaración después de la declaración de cambio.
Cabe señalar que para salir del bloque de casos, use la instrucción break. Si no hay una declaración de interrupción, se ejecutará el siguiente bloque de casos.
switch (k) { case 10: i = 6; break; case 20: i = 2; k = 4; break; / * default* / default: j = 5; } . A-5. switch
La instrucción break también actúa dentro de los bucles for y while. Debe recordarse que si la declaración de ruptura está dentro de una serie de bucles anidados, la salida está solo un nivel arriba.
Una declaración relacionada es la declaración de continuación, que no sale del bucle,
pero provoca la finalización de la iteración actual y el comienzo de la siguiente iteración
inmediatamente Esto es esencialmente un retorno a la parte superior del bucle.
C tiene procedimientos que pueden llamarse con o sin parámetros.
Según Kernigan y Ritchie (p. 121), no está permitido transferir matrices,
estructuras o procedimientos como parámetros, aunque pasando punteros a todo esto
permitido. ¿Hay un libro o no? (Aparecerá en mi memoria: - “Si hay vida en Marte, si no hay vida en Marte”), muchos compiladores de C permiten estructuras como parámetros.
El nombre de la matriz, si está escrito sin un índice, significa un puntero a una matriz, lo que simplifica la transferencia de un puntero de matriz. Por lo tanto, si a es el nombre de una matriz de cualquier tipo, se puede pasar a g escribiendo
g();
Esta regla se aplica solo a las matrices; esta regla no se aplica a las estructuras.
Los procedimientos pueden devolver valores ejecutando una declaración de devolución. Esta declaración puede contener una expresión, cuyo resultado se devolverá como el valor del procedimiento, pero la persona que llama puede ignorar con seguridad el valor devuelto. Si el procedimiento devuelve un valor, el valor de tipo se escribe antes del nombre del procedimiento, como se muestra en la Fig. A-6. Al igual que los parámetros, los procedimientos no pueden devolver matrices, estructuras o procedimientos, pero pueden devolverles punteros. Esta regla está diseñada para una implementación más eficiente: todos los parámetros y resultados siempre corresponden a una palabra de máquina (en la que se almacena la dirección). Los compiladores que permiten el uso de estructuras como parámetros generalmente también permiten su uso como valores de retorno.
int sum (i, j) int i, j ; { return (i + j); } . -6. , .
C no tiene E / S incorporadas. La entrada / salida se implementa llamando a funciones de biblioteca, las más comunes se ilustran a continuación:
printf («x=% dy = %oz = %x \n», x, y, z);
El primer parámetro es la cadena de caracteres entre comillas (de hecho, esta es una matriz de caracteres).
Cualquier carácter que no sea un porcentaje simplemente se imprime tal cual.
Cuando se produce un porcentaje, el siguiente parámetro se imprime en la forma definida por la letra que sigue al porcentaje:
d - imprimir como un entero decimal
o - imprimir como un entero octal
u - imprime como un entero decimal sin signo
x - imprimir como un entero hexadecimal
s - imprimir como una cadena de caracteres
c - imprimir como un personaje
Las letras D, 0 y X también están permitidas para la impresión decimal, octal y hexadecimal de números largos.
A.5. Expresiones
Las expresiones se crean combinando operandos y operadores.
Operadores aritméticos como + y - y operadores relacionales como <
y> similar a sus contrapartes en otros idiomas. % Operador
módulo usado. Vale la pena señalar que el operador de igualdad es ==, ¡y el operador de desigualdad es! =. Para verificar si ayb son iguales, puede escribir así:
if (a == b) <>;
C también le permite combinar el operador de asignación con otros operadores, por lo tanto
a += 4;
equivalente a la grabación
= + 4;
Otros operadores también se pueden combinar de esta manera.
C tiene operadores para manipular bits de una palabra. Se permiten tanto los cambios como las operaciones lógicas bit a bit. Los operadores de desplazamiento izquierdo y derecho son <<
y >> respectivamente. Operadores lógicos a nivel de bit y, | y ^, que son lógicos AND (AND), incluidos OR (OR) y OR exclusivo (XOP), respectivamente. Si tengo el valor 035 (octal), entonces la expresión i & 06 tiene el valor 04 (octal). Otro ejemplo, si i = 7, entonces
j = (i << 3) | 014;
y obtener 074 para j.
Otro grupo importante de operadores son los operadores unarios, cada uno de los cuales acepta solo un operando. Como operador unario, ampersand & obtiene la dirección de una variable.
Si p es un puntero a un número entero e i es un número entero, el operador
p = &i;
calcula la dirección i y la almacena en la variable p.
Lo contrario de tomar una dirección es un operador que toma un puntero como entrada y calcula el valor en esa dirección. Si acabamos de asignar la dirección i al puntero p, entonces * p tiene el mismo significado que i.
En otras palabras, como operador unario, un asterisco es seguido por un puntero (o
expresión que da un puntero) y devuelve el valor del elemento al que apunta. Si tengo un valor de 6, entonces el operador
j = *;
asignará j el número 6.
El operador! (el signo de exclamación es el operador de negación) devuelve 0 si su operando es distinto de cero y 1 si su operador es 0.
Se utiliza principalmente en sentencias if, por ejemplo
if (!x) k=0;
comprueba el valor de x. Si x es cero (falso), a k se le asigna el valor 0. En realidad, ¡el operador! cancela la condición que le sigue, al igual que el operador no en Pascal.
El operador ~ es un operador de complemento bit a bit. Cada 0 en su operando
se convierte en 1 y cada 1 se convierte en 0.
El operador sizeof informa el tamaño de su operando en bytes. En relación a
una matriz de 20 enteros a en una computadora con enteros de 2 bytes, por ejemplo, sizeof a tendrá un valor de 40.
El último grupo de operadores son los operadores de aumento y disminución.
Operador
++;
significa un aumento en p. Cuánto aumentará p depende de su tipo.
Los enteros o caracteres aumentan en 1, pero los punteros aumentan en
el tamaño del objeto señalado de esta manera, si a es una matriz de estructuras yp es un puntero a una de estas estructuras, y escribimos
p = &a[3];
para hacer que p apunte a una de las estructuras en la matriz, luego de aumentar p
apuntará a un [4] sin importar cuán grandes sean las estructuras. Operador
p--;
similar al operador p ++, excepto que disminuye en lugar de aumentar el valor del operando.
En la declaración
n = k++;
donde ambas variables son enteros, el valor original de k se asigna a ny
solo entonces k aumenta. En la declaración
n = ++ k;
k aumenta primero, luego su nuevo valor se almacena en n.
Por lo tanto, un operador ++ (o -) se puede escribir antes o después de su operando, lo que resulta en varios valores.
La última declaración es esta? (signo de interrogación) que selecciona una de dos alternativas
separados por dos puntos. Por ejemplo, un operador,
i = (x < y ? 6 : k + 1);
compara x con y. Si x es menor que y, entonces obtengo el valor 6; de lo contrario, la variable i obtiene el valor k + 1. Los corchetes son opcionales.
A.6. Estructura del programa
Un programa en C consta de uno o más archivos que contienen procedimientos y declaraciones.
Estos archivos se pueden compilar individualmente en archivos de objetos, que luego se vinculan entre sí (utilizando el vinculador) para formar un programa ejecutable.
A diferencia de Pascal, las declaraciones de procedimientos no pueden anidarse, por lo tanto, todas están escritas en el "nivel superior" en el archivo del programa.
Se permite declarar variables fuera de los procedimientos, por ejemplo, al comienzo del archivo antes de la primera declaración del procedimiento. Estas variables son globales y pueden usarse en cualquier procedimiento en todo el programa, a menos que la palabra clave estática preceda a la declaración. En este caso, estas variables no se pueden usar en otro archivo. Las mismas reglas se aplican a los procedimientos. Las variables declaradas dentro de un procedimiento son locales al procedimiento.
El procedimiento puede acceder a la variable entera v declarada en otro archivo (siempre que la variable no sea estática), declarándola externa:
extern int v;
Cada variable global debe declararse exactamente una vez sin el atributo extern para poder asignarle memoria.
Las variables se pueden inicializar cuando se declaran:
int size = 100;
Las matrices y estructuras también se pueden inicializar. Las variables globales que no se inicializan explícitamente reciben un valor predeterminado de cero.
A.7. Preprocesador C
Antes de que el archivo fuente se transfiera al compilador de C, se procesa automáticamente
Un programa llamado preprocesador. Es la salida del preprocesador, no
El programa original se alimenta a la entrada del compilador. El preprocesador realiza
Tres conversiones básicas en un archivo antes de pasarlo al compilador:
1. Inclusión de archivos.
2. Definición y reemplazo de macros.
3. Compilación condicional.
Todas las directivas de preprocesador comienzan con un signo de número (#) en la primera columna.
Cuando una directiva de vista
#include "prog.h"
cumplido por el preprocesador, incluye el archivo prog.h, línea por línea, en
El programa que se pasará al compilador. Cuando la directiva #include se escribe como
#include <prog.h>
entonces el archivo incluido se busca en el directorio / usr / include en lugar del directorio de trabajo. Es una práctica común en C agrupar las declaraciones utilizadas por varios archivos en un archivo de encabezado (generalmente con el sufijo .h) e incluirlos cuando sea necesario.
El preprocesador también permite definiciones de macro. Por ejemplo
#define BLOCK_SIZE 1024
define la macro BLOCK_SIZE y le asigna un valor de 1024. A partir de ahora,cada aparición de una cadena de 10 caracteres "BLOCK_SIZE" en el archivo seráreemplazada por una cadena de 4 caracteres "1024" antes de que el compilador vea el archivo con el programa. Por convención, los nombres de macro se escriben en mayúsculas. Las macros pueden tener parámetros, pero en la práctica pocas.La tercera característica del preprocesador es la compilación condicional. MINIX tiene varioslugares donde el código está escrito específicamente para el procesador 8088, y este código no debe incluirse al compilar para otro procesador. Estas secciones se ven así: #ifdef i8088 < 8088> #endif
Si se define el carácter i8088, las declaraciones entre las dos directivas de preprocesador #ifdef i8088 y #endif se incluyen en la salida del preprocesador; de lo contrario, se omiten. Llamar al compilador con el comando cc -c -Di8088 prog.c
o al incluir una declaración en el programa #define i8088
definimos el símbolo i8088, por lo que se incluirá todo el código dependiente para 8088. A medida que se desarrolla MINIX, puede adquirir un código especial para 68000 y otros procesadores que también se procesarán.Como ejemplo de cómo funciona el preprocesador, considere el programa Fig. A-7 (a). Incluye un archivo prog.h, cuyo contenido es el siguiente: int x; #define MAXAELEMENTS 100
Imagine que el compilador fue llamado por un comando cc -E -Di8088 prog.c
Después de que el archivo haya pasado por el preprocesador, la salida será como se muestra en la Fig. A-7 (b).Es esta salida, y no el archivo fuente, la que se proporciona como entrada al compilador de C.
Tenga en cuenta que el preprocesador hizo su trabajo y eliminó todas las líneas que comienzan con el signo #. Si el compilador se llamara así cc -c -Dm68000 prog.c
entonces se incluiría otra impresión. Si se llamara así: cc -c prog.c
entonces no se incluiría ninguna impresión. (El lector puede reflexionar sobre lo que sucedería si se llamara al compilador con ambos indicadores -D-flags).A.8. Modismos
En esta sección, veremos varias construcciones que son típicas para C pero no comunes en otros lenguajes de programación. Primero, considere el ciclo: while (n--) *p++ = *q++;
Las variables p y q suelen ser punteros de caracteres, yn es un contador. El bucle copia la cadena de n caracteres desde donde q apunta a donde p apunta. En cada iteración del ciclo, el contador disminuye hasta llegar a 0, y cada uno de los punteros aumenta, por lo que apuntan secuencialmente a las celdas de memoria con un número mayor.Otro diseño común: for (i = 0; i < N; i++) a[i] = 0;
que establece los primeros N elementos de a en 0. Una forma alternativa de escribir este ciclo es la siguiente: for (p = &a[0]; p < &a[N]; p++) *p = 0;
En esta declaración, el puntero entero p se inicializa para apuntar al elemento cero de la matriz. El ciclo continúa hasta que p alcanza la dirección del enésimo elemento de la matriz. Una construcción de puntero es mucho más eficiente que una construcción de matriz y, por lo tanto, generalmente se usa.Los operadores de asignación pueden aparecer en lugares inesperados. Por ejemplo
if (a = f (x)) < >;
primero llama a la función f, luego asigna el resultado de llamar a la función a, yfinalmente verifica si es verdadero (no cero) o falso (cero). Si a no es igual a cero, entonces se cumple la condición. Operador
if (a = b) < >;
también, primero, el valor de la variable b de la variable a, y luego comprueba si el valor es distinto de cero. Y este operador es completamente diferente de if (a == b) < >;
que compara dos variables y ejecuta el operador si son iguales.Epílogo
Eso es todo No vas a creer lo mucho que disfruté preparando este texto. Cuánto recordaba útil del mismo lenguaje C. Espero que usted también disfrute de sumergirse en el maravilloso mundo del lenguaje C.