image 4 - quochung.cyou PTIT

[A.I System Programming] B1: Tìm hiểu lại C Core

This entry is part 1 of 6 in the series A.I System Programming

Ngôn ngữ C

C là một ngôn ngữ cho các chương trình “nhỏ và nhanh”. Nó là một ngôn ngữ “bậc thấp” và thấp hơn đa số ngôn ngữ khác, điều này có nghĩa là, chúng ta có thể viết code “gần với” máy tính hơn rất nhiều các ngôn ngữ bậc cao.

Cách C hoạt động

Máy tính thì chỉ hiểu được chỉ một và duy nhất 1 ngôn ngữ: ngôn ngữ máy tính (machine code), hay thực tế là 1 chuỗi số 1 và 0. Để ngôn ngữ C có thể được chuyển thành machine code, ta cần có 1 compiler

image - quochung.cyou PTIT

Đây là một chương trình C cơ bản, hay (source code) – code nguồn, source code thì chứa các ngôn ngữ C mà con người vẫn có thể hiểu được

image 1 - quochung.cyou PTIT

Với sợ trợ giúp của trình biên dịch (compiler), compiler sẽ kiểm tra các lỗi của code, và nếu mọi thứ ổn, nó sẽ biên dịch

image 2 - quochung.cyou PTIT

và cuối cùng trình biên dịch thường sẽ tạo ra 1 file mới, hay thường được gọi là executable (file có thể chạy được). File này chứa ngôn ngữ máy (machine code), 1 chuỗi các số 1 và 0 mà máy tính thực sự có thể hiểu và chạy được. Đây là file chương trình mà chúng ta có thể chạy

C được sử dụng khi tốc độ, không gian, khả năng portable (mang sang một máy khác) được ưu tiên. Thực tế, nhiều hệ điều hành đều được viết bằng C, nhiều ngôn ngữ khác cũng được viết bằng C, nhiều game cũng được viết bằng C.

Thông thường có 3 chuẩn C hiện tại, ANSI C có từ cuối những nam 1980, khá nhiều thứ đã được sửa đổi theo chuẩn C99 (1999). Nhiều tính năng mới cũng được thêm vào chuẩn hiện tại, C11 (2011), có thể sau đó sẽ có nhiều phiên bản khác chưa được đề cập trong bài viết, nhưng nhìn chung các nhà thiết kế luôn cố gắng mang lại nhiều tính năng, lợi ích mới từ các bản C mới nhưng vẫn đảm bảo vẫn có những quy tắc/chuẩn chung để phục vụ các chương trình cũ có thể chạy tốt.

Một chương trình C thường trông như thế nào?

Để tạo ra 1 chương trình hoàn chỉnh, bạn cần viết code vào 1 file mã nguồn C (source code C). File C thì bạn viết bằng gì cũng được, kể cả notepad, text editor, visual studio code, … , file name thường kết thúc bằng .c

image 3 - quochung.cyou PTIT

Đây là hình minh họa cho 1 chương trình C, hiển nhiên đây chỉ là 1 mẫu, không bắt buộc bạn phải tuân theo mẫu này, tuy nhiên nhìn chung trong ngành mọi người thường hay follow 1 chuẩn để tất cả mọi người cùng hiểu

  • Phần 1 đầu thường bắt đầu bằng comment, thường là thông tin về chứng chỉ, người tạo ra file code, mục đích, …
  • Phần 2 thường là các phần include, C thì là 1 ngôn ngữ rất rất bé, và thường thì khá khó để nó có thể tạo 1 chương trình thực tiễn nào nếu không có sự bổ trợ của thư viện ngoài. Hay là, nếu không có thư viện ngoài bổ sung, và chỉ có những tính năng cơ bản của C, ta khó có thể làm được 1 chương trình gì cả. Bạn cần khai báo với compiler (trình phiên dịch) về code ngoài/thư viện ngoài ta muốn sử dụng, ở đây stdio.h phục vụ như 1 thư viện giúp ta đọc/viết vào terminal
  • Phần cuối là các hàm. Tất cả code C đều thường chạy trong các hàm, trong đó có 1 hàm được gọi là main(), đây là điểm khởi đầu của chương trình

Máy tính sẽ bắt đầu chạy từ hàm main(). Nếu chương trình không có hàm main() thì không thể chạy được. Hàm main() có phần trả về thường là kiểu int. Phần này dùng để chỉ báo cho máy tính, nếu số được trả về trong main là 0, máy tính coi chương trình chạy thành công, và có vấn đề nếu số trả về là bất kì số nào khác 0.

Chạy chương trình

Cho một chương trình mẫu như sau:

image 6 - quochung.cyou PTIT

Như đã đề cập, C là một ngôn ngữ biên dịch (compiled language). Điều này có nghĩa là máy tính sẽ không dịch mã code của bạn một cách trực tiếp ngay khi chạy. Thay vào đó, bạn cần phải chuyển đổi, hay nói cách khác là biên dịch (compile), đoạn code nguồn mà con người có thể đọc hiểu (source code) thành ngôn ngữ máy (machine code) để máy tính có thể xử lý.

