25 anos de evolução da linguagem C: de C99 a C23

Introdução

Quando falamos em sistemas embarcados, é comum encontrar projetos que permanecem ativos por décadas. Muitas vezes utilizamos compiladores modernos para compilar códigos escritos sob premissas que remontam ao padrão ANSI C, publicado em 1989.

Enquanto outras linguagens passaram por transformações profundas, a linguagem C evoluiu de forma gradual e conservadora. Ainda assim, os padrões C99, C11, C17 e C23 introduziram recursos que melhoram a segurança, a legibilidade e a capacidade de manutenção do código.

Neste artigo, faremos uma viagem pela evolução da linguagem C ao longo dos últimos 25 anos e discutiremos quais mudanças tiveram maior impacto para o desenvolvimento de firmware.

O mundo antes do C99

Durante muitos anos, o padrão predominante foi o C89/C90.

Algumas limitações eram bastante conhecidas:

  • O padrão não definia tipos inteiros portáveis de largura exata, como uint8_t e uint32_t.
  • Não existiam inicializadores designados.
  • Variáveis precisavam ser declaradas no início do bloco.
  • Não existiam macros variádicas.
  • Comentários de linha (//) não faziam parte do padrão.

Um exemplo clássico:

int i;

for (i = 0; i < 10; i++)
{
    ...
}

Embora perfeitamente funcional, essa sintaxe era mais restritiva do que a encontrada em linguagens modernas.

A revolução chamada C99

O C99 (ISO/IEC 9899:1999) [2] foi provavelmente a atualização mais importante da história da linguagem.

Declarações dentro do for

for (int i = 0; i < 10; i++)
{
    ...
}

Além de reduzir o escopo da variável, essa abordagem evita reutilizações acidentais. O C99 também passou a permitir declarações em qualquer ponto do bloco, e não apenas no início, além de oficializar os comentários com //.

Inteiros de tamanho fixo

O tamanho de tipos como int e long varia conforme a arquitetura, e isso continua assim. Antes do C99, cada compilador, fabricante ou projeto definia seus próprios tipos de largura exata, com nomes como UINT8 ou u32, e trocar de plataforma significava revisar esses typedefs.

O cabeçalho stdint.h introduziu tipos como:

uint8_t
uint16_t
uint32_t
uint64_t

Para firmware, essa mudança foi fundamental: registradores, campos de protocolo e estruturas gravadas em flash têm tamanho definido, e agora o código pode dizer isso explicitamente.

Um detalhe pouco conhecido: os tipos de largura exata são opcionais. Eles só existem se a plataforma tiver um tipo com exatamente aquela largura. Em DSPs nos quais o char tem 16 bits, como a família C2000 da Texas Instruments, uint8_t pode simplesmente não existir. Para esses casos o padrão oferece uint_least8_t (o menor tipo com pelo menos 8 bits) e uint_fast8_t (o mais rápido com pelo menos 8 bits). O cabeçalho complementar inttypes.h traz as macros de formatação para usar esses tipos com printf de forma portável. Cada tipo tem a sua: PRIu32 corresponde a uint32_t, enquanto uint_least8_t usa PRIuLEAST8 e uint_fast8_t usa PRIuFAST8.

Inicializadores designados

struct uart_config cfg = {
    .baudrate = 115200,
    .parity = UART_PARITY_NONE,
    .stop_bits = 1
};

Além de mais legível, o código passa a ser menos sensível à ordem dos campos. Os campos não citados são inicializados com zero.

O mesmo vale para vetores, indexando os elementos pela posição:

static const uint8_t led_pin[LED_COUNT] = {
    [LED_STATUS] = 13,
    [LED_ERROR]  = 12,
};

Combinado com um enum, o vetor fica protegido contra mudanças na ordem das constantes. Explorei esse recurso e os literais compostos com mais detalhes no artigo Algumas particularidades sintáticas de C99, publicado aqui no Embarcados em 2016.

Literais compostos

send_message(&(message_t){
    .id = 1,
    .len = 8
});

Muito útil para construção de objetos temporários. Atenção ao tempo de vida: dentro de uma função, o literal composto tem armazenamento automático e só existe até o fim do bloco em que foi criado. Guardar esse ponteiro para uso posterior, por exemplo em uma fila processada por outra thread, é um erro.

Flexible array members

typedef struct {
    uint8_t id;
    uint8_t len;
    uint8_t payload[];
} packet_t;

packet_t *p = malloc(sizeof *p + n);

O último membro de uma struct pode ser um array sem tamanho. O sizeof(packet_t) considera apenas o cabeçalho, e o payload ocupa a memória alocada logo em seguida. É o modelo natural para pacotes de tamanho variável e substitui o antigo truque do payload[1], que na prática dependia de comportamento indefinido. Em firmware, a memória costuma vir do alocador do RTOS, e não do malloc da libc: no FreeRTOS, é o pvPortMalloc, implementado por um dos arquivos heap_1.c a heap_5.c (o heap_3.c apenas envolve o malloc da libc; os demais gerenciam um array estático); no Zephyr, o k_malloc ou um k_mem_slab, com blocos de tamanho fixo. O princípio é o mesmo.

Macros variádicas

#define LOG(fmt, ...) \
    printf(fmt, __VA_ARGS__)

Hoje amplamente utilizadas por frameworks como o Zephyr. Mas essa macro tem uma armadilha clássica:

LOG("valor = %d\n", x);   /* ok */
LOG("iniciando\n");       /* erro: vira printf("iniciando\n", ) */

Sem argumentos extras, sobra uma vírgula antes do parêntese. Durante anos a solução foi a extensão do GCC ##__VA_ARGS__, que remove a vírgula, mas não é padrão. A solução oficial só chegou com o C23, como veremos adiante.

inline e restrict

inline tornou-se parte do padrão, oferecendo uma alternativa tipada às macros para funções pequenas.

restrict permite ao programador prometer ao compilador que, em termos simplificados, dois ponteiros não apontam para regiões sobrepostas:

void copy(uint8_t *restrict dst, const uint8_t *restrict src, size_t n);

A regra exata é sobre acessos, não sobre endereços. Ao qualificar um ponteiro com restrict, o programador assume uma obrigação: enquanto o bloco em que o ponteiro foi declarado estiver em execução (no caso de um parâmetro, durante a chamada da função), todo objeto acessado por meio dele e que seja modificado só pode ser acessado por meio desse ponteiro ou de valores derivados dele, como dst + i. Por isso, dois ponteiros restrict que apenas leem a mesma região não violam a regra.

Com essa garantia, o compilador pode otimizar de forma mais agressiva. É exatamente o que a assinatura do memcpy declara desde o C99. Se a promessa for quebrada, o comportamento é indefinido.

Outras novidades

O C99 também trouxe long long, o tipo _Bool, o cabeçalho stdbool.h (que, nos padrões anteriores ao C23, disponibiliza o nome bool como macro para _Bool) e a função snprintf, que finalmente permitiu formatar strings com limite de tamanho.

Variable-length arrays: use com cuidado

void process(size_t n)
{
    uint8_t buffer[n];   /* tamanho decidido em tempo de execução, na pilha */
    ...
}

Os VLAs permitem declarar arrays cujo tamanho só é conhecido em tempo de execução. Em firmware, com pilhas de poucos kilobytes, isso é perigoso: um n maior que o esperado estoura a pilha sem nenhum aviso, e a análise estática do uso de pilha deixa de ser possível. A MISRA C:2012 proíbe o recurso na regra 18.8 (“Variable-length array types shall not be used”) [9], o kernel Linux eliminou todos os VLAs do seu código [6] e o próprio C11 tornou o recurso opcional. O GCC e o Clang oferecem a opção -Wvla para detectar seu uso.

O que o C11 trouxe

Embora menos revolucionário, o C11 (ISO/IEC 9899:2011) [3] introduziu recursos extremamente importantes.

Static Assertions

typedef struct {
    uint8_t  id;
    uint8_t  len;
    uint16_t crc;
    uint32_t timestamp;
} packet_header_t;

_Static_assert(sizeof(packet_header_t) == 8,
              "layout do cabeçalho mudou");
_Static_assert(offsetof(packet_header_t, timestamp) == 4,
              "timestamp fora da posição esperada");

Muito útil para protocolos e registradores mapeados em memória. Se alguém adicionar um campo ou se o compilador inserir padding, o erro aparece na compilação, e não em campo, com um dispositivo enviando pacotes corrompidos.

Note que a verificação protege as premissas daquele build, com aquele compilador e aquela arquitetura. Ela não torna o layout da estrutura portável: outro compilador pode inserir padding em outros pontos, e a ordem dos bytes (endianness) continua dependendo do processador. Para trocar dados com outros sistemas, serializar campo a campo continua sendo o caminho portável; o static_assert garante que, se uma premissa mudar, você vai saber.

Alinhamento explícito

#include <stdalign.h>

static alignas(32) uint8_t dma_buffer[512];

_Alignas (ou alignas, via stdalign.h) controla o alinhamento de uma variável, e _Alignof consulta o alinhamento de um tipo. Em microcontroladores com cache de dados, como o Cortex-M7, buffers de DMA precisam estar alinhados à linha de cache (32 bytes) para que as operações de limpeza e invalidação não afetem variáveis vizinhas. Antes isso exigia __attribute__((aligned(32))) ou o equivalente de cada compilador.

Structs e unions anônimas

typedef struct {
    uint8_t type;
    union {
        struct {
            int16_t  temperature;
            uint16_t humidity;
        };
        uint32_t raw;
    };
} reading_t;

reading_t r;
r.temperature = 215;   /* sem r.u.s.temperature */

Membros anônimos são acessados diretamente pelo objeto externo. O recurso é muito usado em mapas de registradores e em mensagens com formatos alternativos.

Atomics

#include <stdatomic.h>

static atomic_uint rx_count;

void uart_isr(void)
{
    atomic_fetch_add(&rx_count, 1);
}

Os atomics introduziram uma forma padronizada de lidar com concorrência sem depender de extensões específicas do compilador. Em firmware, a “concorrência” mais comum é entre o código principal e uma rotina de interrupção.

Mas cuidado: o padrão garante que a operação é atômica, não que ela seja lock-free, nem que seja adequada para uma interrupção. Em um Cortex-M4, o incremento acima vira um laço com as instruções LDREX/STREX, direto no código. O Cortex-M0 não possui essas instruções, e a operação vira uma chamada a uma função auxiliar cujo mecanismo o padrão não define: ela pode desabilitar interrupções, usar uma trava incompatível com ISRs ou simplesmente não existir no seu toolchain. As macros ATOMIC_INT_LOCK_FREE e semelhantes, que valem 0 (nunca lock-free), 1 (às vezes) ou 2 (sempre), permitem verificar isso na compilação:

_Static_assert(ATOMIC_INT_LOCK_FREE == 2,
              "atomic_int precisa ser lock-free para uso em ISR");

Com o GCC 14 do Zephyr SDK, o valor é 2 no Cortex-M4 e 1 no Cortex-M0, onde a verificação falha. O recurso também é opcional no padrão: uma implementação sem suporte define a macro __STDC_NO_ATOMICS__. Voltaremos a esse assunto em um artigo dedicado, que vai mostrar por que volatile não resolve esse problema e comparar os atomics da linguagem com as garantias do RTOS, como a API atomic_t do Zephyr [10].

Generic Selection

#define abs_val(x) _Generic((x), \
    int:    abs,                 \
    long:   labs,                \
    float:  fabsf,               \
    double: fabs)(x)

