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;
}
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;
| ^~~
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;
}
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)
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;
}
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
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;
}
gcc main.c -std=c99 -o main && ./main
Obrigado por reagir ao post!
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:
- PRI: significa que é um print do valor
- formato: formato para inteiros usado no
printfcomo d para inteiro com sinal e u para sem sinal ou x para hexadecimal. - tamanho: tamanhos dos tipos do
stdint.hcomo 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;
}
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
Temos uma macro para escanear valores do usuário. Essas macros segue o seguinte formato:
- SCN: significa que é um sca*n* do valor
- formato: formato para inteiros usado no
scanfcomo d para inteiro com sinal e u para sem sinal ou x para hexadecimal. - tamanho: tamanhos dos tipos do
stdint.hcomo 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;
}
gcc main.c -std=c99 -o main && ./main
Digite sua idade: 25
Voce tem 25 anos.
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;
}
gcc main.c -std=c99 -o main && ./main
formatada Linguagem C.
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;
}
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;
}
gcc main.c -std=c11 -o main && ./main
Valor final do contador: 1000
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)
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;
}
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
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;
}
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");
| ^~~~~~~~~~~~~~
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;
}
gcc main.c -std=c23 -o main && ./main
O valor e valido: 1
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;
}
gcc main.c -std=c23 -o main && ./main
NULL e um ponteiro do tipo void
nullptr e de um tipo desconhecido
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;
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;
}
gcc main.c -std=c23 -o main && ./main
User 1: Fulano, Age: 25
User 2: Ciclano, Age: 20
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;
}
gcc main.c -std=c23 -o main && ./main
1 2 3 4
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;
}
gcc main.c -std=c23 -o main && ./main
O valor do binario 0b101 e: 5
O valor do milhao e: 1000000
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;
}
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.
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
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
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
Top comments (0)