Để biên dịch code, bạn cần một chương trình gọi là compiler (trình biên dịch). Một trong những trình biên dịch C phổ biến nhất là GNU Compiler Collection, hay thường gọi tắt là gcc. gcc có sẵn trên rất nhiều hệ điều hành và nó có thể biên dịch nhiều ngôn ngữ khác chứ không chỉ riêng C.

Bạn có thể tham khảo cách cài tại đây: https://quochung.cyou/cach-cai-dat-c-mingw-cho-vs-code-c-20/

Dưới đây là cách bạn có thể biên dịch và chạy chương trình bằng gcc trên cửa sổ dòng lệnh (terminal/command prompt):

gcc cards.c -o cards
image 4 - quochung.cyou PTIT

Câu lệnh này yêu cầu gcc lấy file mã nguồn cards.c của bạn và biên dịch nó thành một file chạy được có tên là cards (hoặc cards.exe nếu bạn đang dùng Windows).

Bạn có thể gộp chung bước biên dịch và chạy mã trên hầu hết các máy bằng thủ thuật sau (dành cho Mac/Linux):

gcc cards.c -o cards && ./cards

Lệnh này sẽ chỉ chạy chương trình mới nếu nó được biên dịch thành công. Nếu có lỗi xảy ra trong quá trình biên dịch, nó sẽ bỏ qua việc chạy chương trình và chỉ hiển thị lỗi lên màn hình để bạn sửa.

image 5 - quochung.cyou PTIT
Q:Tại sao trên Linux hay Mac, ta lại cần có ./ ở trước tên chương trình khi chạy?
A:Trong các hệ điều hành dạng Unix, chương trình thường chỉ được chạy nếu bạn chỉ chính xác thư mục nơi mà chương trình nằm ở đó, hoặc chương trình có trong danh sách biến môi trường PATH
image 7 - quochung.cyou PTIT

Sơ qua về mảng và chuỗi

Nếu bạn biết đến C sau khi đã làm việc qua với một ngôn ngữ bậc cao, chắc hẳn bạn sẽ đặt câu hỏi là, trong chương trình trên, khi ta hỏi tên của lá bài, chúng ta đang sử dụng 1 mảng các kí tự, và có vẻ nó khác với cách bạn lưu trữ chuỗi trên các ngôn ngữ khác (string), còn nếu bạn chưa hiểu các khái niệm này thì cũng không sao, ta sẽ đi từ qua từng khái niệm.

Thực tế thì, ngôn ngữ C không hỗ trợ kiểu dữ liệu chuỗi (string) một cách trực tiếp từ trong lõi. (Dù vậy, vẫn có các thư viện mở rộng của C cung cấp chuỗi cho bạn).

Bởi vì C là ngôn ngữ “bậc thấp” hơn so với đa số các ngôn ngữ khác, nên thay vì dùng chuỗi, nó thường sử dụng một thứ tương tự: một mảng các ký tự đơn lẻ (an array of single characters).

Nếu bạn đã từng lập trình ở các ngôn ngữ khác, hẳn bạn đã gặp khái niệm “mảng” (array). Mảng đơn giản là một danh sách các phần tử được gán chung một cái tên. Ví dụ, card_name chỉ là tên một biến mà bạn dùng để gọi danh sách các ký tự được người dùng nhập vào từ bàn phím. Nếu bạn định nghĩa card_name là một mảng gồm 2 ký tự, bạn có thể gọi ký tự đầu tiên là card_name[0] và ký tự thứ hai là card_name[1].

Vậy khi C nhìn thấy một chuỗi như thế này: s = "Shatner"

Nó sẽ đọc chuỗi đó giống như một mảng chứa các ký tự riêng biệt: ['S', 'h', 'a', 't', 'n', 'e', 'r'].

image 8 - quochung.cyou PTIT

Mỗi kí tự (char/character) trong chuỗi này chỉ như 1 phần tử của 1 mảng, vì vậy ta có thể trỏ tới 1 kí tự độc lập bất kì sử dụng index như, s[0] = ‘S’, s[1] = ‘h’

image 9 - quochung.cyou PTIT

Chuyện gì xảy ra khi C muốn in chuỗi này ra màn hình? Ở nhiều ngôn ngữ khác, máy tính theo dõi rất sát sao kích thước của một mảng, nó luôn biết 1 mảng có bao nhiêu kí tự, vì vậy giả sử khi ta cần in ra 1 chuỗi nào đó, nó chỉ cần tìm vị trí đầu của chuỗi đó, nhảy một số bước (theo số lượng kí tự nó đã biết) để đến vị trí cuối, và in toàn bộ ra.

Nhưng vì C là ngôn ngữ bậc thấp, nó không phải lúc nào cũng biết chính xác độ dài của mảng là bao nhiêu. Để giải quyết vấn đề này, C thêm một ký tự báo hiệu (sentinel character) vào cuối mỗi chuỗi. Ký tự này có giá trị là \0 (còn gọi là null terminator).

