Menu

Kiểu dữ liệu trong C: int, float, char, double và hơn nữa

Mỗi biến trong C có một kiểu quyết định kích thước và những gì nó chứa được. Đây là tất cả các kiểu cơ bản, kích thước thực tế, signed và unsigned, sizeof, limits.h và chuyện gì xảy ra khi tràn số.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Mỗi biến trong C có một kiểu dữ liệu (data type), được chọn khi bạn khai báo và cố định suốt vòng đời của biến. Kiểu quyết định ba điều: biến chiếm bao nhiêu byte, các byte đó được hiểu như thế nào và những phép toán nào có nghĩa với nó.

Danh sách kiểu của C khá ngắn. Sự phức tạp nằm ở các từ bổ nghĩa.

Bốn kiểu gốc

  • char chứa một byte. Nó dùng cho ký tự đơn ('A'), cho các byte của một chuỗi, và đôi khi như một số nguyên rất nhỏ.
  • int là kiểu số nguyên chủ lực. Biến đếm vòng lặp, kích thước, ID.
  • float và double chứa số thực. double có độ chính xác gần gấp đôi.
  • void theo một nghĩa nào đó là kiểu thứ tư, nhưng nó có nghĩa là "không có giá trị": bạn không thể khai báo biến void, chỉ dùng nó làm kiểu trả về của hàm, danh sách tham số rỗng hoặc con trỏ không kiểu.

Để ý hậu tố f trong 3.14f. Không có nó, literal là một double, và gán nó cho một float sẽ âm thầm mất độ chính xác. Một số trình biên dịch có cảnh báo về điều này.

Từ bổ nghĩa: short, long, unsigned

Các kiểu số nguyên gốc đi kèm những từ bổ nghĩa làm thay đổi kích thước hoặc dấu.

Mỗi kiểu số nguyên cần định dạng riêng: %hd cho short, %d cho int, %ld cho long, %lld cho long long, %u cho unsigned. Dùng sai định dạng là undefined behavior, không phải sai số làm tròn.

unsigned bỏ bit dấu và nhân đôi phạm vi dương. Một unsigned int chứa từ 0 đến khoảng 4,3 tỷ thay vì từ âm 2,1 tỷ đến 2,1 tỷ. Đây là lựa chọn đúng cho những thứ thực sự không thể âm, như kích thước, số byte, mẫu bit, và là một cái bẫy cho bất cứ thứ gì có thể bị trừ xuống dưới 0.

signed là mặc định của int, short, long và long long, nên bạn gần như không bao giờ phải viết nó. Chỗ duy nhất nó quan trọng là char: char thường có dấu hay không dấu là do cài đặt quyết định (implementation-defined), nên hãy viết signed char hoặc unsigned char khi dấu quan trọng.

Kích thước: thực tế bạn nhận được gì

Chuẩn C chỉ quy định kích thước tối thiểu, không quy định chính xác. Trên mọi máy Linux, macOS hoặc Windows 64 bit hiện đại, bạn sẽ thấy như sau:

KiểuKích thước thường gặpPhạm vi thường gặp
char1 bytetừ -128 đến 127 (hoặc từ 0 đến 255)
short2 bytetừ -32.768 đến 32.767
int4 bytetừ -2.147.483.648 đến 2.147.483.647
long8 byte (4 trên Windows)khoảng ±9,2 tỷ tỷ
long long8 bytekhoảng ±9,2 tỷ tỷ
float4 bytekhoảng 7 chữ số có nghĩa
double8 bytekhoảng 15 chữ số có nghĩa
long double16 byte (tùy hệ thống)nhiều hơn double

Hàng long là hàng hay làm người ta vấp: nó chiếm 8 byte trên Linux và macOS, nhưng 4 byte trên Windows 64 bit. Code giả định long chứa được giá trị 64 bit không có tính di động. Hãy dùng long long, hoặc các kiểu có độ rộng chính xác trong stdint.h (int32_t, uint64_t) khi kích thước là một phần của yêu cầu.

sizeof: hỏi trình biên dịch

Đừng bao giờ đoán kích thước, hãy đo nó:

sizeof là một toán tử, không phải hàm, và được tính lúc biên dịch. Nó cho ra giá trị kiểu size_t, in bằng %zu.

sizeof(char) được đảm bảo đúng bằng 1: đó là định nghĩa của một byte trong C. Mọi kiểu khác được đo tương đối so với nó.

limits.h và float.h

Phạm vi chính xác cho trình biên dịch của bạn có sẵn dưới dạng các hằng số có tên:

Đây là những giá trị để so sánh khi bạn cần biết một phép toán có bị tràn hay không. Kiểm tra if (a > INT_MAX - b) trước khi tính a + b là cách phát hiện tràn số trước khi nó xảy ra, và điều này quan trọng vì bạn không thể phát hiện tràn số có dấu sau khi nó đã xảy ra.

Tràn số nguyên

Chuyện gì xảy ra khi một giá trị vượt quá phạm vi của kiểu hoàn toàn phụ thuộc vào dấu.

