4.5.2 Iniciación de estructuras
§1 Sinopsis
Se ha señalado que, al igual que las matrices, las estructuras pueden iniciarse, incluso en el mismo punto de su declaración, con una lista de iniciadores entre corchetes { } separados por comas [1], uno para cada miembro de la estructura. Por ejemplo, la sentencia:
struct Cliente {int i; char str[20]; double d;} s = {33, "Pepe Lopez", 3.14 };
declara una estructura Cliente compuesta por un entero; un array de 20 caracteres, y un doble.También inicia una estructura s como perteneciente al tipo Cliente con valores concretos en cada campo.
§2 En estructuras o uniones con duración automática
( 4.1.8a), el iniciador debe ser
alguno de los siguientes:
-
Una lista de inicializadores constantes (también se permiten expresiones con sizeof) Ejemplo:
struct punto {int x; int y;} p1 = {1, 2};
-
Una sola expresión con una estructura de tipo compatible. En este caso, el valor inicial del objeto es el de la expresión. Ejemplo:
struct punto p2 = p1;
en este caso, (suponiendo los valores anteriores) sería: p2.x == 1 y p2.y == 2
§3 Los miembros complejos de listas de iniciadores. Por ejemplo matrices, pueden inicializarse con
expresiones adecuadas incluidas en bloques de corchetes anidados. Por ejemplo:
struct Pesadas {
int tiket;
int pesos[5];
} s = { 150, {10, 45, 0, 78, 20}};
Si la lista de inicializadores contenidos entre los corchetes { } es menor que los miembros de la estructura, el resto de los miembros es inicializado implícitamente siguiendo las mismas reglas que los objetos con duración estática.
§4 Comentarios
Las estructuras, al igual que las matrices, almacenan sus miembros de forma contigua, razón por la cual, como veremos más
adelante, se les pueden aplicar punteros y una cierta aritmética. Por la misma razón, se pueden crear matrices de estructuras e
incluso estructuras de matrices (sus miembros son matrices). Conceptualmente, estas últimas no se diferencian gran cosa de las
matrices de matrices (a no ser en la notación de acceso a sus miembros). También se puede calcular su tamaño en bytes con la
expresión sizeof, aunque a este respecto debe tenerse en cuenta algunas posibles discrepancias respecto a los valores
"teóricos", debidas a las alineaciones internas realizadas por el compilador, que se comentan
.
§4.1 Se ha dicho que los miembros de estructuras pueden ser de cualquier tipo, incluso otras estructuras
( 4.5.6 Matrices de estructuras). Sin
embargo, existen varias limitaciones, sobre todo las que se opongan al principio anterior. Por ejemplo, la declaración que sigue
conduce a un error:
struct str {int x; char * psch = "Hola";}; // Error!
La razón ya expuesta (4.5.1),
que en la definición de un tipo no pueden realizarse asignaciones, tiene su base en el comentario anterior. En efecto: en
"Constantes de cadena" (
3.2.3f), se indicó que ante la expresión char* psch = "Hola";, el compilador debe
crear una matriz de caracteres "Hola\0"; almacenarlo en algún sitio, y asignar a una variable tipo puntero a carácter
psch (dirección del primer elemento de la matriz). Esto conduciría a que no se podría garantizar que los miembros
estuvieran íntegramente en la estructura.
La expresión anterior sería admisible con una modificación del tipo:
char* psch = "Hola";
struct str {int x; char* psch;};
pero ¡Atención!, con este esquema, el miembro
psch de str no tiene nada que ver con el puntero a carácter psch de la primera línea
(
4.5.1d Espacio de nombres de estructuras ). str.psch
sigue siendo un puntero a carácter genérico.
§4.2 Una alternativa es efectuar la asignación en una variable concreta (en POO diríamos
en una instancia de la clase str):
char * psch = "Hola";
struct str {int x; char * psch;}; // define el tipo de estructura str
struct str strA; // declara strA estructura tipo str
strA.psch = psch; nbsp; // asigna valor
Podemos efectuar una comprobación de la asignación:
printf("%s\n", strA.psch); // -> Hola
Hay que hacer hincapié en que el miembro strA.psch (puntero a carácter) apunta ahora a la dirección de una zona de
memoria donde está almacenada la cadena "Hola\0", dirección que coincide con la indicada por el puntero
[2] psch. Si cambiamos el valor de psch, el del miembro no se verá afectado, lo que podemos
comprobar añadiendo estas sentencias:
char * psch = "Lola";
printf("%s\n", strA.psch); // -> Hola
El resultado evidencia que la cadena "Hola" sigue existiendo en alguna posición de memoria y es accedida mediante strA.psch.
Se habría obtenido un resultado distinto si en vez de las dos sentencias anteriores modificamos directamente la posición de memoria donde está alojada la cadena "Hola". Lo hacemos mediante:
*psch = 'L'; *(psch+3) = 'o';
printf("%s\n", strA.psch); // -> Lolo
Observe que el paréntesis *(psch+3) es necesario, ya que *psch+3 es interpretado como (*psch)+3 =
'K'. Con lo que el intento de asignación 'K' = 'o' produce un error del compilador: No es un Lvalue
. Ya
vimos que el miembro a la izquierda de una asignación debe ser un Lvalue
( 2.1.5).
§4.3 Una variante de la declaración anterior, señalada (§5.1
) es la siguiente:
struct str {int x; char * psch;}; // define el tipo de estructura str
struct str strA = {1, "Hola"}; // declara e inicia strA
printf("%s\n", strA.psch); // salida: Hola
La situación final es análoga a la allí expuesta; el resultado es una cadena "Hola\0" almacenada en algún sitio y un puntero a carácter strA.psch apuntando a ella, como miembro de la estructura strA. Podemos comprobarlo modificando el contenido de las posiciones de memoria que alojan la cadena mediante:
*strA.psch = 'L'; *(strA.psch+3) = 'o';
printf("%s\n", strA.psch); // -> Lolo
§5 Espacio de almacenamiento
A continuación se expone un ejemplo de estructura con una composición de miembros variada. En el comentario de cada línea se
indica el espacio de almacenamiento necesario en bytes [3] según las especificaciones declaradas para
el compilador utilizado
( 2.2.4 Representación interna y rango):
struct general {
int x; // L2. 4 bytes
char ch; // L3. 1 byte
double db; // L4. 8 bytes
char * sg; // L5. 4 bytes
char nom[30]; // L6. 30 bytes
char * dir[]; // L7. 4 bytes
} str; // L8.
printf("M1:%3.0d\n", sizeof(str.x)); // L9. -> M1: 4
printf("M2:%3.0d\n", sizeof(str.ch)); // L10. -> M2: 1
printf("M3:%3.0d\n", sizeof(str.db)); // L11. -> M3: 8
printf("M4:%3.0d\n", sizeof(str.sg)); // L12. -> M4: 4
printf("M5:%3.0d\n", sizeof(str.nom)); // L13. -> M5: 30
printf("M6:%3.0d\n", sizeof(str.dir[0])); // L14. -> M6: 4
printf("Total:%3.0d\n", sizeof(str)); // L15. -> Total: 56
Las líneas 2, 3 y 4 no requieren comentario; L5 define un puntero a carácter, ocupará 4 bytes en un compilador de 32 bits
( 4.2 Punteros). L6 define una
matriz de 30 caracteres, por lo que se reserva espacio para otros tantos. Finalmente, L7 define un puntero a matriz de caracteres;
este miembro ocupa un espacio de 4 bytes en la estructura, aunque la matriz a la que apunta (exterior a la propia estructura) sea
de tamaño indefinido.
Las líneas 9 a 14 son simplemente una confirmación de que las suposiciones teóricas son correctas, como efectivamente se
comprueba. La línea 15 es una comprobación del tamaño total de la estructura, con el sorprendente resultado que se indica (la
compilación se ha realizado con la opción de alineación por defecto -a4
4.5.9a).
La razón de la discrepancia hay que buscarla en el hecho de que, por razones de eficiencia (por ejemplo de velocidad de acceso
a memoria), el compilador, que asigna espacio de memoria en múltiplos de de un cierto número determinado de bits (8 en este caso),
intenta no utilizar fracciones de palabra, con lo que realiza determinados redondeos o alineaciones internas
( 4.5.9a). En este caso, la suma
teórica: 51 x 8 = 408 bits habría supuesto el uso de 12.75 palabras de 32 bits, por lo que el compilador redondea a 14 palabras;
56 x 8 = 448 bits, 14 palabras. Hay que recordar que, aunque existan 5 bytes perdidos (no utilizados), el almacenamiento ocupado
por la estructura str sigue siendo contiguo, y el acceso a sus miembros totalmente transparente para el programador. Del
mismo modo, si más tarde se declara una matriz de estructuras de tipo general y se utilizan punteros, el compilador tiene
automáticamente en cuenta el verdadero tamaño ocupado por cada elemento, a fin de efectuar correctamente los desplazamientos
pertinentes (Aritmética de punteros
4.2.2).
Nota: tanto el compilador Borlanc C++ 5.5 como el MS Visual C++ permiten establecer alineaciones en múltiplos de 1, 2, 4, 8 y 16 octetos.
Como puede verse, en términos de tamaño de la estructura, las estrategias representadas por la líneas 6 y 7 varían
grandemente. La adopción de una u otra, depende del contexto de cada caso. Téngase en cuenta, por ejemplo, que si se guardase en
disco el contenido de la estructura, una recuperación posterior garantizaría el contenido original para el elemento
str.nom. Sin embargo, los punteros str.sg y str.dir quizás podrían apuntar a zonas de memoria erróneas.
[1] No confundir con la definición de la propia estructura, donde los miembros aparecen separados por punto y coma.
[2] Nótese que esto no contradice el principio de que los miembros deben estar contenidos en la estructura y ocupar zonas contiguas de almacenamiento: El puntero "pertenece a" la estructura y su valor es contiguo con el resto de los miembros, otra cosa es que este valor pueda ser considerado la dirección de una zona de memoria arbitraria (incluso inexistente) en la máquina.
[3] Suponemos que se utiliza el Compilador C++ 5.5 para Windows 32 de Borland.