image 10 - quochung.cyou PTIT

Bất cứ khi nào máy tính cần đọc nội dung của một chuỗi, nó sẽ đi qua từng phần tử của mảng ký tự, cho đến khi nó đụng phải \0.

Điều đó có nghĩa là chữ "Shatner" thực chất được lưu trong bộ nhớ máy tính dưới dạng: ['S', 'h', 'a', 't', 'n', 'e', 'r', '\0'].

s = "Shatner"
image 11 - quochung.cyou PTIT

Đó là lý do tại sao khi khai báo biến lưu 2 ký tự, ta lại phải khai báo kích thước là 3: char card_name[3]; Ký tự thứ ba chính là không gian dự phòng dành cho ký tự báo hiệu \0.

H: Tại sao các ký tự lại được đánh số từ 0? Sao không phải là 1?

Đ: Chỉ số (index) thực chất là một khoảng cách (offset): nó là thước đo xem ký tự đó nằm cách ký tự đầu tiên bao xa.

H: Tại sao lại thế?

Đ: Máy tính sẽ lưu trữ các ký tự trong các byte bộ nhớ liên tiếp nhau. Nó có thể sử dụng chỉ số này để tính toán ra vị trí của ký tự. Nếu nó biết rằng c[0] đang nằm ở vị trí bộ nhớ 1.000.000, thì nó có thể tính toán rất nhanh rằng c[96] nằm ở vị trí 1.000.000 + 96.

H: Tại sao nó cần một ký tự báo hiệu (sentinel character)? Chẳng lẽ nó không biết chuỗi dài bao nhiêu sao?

Đ: Thường thì nó không biết đâu. C không giỏi lắm trong việc theo dõi xem các mảng có độ dài bao nhiêu, và chuỗi thì bản chất cũng chỉ là một mảng mà thôi.

H: Nó không biết mảng dài bao nhiêu ?

Đ: Đúng vậy. Đôi khi trình biên dịch (compiler) có thể tìm ra độ dài của một mảng bằng cách phân tích code, nhưng thường thì C dựa dẫm vào bạn trong việc theo dõi các mảng của mình.

H: Dùng nháy đơn hay nháy kép thì có quan trọng không?

Đ: Có chứ. Nháy đơn (' ') được dùng cho các ký tự đơn lẻ, nhưng nháy kép (" ") thì luôn được dùng cho chuỗi.

H: Vậy tôi nên định nghĩa chuỗi của mình bằng dấu nháy kép (" ") hay khai báo rõ ràng nó là một mảng các ký tự? Đ: Thông thường bạn sẽ định nghĩa chuỗi bằng dấu nháy kép. Chúng được gọi là chuỗi ký tự (string literals), và gõ chúng thì dễ dàng hơn nhiều.

H: Có sự khác biệt nào giữa chuỗi ký tự (string literals) và mảng ký tự (character arrays) không?

Đ: Chỉ có một điểm thôi: chuỗi ký tự là các hằng số (constant).

H: Điều đó có nghĩa là gì?

Đ: Có nghĩa là bạn không thể thay đổi từng ký tự đơn lẻ một khi chúng đã được tạo ra.

H: Chuyện gì sẽ xảy ra nếu tôi cố tình thử thay đổi?

Đ: Điều này phụ thuộc vào trình biên dịch, nhưng gcc thường sẽ hiển thị lỗi bus error.

H: Lỗi bus error? Lỗi bus error là cái gì vậy?

Đ: C sẽ lưu trữ các chuỗi ký tự (string literals) trong bộ nhớ theo một cách khác. Lỗi bus error chỉ đơn giản có nghĩa là chương trình của bạn không thể cập nhật (chỉnh sửa) phần bộ nhớ đó

Các phép toán nhẹ nhàng và Cấu trúc điều khiển

Trong C, một dấu bằng (=) được dùng để gán giá trị (assignment). Nhưng hai dấu bằng (==) lại được dùng để kiểm tra sự bằng nhau (testing equality).

  • Tăng/giảm biến một khoảng nhất định: x += 5; hoặc x -= 2;
  • Tăng/giảm biến đi 1 đơn vị: x++; hoặc x--;

Hai loại câu lệnh chính

  1. Làm một việc gì đó (Do something): Hầu hết các lệnh trong C đều là các hành động định nghĩa biến, đọc dữ liệu, in ra màn hình.
image 12 - quochung.cyou PTIT
  1. Bạn có thể nhóm nhiều câu lệnh lại với nhau bằng dấu ngoặc nhọn { } để tạo thành một khối lệnh (block statements).
image 13 - quochung.cyou PTIT
  1. Chỉ làm điều gì đó NẾU điều kiện là đúng (Do something only if): Đây là các câu lệnh điều khiển, tiêu biểu là if.
image 14 - quochung.cyou PTIT
image 15 - quochung.cyou PTIT
C
if (x == 2) {
    puts("Hãy làm gì đó!");
}

