E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 SISTEMAS OPERATIVOS IC Breve introducción al lenguaje C Septiembre 2014 E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 ÍNDICE 1 BREVE INTRODUCCIÓN AL LENGUAJE C ............................................................................................. 2 1.1 Conceptos básicos de C ........................................................................................................................... 2 1.2 Tipos de datos básicos ............................................................................................................................. 2 1.3 Tipos estructurados.................................................................................................................................. 3 1.4 Estructuras de control. ............................................................................................................................. 5 1.5 Expresiones ............................................................................................................................................. 7 1.6 Estructura de un programa....................................................................................................................... 9 1.7 El preprocesador de C. ............................................................................................................................ 9 1.8 Construcciones particulares de C .......................................................................................................... 10 Página - 1 E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 1 BREVE INTRODUCCIÓN AL LENGUAJE C El texto de esta sección es un extracto del apéndice A “Introducción a C” del libro “Sistemas Operativos: Diseño e Implementación” de Andrew S. Tanenbaum publicado por Prentice-Hall. C fue inventado por Dennis Ritchie de AT&T Bell Laboratoires para ofrecer un lenguaje de alto nivel en el cual se pudiera programar UNIX. Ahora se utiliza ampliamente en muchas otras aplicaciones. C es especialmente popular entre los programadores de sistemas, porque permite que todos los programas se expresen en forma simple y concisa. El trabajo definitivo que describe el lenguaje C es The C Programming Language escrito por Kernighan y Ritchie. 1.1 Conceptos básicos de C Un programa en lenguaje C está constituido por un conjunto de funciones (procedimientos que normalmente retornan un valor). Estas funciones contienen declaraciones, instrucciones y otros elementos que juntos indican a la computadora cómo debe realizar alguna tarea. A continuación se muestra una pequeña función que declara tres variables enteras y les asigna valores. main() { int i, j, k; i = 10; j = i + 015; k = j * j + 0xFF; /* este es un comentario */ /* /* /* /* declaracion de 3 variables enteras */ hacer i igual a 10(decimal) */ hacer j igual a i + 15(octal) */ hacer k igual a j * j + 0xFF(hexadecimal) */ } El nombre de la función es main. No tiene parámetros formales, como lo indica la ausencia de identificadores entre los paréntesis. Su cuerpo está encerrado entre llaves. Este ejemplo muestra que C tiene variables y que estas variables se deben declarar antes de utilizarse. C tiene asimismo instrucciones, en este ejemplo instrucciones de asignación. Todas las instrucciones deben acabar con un punto y coma. Los comentarios comienzan con el símbolo /* y terminan con el símbolo */, pudiéndose extender a lo largo de varias líneas. La función contiene tres constantes. La constante 10 de la primera asignación es una constante decimal ordinaria. La constante 015 es una constante octal (igual a 13 decimal). Las constantes octales siempre comienzan con un cero al principio. La constante 0xFF es una constante hexadecimal (igual a 255 decimal). Las constantes hexadecimales siempre comienzan con 0x. Las bases octal y hexadecimal se utilizan muy frecuentemente en los programas en C. 1.2 Tipos de datos básicos C tiene dos tipos de datos principales: entero y carácter, que se denominan int y char, respectivamente. No existe el tipo de datos booleano. En su lugar se utilizan enteros, donde 0 significa falso y todos los demás valores significan verdadero. El lenguaje C también tiene tipos numéricos en coma flotante como float. Página - 2 E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 El tipo int puede calificarse con los “adjetivos” short, long o unsigned, los cuales indican el intervalo de valores enteros más o menos extenso que se cubren, dependiendo siempre del compilador utilizado. La mayoría de los compiladores utilizan enteros de 16 bits para int y short int, y enteros de 32 bits para long int. Los caracteres son siempre de 8 bits. Ejemplos de algunas declaraciones: int i; /* short int z1, z2; /* char c; /* unsigned short int k; /* long flag_pole; /* un entero */ dos enteros cortos */ un caracter */ un entero corto sin signo */ puede omitirse int */ En C más que en otros lenguajes de programación se permite la conversión directa de unos tipos a otros. Por ejemplo la instrucción de asignación flag_pole = i ; está permitida aunque i sea un entero y flag_pole un entero largo. En muchos casos la conversión no es directa, pero puede forzarse indicando el tipo destino entre paréntesis antes de la expresión cuyo valor queremos convertir. Por ejemplo p( (long) i) ; convierte el valor entero contenido en i a tipo entero largo antes de ser pasado como parámetro real en la llamada al procedimiento p que se supone declarado con un único parámetro formal de tipo long. 1.3 Tipos estructurados Vamos a ver cómo se declaran los tipos de datos, tabla (array), registro y puntero. En cuanto a las tablas hay que decir que los únicos índices que admiten son conjuntos de números naturales consecutivos comenzando desde el 0. Por esa razón al indicar el índice de una tabla sólo es necesario especificar el número de componentes que tiene. Por ejemplo la declaración int a[10] ; declara una (variable) tabla, a, con 10 componentes de tipo entero, las cuales podemos referenciar mediante a[0], a[1], .. , a[8] y a[9] respectivamente. Podemos hacer referencias genéricas a las componentes de la tabla indicando dentro de los corchetes expresiones de tipo entero, como a[i], a[j+1], a[i*j–3]. Para declarar una variable de tipo registro se utiliza la palabra reservada struct antes de enumerar el nombre y tipo de los campos del registro. Así por ejemplo la declaración: struct { int i ; char c ; } s ; declara una variable s de tipo registro con dos campos: i de tipo entero y c de tipo carácter. Para referenciar cualquier campo del registro podemos utilizar la notación habitual. Así por ejemplo la instrucción de asignación: s.i = 6 ; asigna el valor 6 al campo i del registro s. Página - 3 E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 Los punteros constituyen un recurso muy utilizado en C. Para declarar variables de tipo puntero se utiliza el asterisco precediendo al nombre de la variable. Por ejemplo en la declaración: int i, *pi, a[10], *b[10], **ppi ; se declara una variable entera i, un puntero pi a un entero, una tabla a de 10 componentes de tipo entero, una tabla b de 10 componentes de tipo puntero a un entero y una variable ppi de tipo puntero a un puntero a un entero. Veamos un ejemplo de declaración de una tabla z de registros: struct entrada { int i ; char *cp, c ; } z [20] ; /* /* /* /* cada registro es de tipo entrada */ un entero */ un puntero a un caracter y un caracter */ una tabla de 20 registros de tipo entrada */ El identificador entrada da nombre al tipo de los registros de la tabla, de manera que podemos declarar nuevos registros de tipo entrada, por ejemplo: struct entrada *p ; declara la variable p como un puntero a un registro de tipo entrada. Durante la ejecución del programa, p podría apuntar en algún momento a z[4] o a cualquiera de las otras componentes de z. Para hacer que p apunte a z[4] podríamos utilizar la asignación: p = & z[4] ; donde el símbolo & corresponde a un operador unario del lenguaje C que da como resultado la dirección de memoria del objeto al que se aplica. En esa situación podemos copiar el valor del campo i del registro apuntado por p, en la variable entera n del modo siguiente: n = p -> i ; obsérvese que la flecha -> se utiliza para acceder a un campo de un registro. Si se utilizara directamente la tabla z bastaría con emplear la notación con punto usual: n = z[4].i ; La diferencia es que z[4] es un registro y la notación con punto selecciona campos de registros. Con punteros no se selecciona un campo de forma directa. Primero se debe seguir el puntero para encontrar el registro, para luego seleccionar el campo . A veces conviene dar un nombre a un tipo, por ejemplo: typedef unsigned short int unshort ; define unshort como un entero corto sin signo. Se puede utilizar ahora unshort como si fuera un tipo básico de C. Por ejemplo unshort u1, *u2, u3[5] ; declara un entero corto sin signo, un puntero corto sin signo y una tabla de enteros cortos sin signo. Página - 4 E.U. de Informática-U.P.M. 1.4 Sistemas Operativos IC Curso 14/15 Estructuras de control. Los procedimientos del lenguaje C contienen declaraciones e instrucciones. Ya hemos analizado las declaraciones; así que ahora estudiaremos las instrucciones. La asignación, el if, y las instrucciones while son en esencia iguales a las de otros lenguajes. A continuación mostramos algunos ejemplos: if (x < 0) k = 3 ; /* if de una rama con una unica instruccion */ if (x > y) { /* if de una rama con varias instrucciones */ j = 2 ; k = j + 1 ; } if (x + 2 < y) { /* if de dos ramas */ j = 2 ; k = j – 1 ; } else { m = 0 ; } while (n > 0) { /* instruccion while normal */ k = k + k ; n = n – 1 ; } do { /* k = k n = n } while instruccion while tipo repeat */ + k ; – 1 ; (n > 0) ; En los ejemplos anteriores hay que destacar que se utilizan llaves para agrupar instrucciones, y que la instrucción while tiene dos formas, la segunda de ellas similar a la instrucción repeat de Pascal. El lenguaje C tiene una instrucción for que difiere de la instrucción for de otros lenguajes. Su forma general es: for ( inicialización ; condición ; expresión ) instrucción ; El significado de la instrucción es el mismo que el de la siguiente instrucción while: inicialización ; while ( condición ) { instrucción ; expresión ; Como ejemplo, consideremos la instrucción for ( i = 0 ; i < n ; i = i + 1 ) a[i] = 0 ; Esta instrucción pone a cero las n primeras componentes de la tabla a. Comienza inicializando el índice i a cero antes de comenzar el bucle. Después mientras que i < n, se ejecuta la asignación a[i] = 0 y se incrementa i. La instrucción que se repite en el for puede por supuesto ser un bloque de instrucciones delimitado por llaves. El lenguaje C tiene una construcción semejante a la instrucción case de Pascal, a la que se denomina instrucción switch. Por ejemplo: Página - 5 E.U. de Informática-U.P.M. switch (k) { case 10 : i = 6 ; break ; case 20 : j = 2 ; k = 4 ; break ; default : j = 5 ; } Sistemas Operativos IC Curso 14/15 /* no continuar con case 20 */ En el switch según el valor de la expresión que va después de la palabra switch, se escoge una cláusula u otra. Si el valor de la expresión no coincide con ninguno de los casos, se selecciona la cláusula default. Si el valor de la expresión no coincide con ningún caso y no hay cláusula default entonces el control simplemente continúa con la siguiente instrucción después del switch. Un aspecto a tener en cuenta es que después de ejecutar cualquiera de los casos, el control simplemente continúa con el caso que aparezca a continuación, a menos que nos encontremos con una instrucción break. En la práctica lo más normal es que en la mayoría de los casos requieran una instrucción break. La instrucción break también tiene validez en el interior de los bucle for y while, de forma que al ejecutarse el control salga del bucle. Si la instrucción break está en el bucle más interno de una serie de bucles anidados, entonces el control sale tan sólo del bucle más interno en el que se encuentre. Una instrucción relacionada con la instrucción break es la instrucción continue, la cual se limita a ceder el control al final del bucle de manera que termine la iteración actual y comience de inmediato la siguiente, pero sin salir del bucle. El lenguaje C tiene procedimientos, a los cuales se les puede llamar indicando los parámetros que necesitan para ejecutarse. En algunos compiladores de C no se permite pasar como parámetros tablas, registros o procedimientos, aunque por supuesto sí se permite pasar como parámetros punteros a objetos de esos tipos. El nombre de una tabla, cuando se escribe sin referirse a una componente, se interpreta como un puntero a la tabla, haciéndose así más sencillo el paso de parámetros de tipo tabla. Por lo tanto si a es el nombre de una tabla de cualquier tipo, podemos pasar como parámetro a un procedimiento g un puntero a esa tabla escribiendo: g(a) ; Esta regla se cumple sólo con tablas, no con registros. Los procedimientos pueden devolver valores utilizando la instrucción return. esta instrucción puede incorporar una expresión que se devuelve como valor de la llamada al procedimiento (función), pero el solicitante puede ignorarlo si lo desea. Si un procedimiento retorna un valor, el tipo del valor se escribe antes del nombre del procedimiento, como se muestra en el ejemplo siguiente: Página - 6 E.U. de Informática-U.P.M. int sum(int i, int j) Sistemas Operativos IC Curso 14/15 /* este procedimiento produce como resultado un entero */ /* i y j son los parametros formales */ { return (i + j) ; /* sumar los parametros y retornar la suma */ } Como sucedía con los parámetros, el lenguaje C no permite retornar ni tablas, ni registros, ni procedimientos, aunque sí pueden devolver punteros a todos ellos. Esta regla está diseñada para hacer eficiente la implementación ( todos los parámetros y el resultado caben en una sóla palabra de la máquina). Los compiladores que admiten registros como parámetros también suelen admitirlos como resultados de procedimientos. El lenguaje C no tiene integrada ninguna instrucción de entrada/salida. La E/S se lleva a cabo llamando a procedimientos disponibles en bibliotecas; a continuación se muestran los procedimientos más comunes: printf(“x = %d y = %o z = %x\n”, x, y, z) ; El primer parámetro es una cadena de caracteres entre comillas (es en realidad una tabla de caracteres). Cualquier carácter que no sea el de tanto por ciento (%) simplemente se imprime como está. Cuando se encuentra un tanto por ciento, se imprime el siguiente parámetro, donde la letra que sigue al tanto por ciento indica cómo imprimirlo: d – se imprime como un entero decimal o – se imprime como un entero octal u – se imprime como un entero decimal sin signo x - imprime como un entero hexadecimal s – se imprime como una cadena c – se imprime como un carácter individual Las letras D, O y X también se admiten para imprimir valores de tipo long en decimal, octal y hexadecimal. 1.5 Expresiones Las expresiones se construyen mediante la combinación de operandos y operadores. Los operadores aritméticos, como + y -, y los operadores de relación < y > son similares a sus semejantes en otros lenguajes. El operador binario % se corresponde con el operador de cálculo del resto módulo un valor dado. Es importante señalar que el operador de igualdad es el operador ==, mientras que el de desigualdad es !=. Para ver durante el programa si dos valores son iguales y actuar en consecuencia podemos escribir: if (a == b) then instrucción ; El lenguaje C permite también la combinación de asignaciones y operadores, de manera que: a += 4 ; significa lo mismo que: a = a + 4 ; Página - 7 E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 Los otros operadores pueden combinarse también de esta manera. Otro importante grupo de operadores lo constituyen los operadores unarios. El & (ampersand) como operador unario proporciona la dirección donde está ubicada la variable a la que se le aplica. Si p es un puntero a un entero e i es un entero, la instrucción: p = &i ; determina la dirección de i y la almacena en la variable p. El operador inverso al & proporciona, dado un puntero, el valor del objeto al que apunta. Si se ha asignado la dirección de i a p, entonces *p tiene el mismo valor que i. En otras palabras, como operador unario, el asterisco va seguido de un puntero (o una expresión que produce como resultado un puntero) y devuelve como resultado el valor señalado por ese puntero. Si i vale 6, entonces la instrucción: j = *p ; asignará 6 a j. El operador ! da como resultado 0 si su operando tiene un valor distinto de 0, y 1 si su operando tiene un valor igual a 0. Se utiliza principalmente en instrucciones if, como operador de negación. Por ejemplo: if ( !x ) k = 8 ; evalúa el valor de x. Si x es cero (falso), a k se le asigna el valor 8. En efecto, el operador ! niega la condición que le sigue, tal como el operador not lo hace en Pascal. El operador sizeof indica cuán grande es su operando en cuanto a ocupación en la memoria. Si se aplica a una tabla a de 20 enteros, en una máquina con enteros de 4 bytes, por ejemplo, sizeof a dará como resultado 80. Cuando se aplica a un registro, sizeof indica igualmente el tamaño del registro. El último grupo de operadores está formado por los operadores de incremento y decremento. La instrucción: p++ ; significa el incremento de la variable p. La cantidad en que se incrementa depende del tipo de p. Los enteros y caracteres se incrementan en 1, pero los punteros se incrementan en el tamaño de los objetos a los que apuntan. Por lo tanto, si a es una tabla de registros y p es un puntero a uno de esos registros, y escribimos: p = & a[3] ; para hacer que p apunte a una de las componentes de la tabla, entonces, después de que se incremente p, p apuntará a a[4] sin importar lo grandes que sean los registros de la tabla. La instrucción: p-- ; Página - 8 E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 es análoga, salvo que decrementa en vez de incrementar. En la asignación n = k++ ; donde ambas variables son enteros, el valor original de k se asigna a n y después tiene lugar el incremento. En la asignación: n = ++k ; primero se incrementa k, después su nuevo valor se almacena en n. Por lo tanto, el operador ++ (o --) se puede escribir antes o después de su operando, con diferentes significados. 1.6 Estructura de un programa Un programa en C consta de uno o más archivos que contienen procedimientos y declaraciones. Estos archivos se pueden compilar por separado, produciendo archivos objeto individuales, que después se enlazan (por el enlazador) para formar el programa ejecutable. A diferencia de Pascal, las declaraciones de procedimientos no se pueden anidar, de manera que figurarán en el “nivel superior” del archivo. Se permite declarar variables fuera de los procedimientos, por ejemplo, al inicio de un archivo antes de la primera declaración del procedimiento. Estas variables son globales y se pueden emplear en cualquier procedimiento de todo el programa, a menos que la palabra reservada static preceda la declaración, en cuyo caso no se permite utilizar las variables en otro archivo. Las mismas reglas se aplican a los procedimientos. Las variables declaradas dentro de un procedimiento son locales al procedimiento en el cual se declaran. Desde un procedimiento puede accederse a una variable entera, v, declarada en un archivo diferente del propio (siempre que la variable no sea static), haciendo la siguiente declaración extern int v ; La declaración extern meramente sirve para indicar al compilador qué tipo de variable tiene; las declaraciones extern no asignan ningún tipo de memoria adicional. Cada variable global debe declararse exactamente una vez sin el atributo extern, con el fin de asignarle memoria. Las variables se pueden inicializar en su declaración. Por ejemplo: int size = 100 ; Las tablas y los registros también pueden incializarse en su declaración, Las variables globales que no se inicializan explícitamente obtienen por omisión el valor cero. 1.7 El preprocesador de C. Antes de que un archivo fuente sea procesado por el compilador de C, el archivo fuente es tratado por un programa denominado preprocesador. La salida del preprocesador, no es el Página - 9 E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 programa original, y se lleva como entrada al compilador. El preprocesador efectúa unas transformaciones importantes sobre el archivo fuente antes de pasárselo al compilador: 1. Incluye los archivos indicados en las directivas include. 2. Almacena y expande las macros incluidas en el archivo fuente. Las directivas del preprocesador comienzan con el símbolo # (almohadilla) en la columna 1. Cuando en el programa fuente se encuentra una directiva de la forma #include “file.h” el preprocesador incluye por completo el archivo file.h en el fichero de entrada al compilador. Cuando la directiva se escribe como: #include <file.h> se busca el archivo file.h en el directorio /usr/include en vez de en el directorio de trabajo. Es una práctica común en C agrupar declaraciones que utilizan varios archivos, en un archivo de encabezado (de cabecera) (por lo general con el sufijo .h) e incluirlas en donde se necesiten. El preprocesador también permite definiciones de macros. Por ejemplo: #define TAMAÑO_BUFER 1024 define una macro TAMAÑO_BUFER dándole el valor 1024. Desde ese punto, todas las ocurrencias de la cadena de 10 caracteres TAMAÑO_BUFFER del archivo se sustituirán por la cadena de cuatro caracteres 1024 antes de que el compilador procese el archivo. Todo lo que sucede aquí es que una cadena de caracteres se sustituye por otra. Por convenio, los nombres de macros se escriben en mayúsculas. Las macros pueden tener parámetros, aunque en la práctica muy pocas los tienen. 1.8 Construcciones particulares de C Existen algunas construcciones que son características del lenguaje C, y que no tienen una correspondencia en otros lenguajes de programación. Para comenzar tengamos en cuenta el bucle: while (n--) *p++ = *q++ ; las variables p y q son punteros a caracteres y n es un contador. Lo que hace el bucle es copiar una cadena de n caracteres de la posición señalada por q a la posición ocupada por p. En cada vuelta del bucle, se decrementa el contador, hasta que llega a cero y cada uno de los punteros se incrementa, de manera que señalen en forma sucesiva a las posiciones siguientes. Otra construcción común es for ( i = 0 ; i < N ; i++ ) a [i] = 0 ; que pone a cero las N primeras componentes de la tabla a. Una forma alternativa de hacer lo mismo sería: Página - 10 E.U. de Informática-U.P.M. Sistemas Operativos IC Curso 14/15 for ( p = &a[0] ; p < &a[N] ; p++ ) *p = 0 ; En esta segunda formulación, el puntero entero, p, se inicializa para señalar a la primera componente de la tabla. El ciclo prosigue en tanto que p no haya llegado a la dirección de a[N]. En cada iteración se pone a cero una nueva componente. El segundo for que utiliza un puntero es mucho más eficiente que el primero, por lo que se utiliza muy frecuentemente. Las asignaciones pueden figurar en sitios inesperados. Por ejemplo en la instrucción: if (a = f(x)) instrucción ; se llama primero a la función f, después se asigna el resultado de esa función a a, y por último se comprueba si a es cierta (distinta de cero) o falsa (cero). Si a es distinta de cero, se ejecuta la instrucción. La instrucción: if (a = b) instrucción ; es análoga en cuanto que asigna b a a y después prueba a para ver si es distinta de cero. Esta es completamente diferente de: if (a == b) instrucción ; que compara dos variables y ejecuta la instrucción en caso de que sean iguales. Página - 11