Reforço de Algoritmos e Programação - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



Aula 5 - Strings e Matrizes

Nesta aula veremos duas extensões naturais do que aprendemos sobre vetores (Aula 4): as strings (vetores de caracteres com uma regra especial de término) e as matrizes (vetores de duas dimensões, organizados em linhas e colunas).


1. O que é uma string em C?

Diferente de outras linguagens, C não possui um tipo de dado dedicado para texto. Uma string em C é, na verdade, um vetor de char (Aula 4), com uma convenção especial: o último caractere útil é sempre seguido por um caractere nulo, escrito como '\0', que marca onde o texto termina.

char nome[10] = {'J', 'o', 'a', 'o', '\0'};

Escrever uma string caractere por caractere, como acima, é raro na prática — o mais comum é usar uma string literal entre aspas duplas, e deixar que o compilador acrescente o '\0' automaticamente:

char nome1[10] = "Joao"; // o compilador acrescenta o '\0' sozinho
char nome2[] = "Maria";  // tamanho calculado automaticamente (6: 5 letras + '\0')
O '\0' "gasta" uma posição. Um vetor char nome[10] guarda, no máximo, uma palavra de 9 caracteres visíveis, já que a décima posição precisa ficar reservada para o '\0'. Esquecer disso é uma fonte comum de erros ao declarar o tamanho de uma string.

2. Entrada e saída de strings

Para exibir uma string, o já conhecido printf com o especificador %s funciona normalmente:

printf("Nome: %s\n", nome1);

Para ler uma string, uma primeira tentativa seria usar scanf com %s:

char nome[50];
printf("Digite seu nome: ");
scanf("%s", nome); // sem & antes de "nome" (veja o quadro abaixo)
Por que não usamos & aqui? Com variáveis simples (Aula 1) e posições de um vetor (Aula 4), sempre usávamos & para informar ao scanf um endereço de memória. Um vetor, porém, já é um endereço (o endereço da sua primeira posição) quando usado sozinho, sem colchetes — por isso nome sozinho já serve, sem precisar de &nome. Esse comportamento dos vetores será explicado com mais detalhes quando estudarmos ponteiros, mais adiante na disciplina.

O problema é que scanf("%s", ...) para de ler assim que encontra um espaço em branco (espaço, tabulação ou quebra de linha) — ele não lê "uma linha", mas sim "uma palavra". Se o usuário digitar um nome com espaço:

Digite seu nome: Joao Silva

a variável nome conterá apenas "Joao" — o "Silva" é simplesmente descartado (ou pior: pode acabar sendo lido pela próxima chamada de scanf do programa, causando um comportamento confuso mais adiante). Por esse motivo, scanf("%s", ...) não deve ser usado para ler texto que possa conter espaços — praticamente qualquer nome completo, frase ou linha de texto "de verdade".

gets e fgets: lendo uma linha inteira

Para ler uma linha inteira, incluindo espaços, existem duas funções: a antiga gets() e a moderna fgets().

char nomeCompleto[50];

// com gets() — sintaxe simples, mas insegura
gets(nomeCompleto);

// com fgets() — um pouco mais verbosa, porém segura
fgets(nomeCompleto, 50, stdin);
gets(destino) fgets(destino, tamanho, stdin)
Vantagem Sintaxe simples, só o vetor de destino. Segura: nunca escreve além dos tamanho caracteres informados.
Desvantagem Não recebe um limite de tamanho — se o usuário digitar mais caracteres do que o vetor comporta, o excesso é escrito além dos limites do vetor (buffer overflow), corrompendo memória vizinha. Mais verbosa; e mantém o caractere de quebra de linha ('\n') dentro da string, caso ele caiba no espaço informado (veja o quadro abaixo).
Situação atual Considerada insegura; removida dos padrões mais recentes da linguagem C. É a alternativa recomendada nos padrões atuais da linguagem.
gets() não deve ser usado "na vida real". Por não impor nenhum limite ao número de caracteres lidos, gets() é uma das causas históricas mais conhecidas de falhas de segurança em programas C (o famoso "buffer overflow"). Ele é mostrado aqui apenas para fins de comparação e para que vocês o reconheçam caso o encontrem em código antigo — em qualquer programa novo, utilize sempre fgets() no lugar dele.