Để viết một chương trình đếm bài (như trong trò Blackjack), bạn cần kiểm tra xem một lá bài có nằm trong khoảng từ 3 đến 6 hay không. Nghĩa là bạn phải kiểm tra nhiều điều kiện cùng lúc. C cung cấp các toán tử logic (boolean operators) để làm việc này:

  • && (AND – VÀ): Chỉ trả về đúng (true) khi cả hai điều kiện đều đúng. (Ví dụ: val >= 3 && val <= 6)
image 16 - quochung.cyou PTIT
  • || (OR – HOẶC): Trả về đúng (true) nếu một trong hai điều kiện đúng.
image 17 - quochung.cyou PTIT
  • ! (NOT – PHỦ ĐỊNH): Lật ngược giá trị của điều kiện. (Đang đúng thành sai, đang sai thành đúng).
image 18 - quochung.cyou PTIT

Lưu ý: Trong C không có khái niệm true hay false một cách nguyên thủy. C quy ước: số 0 nghĩa là Sai (false), và bất kỳ số nào khác 0 đều được coi là Đúng (true).

Gemini đã nói

Dưới đây là phần tiếp nối được viết dựa trên đoạn tiếng Anh bạn cung cấp, giữ nguyên văn phong gần gũi, dễ hiểu và mang tính hướng dẫn từng bước của bạn:


Nhưng làm thế nào để bạn chạy được chương trình?

Như đã đề cập, C là một ngôn ngữ biên dịch (compiled language). Điều này có nghĩa là máy tính sẽ không dịch mã code của bạn một cách trực tiếp ngay khi chạy. Thay vào đó, bạn cần phải chuyển đổi, hay nói cách khác là biên dịch (compile), đoạn code nguồn mà con người có thể đọc hiểu (source code) thành ngôn ngữ máy (machine code) để máy tính có thể xử lý.

Để biên dịch code, bạn cần một chương trình gọi là compiler (trình biên dịch). Một trong những trình biên dịch C phổ biến nhất là GNU Compiler Collection, hay thường gọi tắt là gcc. gcc có sẵn trên rất nhiều hệ điều hành và nó có thể biên dịch nhiều ngôn ngữ khác chứ không chỉ riêng C. Điều tuyệt vời nhất? Nó hoàn toàn miễn phí.

Dưới đây là cách bạn có thể biên dịch và chạy chương trình bằng gcc trên cửa sổ dòng lệnh (terminal/command prompt):

Bash

gcc cards.c -o cards

Câu lệnh này yêu cầu gcc lấy file mã nguồn cards.c của bạn và biên dịch nó thành một file chạy được có tên là cards (hoặc cards.exe nếu bạn đang dùng Windows).

Mẹo nhỏ (Geek Bits): Bạn có thể gộp chung bước biên dịch và chạy mã trên hầu hết các máy bằng thủ thuật sau (dành cho Mac/Linux):

Bash

gcc cards.c -o cards && ./cards

Lệnh này sẽ chỉ chạy chương trình mới nếu nó được biên dịch thành công. Nếu có lỗi xảy ra trong quá trình biên dịch, nó sẽ bỏ qua việc chạy chương trình và chỉ hiển thị lỗi lên màn hình để bạn sửa.


Hiểu sâu hơn về Chuỗi (Strings) trong C

Thực tế thì, ngôn ngữ C không hỗ trợ kiểu dữ liệu chuỗi (string) một cách trực tiếp từ trong lõi. (Dù vậy, vẫn có các thư viện mở rộng của C cung cấp chuỗi cho bạn).

Bởi vì C là ngôn ngữ “bậc thấp” hơn so với đa số các ngôn ngữ khác, nên thay vì dùng chuỗi, nó thường sử dụng một thứ tương tự: một mảng các ký tự đơn lẻ (an array of single characters).

Nếu bạn đã từng lập trình ở các ngôn ngữ khác, hẳn bạn đã gặp khái niệm “mảng” (array). Mảng đơn giản là một danh sách các phần tử được gán chung một cái tên. Ví dụ, card_name chỉ là tên một biến mà bạn dùng để gọi danh sách các ký tự được người dùng nhập vào từ bàn phím. Nếu bạn định nghĩa card_name là một mảng gồm 2 ký tự, bạn có thể gọi ký tự đầu tiên là card_name[0] và ký tự thứ hai là card_name[1].

Vậy khi C nhìn thấy một chuỗi như thế này: s = "Shatner"

Nó sẽ đọc chuỗi đó giống như một mảng chứa các ký tự riêng biệt: ['S', 'h', 'a', 't', 'n', 'e', 'r'].

Đừng để rơi khỏi điểm kết thúc của chuỗi! Chuyện gì xảy ra khi C muốn in chuỗi này ra màn hình? Ở nhiều ngôn ngữ khác, máy tính theo dõi rất sát sao kích thước của một mảng. Nhưng vì C là ngôn ngữ bậc thấp, nó không phải lúc nào cũng biết chính xác độ dài của mảng là bao nhiêu.

Để giải quyết vấn đề này, C thêm một ký tự báo hiệu (sentinel character) vào cuối mỗi chuỗi. Ký tự này có giá trị là \0 (còn gọi là null terminator).

