Apuntes de C

Page 1

Lenguaje de Programación C

LENGUAJE DE PROGRAMACIÓN C

1.

Estructura de un programa

……………………………………

1

2.

Tipos de datos en C

……………………………………

2

3.

Constantes

……………………………………

3

4.

Identificadores

……………………………………

4 5.

Variables

……………………………………

4

6.

Modificadores

……………………………………

7

7.

Instrucción de asignación

……………………………………

8

8.

Operadores

……………………………………

8

9.

Conversiones de tipos

……………………………………

11

10.

Funciones de E/S

……………………………………

12

11.

Sentencias de control

……………………………………

15

11.1. Selección

……………………………………

15

11.2. Bucles

……………………………………

19

11.3. Sentencias de salto

……………………………………

21

Arrays y cadenas

……………………………………

21

12.1. Introducción

……………………………………

21

12.2. Arrays unidimensionales

……………………………………

22

12.3. Cadenas

……………………………………

23

12.4. Arrays bidimensionales

……………………………………

25

12.5. Arrays multidimensionales

……………………………………

26

12.6. Inicialización de arrays

……………………………………

26

12.7. Arrays y punteros

……………………………………

27

12.8. Paso de arrays a funciones

……………………………………

28

Punteros

……………………………………

30

12.

13.

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. i


Lenguaje de Programación C

14.

15.

13.1. Introducción

……………………………………

30

13.2. Operaciones con punteros

..…...……………………………..

30

13.3. Punteros a punteros

……………………………………

35

13.4. Problemas con punteros

……………………………………

36

Funciones

……………………………………

37

14.1. Introducción

……………………………………

37

14.2. Prototipos

……………………………………

38

14.3. Paso de parametros

……………………………………

39

14.4. Argumentos de main: argv y argc

………………….………..……….. 39

Estructuras de datos

……………………………………

40

15.1. Estructuras

……………………………………

40

..………………………………

41

……………………………………

43

15.1.1. Operaciones con estructuras 15.1.2. Estructuras complejas

15.1.3. Paso de estructuras a funciones

16.

………………………………..

44

15.2. Campos de bits

……………………………………

46

15.3. Uniones

……………………………………

47

15.4. Enumeraciones

……………………………………

48

15.5. Typedef

…………………………………… 49

Gestión de memoria dinámica

……………………………………

50

16.1. Funciones

……………………………………

50

16.2. Arrays dinámicos

……………………………………

51

16.3. Estructuras de datos dinámicas

…………….…………………..….. 52

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. ii


Lenguaje de Programación C

1.

ESTRUCTURA DE UN PROGRAMA

Un programa en C es una colección de una o más funciones. Una función es una subrutina que contiene una o más sentencias, y que lleva a cabo una o más tareas. Cada función tiene un nombre y una lista de argumentos. En general se le puede dar a cada función el nombre que se quiera, excepto el de main, que está reservado para la función que inicia la ejecución del programa (denominada función principal) y siempre debe de estar presente en cualquier programa de C. Estructura de un programa en C: Directivas de preprocesador (para incluir otros ficheros, definiciones de constantes y macros) Declaraciones globales (variables globales, prototipos de funciones, etc) tipo main ( lista_parametros ) { Declaraciones de variables locales Sentencias } tipo_retorno1 nombre_funcion1 ( lista_parametros ) { Declaraciones de variables locales Sentencias } …. tipo_retornoN nombre_funcionN ( lista_parametros ) { Declaraciones de variables locales Sentencias } El compilador acepta cualquier patrón de saltos de línea y sangrías, sin embargo, los programas siempre deben organizarse de modo que sean fáciles de leer. Primer programa en C // Un primer programa en C #include <stdio.h> int main() { printf (“Empezamos con C”); return 0; } Explicación del programa:

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 1


Lenguaje de Programación C

17. Comentarios de una línea: cuando se usa un par de barras inclinadas seguidas (//) se está indicando que el resto de la línea debe ser considerado como un comentario, es decir no causan ninguna acción al ejecutarse el programa sino que se añade para hacer más comprensible y legible el programa. (También existen los comentarios que contienen varias líneas, se inician con /* y terminan con */.) 18. Las líneas que se inician con # son directivas del preprocesador. En concreto esta directiva include le dice al preprocesador que durante el lincaje incluya las funciones asociadas a la librería especificada entre < y > (en este caso es la librería estándar de entrada/salida stdio, que debe incluirse para que el programa pueda enviar salida a la pantalla o aceptar datos del teclado). 19. La línea int main () contiene la cabecera de la función main e indica el comienzo del programa. Los paréntesis se usan para transferir parámetros a las funciones. En concreto, en este programa, la función principal no requiere transferir ninguna información, por lo tanto el contenido de los paréntesis es nulo. 20. Las llaves { }, indican el comienzo y final de una función, o en general, de un grupo de sentencias que constituyen un bloque. Cada { debe tener su correspondiente }, delimitando un bloque. Dentro de cada bloque pueden existir otros bloques anidados. 21. El símbolo de punto y coma denominado terminador de sentencia forma parte de las sentencias, y va siempre al final de cada una. 22. Se utiliza una función estándar denominada printf ( ) que se encarga de imprimir por la salida estándar, normalmente es la pantalla. Los paréntesis indican que es una función, y tolo lo encerrado entre ellos es la información que se transfiere. 23. La sentencia return, denominada instrucción de retorno, termina la ejecución del programa y devuelve el control al sistema operativo. El número 0 se utiliza para indicar que el programa ha terminado correctamente. 2.

TIPOS DE DATOS EN C Los tipos de datos fundamentales en C son: a) Caracteres: char. Este tipo puede contener cualquier carácter individual almacenando el número entero correspondiente a dicho carácter según el código ASCII. Por ejemplo, en arquitecturas con palabras de 16 bits, un carácter ocuparía 1 byte siendo por tanto su rango un entero entre -128 y 127, 256 elementos. b) Enteros: int. Un entero es un número sin punto decimal. Por ejemplo, en arquitecturas con palabras de 16 bits ocupan 2 bytes y su rango va de -32768 a 32767.

c) Coma flotante: representan números reales que contienen punto decimal. Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 2


Lenguaje de Programación C

float (reales en simple precisión). Por ejemplo, en arquitecturas con palabras de 16 bits ocupan 4 bytes, usando normalmente 8 bits para el exponente y 24 para la mantisa, por lo tanto un número real en simple precisión no tiene más de 7 dígitos de precisión (significativos) y puede estar comprendido en el siguiente rango: -3.402823E+38 a -1.175494E-38 para números negativos 1.175494E-38 a 3.402823E+38 para números positivos double (reales de doble precisión) Por ejemplo, en arquitecturas con palabras de 16 bits ocupan 8 bytes, dando lugar a que tenga hasta 15 dígitos de precisión (significativos) y puede estar comprendido en el rango de: -1.79769E+308 a -2.22507E-308 para números negativos 2.22507E-308 a 1.79769E+308 para números positivos

d) Nulo, vacío: void. No se pueden declarar variables de este tipo. Se usa: • para indicar que una función no devuelve valor, • para definir punteros genéricos (todavía no se sabe el tipo de dato al que va a apuntar). 3.

CONSTANTES

Representan valores fijos que no pueden ser cambiados por el programa. C tiene cuatro tipos básicos de constantes: 1.-

Constantes enteras: son números sin parte fraccional, consistentes en secuencias de dígitos. Estas constantes se pueden escribir en tres sistemas numéricos diferentes: decimal, octal y hexadecimal. Una constante entera hexadecimal consiste en 0x seguido de la constante en forma hexadecimal, mientras que una octal empieza por 0. Ejemplos: a) 0x80 128 decimal b) 0xFF c) 012 10 decimal d) 10 e) -100

2.-

Constantes en coma flotante: es un número con un punto decimal seguido de los números de la componente fraccional. Ejemplos: a) 11.231 b) 0.18 c) 5.8e-7

3.-

Constantes de carácter: es un solo carácter encerrado entre comillas simples. Ejemplos: a) 'z' b) ’3’ c) ’?’ Hay una serie de caracteres que son imposibles de introducir desde el teclado, por ejemplo salto de línea, tabulador, etc. El C ofrece la posibilidad de introducir estos caracteres especiales como constantes, mediante lo que se denomina secuencias de escape o carácter de barra invertida. Una secuencia de escape siempre comienza con una barra inclinada hacia atrás seguida por uno o más caracteres especiales. Se podría utilizar el valor correspondiente al carácter ASCII directamente, pero si se utiliza estos códigos de barra invertida se asegura la portabilidad del código. En la tabla que figura a continuación vemos algunos de los caracteres de barra invertida (secuencias de escape) más habituales:

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 3


Lenguaje de Programación C

Código \b \f \n \r \t \v \a \0 \” \’ \\

Significado Retroceso de espacio Salto de página Salto de línea Retorno de carro Tabulador horizontal Tabulador vertical Alerta(pitido) Nulo Comillas dobles Comillas simples Barra invertida

4.-

Constantes de cadena de caracteres: es una secuencia de caracteres entre doble comillas ("). No se debe confundir una constante de carácter 'a' con "a", que es una cadena que contiene sólo una letra. Más adelante se verá que esto implicará un tratamiento distinto. A veces es necesario incluir en una constante de cadena ciertos caracteres especiales (como la barra inclinada hacia atrás o las comillas) o ciertos caracteres no imprimibles (como el tabulador o el retorno de carro), para lo cual, se deben de representar en términos de sus correspondientes secuencias de escape. Ejemplos: a) “123” b) “Esto es una cadena\n”

4.

IDENTIFICADORES

Son nombres usados para referirse a variables, funciones y cualquier otro elemento definido por el usuario. Las siguientes normas son las que se deben seguir para su construcción: • el primer carácter debe ser una letra o un símbolo de subrayado; • los siguientes caracteres pueden ser letras, números o caracteres de subrayado; • se distinguen las mayúsculas de las minúsculas (A y a son variables distintas); • un identificador no puede ser igual a una palabra reservada en C y no debe tener el mismo nombre que cualquier función de la librería estándar. Ejemplos: Identificadores válidos: suma, nota_media, des1, _temperatura, TABLA, des_1 Identificadores inválidos: 1nota, suma-total

5.

VARIABLES

Una variable es una posición de memoria con un nombre que se va a usar para mantener un valor que puede ser modificado por el programa. Todas las variables en C tienen que ser declaradas antes de usarse. La forma de declarar una variable es la siguiente: [modificadores] Tipo Nombre_variable1, Nombre_variable2, ..., Nombre_variableN;

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 4


Lenguaje de Programación C

donde: modificador (opcional): indica una acción especial sobre la variable (tipo, almacenamiento o acceso). tipo: ha de ser un tipo de datos válido. lista_de_variables: consiste en uno o más nombres de variables separados por comas. Ejemplos:

a) int i, j, k;

b) float Nota_media;

c) char carácter;

Existen tres lugares donde es posible declarar las variables: • Dentro de las funciones: variables locales • En la definición de los parámetros de las funciones: parámetros formales • Fuera de todas las funciones: variables globales VARIABLES LOCALES Son aquellas que se declaran dentro de las funciones o de los bloques ({}) y por lo tanto sólo se pueden utilizar dentro de donde se definen; puesto que no son conocidas fuera de dicho bloque o función. Sólo existen mientras se ejecuta el bloque, es decir; se crean al entrar y se destruyen al salir de él. Ejemplo: en la siguiente función, las variables x y j sólo existirán durante la ejecución de la misma. void función_1(void) { int x; char j='t'; ... } Si se desea más legibilidad en el código, las variables que se necesitan en una función se deben declarar al principio de la misma. Sin embargo, es posible declarar las variables en el momento de su utilización. Ejemplos: a) void función2(void) { int nota; scanf ("%d",&nota); if ( nota >= 5 ) { char nombre; ... } } La variable nombre se crea tras comprobar que la condición del if es cierta y se destruye tras terminar dicha sentencia de selección, además dicha variable sólo es conocida dentro de ese bloque. La ventaja de declarar una variable en un bloque condicional es que sólo se dispondrá de la memoria necesaria para ella si se necesita, puesto que no existe hasta que se entra en el bloque en que está declarada. Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 5


Lenguaje de Programación C