Ao usar fgets, é importante saber que, se a linha digitada (mais o '\n' do Enter) couber dentro do tamanho informado, o próprio '\n' é guardado dentro da string, antes do '\0'. Isso costuma ser indesejado (por exemplo, ao comparar a string lida com outra usando strcmp), então uma prática comum é substituir esse '\n' por um '\0' logo em seguida, "recortando" a string na posição certa:

#include <string.h>

char nomeCompleto[50];
fgets(nomeCompleto, 50, stdin);

nomeCompleto[strcspn(nomeCompleto, "\n")] = '\0'; // remove o '\n' e recoloca o '\0' no lugar certo

strcspn(s, "\n") retorna a posição do primeiro caractere de s que aparece dentro do conjunto informado (aqui, apenas '\n') — ou seja, exatamente o índice onde está a quebra de linha deixada pelo fgets. Sobrescrever essa posição com '\0' é o que "corta" a string ali, descartando o '\n'.

Sempre garanta o '\0' no lugar certo. Seja usando fgets (removendo o '\n' residual, como acima) ou construindo uma string manualmente (como fizemos em exercícios anteriores), sempre confirme que o último caractere válido é seguido imediatamente por um '\0'. Uma string sem o terminador no lugar certo pode fazer com que printf("%s", ...), strlen e outras funções "leiam" além do que deveriam, produzindo lixo de memória ou comportamento imprevisível.
Combine com seu professor. Diferentes professores (e diferentes materiais) podem ter preferências diferentes sobre usar scanf, gets ou fgets nos exercícios de sala de aula, dependendo do que já foi ensinado e do nível da turma. Mostramos aqui as opções para que vocês reconheçam cada uma e entendam seus riscos, mas confira com o seu professor qual forma ele recomenda usar nos programas desta disciplina.

3. A biblioteca string.h

A biblioteca padrão <string.h> reúne funções prontas para trabalhar com strings, evitando que precisemos escrever laços manuais para tarefas comuns:

Função Descrição Exemplo
strlen(s) Retorna o número de caracteres de s, sem contar o '\0'. int tam = strlen(nome);
strcpy(destino, origem) Copia o conteúdo de origem para destino. strcpy(copia, nome);
strcmp(s1, s2) Compara duas strings; retorna 0 se forem iguais. if (strcmp(senha, "123") == 0)
strcat(destino, origem) Concatena (junta) origem ao final de destino. strcat(saudacao, nome);
#include <stdio.h>
#include <string.h>

int main() {
    char saudacao[30] = "Ola, ";
    char nome[10] = "Maria";

    strcat(saudacao, nome);
    printf("%s\n", saudacao); // imprime "Ola, Maria"
    printf("Tamanho: %lu\n", strlen(saudacao));

    if (strcmp(nome, "Maria") == 0) {
        printf("E a Maria!\n");
    }

    return 0;
}
Cuidados importantes.
  • Nunca compare o conteúdo de duas strings com == (por exemplo, nome == "Maria") — isso compara os endereços dos vetores, não o texto neles. Use sempre strcmp.
  • A atribuição direta (nome = "Outro texto";, após a declaração) não é permitida em C. Use strcpy para copiar um novo conteúdo em uma string já existente.
  • Ao usar strcpy ou strcat, garanta que o vetor de destino tenha espaço suficiente para o resultado (incluindo o '\0') — do contrário, corre-se o risco de escrever além dos limites do vetor.

4. Convertendo entre números e strings

É comum precisar converter um número para texto (para montar uma mensagem) ou um texto para número (para calcular algo a partir de uma entrada lida como string). C oferece funções prontas para isso, todas de <stdlib.h> (números → string usa <stdio.h>):

