C++ Membros static

Até agora, tudo o que você pôs numa classe pertencia a cada objeto: cada produto tem o seu nome, o seu preço, o seu estoque. Mas algumas informações não são de nenhum produto em particular, e sim da classe inteira: "quantos produtos já foram cadastrados?", "qual é o próximo código livre?", "qual é o estoque máximo da loja?". Para isso existem os membros static ("estático"): atributos e métodos que pertencem à classe, existem uma vez só e são compartilhados por todos os objetos.

Você vai aprender
  • Por que um atributo comum não serve para contar objetos.
  • Como declarar um atributo static e defini-lo fora da classe (o jeito clássico).
  • O erro de ligação referência não definida (undefined reference) quando falta a definição.
  • O atalho do C++17: inline static int criados = 0; dentro da classe.
  • Métodos static, que funcionam sem objeto nenhum, e por que eles não têm this.
  • Como acessar tudo isso com Classe::nome, e as constantes static constexpr.

Os programas desta aula foram compilados e executados na máquina fictícia da Ana, na pasta ~/cpp/static, com o g++ 15.2 e o comando padrão g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: o carimbo numerador da loja

Cada ficha de produto da Ana tem os seus campos. Mas, no balcão, há uma coisa que não é de ficha nenhuma: o carimbo numerador, aquele que carimba 1, depois 2, depois 3… Toda ficha nova passa por ele e ganha o próximo número. Existe um carimbo para a loja toda; se cada ficha tivesse o seu próprio carimbo, todas ganhariam o número 1.

Um atributo static é o carimbo: uma variável só, que pertence à classe (o molde) e não a cada objeto. Todos os objetos enxergam e mexem na mesma.


1. O problema: contar com um atributo comum

A Ana quer saber quantos produtos foram criados. A primeira ideia é um atributo que o construtor aumenta:

#include <iostream>
#include <string>

class Produto {
public:
    std::string nome;
    int criados = 0;

    Produto(std::string n) : nome(n) {
        criados++;
    }
};

int main() {
    Produto a{"caneta"};
    Produto b{"caderno"};
    Produto c{"régua"};
    std::cout << "a.criados = " << a.criados << "\n";
    std::cout << "c.criados = " << c.criados << "\n";
}
$ g++ -std=c++23 -Wall -Wextra errado.cpp -o errado
$ ./errado
a.criados = 1
c.criados = 1          « deveria ser 3

Três produtos criados, e o contador diz 1. É o carimbo individual: cada objeto nasce com o seu próprio criados valendo 0, e o construtor aumenta o dele para 1. Os três contadores nunca se encontram.


2. O atributo static (jeito clássico)

Com a palavra static, o atributo deixa de ser de cada objeto e passa a ser da classe. No jeito clássico (que vale em qualquer versão do C++ e que você vai encontrar em muito código), ele é escrito em duas partes:

#include <iostream>
#include <string>

class Produto {
public:
    std::string nome;
    static int criados;          // declaração: existe um só, da classe

    Produto(std::string n) : nome(n) {
        criados++;
    }
};

int Produto::criados = 0;        // definição: aqui ele nasce, com 0

