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.
106 trang |
Chia sẻ: huyhoang44 | Lượt xem: 679 | Lượt tải: 0
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:
- bg0000000008_3433.pdf