SOP-strings

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar

Strings na linguagem C

Uma variável string representa uma cadeia de caracteres (ou sequência de caracteres). A própria palavra string pode ser traduzida para cadeia, no sentido de um encadeamento de coisas. Como não existe um tipo de dados string na linguagem C (ao contrário do Portugol, que possui o tipo de dados Texto), strings são representadas por vetores de caracteres.

int main() {
  char palavra[16];

  printf("Digite uma palavra de até 15 letras: ");
  scanf("%s", palavra);

  printf("Você digitou %s\n", palavra);
}

No exemplo acima, foi declarada uma variável string chamada palavra, que pode conter até 15 caracteres. A quantidade de caracteres de que pode ser composta uma string é sempre uma unidade a menos que a capacidade do vetor, porque uma posição do vetor é reservada para assinalar o final da string. Olhando como uma cadeia de caracteres fica armazenada no vetor ajuda a entender isto, imaginando que foi digitado Teste:

<graphviz> digraph Frase { Frase [shape=Mrecord,label="<0>T|<1>e|<2>s|<3>t|<4>e|<5>\\0|<6> |<7> |<8> |<9> |<10> |<11> |<12> |<13> |<14> |<15> |<16> "] palavra [shape=record] palavra -> Frase:0 } </graphviz>

No diagrama acima, a caixa intitulada palavra corresponde à variável palavra, e esta contém a localização de memória onde está a sequência de caracteres que compõe a string (diz-se que palavra aponta a posição de memória onde inicia a string) . Note que depois do último e há um caractere \0. A barra invertida significa que o número mostrado naquela posição do diagrama corresponde ao código ASCII do caractere. O caractere de código 0 (zero) representa na verdade um caractere nulo (não visível e que não representa nenhum caractere real), e no caso serve para indicar onde está o final de string. Assim, pode-se perceber que as demais posições da variável palavra não são usadas, apesar de estarem ali disponíveis.

Uma outra forma de declarar strings, mostrada abaixo, não aloca memória previamente para conter a cadeia de caracteres.

int main() {
  char * senha;

  senha = "kaboom";

  printf("A senha é %s (não espalhe !)\n", senha);

}

O interessante nessa segunda forma de declarar string é o tipo da variável:

char * senha;

Essa declaração explicita que a variável senha serve para guardar endereços de memória. Esse tipo de variável se chama ponteiro, porque serve para apontar posições de memória que contém um determinado dado. No exemplo acima, a variável senha pode apontar uma posição de memória que contém um dado do tipo char. Mas inicialmente ela não aponta nenhuma posição em particular, uma vez que não foi inicializada. Mas quando a seguinte linha for executada:

senha = "kaboom";

... essa variável passará a apontar a posição de memória onde se encontra a string constante "kaboom". Assim, a relação entre a variável senha e a constante "kaboom" pode ser representada pelo diagrama:

<graphviz> digraph Frase { Frase [shape=Mrecord,label="<0>k|<1>a|<2>b|<3>o|<4>o|<5>m|<6>\\0"] senha [shape=record] senha -> Frase:0 } </graphviz>

Não há diferença de fato entre a primeira figura, que mostra a string que foi guardada na variável palavra, e a figura acima. Ambas mostram que variáveis string apontam posições de memória onde estão sequências de caracteres. A única diferença qualitativa entre esses exemplos é que no primeiro foi alocada (reservada) uma região de memória capaz de conter 16 caracteres, e seu endereço foi armazenado na variável palavra, e no segundo caso a variável não teve memória associada a si, mas foi usada para apontar uma área de memória onde estava a string constante "kaboom".

Uma consequência do que foi mostrado acima é que deve-se ter cuidado ao tentar guardar strings em variáveis declaradas da forma:

char * senha;

... porque inicialmente uma variável como essa não aponta uma região de memória válida. Por exemplo, se o seguinte programa for compilado e executado:

int main() {
  char * senha;

  printf("Digite a senha: ");
  scanf("%s", senha);

  printf("Voce digitou %s\n", senha);

}

... um erro aparecerá na tela:

> ./erro
Digite a senha: kaboom
Segmentation fault