int main() {
    std::cout << "Antes: " << Produto::criados << "\n";
    Produto a{"caneta"};
    Produto b{"caderno"};
    Produto c{"régua"};
    std::cout << "Depois: " << Produto::criados << "\n";
    std::cout << "Pelo objeto a: " << a.criados << "\n";
}
$ g++ -std=c++23 -Wall -Wextra classico.cpp -o classico
$ ./classico
Antes: 0
Depois: 3
Pelo objeto a: 3
static int criados;
A declaração, dentro da classe: "a classe Produto tem um contador criados, um só, compartilhado". Ela só anuncia; ainda não reserva memória.
int Produto::criados = 0;
A definição, fora da classe, uma única vez no programa: aqui a variável realmente nasce, com o valor 0. O Produto:: diz de quem ela é (o mesmo :: de void Produto::mostrar(), aula Métodos). Repare: aqui não se repete a palavra static.
criados++;
Dentro do construtor, criados é o contador da classe. Cada produto novo aumenta o mesmo contador: 1, 2, 3.
Produto::criados
Fora da classe, o jeito certo de acessar: pelo nome da classe, com ::. Funciona até antes de existir qualquer produto ("Antes: 0"), porque o contador é da classe, não de um objeto.
a.criados
Também funciona pelo objeto, e dá 3, o mesmo valor. Mas engana quem lê, que pode achar que é "o criados da caneta". Prefira Produto::criados.
Atributo comum (int estoque;)Atributo static (static int criados;)
Quantas cópias existemUma por objetoUma só, para a classe inteira
Quando nasceCom o objetoAntes do main começar
Quando morreCom o objetoQuando o programa termina
Como acessar de foracaneta.estoqueProduto::criados
Precisa de um objeto?SimNão
Por que essas duas partes? A classe costuma ficar num arquivo .h, incluído por vários .cpp (aula Arquivos .h e .cpp). Se a variável nascesse dentro da classe, ela nasceria uma vez em cada .cpp que incluísse o .h: vários contadores, de novo. Por isso, no C++ clássico, a classe só declara, e a definição fica num único .cpp. A seção 4 mostra a saída moderna para isso.

3. Esquecer a definição: o erro do ligador

O erro mais comum com static é escrever a declaração na classe e esquecer a linha de fora. Para deixar a mensagem curta, este exemplo usa um construtor sem parâmetros:

#include <iostream>

class Produto {
public:
    static int criados;

    Produto() {
        criados++;
    }
};

int main() {
    Produto a;
    Produto b;
    std::cout << Produto::criados << "\n";
}
$ g++ -std=c++23 -Wall -Wextra semdef2.cpp -o semdef2
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccZjSjzl.o: aviso: relocalização contra "_ZN7Produto7criadosE" em secção só de leitura ".text._ZN7ProdutoC2Ev[_ZN7ProdutoC5Ev]"
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccZjSjzl.o: na função "main":
semdef2.cpp:(.text+0x35): referência não definida para "Produto::criados"
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccZjSjzl.o: na função "Produto::Produto()":
semdef2.cpp:(.text._ZN7ProdutoC2Ev[_ZN7ProdutoC5Ev]+0xe): referência não definida para "Produto::criados"
/usr/bin/x86_64-linux-gnu-ld.bfd: semdef2.cpp:(.text._ZN7ProdutoC2Ev[_ZN7ProdutoC5Ev]+0x17): referência não definida para "Produto::criados"
/usr/bin/x86_64-linux-gnu-ld.bfd: aviso: a criar DT_TEXTREL num PIE.
collect2: error: ld returned 1 exit status

Assusta, mas a leitura é simples. Primeiro, quem reclamou: não foi o compilador (nenhuma linha diz error: com um número de linha do seu código), foi o ligador, o ld, a etapa final que junta os pedaços do programa (aula Como o C++ Vira Programa). O compilador aceitou o código porque a classe prometeu que Produto::criados existe; o ligador foi procurar onde ele nasceu e não achou.

LinhaO que quer dizer
referência não definida para "Produto::criados"A linha que importa. "O código usa Produto::criados, mas ninguém o definiu." Aparece uma vez para cada lugar que usa o contador: no main e duas no construtor (ler e gravar o criados++).
na função "main" / na função "Produto::Produto()"Onde o uso foi encontrado.
/tmp/ccZjSjzl.o, .text+0x35Arquivo temporário e posição dentro do código de máquina. O nome muda a cada compilação; ignore.
_ZN7Produto7criadosEO nome "codificado" (mangled) de Produto::criados, como o ligador o enxerga.
As linhas de aviso: (relocalização, DT_TEXTREL)Efeitos colaterais da referência que faltou. Somem quando você conserta o erro.
collect2: error: ld returned 1 exit statusO g++ avisando que o ligador falhou, e por isso o programa não foi gerado.

