Si pasa o retorna una dirección (ya sea un puntero o una referencia), el programador cliente puede recoger y modificar el valor al que apunta. Si hace que el puntero o referencia sea constante, impedirá que esto suceda, lo que puede ahorrarle problemas. De hecho, cada vez que se pasa una dirección como parámetro a una función, debería hacerla constante siempre que sea posible. Si no lo hace, está excluyendo la posibilidad de usar la función con constantes.
La opción de devolver un puntero o referencia constante depende de lo que quiera permitir hacer al programador cliente. Aquí se muestra un ejemplo que demuestra el uso de punteros constantes como argumentos de funciones y valores de retorno.
//: C08:ConstPointer.cpp // Constant pointer arg/return void t(int*) {} void u(const int* cip) { //! *cip = 2; // Illegal -- modifies value int i = *cip; // OK -- copies value //! int* ip2 = cip; // Illegal: non-const } const char* v() { // Returns address of static character array: return "result of function v()"; } const int* const w() { static int i; return &i; } int main() { int x = 0; int* ip = &x; const int* cip = &x; t(ip); // OK //! t(cip); // Not OK u(ip); // OK u(cip); // Also OK //! char* cp = v(); // Not OK const char* ccp = v(); // OK //! int* ip2 = w(); // Not OK const int* const ccip = w(); // OK const int* cip2 = w(); // OK //! *w() = 1; // Not OK } ///:~
Listado 8.7. C08/ConstPointer.cpp
La función t()
toma un puntero no-constante
ordinario como argumento, y u()
toma un
puntero constante. En el cuerpo de u()
puede ver un intento de modificar el valor de un puntero
constante, algo incorrecto, pero puede copiar su valor en una
variable no constante. El compilador también impide crear un
puntero no constante y almacenar en él la dirección contenida en
un puntero constante.
Las funciones v()
y
w()
prueban las semánticas de retorno de
valores. v()
devuelve un const
char*
que se crea a partir de un literal de
cadena. Esta sentencia en realidad genera la dirección del
literal una vez que el compilador lo crea y almacena en área
de almacenamiento estática. Como se ha dicho antes,
técnicamente este vector de caracteres es una constante, como
bien indica el tipo de retorno de v()
.
El valor de retorno de w()
requiere que
tanto el puntero como lo que apunta sean constantes. Como en
v()
, el valor devuelto por
w()
es valido una vez terminada la
función solo porque es estático. Nunca debe devolver un
puntero a una variable local pues se almacenan en la pila y al
terminar la función los datos de la pila desaparecen. Lo que
si puede hacer es devolver punteros que apuntan a datos
almacenados en el montón
(heap), pues siguen siendo
validos después de terminar la función.
En main()
se prueban las funciones con
varios argumentos. Puede ver que t()
aceptará como argumento un puntero ordinario, pero si
intenta pasarle un puntero a una constante, no hay garantía
de que no vaya a modificarse el valor de la variable
apuntada; por ello el compilador lo indica con un mensaje de
error. u()
toma un puntero a constante,
así que puede aceptar los dos tipos de argumentos. Por eso
una función que acepta un puntero a constante es más general
que una que acepta un puntero ordinario.
Como es lógico, el valor de retorno de
v()
sólo se puede asignar a un puntero a
constante. También era de esperar que el compilador rehuse
asignar el valor devuelto por w()
a un
puntero ordinario, y que sí acepte un const int*
const
, pero podría sorprender un poco que también
acepta un const int*
, que no es exactamente el
tipo de retorno declarado en la función. De nuevo, como el
valor (que es la dirección contenida en el puntero) se copia,
el requisito de que la variable original permanezca
inalterable se cumple automáticamente. Por eso, el segundo
const
en la declaración const int* const
sólo se aplica cuando lo use como recipiente, en cuyo caso el
compilador lo impediría.
En C es muy común el paso por valor, y cuando se quiere pasar una dirección la única posibilidad es usar un puntero[63]. Sin embargo, ninguno de estos modos es el preferido en C++. En su lugar, la primera opción cuando se pasa un parámetro es hacerlo por referencia o mejor aún, por referencia constante. Para el cliente de la función, la sintaxis es idéntica que en el paso por valor, de ese modo no hay confusión posible con los punteros, no hay que pensar en términos de punteros. Para el creador de una función, pasar una dirección es siempre más eficiente que pasar un objeto completo, y si pasa por referencia constante significa que la función no podrá cambiar lo almacenado en esa dirección, así que el efecto desde el punto de vista del programador cliente es lo mismo que el paso por valor (sin embargo es más eficiente).
A causa de la sintaxis de las referencias (para el cliente es igual que el paso por valor) es posible pasar un objeto temporario a una función que toma una referencia constante, mientras que nunca puede pasarse un objeto temporario a una función que toma un puntero (con un puntero, la dirección debe darse explícitamente). Así que con el paso por referencia se produce una nueva situación que nunca ocurre en C: un temporario, que es siempre constante, puede pasar su dirección a una función (una función puede tomar por argumento la dirección de un temporario). Esto es así porque, para permitir que los temporarios se pasen por referencia, el argumento debe ser una referencia constante. El siguiente ejemplo lo demuestra:
//: C08:ConstTemporary.cpp // Temporaries are const class X {}; X f() { return X(); } // Return by value void g1(X&) {} // Pass by non-const reference void g2(const X&) {} // Pass by const reference int main() { // Error: const temporary created by f(): //! g1(f()); // OK: g2 takes a const reference: g2(f()); } ///:~
Listado 8.8. C08/ConstTemporary.cpp
f()
retorna un objeto de la clase
X
por valor. Esto significa que
cuando tome el valor de retorno y lo pase inmediatamente a
otra función como en las llamadas a
g1()
y g2()
, se
crea un temporario y los temporarios son siempre
constantes. Por eso, la llamada a g1()
es un error pues g1()
no acepta una
referencia constante, mientras que la llamada a
g2()
sí es correcta.
[63] Algunos autores dicen que todo en C se pasa por valor, ya que cuando se pasa un puntero se hace también una copia (de modo que el puntero se pasa por valor). En cualquier caso, hacer esta precisión puede, en realidad, confundir la cuestión.