Post

Infdev #05 - Ponteiros em C.

Infdev #05 - Ponteiros em C.

No Infdev #03 – Memória RAM e Endereços, falamos sobre como a memória funciona. Agora, vamos entender como C lida com ponteiros (e não C++ neste caso, pois C++ possui diferentes formas de trabalhar com ponteiros), e sobre alguns fatores importantes que costumam ser ignorados por materiais menos aprofundados.

Aviso: não irei falar sobre o uso de ponteiros para alocar memória na heap, pois o próximo post será justamente sobre stack vs heap, e acredito que entender stack vs heap fica mais fácil após compreender o uso de ponteiros. Recomendação: leia o Infdev #04 – Tipos em C e como eles funcionam. Não explicarei questões como tamanho de tipos.

O que é um ponteiros?

Um ponteiro em C é uma variavel/constante que em vez de armazenar um valor, armazena um endereço de memoria. Por exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main() {
    int foo = 10;
    int *ponteiro = &foo; 
    // & indica que queremos o endereço de memoria de foo
    // * indica ao compilador que a variavel guarda um endereço de memoria

    printf("               Valor guardado em `foo`: %d\n", foo);       // 10
    printf("     Endereço de memoria de para `foo`: %p\n", &foo);      // 0xff1c...
    printf("          Valor guardado em `ponteiro`: %p\n", ponteiro);  // 0xff1c...
    printf("Endereço de memoria de para `ponteiro`: %p\n", &ponteiro); // 0xff2c...

    return 0;
}

Este exemplo, cria um valor na memoria nomeado foo, com valor de 10. E em seguida criamos um ponteiro que armazena o endereço de memoria de foo. Explicando melhor a sintaxe da criação de ponteiros: int *ponteiro = &foo pode parecer complicado, porem tal como int foo cria um valor na memoria que armazena um inteiro, int *ponteiro cria um valor na memoria que armazena um endereço de memoria de um inteiro. E sim, como o ponteiro é armazenado em uma variavel como qualquer outra, ele também possui um endereço de memoria que pode ser armazenado em um ponteiro que aponta para um ponteiro caso você queira.