Essa mensagem saiu em português porque o ld tem tradução no Ubuntu (as mensagens do próprio g++ continuam em inglês). Num sistema em inglês, a linha principal é a famosa undefined reference to `Produto::criados', que você vai encontrar em buscas na internet. No editor do guia, quem liga o programa é outro ligador (o do WebAssembly), e a mensagem fala em undefined symbol ("símbolo indefinido"); a causa é a mesma.

Como resolver: escreva a definição, int Produto::criados = 0;, fora da classe, uma vez. Ou use o atalho da próxima seção.


4. O atalho do C++17: inline static

A primeira tentativa de quem aprende é inicializar o static na própria classe, como um atributo comum. Sem mais nada, o C++ recusa:

class Produto {
public:
    std::string nome;
    static int criados = 0;
    …
};
$ g++ -std=c++23 -Wall -Wextra dentro.cpp -o dentro
dentro.cpp:7:16: error: ISO C++ forbids in-class initialization of non-const static member ‘Produto::criados’
    7 |     static int criados = 0;
      |                ^~~~~~~

"O C++ padrão (ISO) proíbe inicializar dentro da classe um membro static que não é const." É a regra das duas partes da seção 2. Desde o C++17, porém, basta acrescentar inline:

#include <iostream>
#include <string>

class Produto {
public:
    std::string nome;
    inline static int criados = 0;

    Produto(std::string n) : nome(n) {
        criados++;
    }
};

int main() {
    Produto a{"caneta"};
    Produto b{"caderno"};
    std::cout << "Produtos criados: " << Produto::criados << "\n";
}
$ g++ -std=c++23 -Wall -Wextra inline.cpp -o inline
$ ./inline
Produtos criados: 2
inline static int criados = 0;
Declaração e definição numa linha só, dentro da classe. O inline diz ao ligador: "esta variável pode aparecer em vários arquivos .cpp (porque a classe está num .h incluído por eles), mas é uma só: junte todas as aparições numa". Acabaram a linha de fora e o risco de esquecê-la.
inline static ou static inline
A ordem das duas palavras não importa; as duas formas aparecem por aí.

O inline em variáveis é do C++17. Pedindo um padrão mais antigo ao g++, ele ainda compila, mas avisa:

$ g++ -std=c++14 -Wall -Wextra inline.cpp -o inline14
inline.cpp:7:5: warning: inline variables are only available with ‘-std=c++17’ or ‘-std=gnu++17’ [-Wc++17-extensions]
    7 |     inline static int criados = 0;
      |     ^~~~~~
$ ./inline14
Produtos criados: 2

Com o comando padrão deste guia (-std=c++23), não há aviso. Como o g++ 15 usa C++17 quando você não pede outro padrão, o inline static funciona até sem o -std.

Qual usar? Em código novo, inline static: uma linha, dentro da classe, sem chance de esquecer a definição. O jeito clássico (declaração + int Produto::criados = 0; num .cpp) continua certo e é o que você vai ler em projetos antigos, ou em projetos presos a C++11/14.

Missão: o contador abaixo mostra 1. Transforme criados num atributo da classe com inline static e mostre-o pelo nome da classe (Produto::criados). A saída deve ser Produtos criados: 3.

#include <iostream>
#include <string>

class Produto {
public:
    std::string nome;
    int criados = 0;

    Produto(std::string n) : nome(n) {
        criados++;
    }
};

int main() {
    Produto a{"caneta"};
    Produto b{"caderno"};
    Produto c{"régua"};
    std::cout << "Produtos criados: " << c.criados << "\n";
}
#include <iostream>
#include <string>

class Produto {
public:
    std::string nome;
    inline static int criados = 0;

    Produto(std::string n) : nome(n) {
        criados++;
    }
};

int main() {
    Produto a{"caneta"};
    Produto b{"caderno"};
    Produto c{"régua"};
    std::cout << "Produtos criados: " << Produto::criados << "\n";
}

5. Métodos static

Um contador público pode ser mexido por qualquer um (Produto::criados = 500;), o mesmo problema da aula public e private. A solução é a de sempre: deixá-lo private e oferecer um método para ler. Mas um método comum precisa de um objeto para ser chamado, e "quantos produtos existem?" é uma pergunta à classe, não a um produto. Para isso existem os métodos static.

O exemplo abaixo também usa o contador para dar a cada produto um código automático, o carimbo numerador da ideia inicial:

#include <iostream>
#include <string>

class Produto {
public:
    Produto(std::string nome) : nome_(nome), codigo_(proximoCodigo_++) {
    }

    void mostrar() const {
        std::cout << "#" << codigo_ << " " << nome_ << "\n";
    }

    static int quantosCriados() {
        return proximoCodigo_ - 1;
    }

private:
    std::string nome_;
    int codigo_;
    inline static int proximoCodigo_ = 1;
};

int main() {
    std::cout << "Criados: " << Produto::quantosCriados() << "\n";

    Produto caneta{"caneta"};
    Produto caderno{"caderno"};
    Produto regua{"régua"};

    caneta.mostrar();
    caderno.mostrar();
    regua.mostrar();
    std::cout << "Criados: " << Produto::quantosCriados() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra metodo.cpp -o metodo
$ ./metodo
Criados: 0
#1 caneta
#2 caderno
#3 régua
Criados: 3
inline static int proximoCodigo_ = 1;
O carimbo: privado, da classe, começa em 1. Ninguém de fora consegue mexer nele.
codigo_(proximoCodigo_++)
Na lista de inicialização, cada produto novo pega o número atual do carimbo para o seu codigo_ (um atributo comum, um por objeto), e o ++ deixa o carimbo pronto para o próximo (aula ++ e --: o pós-incremento entrega o valor antigo e depois soma).
static int quantosCriados()
Um método da classe. Ele pode usar os membros static (proximoCodigo_), mesmo os privados.
Produto::quantosCriados()
Chamado pelo nome da classe, sem objeto nenhum: funciona até antes de existir o primeiro produto ("Criados: 0").

Método static não tem this

Como ninguém chama um método static "sobre" um objeto, ele não recebe o parâmetro escondido this (aula this). Consequência: ele não enxerga os atributos comuns. Não há "o nome" sem um produto.

#include <iostream>
#include <string>

class Produto {
public:
    std::string nome;
    inline static int criados = 0;

    static void mostrar() {
        std::cout << nome << "\n";
    }

    static void mostrar2() {
        std::cout << this->nome << "\n";
    }
};

int main() {
    Produto::mostrar();
}
$ g++ -std=c++23 -Wall -Wextra staticnome.cpp -o staticnome
staticnome.cpp: In static member function ‘static void Produto::mostrar()’:
staticnome.cpp:10:22: error: invalid use of member ‘Produto::nome’ in static member function
   10 |         std::cout << nome << "\n";
      |                      ^~~~
staticnome.cpp:6:17: note: declared here
    6 |     std::string nome;
      |                 ^~~~
staticnome.cpp: In static member function ‘static void Produto::mostrar2()’:
staticnome.cpp:14:22: error: ‘this’ is unavailable for static member functions
   14 |         std::cout << this->nome << "\n";
      |                      ^~~~

Dois erros, a mesma causa. "Uso inválido do membro Produto::nome em função-membro estática": o nome de qual produto? E "this não está disponível em funções-membro estáticas". Se o método precisa de um produto, ou ele não deveria ser static, ou deve receber o produto por parâmetro (static void mostrar(const Produto& p)).

Dentro de…pode usar atributos comuns?pode usar atributos static?tem this?
Método comumSimSimSim
Método staticNão (só de um objeto recebido por parâmetro)SimNão

E o contrário funciona: um método comum (como o construtor acima) pode usar membros static à vontade. Um método static também não pode ser const (aula Métodos const): o const promete não mudar o objeto, e ele não tem objeto.


6. Constantes da classe: static constexpr

Outro uso muito comum: uma constante que vale para a classe toda, como o estoque máximo que cabe na prateleira. Constantes podem ser inicializadas dentro da classe desde sempre, e o jeito moderno é static constexpr (aula Constantes):

class Produto {
public:
    static constexpr int estoqueMaximo = 100;

    Produto(std::string nome) : nome_(nome) {
    }

    bool repor(int quantidade) {
        if (estoque_ + quantidade > estoqueMaximo) {
            std::cout << "Não cabe: máximo " << estoqueMaximo << "\n";
            return false;
        }
        estoque_ += quantidade;
        return true;
    }

    int estoque() const { return estoque_; }

private:
    std::string nome_;
    int estoque_ = 0;
};

int main() {
    Produto caneta{"caneta"};
    caneta.repor(80);
    caneta.repor(30);
    std::cout << "Estoque: " << caneta.estoque() << " de " << Produto::estoqueMaximo << "\n";
}
$ g++ -std=c++23 -Wall -Wextra constante.cpp -o constante
$ ./constante
Não cabe: máximo 100
Estoque: 80 de 100

static porque o máximo é o mesmo para todo produto (não faz sentido guardar 100 dentro de cada objeto); constexpr porque nunca muda e é conhecido na compilação. Pode ser pública sem medo, já que ninguém consegue mudá-la. Membros constexpr static já são inline automaticamente, por isso não precisam da linha de fora.


Formas diferentes: qual usar

SituaçãoForma
Contador, próximo código, total da loja (muda)inline static int x = 0;, de preferência private, com um método static para ler.
Mesmo caso, em código C++11/14static int x; na classe + int Classe::x = 0; num .cpp.
Constante da classe (não muda)static constexpr int maximo = 100;
Operação que não precisa de objetoMétodo static, chamado com Classe::metodo().
Acessar um membro staticClasse::membro (funciona por um objeto, mas engana quem lê).
Contar objetos vivos é outra história. O contador desta aula conta quantos produtos foram criados. Para saber quantos ainda existem, é preciso diminuir o contador quando um produto morre, e isso se faz no destrutor, assunto da próxima aula, Destrutores e RAII. (E cópias de objetos passam por outro construtor, que você verá em Cópia e Movimento.)

Quando dá errado

Declarou o static e esqueceu a definição

É o erro da seção 3: referência não definida para "Produto::criados" (em inglês, undefined reference to `Produto::criados'), vindo do ld, terminando com collect2: error: ld returned 1 exit status. Como resolver: acrescente int Produto::criados = 0; fora da classe, ou troque a declaração por inline static int criados = 0;. Num projeto com vários arquivos, confira também se o .cpp que tem a definição entrou no comando de compilação (aula Compilar Vários Arquivos).