Função Converte Exemplo
atoi(string) stringint int n = atoi("42");n vale 42
atof(string) stringdouble double x = atof("3.14");x vale 3.14
snprintf(destino, tamanho, formato, valor) número → string snprintf(texto, 20, "%d", 42);texto vira "42"

snprintf funciona como printf, mas em vez de escrever na tela, escreve o resultado dentro de uma string (o primeiro argumento) — o segundo argumento, tamanho, limita quantos caracteres podem ser escritos no destino, evitando que o resultado ultrapasse o vetor (o mesmo motivo pelo qual preferimos fgets a gets):

#include <stdio.h>
#include <stdlib.h>

int main() {
    char texto[20];
    int idade = atoi("25"); // string -> int

    snprintf(texto, 20, "Idade: %d", idade); // int -> string, no máximo 20 caracteres
    printf("%s\n", texto); // Idade: 25

    return 0;
}
Objetivamente: se a string não representar um número válido, atoi/atof retornam 0, sem avisar sobre o erro — cuidado ao usá-las com entradas não confiáveis. Para maior controle sobre erros de conversão, existe também strtol (mais robusta, porém mais verbosa), que foge do escopo objetivo desta seção.

5. Matrizes

Uma matriz é um vetor de duas dimensões: em vez de um único índice, cada elemento é acessado por um par (linha, coluna). É útil para representar grades, tabuleiros, tabelas e imagens.

int matriz[3][4]; // 3 linhas, 4 colunas (12 elementos no total)

Assim como vetores, os índices de linha e de coluna também começam em 0. Uma matriz 3x4 tem linhas 0, 1 e 2, e colunas 0, 1, 2 e 3.

matriz[0][0] = 10; // primeira linha, primeira coluna
matriz[2][3] = 99; // última linha, última coluna

6. Percorrendo uma matriz com laços aninhados

Assim como usamos for para percorrer um vetor, percorremos uma matriz com dois laços aninhados (Aula 3): um para as linhas, outro para as colunas.

#include <stdio.h>

int main() {
    int matriz[3][4];
    int i, j;

    // preenchendo a matriz
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 4; j++) {
            printf("Digite o valor [%d][%d]: ", i, j);
            scanf("%d", &matriz[i][j]);
        }
    }

    // exibindo a matriz
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 4; j++) {
            printf("%d ", matriz[i][j]);
        }
        printf("\n"); // nova linha ao terminar cada linha da matriz
    }

    return 0;
}

Repare no printf("\n") fora do laço interno, mas dentro do laço externo: ele garante que cada linha da matriz seja impressa em uma linha separada na tela, em vez de todos os 12 números seguidos.


7. Matrizes com mais de duas dimensões

Assim como uma matriz é um vetor de vetores, nada impede de ir além de duas dimensões: C permite declarar vetores com três, quatro, ou mais dimensões, bastando acrescentar mais colchetes:

int cubo[2][3][4]; // 3 dimensões: 2 x 3 x 4 = 24 elementos no total

Uma forma útil de visualizar uma matriz 3D é como uma pilha de matrizes 2D: cubo[k] é, sozinho, uma matriz 3x4 comum (a "camada" k), e cubo é um vetor de 2 dessas camadas. Percorrer uma matriz 3D exige, naturalmente, três laços aninhados — um para cada dimensão:

#include <stdio.h>

int main() {
    int cubo[2][3][4];
    int i, j, k;

    // preenche cubo[i][j][k] com i*100 + j*10 + k, só para ter valores distintos
    for (i = 0; i < 2; i++) {
        for (j = 0; j < 3; j++) {
            for (k = 0; k < 4; k++) {
                cubo[i][j][k] = i * 100 + j * 10 + k;
            }
        }
    }

    printf("%d\n", cubo[1][2][3]); // imprime 123

    return 0;
}
Use com moderação. Matrizes com três, quatro ou mais dimensões existem e têm aplicações legítimas (por exemplo, representar um conjunto de imagens coloridas, cada uma com linha, coluna e canal de cor), mas não são tão usadas no dia a dia quanto vetores e matrizes 2D. Cada dimensão extra significa mais um laço aninhado, e o código rapidamente fica difícil de ler e de depurar — um erro de índice em um for aninhado com 4 níveis é bem mais difícil de encontrar do que em um único laço.