Tràn số không dấu được định nghĩa rõ: giá trị quay vòng theo modulo 2^N.

Trường hợp thứ hai là nguồn bug thật sự. Một vòng lặp như for (unsigned i = n - 1; i >= 0; i--) không bao giờ kết thúc, vì giá trị không dấu luôn >= 0.

Tràn số có dấu là undefined behavior. Không phải "quay vòng", mà là không xác định. Trình biên dịch được phép giả định nó không bao giờ xảy ra và tối ưu dựa trên giả định đó, nghĩa là một phép kiểm tra tràn số viết sau khi tính có thể bị xóa mất:

int sum = a + b;
if (sum < a) { /* the compiler may remove this entirely */ }

Hãy kiểm tra trước, dùng các giới hạn:

if (b > 0 && a > INT_MAX - b) {
    /* a + b would overflow - handle it */
}

Độ chính xác của số thực

float và double lưu số dưới dạng nhị phân, và hầu hết các phân số thập phân không có dạng nhị phân chính xác, giống như 1/3 không có dạng thập phân chính xác.

Quy tắc rút ra: đừng bao giờ so sánh số thực bằng ==. Thay vào đó, hãy so sánh trị tuyệt đối của hiệu với một sai số nhỏ.

Và đừng bao giờ dùng số thực cho tiền. Hãy lưu số xu dưới dạng số nguyên; một long long lưu số xu là chính xác, còn một double lưu số đô la thì không.

Chọn kiểu nào

Một danh sách quyết định ngắn bao quát phần lớn code:

  • Số nguyên: int, trừ khi bạn có lý do khác. Đây là kiểu CPU xử lý hiệu quả nhất và là kiểu mà mọi quy tắc số học được xây dựng xoay quanh.
  • Bất cứ thứ gì lớn hơn 2 tỷ: long long, hoặc int64_t từ stdint.h.
  • Kích thước, độ dài, chỉ số mảng lấy từ sizeof hoặc strlen: size_t. Kiểu này không dấu và được đảm bảo đủ lớn cho mọi đối tượng.
  • Số thập phân: double. Chỉ dùng float để giảm một nửa bộ nhớ trong mảng lớn hoặc trên phần cứng nhúng không có bộ xử lý số thực độ chính xác kép.
  • Ký tự đơn và byte thô: char cho văn bản, unsigned char cho dữ liệu nhị phân.
  • Đúng/sai: bool từ stdbool.h, xem boolean trong C.
  • Độ rộng bit chính xác (định dạng tệp, giao thức mạng, thanh ghi phần cứng): stdint.h, gồm uint8_t, int16_t, uint32_t và các kiểu tương tự.

Trộn lẫn các kiểu

Khi bạn kết hợp hai kiểu khác nhau trong một biểu thức, C âm thầm chuyển đổi chúng trước khi tính toán. Điều đó thường có ích nhưng đôi khi gây thảm họa:

Trường hợp đầu là phép chia nguyên: cả hai toán hạng đều là int, nên kết quả là int và phần thập phân bị bỏ đi. Trường hợp thứ ba còn tệ hơn: so sánh một giá trị có dấu với một giá trị không dấu sẽ chuyển giá trị có dấu sang không dấu, biến -1 thành một số dương khổng lồ.

Các quy tắc chuyển đổi đó, và cách tự kiểm soát chúng bằng ép kiểu tường minh, là chủ đề của bài ép kiểu trong C.

Câu hỏi thường gặp

Các kiểu dữ liệu cơ bản trong C là gì?

Có bốn kiểu gốc: char cho ký tự đơn và byte, int cho số nguyên, float và double cho số thập phân. Các từ bổ nghĩa thay đổi kích thước và dấu của chúng, gồm short, long, long long, signed và unsigned, tạo nên toàn bộ tập kiểu.

Kiểu int trong C chiếm bao nhiêu byte?

Gần như luôn là 4 byte (32 bit) trên các máy tính để bàn và máy chủ hiện đại, cho phạm vi khoảng từ âm 2,1 tỷ đến 2,1 tỷ. Chuẩn C chỉ đảm bảo ít nhất 2 byte, và các vi điều khiển 16 bit thật sự dùng 2 byte. Dùng sizeof(int) nếu bạn cần biết chắc chắn.

float và double trong C khác nhau thế nào?

float chiếm 4 byte với khoảng 7 chữ số thập phân có nghĩa; double chiếm 8 byte với khoảng 15 chữ số. Double là kiểu mặc định cho literal số thực và các hàm toán học, và trừ khi bạn lưu hàng triệu giá trị hoặc lập trình cho chip nhúng, double là lựa chọn đúng.

Chuyện gì xảy ra khi int bị tràn số trong C?

Với int có dấu (signed), tràn số là undefined behavior: trình biên dịch có thể quay vòng, giữ ở giá trị biên, hoặc tối ưu bỏ hẳn phép kiểm tra. Với int không dấu (unsigned), hành vi được định nghĩa đầy đủ: giá trị quay vòng theo modulo 2^N, nên UINT_MAX + 1 bằng 0. Đừng bao giờ dựa vào tràn số có dấu.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu