Ir para o conteúdo

Última atualização:2026-10-13

const em C++: constantes e os dois tipos de ponteiro const

Sobre o autor

Conta pública no WeChat:

const em C++ explicado: constantes, ponteiro const vs ponteiro para const | C++那些事

1. O significado de const

Um tipo const é um tipo declarado com o qualificador de tipo const; o valor de uma variável ou objeto de tipo const não pode ser atualizado.

2. O papel de const

  • Pode definir constantes
const int a=100;
  • Verificação de tipos

    • A diferença entre constantes const e constantes definidas com a macro #define:

      Uma constante const tem um tipo, e o compilador pode fazer verificação de segurança; a definição de macro #define não tem tipo de dados, é uma simples substituição de strings e não permite verificação de segurança.Agradecemos aos dois especialistas que apontaram o problema aqui; veja: issue

    • Constantes const suportam todos os tipos.

    • Nos demais casos, é apenas uma variável qualificada com const; não a confunda com uma constante.
  • Evitar modificação, exercer papel de proteção e aumentar a robustez do programa

void f(const int i){
    i++; // error!
}
  • Economizar espaço e evitar alocações de memória desnecessárias

    • Um const inicializado com um tipo inteiro/enum e uma expressão constante (ou seja, uma expressão constante) comporta-se, em uso, bem mais como uma constante de tempo de compilação: se você nunca obtém o endereço dele e usa apenas o valor, ele normalmente é otimizado para um imediato, tal como #define; só ao obter o endereço ou fazer um odr-use entre unidades de tradução ele vai para a memória, e existe apenas uma cópia em todo o programa.
    • Depois que #define se expande para um literal, ele é compilado nas instruções como um imediato; as próprias instruções também ocupam memória, então vários usos significam várias ocorrências (veja a discussão no issue #5).
    • Nota: nem toda variável const é uma expressão constante — só o são as de tipo inteiro/enum inicializadas com uma expressão constante; as demais variáveis const são apenas variáveis qualificadas com const (por exemplo, ponto flutuante, tipos definidos pelo usuário); constexpr é a forma correta de expressar constantes de tempo de compilação.

3. Objetos const são locais ao arquivo por padrão

Nota: variáveis não const são extern por padrão. Para que uma variável const possa ser acessada em outros arquivos, é preciso especificá-la explicitamente como extern no arquivo.

Acesso a uma variável não modificada com const em arquivos diferentes

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

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

Acesso a uma constante const em arquivos diferentes

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

Resumo:
Percebe-se que variáveis não modificadas com const não precisam de declaração extern explícita! Já a constante const exige declaração extern explícita e, além disso, precisa ser inicializada! Como uma constante não pode ser modificada depois de definida, ela deve ser inicializada no momento da definição.

4. Definindo 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’

O código acima contém dois erros: + b é uma constante, não pode ser alterada! + i é uma constante, deve ser inicializada! (Porque uma constante não pode ser modificada depois de definida, ela deve ser inicializada no momento da definição.)

5. Ponteiros e const

Há quatro formas de const relacionadas a ponteiros:

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

Resumo:
Se const está à esquerda de *, então const serve para qualificar a variável apontada pelo ponteiro, ou seja, o apontado é uma constante;
se const está à direita de *, const qualifica o próprio ponteiro, ou seja, o ponteiro em si é uma constante.

Outra forma de interpretar
Leia o inglês da direita para a esquerda, tomando "to" como divisor: o que vem antes de "to" descreve a característica do ponteiro, e o que vem depois descreve a característica do alvo

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
Quando o ponteiro recebe a característica const, ele não pode mais mudar o endereço que aponta
Quando a característica do alvo é char, o conteúdo pode ser modificado através do ponteiro, por exemplo: *char='y';
Quando a característica do alvo é const char, o conteúdo não pode ser modificado através do ponteiro

O uso concreto é o seguinte:

(1) Ponteiro para constante

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

ptr é um ponteiro para um objeto const do tipo int; o const define o tipo int, ou seja, o tipo do objeto que ptr aponta, e não o ptr em si, por isso ptr não precisa de valor inicial. Mas não se pode modificar o valor do objeto apontado através de ptr.

