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.
- Por que um atributo comum não serve para contar objetos.
- Como declarar um atributo
statice 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êmthis. - Como acessar tudo isso com
Classe::nome, e as constantesstatic 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
Produtotem um contadorcriados, 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::devoid Produto::mostrar(), aula Métodos). Repare: aqui não se repete a palavrastatic. - 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
criadosda caneta". PrefiraProduto::criados.
Atributo comum (int estoque;) | Atributo static (static int criados;) | |
|---|---|---|
| Quantas cópias existem | Uma por objeto | Uma só, para a classe inteira |
| Quando nasce | Com o objeto | Antes do main começar |
| Quando morre | Com o objeto | Quando o programa termina |
| Como acessar de fora | caneta.estoque | Produto::criados |
| Precisa de um objeto? | Sim | Não |
.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.
| Linha | O 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+0x35 | Arquivo temporário e posição dentro do código de máquina. O nome muda a cada compilação; ignore. |
_ZN7Produto7criadosE | O 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 status | O 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
inlinediz ao ligador: "esta variável pode aparecer em vários arquivos.cpp(porque a classe está num.hincluí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.
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 comum | Sim | Sim | Sim |
Método static | Não (só de um objeto recebido por parâmetro) | Sim | Nã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ção | Forma |
|---|---|
| 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/14 | static 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 objeto | Método static, chamado com Classe::metodo(). |
Acessar um membro static | Classe::membro (funciona por um objeto, mas engana quem lê). |
Quando dá errado
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).
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.
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.
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
| Escrita | O 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 reference | Erro 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):
2. A definição clássica
A classe Cliente declara static int total;. Complete a definição de fora da classe:
3. Chamar sem objeto
Complete para chamar o método static quantosCriados da classe Produto:
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
staticpertence à 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 repetirstatic). 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 oinline,ISO C++ forbids in-class initialization of non-const static member. - Acesse com
Classe::membro; funciona sem nenhum objeto criado. - Métodos
staticsão chamados comClasse::metodo(), não têmthise só enxergam membrosstatic. - Constantes da classe:
static constexpr int maximo = 100;. - Próxima aula: o que acontece quando um objeto morre, em Destrutores e RAII.