A Filosofia por Trás do C++ e a Saída Elegante do printf

A Filosofia por Trás do C++ e a Saída Elegante do printf

Três detalhes aparecem na primeira linha de qualquer programa C++: o iostream no lugar do stdio.h, o cout no lugar do printf e o std:: grudado em tudo. Por trás deles está a filosofia da linguagem — verificação de tipo na compilação em vez de uma string de formato que ninguém confere, e nomes em namespaces.
Linguagem C++

12 min de leitura

No artigo O Mapa da Jornada: de C a C++ em 52 Passos desenhamos o mapa e compilamos um "Olá, C++!" quase sem explicar por quê. Prometi que a partir daqui todo código roda de verdade e que começaríamos pelo que muda de imediato ao sair do C. Cumpro a promessa aqui atacando três coisas que você vê logo na primeira linha de qualquer programa C++: o #include <iostream> no lugar do <stdio.h>, o std::cout no lugar do printf, e esse prefixo std:: que aparece grudado em quase tudo. Por trás desses três detalhes está a filosofia inteira da linguagem — e entendê-la agora vai poupar você de muita confusão adiante.

Por que o C++ existe (e no que ele discorda do C)

O C foi projetado para ser uma camada finíssima sobre o hardware: o que você escreve é quase exatamente o que a máquina executa. Isso o torna previsível e rápido, mas joga toda a responsabilidade nas suas costas — é você quem libera memória, verifica tipos e evita erros. O C++ nasceu da tese de Bjarne Stroustrup de que dava para manter essa performance de C e, ao mesmo tempo, deixar o compilador trabalhar mais a seu favor: pegar erros antes de o programa rodar, deixar os recursos se auto-gerenciarem, e permitir abstrações que não custam nada em tempo de execução. Essa última ideia tem nome — zero-overhead abstraction — e é o lema da casa: você não paga pelo que não usa, e o que usa é tão eficiente quanto se tivesse escrito à mão em C. Guarde essa promessa; ela explica por que tantos recursos "de alto nível" que veremos (ponteiros inteligentes, iteradores, lambdas) não deixam o programa mais lento.

Streams: o fim da string de formato

Em C, você imprime assim:

#include <stdio.h>

int main(void) {
    int idade = 30;
    printf("Idade: %d\n", idade);  // você promete que 'idade' é um int
    return 0;
}

O %d é uma promessa que você faz ao compilador: "aqui vai um int". Se você mentir — passar um double, ou trocar a ordem dos argumentos — o compilador raramente reclama, e o programa imprime lixo ou quebra. Em C++, a saída é feita por streams, e a promessa some:

#include <iostream>   // std::cout, std::cin, std::endl vivem aqui

int main() {
    int idade = 30;
    // O operador << descobre SOZINHO que 'idade' é int e imprime como int.
    // Sem string de formato, sem chance de descasar tipo e argumento.
    std::cout << "Idade: " << idade << '\n';
    return 0;
}

O std::cout é o fluxo de saída padrão (equivalente ao stdout), e << é o operador de inserção: ele "empurra" o valor para dentro do fluxo. A mágica é que << está sobrecarregado para cada tipo — existe uma versão para int, outra para double, outra para std::string — e o compilador escolhe a certa em tempo de compilação, olhando o tipo real do dado. Você nunca mais descasa %d de um double. A contrapartida honesta: a formatação numérica fina (casas decimais, alinhamento, preenchimento) é mais verbosa com streams do que com printf, e voltaremos a ela quando fizer diferença.

A entrada é simétrica. Onde em C você usava scanf("%d", &idade) — com aquele & obrigatório e fácil de esquecer —, em C++ você usa std::cin com o operador >>:

#include <iostream>

int main() {
    int idade;
    std::cout << "Digite sua idade: ";
    // >> lê da entrada e escreve direto na variável.
    // Repare: NÃO precisa do & que o scanf exigia.
    std::cin >> idade;
    std::cout << "Ano que vem você terá " << idade + 1 << " anos.\n";
    return 0;
}

Some o &. Some a string de formato. O std::cin >> idade sabe que idade é um int e lê um inteiro. Se o usuário digitar texto em vez de número, o stream entra num estado de erro em vez de corromper a memória — tratar esse estado é assunto de uma aula futura, mas note desde já que o default aqui é falhar com segurança, não com um comportamento indefinido.

Namespaces e o mistério do std::

