Skip to content

Kiểu dữ liệu và biểu thức

Trang này trình bày các quy tắc về kiểu số nguyên trong C mà sách nhập môn thường chỉ nhắc qua, nhưng lại gây lỗi thật khi cùng một đoạn code chạy trên x86-64 và trên vi điều khiển Arm. Nội dung đi theo thứ tự: kích thước kiểu và dấu của char trên từng nền tảng, các kiểu có độ rộng cố định của <stdint.h>, quy tắc integer promotion và usual arithmetic conversion, chuyển đổi kiểu và cảnh báo của compiler, các bẫy về thứ tự ưu tiên và hằng số, cuối cùng là kiểu bool. Mỗi quy tắc đi kèm một chương trình nhỏ và output chạy thật.

Đọc trước trong K&R

Trang này không trình bày lại phần nền tảng đã có trong The C Programming Language (Kernighan và Ritchie, bản 2, gọi tắt là K&R): tên biến và khai báo (mục 2.1, 2.2, 2.4), toán tử số học, quan hệ và logic (2.5, 2.6), toán tử gán và biểu thức điều kiện (2.10, 2.11). Bảng thứ tự ưu tiên đầy đủ của toán tử nằm ở mục 2.12.

Môi trường dùng trong các ví dụ

Output được chạy thật trên ba môi trường:

  • x86-64: Ubuntu 24.04, GCC 13.3, gcc -std=c17 -Wall -Wextra.
  • Arm 32-bit chạy Linux: arm-linux-gnueabihf-gcc 13, chạy bằng qemu-arm (user mode).
  • Cortex-M3 không có hệ điều hành: arm-none-eabi-gcc 13.2, chạy trên QEMU 8.2 với board mps2-an385, printf đi ra qua semihosting (cơ chế để chương trình trên target dùng console của máy host thông qua debugger hoặc trình giả lập).
Dựng môi trường Cortex-M3 trên QEMU

Board mps2-an385 của QEMU mô phỏng một hệ Cortex-M3 có flash ở địa chỉ 0x00000000 và RAM ở 0x20000000. Chương trình cần một vector table, một hàm reset chép .data và xóa .bss, và một linker script đặt các section vào đúng vùng nhớ.

startup.c
#include <stdlib.h>
#include <string.h>

extern unsigned long _stack_top, __etext;
extern unsigned long __data_start__, __data_end__, __bss_start__, __bss_end__;
extern void initialise_monitor_handles(void);   /* semihosting của newlib */
extern int main(void);

void Reset_Handler(void)
{
    memcpy(&__data_start__, &__etext,
           (char *)&__data_end__ - (char *)&__data_start__);
    memset(&__bss_start__, 0, (char *)&__bss_end__ - (char *)&__bss_start__);
    initialise_monitor_handles();
    exit(main());
}

void Default_Handler(void) { for (;;) ; }
void _init(void) {}
void _fini(void) {}

__attribute__((section(".vectors"), used))
void (*const vectors[16])(void) = {
    (void (*)(void))&_stack_top, Reset_Handler,
    Default_Handler, Default_Handler, Default_Handler, Default_Handler, Default_Handler,
};
mps2.ld
MEMORY { FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 4M
         RAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 4M }
ENTRY(Reset_Handler)
SECTIONS {
  .text : { KEEP(*(.vectors)) *(.text*) *(.rodata*) KEEP(*(.init)) KEEP(*(.fini))
            . = ALIGN(4);
            __init_array_start = .; KEEP(*(.init_array*)) __init_array_end = .;
            __fini_array_start = .; KEEP(*(.fini_array*)) __fini_array_end = .; } > FLASH
  .ARM.exidx : { *(.ARM.exidx*) } > FLASH
  .eh_frame : { KEEP(*(.eh_frame)) } > FLASH
  __etext = .;
  .data : AT(__etext) { __data_start__ = .; *(.data*) . = ALIGN(4); __data_end__ = .; } > RAM
  .bss : { __bss_start__ = .; *(.bss*) *(COMMON) . = ALIGN(4); __bss_end__ = .; } > RAM
  end = .;
  _stack_top = ORIGIN(RAM) + LENGTH(RAM);
}
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -std=c17 -Wall -Wextra -O2 \
    --specs=rdimon.specs -nostartfiles -T mps2.ld startup.c sizes.c -o sizes.elf
