Aula 17 - C++ para Programadores C
Esta aula apresenta C++ para quem já programa em C, de forma direta e prática. O objetivo não é ensinar C++ do zero nem programação orientada a objetos — é mostrar os recursos da linguagem que tornam o código mais simples e seguro sem mudar radicalmente a forma de pensar. Tudo o que você sabe sobre ponteiros, structs, funções e alocação dinâmica continua válido; C++ apenas acrescenta ferramentas.
1. C++ vs. C: principais diferenças
C++ é um superconjunto de C: qualquer programa C válido pode ser compilado como C++ (com raras exceções). As diferenças mais relevantes para quem já sabe C são:
| Recurso | C | C++ | Seção |
|---|---|---|---|
| Entrada e saída | printf / scanf |
cout / cin |
Entrada e Saída |
| Valor booleano | int ou <stdbool.h> |
bool nativo |
Tipos Novos |
| Ponteiro nulo | NULL |
nullptr (preferido) |
Tipos Novos |
| Tipo inferido | — | auto |
Tipos Novos |
| String de texto | char[] + strcpy/strcmp |
std::string com + e == |
Texto com string |
| Vetor dinâmico | malloc + realloc manual |
std::vector<T> |
Vetores Dinâmicos |
| Iterar sobre coleção | for (int i = 0; i < n; i++) |
for (int x : v) |
Range-based for |
| Alterar variável em função | ponteiro (int*), chamada com & |
referência (int&), chamada sem & |
Referências |
| Alterar ponteiro em função | ponteiro para ponteiro (T**) |
referência para ponteiro (T*&) |
Modificando Ponteiros |
| Passar struct sem copiar | ponteiro constante (const T*) |
referência constante (const T&) |
Passagem Eficiente |
| Alocação dinâmica | malloc / free |
new / delete |
Alocação Dinâmica |
| Algoritmos (ordenar, buscar…) | implementar manualmente | sort, find, swap, min/max da STL |
STL Básica |
| Par de valores | struct com dois campos | std::pair<T1,T2> |
STL Básica |
| Funções com mesmo nome | não permitido | sobrecarga por tipo/quantidade de parâmetros | Sobrecarga de Funções |
| Usar nome de struct como tipo | struct Nodo* p; ou typedef |
Nodo* p; diretamente |
Structs em C++ |
| Acessar nomes da biblioteca | nomes globais (sem conflito) | std:: ou using namespace std |
Namespaces |
O compilador é chamado com g++ (em vez de gcc) e a extensão dos arquivos é .cpp. A linha de compilação típica é:
g++ -std=c++17 -Wall -o programa programa.cpp
| Parte | Significado |
|---|---|
g++ |
Compilador C++ do GCC. Em C compilávamos com gcc; para C++ usamos g++, que linka automaticamente a biblioteca padrão C++ (libstdc++) e trata o arquivo como C++. |
-std=c++17 |
Define o padrão da linguagem a usar. c++17 (2017) é uma boa escolha — inclui auto, range-based for, inicialização com {} e diversas melhorias. Sem essa flag o compilador pode usar um padrão antigo e rejeitar sintaxes modernas. |
-Wall |
Ativa os avisos (warnings) mais comuns. O compilador não para de compilar, mas aponta problemas potenciais: variáveis não inicializadas, comparações suspeitas, código que nunca executa etc. Sempre use — avisos ignorados viram bugs difíceis de encontrar. |
-o programa |
Define o nome do executável gerado. Sem essa flag o padrão é a.out (Linux/Mac) ou a.exe (Windows). Escolha um nome descritivo. |
programa.cpp |
Arquivo de entrada. Para projetos com vários arquivos, liste todos: g++ -std=c++17 -Wall -o prog main.cpp lista.cpp arvore.cpp. |
Exemplo completo do zero — arquivo ola.cpp:
#include <iostream> using namespace std; int main() { cout << "Olá, mundo!" << endl; return 0; }
Compilar e executar:
g++ -std=c++17 -Wall -o ola ola.cpp ./ola
Olá, mundo!
2. A STL — Standard Template Library ★
A STL (Standard Template Library) é a parte da biblioteca padrão C++ que fornece estruturas de dados e algoritmos genéricos prontos para uso. Em vez de implementar do zero um vetor dinâmico, uma lista ou uma rotina de ordenação, você inclui o cabeçalho correto e usa diretamente:
| Componente | O que oferece | Cabeçalho |
|---|---|---|
| Containers | Estruturas de dados: vector, list, forward_list, map, set, queue, stack… |
<vector>, <list>… |
| Algoritmos | Operações sobre containers: sort, find, count, min/max, swap… |
<algorithm> |
| Utilitários | Tipos auxiliares: string, pair, tuple… |
<string>, <utility>… |
Todos os nomes da STL vivem no namespace std (seção 3). Nesta aula, usamos principalmente vector, string e alguns algoritmos — o suficiente para substituir a maioria dos padrões manuais de C. Outros containers e algoritmos serão vistos em aulas posteriores.
Algoritmos prontos: <algorithm>
sort
#include <algorithm> vector<int> v = {5, 2, 8, 1, 9, 3}; sort(v.begin(), v.end()); // crescente: [1, 2, 3, 5, 8, 9]
Funciona também com arrays estáticos (sort(arr, arr + n)).
swap
int a = 3, b = 7; swap(a, b); // a == 7, b == 3 // Funciona com qualquer tipo, inclusive structs swap(v[0], v[1]);
find
vector<int> v = {10, 20, 30, 40}; auto it = find(v.begin(), v.end(), 30); if (it != v.end()) { cout << "Encontrado: " << *it << endl; // 30 } else { cout << "Não encontrado" << endl; }
find retorna um iterador: pense nele como um ponteiro para o elemento encontrado dentro do vetor. Se não encontrar, retorna v.end() (um iterador que aponta "além do último elemento"). Desreferenciá-lo com *it dá o valor.
min e max
int menor = min(3, 7); // 3 int maior = max(3, 7); // 7 // Funciona com qualquer tipo comparável double x = min(1.5, 2.3); string s = min(string("Ana"), string("Bruno")); // "Ana"
Utilitário: pair
pair<T1, T2> é uma struct simples que agrupa dois valores de tipos possivelmente diferentes. Útil para retornar dois valores de uma função ou como chave/valor:
#include <utility> pair<int, string> p = {42, "resposta"}; cout << p.first << endl; // 42 cout << p.second << endl; // resposta // make_pair cria um pair com inferência de tipos auto q = make_pair(3.14, true); // vector de pares vector<pair<int, int>> arestas; arestas.push_back({1, 2}); arestas.push_back({2, 3});
3. Namespaces e o operador ::
Em C++, todos os nomes da biblioteca padrão — cout, string, vector, sort, pair e centenas de outros — vivem dentro de um namespace chamado std. Um namespace é um agrupamento de nomes que evita conflitos: se você define uma função chamada sort no seu código, ela não colide com std::sort da STL.
O operador :: é o operador de escopo: acessa um nome dentro de um namespace (ou dentro de uma struct/classe, que você verá em OOP). Para usar qualquer coisa da biblioteca padrão, basta prefixar com std:::
// Usando std:: explicitamente — sem "using namespace std" std::cout << "oi" << std::endl; std::string nome = "Ana"; std::vector<int> v; std::sort(v.begin(), v.end()); std::pair<int, int> p = {1, 2};
A declaração using namespace std; importa todos os nomes de std para o escopo atual, eliminando a necessidade de prefixar:
// Com "using namespace std" — sem precisar de std:: using namespace std; cout << "oi" << endl; string nome = "Ana"; vector<int> v; sort(v.begin(), v.end());
using namespace std; é padrão em código didático e em programas pequenos. Em projetos grandes ou em arquivos de cabeçalho (.h), prefere-se std:: explícito para não "contaminar" o namespace global com centenas de nomes da biblioteca padrão — o que poderia causar conflitos inesperados com funções do seu próprio código. Nesta aula, usamos using namespace std; em todos os exemplos.
|
4. Structs em C++ — sem typedef
Em C, definir um tipo a partir de struct exigia typedef para poder usar o nome sem a palavra struct na frente. Em C++, o nome da struct já é automaticamente um tipo válido:
// C: sem typedef, é preciso escrever "struct" na frente struct Nodo { int dado; struct Nodo* prox; }; struct Nodo* p; // obrigatório em C (sem typedef) // C: com typedef (o que fazíamos na disciplina) typedef struct str_Nodo Nodo; struct str_Nodo { int dado; Nodo* prox; }; Nodo* p; // funciona graças ao typedef
// C++: funciona direto, sem typedef struct Nodo { int dado; Nodo* prox; }; // auto-referência também funciona Nodo* p = nullptr; // sem "struct", sem typedef Nodo n; // idem
Isso vale para struct, enum e union. Em C++, o typedef ainda existe e é válido, mas raramente necessário para structs simples.
5. Entrada e Saída com cin e cout
Em vez de printf/scanf, C++ usa os objetos de stream cout (saída) e cin (entrada), declarados em <iostream>. O operador << envia para o fluxo; >> lê.
#include <iostream> using namespace std; int main() { int a, b; cout << "Digite dois números: "; cin >> a >> b; cout << "Soma = " << a + b << endl; return 0; }
Diferença principal. scanf exige o endereço da variável (&x) e especificadores de formato (%d, %f). cin lê diretamente para a variável (cin >> x, sem &) e o tipo é deduzido automaticamente. cout não usa especificadores de formato — basta encadear os valores com <<.
|
endl emite uma quebra de linha e garante que o buffer seja descarregado. Para apenas a quebra de linha, pode-se usar "\n", que é mais rápido.
6. Tipos Novos: bool, nullptr e auto ★
bool é um tipo nativo em C++: valores true e false, sem necessidade de #include <stdbool.h>:
bool ativo = true; bool vazio = (n == 0); if (ativo) { /* ... */ }
nullptr é o literal de ponteiro nulo em C++ moderno. Prefira-o a NULL: NULL é apenas o inteiro 0, o que pode causar ambiguidades em sobrecarga de funções; nullptr tem tipo próprio (std::nullptr_t):
int* p = nullptr; // em vez de NULL ou 0 if (p != nullptr) { /* ponteiro válido */ }
auto instrui o compilador a deduzir o tipo da variável a partir do valor de inicialização. Evita repetição de tipos longos:
auto x = 42; // int auto y = 3.14; // double auto s = "oi"; // const char*
auto só funciona com variáveis que têm inicializador explícito — o compilador precisa do valor para deduzir o tipo. Não use auto quando o tipo não é óbvio de ler: a clareza do código vem primeiro.
|
7. Texto com string ★
Em C, texto é representado por char[] ou char*, exigindo funções manuais como strcpy, strcmp e strlen. Em C++, std::string é um tipo que gerencia a memória automaticamente e oferece operadores e métodos convenientes. Requer #include <string>:
#include <string> #include <iostream> using namespace std; int main() { string nome = "Ana"; string sobrenome = "Lima"; string completo = nome + " " + sobrenome; // concatenação com + cout << completo << endl; // Ana Lima cout << completo.length() << endl; // 8 cout << completo[0] << endl; // A (acesso por índice) if (nome == "Ana") { // comparação com == cout << "Encontrou!" << endl; } cin >> nome; // lê uma palavra (para com espaço) getline(cin, completo); // lê a linha inteira incluindo espaços return 0; }
| Operação | Com char[] (C) |
Com string (C++) |
|---|---|---|
| Copiar | strcpy(dst, src) |
dst = src |
| Comparar | strcmp(a, b) == 0 |
a == b |
| Concatenar | strcat(dst, src) |
dst + src |
| Tamanho | strlen(s) |
s.length() ou s.size() |
| String vazia | s[0] == '\0' |
s.empty() |
Para obter o char* equivalente (ex: para funções C), use s.c_str().
8. Vetores Dinâmicos com vector ★
std::vector<T> é um vetor dinâmico que cresce automaticamente conforme elementos são inseridos — o equivalente C++ a um array alocado com malloc e redimensionado com realloc, mas sem o trabalho manual. Requer #include <vector>.
#include <vector> #include <iostream> using namespace std; int main() { vector<int> v; // vetor vazio de inteiros v.push_back(10); // insere no final: [10] v.push_back(20); // [10, 20] v.push_back(30); // [10, 20, 30] cout << v.size() << endl; // 3 (número de elementos) cout << v[1] << endl; // 20 (acesso por índice) v.pop_back(); // remove o último: [10, 20] cout << v.empty() << endl; // 0 (falso, não está vazio) v.clear(); // remove todos: [] cout << v.empty() << endl; // 1 (verdadeiro) return 0; }
As principais operações são:
| Operação | Descrição | Complexidade |
|---|---|---|
push_back(x) |
Insere x no final |
O(1) amortizado |
pop_back() |
Remove o último elemento | O(1) |
v[i] |
Acesso por índice (sem verificação de bounds) | O(1) |
size() |
Número de elementos presentes | O(1) |
capacity() |
Capacidade alocada internamente (pode ser maior que size()) |
O(1) |
reserve(n) |
Pré-aloca espaço para pelo menos n elementos (evita reallocs) |
O(n) |
clear() |
Remove todos os elementos (capacity permanece) | O(n) |
empty() |
Retorna true se não há elementos |
O(1) |
capacity vs. size: quando o vetor precisa crescer, ele dobra a capacidade internamente para evitar realocar a cada push_back. Use reserve quando você sabe de antemão quantos elementos vai inserir:
vector<int> v; v.reserve(1000); // aloca espaço de uma vez — sem reallocs internos for (int i = 0; i < 1000; i++) v.push_back(i); cout << v.size() << endl; // 1000 cout << v.capacity() << endl; // pelo menos 1000
Vector de structs funciona naturalmente:
struct Produto { int cod; float preco; }; vector<Produto> lista; lista.push_back({1, 9.90f}); lista.push_back({2, 14.50f}); cout << lista[0].preco << endl; // 9.9
Inicialização e atribuição
vector<string> palavras = {"casa", "arvore", "janela"}; // inicialização
palavras = {"gato", "cachorro", "passaro"}; // atribuição — substitui todo o conteúdo
palavras.push_back("gato"); // adiciona um elemento ao final
palavras.clear(); // esvazia o vector (tamanho volta a 0)
A atribuição com = {...} é permitida a qualquer momento — ela substitui todo o conteúdo do vector existente, sem criar um novo.
Observação. A STL também oferece implementações prontas de listas encadeadas: std::list<T> (lista duplamente encadeada) e std::forward_list<T> (lista simplesmente encadeada), ambas em <list> e <forward_list>. Elas existem, mas na disciplina implementamos nossas próprias listas — o objetivo é entender como funcionam por dentro.
|
9. Iteração Simplificada com Range-based for ★
C++11 introduziu o range-based for, que itera sobre todos os elementos de uma coleção sem precisar de índice:
vector<int> v = {10, 20, 30, 40}; // cópia de cada elemento (não modifica o vetor) for (int x : v) { cout << x << endl; } // referência: modifica o vetor for (int& x : v) { x *= 2; } // const referência: lê sem copiar (eficiente para structs grandes) for (const Produto& p : lista) { cout << p.cod << " " << p.preco << endl; } // com auto: compilador deduz o tipo de x a partir de v (ver seção 6) for (auto& x : v) { x += 1; }
Funciona com arrays estáticos, string e qualquer container STL:
O que é STL. A Standard Template Library é a parte da biblioteca padrão C++ que fornece estruturas de dados e algoritmos genéricos prontos: vector, list, map, set, sort, find, pair etc. Todos os containers STL seguem a mesma convenção de interface — incluindo suporte a range-based for — o que torna o código intercambiável entre eles.
|
int arr[] = {1, 2, 3, 4, 5}; for (int x : arr) { cout << x << " "; } string nome = "Ana"; for (char c : nome) { cout << c << endl; }
10. Referências: Apelidos para Variáveis (T&) ★
Uma referência é um apelido para uma variável existente. Uma vez declarada, ela sempre se refere à mesma variável — não pode ser "reapontada" para outra, e não pode ser nula. Para fins práticos, pense nela como um ponteiro que já vem desreferenciado automaticamente:
int x = 10; int& r = x; // r é um apelido para x r = 20; // x agora vale 20 cout << x; // 20
O uso principal é em parâmetros de função. Compare a versão com ponteiro (C) e com referência (C++):
// C: ponteiro void dobra(int* x) { *x *= 2; } // chamada: dobra(&n); // C++: referência void dobra(int& x) { x *= 2; } // chamada: dobra(n); -- sem & na chamada!
Outro exemplo — troca de valores:
// C: com ponteiros void troca(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp; } // troca(&x, &y); // C++: com referências void troca(int& a, int& b) { int tmp = a; a = b; b = tmp; } // troca(x, y);
O compilador traduz referências para ponteiros internamente. void dobra(int& x) é, sob o capô, a mesma coisa que void dobra(int* x) com *x em todos os usos. A diferença é sintática: referências eliminam os * e & espalhados pelo código, tornando-o mais limpo. Ponteiros continuam necessários para listas encadeadas, alocação dinâmica e casos onde o "sem valor" (null) precisa ser representado.
|
11. Passagem Eficiente sem Cópia (const T&) ★
Para passar estruturas grandes para uma função sem copiá-las (eficiência) e sem permitir modificação (segurança), use const T&:
struct Produto { int cod; char nome[50]; float preco; }; // passa por valor: copia a struct inteira void imprime_v(Produto p) { cout << p.nome; } // passa por const referência: sem cópia, sem risco de modificar void imprime(const Produto& p) { cout << p.nome; } // chamada idêntica em ambos os casos: Produto x = {1, "Caneta", 2.50f}; imprime(x);
Regra prática: passe tipos primitivos (int, float, char) por valor. Passe structs e objetos maiores por const& quando não vai modificá-los, ou por & quando vai. Evite copiar desnecessariamente — especialmente dentro de loops.
|
12. Alocação Dinâmica com new e delete ★
Em C++, new substitui malloc e delete substitui free. As vantagens: não precisa de sizeof, não precisa de cast, e o tipo correto é retornado diretamente.
// C: malloc/free int* p = (int*) malloc(sizeof(int)); *p = 42; free(p); // C++: new/delete int* p = new int; *p = 42; delete p;
Para vetores, use new[] e delete[]:
// C: malloc/free para array int* v = (int*) malloc(10 * sizeof(int)); free(v); // C++: new[]/delete[] para array int* v = new int[10]; v[0] = 1; v[9] = 9; delete[] v; // [] obrigatório para arrays!
O mesmo com structs:
Nodo* n = new Nodo; n->dado = 10; n->prox = nullptr; delete n; int tam = 5; Nodo* arr = new Nodo[tam]; delete[] arr;
| Aloca com | Libera com |
|---|---|
new T | delete p |
new T[n] | delete[] p |
malloc(...) | free(p) |
Nunca misture os pares. new com free, ou malloc com delete, é comportamento indefinido — o compilador não acusa erro, mas o programa pode travar ou corromper memória. Usar delete (sem []) em um array alocado com new[] também é comportamento indefinido.
|
13. Sobrecarga de Funções
Em C, duas funções não podem ter o mesmo nome — se você quer imprimir um int e um float, precisa de nomes diferentes: imprime_int, imprime_float. Em C++, funções com o mesmo nome podem coexistir desde que tenham parâmetros diferentes (tipo ou quantidade). O compilador escolhe qual versão chamar com base nos argumentos passados — isso se chama sobrecarga de funções (function overload).
#include <iostream> #include <string> using namespace std; void imprime(int x) { cout << "inteiro: " << x << endl; } void imprime(double x) { cout << "double: " << x << endl; } void imprime(const string& s) { cout << "texto: " << s << endl; } void imprime(int x, int y) { cout << "par: (" << x << ", " << y << ")" << endl; } int main() { imprime(42); // chama imprime(int) imprime(3.14); // chama imprime(double) imprime("oi"); // chama imprime(const string&) imprime(1, 2); // chama imprime(int, int) return 0; }
inteiro: 42 double: 3.14 texto: oi par: (1, 2)
A sobrecarga é resolvida em tempo de compilação — o compilador analisa os tipos dos argumentos e seleciona a versão correta. Não há custo em tempo de execução.
Um exemplo mais prático: uma função maior que funciona para inteiros, doubles e strings sem precisar de nomes diferentes:
int maior(int a, int b) { if (a > b) return a; else return b; } double maior(double a, double b) { if (a > b) return a; else return b; } int maior(int a, int b, int c) { // versão com 3 argumentos return maior(maior(a, b), c); } int main() { cout << maior(3, 7) << endl; // 7 — chama maior(int, int) cout << maior(1.5, 2.3) << endl; // 2.3 — chama maior(double, double) cout << maior(4, 1, 9) << endl; // 9 — chama maior(int, int, int) return 0; }
| O que distingue as versões | Exemplo |
|---|---|
| Tipos diferentes dos parâmetros | f(int) vs. f(double) |
| Quantidade diferente de parâmetros | f(int, int) vs. f(int, int, int) |
| Combinação de tipo e quantidade | f(int) vs. f(int, double) |
| O tipo de retorno não conta. Duas funções com o mesmo nome e os mesmos parâmetros mas tipos de retorno diferentes causam erro de compilação — o compilador não consegue decidir qual chamar. Só os parâmetros distinguem as versões sobrecarregadas. |
Um exemplo direto do contexto desta disciplina: em C, as funções de inicialização de pilha e fila precisam ter nomes diferentes. Em C++, ambas podem se chamar inicializar:
// Em C: nomes obrigatoriamente diferentes void inicializa_pilha(PilhaEnc* p) { p->ini = NULL; } void inicializa_fila(FilaEnc* f) { f->frente = f->final = NULL; }
// Em C++: mesmo nome, parâmetros diferentes void inicializa(PilhaEnc& p) { p.ini = nullptr; } void inicializa(FilaEnc& f) { f.frente = f.final = nullptr; } int main() { PilhaEnc p; FilaEnc f; inicializa(p); // compilador escolhe inicializa(PilhaEnc&) inicializa(f); // compilador escolhe inicializa(FilaEnc&) return 0; }
O mesmo vale para qualquer operação que faça sentido nos dois TADs: estaVazia, tamanho, imprimir, etc. — um único nome para conceitos equivalentes em tipos diferentes.
14. Parâmetros com Valor Padrão (Default Parameters)
Em C++, é possível definir um valor padrão para um parâmetro. Se o chamador não passar aquele argumento, o valor padrão é usado automaticamente. Em C, a única alternativa seria criar funções separadas ou sempre passar o argumento explicitamente.
void imprimir(int valor, bool newline = true) { cout << valor; if (newline) cout << endl; } int main() { imprimir(42); // imprime 42 com quebra de linha (newline = true) imprimir(42, false); // imprime 42 sem quebra de linha return 0; }
Um exemplo mais próximo da disciplina: uma função de busca em lista que, por padrão, busca a partir do início:
// Em C: dois parâmetros obrigatórios sempre Nodo* buscar_C(Nodo* ini, int cod) { /* ... */ } // chamada: buscar_C(lista.ini, 10); // Em C++: o nodo de início tem padrão nullptr — caller pode omitir Nodo* buscar(ListaEnc& lista, int cod, Nodo* inicio = nullptr) { Nodo* cur; if (inicio != nullptr) cur = inicio; else cur = lista.ini; while (cur != nullptr) { if (cur->dado.cod == cod) return cur; cur = cur->prox; } return nullptr; } // chamadas válidas: buscar(lista, 10); // busca desde o início (inicio = nullptr) buscar(lista, 10, p); // busca a partir do nodo p
Regra: parâmetros com valor padrão devem sempre vir no final da lista de parâmetros. Não é possível ter um parâmetro sem padrão depois de um com padrão — o compilador não conseguiria saber quais argumentos foram omitidos.
// OK: padrões no final void f(int a, int b = 0, int c = 1); // ERRO: parâmetro sem padrão depois de um com padrão void g(int a = 0, int b); |
Parâmetros padrão e sobrecarga de funções são recursos complementares: a sobrecarga cria versões completamente diferentes para tipos distintos; o parâmetro padrão evita sobrecarregar quando a única diferença é um argumento opcional do mesmo tipo.
15. Arquivos com fstream
Em C, arquivos são manipulados com FILE*, fopen, fprintf, fscanf e fclose. Em C++, a biblioteca <fstream> oferece três tipos de stream que funcionam com os mesmos operadores << e >> que já usamos com cout e cin:
| Tipo | Uso | Equivalente em C |
|---|---|---|
ofstream |
Escrever em arquivo (output) | fopen("arq", "w") |
ifstream |
Ler de arquivo (input) | fopen("arq", "r") |
fstream |
Leitura e escrita | fopen("arq", "r+") |
Escrevendo em arquivo
#include <fstream> #include <string> using namespace std; int main() { ofstream arq("saida.txt"); // abre para escrita (cria ou sobrescreve) if (!arq) { // verifica se abriu com sucesso cout << "Erro ao abrir arquivo" << endl; return 1; } arq << "Linha 1" << endl; arq << "Valor: " << 42 << endl; arq << "Pi: " << 3.14 << endl; arq.close(); // opcional — fecha automaticamente ao sair do escopo return 0; }
Lendo de arquivo
#include <fstream> #include <string> using namespace std; int main() { ifstream arq("entrada.txt"); if (!arq) { cout << "Arquivo não encontrado" << endl; return 1; } // ler palavra por palavra string palavra; while (arq >> palavra) { cout << palavra << endl; } return 0; }
// ler linha por linha (preserva espaços) string linha; while (getline(arq, linha)) { cout << linha << endl; }
// ler valores tipados diretamente int cod; float preco; while (arq >> cod >> preco) { cout << cod << " → R$ " << preco << endl; }
Comparativo C vs. C++
// C FILE* f = fopen("saida.txt", "w"); if (f == NULL) { /* erro */ } fprintf(f, "Valor: %d\n", 42); fclose(f); // C++ ofstream f("saida.txt"); if (!f) { /* erro */ } f << "Valor: " << 42 << endl; // close() automático ao fim do escopo
Fechamento automático: quando um ofstream ou ifstream sai de escopo (fim da função, fim do bloco), o arquivo é fechado automaticamente pelo destrutor — sem necessidade de fclose. Chamar .close() manualmente ainda é útil quando você quer reutilizar o objeto ou garantir que os dados foram gravados antes do fim da função.
|
16. Modificando Ponteiros de Dentro de Funções (T*&)
Em C, para uma função alterar o próprio ponteiro do chamador — por exemplo, mudar o início de uma lista ou a raiz de uma árvore — usamos ponteiro para ponteiro (T**). Em C++, usamos referência para ponteiro (T*&), que tem o mesmo efeito com sintaxe mais limpa.
Definição da função:
struct Nodo { int dado; Nodo* prox; }; // C: recebe ponteiro para ponteiro — precisa de * em toda reatribuição void insere_ini_C(Nodo** lista, int val) { Nodo* novo = (Nodo*) malloc(sizeof(Nodo)); novo->dado = val; novo->prox = *lista; // desreferencia para ler o ponteiro atual *lista = novo; // desreferencia para reatribuir } // C++: recebe referência para ponteiro — usa o ponteiro diretamente void insere_ini(Nodo*& lista, int val) { Nodo* novo = new Nodo; novo->dado = val; novo->prox = lista; // lê o ponteiro atual sem desreferenciar lista = novo; // reatribui sem desreferenciar }
Chamada:
Nodo* lista = nullptr; // C: precisa passar o endereço do ponteiro com & insere_ini_C(&lista, 10); // C++: passa o ponteiro diretamente — o & está na assinatura, não na chamada insere_ini(lista, 10);
Ideia-chave. Nodo*& é uma referência para um ponteiro para Nodo — leia de trás para frente. A função pode reatribuir lista e a mudança é visível no chamador, sem precisar de ** nem de & na chamada. Use sempre que precisar mudar o início de uma lista ou a raiz de uma árvore de dentro de uma função.
|
17. Boas práticas de C++ moderno
| Situação | Preferência |
|---|---|
| Vetor dinâmico simples | Use vector<T> em vez de malloc + realloc manual. O vector gerencia a memória, nunca vaza, e redimensiona automaticamente. |
| Texto | Use string em vez de char* quando não precisar de compatibilidade C. Elimina strcpy, buffer overflows e gerenciamento manual. |
| Ponteiro nulo | Use nullptr em vez de NULL ou 0. Mais seguro em sobrecarga de funções e mais expressivo. |
| Structs grandes em parâmetros | Passe por const T& (somente leitura) ou T& (modifica), não por valor. Evita cópias desnecessárias. |
new/delete |
Evite quando possível — prefira vector e string. Quando necessário (estruturas encadeadas, árvores), sempre faça o delete correspondente e nunca misture com malloc/free. |
| Inicialização | Inicialize variáveis na declaração (int x = 0;, Nodo* p = nullptr;). Variáveis não inicializadas causam bugs difíceis de encontrar. |
18. Exemplo de migração: implementação C → C++
A mesma TAD de lista encadeada de alunos, implementada em C e depois em C++. Os comentários indicam a seção desta aula que explica cada mudança.
Versão C
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct str_Nodo { int cod; char nome[50]; float nota; struct str_Nodo* prox; } Nodo; int estaVazia(const Nodo* ini) { return ini == NULL; } void inicializar(Nodo** ini) { *ini = NULL; } void inserir(Nodo** ini, int cod, const char* nome, float nota) { Nodo* novo = (Nodo*) malloc(sizeof(Nodo)); novo->cod = cod; strcpy(novo->nome, nome); novo->nota = nota; novo->prox = *ini; *ini = novo; } void imprimir(const Nodo* ini) { const Nodo* cur = ini; while (cur != NULL) { printf("[%d] %s: %.1f\n", cur->cod, cur->nome, cur->nota); cur = cur->prox; } } int main() { Nodo* lista; inicializar(&lista); inserir(&lista, 1, "Ana", 9.0f); inserir(&lista, 2, "Bruno", 7.5f); inserir(&lista, 3, "Carol", 8.2f); if (estaVazia(lista)) printf("Vazia? sim\n"); else printf("Vazia? nao\n"); imprimir(lista); return 0; }
Versão C++ — com tudo que vimos
#include <iostream> #include <string> // ★ §7: string em vez de char[] #include <vector> // ★ §8: vetor dinâmico #include <algorithm> // §2: sort using namespace std; // §3: sem std:: em todo lugar struct Nodo { // §4: sem typedef int cod; string nome; // ★ §7: char nome[50] → string float nota; Nodo* prox; }; bool estaVazia(const Nodo* ini) { return ini == nullptr; } // §6: bool, nullptr void inicializar(Nodo*& ini) { ini = nullptr; } // §16: T*& em vez de T** void atualizarNota(Nodo& n, float nova) { // ★ §10: T& — modifica sem ponteiro n.nota = nova; } void inserir(Nodo*& ini, int cod, // §16: T*& modifica o início const string& nome, // ★ §11: const string& — sem cópia float nota = 0.0f) { // §14: parâmetro com valor padrão Nodo* novo = new Nodo; // ★ §12: new em vez de malloc novo->cod = cod; novo->nome = nome; // ★ §7: = direto, sem strcpy novo->nota = nota; novo->prox = ini; ini = novo; } void imprimir(const Nodo* ini) { // ★ §11: const T* — lê sem copiar for (const Nodo* cur = ini; cur != nullptr; cur = cur->prox) cout << "[" << cur->cod << "] " << cur->nome // §5: cout << ": " << cur->nota << "\n"; } void imprimir(const Nodo* ini, int max) { // §13: sobrecarga — mesmo nome, parâm extra int i = 0; for (const Nodo* cur = ini; cur != nullptr && i < max; cur = cur->prox, i++) cout << cur->nome << " (" << cur->nota << ")\n"; } void imprimirOrdenado(const Nodo* ini) { vector<string> nomes; // ★ §8: vetor dinâmico for (const Nodo* cur = ini; cur != nullptr; cur = cur->prox) nomes.push_back(cur->nome); sort(nomes.begin(), nomes.end()); // §2: sort alfabético for (const auto& n : nomes) // ★ §9: range-based for; §6: auto cout << n << "\n"; } void liberar(Nodo*& ini) { // §16: T*& while (ini != nullptr) { Nodo* prox = ini->prox; delete ini; // ★ §12: delete em vez de free ini = prox; } } int main() { Nodo* lista; inicializar(lista); // §16: sem &lista — T*& cuida inserir(lista, 1, "Ana", 9.0f); inserir(lista, 2, "Bruno", 7.5f); inserir(lista, 3, "Carol"); // §14: nota usa default 0.0f if (estaVazia(lista)) cout << "Vazia? sim\n"; else cout << "Vazia? nao\n"; cout << "\nLista:\n"; imprimir(lista); // §13: chama imprimir(Nodo*) cout << "\nPrimeiros 2:\n"; imprimir(lista, 2); // §13: chama imprimir(Nodo*, int) atualizarNota(*lista, 10.0f); // ★ §10: *lista é Nodo& cout << "\nNomes em ordem alfabética:\n"; imprimirOrdenado(lista); liberar(lista); // ★ §12: libera com delete return 0; }
O que mudou
| O que mudou | C | C++ | |
|---|---|---|---|
| Definição da struct | typedef struct str_Nodo {...} Nodo |
struct Nodo {...} |
§4 |
| Campo texto | char nome[50] + strcpy |
string nome + = |
★§7 |
| Ponteiro nulo | NULL |
nullptr |
§6 |
| Booleano | int (retorna 0/1) |
bool (retorna true/false) |
§6 |
| Acesso à biblioteca | nomes globais sem prefixo | using namespace std |
§3 |
| Modificar ponteiro em função | Nodo** + *ini = |
Nodo*& + ini = |
§16 |
| Modificar variável em função | T* + desreferenciação |
T& + uso direto |
★§10 |
| Parâmetro texto sem cópia | const char* |
const string& |
★§11 |
| Valor padrão em parâmetro | — | float nota = 0.0f |
§14 |
| Alocação | malloc(sizeof(Nodo)) |
new Nodo |
★§12 |
| Liberação | free(p) |
delete p |
★§12 |
| Saída formatada | printf("[%d] %s: %.1f\n", ...) |
cout << "[" << ... << "\n" |
§5 |
| Coleção auxiliar | array + contador manual | vector<string> |
★§8 |
| Ordenar | implementar manualmente | sort() da STL |
§2 |
| Iterar coleção | for (int i = 0; i < n; i++) |
for (const auto& n : v) |
★§9 |
| Funções com mesmo nome | nomes obrigatoriamente diferentes | sobrecarga de funções | §13 |
Para esta disciplina: as estruturas encadeadas (lista, pilha, fila, árvore) continuam sendo implementadas com ponteiros e new/delete — não se usa std::list, std::stack etc. O que muda é que você pode usar string, vector, referências e cout/cin como auxílio, tornando o código mais limpo e seguro sem abrir mão do controle sobre a estrutura.
|
Exercícios
-
Reescreva a função abaixo em C++ usando referência em vez de ponteiro. Mostre também a chamada:
void incrementa(int* x, int* y) { (*x)++; (*y) += 2; } // chamada: incrementa(&a, &b);
-
Escreva uma função
somaVetorque receba umvector<int>porconst&e retorne a soma de todos os seus elementos. Use range-basedfor. -
Dada a struct abaixo, crie um
vector<Aluno>com 3 alunos, ordene-o pelo camponotaem ordem decrescente usandosortcom um critério personalizado, e imprima os nomes em ordem:struct Aluno { string nome; float nota; };
-
Explique a diferença entre os três parâmetros abaixo e quando usar cada um:
void f1(Produto p); // (a) void f2(Produto& p); // (b) void f3(const Produto& p); // (c)
-
Escreva uma função
insereInicioque receba umaNodo*&(referência para ponteiro) e um inteiro, e insira um novo nodo no início de uma lista encadeada simples. Mostre a chamada. -
Reescreva o trecho C abaixo em C++, substituindo
char[]+strcpy/strcat/printfpelas equivalentes C++:char saudacao[60]; strcpy(saudacao, "Ola, "); strcat(saudacao, nome); printf("%s!\n", saudacao);
-
O código a seguir usa
std::em todo lugar etypedef struct. Simplifique-o usandousing namespace stde a sintaxe C++ de struct:typedef struct { std::string nome; int idade; } Pessoa; void imprime(const Pessoa& p) { std::cout << p.nome << " tem " << p.idade << " anos\n"; }
-
Reescreva o trecho C abaixo em C++, usando
bool,nullptrecin/coutno lugar deint,NULLescanf/printf:int encontrou = 0; Nodo* cur = lista; while (cur != NULL && !encontrou) { if (cur->dado == alvo) encontrou = 1; cur = cur->prox; } if (encontrou) printf("Achou!\n");
-
Reescreva o trecho abaixo trocando
malloc/freepornew[]/delete[]. Por que não se usadelete(sem colchetes) neste caso?int* v = (int*) malloc(10 * sizeof(int)); // ... usa v ... free(v);
-
Escreva duas sobrecargas da função
valorAbsoluto: uma parainte outra paradouble, ambas retornando o valor absoluto do argumento sem usar o operador?:. -
As duas funções abaixo fazem quase a mesma coisa. Una-as em uma única função usando parâmetro com valor padrão:
void imprimeLista(Nodo* ini) { /* imprime todos os nodos */ } void imprimeListaMax(Nodo* ini, int max) { /* imprime no máximo max nodos */ }
Mostre como ficam as duas chamadas após a unificação. -
Escreva um programa completo que leia inteiros de um arquivo
"numeros.txt"(um por linha) e imprima a soma no terminal. Useifstreame o operador>>. -
O código abaixo compila mas tem três problemas de boas práticas de C++ moderno. Identifique-os:
void dobra(int* x) { *x = *x * 2; } int main() { int* p = (int*) malloc(sizeof(int)); *p = 5; dobra(p); cout << *p << endl; }
-
Reescreva o loop abaixo usando range-based
foreauto:vector<string> palavras = {"casa", "arvore", "janela"}; for (int i = 0; i < (int)palavras.size(); i++) cout << palavras[i] << "\n";
Sugestões de Respostas dos Exercícios
Exercício 1
void incrementa(int& x, int& y) { x++; y += 2; } // chamada: incrementa(a, b); -- sem &
Exercício 2
int somaVetor(const vector<int>& v) { int soma = 0; for (int x : v) soma += x; return soma; }
Exercício 3
vector<Aluno> alunos = {
{"Carlos", 7.5f},
{"Ana", 9.0f},
{"Bruno", 8.2f}
};
sort(alunos.begin(), alunos.end(), [](const Aluno& a, const Aluno& b) {
return a.nota > b.nota; // decrescente
});
for (const Aluno& a : alunos)
cout << a.nome << endl;
// Ana, Bruno, Carlos
Exercício 4
(a) void f1(Produto p): passa por valor — a função recebe uma cópia completa. Alterações dentro de f1 não afetam o original. Use quando o tipo é pequeno ou quando você quer modificar a cópia sem alterar o original.
(b) void f2(Produto& p): passa por referência — a função acessa o objeto original e pode modificá-lo. Use quando quer alterar o objeto de quem chamou.
(c) void f3(const Produto& p): passa por referência constante — a função acessa o objeto original sem copiá-lo, mas não pode modificá-lo. Use para leitura eficiente de estruturas grandes (evita cópia sem abrir risco de modificação acidental). É a opção preferida para objetos somente-leitura.
Exercício 5
struct Nodo { int dado; Nodo* prox; }; void insereInicio(Nodo*& lista, int val) { Nodo* novo = new Nodo; novo->dado = val; novo->prox = lista; lista = novo; // modifica o ponteiro original } int main() { Nodo* lista = nullptr; insereInicio(lista, 10); // sem & na chamada insereInicio(lista, 20); // lista: 20 → 10 → nullptr return 0; }
Exercício 6
string saudacao = "Ola, "; saudacao += nome; // strcat → += cout << saudacao << "!\n"; // printf → cout
Exercício 7
using namespace std; struct Pessoa { // sem typedef, sem tag separada string nome; int idade; }; void imprime(const Pessoa& p) { cout << p.nome << " tem " << p.idade << " anos\n"; }
Exercício 8
bool encontrou = false; Nodo* cur = lista; while (cur != nullptr && !encontrou) { if (cur->dado == alvo) encontrou = true; cur = cur->prox; } if (encontrou) cout << "Achou!\n";
Exercício 9
int* v = new int[10]; // ... usa v ... delete[] v;
delete (sem colchetes) libera um único objeto alocado com new. Para arrays alocados com new[] é necessário delete[] — do contrário o comportamento é indefinido e os elementos além do primeiro não são destruídos corretamente.
Exercício 10
int valorAbsoluto(int x) { if (x < 0) return -x; else return x; } double valorAbsoluto(double x) { if (x < 0.0) return -x; else return x; } // o compilador escolhe a versão correta conforme o tipo do argumento
Exercício 11
void imprimeLista(Nodo* ini, int max = -1) { int i = 0; for (Nodo* cur = ini; cur != nullptr; cur = cur->prox) { if (max != -1 && i >= max) break; cout << cur->dado << "\n"; i++; } } imprimeLista(lista); // imprime todos — max usa padrão -1 imprimeLista(lista, 3); // imprime no máximo 3
Exercício 12
#include <fstream> #include <iostream> using namespace std; int main() { ifstream arq("numeros.txt"); int soma = 0, x; while (arq >> x) soma += x; cout << "Soma: " << soma << "\n"; return 0; } // arq fecha automaticamente ao sair de escopo
Exercício 13
Os três problemas são:
void dobra(int* x)deveria servoid dobra(int& x)— o parâmetro nunca énullptr, então referência é mais limpo que ponteiro; a chamada também fica mais simples (dobra(p)em vez dedobra(&p)).malloc(sizeof(int))deveria sernew int— não misturarmalloc/freecom código C++.- Falta
delete p;antes de terminar omain— vazamento de memória.
// versão corrigida: void dobra(int& x) { x = x * 2; } int main() { int* p = new int; *p = 5; dobra(*p); cout << *p << endl; delete p; return 0; }
Exercício 14
for (const auto& p : palavras) cout << p << "\n";
const auto&: o compilador deduz o tipo como const string& — sem cópia, sem possibilidade de modificação acidental.