PARÁMETROS FORMALES Si una función usa argumentos, debe declarar las variables que van a aceptar los valores de dichos argumentos. Éstas se comportan como cualquier variable local, pudiendo ser usadas dentro de la función y siendo destruidas al salir de la misma. Sus declaraciones se dan tras el nombre de la función y entre paréntesis, indicando de qué tipo son. Ejemplo: int máximo(int x, int y) { ... } Los parámetros formales deben de coincidir en tipo con los parámetros actuales (argumentos que se usan en la llamada a la función), si no es posible que se produzcan errores inesperados. Debido a que C permite bastante compatibilidad de tipos, es el programador quien la mayoría de las veces tiene que comprobar ésta. Para facilitar esta tarea es útil el empleo de prototipos de funciones. VARIABLES GLOBALES Son variables que se conocen en todo el programa y se pueden usar en cualquier parte del código, manteniendo su valor durante toda la ejecución del mismo. Se crean al declararlas en cualquier lugar que esté fuera de una función, aunque lo lógico es declararlas en la cabecera del programa. Si una variable local tiene el mismo nombre que una global, entonces todas las referencias a ese nombre dentro de la función donde ha sido declarada como local se refieren a esa variable local y no tienen efecto sobre la variable global.

1.2.3.-

Se debe evitar el uso de variables globales innecesarias, por las siguientes razones: Utiliza memoria durante todo el tiempo de ejecución del programa, no sólo cuando se necesita. Hace que las funciones sean menos independientes y por tanto no facilita la programación modular. Propicia la aparición de efectos laterales, es decir; errores secundarios desconocidos y no deseables. Ejemplo: todo ello puede verse en las dos funciones que se exponen a continuación. La primera de ellas permite calcular el máximo de dos números cualesquiera (versión parametrizada), la segunda sólo permitirá calcular el máximo de las variables r y t, que, además, deberán estar definidas con anterioridad. GENERAL int maximo(int r,int t); { if (r>t) return(r);

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

ESPECÍFICA int r,t; int mul(void) { if (r>t) return(r); Pág. 6


Lenguaje de Programación C

else return(t); } 6.

else return(t); }

MODIFICADORES

Los tipos de datos básicos pueden ser precedidos por distintos modificadores. Un modificador se usa para alterar el significado del tipo base, con el fin de que se ajuste más concretamente a las necesidades de cada momento. A)

MODIFICADORES DE TIPO Los modificadores de tipo son: signed (con signo), unsigned (sin signo), short (corto), long (largo). Todos ellos se aplican a los tipos int y char, mientras que al tipo double, sólo se le puede aplicar long. (En general tamaño (short) <=tamaño (int) ) La siguiente tabla muestra las combinaciones posibles entre los tipos básicos y sus modificadores, junto a los rangos obtenidos en arquitecturas con palabras de 16 bits.

TIPO TAMAÑO EN BYTES char (signed char) 1 unsigned char 1 int (signed/short/signed short) 2 unsigned int (unsigned short) 2 long int 4 unsigned long int 4 float 4 double 8 long double 10

RANGO MINIMO -128 a 127 0 a 255 -32768 a 32767 0 a 65535 -2147483648 a 2147483647 0 a 4294967295 7 dígitos de precisión 15 dígitos de precisión 19 dígitos de precisión

Cada tipo char tiene una representación como entero equivalente, de esta forma un char es realmente una clase especial de entero corto. B)

MODIFICADOR DE ACCESO Las variables definidas con el modificador const no pueden ser cambiadas durante la ejecución del programa, pero si se le puede dar un valor inicial bien de una forma explícita o bien por algún medio dependiente del hardware. Ejemplo: const float iva=0,12; La definición de variables const tiene dos aplicaciones básicas: 1.- Para garantizar que un programa no modifica una variable. Se ha de indicar que una variable const puede ser modificada por algún agente externo al programa, como un dispositivo hardware. 2.- Pueden proteger los argumentos de una función para que estos no sean modificados. Cuando se pasa un valor por referencia puede modificarse la variable, pero si se define como de tipo const, la variable no puede ser modificada.

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 7


Lenguaje de Programación C

7.

INSTRUCCIÓN DE ASIGNACIÓN La sintaxis de la instrucción de asignación es: nombre_variable = expresión; Ejemplos: i = 5; f = 3.2; c = ‘a’; b = false;

C permite usar el operador de asignación con cualquier expresión válida (combinación de operadores, constantes y variables), incluso las que contengan operadores relacionales y lógicos, ya que para asignar un valor se hacen dos cosas: primero se evalúa la expresión y luego se asigna el valor de dicha evaluación a la variable de la parte izquierda. C también permite incluir la instrucción de asignación dentro de otras instrucciones. Ejemplo: if ((suma=nota1+nota2)<5) printf ( "Suspenso"); else printf ( "Aprobado)”; El destino (parte izquierda) debe ser una variable o un puntero, nunca una función o una constante. Inicialización de variables: C permite inicializar una variable en el momento de su declaración. Para ello se usa la siguiente notación: [Modificadores] Tipo Nombre_variable=Constante; Ejemplos: double cuenta=9,98; int limite=10, factor=2; Las variables globales se inicializan sólo al principio del programa, mientras que las locales se inicializan cada vez que se entra en la función en la que están definidas. Asignación múltiple: C también permite mediante una sola sentencia asignar a muchas variables el mismo valor. Ejemplo: x=y=z=0; 24.

OPERADORES

Operadores aritméticos. Los operadores +,-,* y / tienen el significado habitual, y pueden aplicarse a casi todos los tipos de datos permitidos en C, incluido el char. Cuando el operador / se aplica a operandos de tipo entero o carácter, cualquier resto es truncado (resultado de tipo entero). El operador monario menos (-) multiplica su único operador por -1, es decir, cualquier número precedido por un signo menos, cambia de signo. El operador módulo o resto de división entera (%), no puede usarse con los tipos de coma flotante (float, double). Los operadores de incremento (++ (añade uno a su operando)) y decremento (-- (resta uno a su operando)) pueden preceder o seguir al operando: i++ o ++i. La diferencia entre ellos consiste en que cuando el operador precede a la variable, el compilador realiza la operación antes de utilizar el valor de dicha variable, mientras que en el caso de que el operador siga a la Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 8


Lenguaje de Programación C

variable, el compilador realiza la operación después de usar la variable (cambia el valor por ejemplo en una asignación). Ejemplo: las siguientes instrucciones ilustra la diferencia citada anteriormente: a) i=5; j=++i; /* i=6, j=6 */ b) i=5; j=i++; /* i=6, j=5 */ La prioridad o precedencia de los operadores aritméticos es la siguiente: ++, -mayor -(unario) * , /, % +, menor Los operadores del mismo nivel de precedencia son evaluados por el compilador de izquierda a derecha, siempre y cuando no se usen paréntesis, puesto que estos alteran el orden de evaluación ya que fuerzan a que una operación, o un conjunto de operaciones, tengan un nivel de precedencia mayor. Operadores relacionales. Los operadores relacionales son: <, >, >=, <=, == (igual), != (distinto) y se pueden aplicar a operandos de cualquier tipo de dato (char, int, float y double). Operadores lógicos. Los operadores lógicos son: &&

(Y),

||

(O) ,

!

(No)

Las expresiones que utilizan los operadores relacionales y lógicos devuelven un valor que es o verdadero o falso. En C algo es verdadero siempre que sea distinto de cero y falso si es cero. verdadero = cualquier valor que no sea nulo falso = cero o nulo Tanto los operadores relacionales como los lógicos tienen un nivel de precedencia menor que los aritméticos. La prioridad entre ellos es:

! > >= < <= == != && ||

mayor

menor

De nuevo se pueden usar paréntesis para alterar el orden natural de evaluación en una expresión relacional o lógica. Operadores de asignación adicionales C ofrece una serie de operadores de asignación que actúan como una notación abreviada para expresiones utilizadas con frecuencia. Éstos son los siguientes:

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 9


Lenguaje de Programación C

Operador += -= *=

Equivalencia a += b a=a+b a -= b a=a-b a *= b a=a*b

/=

a /= b

%=

a %= b

Ejemplo: a) c) e)

Descripción Suma b y a y el resultado se lo asigna a la variable a. Resta b de a y el resultado se lo asigna a la variable a. Multiplica a por b y el resultado se lo asigna a la variable a a = a / b Divide a entre b y el resultado se lo asigna a la variable a. a = a % b Le asigna a la variable a el resto de la división de a entre b.

x = x+10; equivale a x += 10; x = x*10; equivale a x *= 10; x = x %10; equivale a x %= 10;

b) d)

x = x-10; equivale a x -= 10; x = x /10; equivale a x /= 10;

Operadores de Direccionamiento e Indireccionamiento. Un puntero es la dirección de memoria de una variable. Una variable puntero se declara para contener un punter,o o sea una dirección a otra variable de un tipo específico. Existen dos operadores básicos para el tratamiento de variables de tipo puntero: a) & es un operador monario que devuelve la dirección de memoria del operando. Ejemplo: m = &cont; coloca en m la dirección de memoria de la variable cont. b) * es también un operador monario que devuelve el valor de la variable ubicada en la dirección que se especifica. Ejemplo: si m contiene la dirección de memoria de la variable cont, entonces la instrucción q = *m; colocará el valor de cont en q. Estos dos operadores tienen mayor precedencia que cualquier operador aritmético, excepto el menos monario, respecto del cual la tienen igual. Las variables que vayan a contener direcciones de memoria (o sea, que sean punteros) deben declararse antes de ser usadas, mediante el siguiente formato: tipo_dato_apuntado * Nombre_variable_puntero; el * delante del nombre de la variable le indica al compilador que dicha variable va a contener un puntero a ese tipo de dato. Ejemplo: para declarar c como puntero a un carácter se tendrá que escribir: char *c; El tipo de dato al que apunta un puntero se denomina tipo base del puntero, pero la variable puntero contiene la dirección de un objeto de dicho tipo base. Además, un puntero sólo se puede usar para apuntar a datos que sean del tipo base de dicho puntero. Se pueden mezclar declaraciones de puntero y variables normales en la misma sentencia. Ejemplos: int x, *y; declara x como de tipo entero, e y como puntero a un tipo entero. El siguiente programa muestra cómo se utilizan los operadores anteriores para copiar el contenido de una variable mediante su dirección. #include <stdio.h> int main() Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 10


Lenguaje de Programación C

{

int x, copiax; int *punt; x=10; punt=&x; copiax=*punt;

/* punt contiene la dirección de x */ /* copiax va a contener el valor almacenado en la dirección contenida en punt */ printf (“Valor: %d “, copiax); /* imprimirá 10 */ return 0; } 25.

CONVERSIONES DE TIPOS

Las conversiones de tipos se utilizan para convertir un valor de un tipo a otro sin cambiar el valor que representa. Pueden ser: a) implícitas (ejecutadas automáticamente): C realiza estas conversiones en los siguientes casos: a. cuando se asigna un valor de un tipo a una variable de otro tipo: se convierte el valor del lado derecho de la asignación al tipo del lado izquierdo, pudiendo producirse pérdidas de información o cambios en la representación interna del valor. La siguiente tabla muestra las conversiones de tipo que se realizan automáticamente y las consecuencias que se obtendría si se usa una arquitectura de palabras de 16 bits: Tipo destino

Tipo de expresión

Posible pérdida de información (palabras de 16 bits)

signed char

char

Si valor>127, destino negativo

char

short int

8 bits más significativos

char

int

8 bits más significativos

char

long int

24 bits más significativos

short int

int

Nada

short int

long int

16 bits más significativos

int

long int

16 bits más significativos

int

float

Parte fraccional y posiblemente más

float

double

Precisión, resultado redondeado

double

long double

Precisión, resultado redondeado

Ejemplo:

int i; char ch; float f;

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 11


Lenguaje de Programación C

ch=i; i=f; f=ch; f=i;

/* se pierden los bits más significativos de i*/ /* se pierde la parte fraccionaria de f*/ /* cambio de formato, sin pérdida*/ /* cambio de formato, sin pérdida*/

b. cuando se combinan tipos mixtos en expresiones: se aplica la regla de promoción integral, que consiste en convertir los operandos al tipo del mayor operando. c. cuando se pasan argumentos a funciones. b) explícitas (solicitadas de forma específica por el programador): aplicando el operador molde se puede forzar a que un valor sea de un tipo determinado. La forma general de un molde es: (tipo) expresión donde tipo es uno de los tipos estándar o uno de los definidos por el usuario. Por ejemplo, si se desea asegurar que la expresión x/2 se evalúe como un float aunque x sea un int, se debe escribir la siguiente sentencia: (float) x / 2 Los modes son considerados como operadores monarios y tienen la misma precedencia que cualquier otro operador monario. El siguiente fragmento ilustra la utilización de moldes, puesto que utiliza el mismo entero para controlar un bucle y para realizar un cálculo que requiere parte fraccionaria. #include <stdio.h> void main(void) { /*imprime i e i/2 con fracciones */ int i; for(i=1;i<=100;++i) printf("%d/2 es %f\n",i,(float)i/2); } 26.

