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 a 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 que intentar modificar aquello a lo que apunta el puntero constante es ilegal, pero, por supuesto, puede copiar esa información 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 la semántica de los valores de retorno. 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 el área de almacenamiento estático. Como se ha dicho antes, técnicamente este array 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 válido una vez terminada la función sólo 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 sí puede hacer es devolver punteros que apuntan a datos almacenados en el montículo (heap), pues siguen siendo válidos 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 rehúse 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 acepte 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 tiene sentido cuando intenta usarlo como valor izquierdo, en cuyo caso el compilador se lo impide.

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[65]. 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 a la del 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 casi 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 (sólo que 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 temporal a una función que toma una referencia constante, mientras que nunca puede pasarse un objeto temporal a una función que toma un puntero (con un puntero, la dirección debe tomarse explícitamente). Así que con el paso por referencia se produce una nueva situación que nunca ocurre en C: se puede pasar a una función la dirección de un objeto temporal, que siempre es constante. Por eso, para permitir que los objetos temporales 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 objeto temporal, y ese objeto temporal es constante. 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.



[65] 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). Por muy preciso que sea esto, creo que en realidad confunde la cuestión.