8.3.3. Paso y retorno de direcciones

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.

Criterio de paso de argumentos

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.