Inicializou dentro da classe sem inline

É o erro da seção 4: ISO C++ forbids in-class initialization of non-const static member ‘Produto::criados’. No Clang, do editor do guia: non-const static data member must be initialized out of line ("membro estático não-const precisa ser inicializado fora da classe"). Como resolver: acrescente inline (inline static int criados = 0;) ou tire o = 0 e escreva a definição fora.

Repetir static na definição de fora
#include <iostream>

class Produto {
public:
    static int criados;
};

static int Produto::criados = 0;

int main() {
    std::cout << Produto::criados << "\n";
}
$ g++ -std=c++23 -Wall -Wextra repetirstatic.cpp -o repetirstatic
repetirstatic.cpp:8:1: error: ‘static’ may not be used when defining (as opposed to declaring) a static data member [-fpermissive]
    8 | static int Produto::criados = 0;
      | ^~~~~~

O que aconteceu: "static não pode ser usado ao definir (em oposição a declarar) um membro de dados estático". A palavra static vai só na declaração, dentro da classe; fora, ela teria outro significado (que você vai ver em Compilar Vários Arquivos). O mesmo vale para métodos static definidos fora da classe. No Clang: 'static' can only be specified inside the class definition.

Como resolver: int Produto::criados = 0;, sem static.

Método static usando um atributo comum ou this