Falta explicar esse std:: colado em cout, cin e endl. Em C, todos os nomes vivem num único saco global: se a biblioteca padrão tem uma função count e você também cria uma count, há conflito, e o remédio clássico era prefixar tudo à mão (minhalib_count, outralib_count). O C++ resolve isso com namespaces: gavetas nomeadas onde os nomes moram. Toda a biblioteca padrão vive na gaveta chamada std, e std::cout significa literalmente "o cout que está dentro de std". O :: é o operador de resolução de escopo — ele diz "procure este nome dentro daquela gaveta".

Você pode pedir para abrir a gaveta e não repetir o prefixo:

#include <iostream>
using namespace std;   // traz TODOS os nomes de std para o escopo atual

int main() {
    cout << "Sem prefixo agora\n";  // funciona, mas...
    return 0;
}

Isso compila e muita gente faz — mas eu vou te pedir para não fazer, e a honestidade sobre o trade-off é o ponto. O using namespace std; despeja centenas de nomes no seu escopo, e um deles pode colidir com algo seu (a STL tem count, distance, swap, data... nomes tão comuns que você provavelmente já usou). Em programas pequenos raramente dói; em código real e em cabeçalhos, é fonte de bugs sutis. O meio-termo idiomático é abrir só o que você usa:

#include <iostream>

int main() {
    using std::cout;   // traz apenas 'cout' para este escopo
    cout << "Só o cout foi importado\n";
    // std::cin continua exigindo o prefixo — o que é bom: fica explícito
    return 0;
}

Ao longo da série eu vou manter o std:: explícito na maioria dos exemplos. Não é preciosismo: escrever std::sort deixa claro para quem lê que aquele sort é o da biblioteca padrão, e não uma função sua. Clareza vale mais que economia de cinco caracteres.

Juntando tudo

Um programa completo que lê dois números e imprime a soma reúne as três ideias — streams de entrada e saída, e o namespace std — num exemplo que já é C++ idiomático, não C disfarçado:

#include <iostream>

int main() {
    std::cout << "Programa da soma. Digite dois inteiros separados por espaço: ";

    int a, b;
    std::cin >> a >> b;   // o >> pode ser encadeado: lê a, depois b

    // A saída também encadeia: texto, número, texto, número...
    std::cout << a << " + " << b << " = " << (a + b) << '\n';

    return 0;
}

Compile e rode com a linha que fixamos no artigo O Mapa da Jornada: de C a C++ em 52 Passos:

g++ -std=c++20 -Wall -Wextra soma.cpp -o soma
./soma
# Digite: 7 5
# Saída: 7 + 5 = 12

Nenhum &, nenhum %d, nenhum prefixo artesanal em nomes. É pouco código, mas cada linha marca uma diferença concreta com o C que você já conhecia.

A troca do printf pelo cout não é questão de gosto. A string de formato é um contrato que o compilador do C não conferia, e cada %d desalinhado com o argumento virava comportamento indefinido — daquele tipo que funciona na sua máquina e corrompe a pilha na do cliente. O stream move essa verificação para a compilação. E o std:: que incomoda no começo é o preço de nomes que não colidem, em vez do prefixo manual que o C obrigava. Os dois são a mesma ideia: o compilador trabalhando mais para você errar menos.

Fontes e leituras recomendadas

  • Bjarne Stroustrup, A Tour of C++ (3ª ed.), Capítulo 1 ("The Basics"): apresenta streams e namespaces exatamente no espírito desta aula, direto do criador da linguagem.
  • cppreference.com/w/cpp/io/cout e /w/cpp/io/cin: a documentação precisa dos dois streams que usamos hoje, com todas as sobrecargas do operador.
  • cppreference.com/w/cpp/language/namespace: referência completa sobre namespaces, o operador :: e as formas do using.
  • ISO C++ Core Guidelines, regra SF.7 ("Don't write using namespace at global scope in a header file"): a justificativa oficial para o conselho que dei sobre evitar o using namespace std;.
  • Herb Sutter & Andrei Alexandrescu, C++ Coding Standards: item sobre uso de namespaces e por que o prefixo explícito melhora a legibilidade em bases de código reais.

Exercícios

Exercício 1

Escreva um programa que peça o nome do usuário e o cumprimente, imprimindo "Olá, <nome>!". Use std::string para guardar o nome (basta incluir <string>) e std::cin >> nome para ler uma palavra.

Ver resposta

✓ Resposta: A leitura com >> pega apenas uma palavra (para no primeiro espaço), o que basta aqui:

#include <iostream>
#include <string>          // necessário para std::string

int main() {
    std::string nome;
    std::cout << "Qual é o seu nome? ";
    std::cin >> nome;      // lê uma palavra
    std::cout << "Olá, " << nome << "!\n";
    return 0;
}