Bất cứ khi nào máy tính cần đọc nội dung của một chuỗi, nó sẽ đi qua từng phần tử của mảng ký tự, cho đến khi nó đụng phải \0. Điều đó có nghĩa là chữ "Shatner" thực chất được lưu trong bộ nhớ máy tính dưới dạng: ['S', 'h', 'a', 't', 'n', 'e', 'r', '\0'].

Đó là lý do tại sao khi khai báo biến lưu 2 ký tự, ta lại phải khai báo kích thước là 3: char card_name[3]; Ký tự thứ ba chính là không gian dự phòng dành cho ký tự báo hiệu \0.

Không có câu hỏi nào là ngớ ngẩn (Q&A):

  • H: Tại sao các ký tự trong mảng lại được đánh số từ 0 chứ không phải 1? Đ: Chỉ số (index) thực chất là một “khoảng cách” (offset): nó đo xem ký tự đó nằm cách ký tự đầu tiên bao xa. Ký tự đầu tiên cách chính nó 0 bước, nên nó ở vị trí số 0.
  • H: Có sự khác biệt nào giữa việc dùng nháy đơn (‘ ‘) và nháy kép (” “) không? Đ: Có. Nháy đơn luôn được dùng cho các ký tự đơn lẻ (ví dụ: 'A'), còn nháy kép luôn được dùng cho chuỗi (ví dụ: "Shatner").

Các phép toán nhẹ nhàng và Cấu trúc điều khiển

Không phải dấu Bằng nào cũng giống nhau Trong C, một dấu bằng (=) được dùng để gán giá trị (assignment). Nhưng hai dấu bằng (==) lại được dùng để kiểm tra sự bằng nhau (testing equality). Hãy nhớ kỹ điều này vì nó là nguyên nhân gây ra vô số lỗi khó chịu cho người mới bắt đầu!

  • Tăng/giảm biến một khoảng nhất định: x += 5; hoặc x -= 2;
  • Tăng/giảm biến đi 1 đơn vị: x++; hoặc x--;

Hai loại câu lệnh chính

  1. Làm một việc gì đó (Do something): Hầu hết các lệnh trong C đều là các hành động định nghĩa biến, đọc dữ liệu, in ra màn hình. Bạn có thể nhóm nhiều câu lệnh lại với nhau bằng dấu ngoặc nhọn { } để tạo thành một khối lệnh (block statements).
  2. Chỉ làm điều gì đó NẾU điều kiện là đúng (Do something only if): Đây là các câu lệnh điều khiển, tiêu biểu là if.

C

if (x == 2) {
    puts("Hãy làm gì đó!");
}

Đếm bài? Bằng ngôn ngữ C?

Để viết một chương trình đếm bài (như trong trò Blackjack), bạn cần kiểm tra xem một lá bài có nằm trong khoảng từ 3 đến 6 hay không. Nghĩa là bạn phải kiểm tra nhiều điều kiện cùng lúc. C cung cấp các toán tử logic (boolean operators) để làm việc này:

  • && (AND – VÀ): Chỉ trả về đúng (true) khi cả hai điều kiện đều đúng. (Ví dụ: val >= 3 && val <= 6)
  • || (OR – HOẶC): Trả về đúng (true) nếu một trong hai điều kiện đúng.
  • ! (NOT – PHỦ ĐỊNH): Lật ngược giá trị của điều kiện. (Đang đúng thành sai, đang sai thành đúng).

Lưu ý: Trong C không có khái niệm true hay false một cách nguyên thủy. C quy ước: số 0 nghĩa là Sai (false), và bất kỳ số nào khác 0 đều được coi là Đúng (true).


Câu lệnh Switch: Cách thay thế nhiều lệnh “if”

Đôi khi bạn cần kiểm tra giá trị của cùng một biến hết lần này đến lần khác. Thay vì viết một danh sách dài dằng dặc các lệnh else if, C cho bạn một giải pháp thanh lịch hơn: câu lệnh switch.

C
switch(card_name[0]) {
    case 'K':
    case 'Q':
    case 'J':
        val = 10;
        break;
    case 'A':
        val = 11;
        break;
    default:
        val = atoi(card_name);
}

Khi máy tính chạy đến lệnh switch, nó sẽ tìm một case khớp với giá trị của biến. Khi tìm thấy, nó sẽ chạy TẤT CẢ các dòng code bên dưới nó cho đến khi gặp lệnh break. Nếu bạn quên lệnh break, code sẽ chạy tuột xuống các case bên dưới

Đôi khi làm một lần là chưa đủ… (Vòng lặp)

Nếu bạn muốn chương trình của mình lặp đi lặp lại một hành động thì sao? Hãy dùng vòng lặp (Loops).

  • Vòng lặp while: Chạy đi chạy lại một đoạn code chừng nào điều kiện của nó vẫn còn đúng.
C
while (count < 10) {
    puts("Vẫn đang đếm...");
    count++;
}

