1 breve introducción al lenguaje c

Anuncio
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
Descargar