São os erros da seção 5: invalid use of member ‘Produto::nome’ in static member function e ‘this’ is unavailable for static member functions. Como resolver: se o método trabalha sobre um produto, tire o static (vira um método comum); se ele é mesmo da classe, passe o produto por parâmetro ou use só membros static.


Referência rápida

EscritaO que faz
static int criados; (na classe)Declara um atributo da classe, compartilhado por todos os objetos.
int Produto::criados = 0; (fora, uma vez)Define o atributo static (jeito clássico). Sem static aqui.
inline static int criados = 0;Declara e define dentro da classe (C++17).
static constexpr int maximo = 100;Constante da classe.
static int quantos() { … }Método da classe: sem objeto, sem this, só enxerga membros static.
Produto::criados, Produto::quantos()Acesso pelo nome da classe (forma recomendada).
referência não definida / undefined referenceErro do ligador: faltou a definição de fora.

Exercícios

1. Da classe, não do objeto

Complete a declaração para que o contador seja um só, dentro da classe, com valor inicial 0 (C++17):

int total = 0;

2. A definição clássica

A classe Cliente declara static int total;. Complete a definição de fora da classe:

int = 0;

3. Chamar sem objeto

Complete para chamar o método static quantosCriados da classe Produto:

int n =

4. Três objetos Produto existem e a classe tem inline static int criados. Quantas variáveis criados existem na memória?

