| Contents |

41 Function Specifier, Alignment Specifier/Operator

Theo kinh nghiệm của tôi, mấy thứ này không được dùng nhiều lắm, nhưng cứ trình bày cho đủ.

41.1 Function Specifier

Khi bạn khai báo một hàm, bạn có thể cho compiler vài gợi ý về cách hàm đó có thể hay sẽ được dùng. Điều này cho phép hoặc khuyến khích compiler thực hiện một số tối ưu hoá.

41.1.1 inline để tăng tốc, có lẽ

Bạn có thể khai báo hàm là inline như vầy:

static inline int add(int x, int y) {
    return x + y;
}

Ý nghĩa là khuyến khích compiler làm lời gọi hàm này nhanh nhất có thể. Và trong lịch sử, một cách để làm điều đó là inlining, tức là thân hàm sẽ được nhúng nguyên vẹn tại nơi gọi. Cái này tránh tất cả overhead set up lời gọi hàm và tháo dỡ nó, đổi lại kích thước code lớn hơn vì hàm được copy khắp nơi thay vì tái sử dụng.

Những điều nhanh-gọn cần nhớ:

  1. Bạn có lẽ không cần dùng inline để tăng tốc. Compiler hiện đại biết cái gì tốt nhất.

  2. Nếu bạn dùng nó để tăng tốc, dùng với phạm vi file, tức là static inline. Cái này tránh quy tắc lộn xộn của external linkage và hàm inline.

Đừng đọc phần này nữa.

Kẻ thèm bị trừng phạt hả?

Thử bỏ static đi nào.

#include <stdio.h>

inline int add(int x, int y)
{
    return x + y;
}

int main(void)
{
    printf("%d\n", add(1, 2));
}

gcc báo lỗi linker trên add()232. Spec yêu cầu nếu bạn có một hàm inline không-extern thì bạn cũng phải cung cấp một phiên bản có external linkage.

Nên bạn sẽ phải có phiên bản extern ở đâu đó khác để cái này chạy. Nếu compiler có cả hàm inline trong file hiện tại và phiên bản external của cùng hàm ở nơi khác, nó được chọn gọi cái nào. Nên tôi khuyên mạnh là chúng giống nhau.

Một cách khác bạn có thể làm là khai báo hàm là extern inline. Cái này sẽ thử inline trong cùng file (để tăng tốc), nhưng cũng tạo phiên bản có external linkage.

41.1.2 noreturn_Noreturn

Cái này báo cho compiler rằng một hàm cụ thể sẽ không bao giờ return về chỗ gọi, tức là chương trình sẽ thoát bằng cơ chế nào đó trước khi hàm return.

Nó cho phép compiler có thể thực hiện một số tối ưu quanh lời gọi hàm.

Nó cũng cho phép bạn báo cho các dev khác rằng có logic chương trình phụ thuộc vào một hàm không return.

Có lẽ bạn sẽ không bao giờ cần dùng cái này, nhưng bạn sẽ thấy nó trên một số lời gọi thư viện như exit()233abort()234.

Từ khoá có sẵn là _Noreturn, nhưng nếu nó không làm hỏng code sẵn có của bạn, mọi người đều khuyên include <stdnoreturn.h> và dùng noreturn dễ đọc hơn.

Là hành vi không xác định nếu một hàm được chỉ định là noreturn thực sự return. Cái đó không trung thực về mặt tính toán, thấy đó.

Đây là ví dụ dùng noreturn đúng:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void foo(void) // This function should never return!
{
    printf("Happy days\n");

    exit(1);            // And it doesn't return--it exits here!
}

int main(void)
{
    foo();
}

Nếu compiler phát hiện một hàm noreturn có thể return, nó có thể cảnh báo bạn, hữu ích.

Thay thế hàm foo() bằng cái này:

noreturn void foo(void)
{
    printf("Breakin' the law\n");
}

cho tôi một cảnh báo:

foo.c:7:1: warning: function declared 'noreturn' should not return

41.2 Alignment Specifier và Operator

Alignment235 là về các bội số của địa chỉ mà các đối tượng có thể được lưu. Bạn có thể lưu cái này ở địa chỉ bất kỳ? Hay phải là địa chỉ bắt đầu chia hết cho 2? Hay 8? Hay 16?

Nếu bạn đang code thứ gì đó thấp cấp như bộ cấp phát bộ nhớ giao tiếp với OS, bạn có thể cần nghĩ đến điều này. Phần lớn dev đi hết sự nghiệp mà không dùng chức năng này trong C.

41.2.1 alignas_Alignas

Cái này không phải hàm. Đây là một alignment specifier bạn có thể dùng với một khai báo biến.

Specifier có sẵn là _Alignas, nhưng header <stdalign.h> định nghĩa nó là alignas cho đẹp hơn.

Nếu bạn cần char của mình được căn chỉnh như int, bạn có thể ép như vầy khi khai báo:

char alignas(int) c;

Bạn cũng có thể truyền giá trị hằng hay biểu thức vào làm alignment. Cái này phải là thứ được hệ thống hỗ trợ, nhưng spec ngừng trước chuyện quy định bạn có thể đưa giá trị nào vào. Các luỹ thừa nhỏ của 2 (1, 2, 4, 8, và 16) nhìn chung là đặt cược an toàn.

char alignas(8) c;   // align on 8-byte boundaries

Nếu bạn muốn căn chỉnh ở alignment lớn nhất hệ thống bạn dùng, include <stddef.h> và dùng kiểu max_align_t, như sau:

char alignas(max_align_t) c;

Bạn có thể over-align bằng cách chỉ định alignment lớn hơn của max_align_t, nhưng chuyện đó có được phép hay không phụ thuộc hệ thống.

