El problema se produce debido a que el compilador hace una suposición sobre cómo crear un nuevo objeto a partir de de otro existente. Cuando se pasa un objeto por valor, se crea un nuevo objeto, que estará dentro del ámbito de la función, a partir del objeto original existente fuera del ámbito de la función. Esto también se puede aplicar a menudo cuando una función retorna un objeto. En la expresión
HowMany h2 = f(h);
h2
, un objeto que no estaba creado
anteriormente, se crea a partir del valor que retorna
f()
; por tanto también se crea un nuevo
objeto a partir de otro existente.
El compilador supone que la creación ha de hacerse con una
copia bit a bit, lo que en muchos casos funciona bien, pero en
HowMany
no funciona porque la
inicialización va más allá de una simple copia. Otro ejemplo
muy común ocurre cuando la clase contiene punteros pues, ¿a
qué deben apuntar? ¿debería copiar sólo los punteros o
debería asignar memoria nueva y que apuntaran a ella?
Afortunadamente, puede intervenir en este proceso e impedir
que el compilador haga una copia bit a bit. Se soluciona
definiendo su propia función cuando el compilador necesite
crear un nuevo objeto a partir de otro. Lógicamente, está
creando un nuevo objeto, por lo que esta función es un
constructor, y el único argumento del constructor tiene que
ver con el objeto del que se pretende partir para crear el
nuevo. Pero no puede pasar ese objeto por valor al constructor
porque está intentando definir la función
que maneja el paso por valor, y, por otro lado,
sintácticamente no tiene sentido pasar un puntero porque,
después de todo, está creando un objeto a partir de de
otro. Aquí es cuando las referencias vienen al rescate, y
puede utilizar la referencia del objeto origen. Esta función
se llama constructor de copia, que
también se lo puede encontrar como
X(X&)
, que es el constructor de copia
de una clase llamada X
.
Si crea un constructor de copia, el compilador no realizará una copia bit a bit cuando cree un nuevo objeto a partir de otro. El compilador siempre llamará al constructor de copia. Si no crea el constructor de copia, el compilador intentará hacer algo razonable, pero usted tiene la opción de tener control total del proceso.
Ahora es posible solucionar el problema en
HowMany.cpp
:
//: C11:HowMany2.cpp // The copy-constructor #include <fstream> #include <string> using namespace std; ofstream out("HowMany2.out"); class HowMany2 { string name; // Object identifier static int objectCount; public: HowMany2(const string& id = "") : name(id) { ++objectCount; print("HowMany2()"); } ~HowMany2() { --objectCount; print("~HowMany2()"); } // The copy-constructor: HowMany2(const HowMany2& h) : name(h.name) { name += " copy"; ++objectCount; print("HowMany2(const HowMany2&)"); } void print(const string& msg = "") const { if(msg.size() != 0) out << msg << endl; out << '\t' << name << ": " << "objectCount = " << objectCount << endl; } }; int HowMany2::objectCount = 0; // Pass and return BY VALUE: HowMany2 f(HowMany2 x) { x.print("x argument inside f()"); out << "Returning from f()" << endl; return x; } int main() { HowMany2 h("h"); out << "Entering f()" << endl; HowMany2 h2 = f(h); h2.print("h2 after call to f()"); out << "Call f(), no return value" << endl; f(h); out << "After call to f()" << endl; } ///:~
Listado 11.7. C11/HowMany2.cpp
Hay unas cuantas cosas nuevas para que pueda hacerse una idea
mejor de lo que pasa. Primeramente, el string
name
hace de identificador de objeto cuando se imprima
en la salida. Puede poner un identificador (normalmente el
nombre del objeto) en el constructor para que se copie en
name
utilizando el constructor con un
string
como argumento. Por defecto se crea un
string
vacío. El constructor incrementa
objectCount
y el destructor lo disminuye,
igual que en el ejemplo anterior.
Lo siguiente es el constructor de copia,
HowMany2(const HowMany2&)
. El
constructor de copia simplemente crea un objeto a partir de
otro existente, así que copia en name
el
identificador del objeto origen, seguido de la palabra
«copy», y así puede ver de dónde procede. Si
mira atentamente, verá que la llamada
name(h.name)
en la lista de inicializadores del
constructor está llamando al constructor de copia de la clase
string
.
Dentro del constructor de copia, se incrementa el contador igual que en el constructor normal. Esto quiere decir que obtendrá un contador de objetos preciso cuando pase y retorne por valor.
La función print()
se ha modificado para
imprimir en la salida un mensaje, el identificador del objeto
y el contador de objetos. Como ahora accede al atributo
name
de un objeto concreto, ya no
puede ser un método estático.
Dentro de main()
puede ver que hay una
segunda llamada a f()
. Sin embargo esta
llamada utiliza la característica de C para ignorar el valor
de retorno. Pero ahora que sabe cómo se retorna el valor (es
decir, código dentro de la función que
maneja el proceso de retorno poniendo el resultado en un lugar
cuya dirección se pasa como un argumento escondido), podría
preguntarse qué ocurre cuando se ignora el valor de
retorno. La salida del programa mostrará alguna luz sobre el
asunto.
Pero antes de mostrar la salida, he aquí un pequeño programa
que utiliza iostreams
para añadir
números de línea a cualquier archivo:
//: C11:Linenum.cpp //{T} Linenum.cpp // Add line numbers #include "../require.h" #include <vector> #include <string> #include <fstream> #include <iostream> #include <cmath> using namespace std; int main(int argc, char* argv[]) { requireArgs(argc, 1, "Usage: linenum file\n" "Adds line numbers to file"); ifstream in(argv[1]); assure(in, argv[1]); string line; vector<string> lines; while(getline(in, line)) // Read in entire file lines.push_back(line); if(lines.size() == 0) return 0; int num = 0; // Number of lines in file determines width: const int width = int(log10((double)lines.size())) + 1; for(int i = 0; i < lines.size(); i++) { cout.setf(ios::right, ios::adjustfield); cout.width(width); cout << ++num << ") " << lines[i] << endl; } } ///:~
Listado 11.8. C11/Linenum.cpp
El archivo se pasa a un vector<string>
,
utilizando el mismo código fuente que ha visto
anteriormente en este libro. Cuando se ponen los números de
línea, nos gustaría que todas las líneas estuvieran alineadas,
y esto necesita conocer el número de líneas en el archivo para
que sea coherente. Se puede conocer el número de líneas con
vector::size()
, pero lo que realmente
necesitamos es conocer si hay más líneas de 10, 100, 1000,
etc. Si se utiliza el logaritmo en base 10 sobre el número de
líneas en el archivo, se trunca a un entero y se añade uno al
valor resultante, eso determinará el ancho máximo en dígitos
que un número de línea puede tener.
Nótese que hay un par de llamadas extrañas dentro del bucle
for
: setf()
y
width()
. Hay llamadas de
ostream
que permiten controlar, en este
caso, la justificación y anchura de la salida. Sin embargo se
debe llamar cada vez que se imprime línea y por eso están
dentro del bucle for
. El Volumen 2 de este libro
tiene un capítulo entero que explica los
iostreams
y que cuenta más sobre estas
llamadas así como otras formas de controlar los
iostreams
.
Cuando se aplica Linenum.cpp
a
HowMany2.out
, resulta:
1) HowMany2() 2) h: objectCount = 1 3) Entering f() 4) HowMany2(const HowMany2&) 5) h copy: objectCount = 2 6) x argument inside f() 7) h copy: objectCount = 2 8) Returning from f() 9) HowMany2(const HowMany2&) 10) h copy copy: objectCount = 3 11) ~HowMany2() 12) h copy: objectCount = 2 13) h2 after call to f() 14) h copy copy: objectCount = 2 15) Call f(), no return value 16) HowMany2(const HowMany2&) 17) h copy: objectCount = 3 18) x argument inside f() 19) h copy: objectCount = 3 20) Returning from f() 21) HowMany2(const HowMany2&) 22) h copy copy: objectCount = 4 23) ~HowMany2() 24) h copy: objectCount = 3 25) ~HowMany2() 26) h copy copy: objectCount = 2 27) After call to f() 28) ~HowMany2() 29) h copy copy: objectCount = 1 30) ~HowMany2() 31) h: objectCount = 0
Como se esperaba, la primera cosa que ocurre es que para
h
se llama al constructor normal, el cual
incrementa el contador de objetos a uno. Pero entonces,
mientras se entra en f()
, el compilador
llama silenciosamente al constructor de copia para hacer el
paso por valor. Se crea un nuevo objeto, que es copia de
h
(y por tanto tendrá el identificador
«h copy») dentro del ámbito de la función
f()
. Así pues, el contador de objetos se
incrementa a dos, por cortesía del constructor de copia.
La línea ocho indica el principio del retorno de
f()
. Pero antes de que se destruya la
variable local «h copy» (pues sale de ámbito al
final de la función), se debe copiar al valor de retorno, que
es h2
. Por tanto h2
, que
no estaba creado previamente, se crea de un objeto ya
existente (la variable local dentro de
f()
) y el constructor de copia vuelve a
utilizarse en la línea 9. Ahora el identificador de
h2
es «h copy copy» porque
copió el identificador de la variable local de
f()
. Cuando se devuelve el objeto, pero
antes de que la función termine, el contador de objetos se
incrementa temporalmente a tres, pero la variable local con
identificador «h copy» se destruye, disminuyendo
a dos. Después de que se complete la llamada a
f()
en la línea 13, sólo hay dos objetos,
h
y h2
, y puede
comprobar, de hecho, que h2
terminó con el
identificador «h copy copy».
En la línea 15 se empieza la llamada a f(h)
, y
esta vez ignora el valor de retorno. Puede ver que se invoca
el constructor de copia en la línea 16, igual que antes,
para pasar el argumento. Y también, igual que antes, en la
línea 21 se llama al constructor de copia para el valor de
retorno. Pero el constructor de copia necesita una dirección
para utilizar como destino (es decir, para trabajar con el
puntero this
). ¿De dónde procede esta dirección?
Esto prueba que el compilador puede crear un objeto temporal
cuando lo necesita para evaluar adecuadamente una
expresión. En este caso, crea uno que ni siquiera se le ve
actuar como destino para el valor ignorado retornado por
f()
. El tiempo de vida de este objeto
temporal es tan corto como sea posible para que el programa
no se llene de objetos temporales esperando a ser
destruidos, lo cual provocaría la utilización ineficaz de
recursos valiosos. En algunos casos, el objeto temporal
podría pasarse inmediatamente a otra función, pero en este
caso no se necesita después de la llamada a la función, así
que en cuanto la función termina, llamando al destructor del
objeto local (líneas 23 y 24), el objeto temporal también se
destruye (líneas 25 y 26).
Finalmente, de la línea 28 a la línea 31, se destruye el
objeto h2
, seguido de
h
y el contador de objetos vuelve a cero.