Antes de recorrer a uma matriz de muitas dimensões, vale considerar uma alternativa mais simples: relacionar vetores (ou matrizes) diferentes através do mesmo índice. Por exemplo, em vez de uma única estrutura 4D guardando nome, idade e nota de vários alunos por turma, muitas vezes é mais claro usar vetores separados que "andam juntos" pelo mesmo índice:

// em vez de uma única estrutura com muitas dimensões...
float notas[3][30][4]; // 3 turmas, 30 alunos, 4 notas cada — já difícil de ler

// ...pode ser mais simples usar vetores separados, relacionados pelo índice
char nomes[30][50];   // nomes[i] e notasAluno[i] descrevem o MESMO aluno i
float notasAluno[30][4];

Dessa forma, nomes[i] e notasAluno[i] sempre se referem ao mesmo aluno i, sem precisar de uma única estrutura gigante com muitas dimensões — o código fica mais simples de ler, e cada parte pode ser percorrida e testada separadamente. Essa ideia de "várias estruturas relacionadas pelo mesmo índice" será revisitada mais adiante na disciplina, quando estudarmos structs.


Resumo

  • String: vetor de char terminado por '\0'; o tamanho declarado precisa incluir espaço para esse terminador.
  • Entrada de strings: scanf("%s", ...) para de ler ao encontrar um espaço, e não deve ser usado para texto com espaços; gets lê a linha inteira mas é insegura (não deve ser usada); fgets é a alternativa segura e recomendada, lembrando de tratar o '\n' residual.
  • string.h: strlen, strcpy, strcmp, strcat — nunca compare strings com ==, use strcmp.
  • Matriz: vetor de duas dimensões, acessado por [linha][coluna], ambos começando em 0.
  • Percorrer uma matriz exige dois laços aninhados: o externo para as linhas, o interno para as colunas.
  • Matrizes N-dimensionais existem (um laço aninhado por dimensão), mas devem ser usadas com moderação — prefira relacionar vetores/matrizes separados pelo mesmo índice quando possível.

Exercícios

Nível fácil

  1. Escreva um programa que declare uma string com seu nome (usando uma string literal) e imprima o nome e o seu tamanho (com strlen).
  2. Escreva um programa que leia duas palavras (sem espaços, usando scanf("%s", ...) para cada uma) e informe se elas são iguais, usando strcmp.
  3. Escreva um programa que declare uma matriz 3x3, preencha todas as posições com o valor 0, e depois a imprima.

Nível médio

  1. Escreva um programa que leia uma string (com fgets) e conte quantas vezes a letra 'a' (maiúscula ou minúscula) aparece nela, percorrendo a string caractere por caractere até encontrar o '\0'.
  2. Escreva um programa que leia uma matriz 3x3 de inteiros e calcule a soma de cada linha e a soma de cada coluna, imprimindo os resultados.
  3. Escreva um programa que leia uma string e a inverta (por exemplo, "casa" deve virar "asac"), imprimindo o resultado.

Nível difícil

  1. Escreva um programa que leia uma string e verifique, sem usar nenhuma função pronta de string.h além de strlen, se ela é um palíndromo (lida da esquerda para a direita é igual a lida da direita para a esquerda — por exemplo, "arara" e "ovo" são palíndromos). Ignore diferenças entre maiúsculas e minúsculas.
  2. Escreva um programa que leia uma matriz quadrada n x n de inteiros e calcule sua transposta (a matriz onde a linha i, coluna j da original vira a linha j, coluna i do resultado), armazenando o resultado em uma segunda matriz e imprimindo-a ao final.
  3. Declare uma matriz int turmas[2][3][4], representando 2 turmas, cada uma com 3 alunos, cada aluno com 4 notas. Preencha-a lendo todos os valores do usuário (usando três laços aninhados), e depois calcule e imprima a média de notas de cada aluno de cada turma.

