Saltar a contenido

Última actualización:2026-10-13

const en C++: constantes y los dos tipos de puntero const

Sobre el autor

Cuenta pública de WeChat:

const en C++ explicado: constantes y puntero const vs puntero a const | C++那些事

1. El significado de const

Un tipo const es un tipo declarado con el calificador de tipo const; el valor de una variable u objeto de tipo const no puede actualizarse.

2. El papel de const

  • Puede definir constantes
const int a=100;
  • Comprobación de tipos

    • Diferencia entre las constantes const y las constantes definidas con la macro #define:

      Una constante const tiene un tipo, por lo que el compilador puede hacer comprobaciones de seguridad; una definición de macro #define no tiene tipo de datos, es un simple reemplazo de cadenas y no permite comprobaciones de seguridad.Gracias a los dos expertos que señalaron el error aquí; véase: issue

    • Las constantes const admiten todos los tipos.

    • En los demás casos se trata solo de una variable cualificada con const; no la confundas con una constante.
  • Evitar modificaciones, ejercer un papel protector y aumentar la robustez del programa

void f(const int i){
    i++; // error!
}
  • Ahorrar espacio y evitar asignaciones de memoria innecesarias

    • Un const inicializado con un tipo entero/de enumeración y una expresión constante (es decir, una expresión constante) se comporta en la práctica más bien como una constante de compilación: si no se toma su dirección y solo se usa su valor, normalmente se optimiza como un inmediato, igual que #define; solo al tomar su dirección o al hacer un odr-use entre unidades de traducción pasa a memoria, y en todo el programa existe una sola copia.
    • Tras expandirse a un literal, #define se compila en las instrucciones como un inmediato; las propias instrucciones también ocupan memoria, por lo que varios usos significan varias apariciones (véase la discusión en el issue #5).
    • Nota: no todas las variables const son expresiones constantes; solo lo son las de tipo entero/enum inicializadas con una expresión constante; las demás variables const son solo variables cualificadas con const (por ejemplo, coma flotante, tipos definidos por el usuario); constexpr es la forma correcta de expresar constantes de compilación.

3. Los objetos const son locales al archivo por defecto

Nota: las variables no const son extern por defecto. Para que una variable const sea accesible desde otros archivos, debe especificarse explícitamente como extern en el archivo.

Acceso a una variable no modificada con const desde archivos distintos

// file1.cpp
int ext;
// file2.cpp
#include<iostream>

extern int ext;
int main(){
    std::cout<<(ext+10)<<std::endl;
}

Acceso a una constante const desde archivos distintos

// extern_file1.cpp
extern const int ext=12;
// extern_file2.cpp
#include<iostream>
extern const int ext;
int main(){
    std::cout<<ext<<std::endl;
}

Resumen:
Se puede observar que las variables no modificadas con const no necesitan declaración extern explícita, mientras que las constantes const requieren una declaración extern explícita y, además, inicialización. Como una constante no puede modificarse tras su definición, debe inicializarse en el momento de definirla.

4. Definir constantes

const int b = 10;
b = 0; // error: assignment of read-only variable ‘b’
const string s = "helloworld";
const int i,j=0 // error: uninitialized const ‘i’

Lo anterior contiene dos errores: + b es una constante, ¡no se puede cambiar! + i es una constante, ¡debe inicializarse! (Porque una constante no puede modificarse tras su definición, debe inicializarse en el momento de definirla.)

5. Punteros y const

Hay cuatro formas de const relacionadas con los punteros:

const char * a; // 指向const对象的指针或者说指向常量的指针。
char const * a; // 同上
char * const a; // 指向类型对象的const指针。或者说常指针、const指针。
const char * const a; // 指向const对象的const指针。

Resumen:
Si const está a la izquierda de *, entonces const califica a la variable apuntada por el puntero, es decir, lo apuntado es una constante;
si const está a la derecha de *, const califica al puntero mismo, es decir, el puntero en sí es una constante.

Otra forma de interpretarlo
Lee el inglés de derecha a izquierda, tomando «to» como frontera: lo que va antes de «to» describe la característica del puntero y lo que va después describe la característica del destino

const char * p; //p is a pointer to const char
char const * p; //同上
char * const p; //p is a const pointer to char
const char * const p; //p is a const pointer to const char
Cuando el puntero recibe la característica const, el puntero no puede cambiar la dirección a la que apunta
Cuando la característica del destino es char, el contenido puede modificarse a través del puntero, por ejemplo: *char='y';
Cuando la característica del destino es const char, el contenido no puede modificarse a través del puntero

El uso concreto es el siguiente:

(1) Puntero a una constante

const int *ptr;
*ptr = 10; // error

ptr es un puntero a un objeto const de tipo int; el const califica al tipo int, es decir, al tipo del objeto al que apunta ptr, y no a ptr mismo, por lo que ptr no necesita valor inicial. Pero no se puede modificar el valor del objeto apuntado a través de ptr.

Además, tampoco se puede usar un puntero void* para guardar la dirección de un objeto const; hay que usar un puntero de tipo const void* para guardar la dirección de un objeto const.