FUNCIONES DE E/S

La biblioteca de funciones incluye un cierto número de funciones de entrada/salida, a las que se puede acceder desde cualquier lugar de un programa escribiendo el nombre de la función, seguido de una lista de argumentos entre paréntesis. Las funciones de E/S más comunes son las dos siguientes: scanf

int scanf(const char *cadena_formato, arg1, arg2, …, argn)

Permite leer la entrada desde el dispositivo de entrada estándar, siendo la entrada cualquier combinación de valores numéricos, caracteres sueltos y cadenas de caracteres, es decir, lee cualquier tipo de datos y convierte los números de forma automática a su representación interna apropiada. La función devuelve el número de datos que se han conseguido introducir Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 12


Lenguaje de Programación C

correctamente y si se encuentra un fin de archivo prematuro, devuelve EOF. Cadena_formato hace referencia a una cadena de caracteres que contiene cierta información sobre el formato de los datos. Para ello, esta información va a estar dividida en tantos grupos de caracteres como datos se van a leer, puesto que cada uno de ellos especifica el formato de los mismos. Cada grupo de caracteres comienza con el signo de porcentaje (%), seguido de un carácter de conversión que indica el tipo de dato correspondiente. La siguiente tabla muestra los caracteres de conversión más usuales: Código %c %d %e %f %h %o %s %x %p %u

Significado Leer un único character Leer un entero decimal Leer un número en notación científica (notación exponencial) Leer un número en coma flotante Leer un entero corto Leer un número octal Leer una cadena de caracteres Leer un número hexadecimal Leer un puntero Leer un entero sin signo en base decimal

Un carácter en blanco en la cadena de formato hace que esta función salte uno o más caracteres en blanco de la secuencia de entrada. Un carácter en blanco es un espacio, una tabulación o un carácter de salto de línea. En definitiva, un carácter en blanco en la cadena de formato hace que esta función lea, pero no guarde, cualquier número de espacios en blanco hasta que encuentre el primer carácter que no sea de ese tipo. Por ejemplo, supóngase la siguiente llamada a la función scanf: scanf(“%s “, nombre); , entonces esta función no termina hasta que no se introduzca un carácter de espacio en blanco después de introducir un carácter, puesto que el espacio que hay detrás de %s indica que lea y descarte caracteres de espacios, tabulaciones y saltos de línea. Un carácter distinto de espacio en blanco en la cadena de formato hace que la función lea y descarte los caracteres que coincidan con el mismo en la secuencia de entrada. Por ejemplo “%d,%d” hace que la función lea un entero, lea y descarte una coma y posteriormente lea otro entero. Si no encuentra el carácter especificado, la función termina su ejecución. Otro ejemplo puede ser el siguiente, supóngase que la llamada a la función es : scanf(“%st%s”, &x, &y); y que la secuencia de entrada es “10t20” , entonces en x se colocaría 10 en y se colocaría 20 y la t se descartaría debido a la presencia de este carácter en la cadena de formato. Para cada uno de los datos que se introduce es necesario especificar la dirección donde van a ser almacenados, es decir los argumentos son punteros a las variables que se usan. Esta es la forma con la que se consigue realizar un paso por referencia y permitir que una función altere el contenido de un argumento. Por ejemplo, para leer un entero en la variable cuenta, se utiliza la siguiente llamada a la función scanf: scanf(“%d”, &cuenta); Los elementos de datos de entrada deben estar separados por espacios, tabulaciones o Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 13


Lenguaje de Programación C

saltos de línea. Los símbolos de puntuación, tales como las comas, los punto y comas y similares no cuentan como separadores. Es decir, si la llamada a la función scanf es: scanf(“%d%d”, &r, &c); y la entrada es “10,20”, entonces la llamada falla. Además, los códigos de formato deben corresponderse ordenadamente con las variables que van a recibir la entrada de la lista de argumentos. Las cadenas se asignan a vectores de caracteres y el propio nombre del vector (sin índice) es la dirección del primer elemento del vector. Por lo tanto, para leer una cadena en un vector de caracteres se utiliza sólo el nombre del vector sin tener que ir precedido por el operador &. Supóngase el siguiente ejemplo: char cad[80]; scanf(“%s”, cad); Existe el modificador longitud máxima de campo que consiste en un entero situado entre el % y el código de la orden de formato, que limita el número de caracteres leídos para ese campo. Por ejemplo, “%20s” leerá como máximo 20 caracteres. De esta forma, si la entrada tiene más caracteres que los indicados, una llamada posterior a la función scanf comenzará a leer donde la anterior acabó. printf

int printf(const char *cadena_formato, arg1, arg2, …, argn)

Escribe cualquier combinación de valores numéricos, caracteres sueltos y cadenas de caracteres por el dispositivo estándar de salida, usando para ello formato. La cadena de formato puede contener caracteres que se muestran en la pantalla y órdenes de formato que definen la forma en que se muestran los argumentos. Una orden de formato empieza con un signo de porcentaje y va seguido por el código del formato. La siguiente tabla muestra las órdenes de formato más frecuentes: Código %c %d %e %f %o %s %u %x %% %p

Formato Visualiza un carácter Visualiza un entero decimal con signo Visualiza un real en notación científica Visualiza un real en decimal Visualiza un entero octal, sin el cero inicial Visualiza una cadena de caracteres Visualiza un entero decimal sin signo Visualiza un entero hexadecimal sin el prefijo 0x Visualiza un signo de porcentaje Visualiza un puntero

Los argumentos pueden ser constantes, referencias a funciones, variables simples o nombres de formaciones o expresiones más complejas. Estos argumentos no representan direcciones de memoria y por tanto no son precedidos de &. Las órdenes de formato pueden tener los siguientes modificadores: Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 14


Lenguaje de Programación C

• especificador de longitud mínima de campo: es un entero situado entre el signo % y el código de formato. Este modificador provoca que se rellene la salida con espacios para asegurar que el campo alcanza una cierta longitud mínima. Si se desea rellenar con ceros, es necesario poner un cero antes de este especificador. En el caso de que la cadena o el número sea más largo que la longitud especificada, se visualizara en toda su longitud, aunque sobrepase el mínimo. Por ejemplo, %05d rellena con ceros un número que tenga menos de 5 dígitos para que su longitud sea cinco. • especificador del número de posiciones decimales: consiste en colocar un punto tras el especificador de longitud de campo, seguido del número de decimales que se desea visualizar. Por ejemplo, %10.4f visualiza un número de al menos 10 caracteres con cuatro posiciones decimales. Si este modificador es aplicado a cadenas o a enteros, el significado consiste en especificar la longitud máxima del campo. Por ejemplo, %5.7s visualiza una cadena de al menos cinco caracteres y no más de siete. Si la cadena es más larga que la longitud máxima se truncarán los caracteres finales de la cadena. • especificador de ajuste a la izquierda. Por omisión, todas las salidas se ajustan a la derecha, es decir, si la longitud del campo es mayor que el dato a imprimir, el dato se sitúa en la parte derecha del campo. Sin embargo, se puede forzar a que la salida se ajuste a la izquierda situando un signo menos directamente después del %. Por ejemplo, %-10.2f hace que se ajuste a la izquierda un número real con dos espacios para decimales en un campo de diez caracteres. 27.

SENTENCIAS DE CONTROL

La mayoría de las sentencias de control se basan en una prueba condicional que determina la acción a realizar. Dicha prueba condicional produce un valor que bien es cierto o falso. En C cualquier valor distinto de cero es cierto, y el cero es el único valor que representa falso. 11.1. SELECCIÓN El C soporta dos tipos de sentencias de selección, simple y múltiple: if y switch. Selección simple: if La forma general de la sentencia if es: if(expresión) sentencia1; else sentencia2; donde tanto sentencia1 como sentencia2 pueden ser una instrucción simple o un bloque de instrucciones (conjunto de instrucciones encerradas entre { }) como se denota a continuación: if(expresión) { Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 15


Lenguaje de Programación C

secuencia de sentencias } else { secuencia de sentencias } La parte else es opcional y la expresión que se escribe entre paréntesis, debe ser una expresión numérica, relacional o lógica. El resultado que se obtiene al evaluar dicha expresión es verdadero (no cero) o falso (cero), dando como consecuencia que se ejecute sentencia1 si el resultado es verdadero y sentencia2 si es falso. Ejemplo: El siguiente programa solicita dos números y realiza la división entre ellos. Para ello, se va a usar una expresión numérica con la que se comprueba que el divisor no es cero. #include <stdio.h> void main() { int a, b; printf ("introduzca dos números \n"); scanf ("%d%d",&a,&b); if(b) /* if(b != 0) */ printf ("%d",a/b); else printf ("No se puede dividir por cero"); } Es posible anidar ifs, es decir sentencia1 y sentencia2 pueden contener a su vez otras sentencias ifs. En C cuando existe duda sobre con que if se anida un else concreto, se sigue la siguiente regla: un else siempre se refiere al if anterior más próximo que no esté asociado ya con un else. Esta norma se puede anular utilizando llaves. Estas ideas se pueden ver reflejadas en el siguiente esquema de código: if (expresión1) if (expresión1) { if (expresión2) sentencia1; if (expresión2) sentencia1; } else sentencia2; else sentencia2; // se asocia a if(expresión2) // se asocia a if(expresión1) Usando sentencias ifs anidadas se puede obtener una construcción común denominada escala if-else-if, tal como se muestra a continuación: if (expresión1) sentencia1; else if (expresión2) sentencia2; ... else sentencia_final; donde la primera expresión que sea cierta va a determinar la sentencia que se ejecuta. Si ninguna Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 16


Lenguaje de Programación C

de las expresiones son ciertas se ejecuta el else final. El siguiente programa muestra como se usa la sentencia if para comparar dos números. if(a > b) printf ("%d es mayor que %d",a,b); else if (a < b) printf ("%d es menor que %d",a,b); else printf ("%d es igual que %d",a,b); Operador ? El operador ternario ? tiene el mismo significado que una sentencia if/else, con la restricción de que las sentencias deben ser expresiones simples. Su formato es: expresión1 ? expresión2 : expresión3 y es equivalente a la siguiente sentencia if: if(expresión1) expresión2; else expresión3; Es decir, se evalúa expresión1. Si es cierta, entonces se evalúa expresión2 y su valor se convierte en el valor de la expresión completa. Si expresión1 es falsa, entonces se evalúa expresión3 y su valor se convierte en el valor de la expresión completa. El siguiente ejemplo ilustra como se usa este operador para el cálculo del máximo de dos números: ... maximo = a>b ? a : b; printf ("El máximo es %d",maximo); ... El operador condicional (?) no sólo se usa en sentencias de asignación, sino que también es posible utilizarlo con funciones (salvo aquellas que no devuelven valor, es decir las declaradas como void), ya que estas devuelven un valor, que es el que toma la expresión (se ha de puntualizar que normalmente el valor que devuelve las funciones es ignorado). El siguiente trozo de código ilustra esta idea: a > b ? printf ("El máximo es %d",a) : printf ("El máximo es %d",b); ... Utilizando la sentencia if/else se puede reescribir éste código de la siguiente forma: if (a > b) printf ("El máximo es %d",a); else Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 17


Lenguaje de Programación C

printf ("El máximo es %d",b);

Selección múltiple: switch Esta sentencia compara el valor de una expresión con una lista de constantes enteras o de caracteres, y cuando encuentra correspondencia ejecuta las sentencias asociadas a la constante. Su formato general es: switch (expresión) { [declaraciones] case cte1: sentencia1; [break;] [case cte2:] [sentencia2;] [break;] ... [default:] [secuencia_final;] } donde: expresión es una expresión entera; ctei es una constante entera o de carácter o una expresión constante; en todos los casos, el valor resultante tiene que ser entero; sentenciai es una sentencia simple o compuesta; default determina las sentencias que se ejecutan si no se encuentra ninguna correspondencia. En definitiva, una sentencia switch comprueba el valor de la expresión con los valores de las constantes especificadas en los case, cuando se encuentra el valor adecuado se ejecuta la secuencia de sentencias asociadas a ese case, hasta que se encuentre la sentencia break o para el caso de las sentencias del default el final del switch (no es necesario este último break).

