C++ Escopo

Toda variável tem um território: a parte do código onde ela existe e pode ser usada. Esse território se chama escopo. Até aqui, você já esbarrou nele sem saber: o parâmetro de uma função não existe no main, e a mensagem was not declared in this scope ("não foi declarado neste escopo") apareceu várias vezes. Nesta aula você vai entender onde cada variável nasce e morre, o que são variáveis locais e globais, o perigo de duas variáveis com o mesmo nome, e a variável static, que lembra o valor entre uma chamada e outra.

Você vai aprender
  • O que é uma variável local, e por que ela não existe fora da função.
  • Que todo par de chaves { } cria um escopo, inclusive o do if e o do for.
  • O que é uma variável global, e por que o guia recomenda evitá-las.
  • O que é sombreamento (uma variável escondendo outra) e o aviso -Wshadow.
  • Como uma variável static local cria um contador que sobrevive entre chamadas.

Todos os programas desta aula foram compilados com o g++ 15 na máquina fictícia da Ana, na pasta ~/cpp, com o comando padrão do guia, g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: os cômodos da casa

Na casa da Ana, o que está no quarto fica no quarto: quem está na cozinha não enxerga a escrivaninha dela. Já o que está na sala, que é de todos, qualquer um vê e qualquer um pode mudar de lugar. E dentro do quarto há um armário: o que está lá dentro só existe para quem abriu o armário.

Na casaEm C++
O quartoO corpo de uma função: as variáveis criadas ali são locais
O armário dentro do quartoUm bloco { } dentro da função (o de um if, de um for)
A sala, de todosFora de qualquer função: variáveis globais

A regra geral: de dentro, você enxerga o que está fora; de fora, você não enxerga o que está dentro.


1. Variáveis locais: só existem dentro da função

Uma variável criada dentro de uma função é local: nasce na linha em que é criada e morre no } que fecha a função. Fora dela, o nome não existe:

#include <iostream>

void calcularTotal() {
    double total = 3 * 10.0;
    std::cout << "Total na função: " << total << "\n";
}

int main() {
    calcularTotal();
    std::cout << "Total no main: " << total << "\n";
}
$ g++ -std=c++23 -Wall -Wextra local.cpp -o local
local.cpp: In function ‘int main()’:
local.cpp:10:39: error: ‘total’ was not declared in this scope
   10 |     std::cout << "Total no main: " << total << "\n";
      |                                       ^~~~~
In function ‘int main()’
O problema está no main.
‘total’ was not declared in this scope
"‘total’ não foi declarado neste escopo". O total existe, mas no escopo da calcularTotal; no escopo do main, não.

Mesmo que a função já tenha sido chamada na linha 9, quando ela termina, o total é destruído. Para levar um valor de dentro de uma função para fora, use o return (aula Retorno): double total = calcularTotal();.

Os parâmetros também são locais. Na função void dobrar(int numero), o numero só existe dentro de dobrar; tentar usá-lo no main dá o mesmo erro, ‘numero’ was not declared in this scope.

O lado bom: nomes iguais não se misturam

Como cada função tem o seu território, duas funções podem ter variáveis com o mesmo nome sem nenhuma confusão:

#include <iostream>

void loja() {
    int quantidade = 3;
    std::cout << "loja: " << quantidade << "\n";
}

void estoque() {
    int quantidade = 50;
    std::cout << "estoque: " << quantidade << "\n";
}

int main() {
    int quantidade = 1;
    loja();
    estoque();
    std::cout << "main: " << quantidade << "\n";
}
$ g++ -std=c++23 -Wall -Wextra mesmonome.cpp -o mesmonome
$ ./mesmonome
loja: 3
estoque: 50
main: 1

São três variáveis diferentes, que só têm o nome em comum. É isso que permite escrever uma função sem se preocupar com os nomes usados no resto do programa: quem escreveu a loja não precisa saber que o main também tem uma quantidade.


2. Todo bloco { } cria um escopo

Não são só as funções. Qualquer par de chaves cria um território novo: o do if, o do else, o do while, o do for. Uma variável criada dentro de um bloco morre no } dele:

#include <iostream>
#include <string>