_Generic escolhe uma expressão em tempo de compilação conforme o tipo do argumento. Permite criar interfaces genéricas e seguras, que também serão tema de um artigo específico.

_Noreturn

_Noreturn void fault_handler(void);

Indica que a função nunca retorna, como um tratador de falhas que reinicia o sistema. O compilador pode otimizar o código de quem a chama e emitir avisos mais precisos.

Threads

O padrão C11 também definiu uma API portátil para threads (threads.h).

Embora pouco utilizada em microcontroladores, onde o papel de gerenciar threads é do RTOS, o recurso existe e faz parte da linguagem. Assim como os atomics, é opcional: implementações sem suporte definem __STDC_NO_THREADS__.

A despedida do gets

O C11 removeu da biblioteca padrão a função gets, que lia uma linha sem nenhum limite de tamanho e era sinônimo de buffer overflow. Foi a primeira vez que uma função foi removida da biblioteca padrão.

O discreto C17

O C17 foi essencialmente uma revisão corretiva do C11. Como foi publicado oficialmente em 2018 (ISO/IEC 9899:2018), também é chamado de C18 [5].

Seu objetivo principal foi:

  • corrigir ambiguidades;
  • corrigir defeitos do padrão anterior;
  • melhorar a documentação.

Do ponto de vista do programador, praticamente nenhuma novidade foi introduzida.