1.2.-

3.-

Con respecto a esta sentencia se ha de destacar los siguientes aspectos: La palabra default es opcional, si no está presente no se ejecuta ninguna acción al fallar todas las comprobaciones. La sentencia break es opcional, y se usa para finalizar el bloque de sentencias asociadas con cada constante, puesto que cuando se encuentra el break se salta a la siguiente línea de código después del switch. Si en un bloque case se omite el break, entonces después de ejecutar las sentencias correspondientes a ese case, se seguirán ejecutando las instrucciones del siguiente bloque case y asi sucesivamente hasta encontrar un break o el final del switch. La sentencia switch sólo puede comprobar la igualdad. (Es la diferencia que existe entre

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 18


Lenguaje de Programación C

4.5.6.-

esta sentencia y la sentencia if.) No pueden haber dos case con el mismo valor de constante. Un case puede tener sentencias vacias. La sentencias asociadas con cada case no forman un bloque en si, y por lo tanto, no se pueden declarar variables locales a esas sentencias. El siguiente ejemplo ilustra estas consideraciones: scanf (“%c”, &c) switch(c) { case '1': editar(); break; case '2': compilar(); break; case '3': depurar(); break; default: printf("Ninguna opción seleccionada"); } Anidamiento de switch:

Se puede tener un switch formando parte de la secuencia de sentencias de otro switch. Incluso si las constantes case del switch interior y del exterior contienen valores comunes, no aparecen conflictos. 11.2. BUCLES Permiten repetir la ejecución de un conjunto de sentencias hasta que se alcance una cierta condición. Bucle for Cuando se desea ejecutar una sentencia simple o compuesta, repetidamente un número de veces conocido, la construcción adecuada es la sentencia for. Su formato general es: for (inicialización; condición; incremento) sentencia; En la inicialización se suele dar un valor inicial a una variable de control del bucle. La condición indica cuando finaliza el bucle. La prueba de la condición se realiza siempre al principio, por lo tanto es posible que el cuerpo del bucle no se ejecute nunca pues la condición sea falsa al comienzo. Supóngase el siguiente ejemplo: x = 10; for ( y = 10; x != y; ++y) printf (“%d”, y); El incremento se utiliza para modificar el valor de la variable de control del bucle tras cada iteración. Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 19


Lenguaje de Programación C

El C permite muchas variaciones de la forma general del bucle for. Algunas de ellas son las siguientes: A) Permite hacer uso del operador "," con el fin de utilizar más de una variable de control del bucle. Por ejemplo: for (i=0, j=99; i<j; i++, j--) ... B) Cada una de las tres secciones del for puede consistir en cualquier expresión válida de C. for (inicilizar(); condicion(); actualizar()) ; C) Pueden omitirse todas las secciones de definición del bucle puesto que son opcionales. Los siguientes for muestran esta variante: 1.for(;;) ; 2.x=10; for(; x!=20;) ++x; Bucle while while(condición) sentencia; condición sentencia

es cualquier expresión numérica, relacional o lógica. puede estar vacía, o ser una sentencia simple o un bloque de sentencias.

Las acciones se repiten mientras la condición es cierta, al finalizar el control continua con la sentencia siguiente al final del bucle. En el while, al igual que en el for, se comprueba la condición al principio, con lo que se posibilita que el bucle no se ejecute ninguna vez. Las siguientes sentencias while muestran distintos ejemplos de la misma: 1.while (!final()) ...; 2.while (1) sentencia; /* bucle infinito */ Bucle do while do sentencia; while (condición); Se repite sentencia mientras la condición sea verdadera (hasta que la condición se haga falsa). La evaluación de la condición se hace al final del bucle. Así las sentencias del bucle siempre se ejecutan al menos una vez. El siguiente fragmento de código muestra un ejemplo de como se utiliza esta sentencia: do { ... printf(“Acabar (S/N)”); scanf (“%c”, &c); } while (c!='S’ && c!='s'); Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 20


Lenguaje de Programación C

11.3 SENTENCIAS DE SALTO Estas sentencias hacen que el programa interrumpa su ejecución normal y pase a ejecutar una determinada sentencia. break Se puede utilizar con cualquiera de las tres formas de bucle y con la sentencia switch. Produce un salto en el control del programa, de manera que se evita el resto del bucle o switch que lo contiene, y se reanuda la ejecución con la siguiente sentencia a continuación de dicho bucle o switch. Por ejemplo: for (;;) { ... if (cond) break; } return Se utiliza para indicar el valor de retorno de las funciones. Puede no llevar ningún valor asociado y no es necesario en funciones void. continue Fuerza una nueva iteración del bucle y se salta cualquier código que haya hasta el final del cuerpo del mismo. La diferencia en utilizarlo es bucles for es que es estos hace que se ejecute el incremento, antes de evaluar la condición y si procede continuar la ejecución del cuerpo del bucle. exit Fuerza la terminación del programa. En realidad no es una sentencia, sino una función de la biblioteca estándar. void exit (int código_de_vuelta); // esta en stdlib.h En código_de_vuelta se indica un código que se devuelve al proceso de llamada (posiblemente al sistema operativo), y que indica el tipo de terminación (0 es terminación normal). 28.

ARRAYS Y CADENAS 12.1 INTRODUCCIÓN

Un array es un conjunto de datos del mismo tipo y con el mismo tipo de almacenamiento que son referenciados por un nombre común. A cada elemento individual se accede mediante el nombre del array seguido por uno o más índices encerrados entre corchetes. Cada índice consiste Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 21


Lenguaje de Programación C

en un entero no negativo que puede ser expresado mediante una constante entera, una variable entera o una expresión entera más compleja. El número de índices determinan la dimensión del array. En C los arrays se almacenan en posiciones de memoria contiguas, de la dirección más baja que corresponde al primer elemento, a la más alta que contiene el último elemento. 12.2. ARRAYS UNIDIMENSIONALES. En C los arrays deben declarase explícitamente para que el compilador reserve la memoria necesaria para ellos. La forma general de declarar un array unidimensional es: tipo nombre_del_vector[tamaño]; donde tipo constituye el tipo base del array, es decir el tipo de cada elemento del array, y tamaño indica el número de elementos que contiene dicho array. Por ejemplo, la siguiente declaración: int codigos[1000]; especifica un array compuesto por 1000 enteros. En C todos los arrays están indexados desde cero, es decir, el índice del primer elemento siempre es el 0. Por lo tanto, los índices de la matriz anterior serían del 0 a 999. Supongamos el siguiente ejemplo, en el que se usa una función media para calcular la media de los números enteros almacenados en un array. #include <stdio.h> int media (int elementos [ 10 ] ); void main (void) { int i, notas [10]; for ( i=0; i<=9; i++ ) { printf ("Introducir número %d: ",i); scanf ("%d",&notas [i]); } printf ("\nLa media de los números vale %d",media(notas)); } int media (int elementos [10]) { int i, me = 0; for (i = 0; i <= 9; i++) me = me + elementos [i]; return (me / 10); }

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 22


Lenguaje de Programación C

El compilador de C no comprueba los límites de los arrays, sino que es tarea del programador realizar la comprobación de límites cuando sea necesario. Si el programador no se ocupa de ello, es posible que se sobrepase cualquier extremo de un array y que se modifique cualquier otra variable de datos o incluso el código del programa, siendo posible que el programa genere un error irrecuperable. 12.3 CADENAS. El array más común en C es la cadena de caracteres, que consiste en un array de caracteres terminado en un carácter nulo ('\0'). Dicho carácter indica el final de la cadena y se coloca automáticamente cuando se define ésta. Es por este motivo, que al declarar cadenas es necesario reservar espacio para un carácter más que el requerido por la cadena de mayor tamaño que se pueda almacenar. La siguiente declaración define una cadena de caracteres con 10 caracteres como máximo. char codigo[11]; Aunque el tipo cadena, no esta definido como tal en C, si que se permite la utilización de constantes de cadenas, que no es más que una secuencia de caracteres encerrada entre comillas dobles, donde no es necesario añadir explícitamente el carácter nulo, ya que el compilador lo hace automáticamente. Para el manejo de cadenas C incorpora un gran número de funciones definidas en el archivo de cabecera string.h, siendo las más usuales las siguientes: 1.char *strcpy(char *s1, const char *s2) Copia la cadena apuntada por s2 en la cadena apuntada por s1, devolviendo esta última. 2.char *strcat(char *s1, const char *s2) Concatena la cadena apuntada por s2 al final de la cadena apuntada por s1. También devuelve s1. 3.int strlen(const char *s1) Devuelve la longitud de la cadena apuntada por s1. 4.int strcmp(const char *s1, const char *s2) Compara las cadenas apuntadas por s1 y s2 y devuelve uno de los siguientes valores: * 0 si las dos cadenas son iguales, * un entero menor que 0 si la cadena apuntada por s1 es menor que la apuntada por s2, * y, un entero mayor que 0 si la cadena apuntada por s1 es mayor que la apuntada por s2. 5.char *strchr(const char *s1, int c) Devuelve un puntero a la primera ocurrencia de c en la cadena apuntada por s1. 6.char *strstr(const char *s1, const char *s2) Devuelve un puntero a la primera ocurrencia de la cadena apuntada por s2 en la cadena apuntada por s1. Para usar la función strcmp, no se puede olvidar que proporciona un valor falso cuando las dos cadenas coinciden y por tanto para hacer algo en el caso de que las cadenas sean iguales hay que utilizar el operador de negación.

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 23


Lenguaje de Programación C

Supongamos el siguiente programa, cuyo fin es el adivinar una cadena de caracteres. Para ello, se tiene hasta 10 posibilidades. #include <stdio.h> #include <string.h> #include <stdlib.h> #define clave "lpiic" La directiva #define permite definir constantes simbólicas. Una constante simbólica es un identificador que sustituye a una secuencia de caracteres. Estos caracteres pueden representar una constante numérica, una constante de carácter o una constante de cadena de caracteres (1). El compilador se encarga de sustituir cada aparición de una constante simbólica por su correspondiente secuencia de caracteres, salvo que el identificador aparezca dentro de una cadena (2). Estas constantes se suelen definir al comienzo del programa o incluso se incluyen en un fichero aparte, con el fin de no dispersar dichas definiciones por el programa. La forma general de esta directiva es: #define nombre texto donde nombre representa un nombre simbólico y texto representa la secuencia de caracteres asociada al nombre simbólico. Esta sentencia no acaba en punto y coma, ya que no se trata de una instrucción de C. Además, se suele escribir en mayúscula los nombres simbólicos para distinguirlos de los identificadores ordinarios de C. Otro aspecto que se debe tener en cuenta, es que una constante simbólica ya definida puede ser usada para definir otras (3). Ejemplos: (1) #define CADENA “Esto es una cadena” #define ENTERO 3 #define PI 3.141593 #define CAR ‘a’ (2) #define CAD “Prueba” .............. printf(“CAD”); /* Visualiza CAD y no Prueba */ (3) #define UNO 1 #define DOS UNO+UNO int empezar (void); void main (void) { if ( empezar ( )) printf ("\nEmpieza el programa"); } int empezar (void) { char cad [20]; int cont = 0; do {

printf ("\nintroducir contraseña : "); scanf (“%s”, cad);

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 24


Lenguaje de Programación C

cont++; if (strcmp (cad, clave)) /* son distintas*/ printf ("\ncontraseña incorrecta"); if (cont==10){ printf ("\nse ha agotado el numero de posibilidades"); exit ( 1 ); } } while (strcmp (cad, clave)); return 1; } El hecho de que en C todas las cadenas terminen con un carácter nulo permite simplificar ciertas operaciones con cadenas, como por ejemplo utilizarlo como condición de finalización para recorrer una cadena: for (i=0; cad[i]; i++) ... 12.4 ARRAYS BIDIMENSIONALES El C soporta arrays multidimensionales, el más sencillo de ellos es el bidimensional. Estos se declaran de la siguiente forma: tipo nombre_arrray [tamaño 1ª dimensión] [tamaño 2ª dimensión] Por ejemplo, la siguiente declaración define un array bidimensional de enteros: int matriz [30][50]; Para acceder a un elemento concreto es necesario especificar el valor de sus índices (es decir, fila y columna) entre corchetes. Con nuestro ejemplo, si se desea acceder al elemento situado en la fila 15 y en la columna 40 se indicara: matriz [15] [40]

