Câu đố về ngôn ngữ lập trình C
(stefansf.de)- Quy tắc của ngôn ngữ C có thể khiến ngay cả đoạn mã trông đơn giản như so sánh con trỏ, aliasing, con trỏ null, hay giá trị chưa khởi tạo cũng trở thành hành vi không xác định
- Hằng số nguyên,
sizeof, hằng ký tự, và phép toánuint8_tcó thể cho kết quả khác nhau tùy theo nền tảng, cách biểu diễn, và vị trí gán trung gian do chọn kiểu và integer promotion foo()vàfoo(void)trong khai báo hàm, việc thiếu prototype, default argument promotion, và hàm không trả về giá trị có sự khác biệt về tính hợp lệ hoặc hành vi giữa C và C++- Mảng không phải là con trỏ; tham số mảng được điều chỉnh thành con trỏ; và
a,&a,&a[0]dù có cùng địa chỉ thì vẫn khác kiểu, nên không thể dùng thay thế cho nhau - Độ ưu tiên toán tử và thứ tự đánh giá là hai việc khác nhau; kể cả cấu trúc thân
switchvà vòng đời temporary object, câu chữ của tiêu chuẩn cũng quyết định kết quả thực thi thực tế
Hành vi không xác định và quy tắc con trỏ
-
So sánh con trỏ và quy tắc strict aliasing
- Ngay cả khi hai con trỏ cùng kiểu
pvàqtrỏ tới cùng một địa chỉ, nếu chúng bắt nguồn từ các đối tượng khác nhau và không phải là một phần của cùng một đối tượng aggregate hoặc union, thì phép so sánhp == qcó thể là hành vi không xác định - Việc con trỏ là một khái niệm trừu tượng hơn là chỉ một địa chỉ số được nói tiếp trong bài viết liên quan
- Nếu truy cập một đối tượng
intqua mộtshortlvalue thì đó là hành vi không xác định theo quy tắc strict aliasing - Con trỏ
unsigned charlà ngoại lệ, có thể alias bất kỳ đối tượng nào; vì vậy truy cập một đối tượngintqua mộtunsigned charlvalue là hợp lệ unsigned charđược đảm bảo không có padding bit và trap representation; từ C11,signed charcũng được đảm bảo không có padding bit- Phân tích aliasing dựa trên kiểu được bàn trong bài viết liên quan
- Ngay cả khi hai con trỏ cùng kiểu
-
Con trỏ null và biểu diễn con trỏ
- Biểu diễn bit của con trỏ null không nhất thiết phải là toàn bộ bit 0
- Tiêu chuẩn C định nghĩa null pointer constant, nhưng không định nghĩa biểu diễn của con trỏ null khi chạy hay biểu diễn của con trỏ nói chung
- Symbolics Lisp Machine 3600 dùng tuple dạng
<array-object, index>thay vì con trỏ số, và biểu diễn con trỏ null là<nil, 0> - Có thêm ví dụ tại clc FAQ 5.17
- Hằng
0tùy theo ngữ cảnh có thể là số nguyên hoặc con trỏ null, còn(void *)0được đánh giá là con trỏ null - Việc biểu thức
eđược đánh giá thành0không đảm bảo rằng(void *)esẽ trở thành con trỏ null - Chỉ khi null pointer constant được chuyển sang kiểu con trỏ thì mới được đảm bảo bằng với con trỏ null
- Phép toán số học trên con trỏ null là hành vi không xác định, nên ngay cả khi
elà con trỏ null thìe + 0cũng không được đảm bảo là con trỏ null
-
Giá trị chưa khởi tạo
- Khi đọc một đối tượng có automatic storage duration chưa được khởi tạo, nếu đối tượng đó có thể thuộc storage class
registervà địa chỉ của nó chưa từng được lấy, thì theo C11 § 6.3.2.1 ¶ 2 đó là hành vi không xác định - Quy tắc này có liên hệ với kiến trúc Intel Itanium được đề cập trong DR338
- Thanh ghi số nguyên thông thường của Itanium có 64 bit và một trap bit; trap bit này là
NaT(not-a-thing), dùng để biểu thị thanh ghi đã được khởi tạo hay chưa - Nếu lấy địa chỉ của biến thì điều kiện trên không còn, nhưng giá trị vẫn là indeterminate và có thể là trap representation hoặc unspecified value
- Đọc trap representation là hành vi không xác định theo C11 § 6.2.6.1 ¶ 5
- Nếu là unspecified value thì ngay cả kết quả của
x != xcũng có thể làtruehoặcfalse, và nếuint xlà unspecified thì ngay cả saux *= 0, cũng không có gì đảm bảoxsẽ là 0 - indeterminate và unspecified value được thảo luận trong DR260, DR451, N1793, N1818, N2012, N2013, N2221 thảo luận
- Khi đọc một đối tượng có automatic storage duration chưa được khởi tạo, nếu đối tượng đó có thể thuộc storage class
-
unsigned charvàmemcpy- Kiểu
unsigned charkhông có trap representation theo C11 § 6.2.6.1 ¶ 3, nên giá trị ban đầu là unspecified - Một câu trả lời từ thành viên ủy ban C trên StackOverflow cho rằng sau khi gọi hàm thư viện chuẩn
memcpy, giá trị củaxphải trở thành specified; theo cách hiểu này,x != xsẽ làfalse - Tuy nhiên, không có cơ sở rõ ràng trong tiêu chuẩn C để hậu thuẫn điều này, và phản hồi của ủy ban trong DR451 nói rằng dùng hàm thư viện với indeterminate value là hành vi không xác định, mâu thuẫn với cách hiểu trên
- Câu hỏi này vẫn còn bỏ ngỏ, và có thêm thảo luận trong Uninitialized Reads
- Kiểu
Hằng số nguyên, phép nâng kiểu, sizeof
-
Cách viết và kiểu của hằng số nguyên
- Hằng số nguyên thập phân không có hậu tố luôn được chọn từ danh sách kiểu signed, nhưng hằng số bát phân và thập lục phân có thể trở thành kiểu signed hoặc unsigned
- Theo C17 § 6.4.4.1, kiểu của hằng số nguyên được xác định là kiểu đầu tiên trong danh sách có thể biểu diễn giá trị đó
- Khi không có hậu tố, hằng số thập phân theo thứ tự
int,long int,long long int, còn hằng số bát phân và thập lục phân theo thứ tựint,unsigned int,long int,unsigned long int,long long int,unsigned long long int - Các hằng số từ
INT_MAX+1đếnUINT_MAXcó thể có kiểu khác nhau tùy là thập phân hay thập lục phân, và điều này có thể tạo ra khác biệt trong mã nhạy cảm với ABI như lời gọi hàm biến đối số - Trong Arm 32-bit architecture ABI,
intvàlongđược truyền bằng một thanh ghi 32 bit, cònlong longđược truyền bằng hai thanh ghi 64 bit - Trên nền tảng mà
intlà 32 bit,-1 < 0x8000sẽ làtrue, còn trên nền tảng màintlà 16 bit thì làfalse, nên có thể phát sinh vấn đề về tính khả chuyển - Sự khác biệt về kiểu hằng số cũng có thể làm thay đổi kết quả trong các biểu thức như generic selection, hàm overload của C++, hoặc
sizeof(0x80000000) == sizeof(2147483648)
-
sizeof(int) > -1- Toán tử
sizeoftrả về một số nguyên unsigned có kiểusize_t - Theo usual arithmetic conversions trong C11 § 6.3.1.8, nếu toán hạng signed có rank thấp hơn toán hạng unsigned thì nó sẽ được chuyển thành kiểu unsigned cùng rank
- Số nguyên signed tương ứng với
-1, khi được chuyển sang unsigned, sẽ trở thành giá trị unsigned lớn nhất của rank đó - Vì vậy,
sizeof(int) > -1luôn được đánh giá làfalse
- Toán tử
-
Kiểu của hằng ký tự
- Trong C, hằng ký tự có kiểu
inttheo C11 § 6.4.4.4 ¶ 10 - Do đó không có gì đảm bảo
sizeof(char) == sizeof('x')luôn làtrue, chỉ cósizeof(int) == sizeof('x')là được đảm bảo - Integer character constant có thể là chuỗi một hoặc nhiều ký tự multibyte, nên
'abc'cũng hợp lệ, và cách biểu diễn của nó là do cách triển khai quyết định - Giá trị của integer character constant chứa một ký tự duy nhất bằng với biểu diễn số nguyên của đối tượng kiểu
charbiểu diễn cùng ký tự đó
- Trong C, hằng ký tự có kiểu
-
Phép toán với
uint8_tvà phép chia- Ngay cả khi
a,b,cđã được khởi tạo trước khi đọc, giá trị củaxvàzvẫn có thể khác nhau do phép nâng kiểu số nguyên và vị trí gán trung gian - Giá trị của từng biến được nâng lên kích thước
inttrước khi thực hiện phép cộng và phép chia, và kết quả của mỗi phép gán sẽ bị truncate để lưu vào kiểu của biến tương ứng - Ví dụ, nếu
a=255,b=1,c=2thìxsẽ là((255 + 1) / 2) % 256 = 128 - Biến trung gian
ysẽ là(255 + 1) % 256 = 0, sau đózsẽ là(0 / 2) % 256 = 0, nên128 != 0 - Overflow của số nguyên unsigned là hành vi được định nghĩa
- Phép toán modulo có tính phân phối đối với phép cộng, nên nếu thay phép chia bằng phép cộng thì
xvàzsẽ luôn bằng nhau - Ngay cả khi đổi phép gán đầu tiên thành
uint8_t x = ((uint8_t)(a + b)) / c;thìxvàzcũng sẽ luôn bằng nhau
- Ngay cả khi
-
Biến
constvà variable length array- Dù dùng các biến được định tính
constlànvàmlàm kích thước mảng, chúng vẫn không phải là integer constant expression trong C - Trong C11 § 6.6 ¶ 6, integer constant expression bị giới hạn ở hằng số nguyên, hằng số liệt kê, hằng ký tự,
sizeof,_Alignof, hoặc toán hạng trực tiếp của cast là hằng số dấu phẩy động nếu kết quả là số nguyên, v.v. - Nếu biểu thức kích thước mảng không phải là integer constant expression thì theo C11 § 6.7.6.2 ¶ 4, nó sẽ trở thành variable length array
- Variable length array không được phép ở file scope, nên compilation unit có mảng toàn cục
xsẽ không biên dịch được - Ở block scope, variable length array được phép, nên compilation unit có mảng cục bộ
ycó thể biên dịch được - Variable length array là một conditional feature mà trình biên dịch có thể không hỗ trợ, nên trên trình biên dịch không hỗ trợ tính năng này, ví dụ ở block scope cũng có thể không biên dịch được
- Trong C++, cả hai compilation unit đều biên dịch được, và vì C++ không có khái niệm variable length array,
ysẽ được biên dịch thành một mảng thông thường có 42 phần tử
- Dù dùng các biến được định tính
Khai báo hàm, giá trị trả về, linkage
-
foo()vàfoo(void)- Khai báo hàm dạng
foo()là khai báo một hàm mà chưa biết số lượng và kiểu đối số, cònfoo(void)là khai báo một nullary function không có đối số - Sự khác biệt này được bàn trong bài viết về khai báo·định nghĩa·prototype hàm
- Khai báo không có danh sách đối số chỉ đưa vào tên hàm và không xác định số lượng hay kiểu đối số, nên có thể là hợp lệ khi kết hợp với định nghĩa hàm phía sau
- Nếu gọi hàm mà không có prototype, default argument promotions sẽ được áp dụng nên
floatsẽ được nâng cấp thànhdouble - Nếu kiểu hàm sau khi nâng cấp không tương thích với kiểu trong định nghĩa hàm thực tế thì tổ hợp giữa khai báo và định nghĩa là không hợp lệ
- Lời gọi hàm không có khai báo trước có thể biên dịch được trong C vì hàm ngầm định được cho phép, nhưng trong C++ thì là lỗi biên dịch
- Nếu gọi như
bar(42)mà không có khai báo, quy tắc nâng cấp đối số số nguyên sẽ được áp dụng nên42được biểu diễn thànhint, vì vậy nếubarkhông tương thích vớiT (*)(int)đối với một kiểu trả về nào đóTthì sẽ là hành vi không xác định
- Khai báo hàm dạng
-
Hàm trả về giá trị nhưng không trả về giá trị nào
- Một hàm có kiểu trả về là
intvẫn có thể hợp lệ trong C dù không trả về giá trị, miễn là giá trị kết quả của lời gọi không được sử dụng - Trong K&R C không có kiểu
void, và nếu lược bỏ kiểu thì kiểu mặc định được giả định làint, vì vậy về mặt lịch sử điều này có liên hệ với các hàm không trả về giá trị và quy tắcintngầm định - Quy tắc
intngầm định đã bị loại bỏ trong C99; phần thảo luận liên quan có trong N661 và C99 rationale - C17 § 6.9.1 ¶ 12 quy định rằng nếu đi tới dấu
}ở cuối hàm và bên gọi sử dụng giá trị của lời gọi hàm thì đó là hành vi không xác định - Trong C++98 § 6.6.3 ¶ 2, việc rơi ra khỏi cuối một hàm trả về giá trị tự thân tương đương với
returnkhông có giá trị, và trong hàm trả về giá trị thì đây là hành vi không xác định - Trình biên dịch C++ nói chung không thể chứng minh được nhánh nào sẽ khiến
abort_program()kết thúc chương trình, nên trong các trường hợp như vậy thường chỉ có chẩn đoán chứ không phải lỗi
- Một hàm có kiểu trả về là
-
linkage và
extern- Nếu trong một phạm vi mà khai báo trước đó đang nhìn thấy được, cùng định danh lại được khai báo bằng
extern, thì linkage của khai báo sau sẽ giống với linkage của khai báo trước - C17 § 6.2.2 ¶ 4 quy định rằng nếu khai báo trước chỉ định internal hoặc external linkage thì khai báo
externtheo sau cũng có cùng linkage đó - Nếu không nhìn thấy khai báo trước, hoặc khai báo trước không có linkage, thì định danh
externsẽ có external linkage - Các tổ hợp khai báo theo thứ tự ngược lại có thể trở thành hành vi không xác định, và GCC cùng Clang có thể phát hiện điều này
- Nếu trong một phạm vi mà khai báo trước đó đang nhìn thấy được, cùng định danh lại được khai báo bằng
Qualifier và kiểu chưa hoàn chỉnh
-
constcủa tham số hàm- Trong khai báo hàm, nếu tham số
xđược gắn qualifierconstnhưng trong định nghĩa hàm thì không, và thân hàm có gán giá trị chox, điều đó vẫn hợp lệ - Theo C11 § 6.7.6.3 ¶ 15, khi xác định tính tương thích kiểu và composite type của kiểu tham số hàm, mỗi tham số được khai báo bằng qualified type sẽ được xem như unqualified version của nó
- Cùng chủ đề này cũng được bàn trong DR040
- Trong khai báo hàm, nếu tham số
-
constcủa kiểu trả về hàm- Nếu chỉ kiểu trả về trong định nghĩa hàm được gắn qualifier
constcòn khai báo thì không, đáp án khó có thể đơn giản kết luận là đúng hay sai - Đồng thuận chung là qualifier của rvalue nên bị bỏ qua, nhưng cách diễn đạt trong tiêu chuẩn cho tới C11 không nói rõ điều này một cách tường minh
- Trong C17, việc phải bỏ qua qualifier của rvalue trong cast, lvalue conversion và function declarator đã trở nên rõ ràng hơn
- C17 § 6.7.6.3 ¶ 5 nêu rõ rằng kiểu mà hàm trả về là unqualified version của
T, và câu chữ này được thêm vào ở C17 - Ngay cả khi qualifier
constcủa kiểu trả về khác nhau, phép gán kiểu hàm vẫn có thể là hợp lệ - Thảo luận thêm có trong DR423 và DR481
- Nếu chỉ kiểu trả về trong định nghĩa hàm được gắn qualifier
-
Struct chưa hoàn chỉnh và biến toàn cục
- Tại thời điểm khai báo biến toàn cục, dù
struct foolà kiểu chưa hoàn chỉnh nên chưa biết kích thước, trong một số trường hợp vẫn được phép nếu kiểu đó được hoàn chỉnh về sau trong cùng translation unit - Lập luận tương tự cũng áp dụng cho biến toàn cục hoặc mảng có kiểu chưa hoàn chỉnh
- Nội dung này cũng được bàn trong DR016
- Tại thời điểm khai báo biến toàn cục, dù
-
External object có kiểu
void- Khai báo biến kiểu
voidvới internal linkage là không hợp lệ, nhưng khai báo biến kiểuvoidvới external linkage thì hợp lệ về mặt cú pháp và không bị cấm một cách tường minh ở đâu trong tiêu chuẩn C11 - Theo C11 § 6.2.5 ¶ 19, kiểu
voidlà một kiểu đối tượng chưa hoàn chỉnh không thể hoàn chỉnh gồm tập rỗng các giá trị - C11 § 6.3.2.1 ¶ 1 định nghĩa lvalue là biểu thức có kiểu đối tượng khác
void, nên tên đối tượngfoocó kiểuvoidkhông phải là một lvalue hợp lệ - Theo C11, rất khó nghĩ ra thao tác nào vừa có ý nghĩa vừa conforming đối với một external object kiểu
void - DR012 đề cập rằng nếu đổi kiểu thành
const voidthì việc lấy địa chỉ của đối tượngfoolà hợp lệ, và điều này trông giống một oversight hơn là tính năng được chủ đích
- Khai báo biến kiểu
-
Chuyển đổi con trỏ sang
const- Khi
Tlà kiểu đối tượng suy diễn, phép gáncplà hợp lệ, nhưng việc phép gáncppcó hợp lệ hay không thì không thể trả lời ngắn gọn - Chủ đề này được bàn trong bài viết về implicit pointer to const conversion
- Khi
Mảng, string literal và điều chỉnh con trỏ
-
Mảng không phải là con trỏ
- Khởi tạo mảng và khởi tạo con trỏ không tương đương nhau
- Dạng thứ nhất khởi tạo một mảng có thể sửa đổi với thời hạn lưu trữ tự động hoặc tĩnh
- Dạng thứ hai khởi tạo một con trỏ trỏ tới một mảng có thời hạn lưu trữ tĩnh, và mảng đó không nhất thiết có thể sửa đổi
- Mảng không phải là con trỏ; chi tiết xem trong bài viết liên quan
-
a,&a,&a[0]- Với
int a[42];,a,&avà&a[0]đều được đánh giá thành địa chỉ của phần tử đầu tiên của mảng - Tuy nhiên, kiểu của ba biểu thức này khác nhau nên không thể dùng thay thế cho nhau
- Chi tiết xem trong bài viết liên quan
- Với
-
Tham số mảng và mảng cục bộ
- Nếu kiểu tham số hàm là “mảng của
T” thì nó sẽ được điều chỉnh thành “con trỏ tớiT” - Dù tham số
xtrông nhưint[42], trên thực tế nó được xử lý nhưint * - Nếu biến cục bộ
ylàint[42]thìsizeof(y)sẽ là42 * sizeof(int) - Nói chung kích thước của con trỏ đối tượng không bằng kích thước của 42 số nguyên, nên
sizeof(x) == sizeof(y)thường làfalse - Chi tiết xem trong bài viết liên quan
- Nếu kiểu tham số hàm là “mảng của
Toán tử, thứ tự đánh giá và luồng điều khiển
-
x+++y- Trong C không thể định nghĩa toán tử mới như C++, vì vậy không có toán tử mới như
+++ x+++yđược diễn giải như sự kết hợp của các toán tử hiện có và tương đương với(x++) + y--*--pcũng không phải là toán tử mới mà là sự kết hợp của các toán tử hiện có--*--ptương đương với--(*(--p)), và trong ví dụ được đánh giá thành-1đồng thời với tác dụng phụ là gán-1chox[0]
- Trong C không thể định nghĩa toán tử mới như C++, vì vậy không có toán tử mới như
-
Thứ tự đánh giá của các toán hạng số học
- Độ ưu tiên toán tử được xác định rõ, nhưng thứ tự đánh giá của các toán hạng số học thì không được xác định
(x=1) + (x=2)là hành vi không xác định vì thứ tự của hai phép gán không được xác định, nên giá trị cuối cùng củaxlà1hay2cũng không được quyết định- Với tùy chọn
-std=c11 -O2, GCC 8.2.1 đánh giá biểu thức ví dụ là4, còn Clang 7.0.0 đánh giá là3
-
Thứ tự đánh giá của các toán tử logic
- Với các toán tử logic
&&và||, thứ tự đánh giá của các toán hạng cũng được xác định rõ - Theo cách diễn đạt của tiêu chuẩn C, tồn tại một sequence point giữa việc đánh giá toán hạng thứ nhất và toán hạng thứ hai
- Trong ví dụ, trước tiên
x=1được đánh giá thànhtrue, sau đóx=2cũng được đánh giá thànhtrue, nên toàn bộ biểu thức có giá trịtrue
- Với các toán tử logic
-
Cấu trúc thân
switchlinh hoạt- Phần thân của câu lệnh
switchcó thể là một statement bất kỳ, nên cấu trúc trộn lẫn loop vàifcũng có thể là hợp lệ - Ngay cả nhánh
truebên trong câu lệnhifcó biểu thức điều khiển luôn làfalse, nếu cócaselabel thì câu lệnh đó vẫn trở thành live, vàprintf("1");không phải là dead code - Khi nhảy tới
case 2, clause-1 và biểu thức điều khiển của loop có thể không được thực thi, nên biếniphải được khởi tạo từ trước - Dù
case 1không cóbreaknên xảy ra fall through, nếucase 1nằm trong nhánhtruecủaifcòncase 2nằm trong nhánhfalse, thì có thể bỏ quacase 2và tiếp tục tớicase 3 - Sau ba lần gọi
foo(0); foo(1); foo(2);, đầu ra trên console sẽ là02313223 - Một ví dụ thực tế nổi tiếng về việc trộn loop và switch là Duff's device
- Phần thân của câu lệnh
Vòng đời đối tượng tạm thời và khác biệt giữa các phiên bản chuẩn C
- Một đoạn mã cụ thể có thể là hành vi không xác định trong C11, nhưng có thể không như vậy trong C99
- Trong C11, vòng đời của một số đối tượng nhất định bị rút ngắn, khiến đối tượng do lời gọi hàm trả về chỉ tồn tại trong lúc vế phải đang được đánh giá
- Trong C99, cùng đối tượng đó tồn tại đến hết enclosing block
- Việc tham chiếu tới một đối tượng đã hết vòng đời là hành vi không xác định theo C11 § 6.2.4 ¶ 2
- Ngay cả trong C99, vòng đời của đối tượng có automatic storage duration cũng gắn với enclosing block gần nhất, nên nếu tham chiếu đối tượng bên ngoài block đó thì cũng là hành vi không xác định
- C11 § 6.2.4 ¶ 8 quy định rằng non-lvalue expression có kiểu struct hoặc union, nếu chứa array member, sẽ tham chiếu tới một đối tượng có automatic storage duration và temporary lifetime
- Vòng đời của đối tượng tạm thời này bắt đầu khi biểu thức được đánh giá, và kết thúc khi việc đánh giá full expression hoặc full declarator bao quanh hoàn tất
- Việc cố gắng sửa đổi một đối tượng có temporary lifetime là hành vi không xác định
- Ví dụ này được lấy từ N1285, nơi cũng có thêm thảo luận liên quan
1 bình luận
Ý kiến trên Lobste.rs
Câu 4 không hợp lệ trong C23, nhưng trước đó thì hợp lệ
Câu 10 không phải đáp án đúng cũng không phải đáp án sai, nên hơi khó chịu nếu gọi là trắc nghiệm
Câu 15 sai về mặt kỹ thuật, đặc biệt khi liên hệ với câu 13, và câu 20 là “không được chỉ định”, nên cũng không có đáp án nào đúng
Câu 30 thì mơ hồ tùy cách đọc
Dù vậy tôi vẫn đúng 27/31 câu, và việc là một người phát triển compiler cũng giúp được phần nào
Giải khoảng bốn câu xong thì cảm giác còn sót lại rằng C đủ đơn giản để dùng cho side project cũng biến mất
clangvới-std=<language-standard>-pedantic -Wall -Wextra, và mỗi khi có cảnh báo thì sửa cho đúng thật sự, đồng thời tránh ép kiểu con trỏ và thao tác con trỏ nhiều nhất có thể, thì có lẽ sẽ tránh được những cái bẫy lớnCảnh báo của GCC/
clangdạo này khá tốt, và <language-standard> có thể là c89, c99, c11, c23Nếu dùng một compiler như tcc, thứ không làm các tối ưu hóa kỳ quặc, thì sẽ bớt gặp những bất ngờ quái lạ hơn
Tôi chỉ chọn theo tiêu chí “ở đây hành vi nào vô lý nhất?” và đúng 21/32 câu
Phần lớn câu sai là vì tôi chưa nghĩ đủ sâu về mức độ vô lý đó
Tôi chỉ từng đụng C một chút từ hơn 15 năm trước, và nhìn bài quiz này xong cũng chẳng khiến tôi muốn quay lại thử nữa
Theo C23 thì đáp án của câu 4 không hợp lệ
Thú vị là tôi đã không dùng C một thời gian mà vẫn đúng 27/32 câu
Đây cũng là lý do tôi luôn dựa vào static analyzer và linter
Ngay từ câu 1 tôi đã thấy có gì đó không ổn
Họ không xét đến việc các con trỏ đó có thể đến từ đâu, và để trường hợp được nói tới ở đó成立 thì cần những điều kiện rất đặc biệt
Trong đa số trường hợp, bản thân việc cố tạo ra con trỏ như thế đã là hành vi không xác định, nhưng dù vậy vẫn có thể xem là công bằng
Câu 3 thật sự làm tôi bất ngờ, lại thêm một cái bẫy của C nữa
Việc literal số nguyên trong C có kiểu được ấn định sẵn vốn đã cực kỳ khó chịu
Quy tắc integer promotion phần nào sửa được chuyện đó, nhưng đồng thời cũng là nguồn gốc gây lỗi
Các ngôn ngữ hiện đại đa phần, hoặc nên là tất cả, phải cấm ép kiểu số ngầm định, suy luận kiểu của literal từ ngữ cảnh nếu có thể, và nếu không thể thì buộc phải ép kiểu tường minh
Sau câu 6 thì tôi bỏ luôn vì không còn tin bài test nữa
Ban đầu là vì đáp án của câu 5 về thực chất được thiết kế để khiến người ta làm sai câu 6, nhưng xem lại thì có vẻ chính câu 6 mới là sai
Phần giải thích nói rằng lời gọi hàm là hành vi không xác định, nhưng câu hỏi lại là định nghĩa hàm có hợp pháp hay không, và có lẽ là hợp pháp thật
Và có vẻ đó cũng không phải là trường hợp hiếm đến vậy
Câu
switch()thật sự rất hayKhó nhằn, nhưng quá trình tự giải trong đầu lại cực kỳ vui