Linux · Programming

Lập trình C trên Linux the hard way

Source : http://ngochin.com/2014/08/15/lap-trinh-c-tren-linux-the-hard-way/

Tôi thấy rất nhiều bạn hỏi cách lập trình C trên Ubuntu. Nếu các bạn muốn nhẹ nhàng, ăn sẵn thì có thể bắt đầu với một IDE như Code::Block hay Eclipse có cài CDT hay thậm chí là cài Borland Turbo C trên DOSBox. Còn cá nhân tôi thích nhất Qt Creator. Nếu bạn chọn cách này thì vẫn nên đọc phần conio.h bên dưới.

Nhưng nếu các bạn muốn thực sự hiểu sâu vấn đề thì tốt nhất nên code bằng editor và compile bằng tay. Giống như cách mà mọi người vẫn làm từ những năm 70 đến giờ.

Setup

Bài viết tập trung vào môi trường Ubuntu và các distro tương thích. Một số lệnh trên các distro khác như Fedora hay Arch Linux sẽ hơi khác một chút. Nhưng nếu bạn hiểu được ý tưởng căn bản thì áp dụng ở đâu cũng được.

Trước hết chúng ta sẽ cần một text editor. Sẵn có trên Ubuntu thì có Gedit, nano. Bạn nào dùng KDE thì có Kate cũng rất tốt. Trendy hiện nay thì có Sublime Text 3. Hard core hơn thì có Emacs và Vim, ban đầu khó dùng nhưng rất đáng bỏ công học. Bạn dùng editor nào cũng được, tất cả đều là editor tốt.

Tiếp theo sẽ cần một bộ compiler (là phần mềm để chuyển mã nguồn thành chương trình chạy được). Thông thường với C/C++ trên Linux thì gcc (GNU C Compiler) là compiler thông dụng nhất. Bạn có thể cài đặt gcc và các công cụ hỗ trợ bằng một lệnh sau:

sudo apt-get install build-essential

Compile thủ công

Bây giờ thì ta có thể bắt đầu với một ứng dụng đầu tiên. Bạn gõ chương trình sau vào file hello.c bằng editor đã chọn ở trên.

Một lập trình viên nên có thói quen sắp xếp trật tự ngăn nắp các file và thư mục. Bạn có thể tạo một thư mục dev ở $HOME trong đó có chứa các project, nếu muốn có thể thêm một lớp folder chứa ngôn ngữ nữa:

$ mkdir --parents $HOME/dev/c/hello
$ cd $HOME/dev/c/hello
$ gedit hello.c &
#include <stdio.h>

int main(int argc, char **argv)
{
    printf("Hello, World!\n");
    return 0;
}

Compile và chạy:

$ gcc hello.c -o hello
$ ./hello
Hello, World!

Ở đây ta có thể thấy lệnh gcc được gọi với tham số hello.c là tập tin đầu vào và flag -o hello thể hiện tên của tập tin đầu ra (-o là viết tắt của out). Sau đó ta chạy file hello vừa tạo ra bằng lệnh ./hello. Dấu chấm và gạch chéo nghĩa là chạy file hello ở thư mục hiện tại (biết đâu trong $PATH của bạn lại chứa một chương trình cũng tên là hello?). Nếu bạn không đặt tên cho file đầu ra thì theo truyền thống, gcc sẽ tạo file có tên là a.out.

Một lưu ý nho nhỏ là danh sách các file đầu vào nên là argument đầu tiên của gcc. Nếu không, bạn có thể gặp một vài lỗi tìm kiếm thư viện rất khó hiểu.

Bây giờ giả sử bạn muốn thay đổi chương trình và mở rộng thêm một file nữa. Coi như là main.c đi. Bạn sẽ tách lệnh hiển thị ra màn hình thành hàm hello() chứa trong hello.c còn hàm main() thì chứa trong main.c.

Chương trình giờ gồm 3 file:

hello.h

#ifndef __HELLO_H
#def __HELLO_H

void hello();

#endif

hello.c

#include <stdio.h>
#include "hello.h"

void hello()
{
    printf("Hello, World!\n");
}

main.c

#include "hello.h"

int main(int argc, char **argv)
{
    hello();
    return 0;
}

Bạn vẫn compile như trên nhưng mở rộng thêm file main.c vào câu lệnh.

