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ạng | Tên | Ý nghĩa |
|---|---|---|
| i++ | Tăng hậu tố | Dùng giá trị CŨ, rồi mới tăng |
| ++i | Tă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 |
| --i | Giả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 */#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ệnh | In ra | i sau khi chạy |
|---|---|---|
| printf("%d", i++) | 3 | 4 |
| printf("%d", ++i) | 5 | 5 |
| printf("%d", i--) | 5 | 4 |
| printf("%d", --i) | 3 | 3 |
| printf("%d", i) | 3 | 3 |
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 */a[i] = i++;
printf("%d %d\n", i++, i++);
x = i++ + ++i;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ức | Cách hiểu | Là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ử
- Đ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.
- 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ả. - Biên dịch
i = i++ + ++i;với-Wallvà đọc cảnh báo. - 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. - 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. - 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++ + ++ivàprintf("%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++và++isinh ra mã giống hệt nhau.