const int p = 10;
const void * vp = &p;
void *vp = &p; // error

Otro punto clave: se permite asignar la dirección de un objeto no const a un puntero a objeto const.

Asignar la dirección de un objeto no const a un puntero a objeto const:

const int *ptr;
int val = 3;
ptr = &val; // ok

¡No podemos modificar el valor de val a través del puntero ptr, aunque apunte a un objeto no const!

No podemos usar un puntero a objeto const para modificar el objeto subyacente; sin embargo, si el puntero apunta a un objeto no const, el objeto apuntado puede modificarse por otros medios. El valor apuntado por un puntero const sí puede cambiarse, ¡solo que no a través del puntero a objeto const! Se modifica así:

int *ptr1 = &val;
*ptr1=4;
cout<<*ptr<<endl;

Resumen:
1. Con un puntero a una constante, no se puede modificar el valor del objeto a través del puntero.
2. No se puede usar un puntero void* para guardar la dirección de un objeto const; hay que usar un puntero de tipo const void* para guardar la dirección de un objeto const.
3. Se permite asignar la dirección de un objeto no const a un puntero a objeto const; para modificar el valor del objeto apuntado hay que hacerlo por otros medios, no directamente a través del puntero actual.

(2) Puntero constante

Un puntero const debe inicializarse; el valor apuntado por un puntero const puede modificarse, pero su dirección no.

#include<iostream>
using namespace std;
int main(){
    int num=0, num1=1;
    int * const ptr=&num; // const指针必须初始化!且const指针的指向不能修改
    ptr = &num1; // error! const指针不能修改指向!
    cout<<*ptr<<endl;
}

El código da un error de compilación: un puntero const no puede cambiar aquello a lo que apunta.

#include<iostream>
using namespace std;
int main(){
    int num=0, num1=1;
    int * const ptr=&num; // const指针必须初始化!且const指针的指向不能修改
    *ptr = 1;
    cout<<*ptr<<endl;
}

Con este código no ocurre nada, imprime 1 con normalidad.

Por último, cuando se asigna a ptr la dirección de una constante const, como ptr apunta a una variable y no a una constante const, se produce un error: ¡el error const int* -> int *!

#include<iostream>
using namespace std;
int main(){
    const int num=0;
    int * const ptr=&num; // error! const int* -> int*
    cout<<*ptr<<endl;
}

Si lo anterior se cambia a const int *ptr, o a const int *const ptr, ¡ambos funcionan correctamente!

(3) Puntero constante a una constante

Entendidos los dos casos anteriores, este caso resulta bastante fácil de entender:

const int p = 3;
const int * const ptr = &p; 

ptr es un puntero const que apunta a un objeto const de tipo int.

6. Uso de const en funciones

const calificando el valor de retorno de una función

Esto tiene básicamente el mismo significado que const calificando variables y punteros ordinarios:

(1) const int

const int func1();

Esto en sí carece de sentido, ¡porque el valor devuelto ya se asigna a otra variable!

(2) const int*

const int* func2();

El contenido apuntado por el puntero es inmutable.

(3) int *const

int *const func2();

El puntero mismo es inmutable.

const calificando los parámetros de una función

(1) El parámetro recibido y el puntero mismo son inmutables dentro de la función, ¡carece de sentido!

void func(const int var); // 传递过来的参数不可变
void func(int *const var); // 指针本身不可变

Indica que el parámetro no puede modificarse en el cuerpo de la función, pero aquí no tiene ningún sentido: var es en sí un parámetro formal y no cambia dentro de la función. Lo mismo ocurre cuando el parámetro formal recibido es un puntero.

Cuando un parámetro de entrada usa «paso por valor», la función genera automáticamente una variable temporal para copiarlo, y ese parámetro de entrada no necesita protección, así que no le añadas const.

(2) El contenido apuntado por el parámetro puntero es una constante inmutable

void StringCopy(char *dst, const char *src);

Aquí src es un parámetro de entrada y dst un parámetro de salida. Al añadir const a src, si una sentencia del cuerpo de la función intenta cambiar el contenido de src, el compilador señalará el error. Este es uno de los efectos de añadir const.

(3) El parámetro es una referencia, para ganar eficiencia evitando modificaciones.

void func(const A &a)

Para parámetros de tipos de datos no internos, una función declarada como void func(A a) está condenada a ser poco eficiente, porque el cuerpo de la función creará un objeto temporal de tipo A para copiar el parámetro a, y la construcción, copia y destrucción de ese objeto temporal consumen tiempo.

Para mejorar la eficiencia, la función puede declararse como void func(A &a), porque el «paso por referencia» solo toma prestado el alias del parámetro y no necesita generar un objeto temporal.

Pero la función void func(A &a) tiene un inconveniente:

el «paso por referencia» puede modificar el parámetro a, y eso no es lo que deseamos. La solución es muy sencilla: añadir const, y la función termina siendo void func(const A &a).

Por analogía, ¿debería reescribirse void func(int x) como void func(const int &x) para mejorar la eficiencia? No hay ninguna necesidad: los parámetros de tipos de datos internos no implican construcción ni destrucción, y copiarlos es rapidísimo; la eficiencia del «paso por valor» y del «paso por referencia» es prácticamente la misma.