int main() {
    double nota = 8.5;
    if (nota >= 6) {
        std::string mensagem = "aprovada";
        std::cout << "Ana foi " << mensagem << "\n";
    }
    std::cout << mensagem << "\n";
}
$ g++ -std=c++23 -Wall -Wextra bloco.cpp -o bloco
bloco.cpp: In function ‘int main()’:
bloco.cpp:10:18: error: ‘mensagem’ was not declared in this scope
   10 |     std::cout << mensagem << "\n";
      |                  ^~~~~~~~

A mensagem nasceu na linha 7, dentro do if, e morreu na linha 9. Já a nota, criada no bloco do main, é visível dentro do if: de dentro se enxerga o que está fora. Se você precisa da variável depois do bloco, crie-a antes dele: std::string mensagem; na linha 6 e, dentro do if, só mensagem = "aprovada";.

A variável do for

A variável criada nos parênteses do for pertence ao laço e morre quando ele termina:

#include <iostream>

int main() {
    for (int i = 1; i <= 3; i++) {
        std::cout << "volta " << i << "\n";
    }
    std::cout << "terminou na volta " << i << "\n";
}
$ g++ -std=c++23 -Wall -Wextra forvar.cpp -o forvar
forvar.cpp: In function ‘int main()’:
forvar.cpp:7:42: error: ‘i’ was not declared in this scope
    7 |     std::cout << "terminou na volta " << i << "\n";
      |                                          ^

Isso é proposital: o i só serve para contar as voltas, e assim não "vaza" para o resto da função. Se você precisa saber onde o laço parou, crie a variável antes:

#include <iostream>

int main() {
    int i = 1;
    for (; i <= 3; i++) {
        std::cout << "volta " << i << "\n";
    }
    std::cout << "o laço parou com i = " << i << "\n";
}
$ g++ -std=c++23 -Wall -Wextra forvar2.cpp -o forvar2
$ ./forvar2
volta 1
volta 2
volta 3
o laço parou com i = 4

Dentro de um laço, a variável nasce de novo a cada volta

#include <iostream>

int main() {
    for (int volta = 1; volta <= 3; volta++) {
        int contador = 0;
        contador++;
        std::cout << "volta " << volta << ": contador = " << contador << "\n";
    }
}
$ g++ -std=c++23 -Wall -Wextra recria.cpp -o recria
$ ./recria
volta 1: contador = 1
volta 2: contador = 1
volta 3: contador = 1

O contador nunca passa de 1: a cada volta, ele é criado com 0, somado, mostrado e destruído no }. Para contar as voltas, a variável precisa morar fora do laço. É um engano muito comum de quem está começando.

Chaves sozinhas também valem

Você pode abrir um bloco sem if nem for, só com { }, para limitar a vida de uma variável. É raro em código do dia a dia, mas mostra a regra na forma mais pura: { int y = 2; } e, depois do }, o y não existe mais.

Missão: a Ana quer somar as quantidades da lista, mas o programa não compila. Leia o erro e mude onde a variável total é criada (ela precisa existir antes do laço e continuar existindo depois dele) para aparecer Total de itens: 6.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> quantidades = {1, 2, 3};
    for (int q : quantidades) {
        int total = 0;
        total = total + q;
    }
    std::cout << "Total de itens: " << total << "\n";
}
#include <iostream>
#include <vector>

int main() {
    std::vector<int> quantidades = {1, 2, 3};
    int total = 0;
    for (int q : quantidades) {
        total = total + q;
    }
    std::cout << "Total de itens: " << total << "\n";
}

3. Variáveis globais (e por que evitar)

Uma variável criada fora de qualquer função, normalmente no topo do arquivo, é global: existe durante o programa inteiro, e todas as funções abaixo dela podem lê-la e mudá-la.

#include <iostream>

int visitas = 0;

void registrarVisita() {
    visitas++;
}

void mostrarVisitas() {
    std::cout << "Visitas hoje: " << visitas << "\n";
}

int main() {
    registrarVisita();
    registrarVisita();
    registrarVisita();
    mostrarVisitas();
}
$ g++ -std=c++23 -Wall -Wextra global.cpp -o global
$ ./global
Visitas hoje: 3

