Los vectores son un tipo compuesto porque permiten agrupar muchas variables, una a continuación de la otra, bajo un identificador único. Si dice:
int a[10];
Se crea espacio para 10 variables int
colocadas una
después de la otra, pero sin identificadores únicos para cada
variable. En su lugar, todas están englobadas por el nombre
a
.
Para acceder a cualquiera de los elementos del vector, se utiliza la misma sintaxis de corchetes que se utiliza para definir el vector:
a[5] = 47;
Sin embargo, debe recordar que aunque el tamaño de
a
es 10
, se seleccionan
los elementos del vector comenzando por cero (esto se llama a
veces indexado a cero[42], de modo que sólo se pueden seleccionar los
elementos del vector de 0 a 9, como sigue:
//: C03:Arrays.cpp #include <iostream> using namespace std; int main() { int a[10]; for(int i = 0; i < 10; i++) { a[i] = i * 10; cout << "a[" << i << "] = " << a[i] << endl; } } ///:~
Listado 3.50. C03/Arrays.cpp
Los accesos a vectores son extremadamente rápidos, Sin embargo,
si se indexa más allá del final del vector, no hay ninguna red
de seguridad - se entrará en otras variables. La otra desventaja
es que se debe definir el tamaño del vector en tiempo de
compilación; si se quiere cambiar el tamaño en tiempo de
ejecución no se puede hacer con la sintaxis anterior (C tiene
una manera de crear un vector dinámicamente, pero es
significativamente más sucia). El vector
de C++ presentado en el capítulo anterior, proporciona un objeto
parecido al vector que se redimensiona automáticamente , de modo
que es una solución mucho mejor si el tamaño del vector no puede
conocer en tiempo de compilación.
Se puede hacer un vector de cualquier tipo, incluso de
struct
s:
//: C03:StructArray.cpp // An array of struct typedef struct { int i, j, k; } ThreeDpoint; int main() { ThreeDpoint p[10]; for(int i = 0; i < 10; i++) { p[i].i = i + 1; p[i].j = i + 2; p[i].k = i + 3; } } ///:~
Listado 3.51. C03/StructArray.cpp
Fíjese como el identificador de struct
i
es independiente del i
del bucle for
.
Para comprobar que cada elemento del vector es contiguo con el siguiente, puede imprimir la dirección de la siguiente manera:
//: C03:ArrayAddresses.cpp #include <iostream> using namespace std; int main() { int a[10]; cout << "sizeof(int) = "<< sizeof(int) << endl; for(int i = 0; i < 10; i++) cout << "&a[" << i << "] = " << (long)&a[i] << endl; } ///:~
Listado 3.52. C03/ArrayAddresses.cpp
Cuando se ejecuta este programa, se ve que cada elemento está
separado por el tamaño de un int
del anterior. Esto
significa, que están colocados uno a continuación del otro.
El identificador de un vector es diferente de los identificadores de las variables comunes. Un identificador de un vector no es un lvalue; no se le puede asignar nada. En realidad es FIXME:gancho dentro de la sintaxis de corchetes, y cuando se usa el nombre de un vector, sin los corchetes, lo que se obtiene es la dirección inicial del vector:
//: C03:ArrayIdentifier.cpp #include <iostream> using namespace std; int main() { int a[10]; cout << "a = " << a << endl; cout << "&a[0] =" << &a[0] << endl; } ///:~
Listado 3.53. C03/ArrayIdentifier.cpp
Cuando se ejecuta este programa, se ve que las dos direcciones
(que se imprimen en hexadecimal, ya que no se moldea a
long
) son las misma.
De modo que una manera de ver el identificador de un vector es como un puntero de sólo lectura al principio de éste. Y aunque no se pueda hacer que el identificador del vector apunte a cualquier otro sitio, se puede crear otro puntero y utilizarlo para moverse dentro del vector. De hecho, la sintaxis de corchetes también funciona con punteros convencionales:
//: C03:PointersAndBrackets.cpp int main() { int a[10]; int* ip = a; for(int i = 0; i < 10; i++) ip[i] = i * 10; } ///:~
Listado 3.54. C03/PointersAndBrackets.cpp
El hecho de que el nombre de un vector produzca su dirección
de inicio resulta bastante importante cuando hay que pasar un
vector a una función. Si declara un vector como un argumento
de una función, lo que realmente está declarando es un
puntero. De modo que en el siguiente ejemplo,
fun1()
y func2()
tienen la misma lista de argumentos:
//: C03:ArrayArguments.cpp #include <iostream> #include <string> using namespace std; void func1(int a[], int size) { for(int i = 0; i < size; i++) a[i] = i * i - i; } void func2(int* a, int size) { for(int i = 0; i < size; i++) a[i] = i * i + i; } void print(int a[], string name, int size) { for(int i = 0; i < size; i++) cout << name << "[" << i << "] = " << a[i] << endl; } int main() { int a[5], b[5]; // Probably garbage values: print(a, "a", 5); print(b, "b", 5); // Initialize the arrays: func1(a, 5); func1(b, 5); print(a, "a", 5); print(b, "b", 5); // Notice the arrays are always modified: func2(a, 5); func2(b, 5); print(a, "a", 5); print(b, "b", 5); } ///:~
Listado 3.55. C03/ArrayArguments.cpp
A pesar de que func1()
y
func2()
declaran sus argumentos de
distinta forma, el uso es el mismo dentro de la función. Hay
otros hechos que revela este ejemplo: los vectores no se pueden
pasados por valor[43], es decir, que nunca se puede obtener
automáticamente una copia local del vector que se pasa a una
función. Por eso, cuando se modifica un vector, siempre se
está modificando el objeto externo. Eso puede resultar un poco
confuso al principio, si lo que se espera es el paso-por-valor
como en los argumentos ordinarios.
Fíjese que print()
utiliza la sintaxis de
corchetes para los argumentos de tipo vector. Aunque la
sintaxis de puntero y la sintaxis de corchetes efectivamente
es la mismo cuando se están pasando vectores como argumentos,
la sintaxis de corchetes deja más clara al lector que se
pretende enfatizar que dicho argumento es un vector.
Observe también que el argumento size
se
pasa en cada caso. La dirección no es suficiente información
al pasar un vector; siempre se debe ser posible obtener el
tamaño del vector dentro de la función, de manera que no se
salga de los límites de dicho vector.
Los vectores pueden ser de cualquier tipo, incluyendo vectores
de punteros. De hecho, cuando se quieren pasar argumentos de
tipo línea de comandos dentro del programa, C y C++ tienen una
lista de argumentos especial para main()
,
que tiene el siguiente aspecto:
int main(int argc, char* argv[]) { // ...
El primer argumento es el número de elementos en el vector,
que es el segundo argumento. El segundo argumento es siempre
un vector de char*
, porque los argumentos se
pasan desde la línea de comandos como vectores de caracteres
(y recuerde, un vector sólo se puede pasar como un
puntero). Cada bloque de caracteres delimitado por un espacio
en blanco en la línea de comandos se aloja en un elemento
separado en el vector. El siguiente programa imprime todos los
argumentos de línea de comandos recorriendo el vector:
//: C03:CommandLineArgs.cpp #include <iostream> using namespace std; int main(int argc, char* argv[]) { cout << "argc = " << argc << endl; for(int i = 0; i < argc; i++) cout << "argv[" << i << "] = " << argv[i] << endl; } ///:~
Listado 3.56. C03/CommandLineArgs.cpp
Observe que argv[0]
es la ruta y el nombre del
programa en sí mismo. Eso permite al programa descubrir
información de sí mismo. También añade un argumento más al
vector de argumentos del programa, de modo que un error común
al recoger argumentos de línea de comandos es tomar argv[0]
como si fuera el primer argumento.
No es obligatorio utilizar argc
y
argv
como identificadores de los parámetros
de main()
; estos identificadores son sólo
convenciones (pero puede confundir al lector si no se
respeta). También, hay un modo alternativo de declarar argv:
int main(int argc, char** argv) { // ...
Las dos formas son equivalentes, pero la versión utilizada en este libro es la más intuitiva al leer el código, ya que dice, directamente, «Esto es un vector de punteros a carácter».
Todo lo que se obtiene de la línea de comandos son vectores de
caracteres; si quiere tratar un argumento como algún otro
tipo, ha de convertirlos dentro del programa. Para facilitar
la conversión a números, hay algunas funciones en la librería
de C Estándar, declaradas en <cstdlib>
. Las más fáciles de
utilizar son atoi()
,
atol()
, y atof()
para convertir un vector de caracteres ASCII a
int
, long
y double
,
respectivamente. A continuación, un ejemplo utilizando
atoi()
(las otras dos funciones se
invocan del mismo modo):
//: C03:ArgsToInts.cpp // Converting command-line arguments to ints #include <iostream> #include <cstdlib> using namespace std; int main(int argc, char* argv[]) { for(int i = 1; i < argc; i++) cout << atoi(argv[i]) << endl; } ///:~
Listado 3.57. C03/ArgsToInts.cpp
En este programa, se puede poner cualquier número de
argumentos en la línea de comandos. Fíjese que el bucle
for
comienza en el valor 1
para
saltar el nombre del programa en
argv[0]
. También, si se pone un número decimal
que contenga un punto decimal en la línea de comandos,
atoi()
sólo toma los dígitos hasta el
punto decimal. Si pone valores no numéricos en la línea de
comandos, atoi()
los devuelve como ceros.
La función printBinary()
presentada
anteriormente en este capítulo es útil para indagar en la
estructura interna de varios tipos de datos. El más
interesante es el formato de punto-flotante que permite a C y
C++ almacenar números que representan valores muy grandes y
muy pequeños en un espacio limitado. Aunque los detalles no se
pueden exponer completamente expuestos, los bits dentro de los
float
s y double
s están divididos en
tres regiones: el exponente, la mantisa, y el bit de signo;
así almacena los valores utilizando notación científica. El
siguiente programa permite jugar con ello imprimiendo los
patrones binarios de varios números en punto-flotante de modo
que usted mismo pueda deducir el esquema del formato de punto
flotante de su compilador (normalmente es el estándar IEEE
para números en punto-flotante, pero su compilador puede no
seguirlo):
//: C03:FloatingAsBinary.cpp //{L} printBinary //{T} 3.14159 #include "printBinary.h" #include <cstdlib> #include <iostream> using namespace std; int main(int argc, char* argv[]) { if(argc != 2) { cout << "Must provide a number" << endl; exit(1); } double d = atof(argv[1]); unsigned char* cp = reinterpret_cast<unsigned char*>(&d); for(int i = sizeof(double)-1; i >= 0 ; i -= 2){ printBinary(cp[i-1]); printBinary(cp[i]); } } ///:~
Listado 3.58. C03/FloatingAsBinary.cpp
Primero, el programa garantiza que se le haya pasado un
argumento comprobando el valor de argc
, que
vale dos si hay un solo argumento (es uno si no hay
argumentos, ya que el nombre del programa siempre es el primer
elemento de argv
). Si eso falla, imprime un
mensaje e invoca la función exit()
de la
librería Estándar de C para finalizar el programa.
El programa toma el argumento de la línea de comandos y
convierte los caracteres a double
utilizando
atof()
. Luego el double
se
trata como un vector de bytes tomando la dirección y
moldeándola a un unsigned char*
. Para cada uno de
estos bytes se llama a printBinary()
para
mostrarlos.
Este ejemplo se ha creado para imprimir los bytes en un orden tal que el bit de signo aparece al principio - en mi máquina. En otras máquinas puede ser diferente, por lo que puede querer re-organizar el modo en que se imprimen los bytes. También debería tener cuidado porque los formatos en punto-flotante no son tan triviales de entender; por ejemplo, el exponente y la mantisa no se alinean generalmente entre los límites de los bytes, en su lugar un número de bits se reserva para cada uno y se empaquetan en la memoria tan apretados como se pueda. Para ver lo que esta pasando, necesitaría averiguar el tamaño de cada parte del número (los bit de signo siempre son de un bit, pero los exponentes y las mantisas pueden ser de diferentes tamaños) e imprimir separados los bits de cada parte.
Si todo lo que se pudiese hacer con un puntero que apunta a un vector fuese tratarlo como si fuera un alias para ese vector, los punteros a vectores no tendrían mucho interés. Sin embargo, los punteros son mucho más flexibles que eso, ya que se pueden modificar para apuntar a cualquier otro sitio (pero recuerde, el identificador del vector no se puede modificar para apuntar a cualquier otro sitio).
La aritmética de punteros se refiere a la aplicación de alguno de los operadores aritméticos a los punteros. Las razón por la cual la aritmética de punteros es un tema separado de la aritmética ordinaria es que los punteros deben ajustarse a cláusulas especiales de modo que se comporten apropiadamente. Por ejemplo, un operador común para utilizar con punteros es ++, lo que "añade uno al puntero." Lo que de hecho significa esto es que el puntero se cambia para moverse al "siguiente valor," Lo que sea que ello signifique. A continuación, un ejemplo:
//: C03:PointerIncrement.cpp #include <iostream> using namespace std; int main() { int i[10]; double d[10]; int* ip = i; double* dp = d; cout << "ip = " << (long)ip << endl; ip++; cout << "ip = " << (long)ip << endl; cout << "dp = " << (long)dp << endl; dp++; cout << "dp = " << (long)dp << endl; } ///:~
Listado 3.59. C03/PointerIncrement.cpp
Para una ejecución en mi máquina, la salida es:
ip = 6684124 ip = 6684128 dp = 6684044 dp = 6684052
Lo interesante aquí es que aunque la operación ++
parece la misma tanto para el int*
como para el
double*
, se puede comprobar que el puntero de
int*
ha cambiado 4 bytes mientras que para el
double*
ha cambiado 8. No es coincidencia, que
estos sean los tamaños de int
y
double
en esta máquina. Y ese es el truco de la
aritmética de punteros: el compilador calcula la cantidad
apropiada para cambiar el puntero de modo que apunte al
siguiente elemento en el vector (la aritmética de punteros
sólo tiene sentido dentro de los vectores). Esto funciona incluso
con vectores de struct
s:
//: C03:PointerIncrement2.cpp #include <iostream> using namespace std; typedef struct { char c; short s; int i; long l; float f; double d; long double ld; } Primitives; int main() { Primitives p[10]; Primitives* pp = p; cout << "sizeof(Primitives) = " << sizeof(Primitives) << endl; cout << "pp = " << (long)pp << endl; pp++; cout << "pp = " << (long)pp << endl; } ///:~
Listado 3.60. C03/PointerIncrement2.cpp
La salida en esta máquina es:
sizeof(Primitives) = 40 pp = 6683764 pp = 6683804
Como puede ver, el compilador también hace lo adecuado para
punteros a struct
s (y con class
y
union
).
La aritmética de punteros también funciona con los operadores
--
, +
y -
, pero los dos
últimos están limitados: no se puede sumar dos punteros, y si
se restan punteros el resultado es el número de elementos
entre los dos punteros. Sin embargo, se puede sumar o restar
un valor entero y un puntero. A continuación, un ejemplo
demostrando el uso de la aritmética de punteros:
//: C03:PointerArithmetic.cpp #include <iostream> using namespace std; #define P(EX) cout << #EX << ": " << EX << endl; int main() { int a[10]; for(int i = 0; i < 10; i++) a[i] = i; // Give it index values int* ip = a; P(*ip); P(*++ip); P(*(ip + 5)); int* ip2 = ip + 5; P(*ip2); P(*(ip2 - 4)); P(*--ip2); P(ip2 - ip); // Yields number of elements } ///:~
Listado 3.61. C03/PointerArithmetic.cpp
Comienza con otra macro, pero esta utiliza una característica
del preprocesador llamada
stringizing (implementada
mediante el signo # antes de una expresión) que
toma cualquier expresión y la convierte a un vector de
caracteres. Esto es bastante conveniente, ya que permite
imprimir la expresión seguida de dos puntos y del valor de la
expresión. En main()
puede ver lo útil
que resulta este atajo.
Aunque tanto la versión prefijo como sufijo de ++
y --
son válidas para los punteros, en este
ejemplo sólo se utilizan las versiones prefijo porque se
aplican antes de referenciar el puntero en las expresiones
anteriores, de modo que permite ver los efectos en las
operaciones. Observe que se han sumado y restado valores
enteros; si se combinasen de este modo dos punteros, el
compilador no lo permitiría.
Aquí se ve la salida del programa anterior:
*ip: 0 *++ip: 1 *(ip + 5): 6 *ip2: 6 *(ip2 - 4): 2 *--ip2: 5
En todos los casos, el resultado de la aritmética de punteros es que el puntero se ajusta para apuntar al «sitio correcto», basándose en el tamaño del tipo de los elementos a los que está apuntado.
Si la aritmética de punteros le sobrepasa un poco al
principio, no tiene porqué preocuparse. La mayoría de las
veces sólo la necesitará para crear vectores e indexarlos con
[]
, y normalmente la aritmética de punteros más
sofisticada que necesitará es ++
y
--
. La aritmética de punteros generalmente está
reservada para programas más complejos e ingeniosos, y
muchos de los contenedores en la librería de Estándar C++
esconden muchos de estos inteligentes detalles, por lo que no
tiene que preocuparse de ellos.
[42] (N. de T.) zero indexing
[43] A menos que tome la siguiente aproximación estricta: «todos los argumentos pasado en C/C++ son por valor, y el «valor» de un vector es el producido por su identificador: su dirección». Eso puede parecer correcto desde el punto de vista del lenguaje ensamblador, pero yo no creo que ayude cuando se trabaja con conceptos de alto nivel. La inclusión de referencias en C++ hace que el argumento «todo se pasa por valor» sea más confuso, hasta el punto de que siento que es más adecuado pensar en términos de «paso por valor» vs «paso por dirección».