O que mudou com o C23

Após muitos anos sem mudanças significativas, o C23 (ISO/IEC 9899:2024) [4] trouxe diversas novidades visíveis. Boa parte delas oficializa extensões que o GCC e o Clang já ofereciam havia anos, ou recursos que vieram do C++. Para não transformar esta seção em uma lista interminável, os recursos estão agrupados pelo tipo de problema que resolvem.

bool e nullptr

Antes:

#include <stdbool.h>

bool enabled = true;

Depois:

bool enabled = true;
device_init(nullptr);

bool finalmente virou palavra-chave, sem necessidade do cabeçalho. O mesmo vale para true, false, alignas, alignof, static_assert e thread_local, que deixaram de ser macros de cabeçalho.

nullptr fornece uma representação explícita de ponteiro nulo. Mas por que não continuar usando NULL? O problema é que o padrão permite que NULL seja definido como 0, 0L ou ((void *)0), conforme a implementação. Quando NULL é definido como uma constante inteira, seu tipo pode influenciar a seleção de um _Generic e o tipo do argumento passado a uma função variádica, onde o tipo esperado não é verificado. Já nullptr tem um tipo próprio, nullptr_t, que se converte para tipos de ponteiro e para bool, mas não é um valor inteiro como 0. Em um Cortex-M, onde int e ponteiros têm os mesmos 32 bits, o ganho prático é mais de clareza e verificação de tipos do que de correção, mas em código portável a diferença é real.