$ gcc main.c hello.c -o hello
$ ./hello
Hello, World!

Make

Rất tuyệt. Nhưng bạn có để ý nãy giờ là việc compile bằng cách chạy gcc trực tiếp thế này rất thủ công không? Stuart Feldman cũng nghĩ vậy vào năm 1976 và đã viết chương trình Make để tự động hóa những thao tác lặp đi lặp lại như thế này. Make đã được cài tự động cùng với gói build-essential ở trên.

Để sử dụng Make, trước hết chúng ta sẽ viết một Makefile đơn giản, đặt tên tập tin là Makefile và nằm cùng thư mục với chương trình C của chúng ta.

CHÚ Ý: Sau khi gõ “:” xuống dòng thì bạn nhấn một dấu TAB.

Makefile

all: hello run

hello: hello.c main.c
    gcc main.c hello.c -o hello

run:
    ./hello

clean:
    rm --recursive --force hello

Bây giờ thay vì chạy gcc thì bạn chạy:

$ make all
gcc main.c hello.c -o hello
./hello
Hello, World!

Có thể thấy là chỉ với một lệnh make, các thao tác thủ công của chúng ta đều đã được thực hiện. Cùng phân tích Makefile một chút.

Một Makefile bao gồm các target, là những thứ bạn muốn thực hiện. Ở đây có 3 target là allhello và run. Sau dấu hai chấm của all có liệt kê 2 target còn lại, nghĩa là target all phụ thuộc vào hello và run. Như vậy, khi chúng ta make all thì Make sẽ thực thi hello và run trước, sau đó mới thực thi all. Nếu chỉ chạy make không thì target đầu tiên trong file sẽ được thực thi.

Các target trong Makefile thực chất là các tên tập tin. Như vậy có thể “dịch” nội dung target hello ở trên là “tôi cần file hello, và các lệnh dưới đây là hướng dẫn tạo file hello”. Bây giờ nếu bạn chạy make allmột lần nữa thì sẽ thấy là không còn dòng gcc hiện ra nữa. Target hello không được thực thi vì Make biết rõ là đã tồn tại file hello trong hệ thống. Nhưng nếu bạn make clean (để xóa file hello) thì nó sẽ lại được thực thi. Nó cũng sẽ được chạy nếu một trong các dependency của nó (hello.c và main.c) bị thay đổi.

Ý tưởng của Make là tạo một mạng lưới các tập tin và target phụ thuộc lẫn nhau và chỉ thực thi một target nếu kết quả của nó chưa tồn tại hoặc một trong các tập tin phụ thuộc của nó có thay đổi.

Lấy thêm một ví dụ, chúng ta có thể tạo file run trong cùng thư mục và target run cũng sẽ không được thực thi.

$ touch run
$ make all
make: Nothing to be done for 'all'.
$ rm run

Để tránh hiện tượng này thì ta có thể thêm dòng .PHONY vào cuối Makefile để ép Make luôn thực thi target run:

.PHONY: run

Nhìn lại Makefile, bạn có thể thấy là chúng ta lặp lại chữ hello quá nhiều. Hoàn toàn không tuân thủ theo nguyên tắc DRY (Don’t Repeat Yourself). Để giải quyết vấn đề này, chúng ta sẽ dùng biến.

PRGNAME = hello
HELLO_SRC = hello.c main.c

all: $(PRGNAME)

$(PRGNAME): $(HELLO_SRC)
    $(CC) $(HELLO_SRC) -o $PRGNAME

run: $(PRGNAME)
    ./$(PRGNAME)

clean:
    rm --recursive --force $(PRGNAME)

.PHONY: run clean

Makefile giờ trông đã “generic” hơn. Thậm chí gcc cũng đã được thay bằng biến $(CC), là biến sẵn có của Make, có nội dung là compile chuẩn của hệ thống. Nếu sau này bạn muốn compile bằng compiler khác như clang thì chỉ cần chạy CC=clang make all là được. Không cần phải thay đổi Makefile.

Với chương trình nhỏ vài file như thế này thì Makefile như vậy là hoàn toàn OK. Nhưng bạn có thể nghiên cứu thêm trên Google và tài liệu tra cứu của GNU Make. Ngoài ra tôi cũng thấy Makefile của trình phiên dịch ngôn ngữ Lua được viết cũng rất đơn giản mà vẫn cross-platform, mang sang Windows hay Mac OS X vẫn compile được. Rất hợp để nghiên cứu.

