C Máscaras e Flags

Um único unsigned tem 32 bits, e cada bit pode ser um interruptor independente: luz da sala acesa, luz da cozinha apagada; pizza com queijo, sem cebola; arquivo que pode ser lido, mas não executado. Cada interruptor é uma flag (bandeira). Para mexer em um sem estragar os outros, usamos uma máscara: um número com só os bits que interessam acesos. Esta aula ensina as quatro receitas (ligar, desligar, inverter, testar) e mostra onde elas aparecem na vida real, como no chmod 755 do Linux.

Você vai aprender
  • O que é uma máscara e como criar uma com 1u << n.
  • As quatro receitas: ligar (x |= m), desligar (x &= ~m), inverter (x ^= m) e testar (x & m).
  • Dar nomes às flags com #define e com enum, e combiná-las com |.
  • Testar vários bits de uma vez: "todos" e "pelo menos um".
  • As permissões rwx do Linux, os números octais (0755) e um programa que mostra rwxr-xr-x.
  • Separar e juntar pedaços de um número (as cores RGB).
  • Os erros clássicos: == 1, + no lugar de |, esquecer o ~, macro sem parênteses.

Antes, leia Operadores de Bits: esta aula usa &, |, ^, ~ e << o tempo todo, e a função mostra_bits que você escreveu lá. 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 gcc -std=c23 -Wall -Wextra arquivo.c -o programa, e conferidos no editor do guia.


A ideia: o molde de pintura

Para pintar só uma letra numa parede, você usa um molde vazado (um estêncil): a tinta passa pelos buracos e o resto da parede fica protegido. Uma máscara de bits é esse molde. Os bits acesos (1) da máscara são os "buracos": só eles são mexidos. Os bits apagados (0) protegem o resto do número.

Quero…OperadorReceitaLembrete
Ligar o bit| (OU)x |= m;"acrescenta" os bits de m
Desligar o bit& e ~x &= ~m;"tudo, menos m"
Inverter o bit^ (XOR)x ^= m;interruptor de escada
Testar o bit& (E)if (x & m)filtro: sobra só o bit de m

1. A máscara de um bit: 1u << n

Você viu na aula anterior que 1u << n é um número com só o bit n aceso: 1u << 0 é 00000001, 1u << 3 é 00001000. Essa é a máscara de um bit. O u deixa o 1 sem sinal, para o deslocamento ser seguro até o bit 31.

A Ana tem 8 lâmpadas em casa, numeradas de 0 a 7, e guarda o estado de todas num único unsigned char, a variável luzes. Veja as quatro receitas em ação:

#include <stdio.h>

void mostra_bits(unsigned char x) {
    for (int i = 7; i >= 0; i--) {
        printf("%d", (x >> i) & 1);
    }
    printf("\n");
}