Atributos padronizados

[[nodiscard("verifique o código de erro")]]int flash_write(uint32_t addr, const void *data, size_t len);

void handle(int event, [[maybe_unused]] void *ctx)
{
    switch (event) {
    case EVT_START:
        start();
        [[fallthrough]];
    case EVT_RESUME:
        resume();
        break;
    }
}

Esses recursos permitem que o compilador e as ferramentas de análise detectem problemas com maior facilidade. [[nodiscard]] avisa quando o retorno é ignorado (um erro de escrita em flash que ninguém verificou), [[fallthrough]] documenta que a falta de break é intencional e [[maybe_unused]] silencia o aviso de parâmetro não usado. Há também [[deprecated]] e [[noreturn]], que substitui o _Noreturn do C11. Antes, tudo isso exigia __attribute__ ou extensões equivalentes de cada compilador.

Código de baixo nível: literais binários e enums com tipo fixo

#define UART_CR1_TE   0b0000'1000
#define CPU_FREQ_HZ   480'000'000

enum sensor_state : uint8_t {
    SENSOR_IDLE,
    SENSOR_MEASURING,
    SENSOR_ERROR,
};

static_assert(sizeof(enum sensor_state) == 1);

Para quem escreve código de registradores, os literais binários talvez sejam o recurso mais esperado do C23. Máscaras escritas em binário dispensam a conversão mental a partir do hexadecimal, e o separador ‘ torna números longos legíveis. O GCC aceitava o prefixo 0b como extensão havia muito tempo, mas agora o recurso é padrão.