Sau này bạn có thể tìm hiểu thêm về Autotools và CMake để thay thế việc viết Makefile thủ công và tạo hệ thống build tương thích nhiều nền tảng mà không cần phải lo nghĩ nhiều.

conio.h

Tạm coi như bạn đã hiểu cách build đi. Nhưng khả năng cao là bạn sẽ vấp tiếp một vấn đề nữa. Phần lớn các giáo trình lập trình C/C++ ở Việt Nam được xây dựng dựa trên môi trường DOS/Windows. Bạn có thể nhận biết ngay điều này nếu thấy trong giáo trình có đề cập đến thư viện conio.h và hàm getch(). Thư viện này chỉ có cũng như chỉ hoạt động trên môi trường DOS/Windows. Mục đích của nó là cho phép bạn thực hiện các thao tác vào/ra trực tiếp với console.

Console (hay terminal) thông thường hoạt động trong chế độ có buffer (cooked mode). Tức là các phím nhấn của bạn không được gửi trực tiếp đến chương trình mà sẽ được giữ lại ở phần mềm console để cho phép bạn chỉnh sửa tự do trước khi nhấn ENTER và gửi đến chương trình. Tương tự, các thao tác printf() cũng không ghi trực tiếp vào console mà ghi vào một file đặc biệt, gọi là file stdout. Hệ thống sẽ chuyển tiếp nội dung file này vào console. File này có buffer nên đôi khi lệnh printf() của bạn sẽ không hiện gì ra màn hình hết và bạn phải fflush(stdout) thủ công. Conio.h cho bạn công cụ để đọc/ghi trực tiếp với console mà không qua buffer. VD: getch() sẽ return bất cứ ký tự nào vừa được nhấn xong mà không cần nhấn ENTER.

Chúng ta sẽ implement lại một số tính năng của conio.h trên Linux.

clrscr()

Trước hết là hàm clrscr() dùng để xóa trắng terminal. Với Linux thì có thể dụng lệnh clear:

system("clear");

getch()

Trên Windows, vì khi chạy từ IDE, cửa sổ console sẽ tắt ngay lập tức sau khi chương trình chạy xong nên người ta thường thêm lệnh getch() ngay cuối chương trình làm “đứng hình” console để nhìn rõ kết quả. Với Linux thì lúc nào chúng ta cũng ở trong terminal nên không cần lệnh này.

Nhưng nếu bạn cần tính năng kiểu Press any key to continue thì có thể dùng getchar() và đổi lời nhắn thành Press ENTER to continue.

Hoặc hardcore hơn là chuyển terminal về cbreak modegetchar(), rồi chuyển lại trở về cooked mode.

system("/bin/stty cbreak");
getchar();
system("/bin/stty cooked");

textcolor() và textbackground()

Terminal thông thường trên Linux tuân theo chuẩn VT100, chuẩn này có định nghĩa một số tính năng cho bạn điều khiển trực tiếp terminal sử dụng một loại mã đặc biệt gọi là escape code. Bạn gửi mã này tới terminal chỉ đơn giản bằng cách printf nó ra với ký tự đầu tiên là ESCAPE (mã ASCII 0x1B).

Ví dụ, để đổi màu text thành đỏ và background thành vàng chúng ta làm như sau:

printf("\x1B[31;43m");
printf("This text is red on yellow background.");
printf("\x1B[0m\n");  // Reset + new line
printf("This text is normal.\n");

Trong đó, 31 là mã màu đỏ foreground còn 43 là màu vàng background còn 0 là mã reset. Bạn có thể xem một số mã màu và các hiệu ứng khác ở đây.

gotoxy()

Tương tự như trên, chúng ta có thể printf mã <ESC>[{ROW};{COLUMN}H để đưa con trỏ tới vị trí bất kỳ trong terminal.

// Row 4; column 6
printf("\x1B[4;6H");

Pheww, vậy là xong một bài hướng dẫn khá dài. Hy vọng tôi đã giúp bạn hiểu hơn về lập trình C trên Linux nói riêng và Unix nói chung.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s