41.2.2 alignof_Alignof

Toán tử này sẽ trả về bội số địa chỉ mà một kiểu cụ thể dùng cho alignment trên hệ thống này. Ví dụ, có thể char được căn chỉnh mỗi 1 địa chỉ, và int được căn chỉnh mỗi 4 địa chỉ.

Toán tử có sẵn là _Alignof, nhưng header <stdalign.h> định nghĩa nó là alignof nếu bạn muốn trông chất hơn.

Đây là chương trình sẽ in ra alignment của nhiều kiểu khác nhau. Lại nữa, chúng sẽ thay đổi từ hệ thống này sang hệ thống khác. Chú ý kiểu max_align_t sẽ cho bạn alignment lớn nhất hệ thống dùng.

#include <stdalign.h>
#include <stdio.h>     // for printf()
#include <stddef.h>    // for max_align_t

struct t {
    int a;
    char b;
    float c;
};

int main(void)
{
    printf("char       : %zu\n", alignof(char));
    printf("short      : %zu\n", alignof(short));
    printf("int        : %zu\n", alignof(int));
    printf("long       : %zu\n", alignof(long));
    printf("long long  : %zu\n", alignof(long long));
    printf("double     : %zu\n", alignof(double));
    printf("long double: %zu\n", alignof(long double));
    printf("struct t   : %zu\n", alignof(struct t));
    printf("max_align_t: %zu\n", alignof(max_align_t));
}

Output trên hệ của tôi:

char       : 1
short      : 2
int        : 4
long       : 8
long long  : 8
double     : 8
long double: 16
struct t   : 16
max_align_t: 16

41.3 Hàm memalignment()

Mới trong C23!

(Lưu ý: không compiler nào của tôi hỗ trợ hàm này chưa, nên code chủ yếu chưa được test.)

alignof hay nếu bạn biết kiểu dữ liệu. Nhưng nếu bạn ngu dốt đáng thương về kiểu, và chỉ có con trỏ tới dữ liệu?

Sao điều đó xảy ra được?

À, với người bạn tốt void* của ta, tất nhiên. Ta không thể truyền cái đó cho alignof, nhưng nếu ta cần biết alignment của thứ nó trỏ tới?

Ta có thể muốn biết điều này nếu ta sắp dùng bộ nhớ cho thứ gì đó có nhu cầu alignment đáng kể. Ví dụ, kiểu atomic và floating thường hành xử xấu nếu không căn chỉnh đúng.

Với hàm này ta có thể kiểm tra alignment của một số dữ liệu miễn là có con trỏ tới dữ liệu đó, ngay cả khi là void*.

Hãy làm một test nhanh xem một void pointer có được căn chỉnh tốt để dùng như kiểu atomic hay không, và nếu có, lấy một biến dùng như kiểu đó:

void foo(void *p)
{
    if (memalignment(p) >= alignof(atomic int)) {
        atomic int *i = p;
        do_things(i);
    } else
        puts("This pointer is no good as an atomic int\n");

...

Tôi ngờ bạn sẽ hiếm khi (đến mức không bao giờ, có lẽ) cần dùng hàm này trừ khi bạn đang làm thứ gì đó thấp cấp.

Và xong! Alignment!


  1. https://www.ioccc.org/↩︎

  2. https://en.wikipedia.org/wiki/Python_(programming_language)↩︎

  3. https://en.wikipedia.org/wiki/JavaScript↩︎

  4. https://en.wikipedia.org/wiki/Java_(programming_language)↩︎

  5. https://en.wikipedia.org/wiki/Rust_(programming_language)↩︎

  6. https://en.wikipedia.org/wiki/Go_(programming_language)↩︎

  7. https://en.wikipedia.org/wiki/Swift_(programming_language)↩︎

  8. https://en.wikipedia.org/wiki/Objective-C↩︎

  9. https://beej.us/guide/bgclr/↩︎

  10. https://en.wikipedia.org/wiki/ANSI_C↩︎

  11. https://en.wikipedia.org/wiki/POSIX↩︎

  12. https://visualstudio.microsoft.com/vs/community/↩︎

  13. https://docs.microsoft.com/en-us/windows/wsl/install-win10↩︎

  14. https://developer.apple.com/xcode/↩︎

  15. https://beej.us/guide/bgc/↩︎

  16. https://en.cppreference.com/↩︎

  17. https://groups.google.com/g/comp.lang.c↩︎

  18. https://www.reddit.com/r/C_Programming/↩︎

  19. https://en.wikipedia.org/wiki/Assembly_language↩︎

  20. https://en.wikipedia.org/wiki/Bare_machine↩︎

  21. https://en.wikipedia.org/wiki/Operating_system↩︎

  22. https://en.wikipedia.org/wiki/Embedded_system↩︎

  23. https://en.wikipedia.org/wiki/Rust_(programming_language)↩︎

  24. https://en.wikipedia.org/wiki/Grok↩︎

  25. Tôi biết sẽ có người cãi tôi về điểm này, nhưng chắc ít nhất cũng phải nằm trong top ba, đúng không?↩︎

  26. Ờ thì, về mặt kỹ thuật thì nhiều hơn hai, nhưng thôi, ta cứ giả vờ là có hai, càng ít biết càng vui, nhỉ?↩︎

  27. https://en.wikipedia.org/wiki/Assembly_language↩︎

  28. https://en.wikipedia.org/wiki/Machine_code↩︎

  29. Về mặt kỹ thuật, nó chứa các chỉ thị preprocessor và nguyên mẫu hàm (function prototypes, bàn thêm sau) cho các nhu cầu input/output thông dụng.↩︎

  30. https://en.wikipedia.org/wiki/Unix↩︎

  31. Nếu bạn không chỉ định tên file xuất, nó sẽ xuất ra một file tên là a.out theo mặc định, cái tên này có gốc rễ sâu trong lịch sử Unix.↩︎

  32. https://formulae.brew.sh/formula/gcc↩︎

  33. Một “byte” thường là một số nhị phân 8 bit. Cứ coi như một số nguyên chỉ có thể chứa giá trị từ 0 đến 255. Về mặt kỹ thuật, C cho phép byte có số bit bất kỳ, và nếu muốn chỉ rõ ràng một số 8 bit, bạn nên dùng từ octet. Nhưng lập trình viên mặc nhiên hiểu “byte” là 8 bit trừ khi bạn nói rõ khác đi.↩︎

  34. Tôi đang đơn giản hoá cực độ cách bộ nhớ hiện đại hoạt động. Nhưng mô hình tưởng tượng này vẫn dùng được, nên xin thứ lỗi.↩︎

  35. Tôi đang nói dối một chút ở đây. Về kỹ thuật 3.14159 là kiểu double, nhưng ta chưa tới chỗ đó và tôi muốn bạn gắn float với “Floating Point”, và C sẽ vui vẻ ép kiểu đó thành float. Tóm lại, đừng bận tâm tới khi gặp lại sau.↩︎

  36. Đọc là “pointer to a char” hoặc “char pointer” (con trỏ tới char). “Char” là viết tắt của character (ký tự). Dù tôi không tìm thấy nghiên cứu nào, theo quan sát thì đa số đọc là “char”, một thiểu số đọc “car”, và lác đác đọc “care”. Ta sẽ nói kỹ hơn về con trỏ sau.↩︎

  37. Nói chung ta bảo chúng có giá trị “ngẫu nhiên”, nhưng thật ra không phải số ngẫu nhiên thật, thậm chí cũng chẳng phải ngẫu nhiên giả lập.↩︎

  38. Điều này không hoàn toàn đúng 100%. Khi tới phần static storage duration (thời gian lưu trữ tĩnh), bạn sẽ thấy một số biến được tự động khởi tạo về 0. Nhưng cách an toàn là luôn tự khởi tạo.↩︎

  39. Về kỹ thuật chỉ một bit của char được dùng để biểu diễn bool, nên nó chỉ có thể là 0 hoặc 1. Chỉ là các bit còn lại (padding) của char thì không được quy định rõ. Với false, chắc chắn phải toàn 0. Nhưng với true, tôi không chắc là nó phải toàn 0 hay không.↩︎

  40. Chữ _t là viết tắt của type.↩︎

  41. Trừ trường hợp variable length array, nhưng chuyện đó để dịp khác.↩︎

  42. https://beej.us/guide/bgclr/html/split/stdlib.html#man-srand↩︎

  43. Chuyện này được xem là nguy hiểm đến mức những người thiết kế ngôn ngữ Go đã đặt break làm mặc định; bạn phải dùng rõ ràng câu lệnh fallthrough của Go nếu muốn rơi sang case kế.↩︎

  44. Đừng bao giờ nói “không bao giờ”.↩︎

  45. Thông thường. Tôi chắc chắn có ngoại lệ đâu đó trong những hành lang tối tăm của lịch sử điện toán.↩︎

  46. Byte là một số gồm không quá 8 chữ số nhị phân, gọi tắt là bit. Nghĩa là tính theo chữ số thập phân, giống thứ bà của bạn từng dùng, nó có thể chứa một số không dấu từ 0 đến 255, bao gồm cả hai đầu.↩︎

  47. Thứ tự các byte sắp xếp được gọi là endianness của số. Các ứng cử viên quen thuộc là big-endian (byte quan trọng nhất ở đầu) và little-endian (byte quan trọng nhất ở cuối), hoặc, hiếm gặp hơn bây giờ, mixed-endian (byte quan trọng nhất ở đâu đó).↩︎

  48. Tức cơ số 16 với các chữ số 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, và F.↩︎

  49. https://en.wikipedia.org/wiki/Virtual_memory↩︎

  50. Chưa hết! Nó còn dùng trong /*comments*/ và trong phép nhân, và trong function prototype với variable length array! Tất cả cùng là *, nhưng bối cảnh cho nó nghĩa khác nhau.↩︎

  51. https://en.wikipedia.org/wiki/Null_pointer#History↩︎

  52. https://en.wikipedia.org/wiki/Sentinel_value↩︎

  53. Các biến kiểu con trỏ là a, d, f, và i, vì đó là những biến có * đứng trước.↩︎

  54. Ít ra là dạo này.↩︎

  55. Lại nữa, không hẳn, nhưng variable-length array, thứ mà tôi không khoái lắm, là chuyện để dành dịp khác.↩︎

  56. Vì mảng, sâu bên dưới, chỉ là con trỏ tới phần tử đầu tiên, không có thông tin bổ sung nào ghi lại chiều dài.↩︎

  57. Vì khi bạn truyền mảng cho hàm, thật ra bạn chỉ đang truyền một con trỏ tới phần tử đầu tiên của mảng, chứ không phải “toàn bộ” mảng.↩︎

  58. Trong những ngày xưa tốt đẹp của MS-DOS trước khi memory protection ra đời, tôi đang viết một số code C đặc biệt bạo lực, cố tình đâm đầu vào đủ loại undefined behavior. Nhưng tôi biết mình đang làm gì, và mọi thứ vẫn chạy khá tốt. Cho tới khi tôi lỡ bước làm treo máy và, sau khi reboot, phát hiện toàn bộ cài đặt BIOS đã bị xoá sạch. Vui đáo để. (Gửi lời tới @man vì những lúc vui đó.)↩︎

  59. Có rất nhiều thứ gây undefined behavior, không chỉ truy cập mảng vượt biên. Đây chính là thứ làm ngôn ngữ C trở nên sôi động.↩︎

  60. https://en.wikipedia.org/wiki/Row-_and_column-major_order↩︎

  61. Về kỹ thuật thì không chính xác, vì con trỏ tới một mảng và con trỏ tới phần tử đầu tiên của mảng có kiểu khác nhau. Nhưng ta sẽ đốt cầu khi đến đó.↩︎

  62. C11 §6.7.6.2¶1 yêu cầu nó phải lớn hơn không. Nhưng bạn có thể thấy code ngoài đời với mảng chiều dài bằng không ở cuối các struct, và GCC đặc biệt dễ dãi chuyện đó trừ khi bạn biên dịch với -pedantic. Mảng chiều dài bằng không là một cơ chế kiểu hack để tạo struct có chiều dài biến đổi. Không may, về kỹ thuật truy cập mảng như vậy là undefined behavior, dù nó gần như chạy được ở mọi nơi. C99 chuẩn hoá một phương án thay thế được định nghĩa rõ gọi là flexible array members, ta sẽ tán gẫu chuyện đó sau.↩︎

  63. Cách này cũng tương đương: void print_2D_array(int (*a)[3]), nhưng đó là chuyện tôi không muốn đi sâu ngay lúc này.↩︎

  64. Dù đúng là C không theo dõi chiều dài chuỗi.↩︎

  65. Nếu bạn dùng bộ ký tự cơ bản hoặc một bộ ký tự 8 bit, bạn quen với chuyện một ký tự là một byte. Nhưng điều này không đúng với mọi bộ mã ký tự.↩︎

  66. Cái này khác với con trỏ NULL, và tôi sẽ viết tắt nó thành NUL khi nói về ký tự so với NULL cho con trỏ.↩︎

  67. Sau này ta sẽ học cách làm gọn hơn với pointer arithmetic.↩︎

  68. Có một hàm khác tên strncpy() hạn chế số byte được sao chép. Có người nói bạn nên luôn dùng strncpy() vì bảo vệ chống tràn bộ đệm. Người khác nói bạn không bao giờ nên dùng strncpy() vì nó không nhất thiết kết thúc chuỗi của bạn, một cái bẫy tự bắn chân cực kỳ kinh dị khác. Nếu bạn thật sự muốn an toàn, có thể viết phiên bản strncpy() của riêng mình luôn luôn kết thúc chuỗi.↩︎

  69. Mặc dù trong C các mẩu riêng lẻ trong bộ nhớ như int được gọi là “object”, chúng không phải object theo nghĩa lập trình hướng đối tượng.↩︎

  70. Saturn là một hãng xe phổ thông khá được ưa chuộng ở Hoa Kỳ cho tới khi bị đóng cửa vì khủng hoảng 2008, buồn thay với những fan như chúng ta.↩︎

  71. Một con trỏ có lẽ 8 byte trên hệ thống 64 bit.↩︎

  72. Một deep copy đi theo các con trỏ trong struct và chép cả dữ liệu chúng trỏ tới. Một shallow copy chỉ chép các con trỏ, chứ không chép thứ chúng trỏ tới. C không có sẵn chức năng deep copy tích hợp nào.↩︎

  73. https://beej.us/guide/bgclr/html/split/stringref.html#man-strcmp↩︎

  74. https://beej.us/guide/bgclr/html/split/stringref.html#man-memset↩︎

  75. https://stackoverflow.com/questions/141720/how-do-you-compare-structs-for-equality-in-c↩︎

  76. Trước kia ta có ba loại newline dùng rộng rãi: Carriage Return (CR, dùng trên Mac đời cũ), Linefeed (LF, dùng trên hệ Unix), và Carriage Return/Linefeed (CRLF, dùng trên hệ Windows). May mắn là sự xuất hiện của OS X, vốn dựa trên Unix, đã rút con số xuống còn hai.↩︎

  77. Nếu buffer không đủ lớn để đọc hết một dòng, nó sẽ dừng giữa dòng, và lời gọi fgets() kế tiếp sẽ tiếp tục đọc phần còn lại của dòng đó.↩︎

  78. Thông thường chương trình thứ hai sẽ đọc tất cả byte cùng lúc, rồi mới in ra trong vòng lặp. Cách đó hiệu quả hơn. Nhưng ở đây cốt là để demo.↩︎

  79. https://en.wikipedia.org/wiki/Hex_dump↩︎

  80. https://en.wikipedia.org/wiki/Endianess↩︎

  81. Và đây là lý do tôi dùng từng byte riêng trong các ví dụ fwrite()fread() ở trên, khôn đấy chứ.↩︎

  82. https://en.wikipedia.org/wiki/Protocol_buffers↩︎

  83. Ta sẽ nói thêm về chúng sau.↩︎

  84. Nhớ rằng toán tử sizeof cho biết kích cỡ tính bằng byte của một đối tượng trong bộ nhớ.↩︎

  85. Hoặc chuỗi, thực ra là mảng char. Hơi kỳ là bạn cũng có thể có con trỏ tham chiếu tới một chỗ sau phần cuối mảng mà vẫn làm toán với nó được. Chỉ là không được dereference khi nó ở đó.↩︎

  86. https://beej.us/guide/bgclr/html/split/stdlib.html#man-qsort↩︎

  87. https://beej.us/guide/bgclr/html/split/stdlib.html#man-bsearch↩︎

  88. Vì nhớ rằng ký hiệu mảng chỉ là một dereference cộng chút toán tử con trỏ, mà bạn không dereference được void*!↩︎

  89. Bạn cũng có thể cast void* sang kiểu khác, nhưng ta chưa tới cast.↩︎

  90. Hoặc cho đến khi chương trình thoát, lúc đó mọi bộ nhớ được cấp phát sẽ được giải phóng. Dấu sao: một số hệ thống cho phép cấp phát bộ nhớ tồn tại cả sau khi chương trình thoát, nhưng chuyện đó phụ thuộc hệ thống, ngoài phạm vi của sách này, và chắc chắn bạn sẽ không vô tình làm thế.↩︎

  91. http://www.open-std.org/jtc1/sc22/wg14/www/docs/summary.htm#dr_460↩︎

  92. https://en.wikipedia.org/wiki/Bit_bucket↩︎

  93. “Bit” là viết tắt của binary digit (chữ số nhị phân). Nhị phân chỉ là một cách biểu diễn số khác. Thay vì các chữ số 0-9 như ta quen, là các chữ số 0-1.↩︎

  94. https://en.wikipedia.org/wiki/Two%27s_complement↩︎

  95. Thuật ngữ ngành cho một dãy chính xác, không tranh cãi, 8 bit là octet.↩︎

  96. Nói chung, nếu bạn có số two’s complement \(n\) bit, miền signed là \(-2^{n-1}\) tới \(2^{n-1}-1\). Còn miền unsigned là \(0\) tới \(2^n-1\).↩︎

  97. https://en.wikipedia.org/wiki/ASCII↩︎

  98. https://en.wikipedia.org/wiki/List_of_information_system_character_sets↩︎

  99. https://en.wikipedia.org/wiki/Unicode↩︎

  100. Tuỳ vào char mặc định là signed char hay unsigned char↩︎

  101. https://en.wikipedia.org/wiki/Signed_number_representations#Signed_magnitude_representation↩︎

  102. char của tôi là signed.↩︎

  103. https://en.wikipedia.org/wiki/IEEE_754↩︎

  104. Chương trình này chạy như bình luận cho biết trên hệ thống có FLT_DIG6, dùng số dấu phẩy động IEEE-754 base-2. Ngoài ra, bạn có thể có output khác.↩︎

  105. Tôi khá ngạc nhiên là C chưa có cái này trong spec. Trong tài liệu C99 Rationale, họ viết, “A proposal to add binary constants was rejected due to lack of precedent and insufficient utility.” Nghe hơi ngốc khi so với một số tính năng khác họ tống vào! Tôi cược một trong các bản phát hành tới sẽ có.↩︎

  106. https://en.wikipedia.org/wiki/Scientific_notation↩︎

  107. Chúng giống nhau, chỉ khác snprintf() cho phép bạn chỉ định số byte xuất tối đa, tránh tràn cuối chuỗi. Nên an toàn hơn.↩︎

  108. https://en.wikipedia.org/wiki/ASCII↩︎

  109. Ta phải truyền con trỏ tới badchar cho strtoul() chứ không nó không chỉnh được theo cách ta thấy, tương tự lý do bạn phải truyền con trỏ tới int cho hàm nếu muốn hàm đó có thể đổi giá trị của int đó.↩︎

  110. Mỗi ký tự có một giá trị gắn với nó ứng với sơ đồ mã hoá ký tự cụ thể nào đó.↩︎

  111. Trong thực tế, cái nhiều khả năng đang xảy ra trên cài đặt của bạn là các bit bậc cao bị bỏ khỏi kết quả, nên số 16-bit 0x1234 khi chuyển sang số 8-bit sẽ thành 0x0034, hay chỉ 0x34.↩︎

  112. Lần nữa, trong thực tế, cái có khả năng xảy ra trên hệ thống của bạn là mẫu bit của số gốc sẽ bị cắt rồi dùng luôn để biểu diễn số signed, two’s complement. Ví dụ, hệ của tôi lấy một unsigned char 192 và chuyển thành signed char -64. Trong two’s complement, mẫu bit cho cả hai số này là nhị phân 11000000.↩︎

  113. Thật ra không, cứ vứt như thường.↩︎

  114. Hàm có số đối số thay đổi.↩︎

  115. Hiếm làm vì compiler sẽ than phiền và có prototype là Cách làm đúng. Tôi nghĩ cái này vẫn chạy vì lý do lịch sử, trước khi prototype ra đời.↩︎

  116. https://beej.us/guide/bgclr/html/split/ctype.html↩︎

  117. https://gustedt.wordpress.com/2010/08/17/a-common-misconsception-the-register-keyword/↩︎

  118. https://en.wikipedia.org/wiki/Processor_register↩︎

  119. https://en.wikipedia.org/wiki/Boids↩︎

  120. Về mặt lịch sử, chương trình trên MS-DOS và Windows làm chuyện này khác Unix. Ở Unix, shell sẽ mở rộng ký tự đại diện thành mọi file khớp trước khi chương trình của bạn thấy được, còn mấy bản của Microsoft sẽ chuyển cả biểu thức ký tự đại diện vào chương trình để tự xử. Dù sao, vẫn có tham số được chuyển vào chương trình.↩︎

  121. Vì chúng chỉ là tên tham số thông thường, bạn không nhất thiết phải gọi là argcargv. Nhưng đó là idiomatic đến mức nếu bạn sáng tạo, dev C khác sẽ nhìn bạn bằng ánh mắt nghi hoặc thật sự đấy!↩︎

  122. ps, Process Status, là lệnh Unix để xem tiến trình nào đang chạy lúc đó.↩︎

  123. https://en.wikipedia.org/wiki/Inception↩︎

  124. https://en.wikipedia.org/wiki/Shell_(computing)↩︎

  125. Trên Windows cmd.exe, gõ echo %errorlevel%. Trong PowerShell, gõ $LastExitCode.↩︎

  126. Nếu bạn cần giá trị số, chuyển chuỗi bằng thứ như atoi() hay strtol().↩︎

  127. Trong Windows CMD.EXE, dùng set FROTZ=value. Trong PowerShell, dùng $Env:FROTZ=value.↩︎

  128. https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html↩︎

  129. Bạn không thể lúc nào cũng bọc code trong comment /* */ vì chúng không lồng được.↩︎

  130. Đây không hẳn là macro, về kỹ thuật thì nó là một định danh. Nhưng nó là định danh được định nghĩa trước duy nhất và cảm giác rất giống macro, nên tôi để nó ở đây. Kiểu nổi loạn tí.↩︎

  131. Hosted implementation về cơ bản nghĩa là bạn đang chạy chuẩn C đầy đủ, có lẽ trên một hệ điều hành nào đó. Mà chắc là đúng thế. Nếu bạn đang chạy trên phần cứng trần trong kiểu hệ embedded, bạn chắc đang trên standalone implementation.↩︎

  132. Được, tôi biết đó là câu trả lời né tránh. Về cơ bản có một phần mở rộng tùy chọn mà compiler có thể cài, trong đó chúng đồng ý giới hạn vài kiểu undefined behavior để code C dễ làm static code analysis hơn. Ít khả năng bạn cần dùng cái này.↩︎

  133. Breakin’ the law… breakin’ the law…↩︎

  134. https://www.openmp.org/↩︎

  135. Kỹ thuật mà nói, ta gọi nó có incomplete type.↩︎

  136. Dù vài compiler có tùy chọn ép chuyện này xảy ra, tra __attribute__((packed)) để xem cách làm với GCC.↩︎

  137. Nhân tiện, super không phải từ khóa. Tôi chỉ mượn vài thuật ngữ OOP thôi.↩︎

  138. Giả sử char 8 bit, tức là CHAR_BIT == 8.↩︎

  139. https://en.wikipedia.org/wiki/Type_punning↩︎

  140. Tôi bịa con số đó, nhưng chắc không sai lệch bao xa↩︎

  141. Có chút lắt léo với giá trị được lưu chỉ trong thanh ghi, nhưng ở đây ta có thể bỏ qua an toàn. Với lại spec C không có quan điểm gì về mấy chuyện “thanh ghi” đó ngoài từ khóa register, mà phần mô tả của nó cũng không nhắc tới thanh ghi.↩︎

  142. Trên máy bạn rất có thể ra số khác.↩︎

  143. Spec không nói gì chuyện cái này sẽ luôn hoạt động kiểu này, nhưng tình cờ trên máy tôi nó vậy.↩︎

  144. Kể cả khi ENULL, lạ thay.↩︎

  145. https://beej.us/guide/bgclr/html/split/stringref.html#man-memcpy↩︎

  146. Compiler C không bị bắt buộc phải chèn byte padding, và giá trị của bất cứ byte padding nào được chèn là không xác định.↩︎

  147. Cái này sẽ khác nhau tùy kiến trúc, nhưng hệ của tôi little endian, nghĩa là byte nhỏ nhất của số được lưu trước. Hệ big endian sẽ có 12 trước và 78 sau. Nhưng spec không ra lệnh gì về biểu diễn này.↩︎

  148. Đây là tính năng tùy chọn, nên nó có thể không có, nhưng có lẽ có.↩︎

  149. Tôi in hai giá trị 16-bit theo thứ tự đảo vì tôi đang ở máy little-endian và làm vậy dễ đọc hơn ở đây.↩︎

  150. Giả sử chúng trỏ tới cùng một đối tượng mảng.↩︎

  151. Ngôn ngữ Go lấy cảm hứng cú pháp khai báo kiểu từ điều ngược lại với cái C làm.↩︎

  152. Không phải các ngôn ngữ khác không làm vậy, chúng có làm. Thú vị là bao nhiêu ngôn ngữ hiện đại dùng cùng toán tử bitwise như C.↩︎

  153. https://en.wikipedia.org/wiki/Bitwise_operation↩︎

  154. Nghĩa là đám dev thấp cổ bé họng như ta không phải biết trong đó có gì hay ý nghĩa gì. Spec không ra lệnh chi tiết nó là gì.↩︎

  155. Thành thật mà nói, loại bỏ giới hạn này khỏi ngôn ngữ là khả thi, nhưng ý tưởng là các macro va_start(), va_arg()va_end() có thể viết được bằng C. Và để làm được điều đó, ta cần cách nào đó khởi tạo một pointer tới vị trí của tham số đầu. Và để làm điều đó, ta cần tên của tham số đầu. Sẽ cần mở rộng ngôn ngữ để làm được việc này, và tới giờ ủy ban chưa tìm ra lý do chính đáng.↩︎

  156. ” Hành tinh này có, hay đúng hơn, đã có một vấn đề, đó là: phần lớn những người sống trên nó không hạnh phúc gần như suốt cả thời gian. Nhiều giải pháp được đề xuất cho vấn đề này, nhưng phần lớn đều liên quan đến việc di chuyển các tờ giấy xanh nhỏ, kỳ cục là nhìn chung chẳng phải mấy tờ giấy xanh nhỏ không hạnh phúc.” The Hitchhiker’s Guide to the Galaxy, Douglas Adams↩︎

  157. Nhớ là char chỉ là số nguyên cỡ một byte.↩︎

  158. Trừ isdigit()isxdigit().↩︎

  159. Ví dụ, ta có thể lưu code point trong một số nguyên 32-bit big-endian. Thẳng thớm! Ta vừa phát minh ra một encoding! Thật ra thì không; đó là cái encoding UTF-32BE. Chao ôi, quay lại với công việc thôi!↩︎

  160. Kiểu kiểu vậy. Về kỹ thuật, nó có độ rộng thay đổi, có cách biểu diễn code point lớn hơn \(2^{16}\) bằng cách ghép hai ký tự UTF-16 lại.↩︎

  161. Có một ký tự đặc biệt tên Byte Order Mark (BOM), code point 0xFEFF, có thể tuỳ chọn đi trước luồng dữ liệu và cho biết endianness. Tuy nhiên nó không bắt buộc.↩︎

  162. Lại, điều này chỉ đúng trong UTF-16 cho các ký tự vừa trong hai byte.↩︎

  163. https://en.wikipedia.org/wiki/UTF-8↩︎

  164. https://www.youtube.com/watch?v=MijmeoH9LT4↩︎

  165. Có lẽ compiler cố hết sức dịch code point sang encoding output nào đó, nhưng tôi không tìm thấy đảm bảo nào trong spec.↩︎

  166. Với format specifier kiểu "%.12s" chẳng hạn.↩︎

  167. wcscoll()wcsxfrm() rồi theo sau bởi wcscmp().↩︎

  168. Kiểu kiểu vậy, mọi thứ trở nên lạ với encoding UTF-16 multi- char16_t.↩︎

  169. https://en.wikipedia.org/wiki/Iconv↩︎

  170. http://site.icu-project.org/↩︎

  171. https://en.wikipedia.org/wiki/Core_dump↩︎

  172. Hình như Windows không làm signal kiểu Unix ở tầng sâu, và chúng được giả lập cho các app console.↩︎

  173. Gây bối rối là sig_atomic_t có trước lock-free atomic và không phải cùng một thứ.↩︎

  174. Nếu sig_action_t có dấu, range sẽ ít nhất là -127 tới 127. Nếu không dấu, ít nhất 0 tới 255.↩︎

  175. Đây là do VLA thường được cấp phát trên stack, còn biến static nằm trên heap. Và cả ý tưởng của VLA là chúng sẽ được giải phóng tự động khi stack frame bị pop ở cuối hàm.↩︎

  176. https://en.wikipedia.org/wiki/Goto#Criticism↩︎

  177. Tôi muốn nói rõ rằng dùng goto trong tất cả các trường hợp này đều tránh được. Bạn có thể dùng biến và vòng lặp thay thế. Chỉ là có người thấy goto tạo code tốt nhất trong những hoàn cảnh đó.↩︎

  178. https://en.wikipedia.org/wiki/Tail_call↩︎

  179. Cũng không hoàn toàn giống, vì nó là mảng chứ không phải con trỏ tới int.↩︎

  180. Biến dùng ở đây một biểu thức.↩︎

  181. Cả “stack pointer” và “program counter” đều liên quan tới kiến trúc nằm dưới và cài đặt C, và không phải phần của spec.↩︎

  182. Lý lẽ ở đây là chương trình có thể lưu giá trị tạm thời trong CPU register khi nó đang làm việc với giá trị đó. Trong khoảng thời gian đó, register giữ giá trị đúng, và giá trị trên stack có thể đã cũ. Rồi sau đó giá trị register bị ghi đè và các thay đổi đối với biến bị mất.↩︎

  183. Tức là, vẫn được cấp phát tới khi chương trình kết thúc mà không có cách giải phóng.↩︎

  184. Cái này chạy vì trong C, con trỏ có cùng kích thước bất kể kiểu dữ liệu chúng trỏ tới. Nên compiler không cần biết kích thước struct node tại điểm này; nó chỉ cần biết kích thước con trỏ.↩︎

  185. https://en.wikipedia.org/wiki/Complex_number↩︎

  186. Cái này là cái khó research hơn, và tôi sẽ nhận thêm thông tin ai đó cho tôi. I có thể được định nghĩa là _Complex_I hay _Imaginary_I, nếu cái sau tồn tại. _Imaginary_I sẽ xử lý zero có dấu, nhưng _Complex_I có thể không. Cái này có hàm ý với branch cut và các thứ toán-số-phức khác. Có lẽ. Bạn thấy tôi thực sự ra khỏi khu vực chuyên môn chưa? Dù sao, macro CMPLX() hành xử như thể I được định nghĩa là _Imaginary_I, với zero có dấu, kể cả khi _Imaginary_I không tồn tại trên hệ.↩︎

  187. Sự đơn giản của câu này không làm tròn được lượng công sức khủng khiếp đổ vào việc chỉ đơn thuần hiểu dấu chấm động thực sự hoạt động thế nào. https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/↩︎

  188. Đây là cái duy nhất không bắt đầu bằng chữ c thêm đằng trước, lạ thay.↩︎

  189. Một số kiến trúc có dữ liệu kích thước khác mà CPU và RAM có thể thao tác với tốc độ nhanh hơn các kiểu khác. Trong các trường hợp đó, nếu bạn cần số 8-bit nhanh nhất, có thể nó cho bạn kiểu 16- hay 32-bit thay thế vì cái đó chỉ đơn giản là nhanh hơn. Nên với cái này, bạn sẽ không biết kiểu to bao nhiêu, nhưng nó sẽ ít nhất to như bạn nói.↩︎

  190. Cụ thể, hệ có số nguyên 8, 16, 32, hay 64 bit không padding dùng biểu diễn bù 2, trong trường hợp đó biến thể intN_t cho số bit cụ thể đó phải được định nghĩa.↩︎

  191. Trên Trái Đất, dù sao. Ai biết họ dùng hệ điên rồ gì ngoài kia↩︎

  192. OK, đừng giết tôi! GMT về kỹ thuật là múi giờ còn UTC là hệ thời gian toàn cầu. Ngoài ra vài nước có thể chỉnh GMT cho tiết kiệm ánh sáng ban ngày, trong khi UTC không bao giờ được chỉnh cho tiết kiệm ánh sáng ban ngày.↩︎

  193. Thực ra là nhiều hơn hai.↩︎

  194. https://en.wikipedia.org/wiki/Unix_time↩︎

  195. https://beej.us/guide/bgclr/html/split/time.html#man-strftime↩︎

  196. Bạn sẽ làm được trên POSIX, nơi time_t chắc chắn là số nguyên. Không may cả thế giới không phải POSIX, nên vậy đó.↩︎

  197. https://en.wikipedia.org/wiki/POSIX_Threads↩︎

  198. Bản thân tôi thích kiểu shared-nothing hơn, và kỹ năng của tôi với mấy construct đa luồng cổ điển nói nhẹ là đã cũ.↩︎

  199. Đúng, pthreads với “p”. Viết tắt cho POSIX threads, thư viện mà C11 đã vay mượn rất nhiều cho cách hiện thực threads của nó.↩︎

  200. Theo §7.1.4¶5.↩︎

  201. Trừ khi bạn thrd_detach(). Sẽ nói thêm sau.↩︎

  202. Dù tôi không nghĩ chúng phải vậy. Chỉ là threads có vẻ không được reschedule cho đến khi xảy ra system call nào đó như printf()… đó là lý do tôi để printf() trong đó.↩︎

  203. Viết tắt của “mutual exclusion”, cũng gọi là “lock” trên một đoạn code mà chỉ một thread được phép thực thi.↩︎

  204. Tức là process của bạn sẽ đi ngủ.↩︎

  205. Bạn có thể đã nghĩ đó là “thời gian kể từ bây giờ”, nhưng bạn chỉ muốn nghĩ vậy thôi phải không!↩︎

  206. Và đó là lý do gọi là condition variables!↩︎

  207. Tôi không nói là người ngoài hành tinh… nhưng là người ngoài hành tinh. OK, thực tế nhiều khả năng hơn là một thread khác đã được đánh thức và làm được việc trước.↩︎

  208. Sự sống sót của kẻ khoẻ nhất! Đúng không? Tôi thừa nhận thực ra không giống vậy chút nào.↩︎

  209. Macro __STDC_VERSION__ không tồn tại trong C89 đời đầu, nên nếu bạn lo về điều đó, kiểm tra với #ifdef.↩︎

  210. Lý do là khi optimize, compiler của tôi đã đặt giá trị x vào register để làm while loop nhanh. Nhưng register không có cách nào biết biến đã được cập nhật trong thread khác, nên nó không bao giờ thấy 3490. Cái này không thực sự liên quan phần all-or-nothing của atomicity, mà liên quan hơn đến các khía cạnh đồng bộ trong phần tiếp.↩︎

  211. Cho đến khi tôi nói khác, tôi đang nói chung về các thao tác sequentially consistent. Nói thêm ý nghĩa của nó sớm thôi.↩︎

  212. Tỉnh táo nhất từ góc nhìn của lập trình viên.↩︎

  213. Có vẻ C++23 thêm cái này như một macro.↩︎

  214. Spec lưu ý chúng có thể khác về kích thước, biểu diễn, và căn chỉnh.↩︎

  215. Tôi chỉ lấy ví dụ đó từ không khí. Có thể không quan trọng trên Intel/AMD, nhưng có thể quan trọng ở đâu đó đấy!↩︎

  216. C++ nói thêm nếu signal là kết quả của lần gọi raise(), nó tuần tự sau raise().↩︎

  217. https://en.wikipedia.org/wiki/Test-and-set↩︎

  218. Vì consume là về các thao tác phụ thuộc giá trị biến atomic đã acquire, và không có biến atomic với fence.↩︎

  219. https://www.youtube.com/watch?v=A8eCGOqgvH4↩︎

  220. https://www.youtube.com/watch?v=KeLBd2EJLOU↩︎

  221. https://preshing.com/archives/↩︎

  222. https://preshing.com/20120612/an-introduction-to-lock-free-programming/↩︎

  223. https://preshing.com/20120913/acquire-and-release-semantics/↩︎

  224. https://preshing.com/20130702/the-happens-before-relation/↩︎

  225. https://preshing.com/20130823/the-synchronizes-with-relation/↩︎

  226. https://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/↩︎

  227. https://preshing.com/20150402/you-can-do-any-kind-of-atomic-read-modify-write-operation/↩︎

  228. https://en.cppreference.com/w/c/atomic/memory_order↩︎

  229. https://en.cppreference.com/w/c/language/atomic↩︎

  230. https://docs.microsoft.com/en-us/windows/win32/dxtecharts/lockless-programming↩︎

  231. https://www.reddit.com/r/C_Programming/↩︎

  232. Trừ khi bạn compile có bật optimization (có lẽ)! Nhưng tôi nghĩ khi nó làm vậy, nó không tuân spec.↩︎

  233. https://beej.us/guide/bgclr/html/split/stdlib.html#man-exit↩︎

  234. https://beej.us/guide/bgclr/html/split/stdlib.html#man-abort↩︎

  235. https://en.wikipedia.org/wiki/Data_structure_alignment↩︎


| Contents |