Arrays de cadenas Un caso particular de arrays bidimensionales son los arrays de cadenas. El tamaño del índice izquierdo determina el número de cadenas y el tamaño del índice derecho especifica la longitud máxima de cada cadena. Por ejemplo, la siguiente declaración define un array de 24 cadenas, siendo 80 la longitud máxima de cada una de ellas (incluido el carácter nulo al final del array). char array_cadena [24][80]; Para acceder a una cadena concreta se debe especificar sólo el primer índice, como se puede observar en el siguiente ejemplo: scanf (“%s”, array_cadena[5]) 12.5 ARRAYS MULTIDIMENSIONALES C admite arrays de más de dos dimensiones. La forma general de declarar arrays Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 25


Lenguaje de Programación C

multidimensionales es la siguiente: tipo nombre[tamaño 1ª dimensión][tamaño 2ª dimensión]…[tamaño nª dimensión]; Para acceder a un elemento determinado de un array multidimensional, es necesario especificar el valor de sus índices entre corchetes. Finalmente, se ha de tener en cuenta, que a medida que se incrementa el número de dimensiones de un array su uso es más inadecuado debido a la cantidad de memoria requerida para su almacenamiento, y por la cantidad de tiempo empleado para el cálculo de cada índice. 12.6 INICIALIZACIÓN DE ARRAYS El C permite inicializar arrays en el momento de declararlos. Para ello, se usa la siguiente sintáxis: tipo nombre_var[tamaño 1ª dim.][tamaño 2ª dim.]...[tamaño nª dim.] = {lista_de_valores}; donde lista_de_valores es una lista de constantes separadas por comas y cuyo tipo es compatible con el declarado para el array. La primera constante se coloca en la primera posición del array, la segunda constante en la segunda posición, y así sucesivamente. En el siguiente ejemplo, se inicializa un array de enteros de 10 elementos con los dígitos decimales: int digitos[10] = {0,1,2,3,4,5,6,7,8,9}; es decir, digitos[0] tiene el valor 0 y digitos [9] tiene el valor 9. Un array de caracteres puede ser inicializado usando la sintáxis anterior, o de una forma más abreviada, que consiste en encerrar entre comillas dobles la cadena a asignar. Por ejemplo, las siguientes inicializaciones producen el mismo resultado: char cadena[6]=”texto”; char cadena[6]={’t’,’e’,’x’,’t’,’o’,’\0'}; Usando la forma abreviada no es necesario especificar el carácter nulo, puesto que este se añade automáticamente al final de la cadena. Las siguientes inicializaciones muestran como se inicializan arrays multidimensionales: int notas [2][5] = { 5,7,3,4,9, 6,8,2,1,7}; char cad [4][5] = {”yo”, “tu”, “el”, “ella”}; Como resultado de dicha inicialización, el elemento notas[0][0] contiene un 5, notas[0][1] contiene 7, notas[0][2] contiene un 3, notas[1][0] contiene un 6, y así sucesivamente. Inicialización de arrays indeterminados: Si en una sentencia de inicialización no se especifica el tamaño del array, el compilador de C crea un array lo suficientemente grande para mantener todos los inicializadores presentes; es lo que se denomina array sin tamaño o array indeterminado.

Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 26


Lenguaje de Programación C

Las siguientes inicializaciones usan arrays indeterminados: char error1[ ] = "error al abrir un fichero"; char error2[ ] = "error fichero no encontrado\n"; Se pueden usar arrays indeterminados para inicializar arrays de cualquier dimensión. En estos casos se deben especificar todas las dimensiones excepto la situada más a la izquierda. Por ejemplo, a continuación se muestra la inicialización del array notas siendo indeterminado el número de filas: int notas [ ] [3] = { 3,6,4, 7,6,2, 1,8,6, 9,5,1}; La ventaja indudable de emplear arrays indeterminados es que se puede utilizar cualquier número de filas, aumentando o disminuyendo éste, en función de las características del problema. 12.7 ARRAYS Y PUNTEROS En C, arrays y punteros están estrechamente relacionados. El nombre de un array es un puntero al primer elemento de dicho array. Por tanto, si codigos es un vector, entonces la dirección del primer elemento de dicho vector se puede expresar tanto como &codigos[0] o simplemente como codigos. Además, la dirección del segundo elemento del vector se puede obtener tanto como con &codigos[1] como con (codigos +1), y asi sucesivamente. En general, la dirección del elemento i+1 del vector se puede expresar bien como &codigos[i] o como (codigos + i). En definitiva, existen dos métodos para escribir la dirección de cualquier elemento de un vector escribiendo el elemento del vector precedido por &, o escribiendo una expresión en la cual el índice se añade al nombre del vector. En este último método, el nombre del vector representa una dirección, mientras que i es una cantidad entera que especifica el número de elementos del vector que estan después del primero. Se ha de tener en cuenta el tipo base del vector ya que este determina el número de posiciones de memoria usadas por cada elemento del vector, y por tanto i especifica un cierto número de posiciones de memoria más allá del primer elemento del vector. Puesto que &codigos[i] y (codigos + i) representan la dirección del i-ésimo elemento del codigos, parace razonable que codigos [i] y *(codigos + i) representen el contenido de esa dirección, es decir, el valor del elemento i-ésimo. Para entenderlo mejor se va a realizar un programa que visualice tanto el valor de cada elemento de un vector como la dirección correspondiente a dicho vector usando los diversos métodos vistos anteriormente. #include <stdio.h> Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 27


Lenguaje de Programación C

void main(void) { int x[10]={0,1,2,3,4,5,6,7,8,9}; int i; for (i=0; i<=9; i++) { printf(“Valor del elemento i %d es: x[i]=%d, *(x+i)=%d”, i, x[i], *(x+i)); printf(“Dirección del elemento i %d es: &x[i]=%p, x+i=%p”, i, &x[i], (x+i)); } } Por otra parte, también se puede generar un puntero al primer elemento de un vector, basta con asignarle a dicho puntero el nombre del vector sin índices, o la dirección del primer elemento. Por ejemplo, en este array: int codigos[10]; int *p; p=codigos; /* es equivalente a p=&codigos[0]*/

las siguientes sentencias son iguales:

Como se vera en el apartado siguiente, a una variable puntero se le puede sumar o restar un valor entero, pero el resultado hay que interpretarlo como una nueva dirección que es la localizada a la distancia especificada por el entero a partir de la posición original del puntero. Dicha distancia es el producto del entero por el número de bytes que ocupa cada elemento al que apunta el puntero. Además, una variable puntero puede indexarse como si estuviese declarada como un vector. Teniendo en cuenta estas dos últimas ideas y las sentencias anteriores, las siguientes asignaciones realizan el mismo efecto, es decir almacenar el valor 100 en el sexto elemento del vector codigos. p[5]=100; /* indexación como vector del puntero */ *(p+5)=100; /* usando aritmética de punteros */ La razón por la que se utilizan punteros en lugar de indexación de arrays es que la aritmética de punteros es más rápida que la indexación de arrays. 12.8. PASO DE ARRAYS A FUNCIONES. En general, en C no se puede pasar un array completo como argumento a una función, lo que se puede es pasar un puntero a un array especificando el nombre del array sin indice. Por ejemplo, en el siguiente trozo de código se le pasa a la función definida la dirección del array declarado: void main (void) { int a[10]; funcion(a); Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 28


Lenguaje de Programación C

..... }

a)

b)

c)

Un array unidimensional se puede declarar como parámetro formal de tres formas: Como un puntero: func( int *p) { ... } Como un array delimitado: func( int p[10]) { ... } Como un array sin indice; es decir no delimitado. func( int p[]) { ... }

Las tres declaraciones son equivalentes, ya que en cada caso se le indica al compilador que va a recibir un puntero a un entero, incluso en el caso de definirlo como p[10], no se reserva almacenamiento para 10 elementos, ya que C no comprueba los límites de arrays y por tanto sólo le interesa saber el tipo base. El paso de un array multidimensional como argumento de una función presenta una serie de salvedades. Así, cuando se usa un array multidimensional como argumento de una función sólo se pasa un puntero al primer elemento del array, es decir; basta con pasar el nombre del array sin índices ni corchetes. Para especificar un array multidimensional como parámetro de una función se puede emplear los dos últimos métodos vistos anteriormente, es decir: como un array delimitado, o como un array no delimitado. Si se usa este último método es necesario definir todas las dimensiones excepto la dimensión de más a la izquierda, lo cual es lógico por que el compilador necesita saber el tamaño de cada dimensión para indexar el array correctamente. En concreto, si se trata de un array bidimensional, la función debe declarar al menos la longitud de la segunda dimensión, es decir definir la longitud de cada fila. Por ejemplo, la siguiente función recibe un array de enteros bidimensional declarado previamente: int p[5][10]; funcion( int p[][10]) { ... } En el caso de que el array sea de cinco dimensiones, por ejemplo, se deben especificar Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 29


Lenguaje de Programación C

como mínimo el tamaño de las cuatro últimas: int p[4][5][2][6][3]; funcion(int p[][5][2][6][3]) { ..... } 29.

PUNTEROS 13.1. INTRODUCCIÓN.

Un puntero es una variable que contiene una dirección de memoria. Normalmente, esa dirección es la posición de otra variable en memoria. Se dice que la variable puntero apunta a la otra variable. Como toda variable, los punteros deben ser declarados antes de ser usados. La forma general de declarar un puntero es la siguiente: tipo *nombre; donde: * tipo especifica el tipo base del puntero, que puede ser cualquier tipo válido de C. Con él se define el tipo de variables a las que puede apuntar el puntero. * nombre es el identificador de la variable puntero. * el asterisco * identifica a la variable declarada como un puntero.

* *

Los operadores para el manejo de punteros son & y *: El operador & devuelve la dirección de memoria de su operando. El operador * es el complementario del anterior: devuelve el valor de la variable que está almacenada en la dirección que va a continuación.

Ambos operadores tienen mayor prioridad que todos los operadores aritméticos, excepto el menos unario, respecto del cual tienen igual prioridad. Dentro de una declaración de variables se puede inicializar una variable puntero asignándole la dirección de otra variable previamente declarada. Por ejemplo, supóngase las siguientes declaraciones: float u; float *p=&u; /* Se asigna a p la dirección de u */ Además, al definir un puntero se debe especificar a qué tipo de datos va a apuntar. Las variables puntero deben apuntar siempre al tipo de datos correcto, porque el compilador va a realizar todas las operaciones que se indiquen sobre el puntero, en función de ese tipo base. 13.2. OPERACIONES CON PUNTEROS. Asignación de punteros: Un puntero se puede asignar a otro puntero. El siguiente programa muestra un ejemplo: Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 30


Lenguaje de Programación C

void main (void) { int cod=20; int *p1,*p2; p1 = &cod; p2 = p1; printf (“p1 : %p, p2 : %p”, p1, p2); /* las direcciones contenidas en ambos punteros son iguales */ printf ("%d esta almacenado en %p", *p2,p2); } Operaciones aritméticas: Sólo se pueden realizar dos operaciones aritméticas con punteros: suma y resta. Ambas se realizan teniendo en cuenta su tipo base, es decir; cada vez que se incrementa un puntero, éste apunta a la posición de memoria del siguiente elemento de su tipo base, y cada vez que se decrementa apunta a la posición del elemento anterior. Así, si p1 es un puntero a un entero (se suponen enteros de 2 bytes) y su valor actual es 1000, después de hacer p1++ , este contiene el valor 1002 puesto que apunta al siguiente entero. Lo mismo ocurre al decrementar, siendo 1000 el valor actual de p1, entonces después de hacer p1-- su valor será 0998. En definitiva, los punteros aumentan y decrecen en relación a la longitud del tipo de datos a los que apuntan, puesto que siempre apunta al siguiente o al anterior elemento de ese tipo base. Los punteros no están limitados sólo a los operadores de incremento y decremento, sino que también se les pueden sumar y restar enteros. La asignación p = p + 5, hace que p apunte al quinto elemento del tipo base definido más allá del elemento al que apunta actualmente p. Por último, una variable puntero puede ser restada de otra siempre que ambas apunten a elementos del mismo array. El resultado indica el número de elementos que separan los correspondientes elementos del array. Supóngase el siguiente trozo de código: #include <stdio.h> void main(void) { int a[10]; int *p, *q; p = a; q = &a[9]; printf (“Distancia entre el primer y último elemento %x”, q-p); } Cualquier otra operación aritmética distinta de las anteriores no esta permitida realizarla sobre punteros, como por ejemplo multiplicar o dividir punteros, sumar punteros, etc. Comparación de punteros: Área de Lenguajes y Sistemas Informáticos (Mª Encarnación González Rufino)

