1 điểm bởi GN⁺ 19 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • 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án uint8_t có 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()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 switch và 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 pq trỏ 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ánh p == q có 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 int qua một short lvalue thì đó là hành vi không xác định theo quy tắc strict aliasing
    • Con trỏ unsigned char là ngoại lệ, có thể alias bất kỳ đối tượng nào; vì vậy truy cập một đối tượng int qua một unsigned char lvalue là hợp lệ
    • unsigned char được đảm bảo không có padding bit và trap representation; từ C11, signed char cũ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
  • 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 0 tù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ành 0 không đảm bảo rằng (void *)e sẽ 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 e là con trỏ null thì e + 0 cũ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 register và đị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 != x cũng có thể là true hoặc false, và nếu int x là unspecified thì ngay cả sau x *= 0, cũng không có gì đảm bảo x sẽ là 0
    • indeterminate và unspecified value được thảo luận trong DR260, DR451, N1793, N1818, N2012, N2013, N2221 thảo luận
  • unsigned charmemcpy

    • Kiểu unsigned char khô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ủa x phải trở thành specified; theo cách hiểu này, x != x sẽ 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

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 đến UINT_MAX có 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, intlong được truyền bằng một thanh ghi 32 bit, còn long long được truyền bằng hai thanh ghi 64 bit
    • Trên nền tảng mà int là 32 bit, -1 < 0x8000 sẽ là true, còn trên nền tảng mà int là 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ử sizeof trả về một số nguyên unsigned có kiểu size_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) > -1 luôn được đánh giá là false
  • Kiểu của hằng ký tự

    • Trong C, hằng ký tự có kiểu int theo 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 char biểu diễn cùng ký tự đó
  • Phép toán với uint8_t và phép chia

    • Ngay cả khi a, b, c đã được khởi tạo trước khi đọc, giá trị của xz vẫ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 int trướ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=2 thì x sẽ là ((255 + 1) / 2) % 256 = 128
    • Biến trung gian y sẽ là (255 + 1) % 256 = 0, sau đó z sẽ là (0 / 2) % 256 = 0, nên 128 != 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ì xz sẽ 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ì xz cũng sẽ luôn bằng nhau
  • Biến const và variable length array

    • Dù dùng các biến được định tính constnm là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 x sẽ 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ộ y có 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, y sẽ được biên dịch thành một mảng thông thường có 42 phần tử

Khai báo hàm, giá trị trả về, linkage

  • foo()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òn foo(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 float sẽ được nâng cấp thành double
    • 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ên 42 được biểu diễn thành int, vì vậy nếu bar không tương thích với T (*)(int) đối với một kiểu trả về nào đó T thì sẽ là hành vi không xác định
  • 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à int vẫ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ắc int ngầm định
    • Quy tắc int ngầm định đã bị loại bỏ trong C99; phần thảo luận liên quan có trong N661C99 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 return khô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
  • 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 extern theo 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 extern sẽ 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

Qualifier và kiểu chưa hoàn chỉnh

  • const của tham số hàm

    • Trong khai báo hàm, nếu tham số x được gắn qualifier const nhưng trong định nghĩa hàm thì không, và thân hàm có gán giá trị cho x, đ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
  • const của kiểu trả về hàm

    • Nếu chỉ kiểu trả về trong định nghĩa hàm được gắn qualifier const cò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 const củ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 DR423DR481
  • 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 foo là 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
  • External object có kiểu void

    • Khai báo biến kiểu void với internal linkage là không hợp lệ, nhưng khai báo biến kiểu void vớ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 void là 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ượng foo có kiểu void khô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 void thì việc lấy địa chỉ của đối tượng foo là 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
  • Chuyển đổi con trỏ sang const

    • Khi T là kiểu đối tượng suy diễn, phép gán cp là hợp lệ, nhưng việc phép gán cpp có 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

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, &a&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
  • 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ới T
    • Dù tham số x trông như int[42], trên thực tế nó được xử lý như int *
    • Nếu biến cục bộ yint[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

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
    • --*--p cũ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ó
    • --*--p tươ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 -1 cho x[0]
  • 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)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ủa x1 hay 2 cũ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 &&||, 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ành true, sau đó x=2 cũng được đánh giá thành true, nên toàn bộ biểu thức có giá trị true
  • Cấu trúc thân switch linh hoạt

    • Phần thân của câu lệnh switch có thể là một statement bất kỳ, nên cấu trúc trộn lẫn loop và if cũng có thể là hợp lệ
    • Ngay cả nhánh true bên trong câu lệnh if có biểu thức điều khiển luôn là false, nếu có case label 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ến i phải được khởi tạo từ trước
    • case 1 không có break nên xảy ra fall through, nếu case 1 nằm trong nhánh true của if còn case 2 nằm trong nhánh false, thì có thể bỏ qua case 2 và tiếp tục tới case 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

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

    • Nếu dùng GCC hoặc clang vớ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ớn
      Cảnh báo của GCC/clang dạo này khá tốt, và <language-standard> có thể là c89, c99, c11, c23
    • C thì đơn giản, nhưng màn nhào lộn xoay quanh hành vi không xác định thì không hề đơn giản
      Nế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

    • Tham khảo thêm thì ChatGPT, trong trạng thái chưa xem phần giải thích bổ sung sau mỗi đáp án, đúng 22/32 câu
  • 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

    • Tình huống đó xảy ra nếu hai mảng nằm kề nhau trong bộ nhớ, và một con trỏ trỏ tới phần tử đầu của mảng này còn con trỏ kia trỏ tới ngay sau phần tử cuối của mảng kia
      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 hay
    Khó nhằn, nhưng quá trình tự giải trong đầu lại cực kỳ vui