Strings
Strings, no português chamadas de "cadeias de caracteres", são variáveis que guardam textos, estes que podem ser utilizados para praticamente qualquer coisa, desde nomes, comandos, expressões matemáticas, configurações, etc.
Em muitas linguagens de programação de alto nível, strings são consideradas como um tipo "separado" de dados, geralmente são imutáveis (modificar uma string significa gerar uma nova string, permitindo que strings sejam repassadas por referência de forma "segura").
Na linguagem C, não existe especificamente um "tipo string", strings são arrays de caracteres, geralmente repassada via ponteiro como const char*
para strings não modificáveis, ou char*
para strings que podem ser modificadas.
Mas ai fica a pergunta, se passarmos um único const char*
como vamos saber o tamanho do array ?
A resposta é que, geralmente, utiliza-se o caractere nulo '\0'
que na prática, tem o valor 0
. Consideramos que uma string "acabou" quando, ao ler um caractere, ela tem valor igual a \0
.
Logo uma string declarada como :
"Hello World\n"
Será na prática, um array que contenha :
'H' 'e' 'l' 'l' 'o' ' ' 'W' 'o' 'r' 'l' 'd' '\n' '\0'
Dessa forma, poderiamos fazer um código que "descobre" o tamanho de uma string, lembrando que na prática a função strlen
da biblioteca string.h
é muito mais otimizada:
#include <stdlib.h>
#include <stdio.h>
int tamanhoString(const char *str)
{
const char *c = str;
while(*c != '\0')
c++;
return (c - str);
}
int main()
{
int tam = tamanhoString("abcdefghij");
printf("A string tem tamanho %d\n", tam);
}
Como strings são apenas "ponteiros para arrays" existem várias operações que podem ser realizadas com elas :
"25" + 1 // "5" (avança um caractere)
"comida"[3] // 'm' (pega o 4° caractere)
*"abc" // 'a' (pega o 1° caractere)
[1]"BIFE" // 'I' (pega o 2° caractere)
//Descontamos 1 por causa do \0 em "Hello "
//"World" (pula a frase "Hello ")
"Hello World" + sizeof("Hello ") - 1
Literais de strings
Quando escrevemos uma string diretamente como no exemplo abaixo :
char *teste1 = "Ola, mundo!";
const char *teste2 = "Ola, como vão?";
char *teste3 = "Ola, mundo!";
const char *teste4 = "Ola, como vão?";
Temos uma string "literal", que inicialmente é um array com o tamanho da string (lembrando que há uma reserva de 1 byte para o \0
) mas que pode ser implicitamente convertido para um ponteiro e será trocado para um array constante ou não constante dependendo da variável que o recebe.
Em qualquer um dos casos o compilador provavelmente terá a string inteira já guardada dentro do executável, para que ela seja alocada junto da inicialização do programa, tornando tudo mais eficiente.
No caso do teste1
, como não temos um ponteiro constante, o compilador provavelmente colocará a string numa região com permissão de escrita e será obrigado a reservar uma cópia separada para teste3
caso não consiga garantir que nenhum dos dois modificou a string.
No caso do teste2
, como o ponteiro aponta para um dado constante, o compilador provavelmente colocará a string numa região de apenas leitura, de forma que o programa seja finalizado caso alguém tente modificar a string em teste2
, a variável teste4
receberá uma referência para a mesma string, pois como ela é imutável, podendo ser reutilizada, diminuindo o uso de memória.
Sobre strings literais, também é interessante mencionar que strings separadas apenas por espaços/nova linha, são consideradas como parte da mesma string :
#define PASTA_RECURSOS "/home/pi/resources"
PASTA_RECURSOS "/imagem1.png" //Este texto
"/home/pi/resources/imagem1.png" //É igual a este
//Este texto
"como vai\n"
"você?\n"
//É igual a este
"como vai\nvocê?\n"
Para digitar aspas e outros caracteres especiais numa string literal, utilizamos a mesma sintaxe das sequências de escape
descritas no capítulo sobre caracteres.
Encoding
Antes de falar sobre os tipos de strings literais, precisamos falar sobre encoding.
Encoding, no contexto de strings (que seria "codificação" em português), é o padrão no qual decidimos quais códigos númericos utilizamos para indicar cada caractere, no geral todo encoding moderno segue o padrão ASCII, de forma que os códigos númericos 0 a 127 representem os mesmos caracteres.
Porém, nos encodings modernos geralmente desejamos representar mais do que apenas 128 caracteres diferentes, foi ai que surgiram diferentes codificações que buscavam colocar diferentes caracteres codificados de formas diferentes.
É importante também entender o termo code unit
que simboliza um dos números utilizados para representar um caractere, em alguns casos, mais de um número é utilizado para representar um único caractere, portanto é importante entender a distinção entre code unit
vs caracteres.
Houve uma época onde existiam vários encodings diferentes que codificavam vários caracteres diferentes como Latin-1
e Shift-JIS
, entre outros. O problema dessas codificações é que elas não conversam entre sí, não podemos codificar um caractere japonês no Latin-1
e não temos acesso a acentuação em Shift-JIS
.
Para isso foi criado um padrão conhecido como Unicode
, que busca uma representação universal para todo e qualquer caractere. O padrão Unicode
gerou 3 tipos de codificações, UTF-8
, UTF-16
e UTF-32
, que usam, respectivamente, 1 byte, 2 bytes ou 4 bytes por código (code unit
).
Onde :
UTF-8
é compatível com ASCII e usa 1 byte por code unit, valores além dos caracteres ASCII, podendo usar de 1 a 4code units
por caractere.UTF-16
utiliza 2 bytes porcode unit
, sendo necessário 2code units
para representar alguns caracteres.UTF-32
utiliza sempre 4 bytes mas representa qualquer caractere em umcode unit
.
Tipos de strings literais
Existem 5 tipos diferentes de string literais no C :
"T1" //String literal
u8"T2" //String literal UTF-8 (C11)
u"T3" //String literal UTF-16 (C11)
U"T4" //String literal UTF-32 (C11)
L"T5" //String literal "wide"
String literal
: Uma string literal padrão, utiliza um encoding definido pelo compilador, mas geralmente usa exatamente o mesmo encoding usado no arquivo de texto onde o código fonte se encontra, tem tipochar[N]
.String literal UTF-8 (C11)
: Uma string literal que é sempre em UTF-8, independente do compilador, tem tipochar[N]
(ouchar8_t[N]
noC23
).String literal UTF-16 (C11)
: Uma string literal que é sempre em UTF-16, independente do compilador, tem tipochar16_t[N]
.String literal UTF-32 (C11)
: Uma string literal que é sempre em UTF-32, independente do compilador, tem tipochar32_t[N]
.String literal "wide"
: Uma string literal longa, com encoding definido pelo compilador, no geral éUTF-32
em sistemas baseados em Unix eUTF-16
no Windows, tem tipowchar_t[N]
.
Inicialização de strings
Podemos também utilizar strings literais para inicializar arrays :
#include <string.h>
char arr[] = "Abc"; //Neste caso, arr tem tamanho 4, com 'A','b','c','\0'
const char str2[] = "def"; //Podemos inicializar arrays constantes também
char str3[10];
str3 = "abcdef" //Errado, isso só funciona na inicialização
//Essa é uma das formas de copiar a string (usando a função strcpy)
strcpy(str3, "abcdef");
//Neste caso, temos 'g','h','i','\0' no conteúdo, porém temos mais espaço reservado
//para expandir e colocar mais coisas na string caso necessário...
char str4[20] = "ghi";
//Também podemos inicializar estruturas com strings literais...
struct {
char *txt1;
int i1;
} teste = {"Ola", 0};
Segurança das funções de strings
Devido a idade da linguagem C, existem muitas funções que são consideradas "não ideais" para tratar strings pois são suscetíveis a erros, para isso alguns projetos como git
ou engenheiros de segurança da Microsoft mantêm um header com uma lista de funções "banidas", que causa um erro de compilação caso alguém tente usar elas.
Utilizar essas funções não necessariamente significa que você tem um problema de segurança, mas basta um erro no seu uso para elas se TORNAREM um problema de segurança.
Annex K
Vale lembrar que no C11
a Microsoft empurrou a criação de versões "seguras" de muitas funções no chamado Annex K (Anexo K, o nome da parte do documento do padrão do C11
especificando sobre estas novas funções), essas funções tem o sufixo _s
e nomes muito parecidos com as funções padrões do C.
Porém a implementações das funções adicionadas no Annex K são opcionais, e estarão apenas presentes se a macro __STDC_LIB_EXT1__
existir e o usuário definir __STDC_WANT_LIB_EXT1__
para 1
antes de incluir string.h
ou outra biblioteca, no geral o Annex K é considerado um fiasco pois foi baseadas em implementações da Microsoft que foram posteriormente modificadas, de forma que nem mesmo a própria Microsoft siga o padrão e muitos compiladores simplesmente se recusaram a implementar as funções descritas pois elas são opcionais.
Algumas materias, em inglês, com explicações mais a fundo sobre o Annex K :
No geral muitos criticaram muitas funções do Annex K pois :
- Já existem outras soluções feitas por usuários/bibliotecas que são mais efetivas, simples e seguras
- É muito difícil fazer testes automatizados de código utilizando as funções
_s
- Não é fácil de integrar as funções novas com código que usa as funções antigas
- As funções inerentemente diminuem a performance ao realizar checagens extensas e possivelmente redundantes
Funções de manipulação de strings
Vamos apresentar rapidamente uma lista com as funções de strings recomendadas para cada ação, visando usabilidade e segurança, colocando Sim
nos casos onde a função padrão é suficiente, Não
nos casos que demandam explicações ou o nome da uma outra função quando ela é mais recomendada :
Ação | Padrão | Wide | Recomendado |
---|---|---|---|
Copiar string | strcpy | wcscpy | strlcpy |
Buscar caractere | strchr | wcschr | Sim |
Concatenar strings | strcat | wcscat | strlcat |
Comparar strings | strcmp | wcscmp | Sim |
Tamanho de string | strlen | wcslen | Sim |
Achar substring | strstr | wcsstr | Sim |
No caso de copiar strings, a função strcpy
geralmente é substituida pela strlcpy
, o git
por exemplo usa essa função dessa forma e mantêm uma implementação própria caso a função não esteja disponível (pois ela não é padrão no C):
size_t gitstrlcpy(char *dest, const char *src, size_t size)
{
size_t ret = strlen(src);
if (size) {
size_t len = (ret >= size) ? size - 1 : ret;
memcpy(dest, src, len);
dest[len] = '\0';
}
return ret;
}
A função strlcat
também não é padrão, mas existe em alguns distribuições assim como a strlcpy
, caso ela não exista, podemos ter nossa própria implementação :
size_t strlcat(char *dest, const char *src, size_t size)
{
if(size == 0)
return 0;
size_t destsize = strlen(dest);
size_t ret = 0;
if(destsize < size)
ret = strlcpy(dest + destsize, src, size - destsize);
return ret;
}
Para versão wide, podemos checar se as funções wcslcpy
ou wcslcat
existem, caso contrário podemos utilizar as implementações sugeridas, trocando os tipos char
por wchar_t
e a variável len
no memcpy
por len * sizeof(wchar_t)
.
Iteração com strings
É comum que iniciantes que vieram de outras linguagens de alto nível iterem sobre strings de forma ineficiente em C.
Uma implementação que normalmente podemos ver é :
for(int i = 0; i < strlen(str); i++) {
str[i]; //Caractere
}
Observe que a função strlen
é chamada a cada iteração, isso é extremamente ineficiente, pois cada chamada de strlen
itera a string inteira até achar o caractere \0
que é o finalizador.
Em linguagens alto nível é comum que string seja uma classe que já contêm o tamanho como uma de suas propriedades, tornando essa operação eficiente, o que não é o caso com as strings padrão do C.
Uma forma muito mais simples e muito mais eficiente de iterar sobre strings em C :
for(const char *c = str; *c; c++) {
*c; //Caractere
}
Nesse caso temos um ponteiro que acessa cada caractere, avançando uma posição a cada iteração e que para quando chega no caractere \0
(*c
é Falso
quando chega no valor \0
).
Também podemos implementar de forma similar, utilizando indices :
for(int i = 0; str[i]; i++) {
str[i]; //Caractere
}
Diferentes representações de String
Neste campo vamos falar sobre algo um pouco avançado, as diferentes implementações para representar strings em várias linguagens, incluindo C e até mesmo bibliotecas renomadas que buscam algo mais eficiente, destacando os pontos positivos e negativos de cada formato.
Alguns termos, que precisam de explicação, serão reutilizados ao longo das explicações :
- Localidade de cache : Indica que os dados estão próximos na memória, logo o acesso a string está efetivamente acessando apenas um local da memória para conseguir o tamanho e os dados da string, o que pode ajudar na performance.
- Evita realocações : Geralmente quando a string tem um campo indicando o tamanho realmente reservado para string, permitindo concatenações sem realocar a memória reservada.
- Dificuldade de gerar substrings : A facilidade de pegar um pedaço de uma string e usar ele como uma nova string, útil em diversas situações.
Bom, como já explicado neste guia, a linguagem C utiliza strings terminadas com \0
, sendo necessário percorrer a string para obter o tamanho.
Vantagens :
- Localidade de cache
- Leve nos casos onde saber o tamanho antecipadamente não é necessário
- Utilizada em chamadas de sistemas
- É o formato que usa menos memória para representação de strings
Desvantagens:
- Usa
strlen
para obter tamanho, operaçãoO(n)
- Dificil de obter substrings
- Pode ser necessário realocar para concatenar