Bài 33.526 phút đọc
Điểm tuần tự và thứ tự đánh giá
Sau bài này bạn sẽ làm được
- Kể nơi có điểm tuần tự và nơi không có
- Giải thích vì sao thứ tự đánh giá đối số là không xác định
- Phân biệt unsequenced với indeterminately sequenced
- Áp dụng quy tắc một biến chỉ sửa một lần trong một câu lệnh
i = i++ + ++i là câu hỏi phỏng vấn kinh điển và là câu hỏi sai. Câu trả lời đúng không phải một con số, mà là: đoạn mã này không có nghĩa. Bài này giải thích vì sao, và quan trọng hơn, cách để không bao giờ phải nghĩ về nó nữa.
#Điểm tuần tự là gì
Điểm tuần tự (sequence point)
Một mốc trong quá trình thực thi mà tại đó mọi tác dụng phụ của các phép tính trước đó đã hoàn tất, và chưa tác dụng phụ nào của các phép tính sau đó bắt đầu.
| Vị trí | Có điểm tuần tự | Nghĩa là |
|---|---|---|
| Dấu chấm phẩy cuối câu lệnh | Có | Câu lệnh trước xong hẳn rồi mới sang câu sau |
| Toán tử && và || | Có | Vế trái xong hẳn, và có thể vế phải không chạy |
| Toán tử ba ngôi ? : | Có | Điều kiện xong hẳn rồi mới chọn nhánh |
| Toán tử phẩy , | Có | Vế trái xong hẳn rồi mới sang vế phải |
| Trước khi thân hàm chạy | Có | Mọi đối số đã tính xong |
| Giữa các đối số hàm | Không | Thứ tự tùy trình biên dịch |
| Hai vế của + - * / | Không | Thứ tự tùy trình biên dịch |
| Hai vế của phép gán = | Không | Thứ tự tùy trình biên dịch |
| Trong khởi tạo mảng và struct | Có, từ C11 | Theo thứ tự viết |
&& và || là chỗ điểm tuần tự cứu bạn
if (p != NULL && p->truong > 0) /* AN TOAN */
/* Toan tu && bao dam:
1. p != NULL duoc danh gia xong han
2. neu no sai, ve phai KHONG chay
3. neu no dung, moi den p->truong
Do la ly do mau nay dung duoc, va la mau ban se viet moi ngay. */
if (p->truong > 0 && p != NULL) /* SAI: giai tham chieu truoc */
if (i < n & a[i] > 0) /* SAI: & khong phai &&,
khong co diem tuan tu,
ca hai ve deu chay */#Thứ tự đánh giá đối số
seq.c
static int lan = 0;
static int g(void) { printf(" g chay thu %d\n", ++lan); return 1; }
static int h(void) { printf(" h chay thu %d\n", ++lan); return 2; }
static int f(int a, int b) { return a * 10 + b; }
int main(void) {
printf("f(g(), h()):\n");
printf(" ket qua %d\n", f(g(), h()));
return 0;
}terminal
gcc -std=c11 -O2 -Wall -Wextra -o seq seq.c && ./seq
f(g(), h()): h chay thu 1 g chay thu 2 ket qua 12
#Sửa một biến nhiều lần
Bốn biểu thức không có nghĩa
int i = 1;
int x = i++ + ++i;
int j = 0;
int a[5] = { 0 };
a[j] = j++;
int k = 5;
printf("%d %d\n", k++, k++);
int m = 2;
m = m++;terminal
gcc -std=c11 -O2 -Wall -Wextra -c seq.c
seq.c:13:19: warning: operation on 'i' may be undefined [-Wsequence-point]
^~~
seq.c:18:13: warning: operation on 'j' may be undefined [-Wsequence-point]
~^~
seq.c:23:29: warning: operation on 'k' may be undefined [-Wsequence-point]
~^~./seq
i++ + ++i voi i=1 -> x = 4, i = 3
a[j] = j++ voi j=0 -> a[0]=0 a[1]=0 j=1
printf("%d %d", k++, k++) -> 6 5Sửa một biến nhiều lần trong một câu lệnh
int x = i++ + ++i;
a[j] = j++;
printf("%d %d", k++, k++);
m = m++;
f(i++, i);Một tác dụng phụ cho mỗi câu lệnh
++i;
int x = i + i;
++i;
a[j] = j;
++j;
printf("%d %d", k, k + 1);
k += 2;
++m;
int t = i++;
f(t, i);#Cách C11 phát biểu lại
C11 bỏ hẳn thuật ngữ "điểm tuần tự" và thay bằng một quan hệ thứ tự giữa các phép tính. Cách phát biểu mới chính xác hơn và cần thiết cho mô hình bộ nhớ đa luồng.
| Quan hệ | Nghĩa | Ví dụ |
|---|---|---|
| Sequenced before | A chắc chắn xong trước B | Hai câu lệnh cách nhau dấu chấm phẩy |
| Indeterminately sequenced | Một trong hai xong trước, không biết cái nào, nhưng không chồng lấn | Hai lời gọi hàm trong danh sách đối số |
| Unsequenced | Không có thứ tự nào, có thể xen kẽ nhau | Hai vế của a + b |
#Một quy tắc thay cho mọi luật
Tự làm thử
- Chạy
f(g(), h())với hai hàm có in ra và xác nhận thứ tự trên máy bạn. - Chạy ba biểu thức UB ở mục ba và so kết quả với
-O0và-O2. - Xác nhận
-Wsequence-pointcảnh báo cả ba, và không cảnh báof(g(), h()). - Viết lại mười biểu thức có nhiều tác dụng phụ thành mã một tác dụng phụ mỗi câu.
- Chứng minh
tang() - tang()cho ra kết quả khác nhau khi đổi trình biên dịch hoặc mức tối ưu. - Tìm trong một dự án cũ mọi chỗ có
++hoặc--nằm trong danh sách đối số hàm.
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
- Có điểm tuần tự tại dấu chấm phẩy,
&&,||,?:, toán tử phẩy và trước khi thân hàm chạy. - Không có điểm tuần tự giữa các đối số hàm, giữa hai vế của phép toán, và giữa hai vế của phép gán.
- GCC 8.1 đánh giá đối số từ phải sang trái, nên
printf("%d %d", k++, k++)vớik = 5in ra6 5. Đó là chi tiết cài đặt, không phải quy tắc. - Thứ tự đánh giá đối số là unspecified, còn sửa một biến nhiều lần là undefined. Hai mức rất khác nhau.
- Quy tắc duy nhất cần nhớ: trong một câu lệnh, mỗi biến sửa nhiều nhất một lần và không đọc ở chỗ khác.