Esse erro indica que o processo tentou acessar uma área de memória inválida (que não foi previamente alocada). Mas se ele for modificado da seguinte forma:

int main() {
  char * senha;
  char memoria[1024];

  senha = memoria;

  printf("Digite a senha: ");
  scanf("%s", senha);

  printf("Voce digitou %s\n", senha);

}

... o programa funcionará sem erros. A diferença é que a variável senha agora aponta a mesma região de memória que a variável memoria, à qual foram previamente alocados 1024 bytes.

Para tornar explícito que variáveis string contém na verdade endereços de memória, veja o exemplo abaixo:

int main() {
  char * senha;
  char memoria[1024];

  printf("Variável memoria aponta a posição de memória %p\n", memoria);
  printf("Variável senha aponta a posição de memória %p\n", senha);

  senha = memoria;

  printf("Agora a variável senha contém o endereço %p (deveria ser o mesmo que a variável memoria)\n", senha);

  printf("Digite a senha: ");
  scanf("%s", memoria);

  printf("Voce digitou %s\n", senha);

}

Nos três primeiros printf foi usado um formato %p, que serve para mostrar endereços de memória. Assim, esses printf mostram os endereços de memória que estão guardados nas variáveis memoria e senha, e pode-se ver que incialmente eles são diferentes. Após a operação de atribuição, a variável senha passa a conter o mesmo endereço que está na variável memoria. Outro detalhe interessante no programa reside na leitura da string pelo teclado, a qual é armazenada na região de memória apontada pela variável memoria. Em seguida, o último printf mostra a string apontada pela variável senha, e deve aparecer na tela exatamente a mesma string que foi lida do teclado, já que senha agora aponta a mesma região de memória que memoria. Veja abaixo a execução desse programa:

> ./teste
Variável memoria aponta a posição de memória 0x7fff7187d260
Variável senha aponta a posição de memória (nil)
Agora a variável senha contém o endereço 0x7fff7187d260 (deveria ser o mesmo que a variável memoria)
Digite a senha: KABOOM!!!
Voce digitou KABOOM!!!

Iniciando e copiando strings

A inicialização de variáveis string pode ser feita de diversas formas:

Apontando diretamente uma constante string

  int main() {
    char * s = "uma constante string";

Esta é uma maneira simples, porém impede que o valor da string seja modificado. Assim, não é possível alterar o valor de nenhum dos caracteres dessa string. Para conferir se isto é mesmo verdade, pode-se fazer um teste:

#include <stdio.h>

  int main() {
    char * s = "uma constante string";

    s[0] = 'U';

    printf("s=%s\n", s);
  }

Ao compilar e executar esse programa (chamado de init.c neste exemplo), o resultado mostra um erro fatal de acesso inválido à memória (Falha de Segmentação, ou Segmentation Fault), que causa o término abrupto do processo:

> gcc -o init init.c
> ./init
Segmentation fault

Gerando uma duplicata de outra string

Ao invés de pontar diretamente uma constante string, pode-se gerar uma duplicata, e então apontá-la. A duplicata ocupa uma área de memória que permite modificações, evitando o erro visto ao final da subseção anterior.

#include <stdio.h>
#include <string.h>

  int main() {
    char * s;

    s =  strdup("uma constante string");
    s[0] = 'U';

    printf("s=%s\n", s);
    free(s);
  }

Nesse segundo caso, foi necessário usar a função strdup, que gera uma duplicata de uma string. Essa função aloca memória dinamicamente, e copia para lá a string passada como parâmetro. Ao se compilar e executar esse exemplo, obtém-se:

> gcc -o init2 init2.c
> ./init2
s=Uma constante string

Porém já que strdup aloca memória dinamicamente, é preciso liberar essa região de memória quando não for mais necessária. Para isto se usa a função free, que aparece na linha 11 do exemplo.

Essa função pode ser usada tanto com constantes string quanto com variáveis string. Assim, pode-se facilmente gerar uma cópia de uma string qualquer.

#include <stdio.h>
#include <string.h>

  int main() {
    char * s;
    char x[1024];

    printf("Digite uma frase: ");
    scanf("%[^\n]", x);
    s = strdup(x);
    s[0] = '#';

    printf("s=%s\n", s);
    printf("x=%s\n", x);

    free(s);
  }

No exemplo acima, na variável x se armazena uma frase digitada pelo teclado (linha 9). Em seguida, gera-se uma duplicata dessa string, que passa a ser referenciada pela variável s (linha 10). O primeiro caractere da string referenciada por s é modificado para ser # (linha 11). Finalmente, são mostradas as strings apontadas por s e x (linhas 13 e 14). O resultado da execução desse programa pode ser visto abaixo:

$ gcc -o dup dup.c
$ ./dup
Digite uma frase: um teste novo
s=#m teste novo
x=um teste novo

Copiando uma string para outra

A primeira vista parece a mesma coisa que o caso 2, mas há aqui uma pequena diferença: a variável string destino já tem uma capacidade prealocada:

#include <stdio.h>
#include <string.h>

  int main() {
    char s[32];

    strncpy(s, "uma constante string", 31);
    s[0] = 'U';

    printf("s=%s\n", s);
  }

O resultado da execução desse programa é:

> gcc -o init3 init3.c
> ./init3
s=Uma constante string

Nesse terceiro caso se usou a função strncpy para fazer a cópia de string. O protótipo dessa função segue abaixo:

char *strncpy(char *dest, const char *src, size_t n);

Essa função precisa de três parâmetros:

  • char * dest: a string destino, para onde será feita a cópia. No exemplo, a variável s foi passada como o parâmetro dest.
  • const char * src: a string origem, que será copiada para dest (aqui o prefixo const apenas informa que strncpy não vai modificar o conteúdo de src)
  • size_t n: a quantidade máxima de caracteres que serão copiados de src para dest. Se o tamanho de dest for maior que n, apenas seus n primeiros caracteres serão copiados. A variável dest deve ter capacidade para receber esses n caracteres. Obs: o tipo size_t é um sinônimo para int.

O limite de caracteres a serem copiados tem como propósito evitar que se exceda a capacidade da área de memória da string destino. Caso isto acontecesse, haveria a escrita em áreas de memória alocadas para outros fins, causando erros de difícil detecção, ou mesmo em áreas de memória nao alocadas, gerando um erro Falha de segmentação. Por isto que não se recomenda o uso da função strcpy, que não impõe esse limite.

Quando se tenta escrever em uma string mais caracteres do que sua capacidade permite, ocorre um ero chamado de estouro de buffer (ou buffer overflow em inglês).

Compondo uma string a partir de dados de diferentes tipos

Comumente se precisa gerar uma string a partir de dados que podem ser de diferentes tipos. Por exemplo, imagine que um programa use as variáveis inteiras dia, mes e ano para representar uma data, e a certo momento precisa transformar essa data em uma string da forma dia/mes/ano. A forma mais fácil de fazer isto é usando a função snprintf:

#include <stdio.h>

int main() {
  char data[12];
  int dia, mes, ano;

  dia = 1;
  mes = 1;
  ano = 2009;

  snprintf(data, 12, "%d/%d/%d", dia, mes, ano);

  printf("Data = %s\n", data);
}

Como esperado, a execução do programa acima resulta em:

> gcc -o init4 init4.c
> ./init4
Data = 1/1/2009

A função snprintf é semelhante a printf, porém ao invés de escrever um texto na tela (na verdade, na saída padrão), armazena-o em uma string. O primeiro parâmetro de snprintf especifica a variável string onde será guardado o resultado, e o segundo parâmetro indica a quantidade máxima de caracteres que podem ser escritos (a capacidade da string deve ser maior ou igual a esse número). Assim, no exemplo acima usou-se a função snprintf para gerar a string "1/1/2009", e guardá-la na área de memória apontada pela variável data.

Comparando strings

Como strings são na verdade vetores de caracteres, sua comparação implica comparar um a um de seus caracteres. Assim, devem-se comparar os primeiros caracteres das strings, depois os segundos, em seguida os terceiros, e assim por diante, até que se chegue ao final de ambas strings (quando então se conclui que elas são iguais), ou se encontre um par de caracteres que seja diferente (e as strings portanto são também consideradas diferentes). Por exemplo, sejam duas strings s1 e s2 inicializadas da seguinte forma:

int main() {
  char * s1 = "abacaxi";
  char * s2 = "abacaxi";

A comparação de ambas implica comparar todos seus caracteres:

<graphviz> digraph Frase { p1 [shape=Mrecord,label="<0>a|<1>b|<2>a|<3>c|<4>a|<5>x|<6>i|<7>\\0"] s1 [shape=record] s1 -> p1:0 p2 [shape=Mrecord,label="<0>a|<1>b|<2>a|<3>c|<4>a|<5>x|<6>i|<7>\\0"] s2 [shape=record] s2 -> p2:0 } </graphviz>

s1 -> a | b | a | c | a | x | i | \0
      ^   ^   ^   ^   ^   ^   ^  
      |   |   |   |   |   |   |
      v   v   v   v   v   v   v
s2 -> a | b | a | c | a | x | i | \0

Como todos os caracteres são iguais, e as strings têm mesmo tamanho, então elas são iguais. Agora no caso abaixo:

int main() {
  char * s1 = "abacaxi";
  char * s2 = "abacate";

A comparação de ambas mostra que:

s1 -> a | b | a | c | a | x | i | \0
      ^   ^   ^   ^   ^     
      |   |   |   |   |   X   
      v   v   v   v   v      
s2 -> a | b | a | c | a | t | e | \0

Como os caracteres da posição 5 das strings são diferentes, a comparação encerra e as strings são consideradas diferentes.

Uma função que faz esse tipo de comparação está mostrada abaixo:

int compara_strings(char * s1, char * s2) {
  int i;
  int ok = 1;

  i = -1;
  do {
    i++;
    if (s1[i] != s2[i]) ok = 0;
  } while (ok && (s1[i] != 0) && (s2[i] != 0));

  return ok;
}

Essa função compara_strings recebe como parâmetros as strings a serem comparadas, e retorna como resultado o valor inteiro 1 se elas forem iguais, e 0 se forem diferentes. Dentro da função a comparação é feita usando-se uma estrutura de repetição do { } while (condição);, e usa-se a variável int i para indicar que caractere das strings está sendo comparado a cada repetição do laço. A vaqriável int ok indica se as strings são idênticas até o último par de caracteres comparados. O laço termina se o último par de caracteres for diferente (quer dizer, se ok == 0), ou se chegou ao fim de uma da strings (s1[i] == 0 ou s2[i] == 0). Com essa função se pode imaginar o seguinte programa, que lê duas strings do teclado e mostra se iguais ou diferentes:

#include <stdio.h>

int compara_strings(char * s1, char * s2) {
  int i;
  int ok = 1;

  i = -1;
  do {
    i++;
    if (s1[i] != s2[i]) ok = 0;
  } while (ok && (s1[i] != 0) && (s2[i] != 0));

  return ok;
}

int main() {
  char s1[32];
  char s2[32];

  printf("Primeira string: ");
  scanf("%s", s1);
  printf("Segunda string: ");
  scanf("%s", s2);

  if (compara_strings(s1, s2)) {
    printf("%s = %s\n", s1, s2);
  } else {
    printf("%s != %s\n", s1, s2);
  }

}

Algumas execuções desse programa mostrariam o seguinte:

> gcc -o comp1 comp1.c
> ./comp1
Primeira string: abacaxi
Segunda string: abacaxi
abacaxi = abacaxi
> ./comp1
Primeira string: abacaxi
Segunda string: abacate
abacaxi != abacate
> ./comp1
Primeira string: abacaxi
Segunda string: abacaxis
abacaxi != abacaxis

... o que mostra que a função compara_strings funciona como esperado.

Usando a função strncmp

Comparações de strings aparecem tantas vezes em programas que seria estranho que já não existisse uma função pronta na biblioteca C padrão. E de fato existe a função strncmp, que compara até uma quantidade determinada de caracteres de duas strings. Com strncmp o exemplo acima ficaria assim:

#include <stdio.h>
#include <string.h>

int main() {
  char s1[32];
  char s2[32];

  printf("Primeira string: ");
  scanf("%s", s1);
  printf("Segunda string: ");
  scanf("%s", s2);

  if (strncmp(s1, s2, 32) == 0) {
    printf("%s = %s\n", s1, s2);
  } else {
    printf("%s != %s\n", s1, s2);
  }

}

A chamada de compara_strings foi substituída por strncmp, de forma que compare no máximo 32 caracteres das strings. Em geral, esse limite de caracteres a serem comparados se refere à capacidade das strings, para evitar que a comparação ultrapasse a área de memória alocada e resulte em um erro fatal ("Falha de segmentação").

O resultado da execução desse novo programa seria idêntica:

> gcc -o comp1 comp1.c
> ./comp1
Primeira string: abacaxi
Segunda string: abacaxi
abacaxi = abacaxi
> ./comp1
Primeira string: abacaxi
Segunda string: abacate
abacaxi != abacate
> ./comp1
Primeira string: abacaxi
Segunda string: abacaxis
abacaxi != abacaxis

A função strncmp possui uma diferença importante em relação a compara_strings, pois pode haver três valores de retorno:

  • 0: as strings são iguais
  • -1: a primeira string é alfabeticamente antecessora da segunda string
  • 1: a primeira string é alfabeticamente sucessora da segunda string

Isto abre novas possibilidades: a comparação resulta não somente em informar se as strings são iguais ou não, mas também em, caso sejam diferentes, dizer qual delas vem antes alfabeticamente. Assim, o exemplo anterior poderia ser modificado para aproveitar essa informação:

#include <stdio.h>
#include <string.h>

int main() {
  char s1[32];
  char s2[32];
  int ok;

  printf("Primeira string: ");
  scanf("%s", s1);
  printf("Segunda string: ");
  scanf("%s", s2);

  ok = strncmp(s1, s2, 32);
  if (ok == 0) {
    printf("%s = %s\n", s1, s2);
  } else {
    if (ok < 0) {
      printf("%s vem antes de %s\n", s1, s2);
    } else {
      printf("%s vem antes de %s\n", s2, s1);
    }
  }

}

Algumas execuções mostrariam o seguinte:

> gcc -o comp2 comp2.c
> ./comp2
Primeira string: abacaxi
Segunda string: abacaxi
abacaxi = abacaxi
> ./comp2
Primeira string: abacaxi
Segunda string: abacate
abacate vem antes de abacaxi
> ./comp2
Primeira string: abacaxi
Segunda string: abacaxis
abacaxi vem antes de abacaxis

Assim, com a função strncmp se poderia fazer um programa que ordenasse alfabeticamente uma quantidade arbitrária de strings. No entanto, fica a cargo do aluno curioso como fazer isso.

#include <stdio.h>
#include <string.h>

int main() {
  char s1[32];
  char s2[32];

  printf("Primeira string: ");
  scanf("%s", s1);
  printf("Segunda string: ");
  scanf("%s", s2);

  if (strncasecmp(s1, s2, 32) == 0) {
    printf("%s = %s\n", s1, s2);
  } else {
    printf("%s != %s\n", s1, s2);
  }

}


Uma função similar a strncmp é strncasecmp, que compara as strings porém não fazendo diferença entre maiúsculas e minúsculas. Quer dizer, para strncasecmp, o caractere A é igual a a.

Pesquisando dentro de strings

  1. strstr e strcasestr
#include <stdio.h>
#include <string.h>

int main() {
  char s1[32];
  char s2[32];
  char * res;
  int ok;

  printf("Primeira string: ");
  scanf("%[^\n]", s1);
  printf("Segunda string: ");
  scanf(" %[^\n]", s2);

  res = strstr(s1, s2);

  if (res != NULL) {
    printf("a palavra \"%s\" aparece dentro de \"%s\"\n", s2, s1);
    printf("---> %s\n", res);
    printf("---> %d\n", res - s1);
  } else {
    printf("a palavra \"%s\" NAO aparece dentro de \"%s\"\n", s2, s1);
  }
}
  1. strchr e strrchr
  2. index e rindex