DEV Community

Matheus de Gondra
Matheus de Gondra

Posted on

Liguagem C moderna e suas versões

Se você estuda programação você deve ter visto ou ao menos ouvido falar sobre a linguagem C. Essa linguagem que muitos não entenderam nada ao ver a primeira vez e já sofreram muitos com o uso de ponteiro e gerenciamento de memória manual.

Ao procurar sobre tutoriais ou na faculdade, muitas vezes é ensinado a versão ANSI C ou ISO C. Essa é uma versão bem antiga do C, mas assim como toda linguagem de programação o C foi se modernizando ao decorrer do tempo.

Nesse artigo eu quero comentar um pouco dessas versões que acabei descobrindo sobre elas ao fazer um projeto em C para a faculdade.

C89/C90

A linguagem C foi criada por Dennis Ritchie em 1972. E em 1989 o instituto norte-americano de padrões (ANSI) definiu as especificações para a linguagem C. Posteriormente, essa especificação sofreu pequenas mudanças na organização do documento pela Organização Internacional de Padrões (ISO). Esses padrões da linguagem ficaram conhecidos, respectivamente, como C89 e C90.

Essa versão da linguagem permite uma alta portabilidade da linguagem e é usado em sistemas legados e sistema embarcados sem suporte para versões modernas.

O ISO C ou C90 possui algumas características que são que todas as variáveis devem ser declaradas no inicio de um bloco de código.

main.c

#include <stdio.h>