Pág. 31


Lenguaje de Programación C

Las variables puntero pueden ser comparadas mediante los operadores relacionales, siempre que sean del mismo tipo de datos. Una utilidad importante de la comparación de punteros puede ser la de controlar el acceso dentro de los límites de un vector de un tamaño prefijado. El siguiente programa compara punteros con el fin de verificar los límites de un array. /* Ejemplo de comparación de punteros para verificar límites de arrays */ #include <stdio.h> #include <stdlib.h> #include <conio.h> #define tam_pila 10 int menu(); void guardar(int e); int recuperar(void); void error(int op); /* El uso de variables globales no es aconsejable en la programación modular */ int *tope, *p, pila[tam_pila]; void main(void) { int op,valor; tope=pila; /* inicializar punteros de la pila */ p=pila; do { op=menu(); switch (op) { case '1':printf ("\nintroducir valor a guardar en la pila "); scanf ("%d",&valor); guardar (valor); break; case '2':printf ("\nvalor en el tope de la pila: %d", recuperar( )); getch ( ); break; case '3':; } } while (op!='3'); } int menu( ) { int op; do {

clrscr(); printf("OPERACIONES CON UNA PILA\n");

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 32


Lenguaje de Programación C

printf("\t 1. Meter elemento en la pila\n"); printf("\t 2. Sacar elemento de la pila\n"); printf("\t 3. Salir\n"); printf("Introducir opción: "); scanf (“%c”, &op); } while (op<'1' || op>'3'); return op; } void guarder (int e) /* En el primer elemento no se guarda información. Se guardan elementos mientras la pila no este llena. */ { p++; if (p==(tope+tam_pila)) error(0); *p=e; } int recuperar(void) /* Se recuperan elementos mientras la pila no este vacia */ { if (p == tope ) error(1); p--; return *(p+1); } void error(int cod) /* Ejemplo de vector de punteros */ { char *mensajes[] = { "Desbordamiento en la pila", "La pila esta vacia"}; printf("\n%s",mensajes[cod]); exit(1); } Arrays de punteros: Los punteros como cualquier otro tipo de datos, pueden estructurarse en arrays. Para asignar un valor a un elemento de dicho array, no se debe olvidar que éste tiene que ser una dirección de memoria. Por ejemplo, si se tiene en cuenta las siguientes declaraciones: int *vecpun[5]; /* array de punteros a enteros con tamaño 5 */ int varint; /* variable de tipo entero */ Usando los operadores básicos sobre punteros, se puede asignar la dirección de la variable varint a un elemento del array de punteros vecpun, mediante la siguente sentencia de asignación: vecpun[3] = &varint;

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 33


Lenguaje de Programación C

Posteriormente, para conocer el valor de varint, basta con escribir *vecpun[3] El siguiente programa muestra el uso de arrays de punteros. Este programa almacena en el vector indice las direcciones de los elementos 0, 4 y 8 del vector vector. Se puede observar, que usando las direcciones almacenadas también se puede conocer el contenido de esos elementos del vector vector. /* Ejemplo de arrays de punteros */ #include <stdio.h> void main(void) { int vector[12], *indice[3], i; /* carga de un vector de 12 elementos */ for (i=0;i<12;i++) { printf("\n Introduce elemento %d ",i); scanf("%d",&vector[i]); /* carga del vector de indices, agrupando los elementos de 4 en 4, por tanto 3 indices */ if ((i%4)==0) indice[i/4]=&vector[i]; } /* listar indices, contenido y direcciones de memoria */ for (i=0;i<3;i++) printf("\nindice %d, contenido = %d, dirección %p", i, *indice[i], indice[i]); } Para pasar un array de punteros a una función, se usa la misma forma que para cualquier otro array, es decir; llamar a la función con el nombre del array sin índices. De igual forma, para especificar un array de punteros como parámetro formal se usan los mismos métodos usados para cualquier array. Con el fin de comprender mejor esta idea, el siguiente programa realiza la misma tarea que el anterior, usando una función para visualizar el contenido del array indice. Es por ello, que esta función va a recibir un array de punteros a enteros y por eso su parámetro formal especifica tal array (se puede observar que no es un puntero a un entero). /* Ejemplo de paso de arrays de punteros a funciones */ #include <stdio.h> void mostrar (int *q[]); void main(void) { int vector[12], *indice[3], i; /* carga de un vector de 12 elementos */ for (i=0;i<12;i++) { printf("\n Introduce elemento %d ",i); scanf("%d",&vector[i]); /* carga del vector de indices, agrupando los elementos

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 34


Lenguaje de Programación C

de 4 en 4, por tanto 3 indices */ if ((i%4)==0) indice[i/4]=&vector[i]; } mostrar (indice);

/* Se llama a la función mostrar */

} void mostrar (int *q[]) { int i; /* listar indices, contenido y direcciones de memoria */ for (i=0;i<3;i++) printf("\nindice %d, contenido = %d, dirección %p", i, *q[i], q[i]); } Una aplicación de los arrays de punteros consiste en almacenar punteros que apuntan a mensajes de error. Se puede construir una función que visualice para un código de error su mensaje correspondiente, tal como se muestra a continuación: void error (int cod) { char *mensajes[] = { "Desbordamiento en la pila", "La pila esta vacia"}; printf("\n%s",mensajes[cod]); } Inicialización de Punteros: Es fundamental recordar que después de declarar un puntero pero antes de asignarle un valor, contiene un valor desconocido, por tanto, si se utiliza puede producirse cualquier efecto no deseado. Por convenio, a todo puntero que no apunte a ningún sitio se le debe asignar el valor nulo (NULL). Además, es conveniente mencionar que el puntero nulo se suele usar para facilitar y realizar de forma más eficiente el tratamiento de punteros. 13.3. PUNTEROS A PUNTEROS. Es posible hacer que un puntero apunte a otro puntero que a su vez este apuntando a un determinado valor, esto es lo que se conoce como indirección múltiple. Indirección simple: dirección ------------> valor (Puntero) (Variable) Indirección múltiple:

dirección --------> dirección --------> valor (Puntero) (Puntero) (Variable)

La indirección múltiple podría llevarse hasta el nivel que se desee, pero es muy extraño que sea necesario usar más de dos niveles.

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 35


Lenguaje de Programación C

La declaración de un puntero a un puntero se realiza colocando un * adicional al nombre de la variable. Por ejemplo, la siguiente declaración define un puntero a un puntero a un float: float **pp; Del mismo modo para acceder al valor apuntado por el puntero al puntero, es necesario poner **. En el siguiente ejemplo se puede observar la forma de acceder a un puntero a puntero: void main() { int x, *p, **pp; x=10; p=&x; pp=&p; printf ("%d",**p); /* imprime el valor de x=10 */ } 13.4. PROBLEMAS CON PUNTEROS. Los punteros aportan potencia al C, pero a la vez su uso descontrolado puede causar graves problemas. Los errores que producen los punteros son normalmente difíciles de detectar, ya que el problema no esta en el puntero sino en la zona de memoria donde se esta accediendo. A continuación se comentan algunos de los errores más comunes que surgen cuando se utilizan punteros: 1.) Puntero no inicializado: Consiste en utilizar un puntero que no contiene un valor válido. Esto es una fuente de problemas, puesto que si se lee el contenido del puntero se obtiene cualquier valor invalido (basura), y si se escribe en el puntero se puede llegar a modificar parte del código del programa, o de otros datos, lo cual puede generar un problema en otra parte del programa y será difícil descubrir la causa real. El siguiente programa refleja este error, pues se asigna el valor de una variable a una posición de memoria desconocida. void main() { int x, *p; x=10; *p=x; /* Se asigna el valor 10 a alguna posición de memoria desconocida puesto que el puntero p nunca ha recibido un valor */ } SOLUCIÓN: asegurarse siempre que un puntero esta apuntando a un valor valido antes de utilizarlo. 2.) Asignación a un puntero de un valor que no es una dirección. void main() { int x, *p;

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 36


Lenguaje de Programación C

x=10; p=x; printf("%d",*p); } SOLUCIÓN: comprobar siempre que el valor asignado a un puntero es una dirección de memoria. 3.) Comparar punteros que no apuntan a objetos comunes. Esto produce resultados inesperados, puesto que no tenemos garantía de que el compilador almacene siempre las variables en memoria del mismo modo. char cad1[80], cad2[80]; char *p1, *p2; p1=cad1; p2=cad2; if (p1<p2) ... /* podrá ser verdadero o podrá ser falso */

30.

FUNCIONES 14.1. INTRODUCCIÓN.

Las funciones son los bloques constructores del C, el lugar donde se concentra toda la actividad del programa. La forma general de definición de una función es la siguiente: tipo nombre_función(lista_parámetros) { cuerpo_función } donde: tipo

indica el tipo del valor que devuelve dicha función usando la sentencia return, si no se especifica ninguno, se asume que devuelve un entero (int). Cuando la función no devuelve ningún valor este tipo es void. lista_parámetros es una lista de variables junto con sus tipos separados por comas, que reciben los valores de los argumentos cuando se llama a la función. Todos los parámetros de la función deben incluir tanto el nombre como el tipo. Una función sin parámetros debe contener en el lugar de la lista de parámetros la palabra clave void. La sentencia RETURN sirve para:

Salir de una función devolviendo el control al punto donde fue invocada. Existen dos formas de terminar la ejecución de una función y volver al punto en el que fue llamada. La primera, ocurre cuando se ejecuta la última sentencia de dicha función

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 37


Lenguaje de Programación C

(conceptualmente la llave } del final). La segunda y la más usada, consiste en usar la sentencia return, bien para devolver un valor o bien sin tener ningún valor asociado, sólo con el fin de simplificar el código y permitir que existan múltiples puntos de salida (una función puede tener varias sentencias return, aunque tener demasiadas puede romper la estructura de la función). 2º

Devolver un valor al punto del programa donde se realizo la llamada. Todas las funciones, excepto aquellas de tipo void, devuelven un valor usando esta sentencia. Dicho valor puede ser usado como operando en cualquier expresión válida de C. Del mismo modo, es obvio, que las funciones declaradas como void no pueden ser usadas en ninguna expresión. 14.2. PROTOTIPOS. Su uso tiene como objetivo ayudar a detectar errores antes de que se produzcan. La sintaxis general de un prototipo es: tipo nombre_función(lista de parámetros); en donde: tipo representa el tipo de datos que devuelve la función y lista de parámetros identifica el número y el tipo de argumentos de la misma. Si la función no tiene parámetros se debe utilizar la palabra clave void como lista de parámetros.

En definitiva, el uso de prototipos tiene dos funciones básicas: Identificar el tipo que proporciona la función para que el compilador pueda generar el código correcto (por defecto toda función devuelve un int, si no es asi, hay que indicarselo al compilador). Sirve para especificar el tipo y número de argumentos. No es necesario declarar los nombres de estos argumentos, aunque en la práctica si se incluyen.

Normalmente los prototipos de las funciones van o al principio del programa o en un archivo de encabezado y debe incluirse antes de hacer ninguna llamada a la función. Los prototipos permiten llevar a cabo una comprobación estricta de tipos, puesto que se usan para comprobar cualquier conversión ilegal de tipos entre los tipos de los argumentos utilizados en la llamada a la función y los tipos definidos para los parámetros. También permiten comprobar si el número de argumentos es o no incorrecto. Aunque C convierte el tipo del parámetro actual al tipo del parámetro formal, hay algunas conversiones que no son posibles. Por eso, se debe asegurar que los tipos de los parámetros y de los argumentos coincidan; en general, si hay un error en los tipos el compilador realiza, si es posible, la conversión entre ellos, pero esto puede implicar que se obtengan resultados inesperados. Por ejemplo, si se espera un puntero y se envía un entero, se interpretará el valor del entero como una dirección, dando lugar a que los resultados puedan ser impredecibles. 14.3. PASO DE PARAMETROS. Los parámetros tienen el mismo comportamiento que las variables locales de la función,

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 38


Lenguaje de Programación C

es decir, se guardan en la pila y sólo están accesibles mientras se está ejecutando la función.

1.2.-

Existen varias formas de pasar argumentos a una función: por valor: se copia el valor del argumento en el parámetro formal, y los cambios que se realicen sobre él no afectan a la variable que se usa en la llamada. por referencia: se copia la dirección del argumento en el parámetro, y se accede a esa posición de memoria, por tanto los cambios que se realicen en la función afectan a la variable.