Vòng lặp for: Thực chất là một phiên bản viết gọn gàng hơn của vòng lặp while. Nó gom phần khởi tạo biến, điều kiện chạy, và bước tăng biến vào cùng một dòng, giúp code dễ đọc hơn rất nhiều.

C
for (int i = 0; i < 10; i++) {
    puts("Vẫn đang đếm...");
}

Cách thoát khỏi vòng lặp:

  • Dùng lệnh break để ngay lập tức phá vỡ và thoát ra khỏi vòng lặp hiện tại.
  • Dùng lệnh continue để bỏ qua phần còn lại của vòng lặp và quay ngay lên đầu vòng lặp để bắt đầu một chu kỳ lặp mới.

Hàm (Functions)

Cho đến giờ, chúng ta toàn viết code vào trong một hàm gọi là main(). Nhưng bạn hoàn toàn có thể tự tạo hàm của riêng mình.

Mọi hàm trong C thường có 3 phần cơ bản:

  1. Kiểu trả về (Return type): Cho biết hàm này sau khi chạy xong sẽ ném ra kết quả là kiểu dữ liệu gì (ví dụ: int, float,…). Nếu hàm chỉ chạy mà không trả về kết quả gì cả, bạn dùng từ khóa void (nghĩa là “trống rỗng / không quan trọng”).
  2. Tên hàm.
  3. Tham số (Parameters/Arguments): Các biến mà hàm nhận vào để xử lý.
C
int tim_so_lon_hon(int a, int b) {
    if (a > b) {
        return a;
    }
    return b;
}

Gemini đã nói

Dưới đây là phần tiếp nối được viết dựa trên đoạn tiếng Anh bạn cung cấp, giữ nguyên văn phong gần gũi, dễ hiểu và mang tính hướng dẫn từng bước của bạn:


Nhưng làm thế nào để bạn chạy được chương trình?

Như đã đề cập, C là một ngôn ngữ biên dịch (compiled language). Điều này có nghĩa là máy tính sẽ không dịch mã code của bạn một cách trực tiếp ngay khi chạy. Thay vào đó, bạn cần phải chuyển đổi, hay nói cách khác là biên dịch (compile), đoạn code nguồn mà con người có thể đọc hiểu (source code) thành ngôn ngữ máy (machine code) để máy tính có thể xử lý.

Để biên dịch code, bạn cần một chương trình gọi là compiler (trình biên dịch). Một trong những trình biên dịch C phổ biến nhất là GNU Compiler Collection, hay thường gọi tắt là gcc. gcc có sẵn trên rất nhiều hệ điều hành và nó có thể biên dịch nhiều ngôn ngữ khác chứ không chỉ riêng C. Điều tuyệt vời nhất? Nó hoàn toàn miễn phí.

Dưới đây là cách bạn có thể biên dịch và chạy chương trình bằng gcc trên cửa sổ dòng lệnh (terminal/command prompt):

Bash

gcc cards.c -o cards

Câu lệnh này yêu cầu gcc lấy file mã nguồn cards.c của bạn và biên dịch nó thành một file chạy được có tên là cards (hoặc cards.exe nếu bạn đang dùng Windows).

Mẹo nhỏ (Geek Bits): Bạn có thể gộp chung bước biên dịch và chạy mã trên hầu hết các máy bằng thủ thuật sau (dành cho Mac/Linux):

Bash

gcc cards.c -o cards && ./cards

Lệnh này sẽ chỉ chạy chương trình mới nếu nó được biên dịch thành công. Nếu có lỗi xảy ra trong quá trình biên dịch, nó sẽ bỏ qua việc chạy chương trình và chỉ hiển thị lỗi lên màn hình để bạn sửa.


Hiểu sâu hơn về Chuỗi (Strings) trong C

Thực tế thì, ngôn ngữ C không hỗ trợ kiểu dữ liệu chuỗi (string) một cách trực tiếp từ trong lõi. (Dù vậy, vẫn có các thư viện mở rộng của C cung cấp chuỗi cho bạn).

Bởi vì C là ngôn ngữ “bậc thấp” hơn so với đa số các ngôn ngữ khác, nên thay vì dùng chuỗi, nó thường sử dụng một thứ tương tự: một mảng các ký tự đơn lẻ (an array of single characters).

Nếu bạn đã từng lập trình ở các ngôn ngữ khác, hẳn bạn đã gặp khái niệm “mảng” (array). Mảng đơn giản là một danh sách các phần tử được gán chung một cái tên. Ví dụ, card_name chỉ là tên một biến mà bạn dùng để gọi danh sách các ký tự được người dùng nhập vào từ bàn phím. Nếu bạn định nghĩa card_name là một mảng gồm 2 ký tự, bạn có thể gọi ký tự đầu tiên là card_name[0] và ký tự thứ hai là card_name[1].

Vậy khi C nhìn thấy một chuỗi như thế này: s = "Shatner"

Nó sẽ đọc chuỗi đó giống như một mảng chứa các ký tự riêng biệt: ['S', 'h', 'a', 't', 'n', 'e', 'r'].