Resumen:
1. Para parámetros de entrada de tipos de datos no internos, conviene cambiar el «paso por valor» por el «paso por referencia constante», con el fin de mejorar la eficiencia. Por ejemplo, cambiar void func(A a) por void func(const A &a).

2. Para parámetros de entrada de tipos de datos internos, no cambies el «paso por valor» por el «paso por referencia constante». De lo contrario, ni se gana eficiencia ni se facilita la comprensión de la función. Por ejemplo, void func(int x) no debería cambiarse por void func(const int &x).

Con esto quedan resueltas dos preguntas de entrevista:

  • Si una función recibe un puntero, ¿necesita ese puntero const, y en qué se diferencia colocar const en distintas posiciones del puntero;
  • Si el parámetro que recibe la función es una instancia de un tipo complejo, ¿en qué se diferencian el paso por valor y el paso por referencia, y cuándo conviene añadir const al parámetro de referencia recibido.

7. Uso de const en clases

En una clase, toda función que no modifique miembros de datos debería declararse const. Si al escribir una función miembro const modificas por descuido un miembro de datos, o llamas a otra función miembro no const, el compilador señalará el error, lo que sin duda aumenta la robustez del programa.

Una función miembro especificada con la palabra clave const se llama función miembro constante. Solo las funciones miembro constantes están capacitadas para operar con constantes u objetos constantes; las funciones miembro no especificadas con const no pueden usarse para operar con objetos constantes.

Los miembros de datos const de una clase deben inicializarse mediante la lista de inicialización, como se muestra a continuación:

class Apple{
private:
    int people[100];
public:
    Apple(int i); 
    const int apple_number;
};

Apple::Apple(int i):apple_number(i)
{

}

Los objetos const solo pueden acceder a funciones miembro const, mientras que los objetos no const pueden acceder a cualquier función miembro, incluidas las funciones miembro const.

Por ejemplo:

// apple.cpp
class Apple
{
private:
    int people[100];
public:
    Apple(int i); 
    const int apple_number;
    void take(int num) const;
    int add();
    int add(int num) const;
    int getCount() const;

};
// apple.cpp
Apple::Apple(int i) : apple_number(i)
{
}
int Apple::add(int num)
{
    take(num);
    return 0;
}
int Apple::add(int num) const
{
    take(num);
    return 0;
}
void Apple::take(int num) const
{
    std::cout << "take func " << num << std::endl;
}
int Apple::getCount() const
{
    take(1);
    //    add(); // error
    return apple_number;
}
int main()
{
    Apple a(2);
    cout << a.getCount() << endl;
    a.add(10);
    const Apple b(3);
    b.add(100);
    return 0;
}
// main.cpp

Compilar: bazel run basic_content/const/class_const/first_example:main

En este punto se informa de un error: getCount() es una función miembro const y la resolución de sobrecarga dentro de ella solo encaja con la versión const (int add(int num) const); solo tras comentar esa versión const el error proviene de la llamada al add() no const. Es decir: los objetos const solo pueden acceder a funciones miembro const, y los objetos no const pueden acceder a cualquier función miembro (véase el issue #338).

Cuando la llamada se cambia a:

const Apple b(3);
b.add(); // error

Esto demuestra que los objetos const solo pueden acceder a funciones miembro const.

Además de inicializar las constantes const con la lista de inicialización como se indicó, también se pueden usar los métodos siguientes:

Primero: combinar la definición de la constante con static, es decir:

static const int apple_number

Segundo: inicializarla fuera:

const int Apple::apple_number=10;

Por supuesto, si compilas con C++11, puedes inicializar directamente en la definición y escribir directamente:

static const int apple_number=10;
// 或者
const int apple_number=10;

¡Ambas formas están soportadas en C++11!

¡Basta con añadir -std=c++11 al compilar!

Ha salido static, así que comentémoslo brevemente:

En C++, las variables miembro estáticas static no pueden inicializarse dentro de la clase. Dentro de la clase solo hay una declaración; la definición debe estar fuera del cuerpo de la definición de la clase, normalmente se inicializa en el archivo de implementación de la clase.

Declaración en la clase:

static int ap;

Uso en el archivo de implementación de la clase:

int Apple::ap=666

En cuanto a este punto, C++11 no permite declarar e inicializar a la vez; es decir, el uso es el descrito arriba.

Preguntas frecuentes

¿Puntero a const o puntero const?

const int* p es un puntero a una constante: no puedes modificar el valor señalado. int* const p es un puntero constante: no puedes reasignar el puntero. Regla: const a la izquierda de * protege el valor; a la derecha, el puntero.

¿Puede un objeto const llamar a una función miembro no const?

No. Un objeto const solo puede llamar a funciones miembro const; un objeto no const puede llamar a ambas.

¿const o #define para constantes?

Prefiere const: tiene comprobación de tipos, respeta el ámbito y es depurable. #define es una sustitución de texto sin tipos.

Temas relacionados

Comentarios