5. A classe declara static int criados;, sem inline e sem definição fora. Quem reclama, e com que mensagem?

6. Por que um método static não pode usar o atributo comum nome?

Missão final: carteirinhas numeradas

Missão: cada cliente da lojinha ganha uma carteirinha com número automático. Complete a classe Cliente: um contador inline static int proximo_ = 1; privado; no construtor, o id_ recebe proximo_++; e um método static int total() que devolve quantos clientes foram cadastrados. A saída deve ser:

#1 Bia
#2 Caio
#3 Duda
Clientes cadastrados: 3
#include <iostream>
#include <string>

class Cliente {
public:
    Cliente(std::string nome) : nome_(nome), id_(0) {
    }

    void mostrar() const {
        std::cout << "#" << id_ << " " << nome_ << "\n";
    }

    // escreva o método static total() aqui

private:
    std::string nome_;
    int id_;
    // e o contador aqui
};

int main() {
    Cliente bia{"Bia"};
    Cliente caio{"Caio"};
    Cliente duda{"Duda"};
    bia.mostrar();
    caio.mostrar();
    duda.mostrar();
    std::cout << "Clientes cadastrados: " << Cliente::total() << "\n";
}
#include <iostream>
#include <string>

class Cliente {
public:
    Cliente(std::string nome) : nome_(nome), id_(proximo_++) {
    }

    void mostrar() const {
        std::cout << "#" << id_ << " " << nome_ << "\n";
    }

    static int total() {
        return proximo_ - 1;
    }

private:
    std::string nome_;
    int id_;
    inline static int proximo_ = 1;
};

int main() {
    Cliente bia{"Bia"};
    Cliente caio{"Caio"};
    Cliente duda{"Duda"};
    bia.mostrar();
    caio.mostrar();
    duda.mostrar();
    std::cout << "Clientes cadastrados: " << Cliente::total() << "\n";
}

Resumo

  • Um membro static pertence à classe, não a cada objeto: existe uma vez só e é compartilhado. Serve para contadores, códigos automáticos e constantes da classe.
  • Jeito clássico: static int criados; na classe + int Produto::criados = 0; fora, uma vez (sem repetir static). Esquecer a de fora dá erro do ligador: referência não definida / undefined reference.
  • Desde o C++17: inline static int criados = 0;, tudo dentro da classe. Sem o inline, ISO C++ forbids in-class initialization of non-const static member.
  • Acesse com Classe::membro; funciona sem nenhum objeto criado.
  • Métodos static são chamados com Classe::metodo(), não têm this e só enxergam membros static.
  • Constantes da classe: static constexpr int maximo = 100;.
  • Próxima aula: o que acontece quando um objeto morre, em Destrutores e RAII.