Đừng để rơi khỏi điểm kết thúc của chuỗi! Chuyện gì xảy ra khi C muốn in chuỗi này ra màn hình? Ở nhiều ngôn ngữ khác, máy tính theo dõi rất sát sao kích thước của một mảng. Nhưng vì C là ngôn ngữ bậc thấp, nó không phải lúc nào cũng biết chính xác độ dài của mảng là bao nhiêu.

Để giải quyết vấn đề này, C thêm một ký tự báo hiệu (sentinel character) vào cuối mỗi chuỗi. Ký tự này có giá trị là \0 (còn gọi là null terminator).

Bất cứ khi nào máy tính cần đọc nội dung của một chuỗi, nó sẽ đi qua từng phần tử của mảng ký tự, cho đến khi nó đụng phải \0. Điều đó có nghĩa là chữ "Shatner" thực chất được lưu trong bộ nhớ máy tính dưới dạng: ['S', 'h', 'a', 't', 'n', 'e', 'r', '\0'].

Đó là lý do tại sao khi khai báo biến lưu 2 ký tự, ta lại phải khai báo kích thước là 3: char card_name[3]; Ký tự thứ ba chính là không gian dự phòng dành cho ký tự báo hiệu \0.

Không có câu hỏi nào là ngớ ngẩn (Q&A):

  • H: Tại sao các ký tự trong mảng lại được đánh số từ 0 chứ không phải 1? Đ: Chỉ số (index) thực chất là một “khoảng cách” (offset): nó đo xem ký tự đó nằm cách ký tự đầu tiên bao xa. Ký tự đầu tiên cách chính nó 0 bước, nên nó ở vị trí số 0.
  • H: Có sự khác biệt nào giữa việc dùng nháy đơn (‘ ‘) và nháy kép (” “) không? Đ: Có. Nháy đơn luôn được dùng cho các ký tự đơn lẻ (ví dụ: 'A'), còn nháy kép luôn được dùng cho chuỗi (ví dụ: "Shatner").

Các phép toán nhẹ nhàng và Cấu trúc điều khiển

Không phải dấu Bằng nào cũng giống nhau Trong C, một dấu bằng (=) được dùng để gán giá trị (assignment). Nhưng hai dấu bằng (==) lại được dùng để kiểm tra sự bằng nhau (testing equality). Hãy nhớ kỹ điều này vì nó là nguyên nhân gây ra vô số lỗi khó chịu cho người mới bắt đầu!

  • Tăng/giảm biến một khoảng nhất định: x += 5; hoặc x -= 2;
  • Tăng/giảm biến đi 1 đơn vị: x++; hoặc x--;

Hai loại câu lệnh chính

  1. Làm một việc gì đó (Do something): Hầu hết các lệnh trong C đều là các hành động định nghĩa biến, đọc dữ liệu, in ra màn hình. Bạn có thể nhóm nhiều câu lệnh lại với nhau bằng dấu ngoặc nhọn { } để tạo thành một khối lệnh (block statements).
  2. Chỉ làm điều gì đó NẾU điều kiện là đúng (Do something only if): Đây là các câu lệnh điều khiển, tiêu biểu là if.
C
if (x == 2) {
    puts("Hãy làm gì đó!");
}

Đếm bài? Bằng ngôn ngữ C?

Để viết một chương trình đếm bài (như trong trò Blackjack), bạn cần kiểm tra xem một lá bài có nằm trong khoảng từ 3 đến 6 hay không. Nghĩa là bạn phải kiểm tra nhiều điều kiện cùng lúc. C cung cấp các toán tử logic (boolean operators) để làm việc này:

  • && (AND – VÀ): Chỉ trả về đúng (true) khi cả hai điều kiện đều đúng. (Ví dụ: val >= 3 && val <= 6)
  • || (OR – HOẶC): Trả về đúng (true) nếu một trong hai điều kiện đúng.
  • ! (NOT – PHỦ ĐỊNH): Lật ngược giá trị của điều kiện. (Đang đúng thành sai, đang sai thành đúng).

Lưu ý: Trong C không có khái niệm true hay false một cách nguyên thủy. C quy ước: số 0 nghĩa là Sai (false), và bất kỳ số nào khác 0 đều được coi là Đúng (true).


Câu lệnh Switch: Cách thay thế nhiều lệnh “if”

Đôi khi bạn cần kiểm tra giá trị của cùng một biến hết lần này đến lần khác. Thay vì viết một danh sách dài dằng dặc các lệnh else if, C cho bạn một giải pháp thanh lịch hơn: câu lệnh switch.

C
switch(card_name[0]) {
    case 'K':
    case 'Q':
    case 'J':
        val = 10;
        break;
    case 'A':
        val = 11;
        break;
    default:
        val = atoi(card_name);
}

Khi máy tính chạy đến lệnh switch, nó sẽ tìm một case khớp với giá trị của biến. Khi tìm thấy, nó sẽ chạy TẤT CẢ các dòng code bên dưới nó cho đến khi gặp lệnh break.

H: Tại sao tôi lại dùng lệnh switch thay vì dùng if?