Além disso, também não se pode usar um ponteiro void* para guardar o endereço de um objeto const; é preciso usar um ponteiro do tipo const void* para guardar o endereço de um objeto const.

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

Outro ponto importante: é permitido atribuir o endereço de um objeto não const a um ponteiro para objeto const.

Atribuindo o endereço de um objeto não const a um ponteiro para objeto const:

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

Não podemos modificar o valor de val através do ponteiro ptr, mesmo que ele aponte para um objeto não const!

Não podemos usar um ponteiro para objeto const para modificar o objeto subjacente; contudo, se o ponteiro aponta para um objeto não const, o objeto apontado pode ser modificado por outros meios. O valor apontado por um ponteiro const pode ser alterado — só não através do ponteiro para objeto const! Modifique assim:

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

Resumo:
1. Para um ponteiro para constante, não se pode modificar o valor do objeto através do ponteiro.
2. Não se pode usar um ponteiro void* para guardar o endereço de um objeto const; é preciso usar um ponteiro do tipo const void* para guardar o endereço de um objeto const.
3. É permitido atribuir o endereço de um objeto não const a um ponteiro para objeto const; para modificar o valor do objeto apontado, é preciso fazê-lo por outros meios, e não diretamente através do ponteiro atual.

(2) Ponteiro constante

Um ponteiro const deve ser inicializado, e o valor apontado por um ponteiro const pode ser modificado, mas o seu apontamento não.

#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;
}

O código gera um erro de compilação: um ponteiro const não pode mudar o que aponta.

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

Nada acontece com este código; ele imprime 1 normalmente.

Por fim, quando o endereço de uma constante const é atribuído a ptr, como ptr aponta para uma variável e não para uma constante const, ocorre um erro: o erro 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;
}

Se o acima for alterado para const int *ptr, ou para const int *const ptr, ambos funcionam normalmente!

(3) Ponteiro constante para constante

Entendidos os dois casos anteriores, este caso fica bem fácil de entender:

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

ptr é um ponteiro const que aponta para um objeto const do tipo int.

6. Uso de const em funções

const qualificando o valor de retorno de uma função

Isso tem basicamente o mesmo significado de const qualificando variáveis e ponteiros comuns:

(1) const int

const int func1();

Isso em si não tem sentido, porque o valor retornado já é atribuído a outra variável!

(2) const int*

const int* func2();

O conteúdo apontado pelo ponteiro é imutável.

(3) int *const

int *const func2();

O ponteiro em si é imutável.

const qualificando parâmetros de função

(1) O parâmetro recebido e o ponteiro em si são imutáveis dentro da função — sem sentido!

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

Indica que o parâmetro não pode ser modificado no corpo da função, mas aqui isso não tem nenhum significado: var é em si um parâmetro formal e não muda dentro da função. O mesmo vale quando o parâmetro formal recebido é um ponteiro.

Quando um parâmetro de entrada usa "passagem por valor", a função gera automaticamente uma variável temporária para copiá-lo, e esse parâmetro de entrada, de origem, não precisa de proteção; portanto não adicione const.

(2) O conteúdo apontado pelo parâmetro ponteiro é uma constante imutável

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

Aqui src é um parâmetro de entrada e dst é um parâmetro de saída. Ao adicionar const a src, se alguma instrução do corpo da função tentar alterar o conteúdo de src, o compilador apontará o erro. Este é um dos efeitos de adicionar const.

(3) O parâmetro é uma referência, para ganhar eficiência e ao mesmo tempo evitar modificação.

void func(const A &a)

Para parâmetros de tipos de dados não internos, uma função declarada como void func(A a) está fadada a ser ineficiente, porque o corpo da função criará um objeto temporário do tipo A para copiar o parâmetro a, e a construção, cópia e destruição do objeto temporário consomem tempo.

Para melhorar a eficiência, a declaração da função pode ser alterada para void func(A &a), pois a "passagem por referência" apenas toma emprestado o alias do parâmetro e não precisa gerar um objeto temporário.

Mas a função void func(A &a) tem uma desvantagem:

a "passagem por referência" pode alterar o parâmetro a, o que não desejamos. Resolver o problema é fácil: basta adicionar const, e a função finalmente se torna void func(const A &a).

