C Texto ↔ Número

Tudo o que chega do teclado, de um arquivo ou da linha de comando chega como texto. Se a Ana digita 12, o programa recebe os caracteres '1' e '2', e não o número doze. Para fazer contas, é preciso converter o texto em número. E, no caminho de volta, montar um código de pedido como PED-00042 exige transformar o número em texto. Nesta aula você vai ver as ferramentas do C para as duas direções, e por que a mais famosa delas, o atoi, é a que você menos deve usar.

Você vai aprender
  • Por que "20" e 20 são coisas diferentes.
  • atoi: o jeito curto, que não avisa quando o texto não é um número.
  • strtol com endptr e errno: o jeito certo, que detecta texto inválido, lixo depois do número e número grande demais.
  • Outras bases: binário, hexadecimal e a base 0.
  • strtod para números com casas decimais (e o problema da vírgula).
  • sscanf para tirar vários valores de uma linha, como uma data.
  • Número → texto com snprintf (e por que itoa não existe).

Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa. As funções de conversão moram no stdlib.h ("standard library", biblioteca padrão), menos o sscanf e o snprintf, que são do stdio.h.


A ideia: o desenho do número e o número

Pense numa etiqueta de preço escrita à mão: "20". Você lê e entende "vinte", mas a etiqueta é só tinta no papel, dois desenhos de algarismos. Para somar dois preços, sua cabeça converte os desenhos em quantidades, soma, e escreve o resultado com novos desenhos.

O computador também. Na memória, a string "20" são os bytes 50 e 48 (os códigos de '2' e '0') e um '\0'; o int 20 é a quantidade vinte. Com strings, o máximo que dá para fazer é colar os desenhos:

#include <stdio.h>

