Funções, arrays e texto
Contratos, passagem de argumentos, arrays C, estruturas, strings e leitura de linhas.
Uma função de contagem pode receber as notas já lidas e devolver um total. Não trata a entrada nem imprime.
O contrato diz que dados recebe, se os pode alterar e que resultado devolve. Separar estas decisões permite testar a contagem sem fornecer dados pelo teclado.
Parâmetros e argumentos
Os parâmetros são os nomes declarados na função: a e b em int maximo(int a, int b). Os argumentos são as expressões fornecidas na chamada: 4 e 9 em maximo(4, 9). O parâmetro recebe uma cópia ou acesso conforme o contrato; o argumento diz qual o valor ou objeto usado nessa chamada.
Declarar e chamar funções
A declaração int maximo(int a, int b); anuncia o tipo do resultado e dos parâmetros. A definição acrescenta o corpo. A declaração tem de estar visível antes da chamada; a definição necessária tem de existir quando o programa é ligado.
// Excerto: definição de uma função.
int maximo(int a, int b) {
return a > b ? a : b;
}
O operador condicional condicao ? a : b escolhe a se a condição é verdadeira e b caso contrário. Aqui 4 > 9 é falso, por isso maximo(4, 9) devolve 9; return termina a chamada sem imprimir, por isso o chamador decide se guarda, calcula ou imprime. Uma função void não devolve valor.
Uma sobrecarga usa o mesmo nome com listas de parâmetros diferentes. O compilador escolhe pela chamada; o tipo de retorno, por si só, não distingue duas sobrecargas. Argumentos por defeito permitem omitir os últimos argumentos, por exemplo void imprimir(int n, char separador = ' ');. O valor por defeito tem de estar visível no ponto da chamada. Não o repitas na definição se já o declaraste num cabeçalho.
Valor, referência e const
#include <iostream>
#include <string>
void incrementar_copia(int x) { ++x; }
void incrementar(int& x) { ++x; }
std::size_t comprimento(const std::string& texto) { return texto.size(); }
int main() {
int n = 4;
incrementar_copia(n);
std::cout << n << "\n";
incrementar(n);
std::cout << n << " " << comprimento("Ana Maria") << "\n";
}Executa o código para ver a saída e os gráficos.
Saída:
4
5 9
n começa em 4. incrementar_copia(n) cria um parâmetro independente com o valor 4. Incrementar esse parâmetro não toca em n, que continua 4.
incrementar(n) recebe uma referência ao original e deixa n com 5. comprimento("Ana Maria") conta 9 unidades: 3 de Ana, 1 espaço e 5 de Maria.
Uma referência é um nome alternativo para um objeto. Tem de ser inicializada e nunca muda de destino. Atribuir à referência altera o objeto referido.
| Parâmetro | O que a função recebe | Uso habitual |
|---|---|---|
int n | Um valor independente | Números pequenos, só de leitura |
T& objeto | Acesso ao original | Alteração faz parte da operação |
const T& objeto | Acesso de leitura ao original | Evitar copiar uma string ou coleção |
T* objeto | Um endereço, que pode ser nulo | Opcionalidade ou acesso a um intervalo |
const T& impede alterações através dessa referência; não torna constantes
todas as outras formas de acesso ao objeto. Evitar uma cópia de um int
raramente justifica uma referência.
Retornar um std::string por valor é seguro e permite ao compilador evitar
cópias. Retornar uma referência a uma variável local deixa acesso a um
objeto que já morreu.
Parâmetros apontadores
A secção seguinte passa arrays como const int* valores, que se lê assim: valores guarda o endereço do primeiro inteiro e valores[i] acede ao elemento nessa posição. Ainda não precisas de aritmética de apontadores, que a página Apontadores e memória explica depois com objetos e setas.
Arrays C e dimensões
Um array int v[4] contém quatro inteiros contíguos. Os índices válidos são 0, 1, 2 e 3. v[4] está fora do array. Um array não aumenta de tamanho e não transporta automaticamente o seu comprimento quando é passado como argumento.
Numa lista de parâmetros, const int valores[] é ajustado para const int* valores. Por isso, a função abaixo recebe também n. std::size_t, de <cstddef>, é o tipo inteiro sem sinal usado para tamanhos e índices. Não representa valores negativos.
#include <cstddef>
#include <iostream>
int soma(const int* valores, std::size_t n) {
int total = 0;
for (std::size_t i = 0; i < n; ++i) total += valores[i];
return total;
}
int main() {
int valores[] = {4, -2, 7, 1};
std::cout << soma(valores, 4) << "\n";
std::cout << soma(valores + 1, 2) << "\n";
std::cout << soma(nullptr, 0) << "\n";
}Executa o código para ver a saída e os gráficos.
Saída:
10
5
0
O contrato de soma exige n elementos válidos e que todas as somas parciais caibam em int. Na segunda chamada, o intervalo contém -2 e 7, por isso soma 5. Na chamada vazia, o ciclo não desreferencia o apontador.
| Intervalo | Índices originais | Evolução de total |
|---|---|---|
| Array completo | 0, 1, 2, 3 | 0 → 4 → 2 → 9 → 10 |
| Dois valores a partir de -2 | 1, 2 | 0 → -2 → 5 |
| Vazio | nenhum | 0 |
O índice i é relativo ao início recebido. Na segunda chamada, valores[0] dentro da função é o -2 que está no índice 1 do array do chamador.
sizeof(valores) dentro desta função mede o apontador, não o array do chamador. Para um tamanho fixo com interface que o conserva, usa std::array<T, N>. Para tamanho variável, usa std::vector<T>. Um std::vector<int> guarda uma sequência de inteiros; std::vector<int> v{4, -2, 7} cria três elementos. A página Templates e STL desenvolve esta coleção.
Um for por intervalo, como for (int x : valores), visita cada elemento de um array cujo tamanho é conhecido ou de uma coleção. x recebe uma cópia de cada inteiro; alterar x não altera o elemento. Com for (int& x : valores), x refere-se ao elemento e permite alterá-lo. Um apontador isolado não fornece o tamanho para este percurso.
Para percorrer índices de trás para a frente com std::size_t, que não representa negativos, começa em size() e repete enquanto i > 0. Decrementa primeiro e lê v[i]; em alternativa, lê v[i - 1] e só depois decrementa. Qualquer das ordens evita subtrair um a zero e evita testar i >= 0, que num tipo sem sinal nunca falha.
for (std::size_t i = n; i > 0; ) {
--i;
std::cout << valores[i] << '\n';
}
O exercício de inverter texto treina este percurso com uma string.
Um array C multidimensional tem os elementos da última dimensão contíguos. Em int matriz[2][3], há duas linhas de três inteiros. Percorre com dois índices, um por dimensão:
// Excerto: soma todos os elementos de uma matriz 2x3.
int soma_matriz(const int matriz[][3], int linhas) {
int total = 0;
for (int l = 0; l < linhas; ++l)
for (int c = 0; c < 3; ++c) total += matriz[l][c];
return total;
}
Com {{1, 2, 3}, {4, 5, 6}}, a soma é 21. Uma função que recebe const int matriz[][3] precisa de conhecer a dimensão interior para avançar entre linhas. Isto não equivale a int**: um apontador para linhas de três inteiros e um apontador para apontadores têm representações e tipos diferentes.
Arrays de entrada e de saída
Uma função pode escrever o resultado num array reservado pelo chamador e devolver quantas posições ficaram válidas. O tamanho físico e o número de resultados são diferentes: se há espaço para seis inteiros mas só quatro foram escritos, só essas quatro posições pertencem à saída.
Vamos conservar todas as ocorrências dos valores repetidos, pela ordem original. Em 4, 9, 4, 2, 9, 7, o resultado é 4, 9, 4, 9, não apenas os valores distintos 4, 9. A ocorrência final de 4 também conta, mesmo sem outro 4 depois dela: existe uma ocorrência anterior.
O contrato exige n >= 0, uma entrada com n elementos e uma saída com capacidade para pelo menos n. Os arrays não se sobrepõem e a origem fica intacta. Para n == 0, a função não lê nem escreve, por isso aceita apontadores nulos. Neste algoritmo, usamos só ciclos e arrays, como num enunciado que não permite contentores ou funções da biblioteca.
#include <iostream>
// n >= 0; entrada e saida nao se sobrepoem, com n posicoes validas.
int filtrar_repetidos(const int entrada[], int n, int saida[]) {
int usados = 0;
for (int i = 0; i < n; ++i) {
bool repetido = false;
for (int j = 0; j < n; ++j) {
if (i != j && entrada[i] == entrada[j]) {
repetido = true;
break;
}
}
if (repetido) saida[usados++] = entrada[i];
}
return usados;
}
int main() {
int entrada[] = {4, 9, 4, 2, 9, 7};
int saida[6];
int usados = filtrar_repetidos(entrada, 6, saida);
std::cout << usados << ":";
for (int i = 0; i < usados; ++i) std::cout << " " << saida[i];
std::cout << "\n";
std::cout << filtrar_repetidos(nullptr, 0, nullptr) << "\n";
}Executa o código para ver a saída e os gráficos.
Saída:
4: 4 9 4 9
0
Para cada índice i, procuramos um índice diferente j com o mesmo valor. Não basta comparar o elemento consigo próprio, nem procurar só à sua direita. Ao encontrar uma correspondência, copiamos entrada[i] uma única vez para saida[usados] e avançamos usados.
i | Valor | Existe noutra posição? | Prefixo de saída |
|---|---|---|---|
| 0 | 4 | Sim, em 2 | 4 |
| 1 | 9 | Sim, em 4 | 4, 9 |
| 2 | 4 | Sim, em 0 | 4, 9, 4 |
| 3 | 2 | Não | 4, 9, 4 |
| 4 | 9 | Sim, em 1 | 4, 9, 4, 9 |
| 5 | 7 | Não | 4, 9, 4, 9 |
Antes da iteração i, o prefixo de saida com índices em [0, usados) contém exatamente as ocorrências que passaram o critério nos índices anteriores. Percorrer i por ordem conserva a ordem de entrada. O pior caso faz uma procura linear para cada elemento, com custo quadrático em n; a memória adicional da função é constante, além do destino recebido. A capacidade disponível não inicializa a cauda nem a transforma em resultados. O chamador usa o número devolvido para a percorrer.
Structs e tipos compostos
// Excerto: tipo e acesso aos seus campos.
struct Ponto {
double x;
double y;
};
Ponto p{2.0, 3.0};
A struct reúne campos que descrevem um valor: Ponto é um tipo, p é um objeto desse tipo e p.x vale 2.0. O compilador pode introduzir padding, bytes entre campos ou no fim para alinhar os objetos. Por isso, sizeof(Ponto) não deve ser deduzido somando tamanhos de campos. Não graves a memória bruta de uma struct como formato portátil de ficheiro.
struct e class permitem membros e métodos. A diferença sintática central é o acesso por defeito: público na struct, privado na class. Uma enumeração com âmbito, como enum class Estado { livre, ocupado };, cria valores nomeados e evita confundir um estado com um inteiro qualquer.
Strings e linhas
std::string guarda uma sequência de unidades char. size() conta essas unidades, não caracteres Unicode visuais. Para ler ou alterar unidades do texto, usa índices de 0 a size() - 1; numa string vazia não há nenhuma. at() verifica esse limite e lança uma exceção. texto[texto.size()] permite consultar o terminador nulo, mas não é mais uma unidade do texto nem permite guardar outro carácter nessa posição. + concatena strings e == compara o conteúdo. substr(pos, quantidade) extrai uma parte; find devolve uma posição ou std::string::npos quando não encontra.
Depois de >> ler um número, o fim de linha pode continuar na entrada, por isso um getline imediato leria esse resto vazio. Neste formato, a idade ocupa a primeira linha e o nome a segunda. Descarta o resto da primeira linha antes de leres o nome.
#include <iostream>
#include <limits>
#include <string>
int main() {
int idade = 0;
if (!(std::cin >> idade)) {
std::cout << "Idade invalida\n";
return 0;
}
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::string nome;
if (!std::getline(std::cin, nome)) {
std::cout << "Nome em falta\n";
return 0;
}
std::cout << nome << " | " << idade << " | " << nome.size() << "\n";
}Executa o código para ver a saída e os gráficos.
Saída:
Ana Maria | 19 | 9
ignore descarta até ao \n, incluindo-o. std::getline guarda os espaços interiores e retira o terminador. A alternativa getline(std::cin >> std::ws, nome) descarta também espaços iniciais e linhas vazias; só a uses quando isso corresponde ao formato pretendido.
Para separar campos dentro de uma linha, find localiza um separador e substr extrai cada parte. Com s = "azul,verde", s.find(',') devolve 4. s.substr(0, 4) é "azul" e s.substr(5) é "verde". Quando find não encontra o carácter, devolve std::string::npos; nesse caso não há segundo campo.
Strings de C e o terminador
Uma string de C é uma sequência de char terminada por \0. O array char nome[] = "Ana"; tem quatro posições: A, n, a, \0. O terminador não aparece no texto impresso, mas ocupa memória. <cstring> fornece strlen, que percorre até ao terminador, e operações de cópia. Estas funções exigem que os apontadores e capacidades estejam corretos.
Arrays de char comparados com == não comparam o texto, comparam endereços depois da conversão para apontadores. Usa std::string, ou std::strcmp quando o contrato pede strings de C válidas. strcpy não verifica o tamanho do destino. Reservar só três posições para copiar "Ana" é insuficiente.
Presença de letras em dois textos
Agora queremos as letras que aparecem num dos textos, mas não nos dois. Trata-se da diferença simétrica dos conjuntos de letras. Contar repetições ou retirar caracteres na ordem em que aparecem responderia a outra pergunta.
O alfabeto deste exemplo tem as 26 letras de a a z; a maiúscula correspondente conta como a mesma letra. Espaços, números e pontuação são ignorados. Não estamos a tratar todo o texto Unicode. As duas entradas são strings de C válidas, não nulas, com até 1000 unidades char. O destino não se sobrepõe às entradas e tem pelo menos 27 posições: uma por letra possível e outra para \0.
Guardamos duas tabelas de presença, inicialmente falsas. Depois de ler um texto, em_a[i] diz se a letra de índice i apareceu pelo menos uma vez. Marcar novamente a mesma posição não muda o conjunto. indice_letra procura em dois alfabetos explícitos, sem depender de subtrair códigos dos caracteres nem de chamar uma função da biblioteca.
#include <iostream>
int indice_letra(char letra) {
const char minusculas[] = "abcdefghijklmnopqrstuvwxyz";
const char maiusculas[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (int i = 0; i < 26; ++i)
if (letra == minusculas[i] || letra == maiusculas[i]) return i;
return -1;
}
// texto e uma string de C valida com, no maximo, 1000 unidades char.
void marcar_letras(const char texto[], bool presentes[]) {
for (int i = 0; texto[i] != '\0'; ++i) {
int indice = indice_letra(texto[i]);
if (indice >= 0) presentes[indice] = true;
}
}
// a e b sao validas; saida tem 27 posicoes e nao se sobrepoe as entradas.
int diferenca_letras(const char a[], const char b[], char saida[]) {
bool em_a[26]{};
bool em_b[26]{};
marcar_letras(a, em_a);
marcar_letras(b, em_b);
const char alfabeto[] = "abcdefghijklmnopqrstuvwxyz";
int usados = 0;
for (int i = 0; i < 26; ++i)
if (em_a[i] != em_b[i]) saida[usados++] = alfabeto[i];
saida[usados] = '\0';
return usados;
}
int main() {
char saida[27];
int n = diferenca_letras("ac AB!", "B-cd", saida);
std::cout << n << " [" << saida << "]\n";
n = diferenca_letras("abAB", "ba", saida);
std::cout << n << " [" << saida << "]\n";
}Executa o código para ver a saída e os gráficos.
Saída:
2 [ad]
0 []
Em "ac AB!", ficam presentes a, b, c; em "B-cd", b, c, d. As letras b e c aparecem nos dois e são excluídas. A condição em_a[i] != em_b[i] é verdadeira só para a e d. Percorrer a tabela do índice 0 ao 25 escreve o resultado alfabeticamente, uma vez por letra, sem ordenar o texto original.
No segundo caso, "abAB" repete letras que "ba" contém uma vez, mas os conjuntos são iguais. A saída é vazia e continua a precisar de saida[0] = '\0'. O número devolvido conta letras, sem incluir o terminador. No maior resultado, escrevemos 26 letras e o terminador na posição 26; reservar só 26 posições falharia precisamente nesse caso.
A procura de cada unidade percorre no máximo 26 posições do alfabeto, comparando maiúscula e minúscula. No fim, percorremos as 26 presenças. Com este alfabeto fixo, o tempo é linear no comprimento total dos textos e a memória adicional é constante. O algoritmo não usa string, set, sort nem tolower; o main apenas usa cout para mostrar o resultado. As ferramentas permitidas fazem parte do enunciado, além do resultado esperado.
Recursão e progresso
Uma função recursiva chama-se a si própria e precisa de um caso base com uma medida que diminua até esse caso. A soma recursiva de 1 até n pode usar soma(0) = 0 e soma(n) = n + soma(n - 1) para n > 0. O contrato exige n >= 0 e uma soma representável no tipo escolhido. Se não tratares 0, podes criar chamadas sem fim.
Para n = 3, a descida constrói chamadas e a subida combina resultados:
| Passo | Chamada | Resultado |
|---|---|---|
| descer | soma(3) = 3 + soma(2) | pendente |
| descer | soma(2) = 2 + soma(1) | pendente |
| descer | soma(1) = 1 + soma(0) | pendente |
| base | soma(0) = 0 | 0 |
| subir | 1 + 0 | 1 |
| subir | 2 + 1 | 3 |
| subir | 3 + 3 | 6 |
A descida reduz n e a subida combina os resultados. Uma entrada negativa nunca se aproxima do caso base.
Cada chamada conserva os seus parâmetros e variáveis locais. Um grande número de chamadas pode esgotar a pilha. Quando um ciclo resolve o mesmo problema com um acumulador, não precisas de recursão. Usa-a quando a decomposição do problema a torna clara, não como substituição automática de qualquer ciclo.
Exercícios
A referência não muda de destino
Segue int a = 4; int b = 9; int& r = a; r = b; b = 2;.
Quanto vale a no fim?
Pista
Lembra-te de que r é um nome alternativo para a.
Solução
r = b guarda 9 em a. A última
atribuição altera apenas b para 2. O resultado é
a = 9, b = 2 e r = 9.
Passar um subintervalo válido
Um array v contém 1, 2, 3, 4. Qual chamada pode
ler o intervalo inteiro sem sair do array? A função
soma(inicio, n)
lê n inteiros a partir de inicio e não lê nenhum
quando n é zero.
Pista
Marca os índices 0 a 3 e a posição logo depois do fim.
Solução
soma(v + 2, 2) lê os índices 2 e 3.
O tamanho deve acompanhar o intervalo real, não o tamanho inicial do
array.
soma(v + 2, 2). Os dois elementos são os índices 2 e 3.soma(v + 2, 3). A terceira leitura seria v[4], fora do array.soma(v + 4, 1). v + 4 marca o fim e não aponta para um elemento.soma(nullptr, 1). Uma leitura por um apontador nulo viola o contrato.
Erros frequentes
Formar uma posição depois do fim pode ser válido; ler a partir dela não é.
Programar uma contagem num intervalo fechado
O contrato pede o número de valores em [minimo, maximo], incluindo os extremos: um valor igual a minimo ou a maximo conta. Corrige a função sem alterar os dados de teste. O programa inicial compila e corre, mas os resultados nos limites estão errados. A técnica de testar limites é desenvolvida em Exceções e testes.
#include <iostream>
#include <vector>
int contar(const std::vector<int>& valores, int minimo, int maximo) {
int total = 0;
for (int x : valores)
if (minimo < x && x < maximo) ++total;
return total;
}
int main() {
std::cout << "Vazio: " << contar({}, 1, 3) << " (esperado 0)\n";
std::cout << "Limites: " << contar({1, 2, 3}, 1, 3) << " (esperado 3)\n";
std::cout << "Repetidos: " << contar({2, 2, 2}, 2, 2) << " (esperado 3)\n";
}Executa o código para ver a saída e os gráficos.
Pista
Compara a condição com a definição de intervalo fechado.
Solução
A condição correta é minimo <= x && x <= maximo
. O vazio continua a dar zero, o intervalo de 1 a 3 inclui os três valores
e o intervalo reduzido a 2 inclui as três repetições. A referência
constante conserva a coleção original.
Confere a tua resposta:
- Incluí os dois limites.
- Obtive 0, 3 e 3 nos casos apresentados.
- Mantive a coleção sem alterações e sem cópias desnecessárias.
Erros frequentes
Não alteres só um dos limites. Não mudes os resultados esperados para esconder a falha.
Construir uma string invertida
Define std::string inverter(const std::string& texto) para
devolver as unidades do texto pela ordem inversa. Usa apenas texto ASCII,
como abc, e conserva o argumento. A string vazia devolve uma
string vazia. Esta resposta é de autoavaliação.
#include <iostream>
#include <string>
std::string inverter(const std::string& texto) {
return texto; // Substitui pelo algoritmo.
}
int main() {
std::string original = "abc";
std::cout << "[" << inverter("") << "] (esperado [])\n";
std::cout << inverter("x") << " (esperado x)\n";
std::cout << inverter(original) << " (esperado cba)\n";
std::cout << inverter("abcd") << " (esperado dcba)\n";
std::cout << original << " (esperado abc)\n";
}Executa o código para ver a saída e os gráficos.
Pista
Mais uma pista
Começa com i = texto.size(). Enquanto i > 0,
lê texto[i - 1] e diminui i. Assim, nunca subtrais
um a zero para formar um índice.
Solução
std::string inverter(const std::string& texto) {
std::string resultado;
for (std::size_t i = texto.size(); i > 0; --i)
resultado += texto[i - 1];
return resultado;
}Para abc, os índices lidos são 2, 1 e 0. No vazio, o ciclo não
começa. O argumento só é lido; o resultado tem armazenamento próprio.
Outra solução válida copia o texto e troca pares de posições simétricas.
Confere a tua resposta:
- Devolvi uma string e não imprimi dentro da função.
- Tratei o vazio, um carácter e comprimentos pares e ímpares.
- Conservei o texto original e justifiquei os índices usados.
Erros frequentes
size() usa um tipo sem sinal. Começar em
size() - 1 no vazio, ou testar i >= 0 para
terminar uma contagem descendente sem sinal, dá limites errados. Inverter
bytes de texto UTF-8 também pode estragar caracteres, por isso o contrato
deste exercício limita a entrada a ASCII.
Somar uma linha de uma matriz
A matriz recebida tem duas linhas válidas, linha é 0 ou 1 e a soma cabe em int.
Escreve int soma_linha(const int matriz[][3], int linha) que
devolve a soma das três colunas dessa linha. Usa a matriz
{{1, 2, 3}, {4, 5, 6}} como caso de teste.
Pista
Um ciclo de c = 0 até c < 3 chega para uma linha fixa.
Solução
int soma_linha(const int matriz[][3], int linha) {
int total = 0;
for (int c = 0; c < 3; ++c) total += matriz[linha][c];
return total;
}
// Teste: com m = {{1, 2, 3}, {4, 5, 6}},
// soma_linha(m, 0) devolve 6 e soma_linha(m, 1) devolve 15.Com a matriz do enunciado, a chamada com linha 0 soma
1 + 2 + 3 e devolve 6; com linha 1
soma 4 + 5 + 6 e devolve 15. A dimensão interior
3 tem de estar na assinatura para o compilador calcular o
endereço de matriz[linha][c].
Confere a tua resposta:
- Percorri as três colunas da linha indicada.
- Obtive 6 para a primeira linha de {{1, 2, 3}, {4, 5, 6}}.
- Justifiquei por que a função precisa da dimensão interior.
Erros frequentes
Não declares o parâmetro como int**. São representações
diferentes.
A última ocorrência também conta
Uma função deve copiar todas as ocorrências dos valores que aparecem mais de uma vez, pela ordem de entrada. Recebe n >= 0, arrays distintos e sem sobreposição, com n posições válidas em cada um. Não pode usar contentores nem funções da biblioteca. A entrada vazia deve devolver zero sem aceder aos apontadores.
Analisa esta proposta com 3, -1, 3, 8, -1, -1, 5. Prevê o que escreve, calcula o resultado pedido e corrige a condição de procura. Explica também quantas posições do destino podem ser lidas depois da chamada.
int filtrar(const int entrada[], int n, int saida[]) {
int usados = 0;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (entrada[i] == entrada[j]) {
saida[usados++] = entrada[i];
break;
}
}
}
return usados;
}Pista
A ocorrência de índice 2 precisa de procurar antes ou depois de si?
Solução
A proposta escreve 3, -1, -1 e devolve 3: encontra parceiros à direita dos índices 0, 1 e 4. Perde o 3 do índice 2 e o -1 do índice 5, que só têm parceiros anteriores. O resultado pedido é 3, -1, 3, -1, -1, com cinco elementos.
Para cada i, percorre todos os j de 0 a n - 1 e exige j != i além da igualdade dos valores. Encontrar o próprio elemento não demonstra repetição. Ao encontrar outro, copia uma vez e termina essa procura. O percurso exterior conserva a ordem e não altera a entrada. Não uses uma coleção de valores únicos, pois o contrato conserva cada ocorrência repetida.
O chamador pode ler só o prefixo de tamanho devolvido, neste caso cinco posições, mesmo que tenha reservado sete. No vazio, o ciclo exterior não executa e o resultado é zero. Casos com um elemento e com todos distintos também dão zero; um caso em que todos os valores se repetem conserva o array completo.
Confere a tua resposta:
- Obtive 3, -1, 3, -1, -1, com cinco posições válidas.
- Identifiquei as últimas ocorrências perdidas pela procura só à direita.
- Corrigi a procura, conservei a origem e respeitei a capacidade e o vazio.
Repetir uma letra não muda o conjunto
Define int diferenca_letras(const char a[], const char b[], char saida[]) que escreve as letras presentes num dos textos, mas não nos dois, e devolve o número de letras escritas. Usa as 26 letras de a a z, ignora os restantes caracteres e trata maiúsculas como as minúsculas correspondentes. As entradas são strings de C válidas com até 1000 unidades char; o destino, sem sobreposição, tem 27 posições. Não uses contentores nem funções da biblioteca.
Prevê o resultado para a = "Bbb+c" e b = "b D!". Uma proposta subtrai as frequências e inclui b porque aparece três vezes no primeiro e uma no segundo. Explica o erro e testa também textos sem letras, conjuntos iguais e uma saída com as 26 letras.
Solução
O primeiro conjunto é b,c e o segundo é b,d. A letra b pertence aos dois, por isso fica excluída apesar da frequência diferente. Só c e d pertencem a um dos textos. A função escreve "cd", acrescenta \0 e devolve 2.
Inicializa duas tabelas de 26 valores falsos. Percorre cada texto até \0, localiza a letra num alfabeto de maiúsculas/minúsculas e marca a sua presença; ignorar um carácter é diferente de o usar como índice. Percorre depois o alfabeto e copia apenas as posições em que as duas presenças diferem. Isto produz ordem alfabética sem ordenar as entradas.
Textos sem letras e conjuntos iguais produzem uma string vazia, com saida[0] = '\0'. Se uma entrada contém todo o alfabeto e a outra não tem letras, o resultado ocupa as posições 0 a 25 e o terminador ocupa a 26. O comprimento devolvido não inclui esse terminador. Compara estes casos com a implementação da lição, que mantém as entradas intactas.
Confere a tua resposta:
- Obtive duas letras, cd, ignorando maiúsculas e repetições.
- Usei presença por texto e não uma diferença de frequências.
- Escrevi em ordem alfabética e reservei o terminador, incluindo no vazio e no resultado de 26 letras.