INF01203 - Estruturas de Dados - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



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
  2. A STL — Standard Template Library ★
  3. Namespaces e o operador ::
  4. Structs em C++ — sem typedef
  5. Entrada e Saída com cin e cout
  6. Tipos Novos: bool, nullptr e auto ★
  7. Texto com string ★
  8. Vetores Dinâmicos com vector ★
  9. Iteração Simplificada com Range-based for ★
  10. Referências: Apelidos para Variáveis (T&) ★
  1. Passagem Eficiente sem Cópia (const T&) ★
  2. Alocação Dinâmica com new e delete ★
  3. Sobrecarga de Funções
  4. Parâmetros com Valor Padrão (Default Parameters)
  5. Arquivos com fstream
  6. Modificando Ponteiros de Dentro de Funções (T*&)
  7. Boas práticas de C++ moderno
  8. Exemplo de migração: implementação C → C++

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 Tdelete 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

  1. 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);
    
  2. Escreva uma função somaVetor que receba um vector<int> por const& e retorne a soma de todos os seus elementos. Use range-based for.
  3. Dada a struct abaixo, crie um vector<Aluno> com 3 alunos, ordene-o pelo campo nota em ordem decrescente usando sort com um critério personalizado, e imprima os nomes em ordem:
    struct Aluno { string nome; float nota; };
    
  4. 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)
    
  5. Escreva uma função insereInicio que receba uma Nodo*& (referência para ponteiro) e um inteiro, e insira um novo nodo no início de uma lista encadeada simples. Mostre a chamada.
  6. Reescreva o trecho C abaixo em C++, substituindo char[] + strcpy/strcat/printf pelas equivalentes C++:
    char saudacao[60];
    strcpy(saudacao, "Ola, ");
    strcat(saudacao, nome);
    printf("%s!\n", saudacao);
    
  7. O código a seguir usa std:: em todo lugar e typedef struct. Simplifique-o usando using namespace std e 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";
    }
    
  8. Reescreva o trecho C abaixo em C++, usando bool, nullptr e cin/cout no lugar de int, NULL e scanf/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");
    
  9. Reescreva o trecho abaixo trocando malloc/free por new[]/delete[]. Por que não se usa delete (sem colchetes) neste caso?
    int* v = (int*) malloc(10 * sizeof(int));
    // ... usa v ...
    free(v);
    
  10. Escreva duas sobrecargas da função valorAbsoluto: uma para int e outra para double, ambas retornando o valor absoluto do argumento sem usar o operador ?:.
  11. 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.
  12. Escreva um programa completo que leia inteiros de um arquivo "numeros.txt" (um por linha) e imprima a soma no terminal. Use ifstream e o operador >>.
  13. 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;
    }
    
  14. Reescreva o loop abaixo usando range-based for e auto:
    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:

  1. void dobra(int* x) deveria ser void 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 de dobra(&p)).
  2. malloc(sizeof(int)) deveria ser new int — não misturar malloc/free com código C++.
  3. Falta delete p; antes de terminar o main — 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.