int main(void) {
    unsigned char luzes = 0;
    printf("Começo:          "); mostra_bits(luzes);
    luzes |= (1u << 0);
    printf("Liga a 0:        "); mostra_bits(luzes);
    luzes |= (1u << 3);
    printf("Liga a 3:        "); mostra_bits(luzes);
    luzes |= (1u << 3);
    printf("Liga a 3 de novo:"); mostra_bits(luzes);
    luzes &= ~(1u << 0);
    printf("Desliga a 0:     "); mostra_bits(luzes);
    luzes ^= (1u << 5);
    printf("Inverte a 5:     "); mostra_bits(luzes);
    luzes ^= (1u << 5);
    printf("Inverte a 5:     "); mostra_bits(luzes);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra luzes.c -o luzes
$ ./luzes
Começo:          00000000
Liga a 0:        00000001
Liga a 3:        00001001
Liga a 3 de novo:00001001 « ligar o que já está ligado não muda nada
Desliga a 0:     00001000
Inverte a 5:     00101000
Inverte a 5:     00001000 « inverter duas vezes volta ao começo

2. Ligar: x |= m

O | dá 1 onde qualquer um dos dois tem 1. Onde a máscara tem 0, o bit de x passa igual; onde a máscara tem 1, o resultado é 1, estivesse o bit aceso ou não:

  00000001     luzes
| 00001000     1u << 3
  --------
  00001001     só o bit 3 mudou

Por isso "ligar a 3 de novo" não mudou nada. Ligar é seguro: pode repetir sem medo.


3. Desligar: x &= ~m

Desligar precisa de dois passos, e é a receita que mais gente erra:

  1. ~(1u << 0) inverte a máscara: 00000001 vira 11111110. Agora todos os bits estão acesos, menos o que você quer desligar.
  2. luzes & 11111110: o & deixa passar todos os bits de luzes, menos o bit 0, que é forçado a 0.
  00001001     luzes
& 11111110     ~(1u << 0)
  --------
  00001000     só o bit 0 apagou

Se você esquecer o ~ e escrever luzes &= (1u << 0), o resultado é o contrário: o bit 0 fica como estava e todos os outros apagam. A missão abaixo pede para você desligar uma luz do jeito certo.

Missão: a Ana saiu da cozinha (luz 1) e quer apagar só essa luz. Complete a linha marcada usando &= e ~, sem mexer nas outras luzes. A segunda linha tem que ser depois: 00100101.

#include <stdio.h>

#define COZINHA (1u << 1)

void mostra_bits(unsigned char x) {
    for (int i = 7; i >= 0; i--) {
        printf("%d", (x >> i) & 1);
    }
    printf("\n");
}

int main(void) {
    unsigned char luzes = 0b00100111;
    printf("antes:  "); mostra_bits(luzes);
    // apague só a luz da cozinha aqui
    printf("depois: "); mostra_bits(luzes);
    return 0;
}
#include <stdio.h>

#define COZINHA (1u << 1)

void mostra_bits(unsigned char x) {
    for (int i = 7; i >= 0; i--) {
        printf("%d", (x >> i) & 1);
    }
    printf("\n");
}

int main(void) {
    unsigned char luzes = 0b00100111;
    printf("antes:  "); mostra_bits(luzes);
    luzes &= ~COZINHA;
    printf("depois: "); mostra_bits(luzes);
    return 0;
}

4. Inverter: x ^= m

O ^ inverte os bits onde a máscara tem 1 e deixa o resto igual. É o botão "liga/desliga" de um controle remoto: o mesmo botão acende se estava apagado e apaga se estava aceso. Na saída do luzes.c, inverter o bit 5 duas vezes voltou ao estado inicial.


5. Testar: x & m

O & com a máscara apaga todos os outros bits. Se o bit testado estava aceso, sobra um número diferente de zero (verdadeiro no if); se estava apagado, sobra 0 (falso):

#include <stdio.h>

int main(void) {
    unsigned luzes = 0b00001010;
    for (int n = 0; n < 4; n++) {
        if (luzes & (1u << n)) {
            printf("luz %d: acesa\n", n);
        } else {
            printf("luz %d: apagada\n", n);
        }
    }
    printf("luzes & (1u << 3) = %u\n", luzes & (1u << 3));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra testa.c -o testa
$ ./testa
luz 0: apagada
luz 1: acesa
luz 2: apagada
luz 3: acesa
luzes & (1u << 3) = 8     « não é 1: é o valor do bit 3

Repare na última linha: o teste do bit 3 dá 8, não 1. O resultado de x & m é o próprio bit, no lugar dele. Para o if tanto faz (qualquer número diferente de zero é verdadeiro), mas comparar com 1 dá errado:

#include <stdio.h>

int main(void) {
    unsigned luzes = 0b00001010;
    if ((luzes & (1u << 3)) == 1) {
        printf("luz 3 acesa\n");
    } else {
        printf("luz 3 apagada?!\n");
    }
    return 0;
}
$ gcc -std=c23 -Wall -Wextra igual1.c -o igual1
igual1.c: In function ‘main’:
igual1.c:5:29: warning: bitwise comparison always evaluates to false [-Wtautological-compare]
    5 |     if ((luzes & (1u << 3)) == 1) {
      |                             ^~
$ ./igual1
luz 3 apagada?!          « mas ela está acesa

Bitwise comparison always evaluates to false = "esta comparação de bits é sempre falsa": o gcc percebeu que algo & 8 só pode dar 0 ou 8, nunca 1. O Clang do editor não avisa neste caso: o programa simplesmente dá a resposta errada. Escreva if (x & m) ou if ((x & m) != 0). Se você precisa de exatamente 0 ou 1 (para mostrar ou somar), use (x >> n) & 1, como na mostra_bits.


6. Flags com nome

1u << 3 não diz nada a quem lê o código. Na vida real, cada bit ganha um nome. A pizzaria da Ana guarda os ingredientes de um pedido num unsigned:

#include <stdio.h>

#define QUEIJO     (1u << 0)
#define CALABRESA  (1u << 1)
#define CEBOLA     (1u << 2)
#define AZEITONA   (1u << 3)

void mostra_pedido(unsigned pedido) {
    printf("Pizza com:");
    if (pedido & QUEIJO)    printf(" queijo");
    if (pedido & CALABRESA) printf(" calabresa");
    if (pedido & CEBOLA)    printf(" cebola");
    if (pedido & AZEITONA)  printf(" azeitona");
    printf(" (código %u)\n", pedido);
}

int main(void) {
    unsigned pedido = QUEIJO | CEBOLA;
    mostra_pedido(pedido);
    pedido |= AZEITONA;
    mostra_pedido(pedido);
    pedido &= ~CEBOLA;
    mostra_pedido(pedido);
    return 0;
}
#define QUEIJO (1u << 0)
Dá um nome à máscara do bit 0 (aula Macros). Os parênteses em volta são importantes, como a seção "Quando dá errado" mostra.
QUEIJO | CEBOLA
Combina duas flags: bits 0 e 2 acesos (0101 = 5). Leia o | como "e também".
void mostra_pedido(unsigned pedido)
Uma função que recebe várias opções num único parâmetro. É o grande uso das flags: em vez de quatro parâmetros bool, um número.
pedido |= AZEITONA;
Acrescenta azeitona, sem mexer no resto.
pedido &= ~CEBOLA;
Tira a cebola, sem mexer no resto.
$ gcc -std=c23 -Wall -Wextra pizza.c -o pizza
$ ./pizza
Pizza com: queijo cebola (código 5)
Pizza com: queijo cebola azeitona (código 13)
Pizza com: queijo azeitona (código 9)
Na vida real

A biblioteca do sistema usa flags assim o tempo todo. A função open do Linux, que está por trás do fopen, recebe o modo como flags combinadas: open("dados.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644) quer dizer "só escrita, e também crie se não existir, e também apague o conteúdo antigo". O último argumento, 0644, são as permissões da seção 9.

Experimente: crie uma flag MILHO no bit 4, peça uma pizza com calabresa, cebola e milho, e depois troque a cebola por azeitona. Qual é o código do pedido no final?

#include <stdio.h>

#define QUEIJO     (1u << 0)
#define CALABRESA  (1u << 1)
#define CEBOLA     (1u << 2)
#define AZEITONA   (1u << 3)

void mostra_pedido(unsigned pedido) {
    printf("Pizza com:");
    if (pedido & QUEIJO)    printf(" queijo");
    if (pedido & CALABRESA) printf(" calabresa");
    if (pedido & CEBOLA)    printf(" cebola");
    if (pedido & AZEITONA)  printf(" azeitona");
    printf(" (código %u)\n", pedido);
}

int main(void) {
    unsigned pedido = QUEIJO | CEBOLA;
    mostra_pedido(pedido);
    return 0;
}

Flags com enum

Outra forma comum é um enum (aula Enums), que agrupa as flags debaixo de um nome de tipo. Os valores precisam ser potências de 2, escritas à mão:

#include <stdio.h>

enum Opcao {
    OPC_NEGRITO    = 1 << 0,
    OPC_ITALICO    = 1 << 1,
    OPC_SUBLINHADO = 1 << 2,
};

int main(void) {
    unsigned estilo = OPC_NEGRITO | OPC_SUBLINHADO;
    printf("estilo = %u\n", estilo);
    if (estilo & OPC_ITALICO) printf("itálico\n");
    if (estilo & OPC_SUBLINHADO) printf("sublinhado\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra enumflags.c -o enumflags
$ ./enumflags
estilo = 5
sublinhado
  • Sem o = 1 << n, o enum numeraria 0, 1, 2, 3… e 1 | 2 daria 3, que seria confundido com a quarta opção. Flags precisam ser 1, 2, 4, 8, 16…
  • A combinação OPC_NEGRITO | OPC_SUBLINHADO (5) não é nenhum dos valores do enum. Por isso a variável que guarda a combinação é unsigned, e não enum Opcao.
  • C23 Dá para escolher o tipo por trás do enum: enum Opcao : uint8_t { … };. No laboratório e no editor, sizeof de uma variável desse enum deu 1.

7. Vários bits de uma vez

Uma máscara pode ter vários bits acesos. Aí surgem duas perguntas diferentes: "estão todos acesos?" e "algum está aceso?":

#include <stdio.h>

#define LER       (1u << 2)
#define ESCREVER  (1u << 1)
#define EXECUTAR  (1u << 0)

int main(void) {
    unsigned dono = LER | ESCREVER;
    unsigned precisa = LER | ESCREVER;
    unsigned qualquer = ESCREVER | EXECUTAR;
    if ((dono & precisa) == precisa) printf("tem LER e ESCREVER\n");
    if ((dono & qualquer) != 0) printf("tem ESCREVER ou EXECUTAR\n");
    if ((dono & (ESCREVER | EXECUTAR)) == (ESCREVER | EXECUTAR)) printf("tem os dois\n");
    else printf("não tem ESCREVER e EXECUTAR juntos\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra varios.c -o varios
$ ./varios
tem LER e ESCREVER
tem ESCREVER ou EXECUTAR
não tem ESCREVER e EXECUTAR juntos
PerguntaReceita
Todos os bits de m estão acesos?(x & m) == m
Pelo menos um bit de m está aceso?(x & m) != 0
Nenhum bit de m está aceso?(x & m) == 0

Os parênteses em volta de x & m são obrigatórios: sem eles, o == é feito antes do & (veja "Quando dá errado").


8. Números octais: o zero que muda tudo

Antes das permissões, um detalhe do C que pega muita gente. Um número que começa com 0 é lido em octal (base 8, dígitos de 0 a 7):

#include <stdio.h>

int main(void) {
    int a = 755;
    int b = 0755;
    int c = 010;
    printf("755 = %d, 0755 = %d, 010 = %d\n", a, b, c);
    printf("0755 em octal: %o\n", b);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra octal.c -o octal
$ ./octal
755 = 755, 0755 = 493, 010 = 8
0755 em octal: 755

0755 é 7×64 + 5×8 + 5 = 493, e 010 é 8, não 10. Nenhum compilador avisa, porque é C válido. Nunca escreva zeros à esquerda "para alinhar" números decimais. O %o do printf mostra um número em octal. E por que alguém usaria octal? Porque cada dígito octal são exatamente 3 bits, e as permissões do Linux vêm em grupos de 3.


9. Na vida real: as permissões rwx e o chmod 755

Todo arquivo no Linux tem 9 bits de permissão, em três grupos de três: o que o dono pode fazer, o que o grupo pode fazer e o que os outros podem fazer. Em cada grupo: r (read, ler) vale 4, w (write, escrever) vale 2, x (execute, executar) vale 1. Veja no laboratório da Ana:

$ echo 'echo Olá, Ana' > ola.sh
$ ls -l ola.sh
-rw-rw-r-- 1 ana ana 15 out 17 10:45 ola.sh
$ ./ola.sh
bash: ./ola.sh: Permissão negada        « ninguém tem o x
$ chmod 755 ola.sh
$ ls -l ola.sh
-rwxr-xr-x 1 ana ana 15 out 17 10:45 ola.sh
$ ./ola.sh
Olá, Ana
$ stat -c "%a %A" ola.sh
755 -rwxr-xr-x

O chmod 755 ("change mode", mudar o modo) recebe as permissões em octal, um dígito por grupo:

dono: 7grupo: 5outros: 5
Bit876543210
Letrarwxrwxrwx
755111101101
Mostrarwxr-xr-x

7 = 4 + 2 + 1 (rwx), 5 = 4 + 1 (r-x). É exatamente o que o ls -l mostra depois do primeiro caractere (que diz se é arquivo - ou pasta d). Agora, o mesmo em C: um programa que recebe o modo e mostra as letras, testando cada um dos 9 bits com uma máscara:

#include <stdio.h>

void mostra_permissoes(unsigned modo) {
    const char letras[] = "rwx";
    for (int i = 8; i >= 0; i--) {
        if (modo & (1u << i)) {
            printf("%c", letras[(8 - i) % 3]);
        } else {
            printf("-");
        }
    }
    printf("  (%o)\n", modo);
}

int main(void) {
    mostra_permissoes(0755);
    mostra_permissoes(0644);
    mostra_permissoes(0600);
    return 0;
}
mostra_permissoes(0755)
O 0 na frente faz o C ler em octal, como o chmod. Sem ele, 755 seria outro número.
for (int i = 8; i >= 0; i--)
Visita os 9 bits, do 8 (o r do dono) até o 0 (o x dos outros).
modo & (1u << i)
Testa o bit i.
letras[(8 - i) % 3]
Escolhe a letra: o bit 8 dá (8 - 8) % 3 = 0 (r), o bit 7 dá 1 (w), o bit 6 dá 2 (x), e o padrão se repete a cada grupo de três.
$ gcc -std=c23 -Wall -Wextra permissoes.c -o permissoes
$ ./permissoes
rwxr-xr-x  (755)
rw-r--r--  (644)
rw-------  (600)

A mesma saída do ls -l. O 644 é o padrão para documentos (dono lê e escreve; os outros só leem); o 600 é para arquivos secretos, como chaves (só o dono).

Outra máscara de verdade: o umask

Por que o ola.sh nasceu rw-rw-r-- (664)? Programas pedem 666 ao criar um arquivo, e o sistema desliga os bits da "máscara de criação", o umask. No laboratório, o comando umask respondeu 0002: 0666 & ~0002 = 0664. É a receita "desligar" da seção 3, feita pelo próprio Linux.


10. Pedaços de um número: as cores RGB

Máscaras também servem para guardar vários números pequenos dentro de um grande, com posições que você controla. Uma cor de tela é 0xRRGGBB: 8 bits de vermelho, 8 de verde, 8 de azul. Para separar, desloque e corte com & 0xFF (8 bits acesos); para juntar, desloque cada pedaço para o lugar e combine com |:

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

int main(void) {
    uint32_t cor = 0xFF8000;
    unsigned r = (cor >> 16) & 0xFF;
    unsigned g = (cor >> 8) & 0xFF;
    unsigned b = cor & 0xFF;
    printf("cor 0x%06X: R=%u G=%u B=%u\n", (unsigned)cor, r, g, b);
    uint32_t nova = ((uint32_t)30 << 16) | ((uint32_t)144 << 8) | 255;
    printf("R=30 G=144 B=255 -> 0x%06X\n", (unsigned)nova);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra rgb.c -o rgb
$ ./rgb
cor 0xFF8000: R=255 G=128 B=0
R=30 G=144 B=255 -> 0x1E90FF

0xFF8000 é laranja, e 0x1E90FF é um azul (o "dodger blue" das páginas web). Esse é o jeito portátil de fazer o que os campos de bits fazem: aqui você decide a posição exata de cada pedaço, igual em qualquer compilador, e pode gravar o número num arquivo ou mandar pela rede.


11. Qual usar

FormaExemploQuando usar
#define com parênteses#define QUEIJO (1u << 0)O mais comum em C; é como a biblioteca do sistema faz (O_CREAT)
enum com potências de 2OPC_NEGRITO = 1 << 0,Quando você quer as flags agrupadas e com nome de tipo (o depurador mostra os nomes)
constexpr C23constexpr unsigned QUEIJO = 1u << 0;Constante com tipo, sem macro (aula Constantes)
Campos de bitsunsigned queijo : 1;Quando cada flag é lida e escrita separada e os dados ficam dentro do programa
Vários boolbool queijo;Poucas flags e memória sobrando: o mais simples de ler

Para flags, prefira um tipo sem sinal de tamanho conhecido: unsigned (32 flags) ou uint8_t, uint32_t, uint64_t (aula Inteiros de Tamanho Fixo).


Quando dá errado

warning: bitwise comparison always evaluates to false [-Wtautological-compare]

O que significa: você comparou x & m com 1 (ou outro valor que ele nunca pode ter). x & (1u << 3) dá 0 ou 8, nunca 1. O Clang do editor não avisa: o programa só erra.

Como consertar: teste com if (x & m) ou (x & m) != 0; para todos os bits de uma máscara, (x & m) == m.

warning: suggest parentheses around comparison in operand of ‘&’ (e self-comparison always evaluates to true)

O que significa: você escreveu dono & precisa == precisa. O == vem primeiro: precisa == precisa é sempre 1, e o teste vira dono & 1. No laboratório, com dono = LER, o programa respondeu não pode por um motivo totalmente diferente do certo.

Como consertar: (dono & precisa) == precisa.

Somei flags com + e apareceu outra opção

O que significa: + e | dão o mesmo resultado só se as flags forem diferentes. Com a mesma flag duas vezes, o + "vai um" para o bit vizinho. No laboratório: QUEIJO | QUEIJO = 1, mas QUEIJO + QUEIJO = 2, que é a CALABRESA.

Como consertar: combine flags sempre com |; tire com & ~, nunca com -.

Desliguei um bit e todos os outros apagaram

O que significa: faltou o ~: x &= m mantém só o bit de m.

Como consertar: x &= ~m;, "tudo menos m".

A máscara da macro sai com o valor errado

O que significa: a macro não tem parênteses. Com #define ESCREVER 1u << 1, a expressão ~ESCREVER vira ~1u << 1, que o C lê como (~1u) << 1. No laboratório, deu 4294967292 em vez de 4294967293: o bit 0 também ficou apagado. Nenhum aviso.

Como consertar: sempre #define ESCREVER (1u << 1), com parênteses em volta de tudo.

chmod deu permissões estranhas ou um número saiu diferente

O que significa: confusão entre decimal e octal. Em C, 0755 é octal (493) e 755 é decimal; um zero à esquerda "para alinhar" muda o número.

Como consertar: use o 0 só quando quiser octal (permissões). Para mostrar em octal, %o.


Referência rápida

ReceitaO que faz
1u << nMáscara com só o bit n aceso
x |= m;Liga os bits de m
x &= ~m;Desliga os bits de m
x ^= m;Inverte os bits de m
if (x & m)Algum bit de m está aceso (para um bit só: "o bit está aceso")
(x & m) == mTodos os bits de m estão acesos
(x & m) == 0Nenhum bit de m está aceso
(x >> n) & 1O bit n, como 0 ou 1
(x >> k) & 0xFFOs 8 bits que começam na posição k
(a << 16) | (b << 8) | cJunta pedaços num número
#define F (1u << n)Flag com nome (sempre com parênteses)
A | BCombina flags
0755, %oNúmero octal (cada dígito = 3 bits); mostrar em octal

Exercícios

1. Ligar

Complete para ligar a flag AZEITONA no pedido:

pedido AZEITONA;

2. Desligar

Complete para tirar a cebola do pedido:

pedido &= ;

3. Octal

Quais são as permissões, em octal, de um arquivo rw-r----- (dono lê e escreve, grupo só lê, outros nada)?

chmod arquivo

4. Com unsigned x = 0b1000;, quanto vale x & (1u << 3)?

5. Qual linha testa se as duas flags LER e ESCREVER estão ligadas em p?

6. Por que as flags de um enum precisam valer 1, 2, 4, 8…?

Missão final: mostrar as permissões

Missão: escreva o corpo da função mostra_permissoes. Para cada bit de 8 até 0, teste o bit com modo & (1u << i): se estiver aceso, mostre a letra (r, w ou x, repetindo nessa ordem); se não, mostre -. Termine com \n. A saída tem que ser:

rwxr-xr-x
rw-r--r--
rw-------
#include <stdio.h>

void mostra_permissoes(unsigned modo) {
    const char letras[] = "rwx";
    // para i de 8 até 0: teste o bit i de modo
    // aceso: mostre letras[(8 - i) % 3]; apagado: mostre '-'
}

int main(void) {
    mostra_permissoes(0755);
    mostra_permissoes(0644);
    mostra_permissoes(0600);
    return 0;
}
#include <stdio.h>

void mostra_permissoes(unsigned modo) {
    const char letras[] = "rwx";
    for (int i = 8; i >= 0; i--) {
        if (modo & (1u << i)) {
            printf("%c", letras[(8 - i) % 3]);
        } else {
            printf("-");
        }
    }
    printf("\n");
}

int main(void) {
    mostra_permissoes(0755);
    mostra_permissoes(0644);
    mostra_permissoes(0600);
    return 0;
}

Quer ir além? Teste também 0777 (todo mundo pode tudo, o que quase nunca é uma boa ideia) e 0700.


Resumo

  • Uma flag é um bit que guarda sim/não; uma máscara é um número com só os bits que interessam acesos, como 1u << n.
  • Ligar: x |= m. Desligar: x &= ~m. Inverter: x ^= m. Testar: if (x & m), que dá o valor do bit (8, não 1).
  • Dê nomes às flags com #define F (1u << n) (com parênteses!) ou com um enum de potências de 2, e combine com |, nunca com +.
  • Vários bits: (x & m) == m (todos), (x & m) != 0 (algum). Sempre com parênteses em volta do &.
  • Um número que começa com 0 é octal: 0755 = 493. Cada dígito octal são 3 bits, como os grupos rwx do chmod.
  • Deslocamento e & 0xFF separam pedaços de um número (RGB); << e | juntam. É a forma portátil de fazer o que os campos de bits fazem.