Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 4.513 phút đọc

Tăng và giảm

Sau bài này bạn sẽ làm được

  • Phân biệt chính xác dạng tiền tố và dạng hậu tố
  • Chạy tay được một dãy biểu thức tăng giảm
  • Nhận diện các biểu thức rơi vào hành vi không xác định

Hai toán tử này chỉ tăng hoặc giảm một đơn vị, nhưng mỗi cái có hai dạng với ý nghĩa khác nhau. Quan trọng hơn, kết hợp chúng sai cách sẽ tạo ra hành vi không xác định, và đây là loại lỗi mà trình biên dịch thường không cảnh báo.

#Tiền tố và hậu tố

DạngTênÝ nghĩa
i++Tăng hậu tốDùng giá trị CŨ, rồi mới tăng
++iTăng tiền tốTăng TRƯỚC, rồi mới dùng giá trị mới
i--Giảm hậu tốDùng giá trị cũ, rồi mới giảm
--iGiảm tiền tốGiảm trước, rồi mới dùng giá trị mới
int i = 5;
int a = i++;    /* a bằng 5, i bằng 6 */

i = 5;
int b = ++i;    /* b bằng 6, i bằng 6 */
Cách nhớ
Nhìn vị trí của dấu cộng so với tên biến. Dấu cộng đứng trước thì việc tăng xảy ra trước. Dấu cộng đứng sau thì việc tăng xảy ra sau, tức là biểu thức trả về giá trị cũ.

#Chạy tay từng bước

Cách duy nhất để nắm chắc là tự chạy tay. Hãy đoán kết quả trước khi đọc chú thích.

#include <stdio.h>

int main(void)
{
    int i = 3;

    printf("%d\n", i++);   /* in 3, sau đó i thành 4 */
    printf("%d\n", ++i);   /* i thành 5, in 5 */
    printf("%d\n", i--);   /* in 5, sau đó i thành 4 */
    printf("%d\n", --i);   /* i thành 3, in 3 */
    printf("%d\n", i);     /* in 3 */

    return 0;
}
Câu lệnhIn rai sau khi chạy
printf("%d", i++)34
printf("%d", ++i)55
printf("%d", i--)54
printf("%d", --i)33
printf("%d", i)33

Một ví dụ khó hơn

int i = 2;
int a = i++ + i++;

/* Đừng thử tính. Đây là HÀNH VI KHÔNG XÁC ĐỊNH.
   Xem mục tiếp theo. */

#Những biểu thức không được viết

Trong một biểu thức, C không quy định thứ tự đánh giá các phần. Nếu bạn sửa một biến nhiều lần, hoặc vừa đọc vừa sửa nó, kết quả trở nên không xác định.

i = i++ + ++i;                /* hành vi không xác định */
a[i] = i++;                   /* hành vi không xác định */
printf("%d %d\n", i++, i++);  /* hành vi không xác định */
x = ++i + i++;                /* hành vi không xác định */
f(i++, i++);                  /* hành vi không xác định */
Không nên
a[i] = i++;

printf("%d %d\n", i++, i++);

x = i++ + ++i;
Nên
a[i] = i;
i++;

printf("%d ", i);
i++;
printf("%d\n", i);
i++;

x = i + (i + 1);
i += 2;

Những trường hợp AN TOÀN

Có bốn toán tử tạo ra điểm mốc, nghĩa là vế trái chắc chắn hoàn tất trước khi vế phải bắt đầu.

i++ && j++;        /* an toàn: && tạo điểm mốc */
i++ || j++;        /* an toàn */
i++ ? j++ : k++;   /* an toàn */
i++, j++;          /* an toàn: toán tử phẩy */

/* Cũng an toàn vì sửa hai biến khác nhau */
a = i++ + j++;

#Có khác tốc độ không

Câu hỏi kinh điển: nên viết i++ hay ++i trong vòng lặp?

Khóa học này dùng ++i vì đây là thói quen tốt khi sau này bạn làm việc với C++ hoặc với các kiểu do người dùng định nghĩa. Nhưng i++ cũng hoàn toàn ổn, miễn nhất quán.

#Dùng với con trỏ

Toán tử tăng giảm rất hay đi kèm con trỏ, và đây là chỗ độ ưu tiên trở nên quan trọng. Phần này sẽ được đào sâu ở Chương 12, giờ chỉ cần nhận diện được các cách viết.

Biểu thứcCách hiểuLàm gì
*p++*(p++)Lấy giá trị p trỏ tới, rồi dịch con trỏ
(*p)++(*p)++Tăng giá trị mà p trỏ tới
*++p*(++p)Dịch con trỏ trước, rồi lấy giá trị
++*p++(*p)Tăng giá trị trước, rồi lấy giá trị mới

Nguyên nhân là toán tử hậu tố ++ có độ ưu tiên cao hơn toán tử lấy giá trị *. Chi tiết ở bài tiếp theo.

/* Thành ngữ sao chép chuỗi, xuất hiện khắp nơi trong mã C */
char *sao_chep(char *dst, const char *src)
{
    char *d = dst;

    while ((*d++ = *src++)) {
        /* gán ký tự, dịch cả hai con trỏ,
           và dừng khi gặp ký tự kết thúc vì nó bằng 0 */
    }

    return dst;
}

Tự làm thử

  1. Đoán kết quả rồi chạy để kiểm chứng đoạn code năm dòng ở mục chạy tay.
  2. Viết chương trình có printf("%d %d", i++, i++), biên dịch bằng cả GCC và Clang, so sánh kết quả.
  3. Biên dịch i = i++ + ++i; với -Wall và đọc cảnh báo.
  4. Viết hai vòng lặp giống hệt nhau, một dùng i++ một dùng ++i, so sánh assembly sinh ra ở mức -O2.
  5. Cài lại hàm sao_chep ở trên rồi viết một bản khác dùng chỉ số mảng thay vì con trỏ. So sánh độ dài và độ dễ đọc.
  6. Viết hàm đếm số ký tự trong chuỗi chỉ dùng while (*s++).

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

  • Dạng hậu tố trả về giá trị cũ rồi mới tăng, dạng tiền tố tăng trước rồi trả về giá trị mới.
  • Khi đứng riêng làm một câu lệnh, hai dạng hoàn toàn tương đương.
  • Trong một câu lệnh, mỗi biến chỉ được sửa một lần và không vừa đọc vừa sửa.
  • i = i++ + ++iprintf("%d %d", i++, i++) đều là hành vi không xác định.
  • Bốn toán tử tạo điểm mốc an toàn: &&, ||, ?: và toán tử phẩy.
  • Với kiểu cơ bản, i++++i sinh ra mã giống hệt nhau.