Bài giảng điện tử môn học Ngôn ngữ lập trình C

3. Tham số của hàm main() Nhiều chương trình khi được kích hoạt bằng một câu lệnh có thể cần biết tham số của người sử dụng đưa vào trong câu lệnh. Vậy làm thế nào để một chương trình C có thể nhận biết được những thông số này? Đó chính là thông qua tham số của hàm main() như sau: main(int agrc, char* agrv[]); Trong đó: - agrc là số tham số của câu lệnh kích hoạt chương trình (bao gồm cả tên câu lệnh). - argv là mảng các chuỗi tham số. Chú ý tham số đầu tiên của mảng luôn là tên của câu lệnh (tên chương trình) thực hiện.

pdf106 trang | Chia sẻ: huyhoang44 | Lượt xem: 679 | Lượt tải: 0download
Bạn đang xem trước 20 trang tài liệu Bài giảng điện tử môn học Ngôn ngữ lập trình C, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
n"); i = 0; while(strcmp(ten=nhapten(), "")!=0) danhsach[i++] = ten; n = i; sapxep(n, danhsach); printf("Danh sach ten da nhap:\n"); for(i=0; i<n; i++) printf("%3d. %s\n", i+1, danhsach[i]); } char* chuanhoa(char* s) { int i; i = 0; while (s[i]!='\0') if (s[i]==' '&&(i==0||s[i+1]==' '||s[i+1]=='\0')) strcpy(&s[i], &s[i+1]); else i++; return s; } void tachhoten(const char* hovaten, char* ho, char* ten) { char * ptim; int dodai; ptim = strrchr(hovaten, ' '); if (ptim==NULL) { strcpy(ten, hovaten); strcpy(ho, ""); }else { strcpy(ten, ptim+1); dodai = ptim-hovaten; strncpy(ho, hovaten, dodai); ho[dodai] = '\0'; } } char* nhapten() { char s[80]; printf("Nhap mot ten: "); gets(s); chuanhoa(s); return strdup(s); } void sapxep(int n, char *danhsach[]) { char ho1[80], ten1[80], ho2[80], ten2[80]; int i, j; char *tmp; for (i=0; i<n-1; i++) for(j=i+1; j<n; j++) { /* tách họ và tên của hai chuỗi rồi so sánh chúng */ tachhoten(danhsach[i], ho1, ten1); strupr(ho1); strupr(ten1); tachhoten(danhsach[j], ho2, ten2); strupr(ho2); strupr(ten2); if(strcmp(ten1, ten2)>0|| (strcmp(ten1, ten2)==0&&strcmp(ho1, ho2)>0)) { tmp = danhsach[i]; danhsach[i] = danhsach[j]; danhsach[j] = tmp; } } } BÀI TẬP Câu 1: Tạo một hàm để tìm ra một dãy số đơn điệu tăng có số phần tử lớn nhất trong một dãy số. Ví dụ dãy số 1 4 2 3 6 8 3 5 7 có dãy đơn điệu tăng với số phần tử lớn nhất là 2 3 6 8. Kết quả sau khi tìm kiếm sẽ là một con trỏ đến đầu dãy số tìm thấy và số phần tử của dãy đơn điệu tăng. Sử dụng hàm tạo ra để viết chương trình tìm dãy đơn điệu tăng có số phần tử lớn nhất cho một dãy số nhập vào. Chú ý nếu trường hợp có nhiều dãy có cùng số phần tử lớn nhất thì kết quả tìm thấy là dãy số đầu tiên. Câu 2: Viết hàm để tách từ đầu tiên ra khỏi một xâu kí tự. Cất từ tách được vào một vùng nhớ có địa chỉ truyền vào như là tham số của hàm. Xâu truyền vào sẽ chỉ giữ phần còn lại của xâu sau khi đã tách từ đầu tiên. Áp dụng hàm xây dựng được để viết chương trình in ra các từ của một xâu đọc vào từ bàn phím. Mục 4.3 - Bộ nhớ tĩnh Các biến tổng thể được cấp phát trên bộ nhớ tĩnh của chương trình. Chúng tồn tại trong suốt quá trình chương trình chạy. Chúng ta có thể thay đổi cấp lưu trữ của một biến chương trình vào các loại bộ nhớ khác nhau (tĩnh, thanh ghi,...) Yêu cầu: Đã có kiến thức về bộ nhớ động, bộ nhớ stack. Thời lượng: 2 tiết Bài 29 - Biến tổng thể và bộ nhớ tĩnh Tóm tắt nội dung: Một biến tổng thể được khai báo ngoài hàm và dùng tại mọi nơi trong chương trình. Ngược lại một biến cục bộ chỉ có tầm tác dụng trong chính hàm nơi nó khai báo. Thời lượng: 1 tiết Khác với biến cục bộ của một hàm, biến tổng thể của chương trình được cấp phát nhớ trên bộ nhớ tĩnh của chương trình. Bộ nhớ tĩnh của một chương trình được tạo ra ngay từ khi chương trình chạy và nó tồn tại cho đến khi chương trình kết thúc. Chính vì vậy một biến tổng thể có thể được sử dụng trong mọi hàm của chương trình. Ví dụ: int a; /* khai báo biến tổng thể */ void f() { int b; a = 5; /* truy cập biến tổng thể */ b = 5; /* truy cập biến cục bộ */ } void g() { a = 5; /* truy cập biến tổng thể */ b = 5; /* truy cập biến cục bộ mà không tồn tại */ } Trong một chương trình có thể đặt tên trùng nhau cho một biến tổng thể của chương trình và một biến cục bộ của hàm. Khi đó biến được dùng trong hàm luôn ưu tiên cho biến cục bộ. Ví dụ: int a; /* biến tổng thể a */ void f() { int a; /* biến cục bộ trùng tên biến tổng thể */ a = 5; /* lệnh gán cho biến cục bộ */ } Vì biến tổng thể có thể sử dụng được mọi nơi trong chương trình nên rất khó kiểm soát và dễ gây nhầm lẫn. Một trong những nguyên tắc cơ bản của việc thiết kế chương trình là hạn chế tối đa số biến tổng thể sử dụng trong chương trình. Nói chung ngoài những trường hợp cần biến tổng thể vào một mục đích nhất định nào đó còn không luôn chỉ sử dụng biến cục bộ trong chương trình. Để truyền thông tin giữa các hàm trong chương trình chúng ta có thể dùng tham số truyền vào và kết quả trả về của các hàm. Ví dụ: /* Một chương trình tạo danh sách móc nối động sử dụng biến tổng thể */ struct nut_hs { char ten[25]; int diem; struct nut_hs* tiep; }; /* khai báo biến tổng thể là con trỏ đến nút đầu danh sách */ struct nut_hs *nutdauds = NULL; /* hàm thêm một nút học sinh mới có tên (ten) và điểm (diem) vào danh sách */ void them_nuths(const char* ten, int diem) { struct nut_hs * hs; hs = (struct nut_hs*)malloc(sizeof(struct nut_hs)); strcpy(hs->ten, ten); hs->diem = diem; hs->tiep = nutdauds; nutdauds = hs; } void main() { them_nuths("Nguyen Van A", 9); } /* Một chương trình tương đương nhưng chỉ sử dụng biến cục bộ */ struct nut_hs { char ten[25]; int diem; struct nut_hs* tiep; }; /* hàm thêm một nút học sinh mới cần có tham số là con trỏ đến đầu danh sách cần thêm truyền theo dạng tham biến do nội con trỏ này sẽ bị thay đổi khi thêm nút vào đầu danh sách */ void them_nuths(struct nut_hs **nutdauds_ptr, const char* ten, int diem) { struct nut_hs * hs; hs = (struct nut_hs*)malloc(sizeof(struct nut_hs)); strcpy(hs->ten, ten); hs->diem = diem; hs->tiep = *nutdauds_ptr; *nutdauds_ptr = hs; } void main() { /* khai báo biến cục bộ là con trỏ đến nút đầu danh sách */ struct nut_hs *nutdauds = NULL; /* truyền con trỏ đầu danh sách cho hàm thêm nút theo địa chỉ */ them_nuths(&nutdauds, "Nguyen Van A", 9); } Bài 30 - Cấp lưu trữ đối tượng & các hàm thao tác bộ nhớ Tóm tắt nội dung: Ta có thể thay đổi cấp lưu trữ của một biến cục bộ từ bộ nhớ stack sang bộ nhớ tĩnh. Nó cho phép một biến cục bộ của hàm có thể tồn tại cho nhiều lần gọi hàm khác nhau. Trong bài này chúng ta cũng xem xét một số hàm thư viện cho phép thao tac trực tiếp với bộ nhớ của chương trình. Thời lượng: 1 tiết 1. Cấp lưu trữ đối tượng Một đối tượng dữ liệu được tạo ra trong chương trình luôn cần phải quan tâm đến tầm hoạt động của nó trong chương trình, thời gian tồn tại và vị trí lưu trữ của chúng. Nếu đối tượng được khai báo là biến tổng thể thì nó có mặt trong bộ nhớ tĩnh và tồn tại trong toàn bộ thời gian hoạt động của chương trình. Các biến cục bộ chỉ được cấp phát trên bộ nhớ stack khi hàm của chúng được gọi. Chúng được giải phóng ngay khi hàm kết thúc. Còn các đối tượng được cấp phát động chỉ không còn nữa khi được giải phóng (free()) bởi người sử dụng. Tuy nhiên chúng ta có thể thay đổi bộ nhớ lưu trữ mặc định cho các đối tượng trong chương trình. 2. Biến tĩnh (static) Với các biến cục bộ chỉ được sử dụng trong một hàm và tồn tại trong thời gian hàm hoạt động. Trong một số trường hợp ta phải cần một biến cục bộ tồn tại ngay cả khi hàm không hoạt động để dữ liệu của biến này không bị mất đi và có thể sử dụng lại trong những lần gọi hàm gọi sau. Ví dụ một biến cục bộ dùng để đếm số lần gọi cho hàm chứa nó. Khi đó ta chỉ cần khai báo biến cục bộ với từ khoá static. Một biến cục bộ được khai báo tĩnh (static) sẽ được lưu trữ trên bộ nhớ tĩnh thay vì trên bộ nhớ stack mặc định cho nó. Một biến cục bộ tĩnh khác với biến tổng thể ở chỗ nó vẫn chỉ có tầm hoạt động trong hàm mà thôi. Ví dụ: void func() { /* biến đếm được cấp phát trên bộ nhớ tĩnh ban đầu khởi tạo là 0 */ static int dem = 0; int a = 0; /* biến cục bộ thông thường */ dem++; /* đếm được tăng mỗi lần func được gọi */ a++; printf("dem = %d, a = %d\n", dem, a); } void main() { func(); func(); } /* kết quả màn hình dem = 1, a = 1 dem = 2, a = 1 */ 3. Biến thanh ghi (register) Một biến được khai báo với từ khoá register sẽ được lưu trữ trên các thanh ghi của của bộ vi xử lí của máy tính. Do tốc độ truy cập dữ liệu trên thanh ghi là rất nhanh, nhanh hơn tất cả các loại bộ nhớ khác nên việc khai báo biến register có thể giúp cải thiện tốc độ xử lí của chương trình. Tuy nhiên trong thực tế hiện nay biến register không thực sự là cần thiết nhiều trong các chương trình. Ví dụ: void f() { register int a; /* biến thanh ghi */ } 4. Các hàm thao tác bộ nhớ Toàn bộ dữ liệu xử lí của chương trình đều được đặt trong bộ nhớ trong của chương trình (trên bộ nhớ tĩnh, động hoặc là stack). Trong thực tế chúng ta rất cần các hàm chuyển dữ liệu từ vùng nhớ này sang vùng nhớ khác. Tất cả các hàm thư viện thao tác trên bộ nhớ dữ liệu dạng này có khai báo nguyên mẫu trong . void *memchr (void *s, int c, size_t n); /* Tìm một kí tự trong bộ nhớ s */ int memcmp (void *s1, void *s2, size_t n); /* So sánh dữ liệu tại hai vùng nhớ */ void *memcpy (void *dest, void *src, size_t n); /* Sao chép dữ liệu từ src sang dest */ void *memmove (void *dest, void *src, size_t n); /* Chuyển dữ liệu từ src sang dest */ void *memset (void *s, int c, size_t n); /* Xác lập giá trị kí tự toàn vùng nhớ */ Các hàm thao tác bộ nhớ đều cần tham số chỉ ra địa chỉ của một vùng nhớ (void*) và kích thước vùng nhớ (size_t) cho mỗi lần gọi hàm. Hàm memcmp() hoạt động giống hàm strcmp() nhưng việc so sánh của memcpy() là trên dữ liệu không dấu (unsigned char). Ví dụ: int a[5], b[5]; /* dùng memcpy() copy dữ liệu của mảng a cho giống mảng b */ memcpy(a, b, sizeof(a)); CHƯƠNG 5 - VÀO RA DỮ LIỆU Chương này trình bày các phương thức vào ra dữ liệu trong chương trình C. Kênh xuất nhập là phương thức vào ra có thể áp dụng trên mọi hệ thống mà không phụ thuộc vào hệ điều hành. Với phương thức này, việc vào ra dữ liệu được tổng quát hoá cho cả tệp và các thiết bị ngoại vi (màn hình, máy in, bàn phím,...). Phương thức thứ hai là vào ra tệp mức thấp cho phép truy cập trực tiếp tệp trên đĩa. Các hàm thao tác tệp mức thấp phụ thuộc vào hệ điều hành chương trình được dịch. Yêu cầu: Đã có kiến thức lập trình C căn bản. Thời lượng: 5 tiết Mục 5.1 - Vào ra dữ liệu qua vùng đệm Kênh xuất nhập là phương thức vào ra dữ liệu được thực hiện qua vùng đệm. Các hàm đọc và ghi chỉ truy cập dữ liệu trong vùng đệm mà không truy cập vào các thiết bị vật lí. Việc chuyển dữ liệu từ thiết bị vật lí vào vùng đệm và ngược lại được chuyển giao hoàn toàn cho hệ điều hành đảm nhiệm. Chính vì vậy mà việc vào ra với kênh xuất nhập là không phụ thuộc vào hệ điều hành. Dữ liệu vào ra trên kênh xuất nhập có hai dạng là chuỗi văn bản hoặc nhị phân. Yêu cầu: Đã tìm hiểu hai hàm vào ra printf() và scanf(). Thời lượng: 3 tiết Bài 31 - Kênh xuất nhập Tóm tắt nội dung: Kênh xuất nhập là phương thức vào ra dữ liệu được thực hiện qua vùng đệm. Các hàm đọc và ghi chỉ truy cập dữ liệu trong vùng đệm mà không truy cập vào các thiết bị vật lí. Việc chuyển dữ liệu từ thiết bị vật lí vào vùng đệm và ngược lại được chuyển giao hoàn toàn cho hệ điều hành đảm nhiệm. Có ba kênh xuất nhập chuẩn cho một chương trình là stdin, stdout và stderr dùng làm các kênh nhập, kênh xuất và kênh thông báo lỗi của chương trình. Thời lượng: 1 tiết 1. Kênh xuất nhập là gì? Kênh xuất nhập (stream) là một khái niệm quan trọng trong hệ thống vào ra qua vùng đệm của C. Đây là một phương thức vào ra bậc cao hoàn toàn độc lập với thiết bị phần cứng và hệ điều hành. Các thiết bị vật lí mặc dù có cách thức hoạt động vào ra dữ liệu rất khác nhau nhưng đều giao tiếp với chương trình qua vùng đệm (là một vùng nhớ đặt dữ liệu vào ra và được gọi là kênh). Các hàm xử lí đọc và ghi dữ liệu trong chương trình chỉ thao tác với dữ liệu ở vùng đệm này. Chính vì vậy mà chỉ có một phương thức chung đọc và ghi dữ liệu cho tất cả các loại thiết bị vào ra và lưu trữ dữ liệu như tệp, màn hình, bàn phím, máy in, Hình 10: Mô hình hoạt động của kênh xuất nhập Trong một chương trình luôn có 3 kênh chuẩn phục vụ vào ra dữ liệu. Đó là các kênh: - stdin: kênh nhập dữ liệu chuẩn - stdout: kênh xuất dữ liệu chuẩn - stderr: kênh báo lỗi chuẩn Trong thực tế hàm scanf() đã tìm hiểu thực hiện lấy dữ liệu trên kênh stdin, còn hàm printf() xuất dữ liệu ra kênh stdout. Nhưng vì khi một chương trình chạy bình thường các kênh stdin, stdout và stderr thường được gắn với thiết bị vào ra chuẩn là bàn phím và màn hình nên chúng ta thường coi chúng là các hàm làm việc với bàn phím và màn hình. Tuy vậy các kênh xuất nhập chuẩn của chương trình hoàn toàn có thể đổi hướng sang các thiết bị khác. Khi đó thay vì kết quả của chương trình được in ra màn hình thì có thể được in ra máy in chẳng hạn, dữ liệu vào cho chương trình thay vì phải nhập bằng tay thì có thể lấy từ một tệp. Ví dụ: /* một chương trình được dịch thành tệp chương trình test trên Unix */ void main() { int a; scanf("%d", &a); printf("a = %d", a); } /* chạy chương trình trên Unix */ /* chương trình được chạy không có đổi hướng vào ra chuẩn */ $test 10 a = 10 /* đổi hường vào chuẩn cho chương trình dữ liệu được lấy từ tệp songuyen.txt chứa số 10 */ $test <songuyen.txt a = 10 /* đổi hướng ra chuẩn cho chương trình, kết quả lưu vào tệp ketqua.txt */ $test >ketqua.txt 10 /* nội dung tệp ketqua.txt chứa a = 10 */ 2. Nhập xuất trên kênh vào ra chuẩn Một số hàm vào ra cơ bản trên kênh xuất nhập (nằm trong ) int printf(char *format, ); int scanf(char *format, ); void perror(const char *message); Hàm printf() cho phép đưa một chuỗi kí tự lên kênh stdout theo xâu định dạng format. Hàm này trả về kết quả là số kí tự đã được đưa lên kênh. Ngược lại hàm scanf() lấy dữ liệu trên kênh stdin vào chương trình. Cách thức lấy dữ liệu như thế nào được mô tả trong xâu format truyền vào cho hàm. Kết quả trả về của hàm scanf() là số dữ liệu đã lấy đúng theo mô tả yêu cầu. Hàm perror() dùng để đưa một thông báo lỗi lên kênh stderr. Xem bài 8 (Nhập xuất dữ liệu) để biết cách dùng hàm printf() và scanf(). Ví dụ: int a; printf("Nhap mot so nguyen:") if (scanf("%d", &a)!=1) perror("So nguyen da khong nhap duoc\n"); else printf("So nguyen da nhap la %d\n", a); Một số hàm vào ra cơ bản khác int getchar(void); /* nhập một kí tự trên kênh stdin */ int putchar(char ch); /* xuất một kí tự trên kênh stdout */ char* gets(char*); /* nhập một xâu kí tự trên kênh stdin cho đến khi gặp */ int puts(const char*); /* đưa một xâu kí tự ra stdout */ a. Khuôn dạng nhập dữ liệu Chúng ta đã biết là cần phải dùng các kí tự định dạng như %d, %c, ... để chỉ kiểu dạng dữ liệu nhập. Thực tế xâu định dạng dùng trong hàm scanf() chỉ đơn giản là thể hiện trình tự các loại dữ liệu xuất hiện trên kênh nhập mà thôi. Ví dụ nếu dữ liệu được vào là "abc 123 12.5" có thể được biểu diễn định dạng nhập là "%s%d%f". Khi sử dụng các định dạng nhập cần lưu ý những kí tự nào có thể lấy từ kênh cho mỗi định dạng. Định dạng nhập Ý nghĩa %d Lấy một số dạng thập phân, bỏ qua dấu cách hoặc nếu gặp %x Lấy một số dạng hexa, bỏ qua dấu cách hoặc nếu gặp %f Lấy một số thực, bỏ qua dấu cách hoặc nếu gặp %s Lấy một xâu kí tự trên kênh đến khi gặp dấu trắng hoặc %c Lấy duy nhất một kí tự Ví dụ: "%d%f" "123 ffff" Chỉ nhập được 123 "%d%s" "123 ffff" 123 và xâu có một kí tự cách "%d%c%s" "123 ffff" 123, kí tự cách và "ffff" "%d%x" "123 ffff" 123 và 0xffff "%s%d" "ffff 123" "ffff" và 123 b. Khuôn dạng in dữ liệu Chúng ta cũng đã tìm hiểu về cách tạo khuôn dạng cho dữ liệu in bằng %m hoặc %m.n với m là số ô chữ để in dữ liệu và n là số ô dành cho phần sau dấu chấm của số thực. Sau đây là dạng tổng quát dùng cho việc in dữ liệu. %[][][.] có thể dùng một hoặc vài kí tự sau đây theo thứ tự: - kí tự ‘-’ thể hiện dữ liệu được in căn lề phải, mặc định là được căn lề trái. - kí tự ‘+’ buộc các số luôn được in kí tự dấu kèm theo. có thể là một con số thể hiện cho độ rộng dành ra để in dữ liệu hoặc kí tự ‘*’ thể hiện giá trị độ rộng được lấy trong tham số tiếp theo truyền cho hàm. thể hiện số kí tự in cho phần sau dấu chấm của số thực (mặc định là 6). Nếu dung kí tự ‘*’ cho phần này thì giá trị cũng được lấy trong tham số tiếp theo truyền vào cho hàm. Ví dụ: char * s = "Hello!"; int r = 1, i = 2; printf("%s\n", s); /* in căn phải màn hình */ printf("%80s\n", s); /* in căn trái màn hình 80 cột */ /* để in căn giữa màn hình thì số ô dành cho in là 80-strlen(s)/2 */ printf("%*s\n", s, 80-strlen(s)/2); printf("%d%+di", r, i); /* in số phức với kết quả in là 1+2i */ BÀI TẬP Câu 1: Cho các biến dữ liệu int x, y; char ch, s[10]; Chỉ ra giá trị của các biến sau mỗi câu lệnh nhập khi dữ liệu nhập là "123^a^456^abc" (với ^ thể hiện cho dấu cách). a) scanf("%d%c%d%s", &x, &ch, &y, s); b) scanf("%d^%c%d^%s", &x, &ch, &y, s); c) scanf("%d%s%c%d ", &x, s, &ch, &y); d) scanf("%d%c%s%d ", &x, &ch, s, &y); e) scanf("%c%d^%s%d ", &ch, &x, s, &y); Câu 2: Viết chương trình sử dụng các khuôn dạng in để in bảng dữ liệu sau trên màn hình: +-----+---------------------------+------+ | STT | Ho va Ten | Điem | +-----+---------------------------+------+ | 1|Ta Tuan Anh | 9| | 2|Nguyen Hoa Binh | 8| | 3|Tran Van Nam | 10| | 4|Do Quoc Tuan | 6| +-----+---------------------------+------+ Bài 32 - Vào ra tệp với kênh xuất nhập Tóm tắt nội dung: Dữ liệu xuất nhập trên kênh có thể theo một trong hai dạng, chuỗi văn bản hoặc nhị phân. Hai hàm fprintf() và fscanf() dùng để xuất nhập theo dạng văn bản còn theo dạng nhi phân là fwrite() và fread(). Muốn đọc ghi dữ liệu với một tệp thì trước hết phải mở kênh cho tệp bằng hàm fopen(). Hàm fclose() dùng để đóng kênh. Thời lượng: 2 tiết Trong C chúng ta thường dùng kênh xuất nhập để đọc và ghi dữ liệu trên tệp. Các trình tự cần thiết để làm việc với tệp: mở một kênh xuất nhập cho tệp, đọc ghi dữ liệu và đóng tệp (kênh) lại. 1. Mở/đóng kênh cho tệp FILE *fopen(char *name, char *mode); /* mở tệp */ int fclose(FILE*); /* đóng tệp */ Một kênh xuất nhập sau khi mở cho một tệp được quản lý bởi một con trỏ FILE*. Nếu vì một lí do nào đó mà tệp không thể mở được thì kết quả của hàm này là con trỏ NULL. Hàm mở tệp này cần truyền vào tham số là một đường dẫn tên tệp (name), một chuỗi mô tả chế độ mở tệp. Ví dụ: FILE* f; f=fopen("dulieu.txt", "rt"); /* mở tệp dữ liệu văn bản ra để đọc */ if (p==NULL) perror("Loi khong mo duoc tep"); else { printf("Tep da mo duoc"); fclose(f); } Các chế độ mở đọc/ghi "r" Mở tệp để đọc "w" Mở tệp để ghi (nếu không có thì báo lỗi) "a" Mở tệp để thêm dữ liệu (nếu không có thì báo lỗi) "r+" Mở tệp để đọc và ghi (nếu không có thì tạo mới) "w+" Mở tệp để ghi (nếu không có thì tạo mới) "a+" Mở tệp để ghi thêm dữ liệu (nếu không có thì tạo mới) Một tệp có thể được xử lí đọc ghi dưới dạng nhị phân (binary) hoặc văn bản (text) phụ thuộc vào dữ liệu của chúng. Sự khác nhau giữa nhi phân và văn bản là ở dạng nhị phân dữ liệu chỉ đơn thuần là các byte dữ liệu. Ngược lại dữ liệu ở dạng văn bản là các kí tự trong đó có phân biệt kí tự hiển thị và kí tự điều khiển. Ví dụ ‘\n’ là điều khiển xuống dòng được in vào vào một tệp dưới DOS hai mã 13 và 10, còn nếu là tệp nhị phân thì chỉ có 1 byte số 13 mà thôi. Ngoài ra ở dạng văn bản khi gặp một số 26 (^Z) thì coi nó như là điểm kết thúc tệp bất kể tệp vẫn còn dữ liệu tiếp theo. Muốn mở ở chế độ văn bản ta chỉ cần thêm kí tự ‘t’ vào chế độ mở, kí tự ‘b’ nếu mở ở dạng nhị phân ("r+t", "wt", "r+b",...). Vào ra dữ liệu với tệp lúc này thực chất là vào ra dữ liệu trên kênh xuất nhập. Có hai nhóm hàm vào ra trên kênh là nhóm vào ra dạng văn bản và nhóm vào ra dạng nhị phân. 2. Các hàm vào ra dạng văn bản Vào ra trên kênh dạng văn bản hoàn toàn giống với vào ra trên các kênh stdin và stdout. Thực chất các hàm vào ra này là các hàm vào ra tổng quát trên kênh đã được áp dụng trên hai kênh cụ thể là stdin và stdout. Vào ra kênh tổng quát Vào ra với stdin, stdout int fprintf(FILE* f, char*, ...) int printf(char*) int fscanf(FILE* f, char*, ...) int scanf(char*) int putc(int c, FILE* f) int putchar(int c) int getc(FILE* f) int getchar(void) int fputs(char* str, FILE* f) int puts(char*) char* fgets(char* str, int num_max, FILE* f) char* gets(char*) Chú ý hàm fgets() có khác biệt so với hàm gets() là nó cần thêm thông tin là số kí tự nhiều nhất của xâu lấy cho một lần nhập. Xâu kết quả nhập sẽ không được tự động bỏ như khi dùng hàm gets(). 3. Các hàm vào ra dạng nhị phân size_t fread(void* buf, size_t size, size_t num, FILE* f); size_t fwrite(void* buf, size_t size, size_t num, FILE* f); Hàm fread(), fwrite() cho phép đọc và ghi dữ liệu trên bộ nhớ có địa chỉ trỏ bởi buf, có số phần tử là num, kích thước mỗi phần tử là size. Như vậy tổng số byte cần đọc/ghi với vùng nhớ là size*num. Hai hàm này trả về kích thước thực đã thực hiện đọc/ghi. Ví dụ: FILE* f; int a[10]; f=fopen("songuyen.dat", "w+t"); if (f !=NULL) { /* ghi 10 số nguyên ra tệp nhị phân */ fwrite(a, 10, sizeof(int), f); fclose(f); } 4. Các hàm kiểm tra trạng thái và dịch truyển con trỏ kênh int feof(FILE *stream); /* kiểm tra kênh đã hết dữ liệu chưa */ int ferror(FILE *stream); /* lấy giá trị cờ lỗi hiện tại trên kênh */ void clearerr(FILE *stream); /* xoá trạng thái lỗi của kênh */ int fflush(FILE* f); /* đẩy dữ liệu ra thiết bị hoặc làm sạch dữ liệu có trên một kênh */ int fseek(FILE* f, long offset, int whence); /* dịch con trỏ đến vị trí mới */ void rewind(FILE* f); /* đưa con trỏ bắt đầu lại từ đầu tệp */ Hàm fseek() cho phép dịch con trỏ đến một vị trí có độ lệch tương đối so với một vị trí gốc chỉ ra trong tham sô whence. Các hằng số có thể dùng cho tham số này là: Hằng số Giá trị Ý nghĩa SEEK_SET 0 Tính từ đầu tệp SEEK_CUR 1 Tính từ vị trí hiện tại của con trỏ SEEK_END 2 Tính từ cuối tệp Chú ý khi nhập dữ liệu trên kênh stdin ta thường làm sạch kênh này bằng fflush(stdin) trước mỗi lần nhập xâu hay kí tự. Lí do cần phải làm sạch là do hiện tượng để dư thừa lại kí tự của những lần nhập dữ liệu số trước đó. Ví dụ: int a; char ch; /* nhập số chỉ lấy hết kí tự số còn để lại trên stdin */ scanf("%d", &a); fflush(stdin); /* xoá còn dư trên stdin */ ch = getchar(); /* sẵn sàng cho nhập một kí tự */ Chương trình mẫu (tep.c): Nhập dữ liệu của một danh sách học sinh và ghi ra tệp. Sau đó đọc lại tệp để in ra những học sinh cần kiểm tra lại. #include #define TEPHS "hocsinh.txt" /* hàm nhập học sinh và ghi dữ liệu ra tệp */ int nhaphs() { FILE* f; char ten[25]; int diem; f = fopen(TEPHS, "w+t"); /* mở để ghi */ if (f==NULL) { perror("Loi khong mo duoc tep"); return 0; /* bị lỗi trả về 0 */ } printf("Nhap danh sach hoc sinh (go ten rong de ket thuc)\n"); for (;;) { fflush(stdin); /* làm sạch vùng đệm để nhập dữ liệu */ printf("Ten: "); gets(ten); if (strcmp(ten, "")==0) break; printf("Diem: "); scanf("%d", &diem); /* ghi dữ liệu ra tệp, sử dụng 2 dòng dữ liệu cho mỗi học sinh */ fprintf(f, "%s\n%d\n", ten, diem); } fclose(f); return 1; } /* hàm đọc dữ liệu từ tệp và in ra những học sinh cần thi lại */ void doctep() { FILE* f; char ten[25]; int diem; f = fopen(TEPHS, "rt"); /* mở để đọc */ if (f==NULL) { perror("Loi mo tep de doc"); return; } printf("Danh sach ten hoc sinh thi lai:\n"); while (!feof(f)) { /* nhập tên có kí tự kết thúc */ fgets(tep, sizeof(ten), f); fscanf(f, "%d\n", &diem); if (diem <5) printf("%s\n", ten); } fclose(f); } void main() { if (nhaphs()) doctep(); } 5. Mô phỏng vào ra trên xâu dữ liệu Trong C chúng ta có thể sử dụng một xâu dữ liệu cho nó đóng vai trò vùng đệm của kênh vào ra. Khi đó việc đọc/ghi dữ liệu được thực hiện trên chính bộ nhớ của xâu dữ liệu thay vì trên kênh xuất nhập nào đó. Hai hàm mô phỏng này có tên sprintf() và sscanf(). int sprintf(char *string, char *format, args..); int sscanf(char *string, char *format, args..); Ví dụ: char *s = "123 456"; int a, b; sscanf(s, "%d%d", &a, &b); /* a = 123, b = 456 */ BÀI TẬP Câu 1: Viết chương trình tạo ra tệp văn bản F3 từ việc ghép nội dung hai tệp F1 và F2 Câu 2: Viết chương trình đếm số từ, số dòng có trong một tệp văn bản. Câu 3: Viết chương trình tạo ra một tệp văn bản chứa Tên, Tuổi, Địa chỉ của các cá nhân (mỗi thông tin một dòng). Sau đó chương trình sẽ đọc lại tệp này để in ra màn hình nội dung trên nhưng là một dòng cho một người. Mục 5.2 - Vào ra mức thấp Vào ra mức thấp là phương thức can thiệp trực tiếp với các thiết bị dữ liệu. Phương thức này phụ thuộc chặt vào hệ thống mà chương trình viết cho. Nó không thể đảm bảo tính khả chuyển của chương trình trên nhiều hệ thống. Mục này giới thiệu về vào ra tệp mức thấp trên UNIX và các thao tác trực tiếp với màn hình, bàn phím trên DOS. Yêu cầu: Có kiến thức sử dụng hệ điều hành UNIX. Thời lượng: 2 tiết Bài 33 - Các hàm vào ra tệp mức thấp Tóm tắt nội dung: Có thể đọc ghi trực tiếp tệp dữ liệu (dạng nhị phân) trên UNIX bằng các hàm read() và write(). Các tệp được mở khi muốn đọc và ghi bằng hàm open(). Hàm close() cho phép đóng tệp. Thời lượng: 1 tiết Ngôn ngữ C ban đầu được thiết kế để viết các chương trình chạy trên UNIX. Khi đó việc đọc và ghi dữ liệu trên tệp được thực hiện thông qua các hàm thao tác trực tiếp với dữ liệu của tệp trên đĩa. Với phương thức vào ra này dữ liệu đọc/ghi chỉ là dữ liệu dạng byte nhị phân và không có vùng bộ nhớ đệm cho việc đọc/ghi. Chính vì vậy mà ta còn gọi phương thức vào ra là vào ra tệp mức thấp. Người lập trình được khuyến cáo là tránh sử dụng các hàm vào ra tệp mức thấp vì các hàm này không đảm bảo tính khả chuyển trên nhiều hệ thống khác nhau. Một số hàm vào ra tệp mức thấp () int open(char *filename, int flag[, int perms]); /* mở tệp */ int close(int handle); /* đóng tệp có thẻ tệp handle */ int creat(char *filename, int perms); /* tạo tệp */ int read(int handle, char *buffer, unsigned n); /* đọc dữ liệu trên tệp */ int write(int handle, char *buffer, unsigned n); /* ghi dữ liệu vào tệp */ long lseek(int handle, int offset, int whence); /* dịch chuyển con trỏ tệp */ Hàm mở tệp trả về kết quả là một thẻ tệp tương ứng cho tệp đã được mở. Tất cả các tệp khi được mở trong một hệ thống đều được quản lí thông qua một thẻ tệp. Nhờ những thẻ tệp này mà hệ điều hành biết được tệp nào đang hay không được mở bởi ứng dụng để tránh sự tương tranh. Kết quả là -1 nếu tệp không thể mở được. Giá trị hằng số cờ (flag) cho hàm mở tệp được định nghĩa trong tệp tiêu đề . Một số hằng số chế độ mở tệp hay dùng Hằng số Ý nghĩa O_APPEND Ghi vào cuối tệp O_CREAT Tạo tệp với tham số perms O_RDONLY Mở để chỉ đọc O_RDWR Mở để đọc và ghi O_TRUNC Xoá nội dung tệp hiện đang có O_WRONLY Mở để chỉ ghi Tham số perms chỉ có ý nghĩa với chế độ mở O_CREAT (các chế độ khác đặt perms = 0) để xác lập quyền cho một tệp được tạo ra (vì trên UNIX mỗi tệp có thuộc tính quyền truy nhập). Tham số này có thể nhận giá trị là các hằng số định nghĩa trong . Hằng số quyền Ý nghĩa S_IWRITE Cho phép ghi S_IREAD Cho phép đọc S_IWRITE| S_IREAD Cho phép cả đọc và ghi Hai hàm read(), write() cho phép đọc/ghi dữ liệu trên tệp vào bộ nhớ buffer của chương trình với kích thước n bytes. Kết quả trả về của hai hàm này là số bytes thực sự đã đọc/ghi. Nếu trả về -1 có nghĩa là việc đọc/ghi bị lỗi. Đối với hàm read() khi đọc trả về 0 có nghĩa là dữ liệu đã đọc hết và con trỏ đã đến cuối tệp. Ý nghĩa của các tham số truyền vào cho hàm lseek() giống hàm fseek(). Ví dụ: /* chương trình đọc số thực trong một tệp nhị phân số nguyên đầu tiên là số số thực trong file */ #include #include #define FILENAME "reals.dat" float bigbuff[1000]; main() { int fd, file_length; if ( (fd = open(FILENAME,O_RDONLY)) == -1) { perror("Loi tep khong mo duoc"); exit(1); } if (read(fd,&file_length, sizeof(int)) == -1) { perror("Loi dinh dang tep du lieu"); exit(1); } if (read(fd, bigbuff, file_length*sizeof(float)) == -1) { perror("Loi doc doc du lieu"); exit(1); } printf("Da doc tep xong"); close(fd); } Bài 34 - Thao tác với bàn phím và màn hình Tóm tắt nội dung: Khi viết một chương trình trên hệ điều hành DOS chúng ta có thể sử dụng một số hàm thao tác trực tiếp với bàn phím và màn hình để phục vụ tạo giao diện cho chương trình. Tất cả các hàm thao tác trực tiếp thiết bị này được khai báo nguyên mẫu trong tệp tiêu đề . Thời lượng: 1 tiết Một chương trình được viết chạy trên hệ điều hành DOS có thể sử dụng một số hàm thao tác với màn hình và bàn phím có khai báo trong tệp để tạo giao diện cho chương trình. Đây cũng là các hàm vào ra mức thấp vì nó thao tác trực tiếp với các thiết bị vật lí. 1. Các hàm trình bày màn hình void clrscr(void); /* xoá nội dung trên mà hình */ void clreol(void); /* xoá hết dòng */ void gotoxy(int x, int y); /* chuyển con trỏ màn hình đến toạ độ (x, y) */ int wherex(void); /* lấy vị trí toạ độ x */ int wherey(void); /* lấy vị trí toạ độ y */ void textcolor(int color); /* xác lập màu của chữ in */ void textbackground(int color); /* xác lập màu của nền */ void textattr(int attr); /* xác lập thuộc tính màu cho kí tự in */ void window(int x1, int y1, int x2, int y2); /* xác lập cửa sổ in văn bản trên màn hình */ Các hằng số màu được định nghĩa trong enum COLORS { BLACK, /* màu đậm */ BLUE, GREEN, CYAN, RED, MAGENTA, BROWN, LIGHTGRAY, DARKGRAY, /* màu nhạt */ LIGHTBLUE, LIGHTGREEN, LIGHTCYAN, LIGHTRED, LIGHTMAGENTA, YELLOW, WHITE }; 2. Các hàm xử lí bàn phím int getch(void); /* đọc kí tự vừa gõ trên bàn phím, không cần đợi */ int getche(void); /* đọc kí tự vừa gõ đồng thời in nó ra màn hình */ int kbhit(void); /* chờ một phím nhấn */ Các hàm trên đây khác với các hàm nhập dữ liệu chuẩn trên kênh stdin ở chỗ nó lấy dữ liệu trực tiếp từ vùng đệm vật lí của bàn phím. Trong khi đó các hàm nhập dữ liệu chuẩn lấy dữ liệu trên kênh stdin. Trong trường hợp stdin được kết nối với bàn phím thì mọi dữ liệu gõ trên bàn phím chỉ được đưa vào kênh stdin khi được nhấn. Ví dụ: /* Chương trình in dòng chữ Hello! giữa màn hình màu xanh lơ, chữ đỏ có nhấp nháy */ #include #include void main() { int attr; textbackground(BLUE); clrscr(); /* xác lập thuộc tính kí tự in vào attr */ attr = RED; /* 4 bit thấp thể hiện mầu chữ */ attr |= BLUE<<4; /* 3 bit tiếp theo thể hiện màu nền */ attr |= 0x80; /* bit thứ 8 thể hiện nhấp nháy */ textattr(attr); gotoxy(37, 13); /* nhảy ra giữa mà hình */ printf("Hello!"); /* chờ phím nhấn để kết thúc */ kbhit(); } CHƯƠNG 6 - TIỀN XỬ LÍ VÀ TỆP TIÊU ĐỀ Trong chương này giới thiệu bộ tiền xử lí của C. Các chỉ thị tiền xử lí giúp hướng dẫn quá trình biên dịch với các thao tác gộp tệp, tạo macro hoặc biên dịch có điều kiện. Tệp tiêu đề có vai trò quan trọng trong việc liên kết nhiều tệp chương trình nguồn của một dự án lớn. Yêu cầu: Đã tìm hiểu lập trình C căn bản. Thời lượng: 4 tiết Mục 6.1 - Bộ tiền xử lí Các chỉ thị tiền xử lí không phải là một câu lệnh của chương trình. Chúng giúp hướng dẫn quá trình biên dịch với các thao tác gộp tệp, tạo macro hoặc biên dịch có điều kiện. Một chỉ thị tiền xử lí luôn có cú pháp bắt đầu bằng kí tự # (ví dụ: #include, #define, #error,...). Yêu cầu: Có kiến thức về C căn bản Thời lượng: 2 tiết Bài 35 - Một số chỉ thị tiền xử lí Tóm tắt nội dung: Những bí danh sử dụng trong chương trình đã được định nghĩa cho một giá trị với #define thì sẽ được thay thế trước khi biên dịch bằng giá trị thật của nó. Ta có thể dùng #define để tạo hằng số hoặc macro cho một chương trình. Bài này cũng giới thiệu các chỉ thị tiền xử lí #include và #error Thời lượng: 1 tiết 1. Chỉ thị tiền xử lí #define Công đoạn đầu tiên của biên dịch là tiền xử lí (xem bài 4 - Quá trình biên dịch). Ngoài một số phép xử lí đơn giản như xoá bỏ tất cả các chú thích thì công đoạn này thực hiện thay thế tất cả các bí danh được định nghĩa bằng #define trong chương trình bằng giá trị thật của nó. Người ta có thể các bí danh này làm vai trò là hằng số hoặc macro trong chương trình. Chú ý các chỉ thị tiền xử lí bắt đầu bằng # không phải là một lệnh trong chương trình nên không có dấu chấm phẩy ‘;’ kết thúc. a. Đĩnh nghĩa hằng số #define Ví dụ: #define TRUE 1 #define FALSE 0 int flag = TRUE; /* câu lệnh thực sau tiền xử lí: int flag = 1; */ b. Định nghĩa macro #define () Ta có thể dùng #define để định nghĩa các macro biểu thức với tham số truyền vào. Ví dụ: #define MULT(a, b) ((a)*(b)) int x = MULT(2, 3); /* sau tiền xử lí câu lệnh là: int x = ((2)*(3)); */ Chú ý khi định nghĩa các macro ta luôn dùng các dấu đóng mở ngoặc cho tham số để đảm bảo thứ tự tính toán của biểu thức luôn đúng. Ví dụ: /* macro 1 thiếu () cho các tham số */ #define MULT1(a, b) a*b /* macro 2 có đầy đủ () cho các tham số */ #define MULT2(a,b) ((a)*(b)) int x = 2*MULT1(1+2, 3); /* sau tiền xử lí: x = 2*1+2*3 */ int y = 2*MULT2(1+2, 3); /* sau tiền xử lí: y = 2*((1+2)*(3)) */ /* kết quả của hai macro rõ ràng hoàn toàn khác nhau */ Một số macro hay dùng: #define ABS(x) ( (x)>=0 ? (x): (-(x)) ) #define MAX(a, b) ((a)>=(b) ? (a): (b)) #define MIN(a, b) ((a)<=(b) ? (a): (b)) #define SQR(x) ((x)*(x)) 2. Chỉ thị tiền xử lí #undefine Khi không muốn dùng một hằng hay macro nữa trong chương trình ta có thể dùng #undefine để loại bỏ nó. Ví dụ: #undefine TRUE #undefine FALSE #undefine MULT(a, b) 3. Chỉ thị #include Đây là chỉ thị được sử dụng trong tất cả các chương trình. Chỉ thị này yêu cầu ghép một tệp nguồn vào chương trình hiện tại. Chúng ta thường phải ghép các tệp tiêu đề (.h) vào một chương trình là vì trong các tệp tiêu đề này có chứa khai báo nguyên mẫu của các hàm thư viện được gọi trong chương trình. Như vậy khi ở phần đầu chương trình có chỉ thị ghép các tệp tiêu đề thì các khai báo nguyên mẫu hàm luôn được dịch trước những lời gọi hàm để đảm bảo việc dịch không bị lỗi. Có hai kí pháp dùng cho #include #include #include "tên tệp" Tên tệp có thể có cả đường dẫn thư mục. Tuy nhiên để đảm bảo tính tương thích khi dịch chương trình trên nhiều máy, tên tệp luôn được để dạng đường dẫn tương đối. Sự khác nhau của kí pháp và "tên tệp" là vị trí thư mục gốc quy ước cho đường dẫn tương đối. Với thì vị trí thư mục gốc là các thư mục đã được đặt trước cho các tệp tiêu đề dùng trong hệ thống. Còn với "tên têp" thì thư mục gốc chính là thư mục chứa chương trình đang được dịch. Ví dụ: /* ghép các tệp tiêu đề của hệ thống */ #include #include /* ghép tệp tiêu đề của chương trình */ #include "prog.h" 4. Chỉ thị #error #error Chỉ thị này làm quá trình biên dịch dừng ngay lập tức đồng thời thông báo ra một lỗi biên dịch theo dòng thông báo đi sau chỉ thị Ví dụ: #error Bien dich bi dung lai o day Bài 36 - Biên dịch có điều kiện Tóm tắt nội dung: Các chỉ thị biên dich có điều kiện #if, #ifdef, #ifndef dùng để hướng dẫn quá trình biên dịch có thể dịch hay bỏ qua một số câu lệnh theo một điều kiện chỉ ra. Thời lượng: 1 tiết Biên dich có điều kiện là điều khiển quá trình biên dịch sẽ thực hiện dịch hay bỏ qua một số câu lệnh trong chương trình theo một điều kiện nào đó. Xét một ví dụ sau: #if !define(PI) #define PI 3.14 #endif Trong đoạn chương trình chỉ thị #define chỉ được dịch khi việc kiểm tra hằng số PI chưa được định nghĩa. Điều đó đảm bảo tính logic trong khi dịch chương trình là hằng số PI chỉ cần định nghĩa khi nó chưa được định nghĩa trước đó. Cấu trúc tổng quát của biên dịch có điều kiện #if [#else] [#elif] #endif Chỉ thị #elif tương đương như #else kết hợp với #if. Một trong những cách sử dụng thông thường của #if là: #ifdef /* viết tắt của #if define() */ #ifndef /* viết tắt của #if !define() */ Ví dụ: /* kiểm tra xem có phải chương trình được dich bởi Turbo C */ #ifdef TURBOC /* nếu đúng kích thước của int là 2 byte */ #define INT_SIZE 16 #else /* kích thước của int trên Unix là 4 byte */ #define INT_SIZE 32 #endif Trong nhiều chương trình khi được thiết kế để chỉ chạy trên một hệ điều hành nhất định, ta thường thêm một đoạn kiểm tra để dừng biên dịch nếu chương trình được dịch trên một hệ điều hành khác thiết kế. Ví dụ: #if SYSTEM != MSDOS #error Program only run on MSDOS #endif Biên dịch có điều kiện còn giúp cho việc tạo một chương trình mà có thể dịch thành hai phiên bản: có gỡ rối (debug) và không có gỡ rối. Ví dụ: #ifdef DEBUG printf("Debugging Version\n"); #endif int x, y; x = y *3; #ifdef DEBUG printf("Debugging: Variables (x=%d, y=%d)\n",x,y); #endif Khi muốn tạo phiên bản có debug thì ta chỉ cần thêm định nghĩa #define DEBUG ở ngay dòng đầu tiên của chương trình. Làm như vậy thì tất cả các lệnh in giá trị của các biến thay đổi từng bước sẽ có mặt trong chương trình giúp chúng ta lần bước được sự hoạt động của chương trình. Khi không còn cần gỡ rối nữa thì ta chỉ việc bỏ #define DEBUG đi để biên dich có điều kiện loại bỏ tất cả các lệnh in giá trị lần bước trong chương trình Mục 6.2 - Tệp tiêu đề Tệp tiêu đề thường chứa khai báo nguyên mẫu của các hàm. Trong các tệp chương trình ta phải dùng #include gộp thêm các tệp tiêu đề là để có khai báo nguyên mẫu trước mỗi lời gọi hàm thư viện. Các tệp tiêu đề có vai trò quan trọng trong việc liên kết nhiều tệp nguồn của một dự án chương trình. Yêu cầu: Có kiến thức về hàm và bộ tiền xử lí của C. Thời lượng: 2 tiết Bài 37 - Chương trình có nhiều tệp nguồn Tóm tắt nội dung: Các hàm của một tệp chương trình nguồn thường được khai báo nguyên mẫu trong một tệp tiêu đề. Tệp này được khai báo #include trong một tệp nguồn khác nếu trong nó có gọi hàm được khai báo nguyên mẫu trong tệp tiêu đề này. Thời lượng: 1 tiết 1. Dự án chương trình Trong các chương trình lớn việc viết tất cả mã chương trình trên cùng một tệp là điều không thể. Để phân chia công việc viết chương trình, mỗi người lập trình sẽ đảm nhiệm viết một số hàm và mỗi người sẽ viết trên một tệp chương trình khác nhau. Vì đây là một chương trình một hàm có thể gọi một hàm khác mà được viết trong một tệp khác. Vậy trong trường hợp này trình biên dịch sẽ phải làm thế nào để có thể hợp nhất nhiều têp mã nguồn khác nhau thành một chương trình duy nhất? Làm thế nào để khi chúng ta viết chương trình gọi một hàm được viết trong một tệp khác mà trình biên dịch không báo lỗi? Chúng ta xem xét vấn đề này qua một ví dụ về chương trình có hai tệp nguồn "func.c" và "main.c". Tệp nguồn "func.c": int func1(int, int) { ...} void func2(int) { ... } int func3() { ... } Tệp nguồn "main.c": void main() { func1(5, 6); func2(2); func3(); } Trong ví dụ này hàm main() viết trong tệp "main.c" gọi đến các hàm viết trong tệp "func.c". Theo nguyên tắc một lời gọi hàm sẽ không bị báo lỗi nếu trước đó đã có khai báo nguyên mẫu của hàm. Do vậy trong tệp "main.c" cần phải có khai báo nguyên mẫu của những hàm được gọi như sau. Tệp nguồn "main.c" có khai báo nguyên mẫu hàm được gọi: int func1(int, int); void func2(int); int func3(); void main() { func1(5, 6); func2(2); func3(); } Tuy nhiên nếu viết như thế này thì người viết tệp main.c phải viết nguyên mẫu cho những hàm mà anh ta không phụ trách viết ra. Đáng lí ra thì chính người viết các hàm này sẽ phải viết nguyên mẫu cho chúng để những người sử dụng chỉ cần xem nguyên mẫu hàm của anh ta thế nào và gọi chúng mà thôi. Để làm được điều này người viết hàm của tệp func.c sẽ viết thêm một tệp tiêu đề mà chứa tất cả khai báo nguyên mẫu hàm mà anh ta viết như sau. Tệp tiêu đề "func.h": int func1(int, int); void func2(int); int func3(); Bây giờ trong tệp "main.c" không cần tự viết khai báo nguyên mẫu hàm cần gọi nữa mà chỉ việc #include tệp tiêu đề "func.h". Khi đó các khai báo nguyên mẫu hàm trong "func.h" sẽ được ghép vào tệp "main.c". Tệp nguồn "main.c": #include "func.h" void main() { func1(5, 6); func2(2); func3(); } Như vậy có thể thấy vai trò của các tệp tiêu đề như là nơi chứa "giao diện" của các hàm có thể gọi trong chương trình. Khi cần gọi đến hàm nào thì ta chỉ cần #include tệp tiêu đề mà chứa khai báo nguyên mẫu hàm đó. Các tệp tiêu đề chuẩn của hệ thống như , , ... cũng đóng vai trò này cho các hàm thư viện chuẩn của hệ thống. Một chương trình viết với nhiều tệp nguồn được dịch trong hai pha. Pha thứ nhất là dịch riêng rẽ mỗi tệp nguồn để chuyển thành tệp mã máy. Pha thứ hai là thực hiện liên kết tất cả các tệp mã máy có từ pha dịch để tạo thành một tệp chạy duy nhất. Vai trò của pha liên kết là gắn kết các lời gọi hàm trong chương trình đến phần mã máy thực sự cho nó. Do vậy khi một chương trình có lời gọi một hàm mà hàm này không có khai báo thi hành thì sẽ bị báo lỗi tại pha này. Pha liên k?t Pha d?ch main.c func.c main.o func.o prog.exe Thu vi?n hàm Hình 11: Sơ đồ biên dịch cho ví dụ trên Khi sử dụng một IDE để biên dịch một chương trình với nhiều tệp nguồn thì cần phải tạo dự án (project). Trong dự án ta khai báo tất cả các tệp nguồn có trong chương trình để giúp cho quá trình biên dịch có thể thực hiện dịch và liên kết tự động các tệp cần thiết. 2. Khai báo biến extern Theo nguyên tắc một biến tổng thể có thể sử dụng bởi mọi hàm trong chương trình. Nhưng khi chúng ta viết chương trình với nhiều tệp nguồn thì một biến tổng thể chỉ có thể khai báo một lần trong một tệp nào đó mà thôi. Vậy làm thế nào để một hàm ở những tệp nguồn khác vẫn có thể sử dụng biến tổng thể này? Muốn làm được như vậy chúng ta cần phải có khai báo biến dạng extern. Tệp nguồn "func.c": int global; /* khai báo biến tổng thể */ int func1(int, int) { ...} void func2(int) { ... } int func3() { ... } Tệp nguồn "main.c": #include "func.h" void main() { /* khai báo extern chỉ ra rằng global là biến đã có bên ngoài */ extern int global; global = 5; /* sử dụng biến đã có */ func1(5, 6); func2(2); func3(); } Bài 38 - Khuôn dạng tệp tiêu đề Tóm tắt nội dung: Bài này trình bày phương pháp viết tệp tiêu đề cho các tệp nguồn của chương trình. Thời lượng: 1 tiết Trong một chương trình lớn với nhiều tệp nguồn thường thì chúng ta tạo tệp tiêu đề cho mỗi tệp nguồn. Trong mỗi tệp tiêu đề chứa khai báo nguyên mẫu hàm và cấu trúc dữ liệu có sử dụng trong tệp nguồn. Vì lí do một hàm có thể được gọi ở trong nhiều tệp nguồn khác nhau nên tệp tiêu đề cho nó cũng có thể bị dịch nhiều lần khác nhau (mỗi một #include cho tệp là một lần dịch). Để tránh nội dung của một tệp tiêu đề có thể bị dịch nhiều lần trong chương trình người ta thường viết tệp tiêu đề theo khuôn mẫu sau. #ifndef #define #endif Ví dụ tệp tiêu đề "func.h": #ifndef __FUNC_H #define __FUNC_H int func1(int, int); void func2(int); int func3(); #endif Trong lần dịch đầu tiên __FUNC_H chưa được định nghĩa nên biên dịch có điều kiện sẽ thực hiện dịch nội dung có trong tệp tiêu đề. Nhưng đến khi dịch lần thứ hai do __FUNC_H đã được định nghĩa trong lần dịch thứ nhất nên biên dịch có điều kiện bỏ quả không dịch nội dung khai báo trong tệp tiêu đề. Chương trình mẫu với nhiều tệp nguồn: Quản lí điểm học sinh Tệp tiêu đề "list.h": #ifndef __LIST_H #define __LIST_H /* khai báo cấu trúc danh sách và các hàm xử lí trên danh sách */ struct nut_hs { char ten[25]; int diem; struct nut_hs* tiep; }; /* thêm một học sinh có ten vào danh sách có con trỏ đầu header con trỏ đầu header của danh sách bị thay đổi nên truyền dạng tham biến */ void them_nuths(struct nut_hs**header_ptr, char* ten); /* xoá một học sinh có ten vào danh sách có con trỏ đầu header con trỏ đầu header của danh sách bị thay đổi nên truyền dạng tham biến */ int xoa_nuths(struct nut_hs**header_ptr, char *ten); /* tìm một nút học sinh có tên truyền vào trong danh sách trỏ bởi header kết quả là con trỏ đến nút tìm thấy, NULL nếu không tìm thấy */ struct nut_hs* tim_nuths(struct nut_hs*header, char *ten); /* sắp xếp danh sách trỏ bởi header theo tên học sinh */ void sapxep_nuths(struct nut_hs*header); #endif Tệp nguồn "list.c": /* thi hành cho tệp list.h */ #include "list.h" #include #include #include void them_nuths(struct nut_hs**header_ptr, char* ten) { struct nut_hs * hs; hs = (struct nut_hs*)malloc(sizeof(struct nut_hs)); strcpy(hs->ten, ten); hs->diem = -1; /* ban đầu chưa có điểm thi, vào điểm sau */ /* nối vào đầu danh sách */ hs->tiep = *header_ptr; *header_ptr = hs; } int xoa_nuths(struct nut_hs**header_ptr, char *ten) { struct nut_hs * hs, *p; hs = *header_ptr; p = NULL; /* p trỏ tới nút trước nút cần xoá */ while(hs !=NULL&&strcmp(hs->ten, ten) !=0) { p = hs; hs = hs->tiep; } if (hs !=NULL) /* nếu tìm thấy nút cần xoá */ { /* kiểm tra có nút đứng trước nút cần xoá không */ if (p !=NULL) p->tiep = hs->tiep; else /* nút đầu danh sách sẽ là nút sau nút cần xoá */ *header_ptr = hs->tiep; free(hs); return 1; } return 0; } struct nut_hs* tim_nuths(struct nut_hs*header, char *ten) { struct nut_hs * hs; hs = header; while(hs !=NULL&&strcmp(hs->ten, ten) !=0) hs = hs->tiep; return hs; } void sapxep_nuths(struct nut_hs*header) { struct nut_hs *hs1, *hs2; char ten[25]; int diem; for (hs1=header; hs1!=NULL&&hs1->tiep!=NULL; hs1=hs1->tiep) for (hs2=hs1->tiep; hs2!=NULL; hs2=hs2->tiep) if(strcmp(hs1->ten, hs2->ten)>0) { /* doi cho thong tin ve hai nut trong danh sach */ strcpy(ten, hs1->ten); strcpy(hs1->ten, hs2->ten); strcpy(hs2->ten, ten); diem = hs1->diem; hs1->diem = hs2->diem; hs2->diem = diem; } } Tệp tiêu đề "func.h": #ifndef __FUNC_H #define __FUNC_H #include "list.h" /* các chức năng quản lí của chương trình bao gồm: */ /* thêm một học sinh vào danh sách */ void themhocsinh(struct nut_hs**header_ptr); /* xoá một học sinh khỏi danh sách */ void xoahocsinh(struct nut_hs**header_ptr); /* tra điểm cho môt học sinh trong danh sách */ void tradiem(struct nut_hs *header); /* in danh sách học sinh */ void indanhsach(struct nut_hs *header); /* nhập điểm thi cho học sinh lớp học */ void nhapdiem(struct nut_hs *header); #endif Tệp nguồn "func.c": /* thi hành của các hàm khai báo trong func.h */ #include "func.h" #include #include void themhocsinh(struct nut_hs**header_ptr) { char ten[25]; fflush(stdin); /* xoá vùng d?m stdin d? nh?p xâu tên */ printf("Ten: "); gets(ten); if (strcmp(ten, "")!=0) { them_nuths(header_ptr, ten); printf("Da them hoc sinh %s vao danh sach\n", ten); } } void xoahocsinh(struct nut_hs**header_ptr) { char ten[25]; fflush(stdin); /* xoá vùng d?m stdin d? nh?p xâu */ printf("Ten hoc sinh can xoa: "); gets(ten); if (xoa_nuths(header_ptr, ten)) printf("Da xoa hoc sinh %s khoi danh sach\n", ten); else printf("Khong co hoc sinh %s trong danh sach\n", ten); } void tradiem(struct nut_hs *header) { char ten[25]; struct nut_hs *tim; fflush(stdin); /* xoá vùng d?m stdin d? nh?p xâu */ printf("Ten hoc sinh can tra diem: "); gets(ten); tim = tim_nuths(header, ten); if (tim==NULL) printf("Khong co hoc sinh %s trong danh sach\n", ten); else if(tim->diem!=-1) printf("Co diem thi la %d\n", tim->diem); else printf("Chua co diem\n"); } void indanhsach(struct nut_hs *header) { struct nut_hs *p; int i=1; printf("%3s%25s%8s\n", "STT", "Ho va Ten", "Diem"); for(p=header; p!=NULL; p=p->tiep) { printf("%3d%-25s", i, p->ten); if(p->diem==-1) printf("%8s\n", "Chua co"); else printf("%8d\n", p->diem); i++; } } void nhapdiem(struct nut_hs *header) { struct nut_hs *p; int diem; for(p=header; p!=NULL; p=p->tiep) { printf("Diem cua %s:", p->ten); scanf("%d", &p->diem); } } Tệp nguồn "menu.c": /* chương trình chính là một menu lựa chọn công việc menu gọi các hàm chức năng có trong func.h và list.h */ #include "list.h" #include "func.h" #include void main() { int chon; struct nut_hs *header=NULL; /* con trỏ đầu danh sách học sinh */ /* tạo menu lựa chọn công việc */ do { printf("1. Them hoc sinh\n"); printf("2. Xoa hoc sinh\n"); printf("3. Nhap diem\n"); printf("4. Tra diem\n"); printf("5. In danh sach hoc sinh\n"); printf("0. Thoat\n"); printf("Lua chon: "); scanf("%d", &chon); switch(chon) { case 1: themhocsinh(&header); break; case 2: xoahocsinh(&header); break; case 3: nhapdiem(header); break; case 4: tradiem(header); break; case 5: sapxep_nuths(header); indanhsach(header); } }while(chon !=0); } BÀI TẬP TỔNG HỢP Câu 1: Hãy xây dựng một cuốn từ điển bằng cách dùng cây nhị phân. Hãy xây dựng các hàm xử lý các khâu dưới đây. - Khởi tạo từ điển - Chèn vào một từ mới - In một từ - Hiển thị từ điển Viết một chương trình hoàn chỉnh bằng cách tạo ra các bảng chọn lệnh với các chức năng sau: 1. Tạo từ điển mới 2. Đọc một từ điển từ đĩa vào 3. Thêm các từ vào 4. Xoá một từ đi 5. Hiển thị các từ theo thứ abc 6. Cất từ điển vào đĩa. 0. Thoát ra khỏi chương trình Câu 2: Hãy xây dựng sổ tay Telephone với các tính năng sau: 1. Đọc sổ tay từ đĩa vào 2. Tìm kiếm tên người trong danh mục phone 3. Thêm vào danh mục một người 4. Xoá một người ra khỏi danh mục 5. Cất vào đĩa. 0. Thoát ra khỏi chương trình Lưu ý là danh mục phone luôn luôn được giữ theo thứ tự bảng chữ cái.

Các file đính kèm theo tài liệu này:

  • pdfbg0000000008_3433.pdf