Đ: Nếu bạn đang thực hiện việc kiểm tra nhiều lần trên cùng một biến, bạn có thể sẽ muốn dùng lệnh switch.

H: Ưu điểm của việc dùng lệnh switch là gì?

Đ: Có vài ưu điểm. Đầu tiên: sự rõ ràng. Nhìn vào là thấy rõ cả một khối code đang xử lý chỉ một biến duy nhất. Điều đó không quá rõ ràng nếu bạn chỉ có một dãy các câu lệnh if. Thứ hai, bạn có thể tận dụng logic “rơi tự do” (fall-through) để tái sử dụng các phần code cho các trường hợp (cases) khác nhau.

H: Tôi có thể kiểm tra chuỗi (strings) trong lệnh switch không?

Đ: Không, bạn không thể dùng lệnh switch để kiểm tra một chuỗi các ký tự hay bất kỳ loại mảng (array) nào. Lệnh switch sẽ chỉ kiểm tra một giá trị đơn lẻ mà thôi.


Đôi khi làm một lần là chưa đủ… (Vòng lặp)

Nếu bạn muốn chương trình của mình lặp đi lặp lại một hành động thì sao? Hãy dùng vòng lặp (Loops).

  • Vòng lặp while: Chạy đi chạy lại một đoạn code chừng nào điều kiện của nó vẫn còn đúng.
C
while (count < 10) { 
  puts("Vẫn đang đếm..."); 
  count++; 
}

Bạn có thể dùng vòng lặp while bất cứ lúc nào bạn cần lặp lại một đoạn code, nhưng đa số thời gian các vòng lặp của bạn sẽ có cùng một loại cấu trúc như sau:

  1. Làm một việc gì đó đơn giản trước vòng lặp, ví dụ như cài đặt một biến đếm (counter).
  2. Có một điều kiện kiểm tra đơn giản ngay trên vòng lặp.
  3. Làm một việc gì đó ở cuối vòng lặp, ví dụ như cập nhật biến đếm.

Ví dụ, đây là một vòng lặp while đếm từ 1 đến 10:

image 20 - quochung.cyou PTIT

Những vòng lặp kiểu này chứa đoạn code chuẩn bị các biến cho vòng lặp, một loại điều kiện nào đó được kiểm tra mỗi khi vòng lặp chạy, và cuối cùng là một đoạn code nào đó ở cuối vòng lặp để cập nhật biến đếm hoặc thứ gì đó tương tự.

  • Vòng lặp for: Thực chất là một phiên bản viết gọn gàng hơn của vòng lặp while. Nó gom phần khởi tạo biến, điều kiện chạy, và bước tăng biến vào cùng một dòng, giúp code dễ đọc hơn rất nhiều.

Bởi vì khuôn mẫu này quá phổ biến, các nhà thiết kế ra C đã tạo ra vòng lặp for để làm cho nó trở nên súc tích hơn một chút. Đây là cùng một đoạn code đó nhưng được viết bằng vòng lặp for:

image 21 - quochung.cyou PTIT

Vòng lặp for thực sự được sử dụng rất nhiều trong C, nếu không muốn nói là nhiều hơn, vòng lặp while. Chúng không chỉ làm cho code ngắn hơn một chút, mà còn giúp các lập trình viên C khác dễ đọc hơn, bởi vì toàn bộ phần code điều khiển vòng lặp, những thứ kiểm soát giá trị của biến đếm (counter), giờ đây được gói gọn vào trong câu lệnh for và được đưa ra khỏi phần thân vòng lặp. Mọi vòng lặp for đều cần phải có một thứ gì đó nằm trong phần thân của nó.

Cách thoát khỏi vòng lặp:

  • Dùng lệnh break để ngay lập tức phá vỡ và thoát ra khỏi vòng lặp hiện tại.
  • Dùng lệnh continue để bỏ qua phần còn lại của vòng lặp và quay ngay lên đầu vòng lặp để bắt đầu một chu kỳ lặp mới.

Hàm (Functions)

Cho đến giờ, chúng ta toàn viết code vào trong một hàm gọi là main(). Nhưng bạn hoàn toàn có thể tự tạo hàm của riêng mình.

Mọi hàm trong C thường có 3 phần cơ bản:

  1. Kiểu trả về (Return type): Cho biết hàm này sau khi chạy xong sẽ ném ra kết quả là kiểu dữ liệu gì (ví dụ: int, float,…). Nếu hàm chỉ chạy mà không trả về kết quả gì cả, bạn dùng từ khóa void (nghĩa là “trống rỗng / không quan trọng”).
  2. Tên hàm.
  3. Tham số (Parameters/Arguments): Các biến mà hàm nhận vào để xử lý.
C
int tim_so_lon_hon(int a, int b) {
    if (a > b) {
        return a;
    }
    return b;
}

A.I System Programming

[A.I System Programming] B2: Tìm hiểu C Memory/Pointer

Published by

Nguyễn Quốc Hưng

I'm delighted to see you here :>

Leave a Reply