Bài 52.424 phút đọc
Compound literal và thời gian sống
Sau bài này bạn sẽ làm được
- Phân biệt compound literal trong hàm và ở phạm vi tệp
- Nhận ra con trỏ treo do compound literal
- Dùng compound literal làm đối số tạm an toàn
- Biết C23 đổi gì ở chỗ này
Compound literal là một trong những tính năng C99 hữu ích nhất và cũng nguy hiểm nhất. Cùng một cú pháp, viết ở hai chỗ khác nhau, cho ra hai thời gian sống hoàn toàn khác nhau, và trình biên dịch hầu như không cảnh báo.
#Cú pháp và ba dạng dùng
Compound literal
Một biểu thức tạo ra một đối tượng vô danh có kiểu và giá trị bạn viết ra. Cú pháp là tên kiểu trong ngoặc, rồi danh sách khởi tạo trong ngoặc nhọn.
Ba dạng dùng
typedef struct { int x, y; } Diem;
/* 1. Doi so tam, khong can dat ten bien */
ve_diem((Diem){ 1, 2 });
ve_diem((Diem){ .x = 1, .y = 2 });
/* 2. Gan lai ca struct mot lan */
Diem p = { 0, 0 };
p = (Diem){ 10, 20 }; /* thay cho p.x = 10; p.y = 20; */
/* 3. Mang tam */
int tong = cong((int[]){ 1, 2, 3, 4, 5 }, 5);
/* Va mot dang it gap: con tro toi mot gia tri tam */
f(&(int){ 42 }); /* truyen dia chi cua mot int chua 42 */#Hai thời gian sống
| Viết ở đâu | Thời gian lưu trữ | Sống bao lâu |
|---|---|---|
| Phạm vi tệp, ngoài mọi hàm | Tĩnh | Suốt chương trình |
| Trong một hàm hoặc một khối | Tự động | Tới dấu ngoặc đóng của khối chứa nó |
vongdoi.c
#include <stdio.h>
static int *g = (int[]){1, 2, 3}; /* pham vi TEP: song TINH */
static int *tra_ve_cl(void) {
return (int[]){10, 20, 30}; /* trong HAM: song TU DONG,
chet khi ham tra ve */
}
int main(void) {
printf("g[0] = %d\n", g[0]);
int *p = tra_ve_cl();
printf("p[0] = %d (con tro treo)\n", p[0]);
int *q;
{ int x = 5; q = &x; }
printf("q = %p (doc dia chi cung la UB)\n", (void *)q);
return 0;
}terminal
gcc -std=c11 -O1 -Wall -Wextra -o vongdoi.exe vongdoi.c && ./vongdoi.exe
vongdoi.c:13:5: warning: '<U8750>' is used uninitialized in this function [-Wuninitialized]
printf("p[0] = %d (con tro treo)\n", p[0]);
^
g[0] = 1
p[0] = 0 (con tro treo)
q = 000000000061FE40 (doc dia chi cung la UB)#Đo thật
Phạm vi hẹp hơn bạn nghĩ
void f(int co) {
int *p;
if (co) {
p = (int[]){1, 2, 3}; /* song toi dau ngoac dong cua if */
}
printf("%d\n", p[0]); /* UB neu co != 0 */
}
/* Compound literal chet o cuoi KHOI chua no, khong phai cuoi ham.
Va trong mot vong lap: */
for (int i = 0; i < 3; ++i) {
int *p = (int[]){i, i, i}; /* MOT doi tuong moi moi vong,
va cai cu chet o cuoi moi vong */
ds[i] = p; /* ca ba con tro deu treo sau vong lap */
}terminal
# ASan bắt được, với cờ use-after-scope
gcc -g -O1 -fsanitize=address -fsanitize-address-use-after-scope -o vd vongdoi.c && ./vd
==12345==ERROR: AddressSanitizer: stack-use-after-return on address 0x7f2b1c000020
READ of size 4 at 0x7f2b1c000020 thread T0
#0 0x4011f2 in main vongdoi.c:13
Address is located in stack of thread T0 at offset 32 in frame
#0 0x401180 in tra_ve_cl vongdoi.c:5
This frame has 1 object(s):
[32, 44) '<anonymous>' <== Memory access at offset 32 is inside this variable#Bốn cách dùng an toàn
Bốn cách sai
/* 1. Tra ve tu ham */
int *f(void) { return (int[]){1, 2, 3}; }
/* 2. Luu vao bien song lau hon */
static int *g;
void f(void) { g = (int[]){1, 2, 3}; }
/* 3. Trong vong lap, luu con tro lai */
for (int i = 0; i < 3; ++i) ds[i] = &(Diem){i, i};
/* 4. Truyen cho ham co GIU con tro */
dang_ky_xu_ly(xu_ly, &(NguCanh){ .id = 1 });Bốn cách đúng
/* 1. Tra ve theo GIA TRI, khong tra ve con tro */
Diem f(void) { return (Diem){1, 2}; } /* sao chep, an toan */
/* 2. Neu can song lau: static */
static int *g;
void f(void) {
static int ds[] = {1, 2, 3}; /* thoi gian luu tru tinh */
g = ds;
}
/* 3. Trong vong lap: mang that */
Diem ds[3];
for (int i = 0; i < 3; ++i) ds[i] = (Diem){i, i}; /* SAO CHEP vao ds */
/* 4. Voi ham giu con tro: cap phat dong */
NguCanh *nc = malloc(sizeof *nc);
*nc = (NguCanh){ .id = 1 };
dang_ky_xu_ly(xu_ly, nc); /* va nho free khi huy dang ky */Mẫu rất hữu ích: bảng tra tạm
/* Thay vi khai bao mot mang tinh chi dung mot lan: */
const char *ten_thu(int n) {
static const char *const BANG[] = {
"CN", "T2", "T3", "T4", "T5", "T6", "T7"
};
return (n >= 0 && n < 7) ? BANG[n] : "?";
}
/* Voi compound literal o PHAM VI HAM thi van an toan neu ban
dung ngay va khong luu lai: */
const char *ten_thu(int n) {
return (n >= 0 && n < 7)
? (const char *[]){ "CN","T2","T3","T4","T5","T6","T7" }[n]
: "?";
}
/* CHU Y: cach thu hai tra ve mot con tro toi CHUOI HANG,
khong phai toi compound literal. Chuoi hang song suot chuong trinh,
nen an toan.
Nhung neu ban tra ve chinh mang thi khong: */
const char **bang(void) {
return (const char *[]){ "CN", "T2" }; /* SAI: mang chet */
}
/* Ban thu nhat ro rang hon va khong co bay. Nen dung no. */#C23 đổi gì
/* C23 them tu khoa "constexpr" cho compound literal,
de bat no co thoi gian luu tru TINH ngay ca trong ham: */
void f(void) {
int *p = (static int[]){1, 2, 3}; /* de xuat, chua vao chuan */
constexpr int a[] = {1, 2, 3}; /* C23: hang so that su */
}
/* Thuc te C23 lam duoc hai viec lien quan:
1. constexpr cho bien: gia tri biet luc dich, dung lam kich thuoc
mang va nhan case duoc. Bai 51.3.
2. "{}" rong hop le: */
Diem p = {}; /* C23: moi truong bang 0 */
Diem p = { 0 }; /* truoc C23 phai viet vay */
*c = (CauHinh){}; /* C23: quet sach, gon hon (CauHinh){ 0 } */
/* De xuat "storage-class specifier trong compound literal", tuc
(static int[]){...}, da duoc thao luan nhung chua vao C23.
GCC ho tro no nhu mot mo rong tu lau: */
gcc -std=gnu2x prog.c /* co the chap nhan (static int[]){...} */Tự làm thử
- Chạy
vongdoi.cvà xác nhậng[0]đúng cònp[0]sai. - Đặt compound literal trong một khối
ifvà dùng nó sau khối đó. - Chạy với
-fsanitize=addressvàdetect_stack_use_after_return=1. - Viết hàm trả về
Diemtheo giá trị và so với bản trả về con trỏ. - Viết macro trả về compound literal và chứng minh nó chỉ an toàn khi dùng ngay.
- Đổi một hàm dùng mảng tĩnh sang compound literal và ngược lại, so hai cách.
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
- Compound literal là một đối tượng thật có địa chỉ và vòng đời, không phải một hằng số.
- Ở phạm vi tệp thì nó sống tĩnh; trong hàm thì nó chết ở dấu ngoặc đóng của khối chứa nó.
- Nó hành xử giống hệt một biến cục bộ, nhưng
-Wreturn-local-addrkhông bắt được nó. - Dùng nó như một giá trị, đừng dùng như một vùng nhớ: đừng để con trỏ tới nó sống lâu hơn câu lệnh.
- C23 cho phép
{}rỗng và thêmconstexpr, nhưng biếnstaticcó tên vẫn là cách rõ ràng nhất.