Se você digitar "Maria Silva", só "Maria" será lido — porque >> para no espaço. Para ler a linha inteira existe std::getline, que veremos com calma na aula de strings.

Exercício 2

Adapte o programa da soma para também imprimir a diferença, o produto e a divisão inteira de a por b, tudo em linhas separadas, num único encadeamento por linha.

Ver resposta

✓ Resposta: Basta encadear cada operação em sua linha. Atenção à divisão por zero:

#include <iostream>

int main() {
    std::cout << "Digite dois inteiros: ";
    int a, b;
    std::cin >> a >> b;

    std::cout << "Soma:      " << (a + b) << '\n';
    std::cout << "Diferença: " << (a - b) << '\n';
    std::cout << "Produto:   " << (a * b) << '\n';
    if (b != 0)
        std::cout << "Divisão:   " << (a / b) << '\n';
    else
        std::cout << "Divisão:   indefinida (b = 0)\n";
    return 0;
}

Note que dividir inteiro por zero é comportamento indefinido em C++, exatamente como em C — por isso a checagem. O reflexo de C continua valendo aqui.

Exercício 3

Corrija o código a seguir, que não compila, e explique o erro:

#include <iostream>
int main() {
    cout << "Testando" << endl;
    return 0;
}
Ver resposta

✓ Resposta: O código não compila porque cout e endl vivem no namespace std e não há prefixo nem using. Duas correções válidas:

// Opção A: prefixar (recomendada)
#include <iostream>
int main() {
    std::cout << "Testando" << std::endl;
    return 0;
}
// Opção B: importar os nomes usados
#include <iostream>
int main() {
    using std::cout;
    using std::endl;
    cout << "Testando" << endl;
    return 0;
}

O erro do compilador será algo como 'cout' was not declared in this scope — a pista clássica de que faltou o std::.

Exercício 4

O programa abaixo tem um comportamento perigoso quando o usuário digita uma letra em vez de um número. Compile-o, digite abc quando ele pedir o número, e descreva o que acontece. (Ainda não vamos consertar — só observar.)

#include <iostream>
int main() {
    int n;
    std::cout << "Digite um número: ";
    std::cin >> n;
    std::cout << "Você digitou: " << n << '\n';
    return 0;
}
Ver resposta

✓ Resposta: Ao digitar abc, o std::cin >> n falha: como n é int e a entrada não é um número, o stream entra em estado de falha e não altera n. Como n foi declarado sem inicializar, ele contém lixo — e o programa imprime esse valor indeterminado (frequentemente 0 em muitos compiladores, mas isso não é garantido). O ponto pedagógico é duplo: primeiro, o C++ falhou com segurança (não corrompeu a memória, apenas não leu); segundo, uma variável não inicializada em C++ é tão perigosa quanto em C. Um bom hábito, que adotaremos, é sempre inicializar: int n = 0;. Como detectar e tratar o estado de falha do stream fica para depois.

Exercício 5

Reescreva o exercício 1 usando using std::cout; e using std::cin; no início do main, sem prefixar as chamadas, mas mantendo std::string com prefixo. Isso compila? Por quê?

Ver resposta

✓ Resposta: Sim, compila. O using std::cout; e using std::cin; trazem esses dois nomes específicos para o escopo do main, então você pode chamá-los sem prefixo; e std::string, que você não importou, continua exigindo o std:::

#include <iostream>
#include <string>

int main() {
    using std::cout;
    using std::cin;

    std::string nome;      // ainda com prefixo, pois não foi importado
    cout << "Qual é o seu nome? ";
    cin >> nome;
    cout << "Olá, " << nome << "!\n";
    return 0;
}

Isso ilustra o meio-termo idiomático: importar por nome o que você usa muito, mantendo o resto explícito. É mais seguro que using namespace std; e mais enxuto que prefixar tudo.

Comentários

Mais em Linguagem C++

O Cadeado que Protege — std::mutex e lock_guard
O Cadeado que Protege — std::mutex e lock_guard

O cadeado resolve a corrida serializando o acesso, e o lock_guard garante que…

Tarefas em Segundo Plano e a Arte de Esperar — std::async e condition_variable
Tarefas em Segundo Plano e a Arte de Esperar — std::async e condition_variable

Quase sempre o que se quer não é gerenciar uma thread, e sim disparar uma…

A Disciplina do const — Promessas que o Compilador Cobra
A Disciplina do const — Promessas que o Compilador Cobra

Em C o const é etiqueta modesta, que muita gente ignora sem prejuízo. Em C++…