qemu-system-arm -M mps2-an385 -nographic \
    -semihosting-config enable=on,target=native -kernel sizes.elf

Ý nghĩa của vector table, linker script và section được trình bày trong series C toolchain, cụ thể ở bài Object file và ELF và Linking.

Kích thước kiểu số nguyên

Chuẩn C chỉ quy định độ rộng tối thiểu của các kiểu số nguyên, không quy định độ rộng chính xác. Độ rộng thực tế do từng nền tảng chọn, và tổ hợp lựa chọn đó được gọi là data model.

Data model

Tên của data model ghi kiểu nào rộng 32 hoặc 64 bit: ILP32 nghĩa là int, long và pointer đều 32 bit; LP64 nghĩa là long và pointer 64 bit; LLP64 nghĩa là chỉ long long và pointer 64 bit. Chương trình dưới đây in kích thước các kiểu và giá trị của char khi gán 0xFF:

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

int main(void)
{
    printf("short=%u int=%u long=%u long long=%u pointer=%u\n",
           (unsigned)sizeof(short), (unsigned)sizeof(int), (unsigned)sizeof(long),
           (unsigned)sizeof(long long), (unsigned)sizeof(void *));

    char c = (char)0xFF;
    printf("(char)0xFF = %d\n", c);
    return 0;
}
== x86-64
short=2 int=4 long=8 long long=8 pointer=8
(char)0xFF = -1
== armhf (qemu-arm)
short=2 int=4 long=4 long long=8 pointer=4
(char)0xFF = 255
== cortex-m3
short=2 int=4 long=4 long long=8 pointer=4
(char)0xFF = 255
Kiểu Tối thiểu theo chuẩn x86-64 Linux (LP64) Arm 32-bit, Cortex-M (ILP32) Windows 64-bit (LLP64)
short 16 bit 16 16 16
int 16 bit 32 32 32
long 32 bit 64 32 32
long long 64 bit 64 64 64
pointer không quy định 64 32 64

Cột Windows lấy theo bảng data model của cppreference, không chạy thử trong trang này. Hệ quả thực tế của bảng là long không có độ rộng cố định giữa các nền tảng: code dùng long để chứa giá trị 64 bit chạy đúng trên x86-64 Linux nhưng bị cắt trên Arm 32-bit và trên Windows. Chuẩn còn cho phép int chỉ rộng 16 bit, và một số vi điều khiển 8 bit và 16 bit dùng đúng độ rộng này.

Khi build cho Cortex-M, printf của newlib đi kèm gói gcc-arm-none-eabi trên Ubuntu 24.04 in ra chữ zu thay cho giá trị nếu dùng định dạng %zu. Vì vậy các ví dụ trên trang này ép sizeof sang unsigned và in bằng %u. Một thư viện C cho embedded có thể không hỗ trợ đầy đủ định dạng của C99, nên cần kiểm tra trên đúng bản thư viện sẽ dùng.

Dấu của char

Chuẩn C có ba kiểu ký tự riêng biệt: char, signed char và unsigned char. Việc char có dấu hay không do implementation quyết định. Output ở trên cho thấy (char)0xFF bằng -1 trên x86-64 nhưng bằng 255 trên cả hai môi trường Arm, vì ABI của Arm (AAPCS) định nghĩa char là kiểu không dấu. GCC cho phép đổi mặc định bằng option -fsigned-char hoặc -funsigned-char.

Khác biệt này gây ra một lỗi kinh điển khi đọc input đến hết file. Hàm getchar trả về kiểu int để có thể biểu diễn mọi giá trị của unsigned char cộng thêm giá trị EOF (thường là -1). K&R mục 1.5.1 đã nêu lý do biến nhận kết quả phải là int. Đoạn code dưới đây dùng char:

eof.c
#include <stdio.h>