Parece prático: nada de parâmetros, nada de return. O problema aparece quando o programa cresce. Como qualquer função pode mudar uma global, quando o valor sai errado, você não sabe quem foi o culpado. Veja o que aconteceu com o caixa da Ana:

#include <iostream>

int total = 0;

void somarItem(int preco) {
    total = total + preco;
}

void relatorioDoDia() {
    for (total = 0; total < 3; total++) {
        std::cout << "imprimindo página " << total + 1 << "\n";
    }
}

int main() {
    somarItem(30);
    somarItem(5);
    relatorioDoDia();
    std::cout << "Total da venda: " << total << "\n";
}
$ g++ -std=c++23 -Wall -Wextra globalbug.cpp -o globalbug
$ ./globalbug
imprimindo página 1
imprimindo página 2
imprimindo página 3
Total da venda: 3            « deveria ser 35!

Quem escreveu o relatorioDoDia usou o nome total para contar páginas, sem lembrar que já existia uma global com esse nome. O compilador não reclamou: é tudo válido. O valor da venda foi apagado por uma função que não tinha nada a ver com vendas. Num programa de mil linhas, achar esse erro leva horas.

A alternativa é passar o que a função precisa por parâmetro e receber o resultado pelo retorno. Assim, só mexe no total quem recebeu o total:

#include <iostream>

int somarItem(int total, int preco) {
    return total + preco;
}

int main() {
    int total = 0;
    total = somarItem(total, 30);
    total = somarItem(total, 5);
    std::cout << "Total da venda: " << total << "\n";
}
$ g++ -std=c++23 -Wall -Wextra semglobal.cpp -o semglobal
$ ./semglobal
Total da venda: 35
GlobalQuando é aceitável
int visitas = 0; (variável)Quase nunca. Prefira parâmetros e retorno (ou &, aula Por Valor × Por Referência).
const double TAXA = 0.05; (constante)Tudo bem. Ninguém consegue mudá-la, então não há culpado a procurar (aula Constantes).
Globais começam com zero

Uma global sem valor inicial (int visitas;) começa com 0. Uma variável local sem valor inicial começa com um valor qualquer, o que sobrou na memória, e o -Wall avisa (is used uninitialized; veja Variáveis). De qualquer jeito, a regra do guia vale para as duas: sempre inicialize.


4. Sombreamento: quando um nome esconde outro

Se você cria, num bloco de dentro, uma variável com o mesmo nome de uma de fora, a de dentro "faz sombra" na de fora: dali até o fim do bloco, o nome se refere à de dentro. Isso se chama sombreamento (em inglês, shadowing):

#include <iostream>

int main() {
    int preco = 10;
    if (preco > 5) {
        int preco = 99;
        std::cout << "dentro do if: " << preco << "\n";
    }
    std::cout << "fora do if: " << preco << "\n";
}
$ g++ -std=c++23 -Wall -Wextra sombra.cpp -o sombra       « compila sem nenhum aviso
$ ./sombra
dentro do if: 99
fora do if: 10

Na linha 6, a Ana provavelmente queria mudar o preço (preco = 99;), mas escreveu int na frente e criou outra variável. A de fora continuou 10. Nem -Wall nem -Wextra avisam disso. Existe uma opção específica, -Wshadow ("avisar sombras"):

$ g++ -std=c++23 -Wall -Wextra -Wshadow sombra.cpp -o sombra
sombra.cpp: In function ‘int main()’:
sombra.cpp:6:13: warning: declaration of ‘preco’ shadows a previous local [-Wshadow]
    6 |         int preco = 99;
      |             ^~~~~
sombra.cpp:4:9: note: shadowed declaration is here
    4 |     int preco = 10;
      |         ^~~~~
declaration of ‘preco’ shadows a previous local
"A declaração de ‘preco’ faz sombra a uma variável local anterior".
note: shadowed declaration is here
"A declaração que ficou na sombra está aqui": a linha 4.

O mesmo acontece com uma global e uma local de mesmo nome:

#include <iostream>

int visitas = 100;

void mostrar() {
    int visitas = 5;
    std::cout << "local: " << visitas << "\n";
    std::cout << "global: " << ::visitas << "\n";
}