int main(void) {
    char a[] = "20";
    char b[] = "22";
    printf("Como texto: %s%s\n", a, b);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra soma.c -o soma
$ ./soma
Como texto: 2022          « colou os desenhos, não somou

Para somar, converta primeiro. A tabela mostra os dois sentidos e as ferramentas desta aula:

SentidoFerramentas
Texto → inteiroatoi (curto, sem detectar erro), strtol (o jeito certo), sscanf
Texto → número com casas decimaisatof (sem detectar erro), strtod (o jeito certo), sscanf
Número → textosnprintf

1. atoi: curto, mas cego

atoi = "ASCII to integer", de texto (ASCII) para inteiro. É a primeira função que todo mundo aprende:

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

int main(void) {
    printf("atoi(\"42\")    = %d\n", atoi("42"));
    printf("atoi(\"  -7\")  = %d\n", atoi("  -7"));
    printf("atoi(\"12abc\") = %d\n", atoi("12abc"));
    printf("atoi(\"abc\")   = %d\n", atoi("abc"));
    printf("atoi(\"0\")     = %d\n", atoi("0"));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra atoi.c -o atoi
$ ./atoi
atoi("42")    = 42
atoi("  -7")  = -7        « pula espaços do começo e entende o sinal
atoi("12abc") = 12        « lê até onde dá e ignora o resto, calado
atoi("abc")   = 0         « não é número: devolve 0…
atoi("0")     = 0         « …e um "0" de verdade também dá 0

As duas últimas linhas mostram o defeito: o atoi não tem como avisar que deu errado. Se a Ana digitar "abc" na quantidade, o programa segue com zero unidades, como se ela tivesse digitado 0. E "12abc" vira 12 sem nenhum aviso. Pior: com um número grande demais o resultado é comportamento indefinido. No laboratório, atoi("99999999999999999999") deu -1; no editor do guia, o mesmo código deu 1661992959.

Use atoi só quando o texto com certeza é um número válido (por exemplo, um número que o seu próprio programa escreveu). Para qualquer coisa digitada por uma pessoa, use strtol.


2. strtol: o jeito certo

strtol = "string to long", de texto para long (um inteiro, em geral maior que o int, aula Inteiros). A diferença para o atoi é que ele conta onde parou de ler:

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

int main(void) {
    char texto[] = "125 reais";
    char *fim;
    long valor = strtol(texto, &fim, 10);
    printf("valor = %ld\n", valor);
    printf("parou em: \"%s\"\n", fim);
    printf("leu %d caracteres\n", (int)(fim - texto));
    return 0;
}
char *fim;
Uma variável que vai guardar um lugar dentro do texto (um ponteiro, aula Ponteiros e Strings). Leia como "marcador de página".
strtol(texto,
1º argumento: o texto a converter.
&fim,
2º argumento: endptr ("ponteiro do fim"). O strtol põe em fim o lugar do primeiro caractere que ele não usou. O & é porque a função precisa escrever na sua variável, como no scanf.
10)
3º argumento: a base. 10 é a base decimal de sempre (seção 3 mostra outras).
%ld
O formato de long no printf (aula Formatos do printf).
fim - texto
A distância entre o começo e onde ele parou: quantos caracteres viraram número.
$ gcc -std=c23 -Wall -Wextra strtol.c -o strtol
$ ./strtol
valor = 125
parou em: " reais"        « o resto do texto, a partir do espaço
leu 3 caracteres

Com o marcador fim, dá para responder três perguntas que o atoi não responde:

PerguntaComo saberExemplo
Havia algum número?fim == texto: não andou nada, então não havia dígitos"abc", ""
Sobrou lixo depois?*fim != '\0': o caractere onde parou não é o fim da string"12abc"
Coube no long?errno == ERANGE (precisa zerar errno antes)"99999999999999999999"

O *fim (com asterisco) quer dizer "o caractere que está no lugar marcado por fim". E o errno ("error number", número do erro, do errno.h) é uma variável global que as funções da biblioteca usam para contar o que deu errado; ERANGE quer dizer "fora do intervalo" (aula Erros do Sistema).

Juntando tudo: ler uma quantidade com segurança

Este é o modelo para ler um número digitado: fgets para a linha (aula Ler uma Linha) e strtol com as três conferências:

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

int main(void) {
    char linha[100];
    printf("Quantidade: ");
    if (fgets(linha, sizeof linha, stdin) == NULL) {
        return 1;
    }
    linha[strcspn(linha, "\n")] = '\0';

    char *fim;
    errno = 0;
    long qtd = strtol(linha, &fim, 10);

    if (fim == linha) {
        printf("\"%s\" não é um número.\n", linha);
    } else if (*fim != '\0') {
        printf("Sobrou texto depois do número: \"%s\"\n", fim);
    } else if (errno == ERANGE) {
        printf("Número grande demais.\n");
    } else {
        printf("Ok: %ld unidades.\n", qtd);
    }
    return 0;
}
linha[strcspn(linha, "\n")] = '\0';
Tira o '\n' do Enter. Sem isso, o '\n' seria o "lixo depois do número" em toda leitura.
errno = 0;
O strtol só escreve no errno quando dá erro; ele nunca o zera. Se você não zerar antes, pode ler um erro antigo, de outra função.
if (fim == linha)
Nenhum dígito: texto vazio ou que não começa com número.
else if (*fim != '\0')
Leu um número, mas a linha não acabou ali.
else if (errno == ERANGE)
O número não cabe num long.
$ gcc -std=c23 -Wall -Wextra validar.c -o validar
$ echo "12" | ./validar
Quantidade: Ok: 12 unidades.
$ echo "abc" | ./validar
Quantidade: "abc" não é um número.
$ echo "12abc" | ./validar
Quantidade: Sobrou texto depois do número: "abc"
$ echo "" | ./validar
Quantidade: "" não é um número.
$ echo "99999999999999999999" | ./validar
Quantidade: Número grande demais.
$ echo " 8" | ./validar
Quantidade: Ok: 8 unidades.        « espaços no começo são aceitos
$ echo "-3" | ./validar
Quantidade: Ok: -3 unidades.       « o sinal também (confira se negativo faz sentido)
$ echo "7 " | ./validar
Quantidade: Sobrou texto depois do número: " "   « espaço no fim conta como sobra

Cada entrada errada recebeu a mensagem certa. Repare nas duas últimas: o strtol aceita -3, e uma quantidade negativa não faz sentido na lojinha. Conferir se o valor está na faixa esperada (qtd < 1, por exemplo) é trabalho seu, depois da conversão. Se você precisa do valor num int, confira também se ele cabe (qtd >= INT_MIN && qtd <= INT_MAX, com limits.h) antes de guardar. A aula Validar a Entrada transforma esse modelo numa função reutilizável.

O número grande demais, por dentro

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

int main(void) {
    errno = 0;
    long n = strtol("99999999999999999999", NULL, 10);
    printf("n = %ld\n", n);
    printf("LONG_MAX = %ld\n", LONG_MAX);
    printf("errno == ERANGE? %s\n", errno == ERANGE ? "sim" : "não");
    printf("atoi: %d\n", atoi("99999999999999999999"));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra grande.c -o grande
$ ./grande
n = 9223372036854775807
LONG_MAX = 9223372036854775807    « o maior long no Linux de 64 bits
errno == ERANGE? sim
atoi: -1                          « comportamento indefinido

Quando o número não cabe, o strtol devolve o maior valor possível (LONG_MAX, ou LONG_MIN para negativos) e põe ERANGE no errno: um resultado previsível e um aviso. O NULL no lugar de &fim quer dizer "não quero saber onde parou". No editor do guia, o long tem 4 bytes e o mesmo programa mostra 2147483647 nas duas primeiras linhas (veja O Editor do Guia).

Missão: a Ana digitou 12abc (já está no campo Entrada) e o programa aceitou 12 unidades. Troque o atoi por strtol com &fim e confira se sobrou texto: nesse caso, mostre Entrada inválida.. Depois, teste com 12 na Entrada: tem de continuar dando Ok: 12 unidades..

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

int main(void) {
    char linha[100];
    printf("Quantidade: ");
    if (fgets(linha, sizeof linha, stdin) == NULL) {
        return 1;
    }
    linha[strcspn(linha, "\n")] = '\0';

    int qtd = atoi(linha);
    printf("Ok: %d unidades.\n", qtd);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    char linha[100];
    printf("Quantidade: ");
    if (fgets(linha, sizeof linha, stdin) == NULL) {
        return 1;
    }
    linha[strcspn(linha, "\n")] = '\0';

    char *fim;
    long qtd = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0') {
        printf("Entrada inválida.\n");
        return 1;
    }
    printf("Ok: %ld unidades.\n", qtd);
    return 0;
}

3. Outras bases

O 3º argumento do strtol é a base. Além de 10, as mais usadas são 2 (binário), 16 (hexadecimal, com dígitos de 0 a 9 e de a a f) e a base 0, que descobre a base pelo jeito como o número começa:

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

int main(void) {
    printf("\"ff\" na base 16   = %ld\n", strtol("ff", NULL, 16));
    printf("\"1010\" na base 2  = %ld\n", strtol("1010", NULL, 2));
    printf("\"0x1F\" na base 0  = %ld\n", strtol("0x1F", NULL, 0));
    printf("\"017\" na base 0   = %ld\n", strtol("017", NULL, 0));
    printf("\"017\" na base 10  = %ld\n", strtol("017", NULL, 10));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra bases.c -o bases
$ ./bases
"ff" na base 16   = 255
"1010" na base 2  = 10
"0x1F" na base 0  = 31     « começa com 0x: hexadecimal
"017" na base 0   = 15     « começa com 0: octal (base 8)!
"017" na base 10  = 17
A base 0 e o zero à esquerda

Na base 0, um número que começa com 0 é lido em octal (base 8), como no código C (aula Inteiros). Um CEP, um mês "07" ou um código "017" digitado por uma pessoa viraria outra coisa. Para números digitados por gente, use sempre a base 10.


4. strtod: números com casas decimais

strtod = "string to double". Funciona como o strtol, com o mesmo &fim e o mesmo errno, mas sem a base:

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

int main(void) {
    char *fim;
    double preco = strtod("4.99", &fim);
    printf("preço = %.2f (sobrou \"%s\")\n", preco, fim);
    double virgula = strtod("4,99", &fim);
    printf("com vírgula = %.2f (sobrou \"%s\")\n", virgula, fim);
    double cientifico = strtod("1.5e3", &fim);
    printf("1.5e3 = %.1f\n", cientifico);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra strtod.c -o strtod
$ ./strtod
preço = 4.99 (sobrou "")
com vírgula = 4.00 (sobrou ",99")    « parou na vírgula!
1.5e3 = 1500.0                       « notação científica: 1,5 × 10³

O C usa o ponto como separador decimal, como os Estados Unidos. Com "4,99", o strtod leu o 4 e parou na vírgula, e o fim mostra isso. Num programa para brasileiros, há duas saídas simples: avisar "use ponto" quando *fim for uma vírgula, ou trocar a vírgula por ponto na string antes de converter (com strchr, aula Funções de String). Existe também o atof ("ASCII to float"), o atoi dos decimais, com o mesmo defeito: não avisa erro.


5. sscanf: vários valores de uma vez

O sscanf ("string scanf") é o scanf que lê de uma string em vez do teclado. É prático quando o texto tem um formato fixo, como uma data:

#include <stdio.h>

int main(void) {
    char data[] = "17/10/2026";
    int dia, mes, ano;
    int lidos = sscanf(data, "%d/%d/%d", &dia, &mes, &ano);
    printf("lidos = %d\n", lidos);
    printf("dia %d, mês %d, ano %d\n", dia, mes, ano);

    char outra[] = "17-10-2026";
    lidos = sscanf(outra, "%d/%d/%d", &dia, &mes, &ano);
    printf("com traços: lidos = %d\n", lidos);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra sscanf.c -o sscanf
$ ./sscanf
lidos = 3
dia 17, mês 10, ano 2026
com traços: lidos = 1     « leu o 17 e travou no '-', que não é '/'
sscanf(data, …)
O 1º argumento é a string de onde ler; o resto é igual ao scanf (aula Entrada com scanf).
"%d/%d/%d"
Um número, uma barra, um número, uma barra, um número. As barras do formato precisam aparecer no texto.
lidos
Quantos valores foram lidos. Confira se é o número esperado (3): com traços, só o dia foi lido.

O sscanf com %d detecta texto que não é número (o retorno fica menor), mas não detecta número grande demais: isso é comportamento indefinido. Para um único número digitado, prefira o strtol; para um formato com várias partes, o sscanf é a ferramenta mais prática.


6. Número → texto: snprintf

O caminho de volta é o snprintf da aula Funções de String: o printf que escreve num array, com limite. Todos os formatos do printf valem (aula Formatos do printf):

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

int main(void) {
    int pedido = 42;
    double total = 37.5;
    char codigo[20];
    char valor[20];

    snprintf(codigo, sizeof codigo, "PED-%05d", pedido);
    snprintf(valor, sizeof valor, "R$ %.2f", total);

    printf("%s: %s\n", codigo, valor);
    printf("O código tem %zu caracteres.\n", strlen(codigo));
    return 0;
}
"PED-%05d"
O número com 5 dígitos, completando com zeros à esquerda.
"R$ %.2f"
O double com 2 casas decimais.
strlen(codigo)
Agora é uma string de verdade: dá para medir, comparar, copiar, guardar num arquivo.
$ gcc -std=c23 -Wall -Wextra paratexto.c -o paratexto
$ ./paratexto
PED-00042: R$ 37.50
O código tem 9 caracteres.

Você pode encontrar em tutoriais uma função itoa ("integer to ASCII"). Ela não faz parte do C: existia em alguns compiladores antigos do Windows. No gcc 15:

$ gcc -std=c23 -Wall -Wextra itoa.c -o itoa
itoa.c: In function ‘main’:
itoa.c:6:5: error: implicit declaration of function ‘itoa’ [-Wimplicit-function-declaration]
    6 |     itoa(42, texto, 10);
      |     ^~~~

O jeito portátil é sempre snprintf(texto, sizeof texto, "%d", n);. E o sprintf (sem o n) tem o mesmo problema do strcpy: não recebe o tamanho do array.


Qual usar

SituaçãoUse
Um inteiro digitado por uma pessoastrtol(texto, &fim, 10) + as três conferências
Um número com casas decimais digitadostrtod(texto, &fim) + as mesmas conferências
Vários valores num formato fixo (data, hora, coordenadas)sscanf, conferindo o retorno
Texto que com certeza é um número válidoatoi / atof servem, mas o strtol nunca é pior
Número → textosnprintf(dest, sizeof dest, "%d", n)

Quando dá errado

Digitei letras e o programa aceitou como 0 (ou "12abc" virou 12)

O que significa: o programa usa atoi, que não detecta erro.

Como consertar: troque por strtol(texto, &fim, 10) e confira fim == texto (nenhum dígito) e *fim != '\0' (sobrou texto).

Todo número digitado dá "sobrou texto depois do número"

O que significa: a linha veio do fgets com o '\n' no fim, e o strtol parou nele.

Como consertar: tire o '\n' antes de converter: linha[strcspn(linha, "\n")] = '\0';.

O errno diz ERANGE para um número pequeno

O que significa: o errno tinha um erro antigo de outra função; o strtol não o zera quando dá certo.

Como consertar: escreva errno = 0; logo antes do strtol.

"4,99" virou 4.00

O que significa: o strtod só entende ponto; parou na vírgula.

Como consertar: confira *fim; avise para usar ponto ou troque a vírgula por ponto antes de converter.

"08" ou "017" virou outro número (ou parou no meio)

O que significa: você usou a base 0, que lê números com zero à esquerda em octal ("017" = 15; em "08", o 8 nem é dígito octal).

Como consertar: use a base 10.

error: implicit declaration of function ‘itoa’

O que significa: itoa não existe no C padrão.

Como consertar: snprintf(texto, sizeof texto, "%d", n);.

error: implicit declaration of function ‘strtol’ (ou ‘atoi’)

O que significa: faltou o #include <stdlib.h>. Para errno e ERANGE, falta o #include <errno.h>.

Como consertar: acrescente os includes no topo.


Referência rápida

FunçãoNome em inglêsCabeçalhoConverteDetecta erro?
atoi(s)ASCII to integerstdlib.htexto → intNão (devolve 0; estouro é indefinido)
atol(s)ASCII to longstdlib.htexto → longNão
atof(s)ASCII to floatstdlib.htexto → doubleNão
strtol(s, &fim, base)string to longstdlib.htexto → longSim: fim e errno == ERANGE
strtoll(s, &fim, base)string to long longstdlib.htexto → long longSim
strtoul(s, &fim, base)string to unsigned longstdlib.htexto → unsigned longSim (mas aceita "-1"!)
strtod(s, &fim)string to doublestdlib.htexto → doubleSim
sscanf(s, "%d", &n)string scan formattedstdio.htexto → váriosEm parte: retorno conta os lidos; estouro é indefinido
snprintf(d, sizeof d, "%d", n)string n print formattedstdio.hnúmero → textoRetorno: tamanho que o texto teria

Exercícios

1. A base certa

Complete para converter um texto digitado por uma pessoa:

long n = strtol(texto, &fim, );

2. Nenhum dígito

Complete a condição que é verdadeira quando o texto não tinha nenhum número:

if (fim texto)

3. Número para texto

Complete para guardar o número n como texto em s:

(s, sizeof s, "%d", n);

4. Quanto vale atoi("abc")?

5. Depois de strtol("30kg", &fim, 10), o que printf("%s", fim) mostra?

6. Por que escrever errno = 0; antes do strtol?

7. O que strtod("2,5", &fim) devolve?

Missão final: o recibo da lojinha

Missão: a Ana digita a quantidade (3) e o preço unitário (2.50), uma linha cada (já estão no campo Entrada). Converta a quantidade com strtol e o preço com strtod, calcule o total e monte o recibo numa string com snprintf, no formato "%ld x R$ %.2f = R$ %.2f". Mostre o recibo e quantos caracteres ele tem. Sem avisos.

3 x R$ 2.50 = R$ 7.50
Recibo com 21 caracteres
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    char linha_qtd[50];
    char linha_preco[50];
    char recibo[60] = "";

    if (fgets(linha_qtd, sizeof linha_qtd, stdin) == NULL ||
        fgets(linha_preco, sizeof linha_preco, stdin) == NULL) {
        return 1;
    }

    // converta, calcule e monte o recibo aqui

    printf("%s\n", recibo);
    printf("Recibo com %zu caracteres\n", strlen(recibo));
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    char linha_qtd[50];
    char linha_preco[50];
    char recibo[60] = "";

    if (fgets(linha_qtd, sizeof linha_qtd, stdin) == NULL ||
        fgets(linha_preco, sizeof linha_preco, stdin) == NULL) {
        return 1;
    }

    long qtd = strtol(linha_qtd, NULL, 10);
    double preco = strtod(linha_preco, NULL);
    double total = qtd * preco;
    snprintf(recibo, sizeof recibo, "%ld x R$ %.2f = R$ %.2f", qtd, preco, total);

    printf("%s\n", recibo);
    printf("Recibo com %zu caracteres\n", strlen(recibo));
    return 0;
}

Aqui o NULL no lugar de &fim deixa a solução curta, e o '\n' no fim das linhas não atrapalha porque nada é conferido. Num programa de verdade, use o modelo da seção 2 com as três conferências. O recibo começa como string vazia (= "") para o programa inicial não mostrar lixo.


Resumo

  • Texto que parece número ("20") não é número: é preciso converter para fazer contas.
  • atoi é curto, mas devolve 0 para texto inválido, ignora lixo depois do número e tem resultado indefinido para números grandes. Só para texto que com certeza é válido.
  • strtol(texto, &fim, 10) é o jeito certo: fim == texto (nenhum dígito), *fim != '\0' (sobrou texto), errno == ERANGE (grande demais, com errno = 0 antes). Tire o '\n' do fgets antes.
  • Use base 10 para números digitados; a base 0 lê "017" como octal.
  • strtod converte decimais e só entende ponto. sscanf tira vários valores de um formato fixo; confira o retorno.
  • Número → texto: snprintf(dest, sizeof dest, "%d", n). itoa não existe no C padrão.