La forma normal de realizar las llamadas en C es por valor. La llamada por referencia se realiza mediante punteros. Es decir, los argumentos que se pasan por referencia se definen como punteros y al invocar a la función se pasan direcciones de memoria, permitiendo que la función cambie el valor contenido en dichas posiciones de memoria. El siguiente programa ilustra esta forma de realizar llamadas por referencia. Su objetivo es que una función independiente, se encargue de leer del teclado dos enteros, los cuales serán visualizados por la función principal. #include <stdio.h> void leer_enteros(int *x,int *y); void main(void) { int a,b; leer_enteros(&a,&b); printf(“Valores: %d %d”,a,b); } void leer_enteros(int *x,int *y) { printf(“Introducir dos enteros ”); scanf(“%d %d”, x, y); } 14.4. ARGUMENTOS DE MAIN: ARGV Y ARGC. En determinadas situaciones es útil pasar información a un programa en el momento de su llamada. Esto se consigue mediante el envío de argumentos a la función main (). Para ello, a la hora de ejecutar el programa en la línea de órdenes del sistema operativo, se coloca después del nombre de dicho programa, la información que se le desea pasar como argumentos. Existen dos argumentos especiales: argc y argv, que se usan para recibir los datos de la línea de órdenes: 1º 2ª

argc: contiene el número de argumentos y es de tipo entero. Siempre vale 1 por lo menos 1, ya que el nombre del programa es el primer argumento. argv: es un puntero a un array de punteros a char, cada uno de los cuales contiene uno de los argumentos. Cada argumento debe estar separado por un espacio o tabulador (no vale

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 39


Lenguaje de Programación C

como separador la coma ni el punto). Si es necesario pasar un argumento que contiene espacios o tabulaciones, éste deber ser encerrado entre comillas. La forma del main en este caso es la siguiente: main(int argc, char *argv[]) { } El siguiente programa muestra el uso de estos argumentos con el fin de imprimir por pantalla el nombre del usuario dado desde la línea de comandos. #include <stdio.h> #include <stdlib.h> void main(int argc, char *argv[]) { if(argc!=2) { printf("Olvidó su nombre...\n"); exit(0); } printf("Hola %s",argv[1]); } 31.

ESTRUCTURAS DE DATOS 15.1. ESTRUCTURAS.

Una estructura es un conjunto de variables, relacionadas lógicamente, que se referencian bajo un mismo nombre. La declaración de una estructura forma una plantilla, que puede usarse para crear variables con dicha estructura. El formato general que se sigue para la definición de una estructura es la siguiente: struct [nombre_estructura]{ tipo nombre_elemento; ... } [variables_de_estructura]; donde nombre_estructura es el nombre de la estructura y no el nombre de una variable y nombre_elemento junto con su tipo son las declaraciones de los diversos elementos que componen dicha estructura. Con respecto a estos elementos individuales, se ha de mencionar que su tipo puede ser cualquiera de los tipos de datos básicos existentes en C, punteros, arrays o incluso otras estructuras, y además, que los nombres de dichos elementos deben ser diferentes dentro de una estructura, pero pueden coincidir con el nombre de otras variables definidas fuera de la estructura. Por último, variables_de_estructura son una lista de nombres de variables separadas por coma, que son realmente las variables con la estructura especificada anteriormente. La siguiente declaración muestra como se define una estructura y se declaran diversas variables con dicha estructura: struct libro{

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 40


Lenguaje de Programación C

char titulo[40]; char autor[20]; float precio; } l1, l2, l3; De la sintáxis anterior, se puede observar que tanto el nombre_estructura como variables_de_estructura son opcionales. Si sólo se necesita una variable con una estructura particular, no es necesario ponerle nombre a dicha estructura. Por ejemplo, la siguiente declaración muestra esta idea: struct { char nombre[30]; char calle[40]; char ciudad[20]; }direccion; Por otra parte, puede interesar definir una estructura particular, y posteriormente declarar diversas variables con ese tipo de estructura; es decir omitir las variables de estructura. Entonces, para declarar después variables con estructura se sigue la siguiente sintaxis: struct nombre_estructura variables_de_estructura; donde, de nuevo nombre_estructura es el nombre dado a la estructura en su definición, y variables_de_estructura es la lista de variables separadas por coma que son declaradas con dicha estructura. Puede observarse el siguiente trozo de código donde se refleja lo mencionado anteriormente: struct libro{ char titulo[40]; char autor[20]; float precio; }; struct libro l1; En resumen, tanto nombre_estructura como variables_de_estructura se pueden omitir, pero no ambas a la vez. 15.1.1. Operaciones con estructuras. Acceso a los elementos de una estructura. Los elementos individuales de una variable de estructura son referenciados usando el operador punto ".". Es decir, para referenciar un elemento individual de la estructura se utiliza el nombre de la variable de la estructura seguido de un punto y del nombre del elemento individual: variable_estructura.nombre_elemento Teniendo en cuenta las declaraciones realizadas en el aparado anterior, las siguientes sentencias muestran como se acceden a los elementos de una estructura. l1.titulo="Programacion en C++"; ...

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 41


Lenguaje de Programación C

scanf (“%s”, l1.autor); Punteros a estructuras. Es posible definir punteros a estructuras, para lo cual, en la declaración se antepone el símbolo * al nombre del puntero, tal como se expresa a continuación: struct nombre_estructura *puntero_estructura; De nuevo, usando las declaraciones anteriores, la siguiente sería la declaración de un puntero a una estructura: struct libro *pl, l1; Como cualquier puntero, los punteros a estructuras deben de apuntar al elemento adecuado antes de ser utilizados. Normalmente, esto conlleva almacenar en el puntero la dirección de una variable estructura previamente definida, como se indica en la siguiente sentencia: pl=&l1; Para acceder a cada elemento de una estructura a través de un puntero, es necesario poner el nombre del puntero seguido del operador “->” (se forma con un signo menos y un símbolo de mayor) y del campo concreto al que se desea acceder: puntero_estructura->nombre_elemento Por ejemplo, si se desea asignar valor al campo precio, las siguientes sentencias llevarían a cabo dicha tarea: p1->precio=5999; scanf(“%f”,&p1->precio); Asignación de estructuras. De acuerdo con el estándar ANSI, puede asignarse la información contenida en una estructura a otra del mismo tipo. Dicha idea se ilustra en el siguiente programa: void main() { struct{ int a; int b; }x,y; x.a=10; y=x; /*se asigna una estructura a la otra*/ printf("%d",y.a); } Arrays de estructuras. Uno de los usos más comunes de las estructuras son los arrays de estructuras. Para ello, se define primero la estructura y después una variable array de ese tipo. De nuevo, teniendo en cuenta las declaraciones realizadas anteriormente, se puede declarar el siguiente vector cuyos

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 42


Lenguaje de Programación C

elementos son estructuras: struct libro biblioteca[100]; Para acceder a una determinada estructura, se indexa el nombre del array. Por ejemplo, para visualizar el precio de la tercera estructura, la sentencia sería la siguiente: printf("%f",biblioteca[3].precio); 15.1.2. Estructuras complejas. También es posible construir otros tipos de datos más complejos, teniendo como base una estructura. Como elemento de una estructura se puede utilizar arrays e incluso nuevas estructuras. Arrays dentro de estructuras Es decir, un elemento de una estructura es un array. Supóngase el siguiente ejemplo: struct{ int matriz[10][10]; int numero; }var; Para referenciar el entero cuya posición es 3, 6 de matriz en la estructura var, se usa la siguiente sentencia: var.matriz[3][6]=9; Estructura anidada. Cuando una estructura es un elemento de otra estructura. Se pueden anidar estructuras hasta cualquier nivel. A continuación se muestra un ejemplo: struct autor{ char nombre[20]; char apellidos[30]; }; struct libro{ struct autor primer_autor; char titulo[40]; float precio; }l; Para referenciar los elementos de una estructura anidada se usa repetidamente el operador punto, comenzando a referenciar los elementos de cada estructura ordenadamente desde el más externo al más interno. Teniendo en cuenta la estructura definida anteriormente, la siguiente sentencia accede a uno de sus elementos individuales. l.primer_autor.nombre=”Pepe”; 15.1.3. Paso de estructuras a funciones. Es importante tener en cuenta cómo se puede realizar el paso de estructuras a funciones.

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 43


Lenguaje de Programación C

Paso de elementos individuales. Cuando se pasa un elemento de una variable de estructura a una función, lo que realmente se está pasando es el valor de dicho elemento, o sea, se pasa una variable por valor. Si se desea realizar el paso por referencia, es necesario colocar el operador & delante del nombre de la estructura. Dadas las siguientes declaraciones de variables de estructura, las sucesivas llamadas a funciones realizan el paso por valor y por referencia: struct pieza { char codigo[8]; int unidades; float precio; } p1,p2; Paso por valor func1(p1.codigo[4]) func2(p1.unidades) func3(p1.precio)

Paso por referencia func4(p2.codigo) /*pasa la dirección de la cadena */ func5(&p2.unidades) func6(&p3.precio) func7(&p2.codigo[2]) /*pasa la dirección del carácter */

Paso de estructuras completas. A una función se le puede pasar una estructura completa como argumento. A no ser que se indique lo contrario, el paso se realiza por valor y por tanto el contenido de la estructura no se vera afectado de los posibles cambios dentro de la función. Hay que destacar que el tipo del argumento debe ser compatible con el tipo del parámetro. Para ello, lo normal es definir la estructura globalmente, darle un nombre y definir tanto los parámetros actuales como los parámetros formales de ese tipo. El siguiente programa muestra un ejemplo de esto: #include <stdio.h> struct libro { char autor[40]; char titulo[40]; } void mostrar_autor(struct libro para_libro); void main(void) { struct libro l2; l2.autor=”Federico Garcia Lorca”;

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 44


Lenguaje de Programación C

mostrar_autor(l2); return 0; } void mostrar_autor(struct libro para_libro) { printf(“%s”, para_libro.autor); } Cuando se pasa por valor una estructura a una función existe un inconveniente, se invierte mucho tiempo en el manejo de la pila (introducir/sacar cada elemento individual). Una posible solución es pasar a la función un puntero a la estructura, o sea hacer un paso por referencia, en este caso sólo se trabaja en la pila con un dato, la dirección de la estructura y además se puede modificar el contenido de la estructura. En el siguiente ejemplo se realiza un paso de estructuras por referencia: struct fecha { int dia; int mes int año }; void actualizar(struct fecha *f); ... Llamada a la función:

struct fecha hoy; actualizar (&hoy);

Es conveniente recordar, que el operador punto se usa para acceder a elementos de estructuras cuando se trabaja directamente sobre ellas, y el operador flecha cuando se trabaja con un puntero a una estructura. 15.2. CAMPOS DE BITS El lenguaje C, permite acceder a un bit individual dentro de un byte. Sus ventajas son las siguientes: 1.Ahorra espacio puesto que permite la posibilidad de almacenar varias variables lógicas en un byte. 2.Las interfases de algunos dispositivos transmiten información codificada en los bits dentro de un byte. Puede ser considerado un ejemplo de la característica de bajo nivel del lenguaje C. 3.Permiten codificar/decodificar la información. Un campo de bits no es más que un tipo especial de elemento de estructura que define su tamaño en bits. La sintaxis que se sigue par definir un campo de bits es: struct nombre_estructura{ tipo nombre1: longitud;

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 45


Lenguaje de Programación C

tipo nombre2: longitud; ... tipo nombreN: longitud; }lista_variables; donde, longitud especifica el número de bits en el campo y éste debe ser un valor entero no negativo. Cada campo de bits debe declararse como int, signed o unsigned, pero si sólo tiene un bit deberá ser unsigned int (un sólo bit no puede tener signo). Supóngase el siguiente ejemplo: struct tipo_estado{ unsigned listo_enviar: 1; unsigned fin_tx: 1; unsigned on_line: 1; unsigned: 3; unsigned llamada: 1; unsigned: 1; }estado; Se puede observar, en la declaración anterior, que no es necesario nombrar cada campo de bits, sólo los que interesen.

a) b) c)

Las variables de campos de bits tienen ciertas restricciones: no se puede tomar la dirección de una variable de campos de bits, no se pueden construir arrays de variables de campos de bits, y cualquier código que use campos de bits puede tener algunas dependencias de la maquina.