Sugestões de Respostas dos Exercícios

Exercício 1
#include <stdio.h>
#include <string.h>

int main() {
    char nome[20] = "Dennis";

    printf("Nome: %s\n", nome);
    printf("Tamanho: %lu\n", strlen(nome));

    return 0;
}

Exercício 2
#include <stdio.h>
#include <string.h>

int main() {
    char palavra1[50], palavra2[50];

    printf("Digite a primeira palavra: ");
    scanf("%s", palavra1);
    printf("Digite a segunda palavra: ");
    scanf("%s", palavra2);

    if (strcmp(palavra1, palavra2) == 0) {
        printf("As palavras sao iguais\n");
    } else {
        printf("As palavras sao diferentes\n");
    }

    return 0;
}

Exercício 3
#include <stdio.h>

int main() {
    int matriz[3][3];
    int i, j;

    for (i = 0; i < 3; i++) {
        for (j = 0; j < 3; j++) {
            matriz[i][j] = 0;
        }
    }

    for (i = 0; i < 3; i++) {
        for (j = 0; j < 3; j++) {
            printf("%d ", matriz[i][j]);
        }
        printf("\n");
    }

    return 0;
}

Exercício 4
#include <stdio.h>

int main() {
    char texto[100];
    int i, contador = 0;

    printf("Digite uma frase: ");
    fgets(texto, 100, stdin);

    for (i = 0; texto[i] != '\0'; i++) {
        if (texto[i] == 'a' || texto[i] == 'A') {
            contador++;
        }
    }

    printf("A letra 'a' aparece %d vez(es)\n", contador);

    return 0;
}

O laço usa a própria condição de parada da string (texto[i] != '\0') em vez de um tamanho fixo — assim, o laço se adapta automaticamente ao tamanho real do texto digitado, sem precisar chamar strlen antes.


Exercício 5
#include <stdio.h>

int main() {
    int matriz[3][3];
    int i, j, somaLinha, somaColuna;

    for (i = 0; i < 3; i++) {
        for (j = 0; j < 3; j++) {
            printf("Digite o valor [%d][%d]: ", i, j);
            scanf("%d", &matriz[i][j]);
        }
    }

    // soma de cada linha
    for (i = 0; i < 3; i++) {
        somaLinha = 0;
        for (j = 0; j < 3; j++) {
            somaLinha += matriz[i][j];
        }
        printf("Soma da linha %d: %d\n", i, somaLinha);
    }

    // soma de cada coluna
    for (j = 0; j < 3; j++) {
        somaColuna = 0;
        for (i = 0; i < 3; i++) {
            somaColuna += matriz[i][j];
        }
        printf("Soma da coluna %d: %d\n", j, somaColuna);
    }

    return 0;
}

Note que, para somar linhas, o laço externo percorre i e o interno percorre j (somando matriz[i][j] para i fixo); para somar colunas, invertemos: o laço externo percorre j, e o interno percorre i (agora j é quem fica fixo enquanto somamos).


Exercício 6
#include <stdio.h>
#include <string.h>

int main() {
    char original[50];
    char invertida[50];
    int tamanho, i;

    printf("Digite uma palavra: ");
    scanf("%s", original);

    tamanho = strlen(original);

    for (i = 0; i < tamanho; i++) {
        invertida[i] = original[tamanho - 1 - i]; // pega de trás para frente
    }
    invertida[tamanho] = '\0'; // não esquecer do terminador!

    printf("Invertida: %s\n", invertida);

    return 0;
}

