Revisão de Tipos e Módulos
Igor Machado Coelho
13/09/2020 - 03/04/2025
São requisitos para essa aula o conhecimento de:
Exemplos serão dados com base no sistema GNU/Linux e compiladores GCC, mas existem ferramentas equivalentes para Windows e demais sistemas operacionais. A IDE Visual Studio Code suporta a linguagem C++ tanto para Linux (nativamente) quanto para Windows (com a instalação do compilador MinGW).
Também é possível praticar diretamente em um navegador web com plataformas online: onlinegdb.com/online_c++_compiler. Neste caso, o aluno pode escolher o compilador de C ou da linguagem C++ (considerando padrões C23 e C++23).
Compreender a lógica da programação é a habilidade mais importante para um programador! Com ela, você pode facilmente trocar de linguagem de programação, conhecendo apenas alguns comandos básicos.
O primeiro conceito a ser revisado é de variável. Uma variável consiste de um identificador válido (mesmo para outras linguagens populares como Python) e armazena algum tipo de dado da memória do computador.
A linguagem C/C++ é fortemente tipada, portando o programador deve dizer explicitamente qual o tipo de dado deseja armazenar em cada variável.
int x = 5; // armazena o inteiro 5 na variável x
char y = 'A'; // armazena o caractere 'A' na variável y
float z = 3.7 ; // armazena o real 3.7 na variável z
bool v = true; // armazena o booleano true na variável v
auto b = 'B'; // dedução de tipo com 'auto'... qual tipo?
auto s = "abcd"; // cadeia de caracteres, ainda veremos tipo
Responda: Qual o tipo acima de b? (C++23 e C23)
Pergunta/Resposta: Cuidado com tipos. Quais são os valores armazenados nas variáveis abaixo (C++23 e C23)?
int x1 = 5; // => 5
int x2 = x1 + 10; // => 15
int x3 = x2 / 2; // => 7
float x4 = x2 / 2; // => 7.0
float x5 = x2 / 2.0; // => 7.5
auto x6 = 15; // => 15
auto x7 = x2 / 2; // => 7
auto x8 = x2 / 2.0; // => 7.5
Verifiquem essas operações de variáveis, escrevendo na saída padrão (tela do computador).
Tipos primitivos em C/C++ tem um tamanho definido, então é uma boa prática utilizar tamanhos fixos.
Dê preferência a inicialização direta com chaves { }
, ao
invés de indireta por atribuição (operator=
).
A modularização de programas é muito importante, principalmente quando trechos de código são repetidos muitas vezes.
Nesses casos, é comum criar rotinas, como funções e procedimentos, que podem por sua vez receber parâmetros.
Tomemos por exemplo a função quadrado que retorna o valor passado elevado ao quadrado.
// função que retorna um 'int', com parâmetro 'p'
auto quadrado (int p) -> int {
return p*p;
}
// variável do tipo 'int', com valor 25
int x = quadrado(5);
Importante: a dedução do tipo após a seta
->
é feita automaticamente.
Em C, tipicamente é utilizado o comando printf
, mas
devido a inúmeras falhas de segurança, é recomendado o uso de uma
alternativa mais segura. Assim, em C++, para imprimir na saída padrão
utilizaremos o comando std::print
.
O C++23 traz oficialmente std::print
e
std::println
como parte do módulo de biblioteca padrão
std
. Para utilizá-lo, basta fazer
import std;
.
import std;
auto main() -> int {
std::println("Olá Mundo!");
return 0;
}
// https://godbolt.org/z/dvc5hdv3n
Pergunta: Qual o tipo de retorno da função
main
?
Para imprimir na saída padrão utilizaremos o comando
std::print
, e é possível evitar o prefixo
std::
com um using namespace std;
.
Pergunta: como podemos misturar um texto (também chamado de cadeia de caracteres ou string) com o conteúdo de variáveis?
Resposta: através do padrão de substituição
{}
.
import std;
using namespace std;
auto main() -> int {
int32_t x1 = 7;
println("x1 é {}", x1); // x1 é 7
float x6 = x1 / 2.0;
println("metade de {} é {}", x1, x6); // metade de 7 é 3.5
char b = 'L';
println("isto é uma {}etra", b); // isto é uma Letra
print("Olá mundo! \n"); // Olá mundo! (quebra de linha)
// ====================================================
Problema: dados x
e y
,
imprima o maior valor.
Laços de repetição podem ser feitos através de comandos while ou for. Um comando for é dividido em três partes: inicialização, condição de continuação e incremento.
Pergunta: O que é impresso em ambos laços?
Quando nenhum valor é retornado (em um procedimento), utilizamos a
palavra-chave void
. Procedimentos são úteis mesmo quando
nenhum valor é retornado. Exemplo: (de a até b):
import std;
auto imprime (int a, int b) -> void {
for (auto i=a; i<b; i++)
std::println("{}", i);
}
auto main() -> int {
imprime(2, 5);
return 0;
}
Pergunta: Por que a rotina imprime
não
precisa retornar nada? O que é impresso ao invocar
imprime(2,5)
?
Além dos tipos primitivos apresentados anteriormente (int, float, char, …), a linguagem C/C++ nos permite criar tipos compostos.
Tarefa: estude demais tipos primitivos como double e long long, bem como os modificadores unsigned, signed, short e long.
Os tipos compostos podem ser vetores (arrays) ou agregados (structs, …).
break
e
continue
Controles de fluxo em laços de repetição podem ser efetuados com
break
e continue
. O break
finaliza a execução do laço e o continue
recomeça o
laço.
Problema: Dado um vetor B, encontre o primeiro/último valor negativo, ou imprima -1 caso não exista.
goto
(tópico avançado)Saltos incondicionais no código podem ser feitos com
goto label;
e label:
. Uma aplicação usual é a
“quebra múltipla” de laços de repetição. Evite ao máximo o uso de
goto
e, sempre que for possível, prefira alternativas
estruturadas como for
, while
, if
,
else
, break
, etc.
Contabilize quantos prints são executados (variável
z
):
Comparação C/C++: lembre-se de usar struct ou class public:, caso contrário não será reconhecido como um tipo agregado, mas sim um objeto, que funciona de forma completamente diferente na linguagem C++.
Importante: utilizaremos struct no decorrer desse curso de C/C++.
Retomamos o exemplo da estrutura P anterior e nos perguntamos, como acessar as variáveis internas do agregado P?
Assim como na inicialização designada, podemos utilizar o operador ponto (.) para acessar campos do agregado. Exemplo:
auto p1 = P{.y = 'A'};
p1.x = 20; // atribui 20 à variável x de p1
p1.x = p1.x + 1; // incrementa a variável x de p1
println("{} {}", p1.x, p1.y); // imprime '21 A'
Exemplo de estrutura p1
, com p1.x e p1.y, da esquerda
para direita:
p1: | 21 | 'A' |
p1.x p1.y
Importante: veremos à frente que o (.) também pode acessar métodos internos do tipo agregado.
Todas variáveis de um programa ocupam determinado espaço na memória principal do computador. Assumiremos que o tipo int (ou float) ocupa 4 bytes, enquanto um char ocupa apenas 1 byte.
No caso de vetores, o espaço ocupado na memória é multiplicado pelo número de elementos. Vamos calcular o espaço das variáveis:
int32_t v[256]; // = 1024 bytes = 1 kibibyte = 1 KiB
char x[1000]; // = 1000 bytes = 1 kilobyte = 1 kB
float y[5]; // = 20 bytes
Já nos agregados, assumimos o espaço ocupado como a soma de suas variáveis internas (embora na prática o tamanho possa ser ligeiramente superior, devido a alinhamentos de memória).
Importante: em C++, agregados também podem conter métodos.
C++ permite a definição de tipos genéricos, ou seja, tipos que permitem que algum outro tipo seja passado como parâmetro.
Consideremos o agregado P que carrega um int e um char… como transformá-lo em um agregado genérico em relação à variável x?
template<typename T>
struct G {
T x; // qual o tipo da variável x?
char y;
};
// declara o agregado genérico G com tipo T=float ou T=char
G<float> g1 = {.x = 3.14, .y = 'Y'};
G<char> g2 = {.x = 'A', .y = 'Y'};
Pergunta: Quanto espaço (em bytes) cada variável dessa ocupa?
Em C/C+, podemos definir um valor como constante, através da palavra
const
. Uma mudança de tipos pode ser feita com type
cast. Em C++, utilize static_cast<tipo>
ao invés
do padrão C de cast.
unsigned int x = 10; // 10
double y1 = x / -2; // 0
double y2 = (double)x / -2; // -5
double y3 = static_cast<double>(x) / -2; // -5 (C++)
const unsigned int z1 = x; // 10
// z1 = 20; // ERRO
O const
pode ser removido através de um
const_cast
, sendo inseguro.
Em C23 e C++23 existe o constexpr
, que diferentemente do
const
, nunca pode ser removido ou redefinido
(diferentemente de macros), pois é resolvido em tempo de compilação.
std::string
e std::string_view
na
STLO tipo std::string
representa cadeias de caracteres,
chamadas de strings. Ela substitui a necessidade de
char*
, char[]
ou const char*
em
C.
Caso precise de uma “vista” leve de uma string, como um substring,
utilize std::string_view
(evita a cópia completa do
string
).
std::string s1 = "abcd";
std::string s2 = "ef";
println("tamanho1={} tamanho2={}", s1.length(), s2.length());
// tamanho1=4 tamanho2=2
s1 = s1 + s2;
std::string_view sv = s1;
std::string_view sub = sv.substr(3, 2);
println("s1={} s2={} sv={} sub={}", s1, s2, sv, sub);
// s1=abcdef s2=ef sv=abcdef sub=de
const char* cs = s1.c_str();
println("s1={} cs={}", s1, cs);
// s1=abcdef cs=abcdef
std::vector
na STLA popular estrutura std::vector<tipo>
permite
representar vetores com tamanho variável (através do método
push_back
). Exemplo:
int v1[10];
int v2[] = {1, 2, 3, 4};
std::vector<int> k1{};
std::vector<int> k2 = {1, 2, 3, 4};
k2.push_back(999);
//
print("v[0]={} v[3]={} tam={}\n", v2[0], v2[3],
sizeof(v2) / sizeof(v2[0]));
// v[0]=1 v[3]=4 tam=4
print("k[0]={} k[4]={} tam={}\n", k2[0], k2[4], k2.size());
// k[0]=1 k[4]=999 tam=5
print("{}\n", std::is_aggregate<std::vector<int>>::value);
// false
std::array
na STLAssim como vetores nativos, exemplo int[]
, o agregado
std::array<tipo, tamanho>
permite representar vetores
de tamanho fixo. Exemplo:
int v1[10];
int v2[] = {1, 2, 3, 4};
std::array<int, 10> a1{};
std::array<int, 4> a2 = {1, 2, 3, 4};
print("v[0]={} v[3]={} tam={}\n", v2[0], v2[3],
sizeof(v2) / sizeof(v2[0]));
// v[0]=1 v[3]=4 tam=4
print("a[0]={} a[3]={} tam={}\n", a2[0], a2[3], a2.size());
// a[0]=1 a[3]=4 tam=4
print("{} {} {}\n", std::is_aggregate<int*>::value,
std::is_aggregate<int[]>::value,
std::is_aggregate<std::array<int, 4>>::value);
// false true true
Até agora, verificamos as seguinte estruturas:
É possível retornar múltiplos elementos (par ou tupla), através de um structured binding com tuplas:
Perg.: qual tipo de retorno de ‘duplo’?
R: std::tuple<int, double>
.
Os parâmetros são sempre copiados (em C) ao serem passados para uma função ou procedimento. Como passar tipos complexos (estruturas e vetores de muitos elementos) sem perder tempo?
Nestes casos, a linguagem C oferece um tipo especial denominado
ponteiro. A sintaxe do ponteiro simplesmente inclui um asterisco
(*
) após o tipo da variável. Um estado vazio se faz com
nullptr (ou 0
).
Exemplos:
int* x = nullptr; struct P* p1 = nullptr
;
Um ponteiro simplesmente armazena o local (endereço) onde determinada variável está armazenada na memória (basicamente, um número). Então quando um ponteiro é passado como parâmetro, a cópia do ponteiro pode ser utilizada para encontrar na memória a estrutura desejada.
O tamanho do ponteiro varia de acordo com a arquitetura, mas para endereçar 64-bits, ele ocupa 8 bytes.
Em ponteiros para agregados, o operador de acesso (.
) é
substituído por uma seta (->
). O operador
&
toma o endereço da variável:
struct P {
int32_t x;
char y; // mais alguma coisa gigante aqui?
};
// ...
P p0 = {.x = 20, .y = 'Y'};
Testando procedimentos f
e g
:
Programas frequentemente necessitam de alocar mais memória para uso, o que é armazenado de forma segura em um ponteiro para o tipo da memória:
O tipo de uma função é basicamente um ponteiro (endereço) da localização desta função na memória do computador. Por exemplo:
Este fato pode ser útil para receber funções como parâmetro, bem como armazenar funções anônimas (lambdas):
A linguagem C++ permite métodos membros (member functions) com a inclusão de funções e variáveis dentro de agregados (em C, funções devem ser externas/globais). Para acessar campos do agregado de dentro dessas funções, utilize o ponteiro para o agregado, chamado this:
Funções podem se chamar novamente durante sua execução em um processo recursivo.
std::span
na STLComo std::string_view
, para demais vetores
int[]
, std::array
e std::vector
,
o std::span
suporta sequências de dados sem
posse.
import std; // invocando ./programa 1 2 3
auto main(int argc, char* argv[]) -> int {
int v2[] = {1, 2, 3, 4};
std::span<int> s1{v2};
for (auto i : s1) std::println("{}", i);
// 1 2 3 4
std::vector<int> vec = {1, 2, 3, 4};
std::span<int> s2{vec};
for (auto i : s2) std::println("{}", i);
// 1 2 3 4
std::span<char*> entrada{argv, argc};
for (auto i : entrada) std::println("{}", i);
// ./programa 1 2 3
return 0;
} // ============================================
std::optional
na STLO std::optional<tipo>
representa um valor
opcional, com alocação em stack, não em heap como
ponteiros (e smart pointers, que veremos a seguir). O acesso se
faz com operador (*
).
auto busca(char c, std::span<char> v) -> std::optional<int> {
// busca char 'c' num vetor v e retorna posição
for (int i = 0; i < v.size(); i++)
if (v[i] == c) return i; // encontrou
// não encontrou
return std::nullopt;
}
// ...
std::vector<char> v = {'a', 'b', 'c'};
auto op = busca('x', v);
if(op) println("posicao={}", *op);
else println("não encontrou");
std::expected
na STLO std::expected<tipo, tipo_erro>
representa um
valor esperado, com alocação em stack, não em
heap como ponteiros (e smart pointers, que veremos a
seguir). O acesso se faz com operador (*
).
auto busca2(char c, std::span<char> v)
-> std::expected<int, std::string> {
// busca char 'c' num vetor v e retorna posição
for (int i = 0; i < v.size(); i++)
if (v[i] == c) return i; // encontrou
return std::unexpected{"não encontrou"};
}
// ...
std::vector<char> v = {'a', 'b', 'c'};
auto exp = busca2('x', v);
if(exp) println("posicao={}", *exp);
else println("{}", exp.error());
C++20 traz a possibilidade de definir conceitos (ou concepts). Esse recurso permite definições genéricas sobre algum tipo (inclusive tipos agregados com funções internas).
Por exemplo, podemos criar um conceito
TemNegativo
, que exige que o agregado possua um método
neg()
:
template <typename Agregado>
concept TemNegativo = requires(Agregado a) {
{ a.neg() } -> std::same_as<void>;
};
Exemplo de agregado de acordo com conceito
TemNegativo
:
Assim, podemos utilizar um conceito mais específico ao invés de um tipo automático:
auto a0 = Z{.x = 1}; // tipo automático
auto p0 = new Z{.x = 1}; // tipo ponteiro
auto* p1 = new Z{.x = 1}; // tipo ponteiro
TemNegativo auto a2 = Z{.x = 2}; // tipo conceitual
Z a3 = Z{.x = 3}; // tipo explícito
Outra forma de validação de tipos em tempo de compilação é o
static_assert
. Por exemplo, como garantir que a classe Z
está de acordo com o conceito TemNegativo?
Importante: a noção de conceitos é fundamental para a compreensão de tipos abstratos, central no curso de estruturas de dados.
std::move
em C++Ponteiros são estruturas reconhecidamente problemáticas, portanto
desde a revisão C++11 é recomendado que se use ponteiros
inteligentes (ou smart pointers) ao invés de ponteiros
nativos. Existem dois tipos de smart pointers: unique_ptr
e
shared_ptr
. Ambos evitam que o usuário precise de desalocar
memória (com exceção de estruturas cíclicas, a serem abordadas no
futuro). Para utilizá-los, basta incluir o cabeçalho
<memory>
, e substituir o new
por
std::make_unique
ou std::make_shared
.
Ponteiros podem ser utilizados como marcadores de um espaço de
memória inválido, geralmente chamado de nulo. Em C, a macro
NULL
é geralmente definida como zero, sendo então uma
melhor prática usar o número zero diretamente ao invés de
NULL
. O condicional pode ser usado para verificar um
ponteiro como booleano, que é a opção mais segura. Em C++, existe o
std::nullptr
, que pode ser utilizado em situações
específicas (geralmente smart pointers), mas geralmente evite
NULL
e std::nullptr
.
Em C, só é possível passar variáveis por cópia, o que demanda uso de ponteiros para evitar cópias volumosas e desnecessárias.
Em C++, existem os conceitos de referência de lado esquerdo
(&)
e referência de lado direito
(&&)
. Em resumo, utilizamos um
tipo&
para denotar uma referência a um dado
vivo, e tipo&&
para uma referência a um
dado prestes a morrer (ou dado em movimento). Esse
conceito é fundamental para lidar com unique_ptr
, pois eles
não permitem cópias, sendo obrigatoriamente passados por referência.
Para transformar uma variável viva para uma variável em
movimento, basta usar o comando std::move
.
Além das clássicas rotinas, que retornam (ou não) valores, existem também corrotinas, com capacidade de paralisar e retomar a execução.
Um exemplo é a sequência de fibonacci, que começa de 0, 1, e
segue com a soma dos dois últimos elementos. Essa é uma
sequência infinita, e podemos facilmente representá-la assim
que retornos co_yield
de corrotina com
std::generator
:
Para consumir os valores, basta usar o for range (todos Fib menores que 10):
Desafio: como implementar essa mesma funcionalidade sem corrotina?
Desafio 2: outro uso é o std::future
com corrotinas conectadas a programação concorrente com threads. Isso
foge um pouco do escopo desse curso, mas verifique outras aplicações de
corrotinas e co_await
.
Referências de lado esquerdo (lvalue) complementam referências de lado direito (rvalue). Observe:
Observação: existe também a sintaxe
const tipo&
que permite lifetime extension,
algo que não exploraremos nessa breve revisão.
std::unique_ptr
O std::unique_ptr<tipo>
representa um ponteiro
único para o tipo
(como se fosse tipo*
). Uma
função útil é o get
, que retorna um ponteiro nativo C para
o dado. A função reset
apaga o ponteiro manualmente.
auto p1 = new int{10};
auto p2 = p1;
println("*p1={} *p2={}", *p1, *p2);
// *p1=10 *p2=10
delete p1;
auto u1 = std::make_unique<int>(10);
auto u2 = std::move(u1);
auto p3 = u2.get();
println("*u2={} *p3={}", *u2, *p3);
// *u2=10 *p3=10
u2.reset(); // apaga ponteiro u2 manualmente
u2 = nullptr; // apaga ponteiro u2 manualmente
A biblioteca padrão da linguagem tem componentes já testados e de uso
comum, resolvendo diversos problemas básicos de programação. C++ possui
implementações bastante importantes em sua biblioteca padrão, chamada
STL. Antigamente, era necessário utilizar
#include<...>
para incluir esses componentes, mas
desde o C++23 é possível fazer tudo automaticamente com um
import std
, utilizando a estrutura moderna dos CXX
Modules.
Já vimos indiretamente o uso de algumas dessas estruturas no curso,
como: tuplas em std::tuple
; ponteiros inteligentes em
std::make_unique
ou std::make_shared
; entre
outras coisas. Também vimos exemplos de estruturas muito fundamentais
como: std::string
e std::vector
. Geralmente,
propostas são feitas pela comunidade, e boas implementações são
incorporadas à biblioteca padrão, em revisões futuras da linguagem.
std::shared_ptr
(avançado)O std::shared_ptr<tipo>
representa um ponteiro
compartilhado para o tipo
(como se fosse
tipo*
). Uma função útil é o get
, que retorna
um ponteiro nativo C para o dado. A função reset
apaga o
ponteiro manualmente. O shared permite cópias e compartilhamento,
através de reference counting. Tome cuidado com ciclos, pois
podem acarretar vazamento de memória! Para isso, utilize
std::weak_ptr
ou cycles::relation_ptr
(a
seguir). Para utilizar, basta fazer
#include <memory>
. Exemplo:
std::function
(avançado)A estrutura std::function<tipo>
permite armazenar
funções, seja ela uma lambda sem captura (captureless lambda)
ou uma lambda de captura, também chamada de closure. Uma
captureless lambda pode decair para ponteiro de função,
enquanto as demais só podem ser encapsuladas como
std::function
. Basta fazer
#include <functional>
. Exemplo:
// captureless lambda
int(*fquad1)(int) = [](int p) -> int { return p*p; };
std::function<int(int)> fquad2 = [](int p) { return p*p; };
// capturando variável x (por cópia)
int x = 10;
int y = 20;
// closure x1 (retorna x + 1)
std::function<int()> x1 = [x]() { return x+1; };
// capturando todas variáveis locais com =, y por referência
std::function<int()> fxy = [=, &y]() { y++; return x+y; };
int z = fxy(); // z==31 y==21
this
com C++23 (avançado)Uma capacidade interessante do C++23 é o deducing this, que
permite trabalhar com tipagem sobre a variável this
em
funções. Isso pode ser útil para capturar o this
como
referência, ao invés de ponteiro, e também para nomear funções
anônimas.
std::scan
(avançado/experimental)Assim como o std::print
(atualmente da
fmt
), existem propostas para um std::scan
,
atualmente no projeto scnlib
de
eliaskosunen
.
A proposta experimental para o C++26 se chama P1729 “Text Parsing”, e
busca criar uma função scn::scan
que substitua a
scanf
(pelo mesmo raciocínio empregado na abolição do
printf
). Exemplo:
cycles::relation_ptr
(avançado/experimental)Uma proposta de ponteiro inteligente para resolver casos cíclicos foi
criado pelo prof. Igor Machado Coelho, chamado cycles::relation_ptr
.
Este é um projeto interessante para compreender as limitações dos
ponteiros inteligentes atuais, e o que pode ser possivelmente melhorado
em um C++ futuro. Exemplo:
Para utilizar, basta fazer
#include <cycles/relation_ptr>
. Exemplo:
Citamos o comitê diretor do C++, “DIRECTION FOR ISO C++” (2022-10-15), de H. Hinnant, R. Orr, B. Stroustrup, D. Vandevoorde, M. Wong (página 10):
C++ is seriously underrepresented in academia and often very poorly taught. It has been conventional to start teaching C++ by first introducing the lowest level and most error-prone facilities. Naturally, that discourages students and increases the time needed to get to what students consider meaningful computing (graphics, networking, mathematics, data analysis, etc.). Often, teachers even go to the extreme of insisting on using a C compiler. If the ultimate aim is to teach C++, that’s like insisting people start learning English by reading Beowulf or the Canterbury Tales in their original early-English language versions. Those are great books, but Early English is incomprehensible to most native Modern-English speakers.
Citamos o comitê diretor do C++, “DIRECTION FOR ISO C++” (2022-10-15), de H. Hinnant, R. Orr, B. Stroustrup, D. Vandevoorde, M. Wong (página 10):
In addition to the linguistic difficulties, such ancient sources present cultural conventions and idioms that seem very peculiar today. Instead of C, someone could teach Simula to prepare for learning C++. Why don’t people do that? Because the historical approach to teaching language (natural or programming language) complicates and detracts from the end goal: good code.
Why then do teachers use the C-first approach to teach C++? Part is tradition, curriculum inertia, and ignorance, but part of the reason is that C++ doesn’t offer a smooth path to idiomatic, proper, modern use of C++. It is hard to bypass both the traps of low-level constructs and the complexities of advanced features and teach programming and proper C++ usage from the start.
Em resumo: C++ moderno já é absolutamente superior a C em segurança e
clareza, com desempenho equivalente, mas historicamente carece de boas
estruturas para fazer o básico (como imprimir em tela,
fazer vetores, etc), obrigando o uso de estruturas inseguras, como
ponteiros. Então, as revisões recentes tem buscado esse fim, de
facilitar o uso básico (como std::print
,
std::array
, std::string
,
std::vector
, smart pointers, …) e evitar que a linguagem C
seja necessária para a escrita de programas básicos.
Hoje (2023) ainda existem problemas, como:
fmt::print
e scn::scan
)#include
em um código básico: a
ideia é que, a partir da implementação de import std
no
C++23, será desnecessário incluir bibliotecas externas em códigos
básicos :)
Muitos serão resolvidos na próxima edição do C++ (mas ainda faltará o
scn::scan
), sempre de olho em bons concorrentes modernos
como Rust.
Qualquer programa complexo necessita de divisão em partes, ou módulos, para maior controle e verificação da corretude das operações.
Nesse curso, vamos utilizar um padrão mínimo de modularização, para que seja possível efetuar testes no código (de forma sistemática).
Um programa começa pelo seu “ponto de entrada” (ou
entrypoint), tipicamente uma função
int main()
:
A declaração de funções pode ser feita antes da definição:
int quadrado(int p); // declara a função 'quadrado'
int quadrado(int p) {
return p*p; // implementa a função 'quadrado'
}
Declarações vem em arquivos .h
, enquanto as respectivas
implementações em arquivo .cpp
(ou juntas como
.hpp
).
main.cpp
Quando utilizando o GCC e um entrypoint no arquivo
main.cpp
:
Para compilar:
g++ -fconcepts -O3 main.cpp -o appMain
Para executar código:
./appMain
Importante: consideramos um sistema GNU/Linux, mas
caso seja Windows pode-se usar o compilador C/C++ MinGW e executar o
aplicativo gerado com uma extensão .exe
(padrão executável
Windows).
Modularização mínima: 4 arquivos.
main.cpp
(dica: colocar na pasta src/
)src/
)teste.cpp
(dica: colocar na pasta tests/
)makefile
do GNU (com regras all:
e
test:
)Também é informativo um arquivo extra na raiz com explicações sobre o
código (tipicamente README.md
na linguagem markdown)
Importante: o arquivo do entrypoint deverá
conter exclusivamente a função int main()
(e seus
respectivos #include
), para viabilizar testes de
código.
Durante o curso estudaremos várias estruturas de dados, mas sempre que possível utilize as existentes na biblioteca padrão (STL). São “mais eficientes” e “à prova de erros”.
Por exemplo, é fácil definir um tipo agregado Par
, que
comporta dois elementos internos (tipo genérico). Porém, é mais
vantajoso usar o existente na STL, chamado std::pair
(o
prefixo std::
é chamado namespace e evita colisões
de nomes):
assert
Durante o desenvolvimento, é útil verificar partes do código com
testes simples e necessários para a corretude do mesmo (em tempo real).
Para isso, podemos utilizar o assert()
. Exemplo:
Da mesma forma, podemos verificar tipos, especialmente conceitos, em tempo de compilação:
Uma forma prática de testar um código modularizado com
main.cpp
separado do resto.hpp
, é utilizando a
biblioteca Catch2.
Basta criar um arquivo de teste, por exemplo,
teste.cpp
:
Para baixar o arquivo catch2.hpp
, basta acessar o site
do projeto: github.com/catchorg/Catch2.
Link direto (Agosto 2020):
github.com/catchorg/Catch2/releases/download/v2.13.1/catch.hpp
Para compilar:
g++ -fconcepts teste.cpp -o appTestes
Para executar testes:
./appTestes -d yes
0.000 s: Testa inicializacao do agregado Z
===============================================
All tests passed (1 assertion in 1 test case)
Importante: Recomenda-se a opção
-fsanitize=address
e -g3
para evitar bugs
durante o desenvolvimento usando GCC.
Nessa revisão sobre tipos, buscamos não aprofundar em nenhuma característica “avançada” de C/C++, embora alguns conceitos possam parecer novos. Tópicos recomendados (não cobertos no curso):
std::unique_ptr
e std::shared_ptr
(não requer
delete
)Além da bibliografia do curso, recomendamos (para esse tópico):
Em especial, agradeço aos colegas que elaboraram bons materiais, como o prof. Fabiano Oliveira (IME-UERJ), e o prof. Jayme Szwarcfiter cujos conceitos formam o cerne desses slides.
Estendo os agradecimentos aos demais colegas que colaboraram com a elaboração do material do curso de Pesquisa Operacional, que abriu caminho para verificação prática dessa tecnologia de slides.
Esse material de curso só é possível graças aos inúmeros projetos de código-aberto que são necessários a ele, incluindo:
Agradecimento especial a empresas que suportam projetos livres envolvidos nesse curso:
Esses slides foram escritos utilizando pandoc, segundo o tutorial ilectures:
Exceto expressamente mencionado (com as devidas ressalvas ao material cedido por colegas), a licença será Creative Commons.
Licença: CC-BY 4.0 2020
Igor Machado Coelho