Por último, indicar que se pueden mezclar elementos de estructuras normales con elementos de campos de bits. Por ejemplo: struct empleado{ struct dirección dir; float paga; unsigned casado: 1; unsigned hijos: 3; }; Al igual que antes, se puede acceder a los miembros individuales de una estructura con campos de bits, utilizando el operador punto, o en el caso de que se trabaje con punteros usando el operador flecha. 15.3. UNIONES.

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 46


Lenguaje de Programación C

Una unión es una zona de memoria que es compartida por varias variables, normalmente de distinto tipo, en distintos momentos. Es decir, los elementos que componen una unión comparten el mismo área de almacenamiento, mientras que cada elemento de una estructura tiene asignada su propia área de almacenamiento. Su sintaxis es similar a la de una estructura: union [nombre_union]{ tipo1 nombre_elemento1; tipo2 nombre_elemento2; ... tipon nombre_elementon; } [variables_union]; donde, union es una palabra reservada y nombre_unión es la etiqueta dada a dicha unión. Entre llaves, se encuentra la declaración de los distintos elementos de la unión, para lo cual basta con indicar su tipo y nombre. Y por último, variables_unión son la lista de variables que son de ese tipo de unión. La siguiente declaración muestra un ejemplo: union tipo_u{ int i; char ch; } char_int; En este ejemplo, tanto el entero i como el carácter ch, comparten la misma posición de memoria. Es decir, se puede referir al dato guardado como entero o como carácter. La siguiente figura muestra cómo comparten estos elementos la misma dirección: |<-----------i------------>| Byte 0 Byte 1 |<----ch---->| Al igual que las estructuras, nombre_unión y variables_unión son opcionales. Para declarar variables de unión de forma individual se usa la siguiente sintaxis: union nombre_union variables_union; Teniendo en cuenta la unión declarada anteriormente, la siguiente instrucción declara más variables de ese tipo de unión: union tipo_u union1, union2; Cuando se declara una unión, el compilador crea una variable lo suficientemente grande para guardar el tipo más grande de variable de la unión. (Sólo se almacena uno cada vez). Para acceder a un elemento de la unión se usan los mismos operadores que en estructuras: . y ->. Si se está trabajando directamente con la variable unión, se usa el operador punto. Si se accede a dicha variable a través de un puntero, se utiliza el operador flecha. Las uniones se usan frecuentemente donde se necesitan conversiones de tipos, ya que nos podemos referir a los datos de la unión de diferentes formas.

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 47


Lenguaje de Programación C

15.4. ENUMERACIONES. Una enumeración es un conjunto de constantes enteras con nombre que especifican todos los valores válidos que una variable de ese tipo puede tener. La forma general de declaración es la que sigue: enum [nombre_enumeración] {lista_constantes} [lista_variables]; donde enum es una palabra reservada, y el resto de términos tienen el mismo significado que en una estructura. Tanto nombre_enumeración como lista_variables son opcionales, pero debe estar presente alguna de ellas. La sintaxis que se sigue para declarar variables de enumeración individuales (posteriormente a la definición de una enumeración) es la siguiente: enum nombre_enumeración lista_variables; Las siguientes declaraciones muestran varios ejemplos de enumeraciones: enum moneda {peseta, duro, veinticinco,cien}; enum moneda dinero; enum fruta {pera, platano, manzana, uva} fruta_verano, fruta_invierno; Teniendo en cuenta lo anterior, las siguientes sentencias son válidas: dinero=duro; (dinero=1;) if(dinero==peseta) printf("Podrías ser más generoso\n"); Es muy importante tener en cuenta que cada uno de los símbolos corresponde a un valor entero y recibe un valor mayor en 1 al que le precede. El valor del primer símbolo de la enumeración es 0. De esta forma, pueden usarse en cualquier expresión entera. Por ejemplo: printf("Peseta vale %d, y veinticinco %d\n", peseta, veinticinco); Este ejemplo imprimirá en pantalla 0 y 2. Por otra parte, se puede indicar el valor que se debe asignar a uno o más símbolos especificando un iniciador, el cual se hace siguiendo al símbolo con el operador de asignación y un valor entero. Se ha de tener en cuenta que los símbolos que siguen al inicializado tienen valores sucesivos a partir de este. Por ejemplo, la siguiente declaración asigna el valor 5 a duro: enum moneda{peseta, duro=5, veinticinco}; Con lo que peseta valdría 0, duro 5 y veinticinco 6. Una suposición errónea es que los símbolos pueden leerse y escribirse directamente. Esto no es así, el nombre que damos (peseta) es sólo un nombre para un entero, no un string que puede leerse o escribirse directamente. Para mostrar en palabras el tipo de moneda que contiene dinero, se podría seguir la estructura siguiente: switch(dinero){ case peseta: printf("peseta");

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 48


Lenguaje de Programación C

case duro:

break; printf("duro"); break;

} Es posible, si no se usaron inicializadores (para que este indexado desde 0), declarar un array de cadenas con los nombres y utilizar el valor de la enumeración como un índice de dicho array, para traducir el valor de la enumeración en su cadena correspondiente. Por ejemplo, el siguiente código ilustra este método: char nombre[]={"peseta","duro","veinticinco"}; printf("%s", nombre[dinero]); 15.5. TYPEDEF. Relacionado también con estructuras de datos más complejas, estaría la palabra reservada typedef, que permite definir explícitamente un nuevo nombre de tipo de dato. No se crea una nueva clase de datos, sólo se le da un nuevo nombre para un tipo existente. Una vez que el tipo de datos definido por el usuario ha sido establecido, entonces las nuevas variables, formaciones, estructuras, etc., pueden ser declaradas en términos de este nuevo tipo de datos. La sintáxis que se sigue para declarar un nuevo tipo de datos es la siguiente: typedef tipo nuevo_nombre; donde tipo se refiere a un tipo de datos existente y nuevo_nombre es el nuevo nombre para ese tipo de datos. Este nuevo_nombre no supone un reemplazamiento del nombre de tipo existente, sino que es una adición. La siguiente declaración ilustra un ejemplo, donde se da el nuevo nombre edad al tipo de dato entero: typedef int edad; De esta forma, las siguientes declaraciones son equivalentes: edad x, y, z: int x, y, z; También se puede crear nombres para tipos más complejos, como muestra el siguiente ejemplo: typedef struct { char autor [20]; char titulo [30]; int precio; } libro; /* libro es el nuevo nombre del tipo */ libro l1, l2, l3; /* se definen variables de tipo libro */ Usando typedef se puede obtener un código más fácil de leer y más fácil de transportar a una nueva máquina. 32.

GESTIÓN DE MEMORIA DINÁMICA

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 49


Lenguaje de Programación C

16.1. FUNCIONES. Hay dos formas fundamentales con las que un programa puede almacenar información en la memoria principal: 1.

Almacenamiento estático: mediante variables reservadas en tiempo de compilación, por lo tanto es preciso conocer de antemano la cantidad de espacio que se necesita. En el caso de variables globales, el almacenamiento se mantiene fijo durante toda la ejecución del programa y se usa para ellas la Zona de Memoria de Variables Globales. Para las variables locales, el almacenamiento que se les asigna corresponde a la Pila.

2.

Almacenamiento dinámico: mediante espacio de memoria principal reservado dinámicamente y en tiempo de ejecución. Es decir, el espacio de almacenamiento se asigna según se necesite, devolviéndose cuando se ha terminado con él. La zona del Montón (Heap) es la gestionada por la asignación de este tipo de almacenamiento. En este caso la misma zona de memoria puede utilizarse para almacenar información diferente en distintos momentos de la ejecución del programa. Se gestiona mediante la utilización de punteros. Las funciones que se utilizan para gestionar el heap son las siguientes: void *malloc(size_t num_bytes);

stdlib.h

El tipo size_t es un entero sin signo que está definido en el fichero de cabecera stdlib.h como para contener la mayor cantidad posible de memoria que se pueda asignar de una vez. Para facilitar el cálculo del espacio que es necesario reservar se utiliza el operador sizeof. Esta función devuelve un puntero void al primer byte de una zona de memoria de num_bytes que se ha asignado del montón. Si no hay suficiente memoria libre en el montón devuelve un puntero nulo (NULL) indicando dicho error (es un buen hábito comprobar siempre que se ha obtenido un puntero válido y garantizar la correcta utilización de punteros). Ejemplo: int *p; p = (int *) malloc (100 * sizeof ( int ) ); /* (1) y (2) */ if ( ! p ) /* (3) */ { printf("Error en la asignación de memoria "); return (1); } 1. Se necesita convertir el puntero void a un puntero a un int. 2. Se utiliza la función sizeof para calcular el número de bytes que ocupa un entero. 3. Se hace una comprobación de la correcta reserva de espacio. La comprobación también puede realizarse de la siguiente forma: if ((p = (int *) malloc ( 100 * sizeof ( int ) ) == NULL) {

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 50


Lenguaje de Programación C

printf ("Error en la asignación de memoria"); return (1); } void free(void *p);

stdlib.h

Esta función libera la memoria apuntada por p y que previamente fue asignada para que pueda ser reutilizada. Para evitar errores es importante utilizarla con punteros válidos, es decir con los que previamente se haya reservado memoria dinámicamente. Ejemplo: free (p); 16.2. ARRAYS DINÁMICOS. En muchas situaciones de programación, no es posible conocer el número de elementos que va a tener un array, por lo que no se puede utilizar un array predefinido ya que en ese caso habría que especificar sus dimensiones en tiempo de compilación y éstas no se pueden cambiar durante la ejecución. Este problema se soluciona mediante la definición de arrays dinámicos, arrays definidos en tiempo de ejecución. Estos array usan memoria del heap y son accedidos indexando un puntero a dicha memoria. Ejemplo: int num_notas,i; float *notas_clase; scanf (“%d”, &num_notas); notas_clase = (float *) malloc (num_notas * sizeof ( flota )); if ( !notas_clase ) { printf (“Error de asignación de memoria”); exit ( 1 ); } for (i = 0; i < num_notas; i++) scanf (“%f”, &notas_clase[i]); .... free (notas_clase);

Es conveniente comprobar el puntero que devuelve la función malloc con el fin de prevenir un uso accidental a un puntero nulo. 16.3. ESTRUCTURAS DE DATOS DINÁMICAS. Los arrays dinámicos tienen una limitación, una vez creados no es posible alterar su tamaño. Si el objetivo es disponer de un mecanismo para aumentar o disminuir el número de elementos según la ejecución del programa, se deben emplear estructuras basadas en la utilización de nodos. Un nodo es una estructura que consta de dos partes: una zona de datos donde se puede

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 51


Lenguaje de Programación C

almacenar cualquier tipo de información, y una zona de punteros que contiene al menos un puntero que apunta a otro nodo. De esta forma, cada nodo puede estar situado en posiciones diferentes dentro de la memoria que no tienen porque ser contiguas. Normalmente, un grupo de nodos se asocia con una estructura de datos. A continuación se examinan las siguientes estructuras de datos. Listas enlazadas Una lista enlazada es una colección lineal de nodos enlazados entre sí mediante la zona de punteros que contiene un puntero al siguiente nodo. Por regla convencional, se marca el fin de la lista mediante el puntero nulo (NULL) en la zona de punteros del último nodo. Para definir el nodo de una lista enlazada se puede utilizar la siguiente estructura: struct nodo { tipo informacion; struct nodo *siguiente; }; typedef struct nodo *posicion; typedef posicion lista; A la hora de reservar espacio para un nodo durante la ejecución, se emplea la función malloc: posicion p; p=(posicion) malloc (sizeof(nodo)); La función sizeof determina el tamaño, en bytes, que ocupa la estructura nodo. La dirección de memoria que se obtiene se asigna a la variable puntero p que se declara previamente como un puntero a dicha estructura. Cada nodo se crea dinámicamente en función de las necesidades y se enlazan entre si todos los nodos que componen la lista. Es útil implementar operaciones básicas para manipulación de la información en listas. Pilas Una pila no es más que una lista LIFO, por lo que su implementación es similar a la vista anteriormente: struct nodo { tipo informacion; nodo *siguiente; }; typedef nodo *pila; Las funciones típicas de una pila son las de introducir (push) y extraer elementos (pop).

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 52


Lenguaje de Programación C

Árboles binarios Un árbol binario es una estructura de datos que contiene un campo de información y dos enlaces a otros nodos denominados hijo izquierdo e hijo derecho. Su implementación en C sería: struct nodo { tipo informacion; nodo *hijo_izda; nodo *hijo_decha; }; typedef nodo *arbol;

Área de Lenguajes y Sistemas Informáticos (E.G.R.)

Pág. 53


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.