1
2
3
4
5
6
7
int main() {
    int       foo = 10;
    int       *p1 = &foo;
    int      **p2 = &p1;
    int     ***p3 = &p2;
    int    ****p4 = &p3;
    ...

Existem varios casos onde o uso de ponteiros que apontam para outros ponteiros é util, para lidar com matrizes ou listas de strings, mas isso fica para outros posts.

Algo importante a se notar sobre a declaração de ponteiros, é que o indicador de ponteiro * deve ficar junto ao nome da variavel, e não ao tipo.

1
2
3
4
5
6
int main() {
    int valor = 10;
    
    int *foo = &valor; // ✅
    int* bar = &valor; // ❌
}

A sintaxe não está errada, e vai compilar normalmente caso você coloque o * junto ao tipo, porém é importante manter o * junto ao nome, para ficar claro ao leitor que foo é um ponteiro, e isso pode confundir, em casos de declaração de múltiplos ponteiros:

1
2
3
4
int main() {
    int *foo1, *foo2, *foo3, ...; // ✅
    int*  bar,  bar2,  bar3, ...; // ❌
}

int* pode dar a entender que todos os bars são ponteiros para inteiros, sendo que apenas o primeiro é um ponteiro, e os outros são apenas inteiros normais, e não um ponteiro para um inteiro.

Referência e Dereferência (Reference and Dereference)

Você já percebeu o uso de 2 símbolos específicos, & e *. Esses símbolos possuem usos específicos e “opostos” um ao outro em relação a ponteiros.

& Operador de Referência(Reference):

1
2
3
4
5
6
int main() {
    int valor = 10;
    int *ponteiro = &x;

    printf("Endereço de memoria de valor: %p\n", ponteiro); // 0xffab...
}

& é o operador utilizado para indicar que queremos o endereço de memoria de uma variavel/constante, e não o conteudo que a variavel/constante guarda.

* Operador de Dereferência(Dereference):

1
2
3
4
5
6
7
int main() {
    int valor = 10;
    int *p1 = &valor;

    int valor_2 = *p1 + 20;
    printf("Valor 2: %d\n", valor_2); // 30
}

\* é o operador que é utilizado para indicar que um valor é um ponteiro, porém ele é um dos poucos operadores em C que possui mais de um uso. No caso, ao ser utilizado na declaração de um valor int *p1 ela indica ao compilador que p1 possui um endereço de memoria para um inteiro. Porém como explicado no Infdev #03 o endereço de memoria é um inteiro como qualquer outro, porém este inteiro representa um endereço, e caso você tente fazer int valor_2 = p1 + 20, o seu codigo não vai compilar, pois ponteiro é um endereço de memoria para um inteiro, e não um inteiro normal, para acessar o valor que esta no endereço de memoria guardado em p1, usamos o operador de Dereferência *. Utilizar * antes de qualquer ponteiro, indica que queremos o valor que esta guardado no endereço de memoria do ponteiro, e não o endereço de memoria do ponteiro.

Existem outras formas de dereferenciar um ponteiro, como por exemplo, acessar o index de um array com [index]. Sempre que você faz lista[3], por de baixo dos panos, esta acontecendo uma dereferenciação, isso sera mais explicado em breve.

Uso

O principal uso de ponteiros, é permitir acesso a um valor sem precisar mover ou duplicar o valor, como por exemplo, para lidar com listas grandes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>

int sum(int *x, int len) {
    int total = 0;
    for (int i = 0; i < len; i++)
        total += x[i];
    return total;
}

void square(int *x, int len) {
    for (int i = 0; i < len; i++)
        x[i] = x[i] * x[i];
}

void print_lista(int *x, int len) {
    printf("Lista: ");
    for (int i = 0; i < len; i++)
        printf("%d ", x[i]);
    printf("\n");
}

int main() {
    int lista[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    // sizeof(lista) indica o tamanho da lista em bytes 
    // sizeof(int) diz o tamanho de um int em bytes
    int len = sizeof(lista)/sizeof(int); 
     
    // &lista[] indica que quermos o endereço de memoria do primeiro 
    // index da lista. e não o valor guardado  no primeiro index
    int total = sum(&lista[0], len); 

    print_lista(&lista[0], len);     // Lista: 1 2 3 4 5 6 7 8 9 10
    printf("Total = %d\n", total);   // Total = 55
    
    square(&lista[0], len);

    int total_quadrado = sum(&lista[0], len);
    print_lista(&lista[0], len);            // Lista: 1 4 9 16 25 36 49 64 81 100 
    printf("Total = %d\n", total_quadrado); // Total = 385
    return 0;
}

Neste exemplo, temos uma lista de inteiros com varios valores, e queremos fazer coisas com ela, printar todos os elementos, somar os elementos, operar sobre os elementos… Nessas operações, o uso de ponteiros serve principalmente para economizar memoria e CPU, pois caso não fosse utilizado ponteiros, sempre que uma função fosse chamada, uma nova copia do array seria criada, e seria passada para a função como parametro. E especialmente no caso de operar sobre os elementos da lista, o ponteiro é utilizado para acessar o valor original, e não uma copia, pois queremos editar a lista original, e não gerar uma nova. Com ponteiros é possivel, ao utilizar ponteiros, passar o ponteiro do array para uma função, teriamos acesso aos valores originais sem precisar realizar copias.

Agora, algo que pode estar confuso, é a sintaxe de passar o ponteiro do primeiro elemento, em vez da lista, e as funções recebem um ponteiro de um inteiro, e não de uma lista de inteiros, e é agora que vamos falar do principal aspecto de ponteiros que costuma ser ignorado, ou mal explicado, aritmetica de ponteiros.

Aritmética de ponteiro

Você já se perguntou o porque de indexes de arrays começarem em 0 e não em 1? Isso é por causa de como arrays são armazenados na memoria e pela Aritmética de ponteiros. Vamos lembrar do que foi dito no Infdev #03, a memoria é um grande array, e os ponteiros são endereços de memoria que servem de index para este array. O endereço de memoria é apenas um numero, e apenas isso, um numero muito grande em hexadecimal, mas um numero como qualquer outro, e podemos realizar somas e subtrações (as outras operações são possiveis, mas não são uteis). Quando um conjunto de valores são alocados na memoria, seja um array ou uma estrutura, todos os valores deste conjunto serão alocados sequencialmente, um seguido do outro, sem espaços vazios entre os elementos.

1
2
3
4
5
6
7
int main() {
    ...
    int lista[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    ...
    
    return 0;
}

Quando a lista foo é alocada na memoria, os 10 elementos seram colocados na memoria de forma sequencial, por exemplo, o primeiro elemento sera alocado em 0xFF00, como o tipo int em C possui 4 bytes, o primeiro elemento ocupara do endereço 0xFF00 ao 0xFF03, o segundo elemento vai começar no endereço 0xFF04 e vai ate o 0xFF07, o terceiro no endereço 0xFF08 ate 0xFF0C, e assim por diante.

1
2
3
4
5
6
7
8
9
10
11
12
void square(int *x, int len) {
    for (int i = 0; i < len; i++)
        x[i] = x[i] * x[i];
}

int main() {

    int lista[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    square(&lista[0], len);

    return 0;
}

A sintaxe de &lista[0] é um pouco confusa sem o contexto. Se você utilizar &lista, em vez de &lista[0], vai funcionar, porem o compilador vai te dar um warning, pois a função square espera receber um ponteiro para um int, e &lista é um ponteiro para int[10]. Ao passar um ponteiro para o primeiro valor da lista, junto ao tamanho da lista, nos sabemos quantos valores a lista tem, e podemos ir somando o endereço do primeiro elemento de 4 em 4 a cada etapa para acessar os proximos elementos.

Algo que precisa ser explicado, e que não vejo ter a atenção necessaria é a sintaxe de acesso a elementos de um array. A sintaxe de lista[index], so funciona se soubermos o tipo dos valores armazenados na lista, pois sabendo o tipo, sabemos o tamanho de cada elemento do array, e a forma com que o index é acessado é somando ao primeiro endereço de memoria de lista o index desejado vezes o tamanho. Por exemplo:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
    int lista[] = {1, 2, 3, 4};
    lista[2] = 50; 
    printf("%d\n", lista[2]); // 50
    return 0;
}

Vamos supor que o primeiro elemento da lista está alocado do endereço 0xFF00, nós sabemos que é uma lista de int, e sabemos que um int possui 4 bytes, nós queremos o index 2 (terceiro elemento). Para acessar o index desejado, pegamos o endereço inicial do array, que é o endereço a qual o primeiro elemento começa, e somamos com 4 * index: 0xFF00 + (4 * 2) = 0xFF00 + 8 = 0xFF08. Logo, o index 2, está no endereço 0xFF08.

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
    int lista[] = {1, 2, 3, 4};
    *(&lista[0] + 2) = 50; //0xFF00 + (4 * 2)
    printf("%d\n", lista[2]); // 50
    return 0;
}

No exemplo eu apenas somei 2 ao index, pois o compilador sabe que é preciso fazer 2 * [tamanho do tipo], então podemos apenas dizer quantos valores queremos pular.

Esse comportamento gera algumas consequencias, a principal é a velocidade. Arrays são tão mais rapidos que qualquer outra estrutura de dados justamente porque podemos acessar qualquer valor dele diretamente, apenas realizando uma multiplicação e uma soma. E uma segunda consequencia, é o motivo de que o primeiro elemento de um array é o 0 e não o 1, pois o primeiro já esta no endereço inicial do array, logo não é preciso pular nenhum elemento. E também é por isso tudo, que ao acessar um index de um array, acontece uma dereferenciação, pois como dito anteriormente, para acessar o valor de um endereço de memoria, precisamos dereferenciar ele, e como explicado agora, um index de uma lista, é apenas o endereço de memoria inicial + algum valor.

Algo importante a se dar foco é a questão do tamanho do dado a qual o ponteiro aponta. No exemplo da lista, o ponteiro aponta para um int, porém um ponteiro pode apontar para literalmente qualquer coisa na memoria, independente do tipo, seja um valor primitivo como int ou float, seja uma estrutura com varios valores dentro, podemos ate ter ponteiros para funções. Um ponteiro sempre tera o mesmo tamanho, que é o tamanho da word do sistema, porém o valor apontado pode variar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct Context {
    int eip; // 4 bytes        
    int esp; // 4 bytes       
    int ebx; // 4 bytes       
    int ecx; // 4 bytes       
    int edx; // 4 bytes       
    int esi; // 4 bytes       
    int edi; // 4 bytes       
    int ebp; // 4 bytes       
    int pc;  // 4 bytes   
} Context;   // 36 bytes

int main() {
    Context  c = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    Context *p = &p;
    
    printf("Tamanho de Context: %d\n", sizeof(c));              // 36
    printf("Tamanho de Ponteiro de Context: %d\n", sizeof(p));  // 8
    
    return 0;
}

Neste exemplo, a estrutura Context ocupa 36 bytes na memoria, porém o ponteiro ocupa apenas 8 como esperado. Caso queira criar uma lista de Context, basta criar tal como criamos uma lista de inteiros, o compilador ira se encarregar de realizar a aritmetica para acessar qualquer valor indexado.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct Context {
    int eip;
    ...
    int pc; 
} Context;  

int main() {
    Context contextos[] = { ... };
    ...

    contextos[3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

    ...
    return 0;
}

A grande magica dos ponteiros, é como o compilador consegue realizar tantas operações de forma automatica apenas sabendo o tamanho do tipo a qual o ponteiro aponta.

Ponteiros de funções

Assim como qualquer outra coisa no seu programa, funções estão carregadas na memoria, estando em algum lugar na memoria, podemos acessar com o endereço de memoria correto, portanto, podemos utilizar ponteiros para funções. A sintaxe de ponteiros para funções é um pouco diferente.

1
2
3
4
5
6
7
int soma(int x, int y) {
    return x + y;
}

int mult(int x, int y) {
    return x * y;
}

Ambas funções soma e mult recebem 2 inteiros e retornam um inteiro, por mais que elas possuam funções diferentes, elas possuem a mesma assinatura, e o que importa para um ponteiro de função, é a assinatura da função, os tipos dos parametros e o tipo de retorno. Para mostrar, vamos voltar ao exemplo anterior da lista.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>

int soma_20(int x) {
    return x + 20;
}

int quadrado(int x) {
    return x * x; 
}

void apply(int *x, int len, int (*fun)(int)) {
    for (int i = 0; i < len; i++)
        x[i] = fun(x[i]);
}

void print_lista(int *x, int len) {
    printf("Lista: ");
    for (int i = 0; i < len; i++)
        printf("%d ", x[i]);
    printf("\n");
}

int main() {
    int lista[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int len = sizeof(lista)/sizeof(int); 
     
    print_lista(&lista[0], len);      
    // Lista: 1 2 3 4 5 6 7 8 9 10
    
    apply(&lista[0], len, &quadrado);
    print_lista(&lista[0], len);      
    // Lista: 1 4 9 16 25 36 49 64 81 100 

    apply(&lista[0], len, &soma_20);
    print_lista(&lista[0], len);      
    // Lista: 21 24 29 36 45 56 69 84 101 120 

    return 0;
}

As funções, soma_20 e quadrado, ambas recebem um int e retornam um int, e a função apply recebe uma lista de inteiros, o tamanho da lista, e um ponteiro para uma função que recebe um inteiro e retorna um inteiro. Vamos dissecar a sintaxe do ponteiro para a função: int (*fun)(int).

1º int, o tipo de retorno

O primeiro int é o tipo do retorno da função a qual o ponteiro passado aponta. Você pode ter ponteiros para funções com qualquer tipo de funções:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *concatena(char *str1, char *str2) {
    int tamanho = snprintf(NULL, 0, "%s %d", str1, str2) + 1;
    char *resultado = malloc(tamanho);
    snprintf(resultado, tamanho, "%s %s", str1, str2);
    return resultado;
}

int main() {
    char *(*concat)(char*, char*) = &concatena;
    char *str = concat("Hello", "World");
    printf("%s\n", str); // Hello World
}

Neste exemplo, concatena retorna uma lista de chars (String). E o ponteiro para concatena indica isso.

2º (*fun), o nome do parametro

O primeiro parenteses contem o nome que daremos ao ponteiro da função, como qualquer parametro que uma função recebe, ele precisa de um nome, e o nome do parametro não precisa ser o mesmo do valor passado, e o * como explicado indica que o valor é um ponteiro, e igual qualquer outro valor, o nome da função passada não precisa ser o mesmo do parametro.

3º (int). o parametro

O segundo parenteses contem os parametros da função, no primeiro exemplo, a função recebe um inteiro, porém no segundo exemplo de concatena, a função recebe 2 strings.

Cuidado

Agora que temos explicado a sintaxe de um ponteiro para uma função, eu preciso deixar claro que ponteiro para funções podem ficar um pouco complicado, pois é possivel retornar ponteiros para funções, então é possivel ter um ponteiro para uma função que recebe um ponteiro para uma função e retorna outro ponteiro para uma função, é muito dificil você encontrar um cenario onde isso sera necessario, mas é possivel.

Para finalizar sobre ponteiro de funções, quero deixar claro que é possivel criar uma lista de funções, contando que todas as funções tenham a mesma assinatura. Isso pode ser util para lidar com pipelines de renderização por exemplo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int soma(int a, int b) { return a + b; }
int subtracao(int a, int b) { return a - b; }
int multiplicacao(int a, int b) { return a * b; }
int divisao(int a, int b) { return b != 0 ? a / b : 0; }

int main() {
    // Lista de ponteiros para funções que recebem 2 inteiros e retornam um inteiro
    int (*operacoes[4])(int, int) = {
        soma, subtracao, multiplicacao, divisao
    };
    
    char *nomes[] = {"Soma", "Subtração", "Multiplicação", "Divisão"};
    
    int x = 10;
    int y = 5;
    for (int i = 0; i < 4; i++) {
        int resultado = operacoes[i](x, y);
        printf("%s: %d %c %d = %d\n", nomes[i], x, 
               i == 0 ? '+' : i == 1 ? '-' : i == 2 ? '*' : '/', y, resultado);
    }
    return 0;
}

POO?

Talvez tenha vindo na sua cabeça utilizar isso para fazer algo como um objeto, usando estruturas com ponteiro de funções como metodos, e da pra fazer algo parecido:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct Pessoa {
    char *nome;
    int idade;
    char *(*toString)(struct Pessoa*);
    void (*free)(struct Pessoa*);
} Pessoa;

char *toStringPessoa(Pessoa *p) {
    int tamanho = snprintf(NULL, 0, "Nome: %s\nIdade: %d", p->nome, p->idade) + 1;
    
    char *resultado = malloc(tamanho);
    if (!resultado) return NULL;
    
    snprintf(resultado, tamanho, "Nome: %s\nIdade: %d", p->nome, p->idade);
    return resultado;
}

void freePessoa(Pessoa *p) {
    free(p->nome);
    free(p);
    p = NULL;
}

// Metodo construtor
Pessoa *newPessoa(char *nome, int idade) {
    Pessoa *p = malloc(sizeof(Pessoa)); 
    if (!p) return NULL;

    p->nome = malloc(strlen(nome) + 1);
    memcpy(p->nome, nome, strlen(nome) + 1);

    p->idade = idade;
    p->toString = toStringPessoa;
    p->free = freePessoa;

    return p;
}

int main() {
    Pessoa *p1 = newPessoa("Marceline", 23);
    if (!p1) return -1;

    printf("%s\n", p1->toString(p1));
    p1->free(p1);
    return 0;
}

Outras linguagens que são feitas para serem orientadas a objetos possuem sintaxe muito melhor, porém isso é apenas uma prova de conceito para como ponteiro para funções podem ser uteis, e como você pode ter um certo nivel de polimorfismo, pois você pode ter diferentes funções que se comportam de diferentes formas, porém possuem a mesma assinatura, e você pode utilizar elas tal como em um objeto, sem precisar lembrar qual o nome da versão especifica para esta estrutura. Eu pretendo fazer posts sobre POO e Funcional em C, não sobre como C permite programar de forma puramente funcional ou POO, pois não é possivel, mas eu sei que muitos programadores não tem vontade de migrar pra C, por achar que C é fechado no que ele é e que é impossivel fazer algo que não seja procedural de cima pra baixo, o que também não é verdade. C permite que você faça uma infinidade de coisas diferentes de formas diferentes apenas utilizando ponteiros da forma correta. Isso é um dos fatores a qual eu adoro em C, que é a liberdade para ser criativo na sua solução e organização.

Casting de ponteiros

Para finalizar a conversa sobre ponteiros, algo que eu tentei deixar claro ao longo do post, é como ponteiros não possuem tamanhos diferentes, todo ponteiro vai ter o mesmo tamanho. O tipo de um ponteiro, indica qual o tamanho na memoria do valor a qual ele aponta, um long *p é um ponteiro que aponta para um valor de 8 bytes, um short *p é um ponteiro que aponta para um valor de 2 bytes. No exemplo anterior do objeto Pessoa, Pessoa *p1 = newPessoa("Marceline", 23);, p1 é um ponteiro que aponta para um valor de 32 bytes, pois:

1
2
3
4
5
6
7
8
typedef struct Pessoa {
    char *nome;                         // Ponteiro = 8 bytes
    int idade;                          // Int = 4 bytes
    // 4 bytes de padding para alinhamento (explicarei em outro post)
    char *(*toString)(struct Pessoa*);  // Ponteiro = 8 bytes
    void (*free)(struct Pessoa*);       // Ponteiro = 8 bytes
} Pessoa; // 8 + 4 + 4 + 8 + 8 = 32 bytes                  

toString e free são ponteiros para funções, e não é possivel saber qual o tamanho de uma função de forma normal ou direta sem analisar o binario final ou algo do tipo.

Em C é possivel realizar a conversão (casting) de tipos de ponteiros, o uso disso parece bem menos obvio e não muito util, e de fato são poucos os cenarios que mudar o tipo do ponteiro ira ter um uso, onde realmente essa é a unica forma de resolver o problema. Um desses cenarios a qual casting de ponteiros é a unica solução, é o famoso algoritmo de raiz quadrada inversa de quake, que eu irei fazer um post falando sobre. Mas em resumo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Função muito resumida
float Q_rsqrt(float number) {
	float x2 = number * 0.5F;
	float y  = number;

    // converte o endereço de y (ponteiro para float), para um 
    // ponteiro para um long e dereferencia o valor e salva em i
    int i = *(int *)&y;
    
    // converte o endereço de i (ponteiro para long), para um 
    // ponteiro para um float e dereferencia o valor e salva em y
	y = *(float *)&i;

	return y;
}

Queremos ler o valor de y como um int, e usamos casting de ponteiros para ler o binario cru de y em vez de converter para int, pois ao converter um float para int, aconteceria um arredondamento. O que esta acontecendo é o seguinte:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
    float y = 3.135223;       
    // em binario 01000000.01001000.10100111.01111110
    
    printf("%d\n", (int)y); 
    // 3 em binario 00000000.00000000.00000000.00000011
    
    printf("%d\n", *(int)&y); 
    // 1078503294 em binario 01000000.01001000.10100111.01111110
    
    return 0;
}
This post is licensed under CC BY 4.0 by the author.

Trending Tags