C là ngôn ngữ lập trình mang tính chất mệnh lệnh, được Dennis Ritchie phát triển vào đầu những năm 1970 để sử dụng trong hệ điều hành UNIX. Từ đó, ngôn ngữ này đã được áp dụng rộng rãi trên nhiều hệ điều hành khác và trở thành một trong những ngôn ngữ phổ biến nhất. C là công cụ hiệu quả và được ưa chuộng để phát triển phần mềm hệ thống, mặc dù nó cũng được sử dụng để xây dựng các ứng dụng. Bên cạnh đó, C cũng thường được dùng làm công cụ giảng dạy trong lĩnh vực khoa học máy tính, mặc dù nó không được thiết kế đặc biệt cho người mới bắt đầu.
Đặc điểm nổi bật
Giới thiệu chung
C là ngôn ngữ lập trình tương đối nhẹ nhàng, hoạt động gần với phần cứng và có nhiều điểm tương đồng với ngôn ngữ Assembler so với các ngôn ngữ bậc cao khác. C còn được coi là có khả năng di động, cho thấy sự khác biệt đáng kể so với ngôn ngữ bậc thấp như Assembler, vì mã nguồn C có thể được biên dịch và chạy trên hầu hết các máy tính, vượt trội hơn so với các ngôn ngữ hiện tại, trong khi Assembler chỉ chạy trên một số máy tính đặc biệt. Do đó, C được xem là ngôn ngữ bậc trung.
C được phát triển với mục tiêu giúp viết các chương trình lớn dễ dàng hơn và giảm thiểu lỗi trong mô hình lập trình thủ tục, đồng thời không gây khó khăn cho các nhà phát triển trình biên dịch C với các yêu cầu phức tạp của ngôn ngữ. Cuối cùng, C đã bổ sung các tính năng sau:
- Ngôn ngữ cốt lõi đơn giản, các chức năng quan trọng như hàm và xử lý tệp được cung cấp bởi các thư viện thủ tục.
- Tập trung vào lập trình thủ tục với các công cụ lập trình cấu trúc.
- Hệ thống kiểu đơn giản loại bỏ nhiều phép toán không thực dụng.
- Sử dụng tiền xử lý để định nghĩa macro và kết hợp nhiều tệp mã nguồn (thông qua lệnh tiền xử lý như
#include
). - Cung cấp mức độ kiểm soát bộ nhớ qua kiểu dữ liệu
pointer
. - Danh sách từ khóa rất ngắn gọn.
- Tham số truyền vào hàm qua giá trị thay vì địa chỉ.
- Con trỏ hàm tạo nền tảng cho tính đóng gói và tính đa hình.
- Hỗ trợ các bản ghi và kiểu dữ liệu kết hợp do người dùng định nghĩa bằng từ khóa
struct
để tổ chức dữ liệu liên quan.
Một số tính năng mà C không cung cấp nhưng có ở các ngôn ngữ khác bao gồm:
- An toàn kiểu,
- Thu dọn rác tự động,
- Các lớp và đối tượng cùng hành vi (OOP),
- Các hàm lồng ghép,
- Lập trình mẫu hay lập trình tổng quát,
- Quá tải và quá tải toán tử,
- Hỗ trợ đa luồng, đa nhiệm và mạng.
Dù còn thiếu nhiều tính năng hữu ích, C vẫn được ưa chuộng vì khả năng tạo ra các trình biên dịch mới nhanh chóng trên nền tảng mới và khả năng kiểm soát chi tiết các hoạt động của chương trình. Điều này thường làm cho mã C chạy hiệu quả hơn so với nhiều ngôn ngữ khác. Chỉ có ngôn ngữ ASM với tối ưu hóa tay mới có thể chạy nhanh hơn C, vì ASM kiểm soát toàn bộ máy. Tuy nhiên, với sự phát triển của trình biên dịch C và sự phức tạp của các CPU hiện đại, sự khác biệt về tốc độ giữa C và các ngôn ngữ khác ngày càng thu hẹp.
Một lý do khác giải thích việc C được áp dụng rộng rãi và hiệu quả là vì nhiều trình biên dịch, thư viện và phần mềm thông dịch của các ngôn ngữ bậc cao khác được viết bằng C.
Ví dụ về 'hello, world'
Dưới đây là một ví dụ đơn giản, được giới thiệu lần đầu trong cuốn 'The C Programming Language', và đã trở thành bài học cơ bản trong nhiều sách giáo khoa lập trình. Chương trình này hiển thị câu 'hello, world!' trên màn hình xuất chuẩn, nhưng cũng có thể xuất ra tệp tin hoặc thiết bị phần cứng, tùy thuộc vào cài đặt đầu ra chuẩn khi chương trình được thực thi.
#include <stdio.h> int main(void) { printf('hello, world!'); return 0; }
Chương trình trên sẽ biên dịch thành công trong hầu hết các trình biên dịch hỗ trợ chuẩn ANSI C hoặc C99.
Dưới đây là phân tích chi tiết từng dòng mã của ví dụ trên
#include <stdio.h>
#include
. Chỉ thị này yêu cầu bộ tiền xử lý (công cụ kiểm tra mã nguồn trước khi biên dịch) thay thế dòng lệnh đó bằng toàn bộ nội dung của tập tin được đề cập (tức là tập tin stdio.h
). Các dấu ngoặc nhọn quanh stdio.h
chỉ ra rằng tập tin này được tìm thấy trong các thư mục đã được cấu hình cho bộ tiền xử lý thông qua đường tìm kiếm đến các tệp header
. Tập tin được bao gồm qua chỉ thị tiền xử lý còn được gọi là tập tin bao gồm.int main(void)
Hàm main
là điểm khởi đầu của chương trình, nơi thực thi bắt đầu. Nó không nhận tham số nào và trả về giá trị kiểu int
.
{
Dấu '{' đánh dấu sự bắt đầu của phần định nghĩa hàm main
.
printf('hello, world\n');
Dòng lệnh này gọi hàm chuẩn printf
, đã được định nghĩa trong tập tin stdio.h
. Hàm này sẽ hiển thị chuỗi hello, world<ký tự xuống dòng EOL>
lên đầu ra chuẩn. Ký tự \n
là một dãy thoát được chuyển đổi thành ký tự EOL (End-Of-Line), có nghĩa là chuyển con trỏ xuống dòng mới. Giá trị trả về của hàm printf
là kiểu int
, nhưng vì giá trị này không được sử dụng nên nó bị bỏ qua lặng lẽ.
return 0;
Dòng lệnh này đánh dấu sự kết thúc của hàm main
và yêu cầu trả về giá trị 0, tương ứng với kiểu dữ liệu số nguyên như đã khai báo trong int main
.
}
Dấu '}' chỉ ra sự kết thúc của phần định nghĩa hàm main
.
Các kiểu dữ liệu
Ngôn ngữ C có hệ thống kiểu dữ liệu tương tự như Pascal, mặc dù có một số điểm khác biệt. C cung cấp nhiều kiểu dữ liệu cho số nguyên với các kích thước khác nhau, cả có dấu và không dấu, kiểu số thực (floating point), ký tự với kiểu char
, kiểu liệt kê enum
, kiểu bản ghi record
, và kiểu liên hợp union
.
Ngôn ngữ C khai thác mạnh mẽ khả năng của các con trỏ pointer
, là các biến đơn giản dùng để lưu trữ địa chỉ bộ nhớ. Con trỏ có thể được tham chiếu ngược (dereference) để truy xuất giá trị tại địa chỉ mà nó trỏ tới. Địa chỉ này có thể được thay đổi qua các phép gán và phép toán số học trên con trỏ. Trong quá trình thực thi, con trỏ đại diện cho một địa chỉ bộ nhớ, trong khi khi biên dịch, nó là một kiểu dữ liệu phức tạp bao gồm cả địa chỉ và kiểu dữ liệu. Điều này cho phép kiểm tra kiểu dữ liệu của các biểu thức liên quan đến con trỏ. Con trỏ được sử dụng cho nhiều mục đích trong C, ví dụ như đại diện cho các chuỗi ký tự string
hay quản lý cấp phát bộ nhớ động.
Một con trỏ rỗng không trỏ đến bất kỳ địa chỉ nào. Điều này hữu ích trong các trường hợp như con trỏ next
của nút cuối trong danh sách liên kết linked list
. Việc tham chiếu ngược một con trỏ rỗng có thể gây ra lỗi không mong muốn. Con trỏ kiểu void
có thể trỏ đến bất kỳ kiểu đối tượng nào mà không cần biết kiểu của đối tượng đó, rất tiện lợi trong lập trình tiêu bản, bởi vì kích thước và kiểu của đối tượng không được biết trước và không thể tham chiếu ngược, nhưng có thể chuyển đổi thành các con trỏ của các kiểu khác.
Trong C, các kiểu mảng array
có kích thước cố định và phải được xác định trong thời gian biên dịch. Điều này có thể gây khó khăn trong thực tế vì người dùng có thể cần cấp phát bộ nhớ tại thời điểm thực thi và sử dụng như các mảng. Khác với các ngôn ngữ khác, C coi mảng như các con trỏ: chúng đóng vai trò là địa chỉ bộ nhớ và kiểu dữ liệu. Vì vậy, chỉ số của mảng có thể vượt quá kích thước của nó.
C cũng hỗ trợ các kiểu mảng đa chiều, trong đó các chỉ số được gán theo thứ tự hàng chính. Mặc dù chúng hoạt động như các mảng của các mảng, nhưng thực chất chúng được phân bố như các mảng một chiều với việc tính toán và định vị các vị trí tương đối.
C thường được sử dụng trong lập trình hệ thống thấp cấp, nơi cần xử lý số nguyên như địa chỉ bộ nhớ, giá trị double precision, hoặc kiểu con trỏ. Trong các trường hợp này, C cung cấp khả năng hoán chuyển, tức là chuyển đổi giá trị giữa các kiểu dữ liệu khác nhau. Tuy nhiên, việc sử dụng hoán chuyển có thể làm giảm tính an toàn kiểu dữ liệu mà hệ thống kiểu thường cung cấp.
Quản lý dữ liệu
Một trong những chức năng quan trọng nhất của ngôn ngữ lập trình là cung cấp nền tảng cho việc quản lý bộ nhớ và các đối tượng trong bộ nhớ. Ngôn ngữ C cung cấp 3 phương pháp để cấp phát bộ nhớ cho các đối tượng:
- Cấp phát bộ nhớ tĩnh: Không gian cho đối tượng được cấp phát trong phần mã nhị phân tại thời điểm biên dịch; các đối tượng này có thời gian tồn tại lâu dài cùng với phần mã nhị phân chứa chúng.
- Cấp phát bộ nhớ tự động: Các đối tượng tạm thời được lưu trong vùng chồng (stack), và không gian này tự động được giải phóng và có thể tái sử dụng sau khi khối mã chứa chúng thực thi xong.
- Cấp phát bộ nhớ động: Các khối bộ nhớ với kích thước mong muốn có thể được yêu cầu trong thời gian thực thi bằng các hàm thư viện như
malloc()
,realloc()
vàfree()
từ khu vực bộ nhớ heap; các khối này có thể được tái sử dụng sau khi gọi hàmfree()
để hoàn trả chúng lại cho bộ nhớ.
Ba phương pháp này phù hợp với các tình huống khác nhau và có những hệ quả khác nhau. Ví dụ, cấp phát tĩnh không cần thời gian tính toán cho việc cấp phát, cấp phát tự động cần một khoảng thời gian cho dự tính, và cấp phát động có thể yêu cầu một lượng lớn thời gian để tính toán cho việc cấp phát và giải phóng bộ nhớ. Ngược lại, không gian của chồng thường bị giới hạn so với bộ nhớ tĩnh hoặc heap, và chỉ cấp phát động cho phép cấp phát cho các đối tượng có kích thước chỉ được biết trong thời gian thực thi. Hầu hết các chương trình C đều sử dụng kết hợp cả ba phương pháp này.
Khi có thể, cấp phát tự động hoặc cấp phát tĩnh thường được khuyến nghị vì bộ nhớ được quản lý bởi trình biên dịch, giúp lập trình viên tránh những rắc rối khi phải yêu cầu và giải phóng bộ nhớ bằng tay. Tuy nhiên, nhiều cấu trúc dữ liệu có thể mở rộng trong thời gian thực thi và vì cấp phát tĩnh và tự động yêu cầu kích thước cố định tại thời điểm biên dịch, nhiều tình huống đòi hỏi phải sử dụng cấp phát động. Các mảng thay đổi kích thước là một ví dụ điển hình của trường hợp này. (Xem ví dụ từ bài malloc về các mảng được cấp phát bộ nhớ động.)
Cú pháp ngôn ngữ
Khác với Fortran, C là một ngôn ngữ linh hoạt, cho phép lập trình viên tự do sử dụng ký tự whitespace
để định dạng mã nguồn. Các chú thích có thể được viết giữa /
và /
hoặc trên từng dòng bắt đầu bằng //
theo sau là nội dung chú thích.
Mỗi tệp mã có thể chứa các khai báo và định nghĩa hàm. Các định nghĩa hàm có thể chứa thêm các khai báo và mệnh đề. Các khai báo có thể định nghĩa các kiểu mới bằng các từ khóa như struct
, union
, và enum
, hoặc gán kiểu và vùng bộ nhớ cho biến mới (ví dụ: char myname = 'ABC'). Các từ khóa như char
và int
cùng với ký hiệu con trỏ là các kiểu dữ liệu có sẵn. Các khối mã được đóng lại giữa các dấu
{
và }
để chỉ ra phần mã mà các khai báo và cấu trúc điều khiển (trong dấu ngoặc) có hiệu lực.
Như một ngôn ngữ mệnh lệnh, C dựa vào các câu lệnh để thực hiện các thao tác. Hầu hết các câu lệnh là câu lệnh biểu thức, đơn giản là để đánh giá các biểu thức—trong quá trình này, các biến nhận giá trị mới hoặc trả giá trị ra ngoài. Các câu lệnh điều khiển cho phép thực thi có điều kiện hoặc lặp lại, được tạo ra với các từ khóa như if
, else
, switch
, do
, while
và for
. Câu lệnh goto
cũng có thể dùng để nhảy đến các phần khác của mã. Nhiều phép toán khác nhau được cung cấp sẵn để thực hiện các phép tính số học, logic, so sánh, kiểu bit, chỉ số mảng và phép gán giá trị. Các biểu thức cũng có thể gọi hàm, bao gồm các hàm thư viện để thực hiện các thao tác chung.
Các vấn đề của ngôn ngữ C
Một câu nói nổi tiếng của Bjarne Stroustrup, người sáng lập C++ và thiết kế trình biên dịch, là 'C makes it easy to shoot yourself in the foot.' (tạm dịch: 'C khiến việc tự làm hại mình trở nên dễ dàng') [1]. Điều này có nghĩa là C cho phép nhiều phép toán không mong muốn xảy ra một cách tổng quát, dẫn đến việc tạo ra nhiều lỗi đơn giản mà không thể được phát hiện qua trình biên dịch hoặc ngay cả trong thời gian thực thi. Điều này gây ra những hành vi không lường trước và các lỗ hổng bảo mật. Một phiên bản của ngôn ngữ C, Cyclone, đã cố gắng khắc phục một số vấn đề này.
Một lý do cho những vấn đề này là việc tránh chi phí cao cho kiểm soát lỗi ở thời gian biên dịch và thực thi. Một lý do khác là yêu cầu duy trì hiệu quả và linh hoạt cao của C. Ngôn ngữ càng mạnh thì càng khó khăn hơn trong việc làm rõ ràng mọi thứ trong các chương trình viết bằng ngôn ngữ đó. Một số kiểm tra dựa vào các công cụ bên ngoài, được thảo luận trong phần Các công cụ kiểm tra tĩnh bên ngoài cho trình biên dịch.
Quản lý bộ nhớ
Một vấn đề với C (và điều này thường gây khó khăn cho những người mới học C) là việc cấp phát bộ nhớ tự động hoặc động
Một vấn đề phổ biến khác là bộ nhớ heap
không thể được tái sử dụng cho đến khi được hoàn trả lại cho bộ nhớ bởi người lập trình qua câu lệnh free()
. Kết quả là, nếu người lập trình quên hoàn trả các vùng bộ nhớ đã cấp phát và tiếp tục yêu cầu cấp phát thêm, thì bộ nhớ bị chiếm dụng ngày càng nhiều. Lỗi này được gọi là memory leak
(rò rỉ bộ nhớ). Ngược lại, nếu trả vùng bộ nhớ quá sớm và tiếp tục sử dụng nó, có thể gây ra việc nhận sai giá trị hoặc tình huống không mong đợi. Máy tính có thể sử dụng vùng nhớ đã trả cho các mục đích khác. Một số ngôn ngữ xử lý vấn đề này bằng cách tự động dọn dẹp bộ nhớ.
Con trỏ
Con trỏ là một nguồn gốc chính của nhiều rủi ro vì chúng không được kiểm tra chặt chẽ. Một con trỏ có thể được tạo ra để chỉ đến bất kỳ đối tượng nào, kể cả mã nhị phân, và khi sử dụng có thể gây ra các hiệu ứng không mong đợi. Dù hầu hết con trỏ thường chỉ tới các vị trí an toàn, chúng vẫn có thể di chuyển tới những chỗ không an toàn khi thực hiện phép toán số học trên con trỏ (như cộng trừ địa chỉ), vùng nhớ mà con trỏ chỉ đến có thể đã được trả lại và tái sử dụng (con trỏ lơ lửng), hoặc chưa được khởi tạo (con trỏ hoang). Chúng cũng có thể được gán giá trị thông qua toán tử chuyển kiểu (cast) hoặc từ con trỏ đã bị hỏng. Một vấn đề khác là C cho phép tự do chuyển đổi giữa các kiểu con trỏ khác nhau. Các ngôn ngữ khác thường hạn chế vấn đề này bằng cách sử dụng các kiểu tham chiếu có giới hạn hơn.
Mảng
C hỗ trợ các mảng tĩnh nhưng không thực hiện kiểm tra tính hợp lệ của chỉ số mảng (kiểm tra biên). Ví dụ, có thể truy cập phần tử thứ sáu của một mảng được định nghĩa với chỉ 5 phần tử, điều này có thể dẫn đến hậu quả không mong muốn. Lỗi này là lỗi tràn bộ nhớ đệm, và là nguyên nhân của nhiều lỗ hổng bảo mật trong các chương trình viết bằng C. Ngoài ra, vào thời điểm C ra đời, kỹ thuật kiểm tra biên còn hạn chế, nên việc thực hiện kiểm tra biên có thể ảnh hưởng lớn đến tốc độ thực thi, đặc biệt trong các tính toán số học.
Các mảng đa chiều rất quan trọng trong việc cài đặt các thuật toán số (như đại số tuyến tính) để lưu trữ các ma trận. Tuy nhiên, cấu trúc mảng của C không đáp ứng tốt cho thao tác này. Vấn đề này đã được thảo luận trong sách Numerical Recipes in C, chương 1.2, trang 20 ff (có thể đọc trực tuyến). Sách này cung cấp giải pháp tốt cho vấn đề này mà bạn có thể áp dụng xuyên suốt cuốn sách.
Các hàm tham số thay đổi
Một vấn đề phổ biến khác liên quan đến các hàm với tham số thay đổi (variadic function), tức là những hàm có thể nhận số lượng tham số không cố định. Khác với các kiểu hàm khác trong C, việc kiểm tra số lượng tham số tại thời điểm biên dịch không phải là bắt buộc theo tiêu chuẩn, và thông thường là không thể thực hiện kiểm tra nếu không có thông tin bổ sung. Khi dữ liệu không đúng kiểu được truyền vào, hậu quả có thể rất nghiêm trọng và thường dẫn đến lỗi nghiêm trọng. Các hàm tham số thay đổi cũng có thể xử lý các hằng số con trỏ rỗng theo cách không thể dự đoán.
- Ví dụ: Hàm printf từ thư viện chuẩn, được dùng để định dạng các chuỗi xuất ra, nổi tiếng với những lỗi trong việc quản lý tham số thay đổi của nó; nó dựa vào định dạng chuỗi ký tự để chỉ định số lượng và kiểu của các tham số đi kèm.
Dù việc kiểm tra kiểu của các hàm với tham số thay đổi trong thư viện chuẩn là một vấn đề về chất lượng thiết lập, nhiều trình biên dịch hiện đại thực hiện kiểm tra kiểu khi gọi printf
và đưa ra cảnh báo nếu danh sách tham số không khớp với chuỗi định dạng. Tuy nhiên, không phải tất cả các cuộc gọi printf
đều có thể được kiểm tra tĩnh, vì chuỗi định dạng có thể chỉ được tạo ra tại thời điểm thực thi, khi các hàm tham số thay đổi thường vẫn không được kiểm tra.
Cú pháp
Cú pháp của C chứa nhiều điểm yếu. Đặc biệt là:
- Nguyên mẫu hàm không chỉ rõ tham số sẽ mặc định cho phép bất kỳ số lượng tham số nào. Một vấn đề cú pháp nổi lên liên quan đến sự tương thích ngược của C K&R, liên quan đến việc thiếu các nguyên mẫu.
- Có nhiều lựa chọn đáng nghi ngờ về thứ tự ưu tiên của các toán tử, chẳng hạn như
==
có mức ưu tiên cao hơn&
và|
trong các biểu thức nhưx & 1 == 0
. - Việc sử dụng toán tử '=' có thể gây nhầm lẫn. Khi dùng trong các phép so sánh toán học để chỉ định phép gán, điều này có thể dẫn đến các phép gán không mong muốn trong so sánh và tạo ra ấn tượng sai lầm rằng phép gán có tính chất bắc cầu. Ví dụ: câu lệnh
if (x=0) {...}
dễ gây ra lỗi không mong muốn. - Thiếu các toán tử infix cho các đối tượng phức tạp, đặc biệt là cho các phép toán trên chuỗi ký tự, làm cho chương trình phụ thuộc vào các phép toán khó đọc.
- Quá phụ thuộc vào hệ thống ký hiệu làm cơ sở cho cú pháp ngay cả trong các trường hợp không rõ ràng như '&&' và '||' thay vì sử dụng 'and' và 'or'.
- Cú pháp khai báo không dễ hiểu, đặc biệt đối với hàm con trỏ. Trong C++, nhà nghiên cứu Damian Conway mô tả cú pháp khai báo như sau:
- Khó để chỉ định một kiểu trong C++ vì một số phần của khai báo (như con trỏ) là các toán tử tiền tố trong khi một số khác (như mảng) lại là toán tử hậu tố (nghĩa là
*
phải đứng trước tên con trỏ và[]
sau tên mảng—người dịch). Những toán tử khai báo này có các thứ tự ưu tiên khác nhau và cần được đặt trong dấu ngoặc một cách cẩn thận để đạt được khai báo mong muốn. - Ben Werther & Damian Conway.
- Khó để chỉ định một kiểu trong C++ vì một số phần của khai báo (như con trỏ) là các toán tử tiền tố trong khi một số khác (như mảng) lại là toán tử hậu tố (nghĩa là
A Modest Proposal: C++ Resyntaxed. Mục 3.1.1. 1996.
Các vấn đề liên quan đến bảo trì
Ngoài các lỗi trực tiếp, còn có những vấn đề khác trong C gây khó khăn cho việc xây dựng hệ thống lớn có thể bảo trì và ổn định. Một số ví dụ điển hình bao gồm:
- Hệ thống trở nên phân tán do các chỉ thị nhập (
#include
) dựa vào các dòng chữ phân tán không đồng nhất trong các tập tin, nhằm duy trì sự đồng bộ của các nguyên mẫu và định nghĩa. Điều này làm tăng đáng kể thời gian biên dịch phần mềm. - Mô hình biên dịch rối rắm. Yêu cầu phải theo dõi các phụ thuộc mã một cách thủ công và ngăn cản việc tối ưu hóa giữa các mô-đun (ngoài việc tối ưu hóa thời gian liên kết).
- Hệ thống kiểu yếu dẫn đến việc chương trình có lỗi rõ ràng nhưng vẫn được biên dịch mà không bị phát hiện lỗi.
Các công cụ kiểm tra tĩnh cho trình biên dịch
Nhiều công cụ đã được phát triển để hỗ trợ lập trình viên C tránh lỗi. Việc kiểm tra và phân tích mã nguồn tự động rất hiệu quả trong tất cả các ngôn ngữ. Ví dụ cho C là Lint, công cụ này giúp phát hiện các vấn đề mã khi chương trình được viết lần đầu. Khi chương trình đã được kiểm tra bằng Lint, nó sẽ được biên dịch bởi trình biên dịch C. Cũng có thư viện để kiểm tra biên của mảng và một dạng hạn chế của dọn rác tự động, nhưng không phải là một phần tiêu chuẩn của C.
Cần lưu ý rằng các công cụ này không phải là hoàn hảo. Do tính linh hoạt của C, nhiều lỗi như sử dụng hàm tham lượng động sai cách, truy cập chỉ số ngoài biên của mảng và quản lý bộ nhớ không chính xác không thể được phát hiện. Tuy nhiên, nhiều lỗi phổ biến vẫn có thể được nhận diện.
Lịch sử
Những bước phát triển đầu tiên
Những bước phát triển đầu tiên của ngôn ngữ C diễn ra tại AT&T Bell Labs từ năm 1969 đến 1973, với thời điểm sáng tạo đỉnh cao vào năm 1972 theo lời Ritchie. Ngôn ngữ này được gọi là C vì nó kế thừa nhiều đặc điểm từ ngôn ngữ B trước đó.
Ngoài ra, có những điểm khác biệt so với ngôn ngữ gốc 'B': Ken Thompson đã đề cập đến ngôn ngữ lập trình BCPL, nhưng ông cũng đã tạo ra ngôn ngữ Bon để vinh danh vợ mình.
Có nhiều câu chuyện về nguồn gốc của C và hệ điều hành Unix liên quan, bao gồm:
- Phát triển ngôn ngữ C bắt nguồn từ việc các lập trình viên muốn chơi trò chơi Space TravelLưu trữ 2014-08-06 tại Wayback Machine. Họ chơi trò này trên mainframe của công ty, nhưng gặp khó khăn vì không đủ khả năng chạy trò chơi cho khoảng 100 người dùng và không đủ kiểm soát tàu vũ trụ để tránh va chạm với các thiên thạch. Do đó, họ quyết định chuyển trò chơi sang máy PDP-7 không thuộc văn phòng. Tuy nhiên, máy này không có hệ điều hành, vì vậy họ đã viết một hệ điều hành. Tiếp theo, họ muốn chuyển hệ điều hành này sang PDP-11 trong văn phòng, nhưng việc này gặp khó khăn vì mã nguồn hoàn toàn bằng Assembly. Họ quyết định sử dụng một ngôn ngữ cấp cao hơn để dễ dàng chuyển hệ điều hành giữa các máy tính. Họ đã chọn ngôn ngữ B nhưng thấy nó thiếu các chức năng cần thiết cho PDP-11, vì vậy họ đã tạo ra ngôn ngữ C mới.
- Unix ban đầu được phát triển để tạo ra một hệ thống tự động quản lý hồ sơ bằng sáng chế. Phiên bản đầu tiên của Unix được viết bằng ngôn ngữ Assembly, và sau đó ngôn ngữ C đã được phát triển để thay thế hệ điều hành này.
Đến năm 1973, ngôn ngữ C đã đủ mạnh mẽ để được sử dụng viết nhân hệ điều hành Unix, thay vì trước đó hệ điều hành này được viết bằng Assembly trên các máy PDP-11/20. Đây là lần đầu tiên một hệ điều hành được xây dựng bằng một ngôn ngữ khác ngoài Assembly.
K&R C
Vào năm 1978, Ritchie và Brian Kernighan đã phát hành cuốn sách The C Programming Language lần đầu tiên. Cuốn sách này, được các lập trình viên biết đến với tên gọi 'K&R', đã được sử dụng trong nhiều năm như một đặc tả không chính thức cho ngôn ngữ C. Phiên bản C mà cuốn sách đề cập thường được gọi là 'K&R C'. (Phiên bản tái bản của cuốn sách cũng bao gồm chuẩn ANSI C).
K&R đã giới thiệu các tính năng sau:
- Kiểu dữ liệu
struct
- Kiểu dữ liệu
long int
- Kiểu dữ liệu
unsigned int
- Toán tử
=+
đã được thay đổi thành+=
, và các toán tử tương tự cũng được điều chỉnh để tránh gây nhầm lẫn cho bộ phân tích từ vựng của trình biên dịch C (ví dụ: sự tương đồng dễ gây nhầm lẫn giữa hai câu lệnhi =+ 10
vài = +10
).
K&R C thường được coi là nền tảng cơ bản mà bất kỳ trình biên dịch C nào cũng cần phải hỗ trợ. Ngay cả sau khi ANSI C ra đời, K&R C vẫn được xem là 'cơ sở tối thiểu' mà các lập trình viên C cần tuân thủ nếu muốn đảm bảo tính tương thích trên nhiều nền tảng, bởi vì không phải tất cả các trình biên dịch đều hỗ trợ đầy đủ ANSI C, và mã viết theo K&R C cũng hợp lệ trong ANSI C.
Trong các phiên bản cũ của C, chỉ những hàm trả về kiểu dữ liệu không phải số nguyên mới cần phải được khai báo trước khi sử dụng. Một hàm được gọi mà không có khai báo trước sẽ được mặc định là trả về kiểu số nguyên.
Ví dụ về việc gọi hàm cần khai báo trước:
long int SomeFunction(); int CallingFunction() { long int ret; ret = SomeFunction(); }
Ví dụ về việc gọi hàm mà không cần khai báo trước:
int CallingFunction() { int ret; ret = SomeOtherFunction(); } int SomeOtherFunction() { return 0; }
Do nguyên mẫu của K&R không cung cấp thông tin về các tham số của hàm, việc kiểm tra kiểu đối số không được thực hiện. Tuy nhiên, một số trình biên dịch có thể đưa ra cảnh báo nếu hàm được gọi với số lượng tham số không chính xác.
Trong những năm sau đó, K&R C đã được mở rộng với nhiều tính năng 'không chính thức' được hỗ trợ bởi các trình biên dịch của AT&T và các nguồn khác. Các tính năng này bao gồm:
- Hàm với kiểu
void
và con trỏ kiểuvoid *
. - Hàm trả về kiểu
struct
hoặcunion
. - Tên miền trong không gian tên cho mỗi kiểu
struct
. - Phép gán cho kiểu dữ liệu
struct
. - Hằng
const
được coi là đối tượng chỉ đọc. - Thư viện chuẩn được phát triển nhờ sự hợp tác của nhiều nhà sản xuất.
- Các kiểu enumeration.
- Kiểu số thực đơn
float
.
ANSI C và ISO C
Cuối những năm 1970, C bắt đầu thay thế BASIC như một ngôn ngữ lập trình chính cho các máy tính vi tính. Trong suốt thập niên 1980, nó được chấp nhận rộng rãi trên IBM PC, và sự phổ biến của nó tăng trưởng mạnh mẽ.
Cùng thời điểm đó, Bjarne Stroustrup và nhóm của ông tại Bell Labs đã thêm các tính năng lập trình hướng đối tượng vào ngôn ngữ C.
Ngôn ngữ mới họ phát triển, gọi là C++, đã trở thành ngôn ngữ lập trình chính cho các ứng dụng trên hệ điều hành Microsoft Windows; trong khi đó, C vẫn giữ được sự phổ biến lớn trong thế giới UNIX. Một ngôn ngữ khác phát triển trong khoảng thời gian đó là Objective-C, cũng là một phiên bản mở rộng hướng đối tượng của C. Dù không nổi bật như C++, nó vẫn được sử dụng để phát triển các ứng dụng Cocoa trên Mac OS X.
Năm 1983, Viện Tiêu chuẩn Quốc gia Hoa Kỳ (ANSI) thành lập ủy ban X3J11 để phát triển một tiêu chuẩn cho ngôn ngữ C. Sau một quá trình dài và khó khăn, tiêu chuẩn hoàn thành vào năm 1989 và được công nhận với tên 'Programming Language C' ANSI X3.159-1989. Phiên bản này thường được gọi là ANSI C.
Năm 1990, tiêu chuẩn ANSI C (với một số điều chỉnh nhỏ) đã được chuẩn hóa bởi Tổ chức Quốc tế về Tiêu chuẩn hóa (ISO) dưới tên ISO/IEC 9899:1990.
Một lợi ích lớn của việc tiêu chuẩn hóa ANSI C là đã làm cho K&R C trở thành một tập con của nó; nhiều tính năng không chính thức của K&R C đã được đưa vào tiêu chuẩn. Thêm vào đó, hội đồng tiêu chuẩn đã mở rộng ANSI C với nhiều tính năng mới, chẳng hạn như nguyên mẫu hàm (được mượn từ C++) và khả năng tiền xử lý mạnh mẽ hơn.
Hiện nay, hầu hết các trình dịch đều hỗ trợ ANSI C. Phần lớn mã nguồn C ngày nay được viết theo tiêu chuẩn ANSI C. Các chương trình viết hoàn toàn theo chuẩn C sẽ đảm bảo hoạt động chính xác trên bất kỳ nền tảng nào hỗ trợ C. Tuy nhiên, nhiều chương trình chỉ có thể được biên dịch trên một số nền tảng hoặc trình dịch nhất định do các lý do sau đây:
- Sử dụng các thư viện không chuẩn, chẳng hạn như thư viện GUI.
- Một số trình dịch không hoàn toàn tuân thủ chuẩn ANSI C hoặc các tiêu chuẩn sau đó trong chế độ mặc định của chúng.
- Phụ thuộc vào kích thước của các kiểu dữ liệu và endian của nền tảng. (Ví dụ, kích thước của kiểu
int
có thể khác nhau—4, 8, hoặc 16 byte—trên các nền tảng khác nhau.)
Macro STDC
có thể được sử dụng để phân chia mã nguồn thành các phần dành cho ANSI C và K&R
#if STDC extern int getopt(int,char const ,const char *); #else extern int getopt(); #endif
Một số chuyên gia khuyên dùng #if STDC
thay vì #ifdef STDC
, vì một số trình dịch có thể thiết lập giá trị STDC
là 0 để chỉ rằng không tuân thủ chuẩn ANSI (trong khi các trình dịch khác có thể thiết lập giá trị khác 0).
C99
Sau khi chuẩn hóa ANSI, đặc tả của ngôn ngữ C không thay đổi nhiều trong một thời gian, trong khi C++ tiếp tục phát triển. (Thực tế, đã có một bản sửa đổi vào năm 1995 tạo ra phiên bản mới của C, nhưng phiên bản này ít được chấp nhận.) Đến cuối thập niên 1990, một tiêu chuẩn mới được công bố là ISO 9899:1999, thường được gọi là 'C99'. Tiêu chuẩn này đã được công nhận vào tháng 3 năm 2000.
Các tính năng mới trong C99 bao gồm:
- Các hàm
inline
. - Các biến có thể được khai báo ở bất kỳ vị trí nào trong khối lệnh (như trong C++).
- Thêm nhiều kiểu dữ liệu mới như kiểu
long long int
(để hỗ trợ việc chuyển đổi từ hệ 32-bit sang 64-bit), kiểu boolean và kiểucomplex
cho số phức. - Các mảng với kích thước thay đổi.
- Hỗ trợ chú thích trên một dòng bắt đầu với
//
, như trong C++ và nhiều ngôn ngữ khác. - Các hàm thư viện mới như
snprintf()
. - Các tập tin tiêu chuẩn mới như
stdint.h
.
Việc hỗ trợ chuẩn C99 có kết quả không đồng nhất. Mặc dù GCC và nhiều trình dịch khác đã hỗ trợ hầu hết các tính năng của C99, trình dịch của Microsoft và Borland lại không tuân thủ và dường như không có ý định bổ sung hỗ trợ cho các tính năng này.
Quan hệ với C++
Bjarne Stroustrup, người sáng lập C++, đã nhấn mạnh rằng sự không tương thích giữa C và C++ nên được giảm bớt để đảm bảo tính tương thích tối đa giữa hai ngôn ngữ. Một số ý kiến cho rằng, mặc dù sự tương thích giữa C và C++ là hữu ích, nhưng không phải là điều cần thiết. Theo quan điểm này, nỗ lực giảm sự không tương thích không nên làm giảm sự cải thiện và phát triển của mỗi ngôn ngữ theo cách riêng của chúng.
Hiện nay, những khác biệt cơ bản giữa C và C++ (không tính đến các mở rộng của C++ như lớp, mẫu, không gian tên, và quá tải) là:
inline
— trong C++, các hàm inline có hiệu lực toàn cục, trong khi trong C chỉ có hiệu lực trong phạm vi tập tin.- Từ khóa
bool
trong C99 có tập tin tiêu đề riêng là
. Các phiên bản C trước không định nghĩa kiểuboolean
, và nhiều phương pháp không chuẩn đã được sử dụng để mô phỏng kiểu boolean. - Các ký tự hằng (đặt trong dấu
'
) có kích thước của kiểuint
trong C và kích thước của kiểuchar
trong C++. Tuy nhiên, trong C, các ký tự này sẽ không bao giờ vượt quá giá trị của kiểuchar
, nên việc chuyển kiểu(char)'a'
là hoàn toàn an toàn. - Các từ khóa mới trong C++ không thể được dùng làm tên biến trong C như trước đây. (Ví dụ:
try
,catch
,template
,new
,delete
,...). - Trong C++, trình dịch tự động tạo ra một 'thẻ' cho mỗi
struct
,union
, hayenum
, do đó,struct S {};
trong C++ tương đương vớitypedef struct S {} S;
trong C.
C99 đã tiếp nhận một số tính năng đã xuất hiện đầu tiên trong C++. Các tính năng này bao gồm:
- Khai báo nguyên mẫu của hàm.
- Thêm từ khóa
inline
. - Loại bỏ 'hiểu ngầm' về kiểu trả về là int.
Ngôn ngữ trung gian
C được sử dụng như một ngôn ngữ trung gian vì nó có thể biên dịch ra dạng tập tin đối tượng hoặc ngôn ngữ máy. Điều này làm cho C dễ dàng để vận chuyển và tối ưu hóa. Các trình dịch C thường có sẵn cho nhiều loại CPU và hệ điều hành, và hầu hết đều có khả năng tạo ra tập tin *.obj cũng như mã máy được tối ưu hóa. Do đó, mã nguồn C trở nên dễ vận chuyển và có thể được sử dụng dưới dạng *.obj hoặc mã máy đã được tối ưu hóa. Tuy nhiên, dù C được thiết kế như một ngôn ngữ lập trình, nó không hoàn toàn lý tưởng cho việc sử dụng như một ngôn ngữ trung gian, dẫn đến sự phát triển của các ngôn ngữ trung gian dựa trên C, chẳng hạn như C--.
Các trình biên dịch quan trọng
Hiện nay, các trình biên dịch C thường được cung cấp cùng với C++ và đôi khi cả trình biên dịch cho ngôn ngữ Assembly. Các bộ công cụ biên dịch phổ biến trên thị trường thường đi kèm với nhiều công cụ hỗ trợ lập trình như IDE, trình gỡ lỗi, và nhiều tiện ích khác.
Dưới đây là một số trình biên dịch nổi bật:
- GCC là trình biên dịch miễn phí hoàn toàn theo giấy phép GNU, bao gồm trình biên dịch cho nhiều ngôn ngữ như C/C++ và Fortran. Đây là lựa chọn chính cho các hệ điều hành Linux và hỗ trợ hầu hết các tiêu chuẩn C/C++. Mặc dù miễn phí và không có giao diện đồ họa hỗ trợ gỡ lỗi và lập trình, GCC vẫn đi kèm với các công cụ mạnh mẽ như gdb để phát hiện lỗi.
- Turbo C++ và Borland C/C++ hiện đã được đổi tên thành Borland Builder, mặc dù thị phần đã giảm, đây là trình biên dịch hỗ trợ chuẩn C98.
- Microsoft C/C++ chủ yếu dành cho phát triển phần mềm trên các hệ điều hành Windows. Trình biên dịch này nổi bật với các công cụ hỗ trợ đồ họa và phát triển phần mềm, nhưng không hoàn toàn tương thích với các tiêu chuẩn. Để mã nguồn tuân theo chuẩn, lập trình viên phải điều chỉnh một số cấu hình mặc định. Nó cũng không hỗ trợ các hệ điều hành không phải của Microsoft.
- Còn nhiều trình biên dịch khác ít phổ biến hơn, chẳng hạn như trình biên dịch C/C++ của Intel, Bell Labs, và các tổ chức khác.
Tiếng Việt
- Cú pháp ngôn ngữ C
- Các kiểu dữ liệu và khai báo biến trong C
- Các công cụ hữu ích: Cygwin, GCC, make, Linker
Tiếng Anh
- Tiền xử lý C (C preprocessor)
- Thư viện chuẩn C (C standard library)
- Thư viện C (C library)
- Chuỗi trong C (C string)
- Cú pháp C (C syntax)
- Danh sách các bài viết với chương trình C
- Objective-C
- Các toán tử trong C và C++
- Công cụ lập trình: Dev-C/C++, DJGPP, LCC, SPlint, Small-C, C--
Chú thích
- Brian Kernighan, Dennis Ritchie: Ngôn Ngữ Lập Trình C. Còn được biết đến là K&R — Cuốn sách gốc về C.
- Ấn bản đầu tiên, Prentice Hall 1978; ISBN 0-13-110163-3. Trước ANSI C.
- Ấn bản thứ hai, Prentice Hall 1988; ISBN 0-13-110362-8. ANSI C.
- ISO/IEC 9899. Tiêu chuẩn chính thức C:1999, cùng với các báo cáo lỗi và lý do.
- Samuel P. Harbison, Guy L. Steele: C: Sổ Tay Tham Khảo. Cuốn sách này là tài liệu tham khảo tuyệt vời, đặc biệt cho những người làm việc với trình biên dịch C. Cuốn sách chứa ngữ pháp BNF cho C.
- Ấn bản thứ tư, Prentice Hall 1994; ISBN 0-13-326224-3.
- Ấn bản thứ năm, Prentice Hall 2002; ISBN 0-13-089592-X.
- Derek M. Jones: Tiêu Chuẩn C Mới: Một Nhận Xét Văn Hóa và Kinh Tế, Addison-Wesley, ISBN 0-201-70917-1, tài liệu trực tuyến
- Robert Sedgewick: Thuật Toán Trong C, Addison-Wesley, ISBN 0-201-31452-5 (Phần 1–4) và ISBN 0-201-31663-3 (Phần 5)
- William H. Press, Saul A. Teukolsky, William T. Vetterling, Brian P. Flannery: Công Thức Số Học Trong C (Nghệ Thuật Tính Toán Khoa Học), ISBN 0-521-43108-5
Các liên kết hữu ích
C
- Những Câu Hỏi Thường Gặp về comp.lang.c
- Quá Trình Phát Triển Ngôn Ngữ CLưu trữ 2013-05-23 tại Wayback Machine của Dennis M. Ritchie
- Lập Trình bằng C (Tài liệu từ Lysator)
- Cuộc Thi Mã C Được Che Kín Quốc Tế
- Lập Trình C trên Wikibooks
- Tiêu Chuẩn C Mới: Nhận Xét Kinh Tế và Văn Hóa — sách chưa xuất bản về 'Phân Tích Chi Tiết Tiêu Chuẩn Quốc Tế cho Ngôn Ngữ C'
C99
- Phát Triển Mã Nguồn Mở Sử Dụng C99 — Mã C của bạn có đạt tiêu chuẩn không? bởi Peter Seebach
- Bạn đã sẵn sàng cho C99 chưa?
- Bài viết 'Sự Không Tương Thích Giữa ISO C và ISO C++' của David R. Tribble
Ngôn ngữ lập trình | |
---|---|
Dùng cho kỹ nghệ |
|
Dùng trong giảng dạy |
|
Có giá trị lịch sử |
|
Tiêu đề chuẩn |
|
---|