C++ Boas Práticas
O C++ deixa você fazer quase tudo, inclusive muita coisa errada sem reclamar. Um programa pode compilar, rodar e mostrar um número absurdo, ou funcionar no seu computador e quebrar no de outra pessoa. As boas práticas são hábitos que evitam esses problemas antes que eles apareçam. Esta página junta todas as que o guia ensinou num checklist comentado e termina com um programa "antes × depois": um código cheio de problemas, os avisos reais do compilador e a versão consertada, mudança por mudança.
- Os 12 itens do checklist de um bom código C++, cada um com o porquê.
- Como usar os avisos do compilador (
-Wall -Wextra) e o-Werrora seu favor. - Por que
using namespace std;pode quebrar um programa, com o erro real. - Como os sanitizers encontram o erro que o compilador não viu.
- Como refatorar (melhorar sem mudar o que faz) um programa ruim, passo a passo.
Todos os programas foram compilados com o g++ 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp/boas. Cada item do checklist tem uma aula própria; aqui você vê todos juntos e aprende a usá-los como rotina.
A ideia: o checklist do piloto
Antes de decolar, todo piloto de avião, por mais experiente que seja, segue um checklist: combustível, portas, flaps. Não é porque ele esqueceu como se pilota. É porque, numa rotina repetida mil vezes, um item esquecido custa caro, e o checklist torna o esquecimento quase impossível.
Programar é parecido. Você sabe o que é uma variável sem valor inicial, mas num programa de 300 linhas, às 23h, ela passa. O checklist abaixo é a sua lista antes de "decolar" (entregar o programa):
| # | Prática | Evita |
|---|---|---|
| 1 | Compile com -Wall -Wextra (e -Werror em projetos) | Erros que o compilador já tinha visto |
| 2 | Inicialize toda variável | Números "lixo" |
| 3 | Prefira std::vector, std::string e std::array a arrays crus | Ler e escrever fora dos limites |
| 4 | Use ponteiros inteligentes em vez de new/delete | Vazamentos e delete errado |
| 5 | RAII: deixe os objetos cuidarem dos recursos | Arquivos e memória esquecidos abertos |
| 6 | const sempre que puder | Mudar o que não devia |
| 7 | const& para objetos grandes | Cópias lentas |
| 8 | enum class no lugar de enum | Misturar categorias com números |
| 9 | Evite using namespace std; (nunca em headers) | Conflito de nomes |
| 10 | Funções pequenas, nomes claros | Código que ninguém entende |
| 11 | Sanitizers nos testes | Erros de memória que só aparecem às vezes |
| 12 | Formatação automática (clang-format) | Discussões sobre espaços e chaves |
1. Compile com -Wall -Wextra (e -Werror em projetos)
O compilador percebe muita coisa suspeita, mas só avisa se você pedir. -Wall (de warnings: all, "todos os avisos", que na verdade são os mais importantes) e -Wextra ("avisos extras") ligam os avisos que todo programador deveria ver. A aula Avisos (-Wall -Wextra) explica cada um.
Este programa soma as notas da Ana. Ele compila e mostra a resposta certa, mas tem dois problemas:
#include <iostream>
#include <vector>
int main() {
std::vector<int> notas{7, 9, 10};
int soma = 0;
int maior;
for (int i = 0; i < notas.size(); i++) {
soma += notas[i];
}
std::cout << "Soma: " << soma << '\n';
}
$ g++ -std=c++23 -Wall -Wextra aviso1.cpp -o aviso1
aviso1.cpp: In function ‘int main()’:
aviso1.cpp:8:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<int>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
8 | for (int i = 0; i < notas.size(); i++) {
| ~~^~~~~~~~~~~~~~
aviso1.cpp:7:9: warning: unused variable ‘maior’ [-Wunused-variable]
7 | int maior;
| ^~~~~
$ ./aviso1
Soma: 26
- comparison of integer expressions of different signedness
- "Comparação de inteiros com sinais diferentes": o
iéint(pode ser negativo) e onotas.size()éunsigned(nunca negativo). Comparar os dois pode dar resultados estranhos com números negativos (veja Inteiros). O conserto mais simples é nem usar índice: um range-for. - unused variable ‘maior’
- "Variável não usada": você criou
maiore esqueceu. Às vezes é sobra; às vezes é sinal de que faltou um pedaço do programa.
Um aviso não impede o programa de ser criado. Por isso é fácil ignorá-los, e em pouco tempo um projeto tem 200 avisos e ninguém lê mais nenhum. A solução dos projetos sérios é o -Werror ("avisos viram erros"): com ele, qualquer aviso impede a compilação. Você vai vê-lo em ação no programa antes × depois.
Trate todo aviso como um erro que ainda não aconteceu. Leia, entenda e conserte. Um programa com zero avisos tem uma vantagem escondida: quando aparecer um aviso novo, você vai notá-lo na hora.
Missão: rode e veja os dois avisos (o editor usa o Clang, que escreve as mensagens de outro jeito, mas os avisos são os mesmos). Depois conserte o programa para compilar com zero avisos: apague a variável que sobrou e troque o for com índice por um for (int nota : notas).
#include <iostream>
#include <vector>
int main() {
std::vector<int> notas{7, 9, 10};
int soma = 0;
int maior;
for (int i = 0; i < notas.size(); i++) {
soma += notas[i];
}
std::cout << "Soma: " << soma << '\n';
}
#include <iostream>
#include <vector>
int main() {
std::vector<int> notas{7, 9, 10};
int soma = 0;
for (int nota : notas) {
soma += nota;
}
std::cout << "Soma: " << soma << '\n';
}
2. Inicialize toda variável
Uma variável local sem valor inicial, como int total;, não começa com zero: ela começa com o que estiver naquele pedaço da memória, um número "lixo". Usar esse valor é comportamento indefinido: o programa pode mostrar 0, mostrar -1192551482 ou fazer coisa pior. O mais traiçoeiro é que, às vezes, o lixo é zero por acaso e o programa parece certo.
int total; // ruim: valor lixo
int total = 0; // bom
double preco{}; // bom: as chaves vazias valem zero
std::string nome; // ok: string e vector começam vazios sozinhos
Dentro de uma struct, dê um valor padrão a cada campo (double preco = 0.0;), como você viu em Structs.
3. Prefira std::vector, std::string e std::array a arrays crus
Um array cru (int notas[5];) não sabe o próprio tamanho, não cresce e deixa você ler a posição 5, 50 ou 5000 sem reclamar. As alternativas da biblioteca padrão resolvem isso:
| Em vez de | Use | Por quê |
|---|---|---|
int notas[5]; | std::array<int, 5> notas{}; | Tamanho fixo, mas sabe o tamanho (notas.size()) e funciona com range-for e algoritmos. Aula: std::array. |
int* v = new int[n]; | std::vector<int> v(n); | Cresce, libera a memória sozinho. Aula: std::vector. |
char nome[50]; | std::string nome; | Cabe qualquer tamanho, sem estourar. Aula: Strings. |
4. Ponteiros inteligentes em vez de new/delete
Todo new precisa de exatamente um delete, do tipo certo (new[] pede delete[]), no momento certo, mesmo se uma função terminar no meio. É fácil errar. Os ponteiros inteligentes fazem o delete sozinhos:
Produto* p = new Produto{"maçã", 2.5}; // ruim: e o delete?
delete p;
auto p = std::make_unique<Produto>("maçã", 2.5); // bom: liberado sozinho
Na prática, quase nunca você precisa nem disso: um std::vector<Produto> guarda os produtos e cuida da memória. A regra é: se você escreveu new, pergunte se um container ou um make_unique resolve.
5. RAII: deixe os objetos cuidarem dos recursos
RAII é a sigla de Resource Acquisition Is Initialization ("adquirir um recurso é inicializar um objeto"). O nome é feio, a ideia é simples: quem abre, fecha sozinho. Um std::ifstream abre o arquivo quando é criado e fecha quando sai de escopo; um std::vector pede memória quando é criado e devolve quando sai de escopo. Você não precisa lembrar de nada.
void ler_lista() {
std::ifstream arquivo("lista.txt"); // abre
std::string linha;
while (std::getline(arquivo, linha)) {
// ...
}
} // fecha sozinho aqui, mesmo se houver um return no meio
Os ponteiros inteligentes do item 4 são RAII para memória. A aula Destrutores e RAII mostra como escrever as suas próprias classes assim.
6. const sempre que puder
Se um valor não deve mudar, diga isso ao compilador com const (aula Constantes). Se alguém tentar mudar (inclusive você, daqui a um mês), o programa não compila:
const double taxa = 0.05;
const std::vector<std::string> categorias{"fruta", "limpeza"};
Além de proteger, o const é documentação: quem lê sabe na hora que aquele valor é fixo. Nos métodos de classe, const diz "este método só lê o objeto".
7. const& para objetos grandes
Quando você passa um std::vector ou uma std::string para uma função "por valor", o C++ faz uma cópia inteira. Com mil produtos, são mil produtos copiados a cada chamada. Passar por referência constante (const&) entrega o original, só para leitura, sem copiar nada (aula Por Valor × Por Referência):
double total(std::vector<Produto> estoque); // copia tudo double total(const std::vector<Produto>& estoque); // só olha, sem copiar
Para tipos pequenos (int, double, char, bool), passe por valor: copiar um número é mais barato que criar uma referência.
8. enum class no lugar de enum
O enum antigo vira int sem avisar e espalha os nomes pelo programa inteiro (FRUTA passa a ser um nome solto). O enum class fica fechado no próprio nome: Categoria::fruta, e não se mistura com números (aula Enums).
enum Categoria { FRUTA, LIMPEZA }; // antigo: FRUTA + 1 compila
enum class Categoria { fruta, limpeza }; // moderno: Categoria::fruta + 1 não compila
9. Evite using namespace std; (e nunca em headers)
A linha using namespace std; "despeja" todos os nomes da biblioteca padrão no seu programa, para você escrever cout em vez de std::cout. Parece prático, mas a biblioteca tem milhares de nomes, e alguns são palavras comuns: count, size, distance, max. Veja o que acontece quando a Ana cria uma variável count (contagem):
#include <algorithm>
#include <iostream>
using namespace std;
int count = 0;
int main() {
count++;
cout << count << '\n';
}
$ g++ -std=c++23 -Wall -Wextra colisao.cpp -o colisao
colisao.cpp: In function ‘int main()’:
colisao.cpp:8:5: error: reference to ‘count’ is ambiguous
8 | count++;
| ^~~~~
In file included from /usr/include/c++/15/algorithm:89,
from colisao.cpp:1:
/usr/include/c++/15/pstl/glue_algorithm_defs.h:102:1: note: candidates are: ‘template<class _ExecutionPolicy, class _ForwardIterator, class _Tp> __pstl::__internal::__enable_if_execution_policy<_ExecutionPolicy, typename std::iterator_traits<_II>::difference_type> std::count(_ExecutionPolicy&&, _ForwardIterator, _ForwardIterator, const _Tp&)’
102 | count(_ExecutionPolicy&& __exec, _ForwardIterator __first, _ForwardIterator __last, const _Tp& __value);
| ^~~~~
In file included from /usr/include/c++/15/algorithm:63:
/usr/include/c++/15/bits/stl_algo.h:4070:5: note: ‘template<class _IIter, class _Tp> constexpr typename std::iterator_traits<_Iterator>::difference_type std::count(_IIter, _IIter, const _Tp&)’
4070 | count(_InputIterator __first, _InputIterator __last, const _Tp& __value)
| ^~~~~
colisao.cpp:5:5: note: ‘int count’
5 | int count = 0;
| ^~~~~
colisao.cpp:9:13: error: reference to ‘count’ is ambiguous
9 | cout << count << '\n';
| ^~~~~
/usr/include/c++/15/pstl/glue_algorithm_defs.h:102:1: note: candidates are: ‘template<class _ExecutionPolicy, class _ForwardIterator, class _Tp> __pstl::__internal::__enable_if_execution_policy<_ExecutionPolicy, typename std::iterator_traits<_II>::difference_type> std::count(_ExecutionPolicy&&, _ForwardIterator, _ForwardIterator, const _Tp&)’
102 | count(_ExecutionPolicy&& __exec, _ForwardIterator __first, _ForwardIterator __last, const _Tp& __value);
| ^~~~~
/usr/include/c++/15/bits/stl_algo.h:4070:5: note: ‘template<class _IIter, class _Tp> constexpr typename std::iterator_traits<_Iterator>::difference_type std::count(_IIter, _IIter, const _Tp&)’
4070 | count(_InputIterator __first, _InputIterator __last, const _Tp& __value)
| ^~~~~
colisao.cpp:5:5: note: ‘int count’
5 | int count = 0;
| ^~~~~
Assusta, mas a primeira linha diz tudo: reference to ‘count’ is ambiguous, "a referência a count é ambígua". Existem dois count: o da Ana (‘int count’, a última nota) e a função std::count do <algorithm> (as outras notas, com nomes enormes). Por causa do using namespace std;, os dois estão "soltos", e o compilador não sabe qual você quis. Sem essa linha, count seria só o da Ana e std::count seria o da biblioteca.
Num arquivo .cpp pequeno, o estrago fica nele. Num header (.h), é pior: todo arquivo que fizer #include dele recebe o using namespace std; junto, sem saber (aula Arquivos .h e .cpp). Por isso a regra é: nunca em headers; nos .cpp, prefira escrever std::. Se quiser encurtar um nome muito usado, traga só ele: using std::cout; (aula Namespaces).
10. Funções pequenas, nomes claros
Compare estas duas assinaturas:
double f(vector<Produto> v, int x); double valor_do_estoque(const std::vector<Produto>& estoque);
A segunda dispensa comentário: você sabe o que ela faz, o que recebe e que não muda nada. Duas regras práticas:
- Uma função, uma tarefa. Se para descrever o que ela faz você precisa de "e" ("lê o arquivo e calcula o total e mostra"), divida em três (aula Funções). Uma função que cabe na tela inteira é um bom limite.
- Nomes que dizem o que a coisa é.
total_vendas, nãotv;quantidade, nãoq. A exceção são contadores de laço curtos (i). Veja as regras de nomes em Nomes de Variáveis.
11. Sanitizers nos testes
Alguns erros o compilador não consegue ver, porque só acontecem na hora de rodar: ler fora de um array, usar memória já liberada, estourar um int. Os sanitizers ("higienizadores") são verificações que o g++ coloca dentro do programa: ele fica mais lento, mas para no primeiro erro e diz a linha exata. Use-os sempre que testar:
$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined programa.cpp -o programa
Você vai ver um deles pegando um erro real no programa abaixo. A aula Sanitizers explica cada tipo e como ler o relatório.
12. Formatação automática (clang-format)
Onde vai a chave? Quatro espaços ou dois? Em vez de decidir na mão, os projetos usam um formatador: o mais comum em C++ é o clang-format, da mesma família do Clang. Ele reescreve o arquivo seguindo regras guardadas num arquivo .clang-format na pasta do projeto, e as IDEs (veja Editores e IDEs) podem rodá-lo toda vez que você salva. O guia só cita a ferramenta: o importante é que o projeto inteiro tenha um estilo, qualquer que seja.
O programa antes × depois
Agora o checklist em ação. Este é o controle de estoque da lojinha da Ana, escrito "do jeito rápido". Ele deveria mostrar o valor do estoque, o total de vendas da semana e quantos produtos estão zerados:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
enum Categoria { FRUTA, LIMPEZA };
struct Produto {
string nome;
double preco;
int qtd;
Categoria cat;
};
double f(vector<Produto> v, int x) {
double t;
for (int i = 0; i < v.size(); i++)
t += v[i].preco * v[i].qtd;
return t;
}
int main() {
vector<Produto> estoque;
estoque.push_back({"maçã", 2.5, 40, FRUTA});
estoque.push_back({"sabão", 8.9, 10, LIMPEZA});
estoque.push_back({"pera", 3.0, 0, FRUTA});
int* vendas = new int[3];
vendas[0] = 5; vendas[1] = 2; vendas[2] = 7;
int total_vendas = 0;
for (int i = 0; i <= 3; i++)
total_vendas += vendas[i];
delete vendas;
int zerados;
for (int i = 0; i < estoque.size(); i++)
if (estoque[i].qtd = 0) zerados++;
cout << "Valor do estoque: " << f(estoque, 1) << endl;
cout << "Vendas: " << total_vendas << endl;
cout << "Produtos zerados: " << zerados << endl;
}
As respostas certas seriam: valor 189 (2,5 × 40 + 8,9 × 10 + 3 × 0), vendas 14 e um produto zerado. Primeiro, a Ana compila sem avisos:
$ g++ -std=c++23 antes.cpp -o antes $ ./antes Valor do estoque: 4.83151e-310 « devia ser 189 Vendas: 14 « certo… por sorte Produtos zerados: -1192551482 « devia ser 1
Compilou sem nenhuma mensagem e rodou. Duas respostas são absurdas, e os números mudam de uma execução para outra (são lixo da memória). A terceira parece certa, mas, como você vai ver, é sorte. Agora com os avisos ligados:
$ g++ -std=c++23 -Wall -Wextra antes.cpp -o antes
antes.cpp: In function ‘double f(std::vector<Produto>, int)’:
antes.cpp:17:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<Produto>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
17 | for (int i = 0; i < v.size(); i++)
| ~~^~~~~~~~~~
antes.cpp:15:33: warning: unused parameter ‘x’ [-Wunused-parameter]
15 | double f(vector<Produto> v, int x) {
| ~~~~^
antes.cpp: In function ‘int main()’:
antes.cpp:36:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<Produto>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
36 | for (int i = 0; i < estoque.size(); i++)
| ~~^~~~~~~~~~~~~~~~
antes.cpp:37:28: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
37 | if (estoque[i].qtd = 0) zerados++;
antes.cpp:33:12: warning: ‘void operator delete(void*, std::size_t)’ called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
33 | delete vendas;
| ^~~~~~
antes.cpp:28:28: note: returned from ‘void* operator new [](std::size_t)’
28 | int* vendas = new int[3];
| ^
Cinco avisos. Os mais graves:
- suggest parentheses around assignment used as truth value (linha 37)
- "Sugiro parênteses em volta da atribuição usada como verdadeiro/falso". A Ana escreveu
=(atribuir) no lugar de==(comparar). Esseifnão pergunta "a quantidade é zero?": ele zera a quantidade de todos os produtos! É por isso que o valor do estoque deu praticamente zero. Veja Operadores de Comparação. - called on pointer returned from a mismatched allocation function (linha 33)
- "
deletechamado num ponteiro que veio de uma função de alocação diferente": a memória foi pedida comnew int[3](com colchetes) e devolvida comdelete(sem colchetes). O certo seriadelete[]. Melhor ainda: não usarnew(item 4). - unused parameter ‘x’ (linha 15)
- A função
frecebe umxque nunca usa. Sobra de uma versão antiga. - comparison of integer expressions of different signedness (linhas 17 e 36)
intcomparado com o tamanhounsigneddo vector, como no item 1.
Faltam dois problemas que o g++ só enxerga quando otimiza o código (-O2), porque é nessa hora que ele analisa por onde cada valor passa:
$ g++ -std=c++23 -Wall -Wextra -O2 antes.cpp -o antes 2>&1 | grep -A6 "may be used"
antes.cpp:18:11: warning: ‘t’ may be used uninitialized [-Wmaybe-uninitialized]
18 | t += v[i].preco * v[i].qtd;
| ~~^~~~~~~~~~~~~~~~~~~~~~~~
antes.cpp:16:12: note: ‘t’ was declared here
16 | double t;
| ^
antes.cpp: In function ‘int main()’:
antes.cpp:41:37: warning: ‘zerados’ may be used uninitialized [-Wmaybe-uninitialized]
41 | cout << "Produtos zerados: " << zerados << endl;
| ^~~~~~~
antes.cpp:35:9: note: ‘zerados’ was declared here
35 | int zerados;
| ^~~~~~~
antes.cpp:33:12: warning: ‘void operator delete(void*, std::size_t)’ called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
« o -A6 mostra 6 linhas depois de cada achado: esta já é do aviso seguinte
‘t’ may be used uninitialized: "t pode ser usada sem ter sido inicializada". O mesmo vale para zerados. São os números lixo da saída (item 2). O | grep -A6 "may be used" no fim do comando só filtra a saída: mostra as linhas com "may be used" e as 6 seguintes, para não repetir os cinco avisos que você já viu.
O que o -Werror faria
Num projeto com -Werror, esse programa nem chegaria a existir:
$ rm -f antes « apaga o programa da compilação anterior
$ g++ -std=c++23 -Wall -Wextra -Werror antes.cpp -o antes
antes.cpp: In function ‘double f(std::vector<Produto>, int)’:
antes.cpp:17:23: error: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<Produto>::size_type’ {aka ‘long unsigned int’} [-Werror=sign-compare]
17 | for (int i = 0; i < v.size(); i++)
| ~~^~~~~~~~~~
antes.cpp:15:33: error: unused parameter ‘x’ [-Werror=unused-parameter]
15 | double f(vector<Produto> v, int x) {
| ~~~~^
antes.cpp: In function ‘int main()’:
antes.cpp:36:23: error: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<Produto>::size_type’ {aka ‘long unsigned int’} [-Werror=sign-compare]
36 | for (int i = 0; i < estoque.size(); i++)
| ~~^~~~~~~~~~~~~~~~
antes.cpp:37:28: error: suggest parentheses around assignment used as truth value [-Werror=parentheses]
37 | if (estoque[i].qtd = 0) zerados++;
antes.cpp:33:12: error: ‘void operator delete(void*, std::size_t)’ called on pointer returned from a mismatched allocation function [-Werror=mismatched-new-delete]
33 | delete vendas;
| ^~~~~~
antes.cpp:28:28: note: returned from ‘void* operator new [](std::size_t)’
28 | int* vendas = new int[3];
| ^
cc1plus: all warnings being treated as errors
$ ls antes
ls: não foi possível acessar 'antes': Arquivo ou diretório inexistente « nenhum programa foi criado
Cada warning virou error, e o nome entre colchetes ganhou um -Werror= na frente. A última linha, all warnings being treated as errors, quer dizer "todos os avisos estão sendo tratados como erros" (cc1plus é o nome da parte do g++ que compila C++).
O erro que nem os avisos viram
Repare na linha 31: for (int i = 0; i <= 3; i++). Com <=, o laço vai de 0 a 3, e lê vendas[3], uma posição que não existe (o array tem 0, 1 e 2). Nenhum aviso apontou isso. Por sorte, a posição "fantasma" valia 0 e o total deu 14. Os sanitizers do item 11 pegam esse erro na hora:
$ g++ -std=c++23 -g -fsanitize=address,undefined antes.cpp -o antes-san
$ ./antes-san
=================================================================
==4==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7864169e001c at pc 0x58c21cf5f153 bp 0x7ffca773f6e0 sp 0x7ffca773f6d0
READ of size 4 at 0x7864169e001c thread T0
#0 0x58c21cf5f152 in main /home/ana/cpp/boas/antes.cpp:32
#1 0x7c441762a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#2 0x7c441762a717 in __libc_start_main_impl ../csu/libc-start.c:360
#3 0x58c21cf5e564 in _start (/home/ana/cpp/boas/antes-san+0x14564) (BuildId: b31395161dae059797fd86f8c1bd8d2c83b5375f)
0x7864169e001c is located 0 bytes after 12-byte region [0x7864169e0010,0x7864169e001c)
allocated by thread T0 here:
#0 0x7c441872c8cf in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:111
#1 0x58c21cf5eee2 in main /home/ana/cpp/boas/antes.cpp:28
#2 0x7c441762a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#3 0x7c441762a717 in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x58c21cf5e564 in _start (/home/ana/cpp/boas/antes-san+0x14564) (BuildId: b31395161dae059797fd86f8c1bd8d2c83b5375f)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/ana/cpp/boas/antes.cpp:32 in main
Shadow bytes around the buggy address:
… « um mapa da memória, útil para especialistas
==4==ABORTING
- heap-buffer-overflow
- "Estouro de um bloco da memória dinâmica" (o heap, de onde vem a memória do
new; veja Como a Memória Funciona). - READ of size 4 … in main /home/ana/cpp/boas/antes.cpp:32
- Uma leitura de 4 bytes (um
int) fora do lugar, na linha 32:total_vendas += vendas[i];. - 0 bytes after 12-byte region
- "Logo depois de um bloco de 12 bytes": os 3
intdo array (3 × 4 bytes). Ou seja, a leitura foi na primeira posição depois do fim. - allocated by thread T0 here: … antes.cpp:28
- Onde o bloco foi criado: a linha 28, do
new int[3].
Os endereços (0x7864…) mudam a cada execução; o que importa são os nomes de arquivo e as linhas.
Depois: o mesmo programa com o checklist aplicado
#include <array>
#include <print>
#include <string>
#include <vector>
enum class Categoria { fruta, limpeza };
struct Produto {
std::string nome;
double preco = 0.0;
int quantidade = 0;
Categoria categoria = Categoria::fruta;
};
double valor_do_estoque(const std::vector<Produto>& estoque) {
double total = 0.0;
for (const auto& produto : estoque) {
total += produto.preco * produto.quantidade;
}
return total;
}
int contar_zerados(const std::vector<Produto>& estoque) {
int zerados = 0;
for (const auto& produto : estoque) {
if (produto.quantidade == 0) {
++zerados;
}
}
return zerados;
}
int main() {
const std::vector<Produto> estoque{
{"maçã", 2.5, 40, Categoria::fruta},
{"sabão", 8.9, 10, Categoria::limpeza},
{"pera", 3.0, 0, Categoria::fruta},
};
const std::array vendas{5, 2, 7};
int total_vendas = 0;
for (int venda : vendas) {
total_vendas += venda;
}
std::println("Valor do estoque: {:.2f}", valor_do_estoque(estoque));
std::println("Vendas: {}", total_vendas);
std::println("Produtos zerados: {}", contar_zerados(estoque));
}
$ g++ -std=c++23 -Wall -Wextra depois.cpp -o depois « nenhum aviso $ ./depois Valor do estoque: 189.00 Vendas: 14 Produtos zerados: 1
As três respostas certas, zero avisos. Cada mudança, e o item do checklist que ela aplica:
| Antes | Depois | Item |
|---|---|---|
using namespace std; | Apagado; std:: escrito em cada nome | 9 |
enum Categoria { FRUTA, LIMPEZA }; | enum class Categoria { fruta, limpeza }; | 8 |
Campos double preco; int qtd; sem valor | double preco = 0.0; int quantidade = 0; | 2, 10 |
double f(vector<Produto> v, int x) | double valor_do_estoque(const std::vector<Produto>& estoque): nome claro, sem cópia, sem o x que sobrava | 7, 10 |
double t; | double total = 0.0; | 2 |
for (int i = 0; i < v.size(); i++) | for (const auto& produto : estoque): sem índice, sem aviso de sinal | 1 |
new int[3] … delete vendas; | const std::array vendas{5, 2, 7};: sem new, sem delete, sem estouro | 3, 4, 5 |
i <= 3 (lia uma posição a mais) | for (int venda : vendas): o range-for nunca passa do fim | 3 |
if (estoque[i].qtd = 0) zerados++; no meio do main | Função contar_zerados, com ==; o estoque é const, então um = por engano nem compilaria | 6, 10 |
cout << … << endl; | std::println("… {:.2f}", …), com duas casas decimais | (format e print) |
Repare na penúltima linha: o const do item 6 não só conserta o erro, ele torna o erro impossível. Se alguém escrever produto.quantidade = 0 dentro de contar_zerados, o compilador recusa, porque produto é const. Esse é o espírito das boas práticas: em vez de "tomar cuidado", fazer o compilador tomar cuidado por você.
A Ana não reescreveu tudo de uma vez. Ela fez uma mudança, compilou, rodou, conferiu a saída e só então fez a próxima. Assim, se algo quebrar, você sabe qual mudança foi a culpada.
Quando dá errado
Você (ou o Makefile/CMakeLists.txt do projeto) está usando -Werror, e o código tem pelo menos um aviso. O programa não foi criado.
Como consertar: leia cada linha com error: e o nome entre colchetes ([-Werror=unused-parameter] = o aviso -Wunused-parameter) e conserte o código. Não tire o -Werror para "fazer passar": ele existe exatamente para isso.
Um nome seu bateu com um nome da biblioteca padrão, quase sempre por causa de um using namespace std; (item 9).
Como consertar: apague o using namespace std; e escreva std::, ou dê outro nome à sua variável (contador em vez de count).
Um programa que dá a resposta certa num computador e errada em outro, ou certa hoje e errada amanhã, quase sempre tem uma variável sem valor inicial ou uma leitura fora do array: exatamente o t, o zerados e o vendas[3] do programa "antes". O resultado depende do lixo que estava na memória.
Como consertar: compile com -Wall -Wextra -O2 (para os avisos de "may be used uninitialized") e rode com -fsanitize=address,undefined. Veja Comportamento Indefinido.
Missão final: refatorar a média
Missão: aplique o checklist nesta função de média das notas. Ela deve continuar mostrando Média: 7.5, mas compilar com zero avisos e receber as notas como const std::vector<double>&. Apague o using namespace std;, inicialize a soma e use um range-for. Dica: o Clang do editor avisa que s não foi inicializada; o g++ sem -O2 não avisa, e o programa mostra 7.5 por pura sorte.
#include <iostream>
#include <vector>
using namespace std;
double media(vector<double> notas) {
double s;
for (int i = 0; i < notas.size(); i++) s += notas[i];
return s / notas.size();
}
int main() {
vector<double> notas = {7.5, 9.0, 6.0};
cout << "Média: " << media(notas) << endl;
}
#include <iostream>
#include <vector>
double media(const std::vector<double>& notas) {
double soma = 0.0;
for (double nota : notas) {
soma += nota;
}
return soma / notas.size();
}
int main() {
const std::vector<double> notas{7.5, 9.0, 6.0};
std::cout << "Média: " << media(notas) << '\n';
}
No laboratório, o código original deu estes avisos no clang++ 21, da mesma família do Clang que o editor usa:
$ clang++ -std=c++23 -Wall -Wextra media.cpp -o media
media.cpp:7:23: warning: comparison of integers of different signs: 'int' and 'size_type' (aka 'unsigned long') [-Wsign-compare]
7 | for (int i = 0; i < notas.size(); i++) s += notas[i];
| ~ ^ ~~~~~~~~~~~~
media.cpp:7:44: warning: variable 's' is uninitialized when used here [-Wuninitialized]
7 | for (int i = 0; i < notas.size(); i++) s += notas[i];
| ^
media.cpp:6:13: note: initialize the variable 's' to silence this warning
6 | double s;
| ^
| = 0.0
2 warnings generated.
O Clang até sugere o conserto: a última linha mostra = 0.0 onde ele quer que você inicialize.
Referência
| Prática | Na prática | Aula |
|---|---|---|
| Avisos | g++ -std=c++23 -Wall -Wextra; em projetos, -Werror | Avisos |
| Inicializar | int total = 0;, double preco{};, valores padrão nos campos | Variáveis |
| Containers | std::vector, std::array, std::string | vector, array, string |
| Memória | std::make_unique; nada de new/delete soltos | Ponteiros Inteligentes |
| RAII | Recursos dentro de objetos que se limpam sozinhos | Destrutores e RAII |
const | Em variáveis, parâmetros e métodos que não mudam | Constantes |
const& | Parâmetros vector, string, structs | Valor × Referência |
enum class | Categoria::fruta | Enums |
| Namespaces | std:: explícito; nada de using namespace em .h | Namespaces |
| Funções | Uma tarefa por função, nomes descritivos | Funções |
| Sanitizers | -g -fsanitize=address,undefined nos testes | Sanitizers |
| Formatação | clang-format com um .clang-format no projeto | Editores e IDEs |
Exercícios
1. O g++ mostrou warning: suggest parentheses around assignment used as truth value num if (qtd = 0). O que está errado?
2. Qual é a melhor forma de passar um std::vector<Produto> grande para uma função que só lê os produtos?
3. O que o -Werror faz?
4. Um laço for (int i = 0; i <= 3; i++) lê v[i] num array de 3 posições. Quem encontra esse erro?
5. Complete a declaração para que a variável comece com zero:
6. Complete o comando para compilar com os sanitizers de memória e de comportamento indefinido:
Resumo
- Compile sempre com
-Wall -Wextrae busque zero avisos; em projetos,-Werrorgarante isso. Alguns avisos só aparecem com-O2. - Inicialize tudo. Variável sem valor tem lixo, e lixo às vezes parece certo.
std::vector,std::array,std::stringe ponteiros inteligentes no lugar de arrays crus enew/delete. RAII: quem abre, fecha sozinho.constsempre que puder;const¶ objetos grandes;enum classno lugar deenum.- Nada de
using namespace std;em headers; nos.cpp, prefirastd::. - Funções pequenas com nomes que dizem o que fazem.
- Teste com
-fsanitize=address,undefined: ele encontra o que o compilador não vê. Formate com clang-format.