A posição i do novo vetor recebe o caractere que está na posição tamanho - 1 - i do vetor original — quando i = 0, pegamos o último caractere original; quando i é o último índice, pegamos o primeiro. E, como invertida é construída manualmente, é essencial adicionar o '\0' ao final, ou a string ficaria sem um terminador válido.


Exercício 7
#include <stdio.h>
#include <string.h>

int main() {
    char palavra[50];
    int tamanho, i;
    int ehPalindromo = 1;
    char a, b;

    printf("Digite uma palavra: ");
    scanf("%s", palavra);

    tamanho = strlen(palavra);

    for (i = 0; i < tamanho / 2; i++) {
        a = palavra[i];
        b = palavra[tamanho - 1 - i];

        // normaliza para minúscula, ignorando maiusculas/minusculas
        if (a >= 'A' && a <= 'Z') a = a - 'A' + 'a';
        if (b >= 'A' && b <= 'Z') b = b - 'A' + 'a';

        if (a != b) {
            ehPalindromo = 0;
        }
    }

    if (ehPalindromo) {
        printf("E um palindromo\n");
    } else {
        printf("Nao e um palindromo\n");
    }

    return 0;
}

Só precisamos comparar até a metade da palavra (i < tamanho / 2): comparar o par completo (início/fim, depois o par seguinte, e assim por diante) já garante checar a palavra inteira, sem repetir comparações. A conversão manual de maiúscula para minúscula (a - 'A' + 'a') funciona porque, na tabela ASCII, as letras maiúsculas e minúsculas mantêm a mesma distância relativa entre si.


Exercício 8
#include <stdio.h>

int main() {
    int original[10][10];
    int transposta[10][10];
    int n, i, j;

    printf("Digite o tamanho da matriz (n x n): ");
    scanf("%d", &n);

    for (i = 0; i < n; i++) {
        for (j = 0; j < n; j++) {
            printf("Digite o valor [%d][%d]: ", i, j);
            scanf("%d", &original[i][j]);
        }
    }

    for (i = 0; i < n; i++) {
        for (j = 0; j < n; j++) {
            transposta[j][i] = original[i][j]; // inverte linha e coluna
        }
    }

    printf("Matriz transposta:\n");
    for (i = 0; i < n; i++) {
        for (j = 0; j < n; j++) {
            printf("%d ", transposta[i][j]);
        }
        printf("\n");
    }

    return 0;
}

A ideia da transposta é simples de expressar, mas fácil de confundir na hora de escrever: o elemento que estava na linha i, coluna j da matriz original passa a ficar na linha j, coluna i da nova matriz — por isso transposta[j][i] = original[i][j], com os índices "trocados" de um lado para o outro da atribuição.


Exercício 9
#include <stdio.h>

int main() {
    int turmas[2][3][4]; // 2 turmas, 3 alunos, 4 notas
    int t, a, n;
    int soma;

    // leitura
    for (t = 0; t < 2; t++) {
        for (a = 0; a < 3; a++) {
            for (n = 0; n < 4; n++) {
                printf("Turma %d, aluno %d, nota %d: ", t, a, n);
                scanf("%d", &turmas[t][a][n]);
            }
        }
    }

    // média de cada aluno de cada turma
    for (t = 0; t < 2; t++) {
        for (a = 0; a < 3; a++) {
            soma = 0;
            for (n = 0; n < 4; n++) {
                soma += turmas[t][a][n];
            }
            printf("Turma %d, aluno %d: media %.2f\n", t, a, soma / 4.0);
        }
    }

    return 0;
}

Note como cada dimensão ganha o seu próprio laço, sempre na mesma ordem em que aparece na declaração (turmas[t][a][n]): t para turma, a para aluno, n para nota. Já com apenas três dimensões, o código começa a ficar mais difícil de acompanhar — é exatamente esse o tipo de situação em que vale a pena considerar a alternativa comentada acima (vetores/matrizes separados, relacionados pelo mesmo índice), especialmente se surgir a necessidade de uma quarta dimensão.