DOCUMENTACIÓN PARA EL TRABAJO CON LA PLATAFORMA GUADALBOT I.E.S VIRGEN DE LAS NIEVES Programación C para microcontroladores Tema 5. Estructuras, uniones, matrices y punteros Índice de contenidos Estructuras............................................................................................................................................2 Uniones.................................................................................................................................................2 Matrices................................................................................................................................................3 Punteros................................................................................................................................................4 Dirección (Lvalue)...........................................................................................................................5 Valor (Rvalue)..................................................................................................................................5 Relación entre vectores (matrices) y punteros......................................................................................6 1 Estructuras Podemos declarar variables discretas, matriciales y punteros para almacenar uno o varios valores, pero dichos datos deben ser siempre del mismo tipo. Seria deseable poder disponer de la posibilidad de declarar datos que estén relacionados entre sí y que no sean del mismo tipo, como por ejemplo los datos de una agenda. Es evidente que se pueden declarar tantas variables como necesitemos para nuestra agenda o bien una matriz con todos los datos de tipo string, pero ambas cosas no resultan totalmente satisfactorias. Existe un método bastante cómodo de relacionar datos del tipo descrito mediante la definición de una estructura de datos. Podríamos decir que una estructura es una asociación en un mismo tipo de múltiples datos que pueden ser de cualquiera de los tipos vistos; por lo que podemos considerar que una estructura también es un tipo de dato. La sintaxis de una estructura es la siguiente: struct [<nombre y tipo de estructura>] { [<tipo> <nombre-variable[,nombre-variable, ...]>] ; … … … } [<variables-Estructura>] ; Donde: <nombre y tipo de estructura> es un nombre opcional de etiqueta que se refiere al tipo de estructura y <variables-Estructura> es la definición de datos, también opcional. Aunque ambos elementos sean opcionales al menos debe aparecer uno de ellos. Se definen elementos nombrando un tipo seguido del nombre de una o más variables separadas por comas y separando los distintos tipos de variables con punto y coma. El acceso a elementos o miembros de una estructura se realiza mediante el operador (->). Uniones Los tipos unión son tipos derivados que comparten muchas de las características sintácticas y funcionales de los tipos estructura. La diferencia dominante es que una unión solo permite que uno de sus miembros este "activo" en un momento dado. El tamaño de una unión es el tamaño de su miembro más grande. El valor de cualquiera de sus miembros se puede almacenar en cualquier momento. En el ejemplo siguiente se realiza una declaración de una unión. union mi_union { int Entero; char Caracter; } mu; El acceso a miembros de mi_union se realiza con el operador o selector (->). La sintaxis de declaración de uniones es similar a la de estructuras, siendo las principales diferencias las siguientes: • Las uniones pueden contener campos a nivel de bit, pero solamente uno puede estar activo. • En cualquier caso las estructuras de C y los tipo unión de C no pueden utilizar los especificadores de acceso a clases: public, private, y protected. Todos los campos de una unión son de tipo public. • Las uniones se pueden inicializar solamente a través de su primer miembro declarado: union local { int Entero; float Doble; } a={ 20 }; • Una unión no puede participar en clases jerárquicas, no puede ser derivada de ninguna clase 2 ni puede ser una clase base. Una unión debe tener un constructor. Una de las principales diferencias con las estructuras radica en que una unión es una región de almacenamiento compartida por dos o más miembros lo que permite manipular diferentes tipos de datos utilizando una misma zona de memoria, es decir que los miembros de una unión no tiene cada uno su propio espacio de almacenamiento, sino que todos comparten un único espacio de tamaño igual al del miembro de mayor longitud. Matrices Una matriz es un conjunto de elementos de un mismo tipo a los que podemos acceder de forma individual mediante un indice. El uso de matrices nos va a permitir manipular un gran número de datos sin necesidad de definir una variable para cada uno de ellos. Cada elemento de una matriz puede ser de cualquiera de los tipos vistos. Una matriz también se suele denominar array, arreglo o vector. Aunque matriz suele usarse para referirse a matrices unidimensionales, en realidad una matriz unidimensional es un vector y es propiamente matriz si tiene más de una dimensión. La forma de declarar una matriz unidimensional o vector es: tipo_dato identificador[Elementos]; Donde tipo_dato es un dato cualquiera de los tipos vistos, identificador es el nombre del vector Elementos es la cantidad de elementos que tendrá, que deberá ser necesariamente una constante o una expresión cuyo resultado sea constante. Los elementos de una matriz tienen un indice que se inicia en cero (primer elemento) y se incrementa hasta el número de elemento menos uno. Las matrices de tipo char se pueden utilizar con un solo valor, o sea, como una cadena de caracteres. Algunos ejemplos de matrices son: int var[10]; //reserva espacio para 10 variables de tipo int const int DiasMes[12]={31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; char Dias[ ]={'L', 'M', 'X', 'J', 'V', 'S', 'D'}; char Dias1[ ]="LMXJVSD"; En el primer caso las 10 variables se llaman var y se accede a una u otra por medio de un subíndice, que es una expresión entera escrita a continuación del nombre entre corchetes. En los demás casos hemos asignado un valor inicial a cada elemento de forma similar a como la hacemos con una variable. Disponemos, encerrados entre llaves, los valores correspondientes a cada elemento separados por comas recordando que el indice 0 corresponde al primer elemento. Si se trata de caracteres lo podemos hacer de la misma forma que antes delimitando cada valor con comillas simples o bien entre comillas dobles uno a continuación de otro y sin emplear llaves. En el ejemplo anterior las matrices Días y Dias1 tienen exactamente el mismo valor. Observamos también que en las matrices char se ha obviado el número de elementos, asignándose automáticamente según el número de valores facilitados. Aparentemente Días y Dias1 tendrían 7 elementos, pero no es exactamente así, estribando la diferencia en Dias1 se define como cadena y el lenguaje añade automáticamente un carácter 0 o NULL (\0) de fin de cadena, que implica que Dias1 tenga 8 elementos. Este extremo es importante tenerlo en cuenta al trabajar con matrices de caracteres. 3 Acceso a elementos Dias[0]='L' Dias[1]='M' Dias[2]='X' Dias[3]='J' Dias[4]='V' Dias[5]='S' Dias[6]='D' Dias[0]='L' Dias[1]='M' Dias[2]='X' Dias[3]='J' Dias[4]='V' Dias[5]='S' Dias[6]='D' Dias[7]=\0 Como ya hemos dicho, para acceder a un elemento de un vector tenemos que incluir en una expresión su nombre seguido del subíndice entre corchetes. No es posible operar con un vector completo o toda una matriz como una única entidad, sino que hay que tratar sus elementos uno a uno por medio de bucles. Los elementos de un vector se utilizan en C como cualquier otra variable. Algunos ejemplos de uso de vectores: var[0] var[1] var[2] … var[9] = 1.4142; = 10.0 *v ar[0]; = var[9] – var[1]/var[9]; = (var[0] + var[7])/var[3]; Es evidente que hasta ahora hemos hablado de matrices unidimensionales (vectores), pero en C se pueden definir matrices de varias dimensiones. Supongamos que queremos crear una matriz que permita almacenar un número en cada uno de los elementos, y que estos elementos son los que representan cada uno de los días de cada mes. Necesitaremos una matriz de 12 elementos (tantos como meses), cada uno de los que tendrá a su vez una matriz de 31 elementos (uno por día). Esta matriz se declara así: int MesDias [12] [31]; Para acceder a un determinado elemento de una matriz los distintos indices se delimitan cada uno con su pareja de corchetes. Las siguientes expresiones muestran accesos correctos a elementos de las matrices declaradas anteriormente. Dias[0] = ‘L’; MesDias [1][6] = 0; También podemos emplear el operador sizeof() para determinar la ocupación en memoria de una variable tipo matriz. Punteros Diremos en primer lugar que la programación de punteros plantea algunos problemas prácticos dada su naturaleza bastante ‘delicada’. Un pequeño despiste con ellos puede hacer que nuestro programa de resultados extraños o, incluso, que se "cuelgue". Los punteros son un tipo de variables que almacenan las direcciones físicas de memoria de otras variables. Si tenemos la dirección de una variable, tenemos acceso a esa variable de manera indirecta y podemos hacer con ellas todo lo que queramos. Ya vimos someramente que en lenguaje C se dispone del operador de dirección (&) que permite determinar la dirección de memoria de una variable. Hay también en C un tipo de variables que se caracterizan por contener direcciones de variables y que se denominan punteros (pointers). Un puntero es una variable que se declara, se le asignan valores, se utiliza en expresiones, etc.; pero se diferencia de cualquier otra variable en que no contiene o almacena un dato, sino que contiene una dirección de memoria en la que se almacena la información. Podemos definir un puntero como que es una variable que contiene una dirección que apunta a un valor de un cierto tipo, denominado tipo base. Podremos por tanto definir 4 punteros de cualquiera de los tipos que conocemos, por ejemplo, si en un puntero almacenamos la dirección de memoria donde se almacena un entero, el puntero deberá ser de tipo int. Para declarar un puntero basta con disponer detrás del tipo el operador *, que indicará al compilador que lo que estamos declarando no es una variable, sino un puntero. La sintaxis general de declaración de un puntero es: tipo *Puntero; // Puntero sin inicializar Podemos imaginar un puntero como una flecha que apunta a una dirección de memoria. Por ejemplo, si declaramos el puntero: char *Puntero; tenemos declarada una "flecha" que apunta a una dirección de memoria. No debemos confundir una dirección de memoria con el contenido de esa dirección de memoria. Por ejemplo, si declaramos int x=10; podemos considerar una estructura de memoria como: Direcciones de memoria DirMem10 Contenido --- DirMem20 DirMem30 DirMem40 --- 10 --- La dirección de la variable x es &x = DirMem30 mientras que el contenido de esa dirección de memoria es el valor de la variable x, es decir: 10. Podemos decir que hay dos valores para x. El primero es el valor del entero guardado, en nuestro caso 10 denominado rvalue, mientras que el segundo es la dirección de memoria, que se denomina lvalue. Dirección (Lvalue) La dirección o localización de memoria o Lvalue de la variable-dato es la dirección de memoria donde comienza el almacenamiento. Este nombre viene históricamente de “left value” o valor a la izquierda, en referencia a que normalmente la variable receptora del dato está a la a la izquierda. Por ejemplo, una expresión del tipo x=3; que leemos como asignar el valor 3 a la variable x, podemos enunciarlo de una forma más extensa y real diciendo que asignamos o guardamos el valor 3 en la dirección de memoria señalada por la variable x. El Lvalue puede ser fijo (constante) o variable. Un Lvalue modificable significa que la dirección puede ser accedida y su contenido modificado. Por ejemplo: x=3; es una expresión válida si x es una variable de tipo entero, lo que significa que en su dirección puede guardarse un patrón de bits que significará un valor 3 para el compilador Un Lvalue constante significa lo contrario, por ejemplo, la expresión 3=x no es correcta porque el Lvalue de 3 no es modificable. La expresión x+y=2 tampoco es correcta, porque x+y no tiene Lvalue ya que no puede interpretarse como una dirección de memoria). El Lvalue de las variables estáticas y globales se asigna en tiempo de compilación y el de las variables dinámicas se asigna en tiempo de ejecución. Valor (Rvalue) El Rvalue es la interpretación que hace el programa del patrón de bits alojado en la dirección asignada a la variable. Históricamente Rvalue es abreviatura del inglés “right value”, valor a la derecha, en referencia a los valores que normalmente suelen estar a la derecha en una expresión de asignación. Un Rvalue puede ser una constante, variable, o expresión que pueda traducirse en un valor. Por ejemplo 4+3 es un Rvalue. 5 Las variables que no son constantes pueden modificar su Rvalue a lo largo del programa. Una expresión del tipo x=a; normalmente leido como el valor de la variable o constante a sea asignado a la variable x, podemos leerlo de una manera más formal diciendo que el Rvalue de la variable nombrada a sea asignado a la dirección de memoria (Lvalue) de la variable de nombre x. Bien, en este momento veamos algún ejemplo: int x, y; y= 2; x = 7; //línea 1 y = x; //línea 2 En la línea 1 el compilador interpreta la x como la dirección de la variable (su lvalue) y crea código para copiar el valor 7 a esa dirección. En la línea 2, sin embargo, la x es interpretada como su rvalue (ya que está del lado derecho del operador de asignación). Esto significa que aquí x hace referencia al valor alojado en la dirección de memoria asignado a x, 7 en este caso. Así que el 7 es copiado a la dirección designada por el lvalue de y. Si la declaración de un puntero (por ejemplo int *punt;) se hace fuera de cualquier función, los compiladores ANSI la inicializarán automáticamente a cero. De modo similar, *punt no tiene un valor asignado, esto es, no hemos almacenado una dirección de memoria en la declaración hecha arriba. En este caso, y si la declaración fuese hecha fuera de cualquier función, es inicializado a un valor que asegure que no apunte a un objeto de C o a una función. Esta forma de definición de un puntero es lo que se conoce como puntero "null" (null pointer). Supongamos ahora que queremos almacenar en punt la dirección de memoria de una variable entera y. Para hacerlo debemos usar el operador unitario & y escribir punt=&y; Lo que el operador & hace es obtener la dirección de y, aún cuando y está en el lado derecho del operador de asignación y copia esa dirección en el contenido de nuestro puntero punt. Ahora, punt es un “puntero a y”. El operador de indirección se usa, por ejemplo escribiendo *punt=7; que copiará el 7 a la dirección a la que apunta punt. Así que como punt “apunta a y” (contiene la dirección de y), la instrucción de arriba asignará por tanto, a y el valor de 7. Esto es, que cuando usemos el * hacemos referencia al valor al que punt está apuntando, no al valor del puntero en si. Relación entre vectores (matrices) y punteros La relación entre un puntero y una matriz unidimensional o vector resulta casi evidente ya que el nombre de un vector es un puntero constante (no puede apuntar a otra variable distinta) a la dirección de memoria que contiene el primer elemento del vector. Veamos una ejemplos de sentencias: int vector[10]; int *p; p = &vector[0]; // Hace que p = vect; El identificador vector (el nombre vector) es un puntero al primer elemento de vector[ ] que es lo mismo que decir que el valor de vector es &vector[0]. Como el nombre de un vector es un puntero, podemos escribir, por ejemplo: • Si vector apunta a vector[0] vector+1 apuntará a vector[1] y vector+i apuntará a vector[i] 6 Podemos poner subindices a los punteros como se ponen a los vectores. Por ejemplo: • Si p apunta a vector[0] podremos poner p[3]=p[2]*2.0 que sería lo mismo que vector[3]=vector[2]*2.0. Si p=vector, podemos decir que: *p será equivalente a vector[0], a *vector y a p[0] *(p+1) será equivalente a vector[1], a *(vector+1) y a p[1] *(p+2) será equivalente a vector[2], a *(vector+2) y a p[2] Para ver la relación entre punteros y matrices vamos a partir de la sentencia siguiente: int matriz[5][3], **punt1 //punt1 es un puntero a puntero El nombre de la matriz (matriz) es un puntero al primer elemento de un vector de punteros matriz[ ] y sus elementos contienen las direcciones del primer elemento de cada fila de la matriz. Por lo tanto el nombre matriz es un puntero a puntero. El vector de punteros matriz[ ] se crea al crearse la matriz. Podemos establecer las siguientes igualdades: matriz = &matriz[0]; matriz[0] = &matriz[0][0] matriz[1] = &matriz[1][0] matriz[2] = &mat[2][0] La dirección base sobre la que se direccionan todos los elementos de la matriz no es matriz, sino &mat[0][0]. Recordando que matriz+i apunta a matriz[i], para direccionar una matriz de F filas y C columnas tendremos que establecer la dirección del elemento (i, j) de la siguiente forma: dirección (i, j) = dirección (0, 0) + i*C + j Según esto y haciendo **punt1 = matriz; podemos acceder a los elementos de la matriz con cualquiera de estas formas: *punt1 es matriz[0] **punt1 es matriz[0][0] *(punt1+1) es matriz[1] **(punt1+1) es matriz[1][0] *(*(punt+1)+1) es matriz[1][1] Podemos profundizar más en el tema consultando el TUTORIAL SOBRE APUNTADORES Y ARREGLOS EN C por Ted Jensen distribuido libremente en distintos formatos en la dirección web: http://www. netcom.com/~tjensen/ptr/cpoint.htm La Versión 1.2 de Febrero de 2000 (pointersC.pdf) la podemos ver aquí. 7