int main() {
    mostrar();
}
$ g++ -std=c++23 -Wall -Wextra -Wshadow sombraglobal.cpp -o sombraglobal
sombraglobal.cpp: In function ‘void mostrar()’:
sombraglobal.cpp:6:9: warning: declaration of ‘visitas’ shadows a global declaration [-Wshadow]
    6 |     int visitas = 5;
      |         ^~~~~~~
sombraglobal.cpp:3:5: note: shadowed declaration is here
    3 | int visitas = 100;
      |     ^~~~~~~
$ ./sombraglobal
local: 5
global: 100

O ::visitas (dois-pontos duplos sem nada antes) quer dizer "a visitas global". Funciona, mas é um remendo: o certo é dar nomes diferentes. O :: é o mesmo do std::cout, e tem aula própria: Namespaces.

Vale ligar o -Wshadow

Muita gente acrescenta -Wshadow ao comando de sempre. Ele às vezes avisa de sombras inofensivas, mas pega exatamente o tipo de erro do sombra.cpp, que nenhum outro aviso pega. A aula Avisos lista outras opções úteis.

Missão: este editor está compilando com -Wshadow. O programa deveria mostrar Preço com desconto: 90, mas mostra 100 e um aviso de sombra (no Clang: declaration shadows a local variable). Conserte a linha do desconto para mudar a variável de fora, sem criar outra.

#include <iostream>

int main() {
    double preco = 100;
    bool clienteFiel = true;
    if (clienteFiel) {
        double preco = 90;
    }
    std::cout << "Preço com desconto: " << preco << "\n";
}
#include <iostream>

int main() {
    double preco = 100;
    bool clienteFiel = true;
    if (clienteFiel) {
        preco = 90;
    }
    std::cout << "Preço com desconto: " << preco << "\n";
}

5. static local: a variável que lembra

A lojinha da Ana tem uma máquina de senhas: cada cliente que chega recebe o próximo número. Com uma variável local comum, isso não funciona, porque ela nasce de novo a cada chamada. Com a palavra static na frente, ela nasce uma vez só e guarda o valor entre as chamadas:

#include <iostream>

void atenderSemStatic() {
    int senha = 0;
    senha++;
    std::cout << "Senha " << senha << "\n";
}

void atender() {
    static int senha = 0;
    senha++;
    std::cout << "Senha " << senha << "\n";
}

int main() {
    std::cout << "Sem static:\n";
    atenderSemStatic();
    atenderSemStatic();
    atenderSemStatic();
    std::cout << "Com static:\n";
    atender();
    atender();
    atender();
}
$ g++ -std=c++23 -Wall -Wextra contador.cpp -o contador
$ ./contador
Sem static:
Senha 1
Senha 1
Senha 1
Com static:
Senha 1
Senha 2
Senha 3
int senha = 0;
Variável local comum: criada com 0 a cada chamada e destruída no fim. Sempre mostra 1.
static int senha = 0;
static ("estático", que fica parado): a variável é criada na primeira chamada, e o = 0 só acontece nessa vez. Nas chamadas seguintes, a linha é pulada, e a variável continua com o valor que tinha. Ela só é destruída quando o programa termina.

O melhor dos dois mundos: a senha dura como uma global, mas é visível só dentro de atender, como uma local. Ninguém de fora consegue estragá-la:

$ g++ -std=c++23 -Wall -Wextra staticfora.cpp -o staticfora      « o main tenta mostrar a senha
staticfora.cpp: In function ‘int main()’:
staticfora.cpp:11:18: error: ‘senha’ was not declared in this scope
   11 |     std::cout << senha << "\n";
      |                  ^~~~~
Tipo de variávelQuem enxergaQuanto tempo dura
Local (int x dentro de função ou bloco)Só o bloco onde foi criadaAté o } do bloco; recriada a cada vez
static local (static int x dentro de função)Só a funçãoDa primeira chamada até o fim do programa
Global (fora das funções)Todas as funções abaixo delaO programa inteiro
A palavra static tem outros usos

Numa variável dentro de uma função, static quer dizer "dura o programa todo", como aqui. Em outros lugares ela muda de sentido: fora das funções, deixa a global visível só naquele arquivo (Compilar Vários Arquivos); dentro de uma classe, cria um membro compartilhado (Membros static).