Por analogia, deveria void func(int x) ser reescrita como void func(const int &x) para melhorar a eficiência? Não há a menor necessidade: parâmetros de tipos de dados internos não têm processo de construção/destruição, e a cópia é muito rápida — a eficiência da "passagem por valor" e da "passagem por referência" é praticamente a mesma.

Resumo:
1. Para parâmetros de entrada de tipos de dados não internos, deve-se trocar a "passagem por valor" por "passagem por referência const", com o objetivo de melhorar a eficiência. Por exemplo, trocar void func(A a) por void func(const A &a).

2. Para parâmetros de entrada de tipos de dados internos, não troque a "passagem por valor" por "passagem por referência const". Caso contrário, você não ganha eficiência e ainda reduz a compreensibilidade da função. Por exemplo, void func(int x) não deveria ser alterada para void func(const int &x).

Com isso ficam resolvidas duas perguntas de entrevista:

  • Se a função recebe um ponteiro, esse ponteiro precisa de const, e qual a diferença entre colocar const em posições diferentes do ponteiro;
  • Se o parâmetro que a função recebe é uma instância de um tipo complexo, qual a diferença entre passar parâmetro por valor ou por referência, e quando é preciso adicionar const ao parâmetro de referência recebido.

7. Uso de const em classes

Em uma classe, qualquer função que não modifique membros de dados deveria ser declarada como const. Se, ao escrever uma função-membro const, você modificar por descuido um membro de dados, ou chamar outra função-membro não const, o compilador apontará o erro, o que sem dúvida aumenta a robustez do programa.

Uma função-membro especificada com a palavra-chave const é chamada de função-membro constante. Somente funções-membro constantes têm qualificação para operar constantes ou objetos constantes; funções-membro não especificadas com a palavra-chave const não podem ser usadas para operar objetos constantes.

As variáveis-membro const de uma classe devem ser inicializadas através da lista de inicialização, como mostrado abaixo:

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

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

}

Objetos const só podem acessar funções-membro const, enquanto objetos não const podem acessar qualquer função-membro, incluindo funções-membro const.

Por exemplo:

// 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

Neste momento ocorre um erro: getCount() é uma função-membro const, e a resolução de sobrecarga do add() dentro dela só corresponde à versão const (int add(int num) const); somente depois de comentar essa versão const é que o erro decorreria da chamada ao add() não const. Ou seja: objetos const só podem acessar funções-membro const, e objetos não const podem acessar qualquer função-membro (veja a issue #338).

Quando a chamada é alterada para:

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

Isso prova que objetos const só podem acessar funções-membro const.

Além de inicializar constantes const com a lista de inicialização como acima, também podemos usar os métodos a seguir:

Primeiro: combinar a definição da constante com static, ou seja:

static const int apple_number

Segundo: inicializar fora:

const int Apple::apple_number=10;

É claro que, se você compilar com C++11, pode inicializar direto na definição e escrever simplesmente:

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

Ambas as formas são suportadas no C++11!

Basta adicionar -std=c++11 ao compilar!

Aqui mencionamos static; vamos falar brevemente sobre isso:

Em C++, variáveis-membro estáticas static não podem ser inicializadas dentro da classe. Dentro da classe há apenas a declaração; a definição deve ficar fora do corpo da definição da classe, normalmente a inicialização é feita no arquivo de implementação da classe.

Declarada na classe:

static int ap;

Usada no arquivo de implementação da classe:

int Apple::ap=666

Quanto a este item, o C++11 não permite declarar e inicializar ao mesmo tempo — ou seja, vale o uso descrito acima.

Perguntas frequentes

Ponteiro para const ou ponteiro const?

const int* p é um ponteiro para constante: o valor apontado não muda. int* const p é um ponteiro constante: o ponteiro não é reatribuído. Regra: const à esquerda de * protege o valor; à direita, o ponteiro.

Um objeto const pode chamar uma função-membro não const?

Não. Objetos const só chamam funções-membro const; objetos não const chamam ambas.

const ou #define para constantes?

Prefira const: tem verificação de tipo, respeita escopo e é depurável. #define é substituição de texto sem tipo.

Tópicos relacionados

Comentários