Já o tamanho de um enum, até o C17, era definido pela implementação. Normalmente era um int, mas opções como -fshort-enums mudam isso e quebram a compatibilidade binária entre módulos compilados de formas diferentes. Agora é possível controlar o tipo subjacente do enum, facilitando o controle de seu tamanho e representação, o que ajuda em estruturas de protocolo e em dados gravados em flash. Ainda assim, o layout completo de uma estrutura e a ordem dos bytes precisam ser tratados separadamente, como vimos no static_assert.

Declarações, tipos e constantes

constexpr uint32_t SYSTICK_HZ     = 1'000;
constexpr uint32_t SYSTICK_RELOAD = 480'000'000 / SYSTICK_HZ - 1;

static_assert(SYSTICK_RELOAD <= 0xFF'FFFF);   /* o SysTick tem 24 bits */

#define SWAP(a, b) do { typeof(a) tmp_ = (a); (a) = (b); (b) = tmp_; } while (0)

auto timeout = 1000u;          /* unsigned int */
packet_header_t hdr = {};      /* zera o objeto inteiro */

Um conjunto de melhorias menores, mas que mudam o jeito de escrever o dia a dia:

  • constexpr: constantes com tipo, verificadas em tempo de compilação e com escopo, como alternativa ao #define. Diferente do C++, no C23 o constexpr vale apenas para objetos, não para funções.
  • static_assert: agora é palavra-chave e a mensagem é opcional.
  • typeof: extensão do GCC usada há décadas, inclusive em macros do kernel Linux e do Zephyr, que agora é padrão. Há também typeof_unqual, que remove qualificadores como const e volatile.
  • auto: inferência de tipo a partir do inicializador, bem mais limitada que a do C++: vale apenas para declarações de variáveis com inicializador. E exige atenção: auto x = 1000; é um int, o que pode não ser o que você queria.
  • = {}: zera o objeto inteiro. Antes usava-se = {0}, que funciona, mas gera avisos em alguns compiladores quando o primeiro membro é uma struct ou um array.

Pré-processador: VA_OPT e #embed

#define LOG(fmt, ...) printf(fmt __VA_OPT__(,) __VA_ARGS__)

LOG("iniciando\n");       /* ok: printf("iniciando\n") */
LOG("valor = %d\n", x);   /* ok: printf("valor = %d\n", x) */

static const uint8_t ca_cert[] = {
#embed "certs/ca.der"
};

__VA_OPT__(,) só insere a vírgula quando há argumentos extras, resolvendo de forma padrão o problema que vimos no C99.

#embed incorpora o conteúdo de um arquivo binário diretamente no código. Certificados, chaves públicas, fontes e imagens deixam de depender de scripts com xxd -i ou de etapas extras no sistema de build.

Código mais seguro: ckd_add e memset_explicit

#include <stdckdint.h>
#include <string.h>

uint16_t total;

if (ckd_add(&total, header_len, payload_len)) {
    return -EOVERFLOW;
}

uint8_t psk[16];
...
memset_explicit(psk, 0, sizeof psk);

ckd_add, ckd_sub e ckd_mul retornam true quando o resultado não cabe no tipo de destino. Somar comprimentos recebidos pela rede sem esse cuidado é uma origem clássica de vulnerabilidades.

Um memset em um buffer que não será mais lido pode ser removido pelo otimizador, já que, do ponto de vista da linguagem, ele não tem efeito observável. Para apagar chaves e senhas da memória, isso é um problema. memset_explicit garante que a escrita aconteça. Ela resolve só a escrita final. Na prática, o que mais ajuda é reduzir as cópias do segredo: manter a chave em um único buffer, passar ponteiros em vez de copiá-la, não colocá-la no heap e apagá-la assim que deixar de ser necessária. As bibliotecas de criptografia já fazem isso internamente com funções próprias, como a mbedtls_platform_zeroize do mbedTLS. E, quando o hardware oferece armazenamento seguro de chaves, a chave nem precisa passar pela RAM da aplicação: com a API PSA Crypto, por exemplo, a aplicação manipula só um identificador, e a chave pode ficar dentro de uma fronteira de segurança, como um secure element ou o lado seguro da TrustZone, de onde não pode ser extraída [12, 13].

Biblioteca: stdbit.h e unreachable

O novo cabeçalho stdbit.h traz funções para operações que todo firmware acaba reimplementando: stdc_count_ones (quantidade de bits em 1), stdc_leading_zeros, stdc_trailing_zeros, stdc_has_single_bit (o valor é potência de 2?) e stdc_bit_width, entre outras. O compilador pode mapeá-las diretamente para instruções como a CLZ do Cortex-M3/M4.

#include <stddef.h>

switch (state) {
case SENSOR_IDLE:      return "IDLE";
case SENSOR_MEASURING: return "MEASURING";
case SENSOR_ERROR:     return "ERROR";
default:
    unreachable();
}

A macro unreachable(), declarada em stddef.h, informa ao compilador que um ponto do código nunca é alcançado, permitindo otimizações. Se for alcançado, o comportamento é indefinido. Por isso ela só cabe onde a própria lógica do programa garante que o ponto não é alcançado, como no switch acima, cujo state é sempre atribuído pelo próprio código. Um valor inválido vindo da rede, de um sensor ou da flash não é uma condição impossível, é uma entrada inesperada, e precisa ser validado e tratado como erro.

Como em firmware um estado “impossível” costuma ser sintoma de um bug, vale envolver a macro em uma versão do projeto, que em desenvolvimento registra onde o problema aconteceu:

#ifdef NDEBUG
#define FW_UNREACHABLE() unreachable()
#else
#define FW_UNREACHABLE() fw_panic(__FILE__, __LINE__)
#endif

Limpezas na linguagem

O C23 também removeu ou mudou comportamentos antigos:

  • Inteiros com sinal passam a ser obrigatoriamente representados em complemento de dois. Atenção: overflow de inteiro com sinal continua sendo comportamento indefinido.
  • Definições de função no estilo K&R foram removidas.
  • Uma declaração void f(); agora significa “função sem parâmetros”, como void f(void), e não mais “função com parâmetros não especificados”.

Há ainda _BitInt(N) para inteiros com largura arbitrária, #elifdef, #elifndef, #warning e __has_include no pré-processador, e o especificador %b no printf.

Como saber qual padrão estou usando?

A macro __STDC_VERSION__ informa o padrão com que o código está sendo compilado:

Padrão__STDC_VERSION__Opção no GCC/Clang
C99199901L-std=c99
C11201112L-std=c11
C17201710L-std=c17
C23202311L-std=c23

Compiladores mais antigos, como o GCC 13, só aceitam o nome provisório -std=c2x. As variantes -std=gnu99, -std=gnu11 etc. habilitam também as extensões GNU. A partir do GCC 15, o padrão default passou a ser gnu23 [7], mas muitos toolchains de fabricantes ainda trazem versões bem mais antigas do compilador.

Quais mudanças realmente importam para firmware?

Os recursos mais relevantes para sistemas embarcados tendem a ser:

  • stdint.h: registradores e protocolos têm tamanho fixo, e agora o código também tem. É difícil imaginar firmware moderno sem esses tipos.
  • Inicializadores designados: estruturas de configuração de periféricos são onipresentes, e inicializá-las por nome elimina uma classe inteira de erros por campos fora de ordem.
  • static_assert: transforma premissas sobre layout de memória, que antes viviam em comentários, em verificações que quebram a compilação.
  • Atomics: a comunicação entre interrupções e código principal é o problema de concorrência mais comum em firmware, e o padrão oferece uma resposta portátil para ele.
  • alignas: buffers de DMA em microcontroladores com cache dependem disso para funcionar corretamente.
  • Literais binários, separadores numéricos e enums com tipo fixo: mudanças pequenas, mas que afetam diretamente o código de baixo nível escrito todos os dias.
  • [[nodiscard]], ckd_add e memset_explicit: recursos que ajudam a evitar erros silenciosos e vulnerabilidades, cada vez mais relevantes em dispositivos conectados.

Já utilizei static_assert para verificar o tamanho de arrays. Isso pode ser interessante para dar segurança aos tamanhos de buffers em módulos de leitura de sensores, por exemplo. Um caso clássico é garantir que uma tabela de configuração continue com uma entrada por sensor quando alguém acrescenta um sensor ao enum e esquece de atualizar a tabela:

static const sensor_cfg_t sensor_cfg[] = { /* ... */ };

static_assert(sizeof sensor_cfg / sizeof sensor_cfg[0] == SENSOR_COUNT,
              "sensor_cfg precisa ter uma entrada por sensor");

Já recursos como inferência de tipo com auto podem gerar opiniões divergentes: economizam digitação, mas escondem o tipo justamente em um domínio no qual a largura exata de cada variável importa.

E alguns recursos devem ser evitados: os VLAs são o principal exemplo.

Resumo

PadrãoAnoRecursos-chave para firmware
C991999stdint.h, inicializadores designados, literais compostos, flexible array members, macros variádicas, inline, restrict
C112011_Static_assert, _Alignas, structs e unions anônimas, atomics, _Generic, _Noreturn
C172018Apenas correções
C232024bool e nullptr, atributos, literais binários, enums com tipo fixo, constexpr, typeof, #embed, ckd_add, memset_explicit, stdbit.h

Conclusão

A evolução da linguagem C foi gradual, mas consistente.

Muitos recursos introduzidos ao longo dos últimos 25 anos melhoraram a confiabilidade e a legibilidade dos firmwares modernos. Para quem quiser se aprofundar no uso desses recursos, o livro Modern C [8], de Jens Gustedt, é uma boa leitura: ele ensina a linguagem já a partir dos padrões mais recentes, e não do C89.

Todos os exemplos deste artigo estão no repositório rdws_examples_articles [11], na pasta modern-c/01-c99-a-c23: um programa por recurso, separados por padrão, os casos que devem falhar na compilação e um script que testa tudo, inclusive a diferença dos atomics entre o Cortex-M0 e o Cortex-M4.

Mas uma pergunta permanece:

quantas dessas funcionalidades já estão realmente disponíveis no ecossistema embarcado atual?

É exatamente isso que veremos no próximo artigo.

Referências

Os padrões ISO são documentos pagos. As referências 2 a 4 são rascunhos publicados gratuitamente pelo comitê WG14, e não os documentos oficiais: servem bem para consultar o texto da linguagem, mas podem conter pequenas diferenças em relação à versão publicada pela ISO.

  1. DIAS, Rafael. Algumas particularidades sintáticas de C99. Embarcados, 2016.
  2. WG14. N1256: rascunho do C99 com as correções técnicas TC1, TC2 e TC3.
  3. WG14. N1570: rascunho final do C11.
  4. WG14. N3096: rascunho de trabalho do C23, de abril de 2023. É anterior às votações finais; o padrão publicado é a ISO/IEC 9899:2024.
  5. cppreference.com. História da linguagem C e resumo das novidades do C23.
  6. CORBET, Jonathan. Variable-length arrays and the max() mess. LWN.net, 2018.
  7. GCC. GCC 15 Release Series: Changes, New Features, and Fixes.
  8. GUSTEDT, Jens. Modern C. Livro com versão gratuita, escrito por um dos membros do WG14.
  9. MISRA. MISRA C:2012 – Guidelines for the use of the C language in critical systems. Regra 18.8.
  10. Zephyr Project. Atomic Services.
  11. DIAS, Rafael. rdws_examples_articles: exemplos deste artigo. GitHub.
  12. Arm. PSA Certified Crypto API 1.2: Design goals, seções 2.2 e 2.3.
  13. Trusted Firmware-M. Crypto Service design.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Inscrever-se
Notificar de
0 Comentários
mais recentes
mais antigos Mais votado
Home » Software » Linguagem C » 25 anos de evolução da linguagem C: de C99 a C23

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste:
Nenhum resultado encontrado.