Bỏ qua điều hướng, tới nội dung chính
Học C
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ệnhCó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ạyCóMọi đối số đã tính xong
Giữa các đối số hàmKhôngThứ tự tùy trình biên dịch
Hai vế của + - * /KhôngThứ tự tùy trình biên dịch
Hai vế của phép gán =KhôngThứ tự tùy trình biên dịch
Trong khởi tạo mảng và structCó, từ C11Theo 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 5
Sử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ĩaVí dụ
Sequenced beforeA chắc chắn xong trước BHai câu lệnh cách nhau dấu chấm phẩy
Indeterminately sequencedMột trong hai xong trước, không biết cái nào, nhưng không chồng lấnHai lời gọi hàm trong danh sách đối số
UnsequencedKhông có thứ tự nào, có thể xen kẽ nhauHai vế của a + b

#Một quy tắc thay cho mọi luật

Tự làm thử

  1. 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.
  2. Chạy ba biểu thức UB ở mục ba và so kết quả với -O0 và -O2.
  3. Xác nhận -Wsequence-point cảnh báo cả ba, và không cảnh báo f(g(), h()).
  4. 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.
  5. 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.
  6. 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ới k = 5 in ra 6 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.