C Có Quy Tắc Riêng Của C - nói dối e blog

C Có Quy Tắc Riêng Của C

Gần đây tôi đang phát triển một dự án phần mềm cỡ trung bằng ngôn ngữ C - khoảng dưới 10,000 dòng mã cho module khung giao diện người dùng. Dù biết rõ cả C++ cũng chưa hẳn đã phù hợp với bài toán này, nhưng tôi vẫn cố chấp theo đuổi lựa chọn dùng C để xây dựng nó thật chỉn chu.

Có nhiều ý kiến chỉ trích rằng hệ thống giáo dục lập trình C++ chưa đủ bài bản, dẫn đến việc nhiều lập trình viên C++ vẫn đang viết code theo kiểu C hoặc xem C++ chỉ là phiên bản nâng cấp của C. Nhưng ngược lại, những lập trình viên từng được đào tạo chính quy về C++ thì khi chuyển sang C liệu có đang phạm sai lầm ngược - dùng C như một phiên bản C++ lỗi thời?

Tôi hy vọng mình không thuộc trường hợp đó.

Sau khi tiếp xúc với nhiều ngôn ngữ lập trình khác nhau, tôi ngày càng tin rằng mỗi ngôn ngữ đều có hệ thống luật chơi riêng. C có quy tắc của C, C++ có luật chơi của C++. Càng đi sâu càng thấy rõ sự khác biệt này. C++ không phải là C, và C cũng không phải là C++. Điểm chung lớn nhất giữa chúng chỉ là hệ thống cú pháp tương đồng đến mức có thể dùng chung một bộ compiler.

Trong thế giới C không có hàm khởi tạo/tạm biệt (constructor/destructor), không có bảng hàm ảo (vtable) hay tính kế thừa (inheritance). Từ khóa const được sử dụng linh hoạt hơn, việc ép kiểu (casting) cũng ít bị kiêng kỵ. Không có template nhưng macro lại là công cụ cực mạnh. Giao diện nhị phân ứng dụng (ABI) đơn giản và thống nhất, quy tắc đặt tên tuy dài dòng nhưng hiệu quả.

Trước chuẩn C99, thậm chí chúng ta còn không thể khai báo biến ở bất kỳ đâu trong hàm - điều này chắc chắn khiến nhiều lập trình viên C++ khó chịu vì vi phạm nguyên tắc thiết kế hiện đại. Nhưng chính hạn chế này lại rèn luyện cho lập trình viên C thói quen tốt: giữ cho hàm ngắn gọn, chia nhỏ module logic dù phải dùng đến cấu trúc do{}while(0) “cồng kềnh”.

Tôi từng nghĩ C kém hiệu quả hơn C++ về mặt runtime, nhưng trải nghiệm thực tế lại cho thấy chính những “hạn chế” này khiến tôi phải cẩn trọng hơn trong thiết kế, từ đó tạo ra cấu trúc hợp lý hơn. Trong thế giới C, giá trị mặc định tốt nhất cho mọi cấu trúc dữ liệu là 0 - nhờ đó có thể tận dụng calloc/memset. Cấu trúc dữ liệu nên ở dạng POD (Plain Old Data), hạn chế dùng con trỏ, ưu tiên sắp xếp bộ nhớ liên tục dù đôi khi phải chấp nhận những giải pháp nhìn qua có vẻ tồi tệ theo tiêu chuẩn C++ - nhưng lại giúp việc giải phóng bộ nhớ trở nên đơn giản và không lo quên hủy.

Khi xây dựng framework bằng C, nên giảm tối đa số lượng API công khai. Khác với C++ có thể phơi bày từng nhóm interface, mỗi lần C chỉ nên xuất ra một API duy nhất. Chính nguyên tắc này đã tạo nên hàng loạt thư viện C xuất sắc, dễ sử dụng hơn nhiều so với các thư viện lớp C++. Tuy nhiên nó cũng đặt ra thách thức lớn hơn cho thiết kế, bởi ngay trong thư viện chuẩn của C vẫn tồn tại những API thiết kế thiếu hợp lý.

Chỉ cần lơ là một chút, chúng ta có thể biến dự án C thành cơn ác mộng, trong khi C++ lại “giấu” những thiết kế tồi tệ lâu hơn. Với C, nếu thiết kế tốt sẽ mang lại trải nghiệm tuyệt vời, nhưng chỉ cần cảm thấy bất tiện nhỏ cũng cần重构 (refactor) ngay. Làm việc với C giống như đi trên băng mỏng - đòi hỏi sự cẩn trọng từng bước.

C vẫn luôn đơn giản đến mức vừa đáng yêu vừa đáng ghét. Tôi mất 5 năm để cảm thấy thành thạo C++, nhưng đã 15 năm trôi qua vẫn tự hỏi liệu mình có thể dùng C để xây dựng dự án lớn đến đâu.

0%