int main() {
    int x = 5;

    printf("O valor de x é: %d\n", x);

    int y = 10;

    printf("O valor de y é: %d\n", y);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Para compilar para C90 vamos usar a flag -std=c90 e -pedantic para o alerta aparecer ou -pedantic-errors para transforma o alerta em erro e impedir a compilação.

gcc main.c -std=c90 -pedantic-errors 
main.c: In function ‘main’:
main.c:8:5: error: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
    8 |     int y = 10;
      |     ^~~
Enter fullscreen mode Exit fullscreen mode

No C90 não havia comentário de uma linha com o // apenas blocos de comentário com /* */. Usar comentários com // gera um erro de compilação.

main.c

int main() {
    /* comentario no C89/C90 */

    // isso gera erro
    return 0;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c90                 
main.c: In function ‘main’:
main.c:4:5: error: C++ style comments are not allowed in ISO C90
    4 |     // isso gera erro
      |     ^
main.c:4:5: note: (this will be reported only once per input file)
Enter fullscreen mode Exit fullscreen mode

O loop for não permitia que você declarasse a variável dentro dele. Ela deveria ser declarada antes, e como dito, no inicio do bloco.

#include <stdio.h>

int main() {
    /* correto no C89/C90 */
    int i;

    for (i = 0; i < 10; i++) {
        printf("Hello, World! %d\n", i);
    }

    /* erro no C89/C90 */
    for (int j = 0; j < 10; j++) {
        printf("Hello, World! %d\n", j);
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c90 
main.c: In function ‘main’:
main.c:12:5: error: ‘for’ loop initial declarations are only allowed in C99 or C11 mode
   12 |     for (int j = 0; j < 10; j++) {
      |     ^~~
main.c:12:5: note: use option ‘-std=c99’, ‘-std=gnu99’, ‘-std=c11’ or ‘-std=gnu11’ to compile your code
Enter fullscreen mode Exit fullscreen mode

C99

Em 1999, a linguagem C recebeu uma revisão e surgiu a versão conhecida como C99. Essa versão trouxe muitas melhorias a linguagem.

Uma das mudanças e que agora variáveis não precisavam mais estar no inicio do bloco e a variável de controle do for poderia ser declarada dentro dele. Também era possível usar comentário com //.

O C99 trouxe os tipos booleanos para a linguagem através do cabeçalho stdbool.h. Ou seja, quando disserem que C não tem tipo booleano, saiba que ele tem desde da versão C99!

main.c

#include <stdio.h>
#include <stdbool.h> // adiciona suporte a booleanos

int main() {
    bool reagiu_ao_post = true; // exemplo de variável booleana

    if (!reagiu_ao_post) {
        printf("Deixe sua reação ao post!\n");
    } else {
        printf("Obrigado por reagir ao post!\n");
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c99 -o main && ./main
Obrigado por reagir ao post!
Enter fullscreen mode Exit fullscreen mode

Outra adição é tipos inteiros com tamanho padronizados. Pois, o tipo int poderia ter tamanhos diferentes dependendo do compilador e da arquitetura do sistema. Quando você tem pouca memória, como em sistemas embarcados, essa variação pode te fazer consumir mais memória do que você acha que está ou estar com memória sobrando por ter um inteiro menor do que você achava.

O C99 adicionou o cabeçalho stdint.h com esses novos tipos inteiros:

  • int8_t: inteiro com sinal de 8 bits ou 1 byte. Armazena de -128 até 127.
  • uint8_t: inteiro sem sinal de 8 bits ou 1 byte. Armazena de 0 até 255.
  • int16_t: inteiro com sinal de 16 bits ou 2 bytes. Armazena de -32.768 até 32.767.
  • uint16_t: inteiro sem sinal de 16 bits ou 2 bytes. Armazena de 0 até 65.535.
  • int32_t: inteiro com sinal de 32 bits ou 4 bytes. Armazena de -2.147.483.648 até 2.147.483.647.
  • uint32_t: inteiro sem sinal de 32 bits ou 4 bytes. Armazena de 0 até 4.294.967.295.
  • int64_t: inteiro com sinal de 64 bits ou 8 bytes. Armazena de -9.223.372.036.854.775.808 até 9.223.372.036.854.775.807.
  • uint64_t: inteiro sem sinal de 64 bits ou 8 bytes. Armazena de 0 até 18.446.744.073.709.551.615.

E temos o cabeçalho inttypes.h que adicionar macros de formatação para lidar com a coleta e impressão desses tipos.

Para a impressão temos macros que são formadas com o seguinte padrão:

  1. PRI: significa que é um print do valor
  2. formato: formato para inteiros usado no printf como d para inteiro com sinal e u para sem sinal ou x para hexadecimal.
  3. tamanho: tamanhos dos tipos do stdint.h como 8, 16, 32 ou 64

main.c

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main() {
    int8_t num = INT8_MAX; // inteiro de 8 bits com sinal
    uint8_t unum = UINT8_MAX; // inteiro de 8 bits sem sinal

    int16_t num16 = INT16_MAX; // inteiro de 16 bits com sinal
    uint16_t unum16 = UINT16_MAX; // inteiro de 16 bits sem sinal

    int32_t num32 = INT32_MAX; // inteiro de 32 bits com sinal
    uint32_t unum32 = UINT32_MAX; // inteiro de 32 bits sem sinal

    int64_t num64 = INT64_MAX; // inteiro de 64 bits com sinal
    uint64_t unum64 = UINT64_MAX; // inteiro de 64 bits sem sinal

    // O compilador junta as strings literal na compilação
    printf("int8_t:   %" PRId8 "\n", num);
    printf("int16_t:  %" PRId16 "\n", num16);
    printf("int32_t:  %" PRId32 "\n", num32);
    printf("int64_t:  %" PRId64 "\n", num64);
    printf("uint8_t:  %" PRIu8 "\n", unum);
    printf("uint16_t: %" PRIu16 "\n", unum16);
    printf("uint32_t: %" PRIu32 "\n", unum32);
    printf("uint64_t: %" PRIu64 "\n", unum64);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c99 -o main && ./main
int8_t:   127
int16_t:  32767
int32_t:  2147483647
int64_t:  9223372036854775807
uint8_t:  255
uint16_t: 65535
uint32_t: 4294967295
uint64_t: 18446744073709551615
Enter fullscreen mode Exit fullscreen mode

Temos uma macro para escanear valores do usuário. Essas macros segue o seguinte formato:

  1. SCN: significa que é um sca*n* do valor
  2. formato: formato para inteiros usado no scanf como d para inteiro com sinal e u para sem sinal ou x para hexadecimal.
  3. tamanho: tamanhos dos tipos do stdint.h como 8, 16, 32 ou 64

main.c

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main(){
    int8_t age;

    printf("Digite sua idade: ");
    scanf("%" SCNd8, &age);

    printf("Voce tem %" PRId8 " anos.\n", age);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c99 -o main && ./main
Digite sua idade: 25
Voce tem 25 anos.
Enter fullscreen mode Exit fullscreen mode

O C99 adicionou versões mais seguras de algumas funções como a fnprintf para ser usada no lugar da sprintf que poderia causar um estouro de buffer.

main.c

#include <stdio.h>

int main() {
    char buffer[30];
    char string[] = "Linguagem C";

    /*
     Causa um buffer overflow, pois a string formatada é maior que o tamanho do buffer

    sprintf(buffer, "Escrevendo a string formatada %s.", string); 

    */

    // Evita o buffer overflow, pois limita a escrita ao tamanho do buffer
    snprintf(buffer, sizeof(buffer), "formatada %s.", string); 

    printf("%s\n", buffer);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c99 -o main && ./main
formatada Linguagem C.
Enter fullscreen mode Exit fullscreen mode

Foi adicionado também um suporte matemático melhor com complex.h para lidar com números complexos. Uma api genérica com macros do tgmath.h que chama a função correta de acordo com o tipo passado, por exemplo a macro sin() chama as funções especificas para cada tipo, como sin() para double, sinf() para float, etc. E foi adicionado mais funções para math.h

main.c

#include <complex.h>
#include <tgmath.h>

int main() {
    double x = 10.0f;
    float y = 10.0f;
    double complex z = 2.0 * I;

    sin(x);  // chama sin(x)
    sin(y);  // chama sinf(y)
    sin(z);  // chama csin(z)

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

C11

Em 2011 uma nova versão estável da linguagem C, chamada C11. Essa versão trouxe um suporte nativo a threads e operações atômicas.

Antes, para usar threads no C era necessário usar o Posix Threads no Linux com o pthreads.h e a Windows API no Windows com o windows.h. No C11 foi adicionado o cabeçalho threads.h para trabalhar com threads e stdatomic.h para operações atômicas, evitando condições de corrida.

main.c

#include <stdio.h>
#include <stdlib.h> // para macro EXIT_SUCCESS
#include <threads.h>
#include <stdatomic.h>

int incrementar_contador(void* arg) {
    atomic_int *contador = (atomic_int*) arg;

    for (int i = 0; i < 100; i++) {
        atomic_fetch_add(contador, 1); // incrementa o contador 100 vezes
    }

    return EXIT_SUCCESS; // retorna 0 de forma mais clara, indicando sucesso
}

int main() {
    const int num_threads = 10;
    atomic_int contador = 0;
    thrd_t threads[num_threads];


    // cria as threads para incrementar o contador
    for (int i = 0; i < num_threads; i++) {
        // cria a thread passando incrementar_contador para ser executado nela com a parâmetro da função sendo o contador
        thrd_create(&threads[i], incrementar_contador, &contador);
    }

    // aguarda as threads terminarem
    for (int i = 0; i < num_threads; i++) {
        thrd_join(threads[i], NULL);
    }

    printf("Valor final do contador: %d\n", contador);

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c11 -o main && ./main
Valor final do contador: 1000
Enter fullscreen mode Exit fullscreen mode

O C11 introduziu o _Generic que retorna instruções baseado no tipo passado para ele.

Você passa a expressão no primeiro argumento e dependendo do tipo da expressão ele retorna algo.

_Generic(expressao, tipo1: instrucao1, tipo2: instrucao2, ..., tipoN: instrucaoN)
Enter fullscreen mode Exit fullscreen mode

Isso nos permite cria macros genéricas. Por exemplo, uma macro print_arr para printar qualquer tipo de array. A macro irá receber o array e o tamanho e dependendo do tipo do array irá retornar a função correta para printar e irá executar ela.

main.c

#include <stdio.h>

// o \ permite quebrar linha nas macros
#define print_arr(arr, size) _Generic((arr), \
    int*: print_arr_int, \
    double*: print_arr_double, \
    char*: print_arr_char, \
    float*: print_arr_float \
)(arr, size) // _Generic retorna uma função e executamos passando arr e size

void print_arr_int(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

void print_arr_double(double *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%.2lf ", arr[i]);
    }
    printf("\n");
}

void print_arr_char(char *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%c ", arr[i]);
    }
    printf("\n");
}

void print_arr_float(float *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%.2f ", arr[i]);
    }
    printf("\n");
}

int main() {
    char char_arr[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
    int int_arr[6] = {1, 2, 3, 4, 5, 6};
    double double_arr[6] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6};
    float float_arr[6] = {1.1f, 2.2f, 3.3f, 4.4f, 5.5f, 6.6f};

    print_arr(char_arr, 6);
    print_arr(int_arr, 6);
    print_arr(double_arr, 6);
    print_arr(float_arr, 6);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c11 -o main && ./main
H e l l o  
1 2 3 4 5 6
1.10 2.20 3.30 4.40 5.50 6.60
1.10 2.20 3.30 4.40 5.50 6.60
Enter fullscreen mode Exit fullscreen mode

Temos asserções estáticas com o _Static_assert para fazer verificações em tempo de compilação em vez de tempo de execução.

main.c

#include <stdlib.h>

// Garante em tempo de compilação que o int tenha 4 bytes
_Static_assert(sizeof(int) != 4, "O compilador nao suporta inteiros de 4 bytes");


int main() {

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
 gcc main.c -std=c11 -o main && ./main
main.c:5:1: error: static assertion failed: "O compilador n\37777777703\37777777643o suporta inteiros de 4 
bytes"
    5 | _Static_assert(sizeof(int) != 4, "O compilador nao suporta inteiros de 4 bytes");
      | ^~~~~~~~~~~~~~
Enter fullscreen mode Exit fullscreen mode

C17

O C17 é uma versão que não trouxe novas funcionalidades. Essa versão focou em correções de bug da versão C11.

C23

Esse é a versão mais atual da linguagem C, lançada em 2023, o C23 trouxe grandes mudanças trazendo funcionalidades que deixaram o C mais próximo ao C++.

Uma dessas mudança é que os tipos booleanos se tornaram padrão e você não precisa mais incluir o cabeçalho stdbool.h para usar no seu código.

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

int main() {
    bool isValid = false;

    isValid = 10 == 10;
    if (isValid) {
        printf("O valor e valido: %d\n", isValid);
    } else {
        printf("O valor nao e valido: %d\n", isValid);
    }

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c23 -o main  && ./main
O valor e valido: 1
Enter fullscreen mode Exit fullscreen mode

Foi introduzido na linguagem o nullptr que, como no C++, faz um ponteiro apontar para nada. Antes era usado a macro NULL, mas em alguns casos essa macro poderia causar comportamentos inesperados.

A macro NULL geralmente é expandida para 0 ou (void *)0. Se NULL fosse passado para um _Generic ele seria tratado como int ou void *. Já o nullptr é um novo tipo para representar um valor de um ponteiro nulo e não seria confundido com outro tipo

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

int main() {
    _Generic(NULL, 
        int: printf("NULL e um inteiro\n"),
        int*: printf("NULL e um ponteiro do tipo int\n"), 
        void*: printf("NULL e um ponteiro do tipo void\n"), 
        default: printf("NULL e de um tipo desconhecido\n")
    );

    _Generic(nullptr, 
        int: printf("nullptr e um inteiro\n"),
        int*: printf("nullptr e um ponteiro do tipo int\n"), 
        void*: printf("nullptr e um ponteiro do tipo void\n"), 
        default: printf("nullptr e de um tipo desconhecido\n")
    );

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c23 -o main  && ./main
NULL e um ponteiro do tipo void
nullptr e de um tipo desconhecido
Enter fullscreen mode Exit fullscreen mode

No C23 a palavra chave auto foi redefinida. Antes o auto era um modificador para indicar que a variável tinha armazenamento automático na stack, servia como a contrapartida do static. Mas, isso já era o padrão de uma variável e usar o auto era uma redundância.

// as duas declarações são a mesma coisa
int valor = 10;
auto int outro_valor = 10;
Enter fullscreen mode Exit fullscreen mode

A partir do C23, você pode usar o auto para inferir o tipo na variável. Há também o typeof que serve para inferir o tipo em uma variável.

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

typedef struct { 
    char *name;
    uint8_t age;
} User;

int main() {
    auto age = 10; // infere o tipo int
    auto name = "Fulano"; // infere o tipo const char* (string literal)

    User user = { .name = name, .age = age };
    // typeof infere o tipo da variável user e cria user2 com o mesmo tipo
    typeof(user) user2 = { .name = "Ciclano", .age = 20 }; 

    printf("User 1: %s, Age: %d\n", user.name, user.age);
    printf("User 2: %s, Age: %d\n", user2.name, user2.age);

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c23 -o main  && ./main
User 1: Fulano, Age: 25
User 2: Ciclano, Age: 20
Enter fullscreen mode Exit fullscreen mode

Temos o constexpr que cria uma constante. Diferente do const que é avaliado em tempo de execução o constexpr é avaliado em tempo de compilação.

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

int main() {
    // VALUE é resolvido em tempo de compilação, permitindo a definição de um array de tamanho fixo
    // usar `const` daria um erro, pois `const` é resolvido em tempo de execução
    // e o tamanho do array precisa ser conhecido em tempo de compilação
    constexpr int VALUE = 4;
    int arr[VALUE] = { 1, 2, 3, 4 };

    for (int i = 0; i < VALUE; ++i) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c23 -o main  && ./main
1 2 3 4 
Enter fullscreen mode Exit fullscreen mode

C23 adicionou o prefixo 0b para declarar números binários literais e uso do ' para separar os dígitos dos números, facilitando a leitura.

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

int main() {
    int binario = 0b101; // O prefixo "0b" indica que o número é binário
    printf("O valor do binario 0b101 e: %d\n", binario);

    int milhao = 1'000'000; // O apóstrofo é usado como separador de dígitos para melhorar a legibilidade
    printf("O valor do milhao e: %d\n", milhao);

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c23 -o main  && ./main
O valor do binario 0b101 e: 5
O valor do milhao e: 1000000
Enter fullscreen mode Exit fullscreen mode

A inicialização segura de array, union e struct mudou um pouco. Antes para fazer a inicialização de valores de uma struct, union ou array você usava {0} e no C23 isso foi simplificado para {}.

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

struct ProdutoEstoque {
    int id_produto;
    float preco;
    char nome[50];
    int *historico_movimentacoes;
};

void print_produto(struct ProdutoEstoque *produto) {
    printf("ID: %d\n", produto->id_produto);
    printf("Preco: %.2f\n", produto->preco);
    printf("Nome: '%s'\n", produto->nome);

    if (produto->historico_movimentacoes == NULL) {
        printf("Ponteiro de historico inicializado como nulo de forma segura.\n");
    }
}

int main() {
    // Inicialização segura antes do C23
    struct ProdutoEstoque novo_item = {0}; 

    // Inicialização segura usando a nova sintaxe do C23
    struct ProdutoEstoque outro_item = {};

    print_produto(&novo_item);
    print_produto(&outro_item);

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c23 -o main && ./main
ID: 0
Preco: 0.00
Nome: ''
Ponteiro de historico inicializado como nulo de forma segura.
ID: 0
Preco: 0.00
Nome: ''
Ponteiro de historico inicializado como nulo de forma segura.
Enter fullscreen mode Exit fullscreen mode

Versão do compilador

Mostrei algumas funcionalidades de cada versão do C, mas como eu sei qual versão o meu compilador está usando?

Existe uma macro chamada __STDC_VERSION__ que armazena um número inteiro do tipo long que indica a versão do C usada na compilação. O valor dessa macro é o ano mais o mês de lançamento da versão. Usando a flag -std mudamos a versão do C usada na compilação e o valor dessa macro também é mudado.

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

/* 
O C89/C90 não define __STDC_VERSION__ então se for C89/C90
compilamos o primeiro main, caso contrário compilamos o segundo main
*/
#ifndef __STDC_VERSION__

int main() {
    printf("C89/C90\n");
    return EXIT_SUCCESS;
}

#else

int main() {
    switch (__STDC_VERSION__) {
        case 202311L: // ano 2023 + mês 11 = 202311
            printf("C23\n");
            break;

        case 201710L: // ano 2017 + mês 10 = 201710
            printf("C17\n");
            break;

        case 201112L: // ano 2011 + mês 12 = 201112
            printf("C11\n");
            break;

        case 199901L: // ano 1999 + mês 01 = 199901
            printf("C99\n");
            break;
    }

    return EXIT_SUCCESS;
}
#endif
Enter fullscreen mode Exit fullscreen mode
gcc main.c -std=c89 -o main && ./main
C89/C90

gcc main.c -std=c99 -o main && ./main
C99

gcc main.c -std=c11 -o main && ./main
C11

gcc main.c -std=c17 -o main && ./main
C17

gcc main.c -std=c23 -o main && ./main
C23
Enter fullscreen mode Exit fullscreen mode

Eu estou usando o gcc versão 15.2.0, que adota o C23 como padrão ao compilar. Se eu tirar a flag -std e compilar terei "C23" como saída.

gcc main.c -o main && ./main
C23
Enter fullscreen mode Exit fullscreen mode

Top comments (0)