int main(void)
{
    char c;
    int n = 0;

    while ((c = getchar()) != EOF)
        n++;
    printf("%d ký tự\n", n);
    return 0;
}
== x86-64
3 ký tự
== armhf
eof.c:8:28: warning: comparison is always true due to limited range of data type [-Wtype-limits]
    8 |     while ((c = getchar()) != EOF)
      |                            ^~
(chương trình không dừng, bị timeout ngắt sau 3 giây)

Trên x86-64, char có dấu nên EOF gán vào c vẫn là -1 và vòng lặp dừng. Trên Arm, -1 gán vào char không dấu trở thành 255, sau đó được promotion lên int là 255, không bao giờ bằng -1, nên vòng lặp chạy mãi. Compiler cho Arm phát hiện được lỗi này và cảnh báo -Wtype-limits (bật cùng -Wextra), còn compiler cho x86-64 không cảnh báo vì trên nền tảng đó phép so sánh có thể đúng. Cách sửa là khai báo int c. Quy tắc chung rút ra từ ví dụ: char chỉ dùng cho ký tự văn bản, còn dữ liệu dạng byte dùng uint8_t hoặc unsigned char.

Kiểu có độ rộng cố định

Header <stdint.h> (từ C99) định nghĩa các kiểu số nguyên có độ rộng biết trước. K&R bản 2 ra đời năm 1988, trước C99, nên không có phần này.

Nhóm Ví dụ Ý nghĩa
Độ rộng chính xác int8_t, uint16_t, uint32_t, int64_t Rộng đúng N bit, không có bit đệm, số có dấu dùng bù hai. Chuẩn chỉ bắt buộc có khi nền tảng hỗ trợ kiểu như vậy.
Độ rộng tối thiểu int_least8_t, uint_least16_t Kiểu nhỏ nhất rộng ít nhất N bit. Luôn có.
Nhanh nhất int_fast8_t, uint_fast16_t Kiểu rộng ít nhất N bit mà nền tảng xử lý nhanh nhất. Luôn có.
Theo pointer intptr_t, uintptr_t Đủ rộng để chứa một pointer đã chuyển sang số nguyên.
Lớn nhất intmax_t, uintmax_t Kiểu số nguyên rộng nhất của nền tảng.

Hai kiểu liên quan nằm trong <stddef.h>: size_t là kiểu kết quả của sizeof, và ptrdiff_t là kiểu hiệu của hai pointer. Cả hai có độ rộng bằng độ rộng của pointer trên các nền tảng trong bảng ở mục trước.

Các tên trong <stdint.h> chỉ là typedef của kiểu cơ bản, và mỗi nền tảng chọn kiểu cơ bản khác nhau. Danh sách macro định nghĩa sẵn của compiler (gcc -dM -E - < /dev/null) cho thấy lựa chọn đó:

Macro x86-64 Linux armhf Linux arm-none-eabi (Cortex-M3)
__UINT32_TYPE__ unsigned int unsigned int long unsigned int
__INT_FAST16_TYPE__ long int (8 byte) int (4 byte) int (4 byte)
__CHAR_UNSIGNED__ không định nghĩa 1 1

Dòng đầu tiên là một bẫy khi in giá trị. Trên arm-none-eabi, uint32_t là unsigned long dù có cùng độ rộng 32 bit với unsigned int. Định dạng %u vì vậy không khớp kiểu:

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

int main(void)
{
    uint32_t counter = 4000000000u;
    printf("%u\n", counter);
    printf("%" PRIu32 "\n", counter);
    return 0;
}
== x86-64
(không cảnh báo)
== cortex-m3
fmt.c:8:14: warning: format '%u' expects argument of type 'unsigned int', but argument 2 has type 'uint32_t' {aka 'long unsigned int'} [-Wformat=]
    8 |     printf("%u\n", counter);
      |             ~^     ~~~~~~~

Hai kiểu có cùng độ rộng nên giá trị in ra vẫn đúng trên Cortex-M3, nhưng cảnh báo cho thấy code không portable: cùng một dòng code đúng kiểu trên nền tảng này và sai kiểu trên nền tảng khác. Header <inttypes.h> cung cấp macro định dạng cho từng kiểu, như PRIu32, PRId64, PRIx8, và các macro này được định nghĩa đúng cho từng nền tảng.