Quando dá errado

error: ‘x’ was not declared in this scope, mas a variável existe

O que significa: a variável existe, mas em outro território: dentro de outra função, dentro de um if/for que já fechou, ou é o parâmetro de outra função.

Como consertar: procure onde a variável foi criada e a que { } ela pertence. Se precisa dela depois do bloco, crie-a antes do bloco. Se o valor vem de outra função, traga-o com return.

O contador nunca passa de 1

O que significa: a variável foi criada dentro do laço ou da função, e nasce de novo, com o valor inicial, a cada volta ou chamada.

Como consertar: num laço, crie o contador antes do laço. Numa função que precisa lembrar entre chamadas, use static.

Mudei a variável dentro do if, mas fora dela o valor é o antigo

O que significa: provavelmente você escreveu o tipo na frente (int preco = 99;) e criou uma variável nova com o mesmo nome: sombreamento. Sem -Wshadow, não há aviso.

Como consertar: tire o tipo para mudar a variável existente (preco = 99;). Compile com -Wshadow para o g++ apontar casos assim: warning: declaration of ‘preco’ shadows a previous local.

Uma variável global muda de valor "sozinha"

O que significa: alguma função está mexendo nela, talvez sem querer, por usar o mesmo nome.

Como consertar: o melhor é eliminar a global: passe o valor por parâmetro e devolva com return (ou use &). Se precisa mesmo ser global e nunca muda, faça-a const.


Referência rápida

EscritaO que é
void f() { int x = 0; }x é local de f
if (…) { int y; }y só existe dentro do if
for (int i = 0; …) { }i só existe dentro do laço
int total = 0; fora das funçõesGlobal: evite (constantes globais tudo bem)
static int n = 0; dentro da funçãoCriada uma vez, lembra o valor entre chamadas
::nomeA variável global nome, mesmo com uma local de mesmo nome
-WshadowOpção do g++ que avisa quando um nome esconde outro

Exercícios

1. A palavra que faz lembrar

Complete para que a variável mantenha o valor entre as chamadas da função:

int chamadas = 0;

2. O que aparece?

int x = 1; { int x = 2; } std::cout << x;

3. A opção de aviso

Qual opção do g++ avisa quando uma variável esconde outra de mesmo nome?

4. Uma variável criada dentro do bloco de um if…

5. A função void f() { static int n = 0; n++; std::cout << n; } é chamada três vezes. O que aparece?

6. Por que o guia recomenda evitar variáveis globais?

Missão final: os pedidos da lojinha

Missão: complete a função registrarPedido para que cada pedido receba o próximo número, começando em 1. Use uma variável static dentro da função, sem variáveis globais e sem mexer no main. Saída esperada:

Pedido nº 1: caderno
Pedido nº 2: caneta
Pedido nº 3: mochila
#include <iostream>
#include <string>

void registrarPedido(const std::string& produto) {
    // crie aqui o contador de pedidos
    std::cout << "Pedido nº " << "?" << ": " << produto << "\n";
}

int main() {
    registrarPedido("caderno");
    registrarPedido("caneta");
    registrarPedido("mochila");
}
#include <iostream>
#include <string>

void registrarPedido(const std::string& produto) {
    static int numero = 0;
    numero++;
    std::cout << "Pedido nº " << numero << ": " << produto << "\n";
}

int main() {
    registrarPedido("caderno");
    registrarPedido("caneta");
    registrarPedido("mochila");
}

Resumo

  • Escopo é a parte do código onde uma variável existe. De dentro se enxerga o que está fora; de fora não se enxerga o que está dentro.
  • Variáveis locais (e parâmetros) existem só na função. Fora dela: ‘x’ was not declared in this scope.
  • Todo bloco { } cria um escopo: a variável do if, do for e do laço morre no }, e é recriada a cada volta.
  • Variáveis globais são vistas e mudadas por todos: evite. Constantes globais (const) são aceitáveis.
  • Sombreamento: uma variável de dentro com o mesmo nome esconde a de fora. Só o -Wshadow avisa.
  • static numa variável local faz ela ser criada uma vez e lembrar o valor entre chamadas, sem ficar visível fora da função.
  • Próxima aula: Recursão, uma função que chama a si mesma.