Sebenta · C/C++ avançado (UC02839)
Introdução
Esta UC continua a UC00669 (C/C++ básico) e foca-se no que torna C++ uma linguagem dominante em sistemas de alta performance: templates (generics), STL (Standard Template Library), smart pointers (RAII), lambdas, exceptions, concorrência com threads, move semantics e ferramentas modernas de build (CMake) e testes (Catch2, GoogleTest).
A C++ moderna (a partir de C++11, 2011) é uma linguagem essencialmente diferente da C++ tradicional. É expressiva, segura, e quase tão rápida como C. Aprender C++ moderno abre portas em jogos AAA (Unreal Engine), motores 3D, sistemas embedded sérios, finanças de alta frequência, drivers, navegadores e muito mais.
Esta sebenta assume base sólida em C (UC00669).
1. C++ moderno
Versões e standards
- C++98/03: original. Hoje quase obsoleto.
- C++11: revolução — auto, lambdas, smart pointers, threads, move.
- C++14: refinamentos.
- C++17: structured bindings, optional, variant, filesystem.
- C++20: concepts, ranges, coroutines, modules.
- C++23: refinamentos.
Compilar com versão moderna:
g++ -std=c++20 -Wall -Wextra programa.cpp -o programa
Features essenciais
auto — inferência de tipo:
auto x = 42; // int
auto pi = 3.14; // double
auto v = vector<int>{1,2,3};
Range-based for:
for (auto& n : v) {
n *= 2;
}
Initializer lists {}:
vector<int> v{1, 2, 3};
Pessoa p{"Ana", 25};
int x{42};
Structured bindings (C++17):
auto [chave, valor] = par;
for (auto& [k, v] : mapa) { ... }
nullptr em vez de NULL:
int* p = nullptr;
constexpr — calculado em compile-time:
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int x = factorial(5); // 120 em compile-time
2. Templates
Templates permitem código genérico — funciona com qualquer tipo, sem perda de performance.
Function templates
template <typename T>
T maximo(T a, T b) {
return (a > b) ? a : b;
}
cout << maximo(3, 7); // int
cout << maximo(2.5, 1.8); // double
cout << maximo<string>("ana", "bia"); // string
O compilador gera versões específicas (instanciação) para cada tipo usado.
Class templates
template <typename T>
class Caixa {
private:
T conteudo;
public:
Caixa(T val) : conteudo(val) {}
T obter() const { return conteudo; }
void definir(T val) { conteudo = val; }
};
Caixa<int> c1(42);
Caixa<string> c2("Olá");
Caixa<Pessoa> c3(Pessoa("Ana", 25));
Multiple template parameters
template <typename K, typename V>
class Par {
public:
K chave;
V valor;
Par(K k, V v) : chave(k), valor(v) {}
};
Par<string, int> p("Ana", 25);
Variadic templates
Aceita número variável de argumentos:
template <typename T>
void imprimir(T arg) {
cout << arg << endl;
}
template <typename T, typename... Args>
void imprimir(T primeiro, Args... resto) {
cout << primeiro << " ";
imprimir(resto...);
}
imprimir(1, "Ana", 3.14, true);
Concepts (C++20)
Restringe templates a tipos que satisfazem certos requisitos:
#include <concepts>
template <integral T> // só inteiros
T dobro(T x) { return x * 2; }
dobro(5); // OK
dobro(2.5); // ERRO de compilação
3. STL — Standard Template Library
A STL tem três pilares: containers, iterators, algorithms.
Containers
Sequência (mantêm ordem de inserção):
- vector<T> — array dinâmico (padrão).
- list<T> — lista duplamente ligada.
- deque<T> — double-ended queue.
- array<T, N> — array fixo no stack.
- forward_list<T> — lista simples ligada.
Associativos ordenados (baseados em árvore balanceada):
- map<K, V> — chave → valor único, ordenado.
- set<T> — únicos, ordenado.
- multimap<K, V> — pode haver chaves duplicadas.
- multiset<T>.
Associativos unordered (hash table):
- unordered_map<K, V> — O(1) amortizado.
- unordered_set<T>.
- E versões multi.
Adaptadores:
- stack<T> — LIFO.
- queue<T> — FIFO.
- priority_queue<T> — heap (max-heap por padrão).
vector — o trabalho-cavalo
#include <vector>
vector<int> v; // vazio
vector<int> v2(5); // 5 zeros
vector<int> v3(5, 100); // 5 cópias de 100
vector<int> v4{1, 2, 3, 4, 5}; // initializer list
// Adicionar
v.push_back(10);
v.emplace_back(20); // constrói in-place (eficiente para tipos complexos)
// Tamanho
v.size();
v.empty();
v.capacity(); // memória alocada (≥ size)
v.reserve(1000); // reserva sem allocar elementos
// Acesso
v[0]; // sem bounds check
v.at(0); // com bounds check (lança out_of_range)
v.front(), v.back();
// Remover
v.pop_back();
v.erase(v.begin() + 2); // remove índice 2
v.clear();
// Iterar
for (auto& n : v) { ... }
for (auto it = v.begin(); it != v.end(); ++it) { ... }
map / unordered_map
#include <map>
#include <unordered_map>
map<string, int> idades; // ordenado por chave
idades["Ana"] = 25;
idades["Bruno"] = 30;
// Acesso
int n = idades["Ana"]; // 25
// Verificar (sem criar)
if (auto it = idades.find("Carla"); it != idades.end()) {
cout << it->second;
}
// C++20: contains
if (idades.contains("Ana")) { ... }
// Iterar (ordenado por chave)
for (const auto& [nome, idade] : idades) {
cout << nome << ": " << idade << endl;
}
// Remover
idades.erase("Ana");
unordered_map tem a mesma API mas é hash-based — muito mais rápido para acessos pontuais (O(1) vs O(log n)), mas iteração sem ordem garantida.
Iterators
Iterators são "pointers generalizados" — abstraem percurso de containers.
vector<int> v{1, 2, 3, 4, 5};
auto it = v.begin(); // primeiro
auto end = v.end(); // depois do último
while (it != end) {
cout << *it << " ";
++it;
}
Categorias: - Input/Output: leitura/escrita uma passagem. - Forward: andar para a frente. - Bidirectional: para a frente e para trás (list, map). - Random access: salto a posição (vector, deque, array).
Algorithms
<algorithm> tem ~100 funções genéricas que operam sobre iterators:
#include <algorithm>
#include <numeric>
vector<int> v{3, 1, 4, 1, 5, 9, 2, 6};
// Ordenar
sort(v.begin(), v.end());
sort(v.begin(), v.end(), greater<int>());
// Reverse
reverse(v.begin(), v.end());
// Pesquisar
auto it = find(v.begin(), v.end(), 5);
bool tem = (it != v.end());
binary_search(v.begin(), v.end(), 5); // requer ordenado
// Contar
int n = count(v.begin(), v.end(), 1);
int n2 = count_if(v.begin(), v.end(),
[](int x) { return x > 3; });
// Transformar (in-place)
transform(v.begin(), v.end(), v.begin(),
[](int x) { return x * 2; });
// Acumular
int soma = accumulate(v.begin(), v.end(), 0);
int prod = accumulate(v.begin(), v.end(), 1, multiplies<int>());
// Min/max
int mn = *min_element(v.begin(), v.end());
auto [mn2, mx] = minmax_element(v.begin(), v.end());
// Verificar predicados
bool todos = all_of(v.begin(), v.end(), [](int x) { return x > 0; });
bool algum = any_of(v.begin(), v.end(), [](int x) { return x > 100; });
bool nenhum = none_of(v.begin(), v.end(), [](int x) { return x < 0; });
// Copy / move
vector<int> dest;
copy_if(v.begin(), v.end(), back_inserter(dest),
[](int x) { return x % 2 == 0; });
// Unique (em sequência ordenada)
sort(v.begin(), v.end());
auto last = unique(v.begin(), v.end());
v.erase(last, v.end());
Ranges (C++20)
Sintaxe mais limpa para chains de operações:
#include <ranges>
vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto resultado = v
| views::filter([](int n) { return n % 2 == 0; })
| views::transform([](int n) { return n * n; })
| views::take(3);
for (int n : resultado) cout << n << " ";
// 4 16 36
4. Smart pointers e RAII
RAII — Resource Acquisition Is Initialization
Princípio fundamental de C++: recursos são adquiridos em construtores e libertados em destrutores. Garante limpeza mesmo com exceptions.
class Ficheiro {
FILE* f;
public:
Ficheiro(const char* nome) : f(fopen(nome, "r")) {
if (!f) throw runtime_error("Não abriu");
}
~Ficheiro() {
if (f) fclose(f);
}
};
void usar() {
Ficheiro f("dados.txt");
// ...
// mesmo com throw aqui, ~Ficheiro é chamado
}
unique_ptr
Ownership único, sem cópia, com auto-delete:
#include <memory>
void exemplo() {
auto p = make_unique<Pessoa>("Ana", 25);
p->apresentar();
cout << p->getNome();
} // delete automático aqui
// Move (não cópia)
auto p2 = move(p); // p agora é nullptr
// Não funciona:
auto p3 = p; // ERRO de compilação
Para arrays:
auto arr = make_unique<int[]>(10);
arr[0] = 42;
shared_ptr
Ownership partilhado com reference counting:
auto p1 = make_shared<Pessoa>("Ana", 25);
auto p2 = p1; // OK — incrementa refcount
auto p3 = p1;
cout << p1.use_count(); // 3
p2.reset();
cout << p1.use_count(); // 2
// Pessoa é deletada quando o último shared_ptr morre
Custo: mais memória (control block), atomic refcount (lock-free).
weak_ptr
Referência não-owning para shared_ptr — evita ciclos:
class Pai;
class Filho {
weak_ptr<Pai> pai; // weak para evitar ciclo
};
class Pai {
vector<shared_ptr<Filho>> filhos;
};
// Para usar weak_ptr:
weak_ptr<Pessoa> wp;
if (auto sp = wp.lock()) { // tenta promover para shared_ptr
sp->apresentar();
} else {
cout << "Já não existe";
}
Regra geral
unique_ptrpor padrão.shared_ptrquando precisas de ownership partilhado.weak_ptrpara quebrar ciclos.- Raw pointers (
T*) só para observação (não ownership). new/deleteraros (sempre que possível, smart pointer).
5. Lambdas
Sintaxe básica
auto somar = [](int a, int b) {
return a + b;
};
cout << somar(2, 3); // 5
// Tipo deduzido (auto), pode ser passado a funções
Captures
int x = 10;
auto a = [x]() { return x; }; // captura por valor
auto b = [&x]() { x++; }; // captura por referência
auto c = [=]() { return x; }; // tudo por valor
auto d = [&]() { x++; }; // tudo por referência
auto e = [x, &y]() { return x + y; }; // mistura
// Capturar this (em método de classe)
class Foo {
int membro;
void bar() {
auto f = [this]() { return membro; };
}
};
Lambdas com STL
vector<Pessoa> pessoas{ ... };
// Sort por idade
sort(pessoas.begin(), pessoas.end(),
[](const Pessoa& a, const Pessoa& b) {
return a.idade < b.idade;
});
// Filter
vector<Pessoa> adultos;
copy_if(pessoas.begin(), pessoas.end(), back_inserter(adultos),
[](const Pessoa& p) { return p.idade >= 18; });
// for_each
for_each(pessoas.begin(), pessoas.end(),
[](const Pessoa& p) { cout << p.nome << endl; });
Lambdas como callbacks
void executar(function<void()> f) {
f();
}
executar([]() { cout << "Olá!" << endl; });
int x = 5;
executar([x]() { cout << x << endl; });
6. Exceptions
Hierarquia padrão
exception
├── logic_error
│ ├── invalid_argument
│ ├── out_of_range
│ ├── length_error
│ └── domain_error
└── runtime_error
├── overflow_error
├── underflow_error
└── range_error
Throw / catch
#include <stdexcept>
void validar(int idade) {
if (idade < 0)
throw invalid_argument("Idade negativa");
if (idade > 150)
throw out_of_range("Idade demasiado alta");
}
try {
validar(-5);
}
catch (const invalid_argument& e) {
cerr << "Argumento: " << e.what() << endl;
}
catch (const exception& e) {
cerr << "Geral: " << e.what() << endl;
}
catch (...) {
cerr << "Desconhecido" << endl;
}
Custom exceptions
class SaldoInsuficiente : public runtime_error {
public:
double saldoActual;
SaldoInsuficiente(double saldo, const string& msg)
: runtime_error(msg), saldoActual(saldo) {}
};
void sacar(double valor) {
if (valor > saldo)
throw SaldoInsuficiente(saldo, "Não chega");
}
try {
sacar(1000);
} catch (const SaldoInsuficiente& e) {
cerr << e.what() << " (saldo: " << e.saldoActual << ")";
}
noexcept
Indica que função não lança:
int somar(int a, int b) noexcept { return a + b; }
Move constructors devem ser noexcept (STL optimiza).
7. Concorrência
std::thread
#include <thread>
void trabalho(int id) {
cout << "Thread " << id << endl;
}
int main() {
thread t1(trabalho, 1);
thread t2(trabalho, 2);
t1.join(); // espera t1
t2.join();
}
Mutex
Para proteger dados partilhados:
#include <mutex>
mutex m;
int contador = 0;
void incrementar() {
for (int i = 0; i < 1000; i++) {
lock_guard<mutex> lock(m); // RAII
contador++;
}
}
// Threads acabam, contador = N * 1000 (garantido)
unique_lock é mais flexível (pode unlock manualmente):
unique_lock<mutex> lock(m);
// ...
lock.unlock();
// ...
lock.lock();
Atomic
Operações thread-safe sem mutex:
#include <atomic>
atomic<int> contador{0};
void incrementar() {
for (int i = 0; i < 1000; i++) {
contador++; // atómico, sem mutex
}
}
async / future
#include <future>
int trabalho_pesado() {
this_thread::sleep_for(chrono::seconds(2));
return 42;
}
auto futuro = async(launch::async, trabalho_pesado);
// Fazer outras coisas...
int resultado = futuro.get(); // bloqueia até estar pronto
Condition variables
Para sinalização entre threads:
#include <condition_variable>
mutex m;
condition_variable cv;
bool pronto = false;
void produtor() {
{
lock_guard<mutex> lock(m);
pronto = true;
}
cv.notify_one();
}
void consumidor() {
unique_lock<mutex> lock(m);
cv.wait(lock, []{ return pronto; });
// ...
}
8. Move semantics
Problema da cópia
vector<int> v{1, 2, 3, ... 1'000'000};
vector<int> v2 = v; // copia TUDO — lento
Solução: move
vector<int> v3 = move(v); // move — só transfere pointer interno
// v fica vazio (estado "moved-from")
Move constructor / assignment
class MeuVec {
int* dados;
size_t n;
public:
// Copy constructor (caro)
MeuVec(const MeuVec& outro) : n(outro.n) {
dados = new int[n];
copy(outro.dados, outro.dados + n, dados);
}
// Move constructor (barato)
MeuVec(MeuVec&& outro) noexcept
: dados(outro.dados), n(outro.n)
{
outro.dados = nullptr;
outro.n = 0;
}
// Copy assignment
MeuVec& operator=(const MeuVec& outro) {
if (this != &outro) {
delete[] dados;
n = outro.n;
dados = new int[n];
copy(outro.dados, outro.dados + n, dados);
}
return *this;
}
// Move assignment
MeuVec& operator=(MeuVec&& outro) noexcept {
if (this != &outro) {
delete[] dados;
dados = outro.dados;
n = outro.n;
outro.dados = nullptr;
outro.n = 0;
}
return *this;
}
~MeuVec() { delete[] dados; }
};
Rule of Zero / Five
- Rule of Zero: se classe não gere recursos directos (usa STL), não definas nada — compilador faz tudo bem.
- Rule of Five: se defines um dos seguintes, define todos:
- Destructor.
- Copy constructor.
- Copy assignment.
- Move constructor.
- Move assignment.
9. CMake — build moderno
CMakeLists.txt mínimo
cmake_minimum_required(VERSION 3.20)
project(MeuProjecto LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(programa
src/main.cpp
src/utils.cpp
)
target_include_directories(programa PRIVATE include)
target_compile_options(programa PRIVATE -Wall -Wextra)
Build
cmake -B build # configurar
cmake --build build # compilar
./build/programa # correr
Out-of-source build (build/ separado do código) é boa prática.
Dependências externas
find_package(Threads REQUIRED)
target_link_libraries(programa PRIVATE Threads::Threads)
# Bibliotecas modernas com FetchContent
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.1.1
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(programa PRIVATE fmt::fmt)
10. Testes com Catch2
#include <catch2/catch_test_macros.hpp>
#include "calculadora.h"
TEST_CASE("Somar funciona", "[calc]") {
Calculadora c;
REQUIRE(c.somar(2, 3) == 5);
REQUIRE(c.somar(-1, 1) == 0);
}
TEST_CASE("Dividir por zero lança", "[calc]") {
Calculadora c;
REQUIRE_THROWS_AS(c.dividir(10, 0), invalid_argument);
}
TEST_CASE("Sections", "[calc]") {
Calculadora c;
SECTION("positivos") {
REQUIRE(c.somar(2, 3) == 5);
}
SECTION("negativos") {
REQUIRE(c.somar(-2, -3) == -5);
}
}
Adicionar a CMakeLists:
enable_testing()
add_executable(tests tests/test_main.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
add_test(NAME tests COMMAND tests)
Correr:
cd build
ctest --output-on-failure
11. Boas práticas
Modern C++ guidelines
- Prefere
autoquando o tipo é óbvio. - Range-based for > iterators manuais.
{}initialization quando possível.nullptrem vez deNULLou 0.make_unique/make_sharedem vez denew.constem tudo o que não modificas.overrideem funções virtuais sobrescritas.noexceptem moves e funções que não lançam.= deletepara impedir cópia/move.= defaultpara usar implementação por defeito.
Princípios
- RAII sempre: recursos em destrutores.
- Single Responsibility Principle.
- Don't Repeat Yourself: templates ajudam.
- Pay only for what you use: STL é eficiente.
- Compile-time > Runtime: constexpr, templates.
Anti-patterns a evitar
new/deleteraw sem smart pointer.- C-style casts (
(int)x) — usastatic_cast,dynamic_cast, etc. - Macros quando inline funciona.
using namespace std;em headers (pollution).goto, exceptions para fluxo normal.
12. Próximos passos
- C++20 modules — alternativa moderna a #include.
- Coroutines — async sem callbacks.
- Ranges/views profundamente.
- Boost — biblioteca de bibliotecas.
- Unreal Engine com C++.
- Performance: profilers (perf, Instruments), benchmarking (Google Benchmark).
- Concorrência avançada: thread pools, lock-free.
Recursos
- "A Tour of C++" (Stroustrup) — visão geral.
- "Effective Modern C++" (Scott Meyers) — bíblia C++11/14.
- "C++ Core Guidelines" (online, Stroustrup + Sutter).
- cppreference.com — referência exaustiva.
- learncpp.com — gratuito.
- CppCon no YouTube — conferências anuais.
C++ moderno é uma das linguagens mais poderosas e expressivas que existem. A curva inicial é íngreme — templates, move semantics, RAII demoram a interiorizar. Mas o domínio compensa: tens controlo absoluto sobre o hardware com abstrações elegantes. Para jogos, sistemas críticos, finanças, robótica — C++ continua a ser rei.