Ứng dụng thực tế
Sau bài này bạn sẽ làm được
- Dùng union để xem biểu diễn byte của một giá trị
- Biết vì sao đọc thành viên khác là hợp lệ trong C nhưng không di động
- Kết hợp union với trường bit cho thanh ghi phần cứng
- Chọn đúng công cụ giữa union, memcpy và ép kiểu con trỏ
Bài cuối của Phần 7 nói về công dụng thứ hai của union: nhìn cùng một vùng nhớ qua hai lăng kính khác nhau. Nó rất hữu ích trong lập trình hệ thống và nhúng, nhưng mỗi ứng dụng đều kèm một cái giá về tính di động mà bạn phải biết rõ.
#Xem biểu diễn byte của một giá trị
#include <stdio.h>
union Xem {
float f;
unsigned int u;
unsigned char b[sizeof(float)];
};
int main(void)
{
union Xem x = { .f = 3.14f };
printf("gia tri : %.6f\n", x.f);
printf("dang so nguyen: %u\n", x.u);
printf("dang thap luc : 0x%08X\n", x.u);
printf("tung byte : ");
for (size_t i = 0; i < sizeof x.b; ++i)
printf("%02X ", x.b[i]);
printf("\n");
return 0;
}gia tri : 3.140000 dang so nguyen: 1078523331 dang thap luc : 0x4048F5C3 tung byte : C3 F5 48 40
/* Cách chuẩn để kiểm tra thứ tự byte của máy đang chạy */
int la_nho_truoc(void)
{
union { unsigned int u; unsigned char b[4]; } x = { .u = 1 };
return x.b[0] == 1;
}
int main(void)
{
printf("%s\n", la_nho_truoc() ? "nho truoc" : "lon truoc");
return 0;
}Ứng dụng: in cấu trúc của một số thực
#include <stdio.h>
void phan_tich(float f)
{
union { float f; unsigned int u; } x = { .f = f };
unsigned dau = (x.u >> 31) & 0x1u;
unsigned mu = (x.u >> 23) & 0xFFu;
unsigned phan_le = x.u & 0x7FFFFFu;
printf("%.6f -> dau=%u mu=%u (that: %d) phan le=0x%06X\n",
f, dau, mu, (int)mu - 127, phan_le);
}
int main(void)
{
phan_tich(1.0f);
phan_tich(0.5f);
phan_tich(3.14f);
phan_tich(-2.0f);
phan_tich(0.0f);
return 0;
}1.000000 -> dau=0 mu=127 (that: 0) phan le=0x000000 0.500000 -> dau=0 mu=126 (that: -1) phan le=0x000000 3.140000 -> dau=0 mu=128 (that: 1) phan le=0x48F5C3 -2.000000 -> dau=1 mu=128 (that: 1) phan le=0x000000 0.000000 -> dau=0 mu=0 (that: -127) phan le=0x000000
Bài 2.3 đã giới thiệu chuẩn IEEE 754. Đây là cách bạn nhìn thấy cấu trúc đó bằng mắt, và nó giải thích rất nhiều hiện tượng của số thực, ví dụ vì sao 0.1 không biểu diễn chính xác được.
#Bốn thứ phụ thuộc nền tảng
| Thứ phụ thuộc | Ví dụ khác biệt | |
|---|---|---|
| 1 | Thứ tự byte | x86 và ARM thường nhỏ trước, mạng và một số kiến trúc lớn trước |
| 2 | Kích thước kiểu | long là 8 byte trên Linux 64 bit, 4 byte trên Windows 64 bit |
| 3 | Phần đệm trong struct | Phụ thuộc yêu cầu căn chỉnh của từng kiến trúc |
| 4 | Biểu diễn số thực | Gần như mọi máy dùng IEEE 754, nhưng chuẩn C không bắt buộc |
fwrite(&b, sizeof b, 1, f);
fread(&b, sizeof b, 1, f);#include <stdint.h>
/* Ghi số nguyên 32 bit theo thứ tự byte lớn trước, tức thứ tự mạng */
static void ghi_u32(FILE *f, uint32_t v)
{
unsigned char b[4] = {
(unsigned char)(v >> 24),
(unsigned char)(v >> 16),
(unsigned char)(v >> 8),
(unsigned char)(v),
};
fwrite(b, 1, 4, f);
}
static int doc_u32(FILE *f, uint32_t *ra)
{
unsigned char b[4];
if (fread(b, 1, 4, f) != 4) return -1;
*ra = ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16)
| ((uint32_t)b[2] << 8) | (uint32_t)b[3];
return 0;
}
/* Cách này ĐỘC LẬP hoàn toàn với thứ tự byte của máy,
vì nó xây giá trị bằng phép dịch bit chứ không đọc byte thô. */#Ba cách diễn giải lại byte
| Cách | Hợp lệ trong C | Hợp lệ trong C++ | Đánh giá |
|---|---|---|---|
| union | Có | Không | Gọn, nhưng không dùng được nếu mã phải chạy cả hai |
| memcpy | Có | Có | Luôn đúng, và trình biên dịch tối ưu thành không lệnh nào |
| Ép kiểu con trỏ | Vi phạm quy tắc bí danh | Vi phạm | Tránh, kể cả khi nó có vẻ chạy được |
#include <stdio.h>
#include <string.h>
float f = 3.14f;
/* Cách 1: union. Hợp lệ trong C. */
union { float f; unsigned u; } x = { .f = f };
unsigned a = x.u;
/* Cách 2: memcpy. Hợp lệ ở mọi nơi, và đây là cách nên dùng. */
unsigned b;
memcpy(&b, &f, sizeof b);
/* Cách 3: ép kiểu con trỏ. VI PHẠM quy tắc bí danh nghiêm ngặt. */
unsigned c = *(unsigned *)&f; /* đừng viết */#Union với trường bit cho thanh ghi
Trong lập trình nhúng, một thanh ghi phần cứng thường gói nhiều trường nhỏ vào một từ. Union cho phép bạn vừa đọc ghi cả từ, vừa truy cập từng trường bằng tên.
#include <stdint.h>
/* Thanh ghi điều khiển 8 bit của một thiết bị giả định:
bit 0 : bật hay tắt
bit 1..3 : chế độ, giá trị 0 tới 7
bit 4..6 : mức ưu tiên
bit 7 : cờ báo lỗi */
typedef union {
uint8_t ca_thanh_ghi;
struct {
uint8_t bat : 1;
uint8_t che_do : 3;
uint8_t uu_tien : 3;
uint8_t loi : 1;
} bit;
} ThanhGhiDieuKhien;
ThanhGhiDieuKhien tg = { .ca_thanh_ghi = 0 };
tg.bit.bat = 1;
tg.bit.che_do = 5;
tg.bit.uu_tien = 3;
printf("0x%02X\n", tg.ca_thanh_ghi); /* ghi cả từ vào thanh ghi thật */
/* Đọc ngược lại */
tg.ca_thanh_ghi = doc_thanh_ghi_phan_cung();
if (tg.bit.loi)
xu_ly_loi();tg.bit.che_do = 5;#define TG_CHE_DO_DICH 1
#define TG_CHE_DO_MASK (0x7u << TG_CHE_DO_DICH)
static inline uint8_t tg_dat_che_do(uint8_t tg, uint8_t v)
{
return (uint8_t)((tg & (uint8_t)~TG_CHE_DO_MASK)
| ((v << TG_CHE_DO_DICH) & TG_CHE_DO_MASK));
}
static inline uint8_t tg_lay_che_do(uint8_t tg)
{
return (uint8_t)((tg & TG_CHE_DO_MASK) >> TG_CHE_DO_DICH);
}
uint8_t tg = 0;
tg = tg_dat_che_do(tg, 5); /* luôn đúng trên mọi trình biên dịch */Thêm volatile cho thanh ghi thật
/* Thanh ghi phần cứng có thể đổi giá trị mà chương trình không hề ghi vào,
ví dụ do thiết bị cập nhật. Nên phải đánh dấu volatile. */
#define TG_DIEU_KHIEN (*(volatile uint8_t *)0x40021000u)
TG_DIEU_KHIEN = 0x2Bu;
while ((TG_DIEU_KHIEN & 0x80u) == 0)
; /* chờ bit lỗi. Không có volatile, trình biên dịch đọc một lần rồi lặp mãi. */Bài 14.4 đã nói về volatile với cờ tín hiệu, và đây là ứng dụng quan trọng thứ hai của nó. Chương 32 sẽ trình bày đầy đủ.
#Gói tin và định dạng nhị phân
typedef struct {
uint16_t loai;
uint16_t do_dai;
uint32_t ma;
} DauGoi;
unsigned char bo_dem[1024];
recv(fd, bo_dem, sizeof bo_dem, 0);
DauGoi *g = (DauGoi *)bo_dem; /* SAI ở ba mức */
/* 1. Vi phạm quy tắc bí danh nghiêm ngặt.
2. bo_dem có thể không căn chỉnh đủ cho uint32_t.
3. Thứ tự byte và phần đệm phụ thuộc nền tảng. */typedef struct {
uint16_t loai;
uint16_t do_dai;
uint32_t ma;
} DauGoi;
/* Đọc từ bộ đệm byte, thứ tự lớn trước tức thứ tự mạng */
static int phan_tich_dau(const unsigned char *b, size_t n, DauGoi *ra)
{
if (b == NULL || ra == NULL || n < 8) return -1;
ra->loai = (uint16_t)((b[0] << 8) | b[1]);
ra->do_dai = (uint16_t)((b[2] << 8) | b[3]);
ra->ma = ((uint32_t)b[4] << 24) | ((uint32_t)b[5] << 16)
| ((uint32_t)b[6] << 8) | (uint32_t)b[7];
return 0;
}
/* Ghi ra bộ đệm byte */
static int dong_goi_dau(unsigned char *b, size_t n, const DauGoi *g)
{
if (b == NULL || g == NULL || n < 8) return -1;
b[0] = (unsigned char)(g->loai >> 8);
b[1] = (unsigned char)(g->loai);
b[2] = (unsigned char)(g->do_dai >> 8);
b[3] = (unsigned char)(g->do_dai);
b[4] = (unsigned char)(g->ma >> 24);
b[5] = (unsigned char)(g->ma >> 16);
b[6] = (unsigned char)(g->ma >> 8);
b[7] = (unsigned char)(g->ma);
return 0;
}Bảng tổng kết: chọn công cụ nào
| Bài toán | Công cụ đúng | Vì sao |
|---|---|---|
| Lưu một trong nhiều kiểu | Union có nhãn | An toàn, và trình biên dịch kiểm tra hộ |
| Xem byte thô của một giá trị | Union với mảng unsigned char, hoặc memcpy | Hợp lệ theo chuẩn, và đúng ở mọi nơi |
| Đổi kiểu giữ nguyên byte | memcpy | Hợp lệ cả C lẫn C++, và tối ưu thành một lệnh |
| Đọc ghi định dạng nhị phân | Phép dịch bit trên mảng unsigned char | Độc lập hoàn toàn với nền tảng |
| Thanh ghi phần cứng | Phép toán bit cộng volatile | Bố cục bit do phần cứng quy định, không do trình biên dịch |
| Cấu trúc nội bộ tiết kiệm bit | Trường bit | Gọn, và tính di động không quan trọng ở đây |
Tự làm thử
- Cài chương trình xem byte của một
float, thử với 1.0, 0.5, âm 2.0 và 0.1. - Viết hàm kiểm tra thứ tự byte của máy, chạy trên máy bạn và giải thích kết quả.
- Cài hàm phân tích cấu trúc IEEE 754 của
float, kiểm chứng với năm giá trị trong bài. - Ghi một struct thẳng ra file rồi đọc lại bằng một chương trình biên dịch với
-m32, quan sát dữ liệu sai. - Cài cặp hàm ghi và đọc số 32 bit theo thứ tự mạng, kiểm chứng bằng cách ghi rồi đọc lại.
- Chạy ví dụ vi phạm quy tắc bí danh với
-O0và-O2, ghi lại hai kết quả khác nhau. - Cài thanh ghi điều khiển bằng cả trường bit lẫn phép toán bit, so sánh giá trị thanh ghi cuối cùng của hai cách.
- Cài cặp hàm phân tích và đóng gói tiêu đề gói tin tám byte, kiểm chứng bằng bộ dữ liệu mẫu.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Union cho phép nhìn cùng vùng nhớ qua nhiều lăng kính. Đọc qua mảng
unsigned charluôn hợp lệ, đọc qua kiểu khác thì hợp lệ trong C nhưng không di động. - Bốn thứ phụ thuộc nền tảng: thứ tự byte, kích thước kiểu, phần đệm, và biểu diễn số thực.
- Đừng ghi struct thẳng ra file hay ra mạng. Hãy đọc ghi từng trường bằng phép dịch bit trên mảng
unsigned char. - Dùng
memcpyđể đổi kiểu giữ nguyên byte. Ép kiểu con trỏ vi phạm quy tắc bí danh và cho kết quả khác nhau giữa các mức tối ưu. - Trường bit tiện cho cấu trúc nội bộ, nhưng thanh ghi phần cứng và định dạng nhị phân phải dùng phép toán bit rõ ràng.