Integer promotion và usual arithmetic conversion

C không thực hiện phép tính số học trên kiểu hẹp hơn int. Trước khi tính, toán hạng được chuyển kiểu theo hai bước: integer promotion áp dụng cho từng toán hạng, sau đó usual arithmetic conversion đưa hai toán hạng về cùng một kiểu. K&R mục 2.7 có nhắc tới các quy tắc này, nhưng chỉ ở mức tóm tắt và chưa có các kiểu của <stdint.h>.

Quy tắc

Integer promotion (chuẩn C11, mục 6.3.1.1): một giá trị có kiểu hạng thấp hơn int, gồm char, short, các kiểu int8_t, uint16_t và bit-field, được chuyển thành int nếu int biểu diễn được mọi giá trị của kiểu gốc; nếu không thì được chuyển thành unsigned int.

Usual arithmetic conversion (mục 6.3.1.8) áp dụng sau promotion cho hai toán hạng của phép toán hai ngôi, theo thứ tự:

  1. Hai toán hạng cùng kiểu thì không chuyển nữa.
  2. Hai toán hạng cùng có dấu hoặc cùng không dấu thì kiểu có hạng thấp hơn chuyển sang kiểu có hạng cao hơn.
  3. Toán hạng không dấu có hạng lớn hơn hoặc bằng toán hạng có dấu thì toán hạng có dấu chuyển sang kiểu không dấu đó.
  4. Kiểu có dấu biểu diễn được mọi giá trị của kiểu không dấu thì toán hạng không dấu chuyển sang kiểu có dấu đó.
  5. Các trường hợp còn lại: cả hai chuyển sang phiên bản không dấu của kiểu có dấu.

Quy tắc 3 là nguồn gốc của phần lớn lỗi so sánh giữa số có dấu và số không dấu.

Ba hệ quả thường gặp

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

int main(void)
{
    uint8_t a = 200, b = 100;
    uint8_t sum8 = a + b;

    printf("a + b         = %d\n", a + b);
    printf("sizeof(a + b) = %u\n", (unsigned)sizeof(a + b));
    printf("sum8          = %u\n", sum8);

    uint8_t mask = 0x0F;
    printf("~mask == 0xF0 : %d\n", ~mask == 0xF0);
    printf("~mask         = 0x%X\n", ~mask);

    int i = -1;
    unsigned int u = 1;
    printf("-1 < 1u       : %d\n", i < u);
    return 0;
}
promo.c:14:42: warning: comparison of promoted bitwise complement of an unsigned value with constant [-Wsign-compare]
promo.c:19:38: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
a + b         = 300
sizeof(a + b) = 4
sum8          = 44
~mask == 0xF0 : 0
~mask         = 0xFFFFFFF0
-1 < 1u       : 0

Output trên x86-64 và trên Cortex-M3 giống hệt nhau. Ba hệ quả đọc được từ output:

  • Phép cộng hai uint8_t cho kết quả kiểu int. a + b bằng 300 và có kích thước 4 byte, vì cả hai toán hạng được promotion lên int trước khi cộng. Giá trị chỉ bị cắt khi gán vào biến uint8_t: 300 lấy modulo 256 còn 44.
  • Toán tử ~ đảo cả các bit cao sinh ra do promotion. mask được promotion thành int có giá trị 0x0000000F, nên ~mask là 0xFFFFFFF0 chứ không phải 0xF0. Phép so sánh cần viết (uint8_t)~mask == 0xF0.
  • So sánh int với unsigned int chuyển số âm thành số dương lớn. Theo quy tắc 3, -1 được chuyển thành unsigned int có giá trị UINT_MAX, nên -1 < 1u sai. Cả hai cảnh báo đều thuộc -Wsign-compare, bật cùng -Wextra khi biên dịch C.

Chi phí của kiểu hẹp trên core 32 bit

Promotion không chỉ ảnh hưởng tới kết quả mà còn tới code sinh ra. Hai hàm dưới đây chỉ khác kiểu dữ liệu:

add8.c
#include <stdint.h>

uint8_t add_u8(uint8_t a, uint8_t b)
{
    return a + b;
}

uint32_t add_u32(uint32_t a, uint32_t b)
{
    return a + b;
}
$ arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -O2 -c add8.c
$ arm-none-eabi-objdump -d --no-show-raw-insn add8.o
00000000 <add_u8>:
   0:   add r0, r1
   2:   uxtb    r0, r0
   4:   bx  lr

00000008 <add_u32>:
   8:   add r0, r1
   a:   bx  lr

Thanh ghi của Cortex-M3 rộng 32 bit, nên phép cộng luôn thực hiện trên 32 bit. Với uint8_t, compiler phải thêm lệnh uxtb (zero-extend byte) để cắt kết quả về 8 bit trước khi trả về. Vì vậy biến đếm và chỉ số vòng lặp trên core 32 bit nên dùng int, uint32_t hoặc các kiểu int_fastN_t. Kiểu hẹp phù hợp cho dữ liệu lưu trữ: phần tử của buffer, field của struct, giá trị ghi vào thanh ghi phần cứng.

Tràn số khi nhân hai uint16_t

Promotion có thể biến một phép tính trên kiểu không dấu thành phép tính có dấu. Hàm dưới đây nhân hai giá trị uint16_t và trả về uint32_t:

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

uint32_t mul16(uint16_t a, uint16_t b)
{
    return a * b;
}

int main(void)
{
    printf("%u\n", (unsigned)mul16(65535, 65535));
    return 0;
}
$ gcc -std=c17 -O0 -fsanitize=undefined mul16.c -o mul16 && ./mul16
mul16.c:6:14: runtime error: signed integer overflow: 65535 * 65535 cannot be represented in type 'int'
4294836225

Trên nền tảng có int 32 bit, int biểu diễn được mọi giá trị của uint16_t, nên a và b được promotion thành int (có dấu). Tích 4 294 836 225 lớn hơn INT_MAX (2 147 483 647), và tràn số có dấu là undefined behavior (hành vi không được chuẩn định nghĩa). Kết quả in ra trông đúng ở -O0, nhưng compiler được phép tối ưu dựa trên giả định rằng tràn số không xảy ra. UndefinedBehaviorSanitizer (-fsanitize=undefined) phát hiện lỗi lúc chạy. Cách sửa là ép một toán hạng sang kiểu đủ rộng trước khi nhân: return (uint32_t)a * b;.

Cùng đoạn code này không có undefined behavior trên nền tảng có int 16 bit, vì khi đó int không chứa được mọi giá trị của uint16_t và promotion cho ra unsigned int. Hành vi của một biểu thức trên kiểu hẹp vì vậy phụ thuộc vào độ rộng của int trên nền tảng đích.

Chuyển đổi kiểu và cảnh báo của compiler

Chuẩn C quy định kết quả chuyển đổi giữa các kiểu số nguyên như sau (mục 6.3.1.3):

Chuyển đổi Kết quả
Giá trị biểu diễn được trong kiểu đích Giữ nguyên giá trị
Sang kiểu không dấu, giá trị không biểu diễn được Lấy modulo 2^N, với N là độ rộng kiểu đích. Hành vi được định nghĩa.
Sang kiểu có dấu, giá trị không biểu diễn được Implementation-defined (do từng compiler định nghĩa và phải ghi trong tài liệu). GCC lấy modulo 2^N.

Cần phân biệt chuyển đổi kiểu với tràn số trong phép tính. Phép tính trên kiểu không dấu không bao giờ tràn theo nghĩa của chuẩn, vì kết quả luôn được lấy modulo. Phép tính trên kiểu có dấu cho kết quả ngoài phạm vi là undefined behavior, như ví dụ mul16 ở mục trước. Undefined behavior được trình bày riêng ở một trang khác trong series này.

Chuyển đổi thu hẹp (từ kiểu rộng sang kiểu hẹp) xảy ra ngầm khi gán, khi truyền tham số và khi trả về giá trị, và -Wall -Wextra không cảnh báo:

conv.c
#include <stdint.h>

void set_duty(uint8_t *reg, int percent)
{
    *reg = percent * 255 / 100;
}

uint16_t read_adc(int32_t raw)
{
    return raw >> 4;
}
$ gcc -std=c17 -Wall -Wextra -c conv.c
(không cảnh báo)
$ gcc -std=c17 -Wall -Wextra -Wconversion -c conv.c
conv.c:5:12: warning: conversion from 'int' to 'uint8_t' {aka 'unsigned char'} may change value [-Wconversion]
    5 |     *reg = percent * 255 / 100;
conv.c:10:16: warning: conversion from 'int32_t' {aka 'int'} to 'uint16_t' {aka 'short unsigned int'} may change value [-Wconversion]
   10 |     return raw >> 4;

Option -Wconversion cảnh báo mọi chuyển đổi ngầm có thể làm đổi giá trị. Với code đã có sẵn, option này thường sinh nhiều cảnh báo, nên cách áp dụng phổ biến là bật cho module mới hoặc cho code xử lý dữ liệu từ phần cứng. Khi chuyển đổi là có chủ ý, ép kiểu tường minh (*reg = (uint8_t)(percent * 255 / 100);) vừa tắt cảnh báo, vừa cho người đọc biết giá trị có thể bị cắt. Quy tắc 10.3 của MISRA C cũng cấm gán giá trị vào đối tượng có kiểu hẹp hơn hoặc khác loại mà không ép kiểu tường minh; xem trang Phân tích tĩnh mã nguồn.

Bẫy thứ tự ưu tiên và hằng số

Một số tổ hợp toán tử có thứ tự ưu tiên ngược với cách đọc tự nhiên, đặc biệt khi viết code thao tác bit. Hằng số nguyên cũng có quy tắc riêng về hệ cơ số và kiểu.

prec.c
#include <stdio.h>

#define READY 0x04

int main(void)
{
    unsigned status = 0x01;            /* bit READY chưa bật */
    unsigned perm = 010;               /* octal: bằng 8, không phải 10 */
    unsigned word = 1 << 4 + 1;        /* 1 << 5, không phải (1 << 4) + 1 */

    if (status & READY == 0)
        printf("chưa sẵn sàng\n");
    else
        printf("sẵn sàng?\n");

    printf("perm = %u, word = %u\n", perm, word);
    return 0;
}
prec.c:9:23: warning: suggest parentheses around '+' inside '<<' [-Wparentheses]
prec.c:11:16: warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]
sẵn sàng?
perm = 8, word = 32
  • & có độ ưu tiên thấp hơn ==. status & READY == 0 được hiểu là status & (READY == 0), tức status & 0, luôn bằng 0. Chương trình in "sẵn sàng?" dù bit READY chưa bật. Cần viết (status & READY) == 0. Quy tắc này cũng áp dụng cho | và ^.
  • << và >> có độ ưu tiên thấp hơn + và -. 1 << 4 + 1 bằng 1 << 5. Khi tính offset hoặc ghép field bit, mỗi phép dịch nên đặt trong ngoặc.
  • Hằng số bắt đầu bằng chữ số 0 là hằng octal. 010 bằng 8. Lỗi này thường xuất hiện khi căn thẳng cột một bảng số bằng cách thêm số 0 phía trước. Compiler không cảnh báo, vì đây là cú pháp hợp lệ.
  • Kiểu của hằng số phụ thuộc vào giá trị và hệ cơ số. Hằng thập phân không có hậu tố lấy kiểu đầu tiên chứa được giá trị trong dãy int, long, long long. Hằng hexa và octal có thể lấy thêm kiểu không dấu: 0xFFFFFFFF có kiểu unsigned int khi int rộng 32 bit. Hậu tố u, l, ul, ll chỉ định kiểu tường minh. Trong code thao tác bit, nên dùng hằng không dấu như 1u << 31, vì 1 << 31 dịch bit vào bit dấu của int.

Hai cảnh báo trong output thuộc -Wparentheses, bật cùng -Wall.

Kiểu bool

C99 thêm kiểu _Bool cùng header <stdbool.h> định nghĩa bool, true, false dưới dạng macro. Từ C23, bool, true và false trở thành từ khóa, không cần include header nữa. Khi chuyển một giá trị vô hướng (số hoặc pointer) sang bool, mọi giá trị khác 0 cho kết quả 1 (chuẩn C11, mục 6.3.1.2). Quy tắc này khác với chuyển sang kiểu số nguyên hẹp, nơi các bit cao bị cắt bỏ:

boolt.c
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

#define RX_READY (1u << 8)

int main(void)
{
    uint32_t status = 0x100;           /* bit 8 đang bật */

    uint8_t ready8 = status & RX_READY;
    bool    ready  = status & RX_READY;

    printf("ready8 = %u, ready = %d\n", ready8, ready);
    return 0;
}
ready8 = 0, ready = 1

Kết quả của status & RX_READY là 0x100. Gán vào uint8_t giữ lại 8 bit thấp, tức 0, nên cờ đang bật bị đọc thành tắt. Gán vào bool cho 1 như mong muốn. Biến lưu kết quả kiểm tra một bit nên có kiểu bool, hoặc viết tường minh (status & RX_READY) != 0. Lỗi này cũng chỉ được cảnh báo khi bật -Wconversion.

Tóm tắt

Bẫy Dấu hiệu Cách tránh Option cảnh báo
long khác độ rộng giữa các nền tảng Giá trị bị cắt khi chuyển từ x86-64 sang Arm 32-bit Dùng int64_t, uint32_t Không có
Dấu của char So sánh với EOF hoặc giá trị âm sai trên Arm int cho kết quả getchar, uint8_t cho byte -Wtype-limits
Định dạng printf sai kiểu Cảnh báo chỉ xuất hiện trên một số target Macro PRIu32, PRId64 -Wformat
~ trên kiểu hẹp So sánh với hằng luôn sai Ép kiểu kết quả về kiểu gốc -Wsign-compare
So sánh có dấu với không dấu -1 < 1u sai Cùng kiểu cho hai vế -Wsign-compare
Nhân hai uint16_t Tràn số có dấu Ép một toán hạng sang uint32_t -fsanitize=undefined
Chuyển đổi thu hẹp ngầm Giá trị bị cắt, cờ bit bị mất Ép kiểu tường minh, dùng bool cho cờ -Wconversion
Thứ tự ưu tiên của &, << Biểu thức bit cho kết quả sai Đặt ngoặc -Wparentheses
Hằng octal 010 bằng 8 Không thêm số 0 phía trước hằng thập phân Không có

Liên quan

Tài liệu tham khảo

  • B. W. Kernighan, D. M. Ritchie, The C Programming Language, bản 2, Prentice Hall, 1988: mục 1.5.1 (kiểu của biến nhận getchar), chương 2 (kiểu, toán tử và biểu thức).
  • ISO/IEC 9899:201x, Committee Draft N1570, 2011: mục 5.2.4.2.1 (giới hạn của kiểu số nguyên), 6.3.1.1 (integer promotion), 6.3.1.2 (chuyển sang _Bool), 6.3.1.3 (chuyển đổi giữa số có dấu và không dấu), 6.3.1.8 (usual arithmetic conversion), 6.4.4.1 (hằng số nguyên), 6.5 (tràn số trong biểu thức), 7.8 (<inttypes.h>), 7.20 (<stdint.h>).
  • cppreference, Arithmetic types: data model ILP32, LP64, LLP64 và kiểu bool trong C23.
  • Arm, Procedure Call Standard for the Arm Architecture (AAPCS32): kích thước kiểu cơ bản và char không dấu.
  • GCC, Options Controlling C Dialect: -fsigned-char, -funsigned-char.
  • GCC, Options to Request or Suppress Warnings: -Wconversion, -Wsign-compare, -Wtype-limits, -Wparentheses, -Wformat.
  • GCC, Integers implementation: kết quả chuyển sang kiểu có dấu khi giá trị không biểu diễn được.
  • MISRA C:2012, quy tắc 10.3: gán giá trị sang kiểu hẹp hơn hoặc khác loại.