Lập trình C - Chương 9: Mảng

pdf 143 trang vanle 2780
Bạn đang xem 20 trang mẫu của tài liệu "Lập trình C - Chương 9: Mảng", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdflap_trinh_c_chuong_9_mang.pdf

Nội dung text: Lập trình C - Chương 9: Mảng

  1. Chương 9 MẢNG ì. GIỚI THIỆU CHUNG Trong hầu như mọi ngôn ngữ lập trình bậc cao đều có cấu trúc dữ liệu kiểu mảng. Từ các cấu trúc dữ liệu đơn giản như số nguyên, số thực, ký tự, việc hình thành kiểu dữ liệu mảng có thể nói là rất tự nhiên và là một yêu cầu thiết yếu khi cần thiết phải giải quyết các bài toán có thuật toán bắt đầu trở nên phức tạp mà việc sử dụng các dữ liệu đơn giản không còn hiệu quả nữa. Trong c, mặc dù việc sử dụng con trỏ tạo nên một công cụ rất mạnh, trong nhiều trường hợp có thể bỏ qua cấu trúc mảng, nhưng vẫn có mảng, và việc thể hiện nhiều thuật toán thông qua cấu trúc mảng đơn giản, dễ hiểu hơn nhiều so với việc sử dụng con trỏ. 1. Định nghĩa Mảng là một tổ chức dữ liệu liên tục, trong đó có một số hữu hạn các phần tử dữ liệu cùng kiểu với nhau, được sắp xếp có thứ tự. Thứ tự của các phần tử trong mảng gọi là chỉ số. Việc truy xuất vào mỗi phần tử được thực hiện thông qua một hoặc nhiều chỉ số. Nếu không quan tâm tới tổ chức bộ nhớ của mảng thì có thể hiểu một cách đơn giản là: Mảng là một dãy các biến cùng tên, cùng kiểu được phân biệt bằng một hoặc một số chỉ số. 2. Phân loại Mảng bao giờ cũng gắn liền với một kiểu dữ liệu đã được định nghĩa từ trước. Đó là kiểu của các phần tử. Kiểu của mỗi phần tử có thể là một kiểu chuẩn của c, cũng có thể là một kiểu do người dùng định nghĩa, và rất có thể đó lại là một kiểu mảng. Do đó có thể phân loại mảng theo kiểu dữ liệu. 151
  2. Tuy nhiên, người ta hay phân loại mảng theo số chi số cần thiết để truy xuất vào các phần tử, ta gọi là số chiều. Máng Ì chiều là máng trong đó có N phần tử được sắp xếp tuyến tính, có một phần tử đầu tiên được ghi chỉ số 0, một phần tử cuối cùng có chỉ số bằng N - Ì. Mỗi phần tử còn lại có chỉ số i và luôn đứng trước một phần từ có chỉ số i + Ì và đứng sau một phần tử có chỉ số i - Ì. Mảng 2 chiều là M mảng Ì chiều có N phần tử xếp kế tiếp nhau. Mỗi phần tử vì vậy được coi như đứng trong một bảng có M hàng và N cột. Cần phải có hai chỉ số để truy cập một phần tử, chỉ số hàng là i có giá trị từ 0 đến M - Ì, chi số thứ cột là j có giá trị từ 0 đến N - Ì. Chú ý đến đặc điểm mảng là tổ chức dữ liệu liên tục, la hoàn toàn có thể coi mảng hai chiều M hàng N cột như mảng một chiêu có MxN phần tử, trong đó mồi phần tử trên hàng i cột ị của mảng 2 chiều sẽ cố chỉ số k = i*N + ị. Tương tự như thế, mảng n chiều là mảng Ì chiều của các mảng (n - 1) chiều và cần có n chí số đế truy cập vào Ì phần tử. 3. Ưu nhược điểm của cấu trúc mảng 3.1. Ưu điểm Mảng là một cấu trúc dữ liệu có nhiều ứng dụng thực tế. Những bài toán trong đó có nhiều dữ liệu cùng kiểu và có vai trò giống nhau thường tổ chức dữ liệu dưới dạng mảng. Người lập trình tự do truy cập một phần tử bất kỳ trong mảng nhờ các chỉ số với mức độ thuận lợi như nhau bất kể phần tử đó nằm ở vị trí nào trong mảng. Điều này kết hợp với cấu trúc lặp làm cho bài toán trở nên đơn giản hơn nhiều. Trong số các cấu trúc danh sách thì mảng là loại danh sách dễ sử dụng nhất, thuận tiện nhất. Có thể duyệt danh sách theo chiều nào cũng được. 3.2. Nhược điểm Mảng thường kém mềm dẻo và nhiều khi lãng phí. Có chương trình làm việc tốt với các bài toán nhỏ, khi gặp các bài toán lớn thì số phần tử mảng không đủ đáp ứng. Khi lập trình khai báo mảng kích thước lớn để đáp ứng được nhiều cỡ bài toán thì phần lớn các bài toán chỉ cần sử dụng chưa đến một nửa số phần tử đã khai báo. 152
  3. Dưới hệ điều hành DOS, ở chế độ thực, kích thước mảng không thể vượt quá kích thước một segment (64 K). Vì vậy, một khai báo mảng đòi hỏi một vùng bộ nhớ lớn hơn 64 K sẽ bị báo lỗi. Ví dụ: Nếu khai báo một mảng có 100 X 100 phần tử là số thực kiểu float thì có thể không có vấn đề gì, vì dung lượng bộ nhớ do máng chiếm là 40000 byte. Nhưng nếu vì lý do nâng cao độ chính xác, phải thay kiểu f!oat bằng kiểu double, thì dung lượng bộ nhớ của mảng này tăng lên gấp đồi, vượt xa giới hạn 64 K và không thể đáp ứng được. Việc chèn thêm một số phần tử vào đáu đó giữa các phần tử đã có, hay xoa bớt một số phần tứ nằm giữa các phần tử khác có phần phức tạp hơn các kiểu danh sách khác. li. KHAI BÁO MẢNG 1. Khai báo trực tiếp Là cách khai báo biến mảng. 1.1. Cú pháp [ ] [ ]; Trong đó: : Một kiểu dữ liệu chuẩn hay kiểu dữ liệu người dùng đã khai báo từ trước. : Tên mảng cần khai báo. : Số phần tử tối đa của mảng. Đáy phải là một hằng nguyên hoàn toàn xác định. Mảng có bao nhiêu chiều sẽ có bấy nhiêu cặp ngoặc vuông chứa ký hiệu này. Chú ý: Cặp dấu ngoặc vuông không phải để biểu diễn tính chất tuy chọn, mà là một ký hiệu kết thúc trong thành phần cửa cú pháp. 1.2. Ví dụ li Tính n sô chính phương đầu tiên Chương trình VD59.C: #include int Mía 100] ĩ main () 153
  4. { int i, n = 5; for (i = 2; i #include double M2C[ 50] [ 2 00] ; main () { int i, j, n = 50; double da = M_PI/2/n; for (i = 0; i < n; i++) { M2C[ i] [ 0] = i* da; M2C[ i] [ 1] = sin (i* da) ; M2C[ i] [ 2] = cos (i* da) ỉ M2C[ i] [ 3] = tan (i* da) ; 154
  5. M2C[ i] [ 4] = 1/tan (i*da) ĩ } printf("\n%7s%7s%7s%7s%7s", "x", "sin(x)", "cos (X)" , " tg(x)" , " ctg(x)" ) ỉ for (i =0/ i int i, A[ 3] = { 3, 5, 11} ỉ main () { for (i = 0; i < 3; i++) printí ("\n%3d, %5d" , i, A[ Ì] ) ; return 0; } 155
  6. Như vậy, A[0] có giá trị là 3, A[l] có giá trị là 5 và A[2] có giá trị là li. Trong trường hợp này có thể khai báo đơn giản hơn như sau: int AI ] = { 3, 5, 11} ; Dù khai báo không tường minh số phần tử của mảng thì chương trình dịch cũng cứ chấp nhận mảng A có 3 phần tử. Như vậy, số giá trị trong cặp ngoặc { Ị dùng đế khới gán sẽ quyết định kích thước của mảng. Nếu khai báo: int A[3] = (3.5} thì chí có hai phần từ đầu được khởi gán giá trị. còn A[2] sẽ có giá trị là 0. Đối với máng hai chiều hoặc số chiều nhiều hơn 2, cũng có thể khởi gán giá trị cho từns phần tử. Chương trình VD62.C: #include double B[ 2] [ 3] = { { 2, 4} , {9, 10} } ; main () { int i, j; for (i =0; i < 2; i++) { printf ("\n" ) Ị for (j = 0; j < 3; j++) printf (" %5 . llf" , B[ ì] [ j] ) ; re tùm 0; } Ta sẽ có hình ảnh của mảng B như sau: Nếu khai báo: double B[ 2] [ 3] = { { 2} , { 9} } ; Thì hình ảnh của mảng B sẽ là: 156
  7. Tức là phần tử nào không được khởi gán sẽ có giá trị là 0. Cũng có thể khai báo theo kiểu để chương trình dịch c tự hiểu mảng hai chiêu có bao nhiêu hàng như sau: float X[ ] [ 4] = { { Ị, 2, 3} , {5, 6} } ; Lúc này, chương trình dịch c vẫn hiếu là máng có 2 hàng, mỗi hàng có 4 cột. Hàng thứ nhất chi có 3 phần tử đầu được khới gán. phần tử còn lại có giá trị 0.0. Hàng thứ hai chi có hai phần tử đáu được khởi gán. hai phần tử còn lại mang giá trị 0.0. Tuy nhiên, nếu viết: float XUM = { { Ị, 2, 3, 4} , {5, 6, 7, 8} } ; Thì c sẽ không chấp nhận và báo lỗi. Chương trình dịch chí chấp nhận suy đoán mảng X có 2 hàng, nhưng không chịu suy đoán rằng mỗi hàng có 4 cột. Nếu mảng có nhiều chiều hơn thì c cũng chí chịu trách nhiệm suy đoán số chiều thứ nhất, còn tất cả các chiều sau đó phải được khai báo tường minh. Ví dụ: float X[ ] [ 4] = { { Ì, 2, 3, 4} , {5, 6, 7, 8} } ; int Ạ ] [ 3] Ị 5] - {{{ Ị, 2} , {3, 4} , { 5}} , {{ 6} , {7, 8, 9} , { 10}} Ị ; Hình ảnh của máng X sẽ là: Còn mảng A sẽ là: .'í "7 10 ĩ' 157
  8. Nhiêu khi trong phát biểu các bài tập, các mảng được nói đến như các dãy, các véc tơ hay các ma trận và được biểu diễn bằng các khái niệm về ma trận trong đại số. Ví dụ: Cho một dãy N số nguyên, hay cho một véc tơ N chiều, hay cho một ma trận vuông kích thước NxN Cũng có khi người ta dùng chung và lẫn lộn các khái niệm mảng trong ngôn ngữ lập trình và các khái niệm dùng trong đại số, ví dụ: Cho một mảng kích thước MxN số thực, cho một véc tơ N phần tử số nguyên Điều đó nói lên rằng mảng là hình thức tổ chức các dữ liệu đặc biệt thích hợp để giải quyết các bài toán trong đại số tuyến tính. 3. Khai báo gián tiếp Là cách khai báo kiểu mảng. Từ kiểu mảng có thể khai báo các biến mảng. Cách làm này đem lại nhiều bổ ích hơn cách khai báo trực tiếp. 3.1. Cú pháp typedef [ ] [ ]; Cú pháp này chỉ khác với khai báo trực tiếp ở từ khoa typedef. Kết quả là khai báo một kiểu mảng chứ không phải là một mảng. 3.2. Ví dụ Ì const NMAX = 100; typedef float Vector[ NMax] ; Dòng này khai báo một kiểu mảng có tên là Vector, bao gồm tối đa NMax phần tử số thực. Sau dòng này, ta có thể khai báo: Ịvector Ạ, B, C; Thay vì phải khai báo float AI NMax] , B[ NMax] , C[ NMax] ; 3.3. Ví dụ 2 Tương tự như thế, ta có thể khai báo một kiểu mảng 2 chiều như sau: typedef double MaTran[ NMax] [ NMax] ; Và sau đó có thế khai báo các biến như sau: MaTran X, Y, Z; 158
  9. Cần lưu ý là NMAx trong các khai báo trên trước đó đã được khai báo là một hằng có giá trị hoàn toàn xác định. HI. TRUY CẬP CÁC PHẨN TỬ CỦA MẢNG Khi đã khai báo mang thì mỗi phần tử mảng có thế được thao tác như các biến đơn. Ta có thể dùng các lệnh scanf, printí để đọc, đê in giá trị của chúng cũng như dùng toán tử & để lấy địa chỉ của chúng. scanf (" %f" , &M[ i] [ j] ) ì Nếu là máng kiểu ký tự thì có thê dùng các hàm getch, gets, putch, puts để đọc và in giá trị cùa các phần tử. Ví dụ VD63.C: #include int A[ 4] ; ma in () { for (i = 0; i #include char c[ 5] ; main () 159
  10. { for (j = 0; j = 0; i —) XuLy (AI i] ) ; 160
  11. Hàm XuLy có thế chí làm một công việc đơn giản như nhập vào từ bàn phím hoặc in ra màn hình một phần tử mảng, nhưng cũng có thể làm một cônơ việc phức tạp hơn nhiều. Nếu quá trình xử lý không được viết thành một hàm thì chỗ này được thay bằng một khối lệnh có cùng chức năng. Ví dụ VD65.C: Nhập mảng một chiều gồm tối đa 100 phần tử số nguyên. Tính tống các phần tứ. [iinclude int A[ 10 0] ; void NhapSoNguyen(int *a) { printí("\nCho một số nguyên: "); scanf("%d", ã) ỉ } void InPhanTu(int i, int a) { prỉnt ("\n%3d\ t%6d" , i, a) ; } ma in 0 { int i, n, T = 0; NhapSoNguyen(&n); for (i = 0; i < n; i++) NhapSoNguyen (&A[ i] ) ; printf("\nM ANG DA N H A P" ) ; for (í = 0; i < n; i++) InPhanTu (i, A[ i] ) Ị for (i = 0; i < n; i++) T += A[ i] ; printf("\nTONG CÁC PHAN TU LA: %d", T)• getc(stdin); return 0; } 11-Giáo trinh NN lập trình c 161
  12. 2. Duyệt mảng hai chiểu Giả sử có mảng A gồm M hàng N cột. Có thể tiến hành duyệt mảng theo hàng như sau: for (i = 0; i typedef int Mang[ 20] [ 20] ; Mang M; void NhapMang(Mang A, int n) { int i, j; for (i = 0; i < n; i++) for (j = 0; j < n; j++) { printf ("\nCho A[ %d] [ %d] : ", i, j); scanf (" %d" , &A[ i] [ j] ) ; } ĩflush(stdin); } void InMang(Mang A, int n) { int i, j; for (i =0/ i < n; i++) { printf ("\n" ) ; for (j = 0; j < n; j++) 162
  13. printf (" %5d" , A[ i] [ j] ) / } printf ("\n" ) ; void ChuyenVi(Mang A, int n) { int i, j, x; for (i = 0; i < n - 1; i++) for (j = i + 1; j < n; { X = A[ i] [ j] ; A[ i] [ j] = Át j][ ì] ; A[ j] [ 1] = Kỉ } } main () { int n; printf(" \nCho bác cua ma tran: "); scanf (" %d" , &n) ; NhapMang(M, n); InMang(M, n); ChuyenVi(M, n); InMang(M, n); getc(stdin)ỉ re tùm 0; } Chú ý là việc dùng các chỉ số ị cho hàng, j cho cột ở đây chỉ là thói quen, không phải bắt buộc. Tuy nhiên những người lập trình thường tự tạo cho mình 'một số thói quen tốt để các chương trình của mình được thể hiện theo một phong cách nhất quán, sè dễ dọc, dễ hiểu và bớt nhầm lẫn. Một trong các thói \quen đó là sử dụng các biến có tên là ì, j ti làm chỉ số máng, hoặc dật tên ịcác biến có chữ cái đầu tiền giống chữ cái đầu tiên của kiểu dữ liệu (ì: int, c: \char, ả: double ). 163
  14. V. MẢNG CẤP PHÁT ĐỘNG Khi một con trỏ có kiểu nào đó trò vào một vùng dữ liệu có kích thước bằng bội sô của kích thước kiểu dữ liệu của con trỏ thì ý nghĩa cùa con trỏ không khác gì của mảng một chiều. Ngược lại, một biến máng một chiều M chính là một con trỏ, trong đó giá trị của M chính là địa chi của phần từ đầu tiên, *M là giá trị của phần tử đầu tiên. M[i] tương đương với *(M+i) tức là giá trị của phần tử thứ i của mảng chính là giá trị nằm ở vị trí từ M dịch đi i. Chú ý rằng mỗi bước dịch ở đây tương đương với số byte xác định bởi kiểu cùa dữ liệu mà con trỏ trỏ tới, ví dụ nếu p là con trỏ kiểu int thì P++ sẽ dịch p đi 2 byte, nếu f là con trò kiểu float thì f + 2 trỏ vào một ô nhớ cách f 8 byte. Tuy vậy, mảng khác con trỏ ở điểm cốt yếu là: Khi khai báo mảng, bắt buộc người lập trình phải cho biết số phần tử của mảng. Nếu con số này không biết trước thì chỉ có cách cho đại một giá trị nào đó đủ lớn để chương trình có thế chạy được trong đa số các trường hợp. Khi chạy chương trình, con số này mới được đưa vào, và dĩ nhiên chi có thể nhỏ hơn hoặc bằng con số đã cho khi lập trình. Ví dụ VD67.C sau đây minh hoa điều đó: #include #define NMax 100 int Mang[ NMax] ; int n, i, T; main 0 { printf("\nCho n: " ) ; scanf (" %d" , ỉn ỉ ; for (i = 0; i < n; i++) { printf ("\nMang[ %d] = ", ì); scanf (" %d" , &Mang[ i] ) ì for (i = 0; i < n; T += Mang[ i-t -+] ) printf ("\n%3d\ t%5d" , i, Mang[ i] ) í printf("\nTong các phan tu la: %d" , T) ; } 164
  15. Với con trỏ thì vấn đề này được giải quyết ổn thoa. Hơn thế nữa, ta còn có thể giải phóng bộ nhớ khi không cần đến mảng nữa. Trong ví dụ trên, biến Mang được khai báo như biến toàn cục. Khi chạy chương trình, nếu cho n = 5 thì thực tế ta chí dùng hết có 10 byte, vậy mà biến Mang chiếm giữ liên tục 200 byte trong suốt thời gian chương trình làm việc. Đó rõ ràng là một sự lãng phí. Còn với con trỏ, ta chí cấp phát bộ nhớ khi đã biết đích xác n là bao nhiêu, và khi cần có thế giải phóng trá lại đúng lượng bộ nhớ đã cấp phát. Khi cần thay đối kích thước máng, ta giải phóng vùng nhớ cũ và cấp phát lại. 1. Khai báo và cấp phát mảng động một chiều Khi biết số phần tử của mảng là n, ta có thể tiến hành cấp phát bộ nhớ cho máng bằng các hàm malloc hoặc calloc. Khi không dùng tới mảng hoặc khi muốn thay đổi kích thước mảng, ta giải phóng vùng nhớ của nó bằng hàm free hoặc toán tử delete. Ví dụ VD68.C sau đây minh hoa điều đó: #include #include int *Mang, i, n, TỊ main() { printf("\nCho số phần tử: "); scanf("%d", &n) ỉ Mang = (int *) malloc(n*sizeof(int) ) ; for (i = 0; i < n; i++) { printf ("\ nMang[ %d] = " , i); scanf (" %d" , &Mang[ ỉ] ) í } for (1 = 0; i < n; T += Mang[ ) printf ("\n%3d\ t%5d" , i, Mang[ i] ) ; printf(" \nTong các phan tu la: %d" , T); free(Mang); return 0; } 165
  16. Ngoài việc cấp phát và giải phóng bộ nhớ, các thao tác xuất. nhập ta có thể dùng lẫn lộn máng như con trỏ hay con trỏ như mảng được. Ví dụ scanf (" %d" , Mang+i) ỉ printf ("Mang[ %dj = %d" , ị, * (Mang+i) ) ; 2. Khai báo và cấp phát mảng động hai chiều Đối với mảng hai chiều trở lên, việc sử dụng con trỏ sẽ đem lại nhiều lợi ích hơn. Tuy nhiên, sẽ có nhiều cách để cài đặt một mảng hai chiều với các con trỏ. 2.1. Cách thứ nhất Coi mảng hai chiều là mảng một chiều, trong đó các hàng sẽ nối đuôi nhau. Ví dụ: Thay vì khai báo: #define NMax 5 #define MMax 5 float M2Dim[ Nmax] [ NMax] Ị int n_hang, n_cot; Ta sẽ khai báo: float *M2Dim; int n_hang, n__cot; Và sau khi nhập n_hang, n_cot, ta cấp phát bộ nhớ như sau: M2Dim = (int *)malloc(n_hang*n_cot); Như vậy, một phần tử của mảng: M2Dim[ ị] [ jj Sẽ tương ứng với: * (M2Dim + i*n_cot + j) Ví dụ, nếu theo cách khai báo mảng thông thường ở trên, với n_hang = 3, ncot = 3 ta có M2Dim là: 1 2 3 4 5 6 7 8 9 166
  17. Dùng con trỏ theo cách thứ nhất, ta có M2Dim dưới dạng sau: 1 2 3 4 5 6 7 8 9 Phần tử M2Dim[2][l] trên mảng ở trên sẽ tương đương với phần tử M2Dim[8] ở dưới. 2.2. Cách thứ hai Theo cách khai báo mảng thông thường thì ta cũng thấy thực chất mảng hai chiều được khai báo như mảng một chiều của các mảng một chiều. Thực vậy, khi khai báo: int M [ 100] [ 50] ; Ta thấy rõ ràng đây là khai báo mảng 100 phần tử, trong đó mỗi phần tử có 50 số nguyên. Từ bản chất đó của mảng hai chiều và suy rộng ra cho mảng nhiều chiều, có thể nói mảng nhiều chiều là mảng của các mảng. Khi dùng con trỏ để khai báo mảng, ta cũng có thể suy diễn ra từ đó để thấy rằng mảng hai hay nhiều chiều là con trỏ tới các con trỏ. Hiện tại, ta chỉ nói về mảng hai chiều. * Khai báo ; Ví dụ: int M; * Cấp phát và giải phóng bộ nhớ M ở đây là một con trỏ tới một con trỏ kiểu int. Nếu số hàng của mảng 2 chiều là h, số cột là c, ta sẽ cấp phát bộ nhớ như sau: Trước hết cấp phát bộ nhớ cho h con trỏ: M = (int )malloc(h*sizeof(int *)) ; Sau lệnh này, ta được M như đầu mối tìm tới h con trỏ. Biểu thức sizeof(int *) thực ra không có gì quan trọng, vì đã là con trỏ gần thì độ lớn là 2 byte, bất kể là kiểu dữ liệu nào. Tuy nhiên, các con trỏ này vẫn chưa sử dụng được vì 167
  18. chưa được cấp phát bộ nhớ cần thiết. Biết rằng mỗi con trỏ ứng với một hàng gồm c phần tử số nguyên, ta tiến hành cấp phát bộ nhớ như sau: for (i = 0; i #include int M; void NhapMang(int A, int n) { int i, j; for (i = 0; i < n; i++) for (j = 0; j < n; j++) { printf ("\nCho A[ %d] [ %d] : ", i, j); scanf (" %d" , &A[ í] [ j] ) ; } fflush(stdin)ỉ } 168
  19. void InMang(int A, int n) { i n t i, j ; for (i = 0; i < n; i++) { printí ("\n" ) ỉ for (j = 0; í < n; j++) printf (" %5d" , A[ ỉ] ĩ j] ) ; } printf ("\n" ) ỉ } void ChuyenVi(mt A, int n) { int i, Ì, X; for (i = 0; i < n - 1; i++) for (j = i + 1; j < n; j++) { X = A[ 1] [ j] ; A[ i] [ j] = A[ j] [ i] ; A[ j] [ i] = X; } } main () { i n t n, ì ỉ printĩ("\nCho bác cua ma tran: "); scanf (" %d" , &n) ; M = (int * *)malloc(n*sizeof(int *)); for (i =0; i < n; i++) M[ i] = (int * ) malloc (n* sizeof (int) ) ; NhapMang(M, n); InMang(M, n); ChuyenVi(M, n); InMang(M, n) ; gete(stdin); 169
  20. for (ì = 0; i < n; i++) free (M[ i] ) ; free (M) ỉ return 0; } VI. TRUYỀN MẢNG QUA DANH SÁCH THAM số CỦA HÀM 1. Trường hợp mảng một chiều Vấn đề liên quan đến cá con trỏ và hàm. Truvền tham số mảng từ hàm gọi vào hàm bị gọi như thế nào? Ta xét ví dụ sau: typedef float Mang[ 5] ; void NhapMang (Manơ *M) { for (i = 0; i < 5; i++) *M[ i] = Ì; Ị Với lời gọi hàm: Mang M; NhapMang(&M); Trong ví dụ này, ta có ý định đưa địa chí của mảng vào hàm. nhập các phần tử cho nó và đinh ninh là kết quả sẽ đúng, vì khi học về hàm, ta đã được học cách làm như vậy. Song sau khi gọi hàm và in kết quả thì thật là đáng thất vọng. Hình như duy nhất chỉ có phần tử đầu tiên là có giá trị mong muốn, còn các phần tử sau đó có các giá trị không thể nào hiểu được. Thực ra, khi viết Mang M ta đã khai báo một con trỏ tới một cấu trúc dữ liệu có kích thước 5*4 = 20 byte. M+l hay *(M+1) hay M[l] ở trong hàm đều cho cùng một giá trị địa chỉ, nhưng không phải đó là giá trị phần tử thứ 2 của mảng, mà đó là địa chí của cấu trúc kế tiếp dài 40 byte sau cấu trúc mảng mà ta đã đưa vào. Đây quá là một sai sót nghiêm trọng. 170
  21. Trong khi đó, ví dụ có vẻ sai sau đây lại cho kết quả tốt đẹp đúng như mong đợi của chúng ta: typedef float Mang[ 5] ; void NhapMang(Mang M) { for (i = 0; i < 5; i++) Hi} = i; Ị Với lời gọi hàm: Mang M; NhapMang(M)ỉ Lý do là khi viết: void NhapMang(Mang M) Không có gì khác so với việc ta viết: void NhapMang(floát *M) Còn lời gọi: NhapMang(M); Hoàn toàn đồng nghĩa với lời gọi: NhapMang (&M[ 0] ) ĩ Tức là ta đưa vào hàm địa chỉ của phần tử đầu tiên của mảng, còn ở trong hàm, cách viết: Hi} = ị; Đồng nghĩa với cách viết: * (M+i) = ị; Vì vậy, kết quả hoàn toàn mỹ mãn. Từ đó, ta thấy để truyền địa chỉ mảng một chiều vào hàm, chí cần truyền địa chí của phần tử đầu tiên. Và dù không muốn, cách viết truyền trị thông 171
  22. thường của khai báo hàm và lời gọi hàm với dữ liệu là mảng một chiều vẫn là truyền địa chi. Ví dụ: typedef float Mang[ 5] ; void InMang(Mang M) { for (i = 0; i ; //vi dụ Mang M; Hoặc: * ; //vỉ dụ float *M; Hoặc: & ; //ví du Mang &M; Nên dùng cách thứ 2 và thứ 3. tức là dùng con trỏ và dùng tham chiếu hơn, và nhớ là các cách viết đó đều là truyền tham số bằng địa chì hoặc tương đương, do đó phái cấn thận đê không làm thay đổi dữ liệu ngoài ý muốn. 172
  23. 2. Trường hợp mảng hai chiểu Tương tự như trường hợp máng hai chiều, một khai báo hàm như sau: #define h 5 #define c 5 typedef float Mang2D[ h] [ c] ; void NhapMang (Mang2D *M){ } sẽ dẫn đến kết quá sai. Hơn nữa, phép lấy địa chì của phần tử máng &M[i]Ịj] là sai như đã nói trong bài về mảng, nên tốt hơn cà là ta nên áp dụng cách truyền tham sỏ mảng bằng con trỏ tới phần tử đầu tiên của dòng đầu tiên, hoặc truyền qua tham chiếu. Xét các ví dụ sau: Với khai báo mảng sau đây: #defme h 5 #define c 5 typedef int Matrice[ h] [ c] ; Thì các cách viết sau là đúng: void NhapMatl(Matrice V, int H, int C) { int i, j/ for (Ì = 0; i < H; i++) for (j = 0; í < C; j++) V[ í] [ j] = i*c + j; } //OK void NhapMat2(Matrice &v, int H, int C) { int i, j; for (i =0; i < H; i++) for (j = 0; j < C; j++) Vĩ i] [ j] = i*c + j ; 173
  24. } //OK void NhapMat3(int V[ ] [ c] , int H, int C) { int i, j/ for (i = 0; i < H; i++) for (j = 0; j < C; j++) V[ i] [ j] = 1*C + j; } //OK void inMatl(Matrice V, int H, int C) { int i, j; for (i = 0; i < H; i++) { printf ("\n" ) ; for (j = 0; j < C; j++) printf (" %5d" , V[ i] [ j] ) ; } } Và cách viết sau đây là sai: void NhapMat4(Matrice *v, int H, int C) { for (i = 0; i < H; i++) for (j = 0; j < C; j++) *V[ i] [ j] = i*c + j; } //Bad Còn với cách khai báo mảng sau đây: int v = (int )malloc(h*sizeof(int *)); for (i = 0; i < c; i++) * (V+i) = (int *)malloc(c*sizeof(int) ) ; Thì các cách viết sau đây là đúng: void NhapMat5(int v, int H, int C) { int i, j; 174
  25. for (i = 0; i < H; i++) for (j = 0; í < C; j++) Vĩ i] [ j] = i*c + j; } //OK void InMat2 (int v, int H, int C) { int i, j; for (i = 0; i < H; i++) { printf ("\n" ) ; for (j = 0; j < C; j++) printf (" %5d" , V[ i] [ j] ) ; } } Qua các ví dụ trên đây, ta lại càng thấy cách biểu diễn máng bằng con trỏ có nhiều sức mạnh, rất đơn giản, mềm dẻo và hiệu quả. Đỏi với các mảng nhiều chiều, ta cũng nên sử dụng cách khai báo mảng động vù truyền tham số bằng con trỏ của các con trỏ hoặc khai báo mảng tự động và truyền dữ liệu mảng bằng tham chiếu. VU. XÂU KÝ Tự Xâu ký tự thực chất là mảng một chiều của các ký tự, và do đó, về bản chất chính là một con trỏ kiểu ký tự. Điểm khác biệt duy nhất giữa hai khái niệm này là xâu ký tự luôn dành cho ký tự cuối cùng của nó giá trị \0', còn ở mảng các ký tự thi điều đó không bắt buộc. Do đó, khi áp dụng các phép toán và hàm của xâu ký tự trên mảng các ký tự thì phải chú ý đặt giá trị \0' vào phần tử thích hợp của mảng. Điều này khác biệt so với xâu ký tự của Pascal. Pascal luôn lấy phần tử đầu tiên (byte số 0) làm nơi lun trữ chiều dài của xâu, vì thế chiều dài xâu không thể vượt quá 255 ký tự. Xâu ký tự của c không lệ thuộc vào điều đó. Vì vậy chiều dài xâu có thể dài tối đa 65535 ký tự. Từ nhận xét là một mảng hai chiều là mảng một chiều của các mảng một chiều, hay là mảng cùa các con trỏ, còn có thế rút ra là: Mảng của các xâu ký 175
  26. tự có thế được thay the bằng máng hai chiều cùa các ký tự. và như vậy, bản chát cua máng các vầu ký tự là mảng của các con trò kiểu ký tự. 1. Sao chép xâu ký tự Sao chép xâu ký tư khống có nghĩa là gán xâu này vào xâu kia. Nếu làm như vậy một cách vô Ý thức thi sẽ gây hậu quả khó lường trước cho chương trình. Cần thực hiện nguyên tắc là khai báo xâu ký tự, cấp phát bộ nhớ vừa đù cho xâu, khi sao chép thì chép từng ký tự từ xâu này sang xâu kia hoặc dùng các hàm sao chép có sẵn. Khi sử dụng xong xâu ký tự thì giải phóng bộ nhớ. Xét ví dụ VD70.C sau đây: sa - • X Hình 47. Gán xâu ký tự thực chất là gán địa chỉ Trong ví dụ này. s được khai báo như một con trỏ kiểu ký tự. Dấu gán sau đó không có nghĩa là gán xâu ký tự "ESTIH" cho xâu s. mà là gán địa chỉ cùa một xấu hằng cho con trỏ s. Thật vậy. trên cửa sổ \vatch ta thấy giá trị của s là một đìa chì của dữ liệu tĩnh. chứ không phái dữ liệu trong vùng heap. vì "ESTIH " là một xâu hằr)2 tổn tại ngay từ khi biên dịch chương trình, nên nó được đặt vào đầu vùng dữ liệu. Và cũng vì vậy mà cả hai lần in giá trị cùa bộ 176
  27. nhớ heap còn lại đêu cho kết quá như nhau. trước và sau khi khai báo s. Nếu dùng lệnh nào đó đế giải phóng con trỏ s đều dẫn đến lỗi "Niill pointer assignment". #ỉnẽlude tiitclude i lít,, nainO { char *s; '•' .'* • ỉ. void *p; printf ( \nBo nhò truoc dung Si Hốc", coreĩeftO)ĩ p - s « (char *)raalloc(60); s = "Ẹ5TÍH"; prinưCVKs", s); free(s); printf( \nBo nho sau dung • 5' : %x", coreleftO}ĩ printf("\ns » %s", s); 1:1. Bo nho truoc dung 5: f8ĨO ESTIH Bo nho sau dung s : f7d0 5 -1 ra Hếlp F7 Trace F8 5tep F9 Make F10 Menu Hình 48. Gán xâu ký tự có thẻ sẽ làm thất thoát bộ nhớ Nếu ta viết lại chương trình như trong VD71.C, sẽ thấy: Địa chỉ của s cấp phát bằng hàm malloc là DS:0622, sau khi gán xâu "ESTIH" biên thành DS:00C3. Chương trình vẫn chạy bình thường, nhưng có một vùng nhớ 64 byte bị lãng quên. Xâu ký tự s được cấp phát một địa chi trong vùng heap đế nó sở hữu 64 byte. Lệnh gán sau đó đã lôi kéo s sang chỗ ở mới mà không hề bàn giao lại căn nhà cũ cho chương trình. Lệnh delete []s đã giải phóng vùng dữ liệu của xâu "ESTIH" ở đầu vùng dữ liệu. Vì vậy. hai lần in xâu s trước và sau lệnh free(s) đã cho kết quả chênh nhau 64 byte (OxF7AO - OxF760 = 0x40 = 641()). Muốn xâu s vẫn giữ 64 byte trong vùng heap, mà lại nhận được cả 5 ký tự mà nó cần thì phái làm như trong VD72.C. 12-Giáo trinh NN láp trinh c 177
  28. s X ị : : —— —— VD72.C 1 #include rinclude * *include > •V - • - . int mainQ ; ' - • { diar *s; •• void *p; prii*tf( \«Bo nho truoc dung s: %x", c»re1eftO)í p - s = Cchar *)i»anoc(60>ĩ strcpy(s,"E5TIH"); ' ! printfr\n3fe", s); • free(s); • printf C"\nBo nho sau dung s : Súc , fx>re1eftO); Ị ' return 0; 1 _ 3.3:1 . ị : • • :— Fl Help F? Trace Ffi step *J Eđit Ins Add De! DẽTeĩe "Flỡ Menu Hình 49. Cần cấp phát bộ nhớ trước, sau đó sao chép từng ký tự từ xâu này sang xâu kia Hàm strcpy có nguyên mẫu trong tệp tiêu đề string.h làm nhiệm vụ sao chép một xâu ký tự vào một xâu khác trong khi địa chỉ điểm đầu mỗi xâu, hay giá trị của con trỏ không bị thay đổi. Cú pháp như sau: strcpy( , ); Từng ký tự của xâu nguồn sẽ được chép sang các vị trí tương ứng của xâu đích cho đến khi chép xong ký tự \0' thì kết thúc. Điều quan trọng cần phải lưu ý là xâu đích phải được cấp phát đủ chiều dài để đón nhận các ký tự của xâu nguồn. Tuy nhiên, ngay cả khi xâu đích có 6 ký tự và xâu nguồn có 20 ký tự thì chương trình có thể vẫn cứ chạy như không có sai sót. Còn hậu quả của nó ra sao, người lập trình phải chịu trách nhiệm. 2. Một số ví dụ 2.1. Đảo ngược xâu ký tự Coi xâu ký tự như một mảng, ta có thể viết một chương trình VD73.C đảo ngược xâu ký tự một cách đơn giản sáng sủa hơn. Bạn hãy so sánh hai hàm Invl và Inv2 sau đây: 178
  29. tinclude #include #include void Invl(char *s) { char *p = s + strlen(s) - 1; while (s < p) { char c = * s; * s + + = * p; *p— = c; } } void Inv2(char *s) { int i = 0, n = strlen(s) - li while (i < n) { char c = s[ i] ; s[ i] = s[ n] s[ n] = c; } } int main(void) { char *s = "Mua dam ngâm dát"; printf ("\n%s" , s) ; /* Invl (s) ; * / Inv2 (s); printf ("\n%s" , s) ; getch (); re tùm 0; } Kết quả bài này sẽ là: Mua dam ngâm dát tad magn mad auM
  30. 2.2. Chia mảng thành các xâu Xét chương trình VD74.C sau: #include #include #include main () { char s[ ] = "Co cong mai sát, co ngay nen kim"; char * * p; int i; p[ 0] = s; p[ 0] [ 16] = 'XO'; p[ li = s + 17; for (i = 0; i < 2; i + +) printí ("\n%s" , p[ i] ) ; getch () ; re tùm 0 ; } Ta có mảng s được khởi tạo bằng một xâu. Xâu này dài 32 ký tự, tức là ký tự thứ 32, hay s[32] là ký tự kết thúc xâu (\0'). Tạo con trỏ p là con trỏ của các con trò kiểu ký tự. Con trỏ này tương đương như một mảng của các xâu ký tự. Đặt phần tử thứ 0 của p trỏ vào đầu xâu s. Đặt phần tứ thứ Ì cùa p trỏ vào phần tử thứ 17 của xâu s. Đặt vào phần tử thứ 16 của p[0] ký tự 0 để ngắt xâu. Kết quả ta có máng hai xâu ký tự p[ơ] = "Co cong mai sát,", p[l] = "co ngay nen kim". Kết quả lệnh printf cho thấy rõ điều đó: c c c 0 n ĩ r.ì • - í t 0 c 0 n c V ũ e n k - tri 0 p[0] t pin Co cong mai sát, Co ngay nen kim 180
  31. Chương 10 CẤU TRÚC ì. ĐỊNH NGHĨA CÂU TRÚC Cấu trúc là một tổ chức dữ liệu gồm nhiều thành phần, trong đó mỗi thành phần được đặc trưng bằng một tên và một kiểu. Kiểu của các thành phần khác nhau thường khác nhau nhưng cũng có thê giống nhau. Đê truy cập một thành phần của cấu trúc phải truy cập qua tên cấu trúc và tên thành phần. Ta thấy điếm khác nhau cơ bán giữa cấu trúc và mảng là ở chỗ: Trong cấu trúc, các thành phần dữ liệu có kiểu khác nhau. và truy cập tới các thành phần đó qua tên của chúng; còn trong mảng, kiểu của các thành phần bắt buộc phải giống nhau. mỗi thành phần được đặc trưng bằng số thứ tự của nó trong mảng. Mặc dù cấu trúc là một tổ chức dữ liệu mạnh, thể hiện tính đa dạng, phức tạp của dữ liệu, nhưng cấu trúc không thể thay thế được mảng. cho nên, trong các bài toán lớn và phức tạp, kiểu mảng và kiểu cấu trúc luôn được kết hợp với nhau. li. KHAI BÁO CÂU TRÚC 1. Khai báo biên cấu trúc struct { [ [, ]*;]+ } [, ]*; Khai báo như trên có nghĩa là: Đầu tiên phải có từ khoa struct và sau đó là cặp dấu ngoặc { Ị. Trong cặp dấu ngoặc đó có ít nhất một dòng khai báo giống như khai báo biến, tức là sau đó là của một hoặc nhiều thành 181
  32. phần cùng kiêu, viết cách nhau bởi dấu phẩy. Sau cặp dấu ngoặc là tên của một hoặc nhiều biến có cấu trúc như đã khai báo. Ví dụ: struct { char HoTen[ 15] ; int SoThe; int NgSinh, ThSinh, NSinh; float Toan, Ly, Hoa; } HI, H2, H3; Khai báo trên khai báo 3 biến HI, H2, H3 có cấu trúc giống nhau, trong đó có 8 trường. Đó là trường HoTen là một mảng 15 ký tự, kế sau đó là trường SoThe, trường NgSinh, ThSinh, NSinh đều thuộc int. Ba trường cuối cùng là Toan, Ly, Hoa thuộc kiểu float. Trong bộ nhớ, mỗi biến này chiếm 35 byte trong đó chứa các thành phần đúng theo thứ tự như đã khai báo: Đầu tiên là 15 byte ứng với trường HoTen, sau đó là 4 vùng 2 byte ứng với các trường SoThe, NgSinh, ThSinh, NSinh; cuối cùng là 3 vùng 4 byte ứng với 3 trường Toan, Ly, Hoa. 2. Khai báo kiểu cấu trúc struct { [ [, ]*;]+ } [ [, ]*]; Khai báo trên rất giống với cú pháp khai báo biến cấu trúc, chỉ thêm và phần tên các biến để trong dấu ngoặc vuông có nghĩa là tuy chọn, có hoặc không có cũng được. Như thế không có nghĩa là không phải khai báo biến nữa, mà là khi đã có tên kiểu rồi thì sau này ta có thể khai báo biến theo cú pháp khai báo biến thông thường: [, ]*; Theo cú pháp của c thì không hoàn toàn đơn giản như thế. Khi khai báo kiêu cấu trúc phải có từ khoa typedeí, còn khi khai báo biến phải thêm vào trước tên kiểu cấu trúc khi khai báo biến từ khoa struct. Tức là: struct [, ]*; 182
  33. Nhưng theo cú pháp của C++ thì có thế bỏ từ khoa này đi. Ví dụ: struct HocSinh{ char HoTen[ 15] ; int SoThe/ int NgSinh, ThSinh, NămSinh; float Toan, Ly, Hoa; } ; HocSinh HI, H2, H3, H[ 10] ; Ví dụ này khai báo một kiểu có tên là HocSinh có cấu trúc hoàn toàn giống ví dụ trước. Sau đó khai báo các biến HI, H2, H3 và một mảng H gồm 10 phần tử kiểu HocSinh. Đây cũng đồng thời là một ví dụ về sự kết hợp giữa mảng và cấu trúc: Trong cấu trúc có mảng và mảng với các phần tử là cấu trúc 3. Con trỏ kiểu cấu trúc Giống như mọi kiểu dữ liệu khác, khi đã có tên kiểu cấu trúc, ta có thể khai báo con trỏ tới cấu trúc: struct HocSinh *T; Và cấp phát bộ nhớ cho con trỏ: T = (struct HocSinh *)malloc(20*sizeof(HocSinh)) ; Đặc biệt, ta hoàn toàn có thể khai báo con trỏ tới cấu trúc đang được khai báo như một thành phần dữ liệu của cấu trúc đó. Ví dụ: struct HocSinh{ char HoTen[ 15] ; int SoThe; int NgSinh, ThSinh, NSinh; float Toan, Ly, Hoa; struct HocSinh *Next, *Last; } ; 183
  34. Điều này rất cần thiết khi ta cài đặt các cấu trúc dữ liệu phức tạp như các loại danh sách. cây. HI. TRUY CẬP CÁC THÀNH PHẨN CỦA CÂU TRÚC Mồi thành phần của một biến kiêu cấu trúc được truy cập tới bằng toán tử chàm (.) đặt sau tên biến, trước tên của thành phần. Ví dụ VD75.C: #include #include #include struct HocSinh { char * HoTen; int SoThe; int NgSinh, ThSinh, NSinh; float Toan, Ly, Hoa; int main() { struct HocSinh H; char *s; printf("\nHo va ten: "); gets (s) ; H.HoTen = (char * )malloc(strlen(s) + í); strcpy (H.HoTen, s); printf ("\nCho ngay thang nam sinh: (dd/mm/yV)"); scanf("%d/%d/%d",SH.NgSinh,SH.ThSinh, &H.NSinh) ; printf("\nSo the sinh viên: "); scanf (" %d" , &H . SoThe) ; printf ("\nDiem toan, Ly, Hoa: "); float t, Ì, h; 184
  35. scanf (" %f%f%f" , ít, Si, &h) ; H.Toan = t; H . Ly = Ìĩ tì.Hoa = h; printf("\nHo va ten : %s" , H.HoTen); printf CXnNgay sinh : %d-%d-%đ', H.NgSinh, H.ThSinh, H.NSinh) ; printf("\nSo the : %d" , H.SoThe); printí("\nDiem Toan : %4.2f", H.Toan); printf ("\nDiem Lỵ : %4.2f", H . Ly) ; printf ("\nDiem Hoa : %4.2f" , H . Hoa) ; re tùm 0 ỉ Ị Qua ví dụ trên ta thấy có thể thao tác trên các thành phần dữ liệu của cấu trúc giống như các biến bình thường. Có thể dùng toán tử & để lấy địa chỉ của thành phần cấu trúc. Nếu có con trỏ kiểu cấu trúc thì có thể truy cập tới các thành phần dữ liệu bằng một trong hai cách sau: Dùng toán tử sao (*) đặt trước biến con trỏ để lấy giá trị của nó, và dùng toán tử chấm (.) để truy cập tới các thành phần dữ liệu giống như đối với một biến thông thường. Ví dụ: struct HocSinh *H = (struct HocSinh *)malloc(sizeof(HocSinh)); strcpỵ ( (* H) . HoTen, "Tran Van Xuân"); (* H) . Toan = 6.5; printf("Ho va ten: %s" , (* H) . HoTen) ; Hoặc dùng toán tử ngay sau biến con trỏ -> thay cho toán tử chấm (.)• Ví dụ: struct HocSinh *H = (struct HocSinh *)malloc(sizeof(HocSinh)); strcpỵ(H->HoTen, "Tran Van Xuân"); H->Toan = 6.5; printf("Ho va ten: %s" , H->HoTen) ; 185
  36. IV. KHỞI GÁN CÁC GIÁ TRỊ CỦA CÂU TRÚC Giống như các kiêu dữ liệu khác, có thê khởi gán cho cấu trúc ngay lúc khai báo. Ví dụ: struct HocSinh{ char HoTen[ 15] ; int SoThe; int NgSinh, ThSinh, NSinh; float Toan, Ly, Hoa; }H = {"Tran Huy Thang" , 12345, 4, 3, 76, 7.5, 8.0, 9 .5} í struct HocSinh T = {"Le Đúc Viiih", 12346, li, 5, 75, 3, 9.5, 6.0} ; V. ví DỤ VẾ CÂU TRÚC Ta viết một chương trình quản lý một danh sách học sinh. Ó mức độ đơn giản, chương trình chí nhập danh sách, in danh sách, thực hiện tính điểm trung bình và phân loại học sinh, in danh sách, tìm kiếm học sinh theo tên, lọc danh sách học sinh giỏi. Dưới đây là từng phần của chương trình, được cắt ra để giải thích. Chí cần ghép liền lại là chương trình hoàn chỉnh và chạy được. Trong chương trình này, vì có sử dụng một số các hàm mở rộng của Turbo c nên bạn phải chọn bộ biên dịch Turbo c. Ta gọi đây là ví dụ VD76.C. 1. Phần khai báo toàn cục #include #include #include #include typedef struct HocSinh { unsigned int No; char *HoTen; unsigned short Ngay, Thang, Nam; 186
  37. float Toan, Ly, Hoa; int PhanLoai; HocSinh *Next, *Prev; } ; struct HocSinh *Ds = NULL; struct HocSinh *First = NULL; unsigned int Nb = 0; char *m[ 25] = {"Nháp danh sách", "In danh sách", "Tim kiêm", "Hoe sinh gioi", "Xoa danh sách", "Két thúc" } ; void NhapBG(struct HocSinh *); void XuatBG(struct HocSinh *, int) ; void InDs(struct HocSinh *, int) ; void Add () ỉ void NhapDs () ; struct HocSinh *Copỵ(struct HocSinh *); int TimTen(struct HocSinh *); void XoaDs(struct HocSinh *, int &); Phần này khai báo kiêu cấu trúc HocSinh với các thành phần Ngày, Tháng, Năm sinh, điếm 3 môn học Toán, Lý, Hoa. Trường HoTen là một xâu ký tự chưa được cấp phát. Trường PhanLoai được mã hoa bằng số nguyên: Ì là giỏi, 2 là khá, 3 là trung bình, 4 là kém. Ba biến toàn cục là: • Ds và First: Con trỏ kiểu HocSinh. Khi có nhiều bản ghi móc nối với nhau bằng các con trỏ Next và Prev thì ta được một danh sách quay vòng mà Ds là con trỏ hiện tại và First đánh dấu con trỏ đầu tiên trong danh sách. Lúc đầu cả hai con trỏ đều là NULL. 187
  38. • Nb: Số học sinh có trong danh sách. Số học sinh ban đầu bằng 0. nghĩa là chưa có gì. • Ngoài ra ta còn có một mảng các xâu ký tự m. Đây là danh sách các mục chọn trong một menu đơn giản mà ta sẽ dùng để điều khiển chương trình. Sau đó là các nguyên mẫu hàm. 2. Cấu trúc dữ liệu Trong chương trình có dùng một kiểu cáu trúc danh sách quay vòng hai chiều. Mỗi bản ghi kiểu HocSinh được trỏ tới bởi một con trỏ, sẽ có một con trỏ móc với bản ghi trước nó gọi là Prev, một với bản ghi sau nó gọi là Next. Bản ghi cuối cùng sẽ liên hệ trở lại với bản ghi đầu tiên của danh sách để tạo thành một vòng liên kết khép kín theo sơ đồ dưới đây: Khi danh sách chỉ có Ì con trỏ bản ghi ì ?wv FIR5T ỉ Hình 50. Phần tử đầu tiên khởi tạo danh sách Con trỏ đó chính là First. Hai mối dây liên hệ của First là Prev và Next đều quay lại trỏ vào chính nó. Khi danh sách có nhiều hơn Ì con trỏ bản ghi Hình 51. Sơ đồ danh sách liên kết móc vòng 2 chiều 188 •
  39. Trong hình là sự liên kết của 17 con trỏ bản ghi, được đánh số từ 0 đến 16. Con trỏ số 0 là con trỏ First. Con trỏ cuối cùng là con trỏ Ds. Việc chèn thêm một con trỏ H vào sau vị trí của Ds sẽ diễn ra theo các bước sau: • Lấy Prev của H trò vào Ds. • Lấy Next cùa H trỏ vào Next của Ds. • Lấy Ne xí của Ds trỏ vào H. • Lấy Prev của Next của H trỏ vào H. • Trỏ Ds vào H. 3. Hàm nhập bản ghi Hàm NhapBG nhận một con trỏ H như tham số. Con trỏ này phái được cấp phát từ trước, trừ trường HoTen. Việc nạp giá trị cho trường này tiến hành như sau: • Khai báo một xâu ký tự s có chiều dài xác định. Biến này là biến tự động, sẽ tự huy khi kết thúc hàm. • Nhập họ tên vào s. • Lấy chiều dài xâu s cộng thêm Ì để xác định chiều dài cần cấp phát cho trường HoTen. • Cấp phát bộ nhớ cho trường HoTen. • Sao chép giá trị xâu s vào trường HoTen. • Các trường Ngav, Thang, Nam được nhập từ bàn phím, có kiếm trá tính hợp lý của các giá trị nhập vào. Nếu không hợp lệ phải nhập lại. Riêng giá trị năm có thế nhập 2 chữ số hay 4 chữ số đều được. Hàm sẽ tự chuyến giá trị năm thành 4 chữ sô. • Các trường Toan, Ly, Hoa được nhập thông qua các biến trung gian. • Trường PhanLoaí được xác định trên cơ sở giá trị trung bình của điểm 3 môn. Trong hàm có sử dụng các lệnh fflush(stdin) để dọn dẹp vùng đệm. void NhapBG(struct HocSinh *H) I char s[ 30] ; 189
  40. float t, Ì, h, tb; fflush(stdin); window(l, 13, 80, 25); textattr(GREEN + (MAGENTA « 4)); c Ì r s c r 0 ; printf("\nHo va ten : "); gets (s); H->HoTen = (char *)malloc(strlen (s)+1) ; strcpy(H->HoTen, s) ; do { fflush(stdin); printf("\nNgay, thang, nam sinh: "); scanfr%d/%d/%d", &H->Ngay, &H->Thang, &H->Nam); } while (H->Ngay > 31 II H->Thang > 12 II H->Nam > 2003) ; H->Nam = H->Nam%100 + 1900; printf("\nDiem Toan : " ) ; scanf("%f", &t) ; printf("\nDiem Ly : * ) ; scanf("%f", &1); printf("\nDiem Hoa : " ) ; scanf("%f", &h) ; H->Toan = t; H->Ly = 1; H->Hoa = h; tb = (t + Ì + h) /3; H->PhanLoai = tb >= 8?1: tb >= 6? 2: tb >= 4? 3:4; 190
  41. 4. Hàm xuất bản ghi Hàm XuatBG nhận hai tham số: Tham số thứ nhất là con trỏ bản ghi cần hiến thị. Trường thứ 2 là tuy chọn cách hiến thị. Nếu Dừ = 0 thì hiển thị các thông tin theo hàng ngang, nếu Dir = Ì thi các trường sẽ thế hiện trên cột dọc giống nhu' một phiêu báo điểm. void XuatBG (struct HocSinh *H, int Dir) ỉ if (Dir) { c Ì r s c r () ; printf("\nSo the : %d", H->No); printf("\nHo va ten : %s", H->HoTen) ; printfr\nNgay, thang, nam sinh: %02d-%02d-%04d", H->Ngay, H->Thang, H->Nam); printf ("\nDiem Toan : %5.1f", H->Toan); printf ("XnDiem Ly : %5.1f", H->Ly) ; printf ("\nDiem Hoa : %5.1f", H->Hoa) ; } el se princf r\n%4đ\t%-20s\t%02d/%02d/%4đ\ t%-5.lf%-5.lf%-5.lĩ', H->No, H->HoTen, H->Ngay, H->Thang, H->Nam, H->Toan, H->Ly, H->Hoa); 5. Thêm bản ghi vào danh sách Thực hiện ý tưởng đã nêu trong mục B.2. Cấp phát con trỏ H, gán cho trường No giá trị của Nb. Nếu Ds = NULL tức là danh sách rỗng thì gán Ds = H, móc nối các con trò Next và Prev của nó cũng vào chính H. Đặt First = H. Nếu Ds khác NƯLL thì chèn H vào danh sách. Đặt Ds = H. Tăng Nb lên Ì đơn vị. 191
  42. void Add( struct HocSinh *H = (struct HocSinh *)malloc(sizeof(HocSinh) H->No = Nồ; if (!Ds) { Ds = H; Ds->Prev = H; Ds->Next = H; First = H; ì else { H->Prev = Ds; Ị H->Next = Ds->Nekt; Ds->Next = H; if (H->Next) H->Next->Prev = H; Ds = H; } Nb++; 6. Nhập danh sách Liên tục gọi hàm Add và nhập giá trị cho con trỏ mới. Việc tiếp tục hay kết thúc vòng lặp thực hiện thông qua hội thoại. Hàm strchr(const char *s, char c) dùng để xác định xem ký tự c có nằm trong xâu s hay không. Người chạy chương trình bắt buộc chỉ có thể trả lời bằng các chữ c, c (có) hay K, k (không). void NhapDs () { char c; do { Add(); NhapBG (Ds) ; 192
  43. printf("\nCo tiep túc không (C/K): do { c = getch(); } while ( !strchr("CcKk" , c) ) ; } while (strchr("Cc",c))ỉ ị 7. Sao chép một con trỏ Dùng đế sao chép dữ liệu từ một con trỏ sang một con trỏ khác sao cho hai con trỏ cuối cùng có nội dung giông nhau nhưng không có gì chung nhau cả. Thao tác này rất cần thiết khi trong bản ghi cần sao chép có thành phần con trỏ. Hàm này sẽ được dùng đến trong việc tìm kiếm hoặc lọc danh sách học sinh giỏi. Ta sẽ tạo lại một danh sách theo đúng mô hình của danh sách hiện có, nhưng sẽ chứa bản sao của tất cả các bản ghi đã được tìm thấy. struct HocSinh *Copỵ (struct HocSinh *H) { struct HocSinh *h = (struct HocSinh *)malloc(sizeof(HocSinh)); *h = *H; h->HoTen = (char *)maÌÌoe(strlen(H->HoTen)+1); strcpy(h->HoTen, H->HoTen); return h; Ị 8. Tìm các bản ghi theo tên Bắt đầu bằng việc khởi tạo một danh sách rỗng mới có tên là T, trong đó vai trò của con trỏ First bây giờ là F. Hàm nhập vào một xâu là tên của học sinh cần tìm, nhưng không nhất thiết là tên đầy đủ. Người chạy chương trình có thể chỉ cần gõ một phần tên, càng đầy đủ thì tìm càng chính xác. Trong hàm có dùng hàm strstr(const char *sl, const char *s2) dùng đê tìm sự có mặt của xâu s2 trong xâu si. Để đảm bảo tìm thấy ngay cả khi hai người chạy chương 13-rỉiảr. trình NN lâD trinh c 193
  44. trình gõ sai chữ in, chữ thường, cả hai xâu được đổi sang chữ i bằng hàm strupr. Mỗi lần tìm thấy một bản ghi thì một bản sao được tạo ra và được chèn vào danh sách T, sô đếm được tăng lên để làm giá trị trả về. Khi kết thúc tìm kiếm, nếu số đếm khác 0 thì danh sách kết quả được in ra thành bảng. Sau đó danh sách kết quả bị xoa. int TimTen(struct HocSinh *First) { clrscr 0 ; char s[ 30] ; int Count = 0; struct HocSinh *T = NULL, *F; window(l, 13, 80, 25); textattr(GREEN + (MAGENTA « 4)); clrscr(); fflush(stdin); printf r\nCho ten: "); gets (s) ; Ds = First; do { if (strstr(strupr(Ds->HoTen), strupr(s))) { if (!T) { T = Copy(Ds)ỉ T->Next = T; T->Prev = T; F = T ; } else { struct HocSinh *h = Copy(Ds); h->Prev = T; 194
  45. h->Next = T->Next; T->Next = h; h->Next->Prev = lì; T = lì; } Count++; } Ds = Ds->Next; } while (Ds != First); if (Count) InDs(F, 0); int k = Count; XoaDs(F, k); return Count; Ị 9. Danh sách học sinh giỏi Hàm này giống hệt hàm TimTen, chỉ khác ở điều kiện tìm kiếm. Điều kiện này không cần nhập vào, mà chỉ cần so sánh xem trường PhanLoai có bằng Ì hay không. int DsHsGioi(struct HocSinh *First) { c Ì r s c r () ; HocSinh *T = NULL, *F; int Count = 0; window(l, 13, 80, 25); textattr(GREEN + (MAGENTA « 4)); clrscr(); Ds = First; do { 195
  46. if (Ds->PhanLoai == 1) í if (ÍT) { T = Copy(Ds) ĩ T->Next = T; T->Prev = T; F = T; } else Ị struct HocSinh *h = Copỵ(Ds); h->Prev = T; h->Next = T->Next; T->Next = h; h->Next->Prev = h; T = h; } Count++; } Ds = Ds->Next; } while (Ds != First); if (Count) InDs(F, 0); XoaDs(F)ỉ return Count; Ị 10. In danh sách Tạo ra một cửa sổ ở nửa dưới màn hình và in lần lượt từng bản ghi theo chiều ngang hoặc theo chiều dọc. void InDs(struct HocSinh *F, int Dir) { window(l, 13, 80, 25); textattr(GREEN + (MAGENTA « 4)); clrscr () ; 196
  47. if (!F) return; struct HocSinh *Ds = F; ìf (!Dir) printfc\nSoTT\tHo va ten "XtNgaỵ sinh\tToan Ly Hoa"); do { XuatBG(Ds, Dir)ị if (Dir) getch () ỉ Ds = Ds->Next; } while (Ds != F); if (!Dir) getch () ; Ị 11. Xóa danh sách Tách bản ghi đầu tiên F ra, đặt bản ghi kế tiếp là F, móc nối để danh sách không bị đứt quãng, sau đó huy bản ghi đã tách. Lặp cho đến khi F là NULL. Nếu danh sách rỗng từ trước thì kết thúc ngay bằng lệnh return. void XoaDs(struct HocSinh *F, int &n) { if í í F) return; while(F) { struct HocSinh *h = F; F->Prev->Next = F->Next; F->Next->Prev = F->Prev; F = F->Next; if (F == h) F = NULL; free (h->HoTen); free (h); rì ; h = NULL; } } 197
  48. 12. Menu Tạo một cửa sổ ở phần trên của màn hình, in danh sách các lựa chọn và xử lý các phím Úp, Down hoặc các phím số để chọn một mục chọn làm giá trị trả về. Tham số char m là mảng các xâu ký tự, n là số mục chọn. int Menu(char m, int n) { int i, k = 0; char c; window(l, Ì, 80, 12); textattr(WHITE + (BLUE « 4)); clrscr(); do { for (i = 0; i < n; i++) { if (i == k) textattr (YELLOĨM + (RED « 4)); else textattr(WHITE + (BLUE << 4)); gotoxỵ (2, i + 2); cprintí ("%-25s" , m[ i] ) ; } c = getch(); if (!c) c = getch(); switch (c) { case 72: if (k) k ; else k = n-1; break; case 80: if (k < n-1) k++; else k = 0; break; default: 198
  49. if (c > 48 && c <= 57) k = c - 49; } while (c != 13) ; return k; 13. Hàm main Liên tục gọi hàm Menu, qua giá trị trả về quyết định gọi các hàm tương ứng. Kết thúc khi giá trị trả về là mục chọn cuối cùng. Kết thúc chương trình bằng việc xóa danh sách. int main(void) { int k = 0; do { k = Menu(m, 6); switch (k) { case 0: NhapDs(); break; case 1: InDs(First, 1); break; case 2: TimTen(First) ; break; case 3: DsHsGioi(First) ĩ break; case 4: XoaDs(First) ; } } while (k != 5) ; XoaDs(First) ; return 0; } 199
  50. VI. CÂU TRÚC BÍT 1. Khai báo cấu trúc bít c có khá năng liên kết các thành phần dữ liệu có kích thước tính bằng bít trong cấu trúc. Ta gọi đó là cấu trúc bít. Ví dụ: struct DATE { unsigned Ngay:5 ĩ unsigned Thang:4; unsigned Nam:7; } ỉ Cấu trúc trên có 3 trường: Trường Ngay có độ lớn 5 bít, giá trị tối đa là 32, đủ để lưu trữ số chỉ ngày trong tháng; trường Thang dài 4 bít, giá trị tối đa là 16, dư dật để lưu trữ các số chỉ tháng; trường Nam dài 7 bít, giá trị tối đa là 128. Nếu lấy năm 2000 làm mốc thì chương trình có cấu trúc này dùng được đến năm 2128, và sau đó sẽ có sự cố năm 2128. Qua đó ta hiếu được cách khai báo các trường của cấu trúc bít: Giống như khai báo bình thường, chỉ thêm vào dấu hai chấm (:) và số bít cần thiết. Trong một dãy bít, nếu có một vài bít nào không dùng và bị bỏ qua thì ta vẫn khai báo kiểu cho nó và sau đó cho số bít luôn mà không có tên, coi như một trường vô danh. Ví dụ trong cấu trúc DATE ở trên, nếu ta muốn đặt hai bít thừa không phải ở cuối cấu trúc mà ở giữa trường Thang và trường Nam thì khai báo như sau: struct DATE { unsigned Ngay: 5; unsigned Thang: 4; int: 2 í unsigned Nam: 5; } ; Toàn bộ cấu trúc chí chiếm 2 byte. Thực chất, bộ nhớ cấp phát cho các cấu trúc loại này cũng được cấp theo phát từng từ nhớ 2 byte. Nếu cấu trúc chỉ có một trường dữ liệu dài Ì bít thì vẫn phải chi cho nó toàn bộ 16 bít. Dù chỉ có 200 ì
  51. vài bít, khai báo kiểu cho các trường vẫn là int, char, unsigned như bình thường, trường nào là int thì có cả phần giá trị âm lẫn phần giá trị dương. Các trường được xếp lần lượt theo thứ tự khai báo vào trong từ nhớ 2 byte từ phải sang trái (từ bít thấp nhất đến bít cao nhất). Nếu thừa một số bít thì bỏ không dùng, nếu thiếu thì xếp tiếp sang từ nhớ tiếp theo. Ta hãy xem hình ảnh của cấu trúc mà ta cho trong ví dụ trên đây: HI LOW Năm X Tháng Ngày Thực ra, không nhất thiết là cả cấu trúc chỉ chứa các thành phần dữ liệu như vậy. Ta có thể cải tiến đôi chút cấu trúc HocSinh trước đây như sau: struct HocSinh{ char HoTen[ 15] ; unsigned SoThe:12; unsigned NgSinh:5; unsigned ThSinh:4; unsigned NămSinh:10; float Toan, Ly, Hoa; } ; Cấu trúc này có trường SoThe chiếm 12 bít, như vậy giá trị tối đa có thể lên tới 4096. Ba trường tiếp theo giống như ta đã xem xét phía trên, tuy nhiên trường NSinh được dành nhiều bít hơn. Do đó phải 1024 năm sau mới có thể có sự cố máy tính như kiểu Y2K. Sau 15 byte đầu thuộc trường HoTen, bốn trường sau đó chiếm 4 byte nhưng chỉ dùng hết 31 bít, một bít cuối cùng để trống không dùng đến. Sau đó là 3 trường Toan, Ly, Hoa, mỗi trường chiếm 4 byte. Tổng độ dài của cấu trúc là 31 byte. 2. Ví dụ về cấu trúc bít Các cấu trúc bít đặc biệt có lợi khi ta đọc các thông tin phần cứng bằng cách truy cập vào bộ nhớ lưu trữ các thông tin hệ thống, vì đây là vùng nhớ có kích thước hạn chế và các thông tin thường rất cô đọng. Ví 201
  52. dụ: Tại địa chỉ tuyệt đối 410H có một cấu trúc dài 2 byte chứa danh sách thiết bị, trong đó: L í Bít thấp nhất cho biết nếu hệ thống có ít nhất một ổ đĩa mềm (Ì) hoặc không có ổ đĩa mềm nào (0). Bít kế tiếp cho biết hệ thống có bộ đồng xử lý toán học (Ì) hav không (0). • Hai bít tiếp theo cho biết số lượng RAM có trên bord mạch chủ: 00: 16K; 01: 32K; 10: 48K; 11: 64K. Trong máy ÁT 2 bít này không được dùng đến. • Hai bít tiếp theo cho biết kiểu màn hình đang dùng: OI: PCjr màu 40 cột; 10: Màu 80 cột; 11: Đơn sắc 80 cột; 00: Không phải các kiểu đã nêu trên. • Hai bít kế tiếp cho biết số ổ đĩa mềm: 00 là Ì ổ; OI là 2 ổ, 10 là 3 ổ; 11 là 4 ổ. • Bít đầu tiên của byte cao, cho biết máy có lắp chíp DMA (0) • Ba bít kế tiếp cho biết số các cổng nối tiếp RS232. • Bít kế tiếp cho biết máy có lắp bộ phối ghép trò chơi (1) hay không (0). • Bít tiếp theo cho biết máy có cắm máy in nối tiếp hay không. Không dùng đến trên máy PC. • Hai bít cuối cùng cho biết số máy in được cắm vào máy. "P 1-. ý M •5 Ọ c- ễ xi Xí u cạ u en ổ *y Xu 'O " 1 tì JS 9- cọ Cu o X •Ọ- c viy ,a u £ BẠ É -5 V .s Q c c •ỉệs >> Oi) 5 /9 >1 D D- ra Xi ••c c ã Ề Ề 3 <o- <-<o u ũ ũ 3 3 <D Xi 3 'à ã '<!) ã 5 z ũ z 3 z Trên cơ sở hiểu biết phần cứng như trên, chúng ta tạo ra một kiểu cấu trúc «ụ bít có tên là DSTzB như trong chương trình dưới đây. Khai báo một con trỏ 202
  53. thuộc kiểu DSTB, dùng lệnh MK_FP đặt nó vào địa chỉ 410H. Sau đó ta dễ dàng lấy ra được các thông tin cần thiết. Trong chương trình còn dùng đến hàm MaHieu. Hàm này đặt một con trỏ xa vào địa chỉ F000:FFFE để lấy ra mã hiệu của máy. Ví dụ VD77.C. #include #include #include #include char * MaHieu () ; struct DSTB{ unsigned FloppyDisk : Ì unsigned Coprocessor : Ì unsigned RAM : 2 unsigned Monitor : 2 unsigned NFloppyDisk : 2 unsigned DMA : Ì unsigned NRS232 : 3 unsigned ơoystic : Ì unsigned SerialPrt : Ì unsigned NPrinter : Ì } ỉ DSTB far *List = (DSTB far *)MK_FP(0, 0x0410); void DisplaỵO { if (List->FloppyDisk) printf("\nCo Ít nhát mót o đìa mèm."); if (List->Coprocessor) printf("\nCo bo dong xu ly toan hoe"); if (strcmp (" ÁT" , MaHieuO)) 203
  54. printf("\nCo %d K trên main boad" , 16 RAM)ỉ printf("\nKieu man hình: ")/ switch (List->Monitor) { case 0: puts("Không ro kiêu man hình"); case 1: puts("PCjr mau 4 0 cót"); break; case 2: puts ("80 cót mau"); break; case 3: puts ("Don sác 80 cót"); break; } printf ("\nSo o đìa mèm: %đ" , List->NFloppyDisk + 1); if (!List->DMA) printf("\nCo chip DMA")ỉ printf("\nSo cong noi tiep: %d" , List->NRS232); if (List->Joystic)printf ("\nCo bo phoi ghép tro choi"); if (List->SerialPrt) printf ("\nCo may in noi tiep" ) ; printf("\nSo cong may in: %d", List->NPrinter); } char *MaHieu() í char far *MaHieu (char far *)MK FP(0xF000, 0xFFFE); char (char *)malloc(15; unsigned char m * MaHieu; switch (m) { case 0xFF: strcpy(s, "PC IBM"); break; case 0xFE strcpy(s, "XT/PC portable") break ỉ case 0xFD strcpy(s, "PCjr"); break; case 0xFC strcpy(s, "ÁT"); break; case 0x2D strcpy(s, "Compaq"); break; case 0x9A strcpy(s, "Compaq Plus"); break; re tùm s/ 204
  55. main () { Display(); getch () ; re tùm 0 ; Ị Trên đây chỉ là một ví dụ về sử dụng cấu trúc bít. Khi lập trình hệ thống, điều khiển phần cứng, người lập trình thường phải chuẩn bị các byte dữ liệu đế đặt chúng vào các thanh ghi của thiết bị trước khi gọi một ngắt nào đó, cấu trúc bít là một công cụ hiệu quả và dễ sử dụng. Việc truy cập vào các vùng chứa thông tin về tổ chức của đĩa cứng hoặc đĩa mềm cũng rất cần có cấu trúc bít. 205
  56. Chương li KIỂU ENUM VÀ UNION ì. KIỂU ENUM 1. Định nghĩa Enum là một kiểu dữ liệu do người dùng định nghĩa trong đó miền giá trị là một dãy các hằng kiểu int, mỗi hằng có một tên. 2. Khai báo kiểu enum Cú pháp: enum [ ] [ [= ], ] [ ]; Trong đó: • là tên một kiểu mà ta muốn tạo ra. Nếu thiếu thì danh sách các hằng chỉ dùng được một lần để khai báo các biến trong [ ]. Nếu có thì sau đó trong chương trình ta có thể khai báo các biến thuộc kiểu này nhiều lần. • [= ] là tên hằng có thể có giá trị nguyên tương ứng theo sau dấu bằng. Nếu không có [= ] thì tên hằng mặc nhiên có giá trị 0 nếu nó đứng đầu danh sách, nếu không nó sẽ có giá trị bằng giá trị kề sau của hằng đứng trước nó trong danh sách. ì [ ] là tên các biến viết cách nhau một dấu phẩy. Nếu trong khai báo không có thì nhất thiết phải có [ ], trong đó có ít nhất Ì biến. Nếu có tên kiểu thì không nhất thiết phải có [ ], vì thực ra sau đó ta có thể khai báo các biến khi nào cần đến chúng. 206
  57. 3. Lợi ích của kiểu enum Cũng giống như việc khai báo các hằng, việc sử dụng enum đối với người lập trình là hoàn toàn tự nguyện, không dùng cũng chẳng sao, nếu dùng thì sẽ có lợi. Cái lợi thứ nhất là thay vì các con số câm lặng và lẫn lộn là các hằng mà bản thân cái tên của chúng đã gợi lên nhiều ý nghĩa, rõ ràng và không thế gây nhầm lẫn. Cái lợi thứ hai là từ các hằng đó ta có thể tạo nên một kiểu dữ liệu mới tự nhiên hơn, gần với đời thường hơn là kiểu số nguyên. Ta biết rằng một ngôn ngữ lộp trình dù hoàn hảo đến đâu đi nữa vẫn chí là thứ ngôn ngữ nhân tao, gò bó, vô lý và thiếu tự nhiên một cách ghê gớm đến mức nhiều bạn học sinh phải kinh hãi khi nghe nói đến môn học lập trình. Thực ra trước khi học đến phần này thì chúng ta cũng đã sử dụng các kiểu dữ liệu được định nghĩa bằng enum rồi. ví dụ khi bạn sử dụng màu sắc bằng các tên hằng BLƯE, RED, YELLOW khai báo trong tệp conio.h. 3.1. Các hằng chỉ màu sác enum COLORS { BLACK, /* dark colors */ BLUE, GREEN, CYAN, RED, MAGENTA, BROWN, LIGHTGRAY, DARKGRAY, /* light colors */ LIGHTBLUE, LIGHTGREEN, LIGHTCYAN, LIGHTRED, LIGHTMAGENTA, YELLOW, WHITE } ; 207
  58. Trong khai báo trên, người ta dùng từ khoa enum để định nghĩa kiểu COLORS trong đó có các giá trị từ BLACK đến WHITE. Vì không có quy định tường minh nên mặc nhiên BLACK có nghĩa là giá trị 0, BLUE có nghĩa là Ì, và WHITE có giá trị là 15. Nhờ có khai báo này mà chúng ta có thể đặt màu: textcolor(BLUE); Thay cho lệnh: textcolor (Ì); Và có thể thực hiện đoạn lệnh sau: for (COLORS c = BLACK; c <= WHITE; C++) { textcolor (c); cprintf (" ESTIH" ) ; } Bạn hoàn toàn có thể bắt chước đoạn khai báo trên trong chương trình của bạn với một chút sửa đổi như sau: enum MAU SÁC { ĐEN, /* Màu đen * / XANH_DATROI, /* Xanh da trời */ XANH_LACAY, /* Xanh lá cây */ TIM, /* Tím */ DO, /* Đỏ * / CANH_SEN, /* Cánh sen */ NAU, /* Nâu */ XAM_NHAT, /* Xám nhạt * / XAM_DAM, /* Xám sẫm * / XANH_DATROì SANG, /* Xanh da trời nhạt */ XANH_LACAY SANG, /* Xanh lá cây nhạt */ TIM_NHAT, /* Tím nhát * / DO_NHAT, /* Đỏ nhát */ CANH_SEN_NHAT, /* cánh sen nhạt */ 208
  59. VANG, /* Vàng */ TRANG /* Trắng */ } ỉ Và thực hiện các lệnh sau: textcolor(VANG); textbackground(DO); cprintf (" Viet Nam" ) ; hoặc for (MAU_SAC c = ĐEN; c <= TRANG; C++) { textcolor (c) ; cprintí (" ESTIH" ) Ị } Rõ ràng chương trình của bạn đã gần hơn một chút với ngôn ngữ hàng ngày. 3.2. Các hàng chọn nét vẽ enura line widths { /* Line widths for get/setlinestyle */ NORM WIDTH = 1 THICK WIDTH = 3 } ; Ví dụ này lấy trong tệp GRAPHICS.H dùng để định nghĩa bề dày các nét vẽ. Vì chi có hai loại bề dày, loại mảnh và loại dày gấp 3 lần nên kiểu này chỉ có 2 hằng và chỉ định tường minh là NORM_WIDTH ứng với Ì, THICK_WIDTH ứng với 3. 3.3. Các hằng chè độ hiển thị văn bản enum MODES { LASTMODE = -1, BW40 = 0, C40, BW80, C80, MONO = 7 }; Trong đó: • "modes" là tên kiểu. G "LASTMODE", "BW40", "C40", là tên các hằng. • Giá trị của C40 mặc nhiên là Ì vì là (BW40 + 1); c Giá trị của BW80 mặc nhiên là 2 vì là (C40 + Ì); 14-Giáo trinh NN láp trinh c 209
  60. Giá trị của C80 mặc nhiên là 3 vì là (BW80 + Ì); 4. Điểm bất lợi của kiểu enum Tuy kiểu enum có cái hay như ta đã nói trên, nhưng nó lại có cái bất lợi khi cần nhập và in các giá trị của kiểu này. Xét ví dụ VD78.C sau đáy: 4.1. Ví dụ VD78.C #include #include int main(void) { for (COLORS c = BLACK; c <= WHITE; C++) { textcolor(c); cprìntí (" ESTIH" ) ; } print f (" \ nCho mau: "); scanf (" %d" , se) ỉ printf (" %d" , c) ; getch ()ỉ re tùm 0; Ị Trong chương trình này, mặc dù kiểu COLORS đã được khai báo nhưng khi thực hiện lệnh: scanf (" %d" , &c) ; Ta không thể nhập giá trị BLACK hay MAGENTA được. Nếu ta nhập số 5 và xem giá trị cùa c trong cửa sổ watch, ta thấy bên cạnh giá trị 5. BORLAND C++ còn ghi chú thêm là "MAGENTA", nhưng nếu ta nhập MAGENTA thì c sẽ chỉ cho giá trị là 16. Tương tự, khi in c ra, ta không có một mẫu định dạng nào thích hợp để in kiêu dữ liệu này, và do đó, bản chất nó là kiểu int thì ta lại phải dùng đặc tả "%d". Kết quá giá trị in ra chí là con số. 210
  61. Có một cách hơi nhiêu khê để giải quyết vấn đề này. Xin hãy xem ví dụ VD79.C sau đây: 4.2. Ví dụ VD79.C #include #include #include COLORS NhapMau () { char s[ 10] ; printf (" \nCho mau: " ) ; gets (s); char * us = strupr (s); if (! strcmp(us, "BLACK")) return BLACK; else if (!strcmp(us, "BLUE")) return BLUE; else if (!strcmp(us, "GREEN")) return GREEN; else if (!strcmp(us, "CYAN")) return CYAN; else if (!strcmp(us, "RED")) return RED; else if (!strcmp(us, "MAGENTA")) return MAGENTA; else if (!strcmp (us, "BROWN") ) return BROWN; else if (!strcmp(us, "LIGHTGRAY" ) ) re tùm LIGHTGRAY; else if (!strcmp(us, "DARKGRAY" ) ) re tùm DARKGRAY ; else if (!strcmp(us, "LIGHTBLUE")) return LIGHTBLUE; else if (!strcmp(us, "LIGHTGREEN")) return LIGHTGREEN; else if (!strcmp(us, "LIGHTCYAN")) re tùm LIGHTCYAN; else if (!strcmp (us, "LIGHTRED") ) re tùm LIGHTRED; 211
  62. else if (!strcmp(us, "LIGHTMAGENTA" ) ) re tùm LIGHTMAGENTA; else if (!strcmp (us, "YELLOW")) return YELLOW; else if ( !strcmp (us, "WHITE")) re tùm WHITE; else re tùm BLACK; } void InMau (COLORS c) { textattr(c + ( (7 - (c % 8) ) « 4) ) ; switch (c) { case 0 cprintf (' \n\rBLACK" ) ; break; case 1 cprintí (' \n\rBLUE"); break; case 2 cprintf (' \n\rGREEN"); break; case 3 cprintf (' \n\rCYAN"); break; case 4 cprintf (' \ n\ rRED" ) ; break; case 5 cprintf (' \n\rMAGENTA"); break; case 6 cprintí (' \n\rBR0WN"); break; case 7 cprintf (' \n\rLIGHTGRAY" ) ; break; case 8 cprintí (' \n\rDACKGRAY" ) ; break; case 9 cprintf (' \n\rLIGHTBLUE" ) ; break; case 10 cprintf ("\n\rLIGHTGREEN"); break; case li cprintf ("\n\ rLIGHTCYAN" ) ; break; case 12 cprintf (" \ n\ rLIGHTRED" ) ; break; case 13 cprintf ("\n\rLIGHTMAGENTA" ) ; break; case 14 cprintf (" \ n\ rYELLOW" ) / break; case 15 cprintf ("\n\rWHITE" ) ; break; } } int mâm (void) { clrscr 0 ; 212
  63. COLORS c; for (c = BLACK; c <= WHITE; C++) { textcolor(c); InMau (c) ; } c = NhapMau(); InMau(c); getch () ; re tùm 0 ; } Trong ví dụ trên đây, hàm InMau nhận một giá trị màu (bất kế số hay hằng kiểu COLORS), và in ra một xâu ký tự ứng với tên hằng tương ứng bằng chính màu mà giá trị đó thể hiện. Hàm NhapMau nhận một xâu ký tự, chuyển đổi xâu đó thành chữ in và so sánh với tên các hằng của kiểu COLORS và trả về giá trị màu bằng tên hằng tương ứng. Hàm main ứng dụng hai hàm trên để in ra tất cả các màu trong danh sách màu. nhập vào một màu bằng hàm NhapMau và in ra giá trị màu đó bằng hàm InMau. Ví dụ trên đây minh hoa một cách giải quyết vấn đề, có thể áp dụng cho các trường hợp kiểu enum khác. Tuy nhiên không nên lạm dụng vì rõ ràng là cách làm này quá rườm rà. Thông thường vì bản chất các giá trị trong kiểu enum là số nguyên thì ta cứ cố gắng sử dụng chúng như số nguyên. Chỉ khi thực sự cần thiết xuất nhập các giá trị hằng theo tên gọi của chúng thì ta mới dùng cách làm này. 5. Tự tạo ra kiểu Boolean Đối với những lập trình viên Pascal, khi chuyển sang c thường có phần luyến tiếc kiêu Boolean của Pascal. Đó là kiểu dữ liệu chi nhận các giá trị là True hoặc False. 213
  64. Trong c không có kiểu này. c chỉ xem xét xem kết quả so sánh hoặc kết quả một phán quyết cho giá trị số là 0 hay khác 0. Có thể quy ước với nhau 0 là False và khác 0 là Trúc Thực ra cách làm của c mềm dẻo, cô đọng và hiệu quả hơn nhiều so với Pascal. Nếu bạn muốn có một kiểu Boolean như trong Pascal thì bạn có thể tự làm được điều đó chẳng mấy khó khăn. Khai báo như sau: enum Boolean { False, True }_2 Và sau đó có quyền khai báo: Boolean a, b = True; Hay so sánh: if (m > n == True) Max = m; else Max = n; li. KIỂU UNION 1. Định nghĩa union là một cấu trúc dữ liệu có cách khai báo rất giống như struct nhưng ý nghĩa của hai cấu trúc dữ liệu này khác hẳn nhau. Nếu như struct là một tổ chức dữ liệu gồm nhiều thành phần khác kiểu nhau được sắp xếp liền kề nhau trong bộ nhớ thì union là một tổ chức dữ liệu gồm nhiều thành phần đặt vào cùng một vị trí trong bộ nhớ. 2. Khai báo Khai báo của union như sau: union [ ] { 214
  65. [ ;]+ } [ ] ; 3. So sánh union và struct union struct int or long { int and long { int i; i n t i; long 1; long ì; } a number; } a struct; 71 3 lon 2 a ạ ra HI t Ì long ĩ ZJZZ z ^ É =3Z Hình 52. So sánh struct và union Hai khai báo trên rất giống nhau, nhưng ta thấy trong khi kiểu int_or_Iong cần 4 byte thì khiếu int_and_long cần 6 byte. Điều này không có nghĩa là kiểu int_or_long tiết kiệm hơn kiểu int_and_long. Trong biến a_number có hai thành phần chổng lên nhau. Thành phần int i ngắn hơn chiếm 2 byte đầu của biến. Thành phần long Ì chiếm toàn bộ 4 byte. Như vậy hai byte đầu tiên là chung cho cả hai thành phần. Để truy cập vào một thành phần của union, cách làm hoàn toàn giống như đối với kiểu struct. Cũng có thế khai báo một union không tên như sau: union { int a ; long b; Với khai báo này, chúng ta có thể truy cập vào các thành phần a và b như các biến độc lập thông thường. Chỉ khác là cả hai biến này đều bắt đầu từ cùng mót đìa chỉ. 215
  66. Chương 12 TỆP ì. TỔNG QUAN VỀ TỆP Trong ngôn ngữ c, việc đóng mở và truy cập các tệp rất gần với công việc của các dịch vụ ROM-BIOS và hệ điều hành. Điều đó có nghĩa là người lập trình có thể sử dụng những cổng cụ tinh vi nhất để can thiệp vào ổ đĩa của mình, nhưng đổng thời cũng có nghĩa là người lập trình bị đặt trước bức tường ngăn cách giữa phần cứng bí hiếm và môi trường lập trình đơn gián, thản thiện. Lẽ ra không cần biết gì về cấu trúc vật lý của tệp, chi cần vài lệnh mờ tệp. đóng tệp, đọc tệp và ghi tệp, cùng lắm là cần phân biệt một tệp văn bản với một tệp truy cập ngẫu nhiên, đọc và ghi từng bán ghi một thì người lập trình luôn luôn chi được phép coi các tệp của mình chỉ là một chuỗi các byte hay các ký tự, bất kể nội dung của chúng là gì. Có thê nói, lập trình c với các tệp gần giống như lập trình bằng hợp ngữ. Công cụ của c cho công việc thao tác với các tệp là các dịch vụ quản lý tệp của hệ điều hành, cụ thể là với các máv PC, đó là các dịch vụ cùa ngất 21H, một số các lệnh truy xuất gọi là các hàm truy xuất cấp Ì. Tuy nhiên, tình trạng nêu lên trên đây chi tồn tại với các phiên bản đầu tiên của c. Trong các phiên bản sau, người ta đã tạo nên từ vốn liếng ban đầu của c các hàm nhập xuất trên tệp gần gũi thân thiện hơn. Công cụ của c để làm các công việc đó được gọi là các hàm truy xuất tệp cấp 2. Người lập trình có thể không cần biết gì về ngắt 21H, và có thể thao tác đơn giản hơn với các bản ghi của mình, có thế ghi và đọc từ tệp các dữ liệu kiêu nguyên. Tuy nhiên, không ai cấm người lập trình sứ dụng cách truy xuất cũ. hoặc phối hợp một cách nhuần nhuyễn mềm dẻo cả hai nhóm hàm truy xuất tệp của c. Như vậy, thực tế có 3 cấp truy xuất tệp: 216
  67. Cấp 0: Sử dụng các dịch vụ ngắt 21H. Vì ta chưa học cách sử dụng các ngắt nên bỏ qua mức này. Cấp Ì: Sử dụng các hàm truy xuất cấp Ì. Cấp 2: Sứ dụng các hàm truy xuất cấp 2. li. CÁC HÀM TRUY XUẤT TỆP CÁP Ì 1. Đặc điểm của các hàm truy xuất tệp cấp 1 Coi các tệp là nơi lưu trữ các byte. Bất kể dữ liệu thuộc kiểu gì đều được thao tác theo từng bvte. Ví dụ: Số nguyên là 2 byte kề nhau; số thực (float) là 4 byte kế tiếp nhau. Trao đổi dữ liệu từ chương trình với tệp không thông qua vùng đệm. Hay nói đúng hơn, người lập trình phải tự tạo ra vùng đệm cần thiết cho công việc vào/ra. Muốn thao tác với một cái ấm thì ta phải cầm vào quai của nó. Mỗi tệp cũng cần có một cái quai. Mọi thao tác trên tệp đều phái dựa vào cái quai đó. Thao tác mở tệp nhằm tạo ra một handle (tay cầm hoặc quai). Sau đó mọi thao tác lên tệp đều thông qua trung gian là cái quai. Handle thực ra là một số nguyên chí số hiệu tệp. Mỗi khi mở tệp, c sẽ gán cho tệp một số hiệu. Mọi thông tin liên quan đến tệp đều do hệ thống lưu giữ. Người lập trình chỉ cần handle là đủ. Có một số tệp đặc biệt đã được hệ điều hành mở sẵn và cấp cho các số hiệu (handle) như sau: 0: Đơn vị vào chuẩn (bàn phím). Ì: Đơn vị ra chuẩn (màn hình). 2: Đơn vị báo lỗi chuẩn (màn hình). 3: Đơn vị phụ (cổng COM1). 4: Cổng máy in (LPT1). Các số hiệu trên kia không liên quan tới tệp nào cả, vì đó thực chất là các thiết bị. c đã coi chúng như các tệp cốt tạo ra một giao diện thống nhất để liên lạc giữa chương trình và các thiết bị ngoại vi bao gồm bàn phím, màn hình, cống COM, máy in, ổ đĩa Điều đó có nghĩa là nếu chúng ta muôn ghi cái gì đó ra máy in thì cứ yên trí coi nó như một tệp và thực hiện các thao tác in ra máy in như ghi vào một tệp. Các thao tác ghi ra tệp hoặc 217
  68. ghi ra máy in hoàn toàn giống nhau nên trong chương trình, chúng ta có thể lựa chọn nén in kết quả ra máy in hay lên màn hình bằng cách lựa chọn giữa hai handle. Tương tự như vậy, chúng ta có thể lựa chọn giữa việc nhập dữ liệu từ bàn phím hay từ tệp bằng cách lựa chọn handle, trong khi các thao tác "hi hoặc đọc hoàn toàn không thay đổi. Dưới hệ điều hành. người sử đụn" có thế hướng lại các số hiệu của các thiết bị trên vào hoặc từ các thiết bị khác, ví dụ thay vì nhập dữ liệu cho một chương trình từ bàn phím thì nhập từ một tệp. Công việc của chúng ta là học cách mở tệp, đóng tệp, ghi vào tệp, đọc từ tệp, xử lý các dữ liệu trên tệp và cập nhật chúng. 2. Truy xuất tệp Ngay cả khi chưa biết mở tệp, ta cũng có thể học cách ghi vào tệp và đọc dữ liệu từ tệp với các tệp vào ra chuẩn đã được mở sẵn. Xét ví dụ VD1.C sau: #include #include void main () { int n ; char buf[ 2 0] ; while (((n = read (0, buf, 20)) > 1)) write (Ì, bui, n); } Ví dụ trên đây khai báo một mảng bui 20 ký tự dùng làm vùng đệm cho quá trình đọc và ghi. Lệnh read đọc từ tệp có số hiệu 0 vào vùng đệm bui 20 byte dữ liệu. Tuy lượng dữ liệu có trong tệp, lệnh read sẽ trả về số bvte mà nó đọc được, nhiều nhất là 20. Nếu trong tệp còn dữ liệu thì dữ liệu đó sẽ được đọc trong vòng lặp tiếp theo, nếu tệp còn chưa đến 20 byte thì n sẽ là số byte còn lại đó và vòng lặp sau sẽ phát hiện ra tệp hết dữ liệu. Khi phát hiện ra tệp hết dữ liệu thì lệnh read trả về n giá trị 0. Lệnh vvrite làm công việc ngược lại. Nó ghi ra tệp có số hiệu Ì n byte có trong bui. Trong chương trình, n luôn là số 218
  69. byte đọc được bởi lệnh read, đọc được bao nhiêu ghi ra bấy nhiêu nên ta không cần quan tâm đến giá trị trả về của lệnh write. Tệp cung cấp dữ liệu mang số hiệu 0 chính là thiết bị vào chuẩn (bàn phím) và tệp đón nhận dữ liệu của lệnh write là tệp mang số hiệu Ì chính là thiết bị ra chuẩn. Chương trình này chưa cần đến các thao tác mở tệp và đóng tệp vì các tệp nói trên đã được mở sẵn rồi. Sau khi nghiên cứu ví dụ trên, ta sẽ nói kỹ hơn về cú pháp của lệnh read và lệnh write. 2.1. Lệnh read • , , : Giá trị nguyên là handle của tệp đã mở chứa dữ liệu cần đọc. • : Địa chỉ một vùng nhớ nào đó đã được cấp phát. Thực ra c không cần quan tâm xem vùng nhớ này đã được cấp phát hay chưa và lượng bộ nhớ được cấp phát là bao nhiêu. Nếu có một địa chí, dù là địa chỉ hú hoa thì lệnh read vẫn cứ đọc dữ liệu và chất vào đó, không quan tâm là ở đó đang chứa những gì Vì vậy cần thận trọng kiểm tra trước khi dùng lệnh read. • . Nếu cần đọc nhiều byte mà vùng đệm lại bé thì chia ra đọc nhiều lần. = write( , , ); • : Là một biến nguyên, nhận giá trị là số byte được ghi thực tế bằng lệnh vvrite. Nếu write trả về giá trị -Ì thì thao tác ghi có lỗi (tệp không tổn tại, đĩa hoặc tệp cấm ghi, đĩa đầy). 219
  70. : Giá trị nguyên là handle của tệp đã mở để ghi dữ liệu. : Địa chi một vùng nhớ chứa dữ liệu cần ghi. c không cần quan tâm xem vùng nhớ này chứa dữ liệu kiêu gì. Nó chí đếm lấy đù số byte cần ghi từ đó và ghi vào tệp. : Một giá trị nguyên chí cho lệnh \vrite cần lấy bao nhiêu byte cho một lần ghi. Cần chú ý đế nhò hơn hoặc bằng dung lượng bộ nhớ được cấp phát của . Nếu vùng đệm lớn mà số byte cần ghi bé thì phải ghi thành nhiều lần. có thể bằng trong trường hợp bình thường. Ngược lại lớn hơn rất có thế là do đĩa đầy. Sau mỗi lần ghi. con trỏ định vị dữ liệu trong tệp sẽ tịnh tiến về phía cuối tệp n byte với n = . 3. Tạo tệp và mỏ đóng tệp 3.1. Tạo tệp Bây giờ ta sẽ khống dựa vào các tệp mở sẩn nữa mà tự tạo ra tệp của mình. hoặc tự mớ tệp ra mà dùng. Trước tiên ta học cách tạo tệp mới hoàn toàn. Xét vídụ VD81.C sau: #include #include #include void main () { int n ; char buf[ 2 0] ; int f = creat("Data.byt", S_IWRITE); while (Un = read (0, buf, 20)) > 1)) write (í, bui, Vì) ỉ close(f); Ị Chương trinh này chi khác chương trình trước ở chỗ: 220
  71. Có thêm dòng khai báo #include ! Lệnh int f = createCData.byt", S_IWRITE); • Lệnh vvrite (Ì, bui", n) đổi thành write (f. bui, n); • Lệnh close(f); Lệnh int f = create("Data.byt", S_IWRITE) khai báo một biến nguyên và khởi gán giá trị cho nó bằng kết quả trả về của lệnh creat. Lệnh creat nhằm tạo ra trên thư mục hiện tại một tệp có tên là "Dulieu.byt", ớ mode S_IWRITE, có nghĩa là người chú của tệp có quyền ghi vào tệp. S_IWRITE là một hằng có giá trị 0x0080 được định nghĩa trong tệp SYSNSTATH Sau lệnh này, tệp "Data.byt" được tạo ra trên đĩa. có số hiệu lưu trong biến f và sẵn sàng chờ được ghi. Lệnh \vrite(f, bui, n) sẽ ghi n byte trong bui vào tệp f. Sau khi ghi xong, lệnh close(f) sẽ đóng tệp "Data.byt" lại. Trong chương trình này, thao tác đọc dữ liệu vẫn tiến hành như trong ví dụ trước, tức là đọc từ bàn phím. Đế đảm báo chương trình có thế thoát ra khỏi vòng lập khi mong muốn, ta đặt điều kiện là n > Ì, vì nếu đọc các byte từ bàn phím. không bao giờ xảy ra trường hợp kết quả đọc cho n = 0. Bất kế ta gõ phím nào thì nó cũng tạo ra ít nhất là Ì byte dữ liệu. 3.2. Cú pháp lệnh creat = creat( , ); là một biến kiểu int; là một giá trị kiểu xâu ký tự. Nếu bao gồm cả đường dẫn có các dấu chéo (\) phân cách giữa các tên thư mục thì phải ghi thành hai dấu chéo (\\). một giá trị nguyên cho biết mode bảo vệ của tệp sẽ được tạo ra. Đâv là một số nguyên dài 2 byte, trong đó một số bít tương ứng với các mode bảo vệ khác nhau cùa tệp. S_IWRITE (cho phép ghi) S_IREAD (cho phép dóc) S_IREAD S_IWRITE (cho phép dóc và ghi) creat thực ra chỉ cần kiểm tra bít S_IWRITE. 221
  72. Dưới DOS, S_IWRITE đồng thời cho phép đọc. Tất cả các thuộc tính DOS khác của tệp được đặt bằng 0. Tệp luôn là tệp nhị phân trong cả hai mode trên, có nghĩa là ghi và đọc các byte. Khi việc tạo tệp thành công, con trỏ định vị được đặt ở đầu tệp. 3.3. Biến _fmode Lệnh creat đồng thời cũng là lệnh mở tệp để sẵn sàng ghi vào đó. Do đó cũng cần quy định rõ mode làm việc của tệp trước khi thực hiện các thao tác đọc và ghi. Việc này có thể thực hiện được nhờ gán trị cho biến _fmode trước khi tạo tệp. Biến này đã được khai báo sẵn trong tệp FCNTL.H hoặc STDLIB.H. _fmode = 0_BINARY; //quỵ định tép sẽ được tao ra làm việc ở mode nhị phân. _fmode = 0_TEXT; //quy định tép sẽ được tao ra làm việc ở mode văn bản. Nếu tệp định mở đã tồn tại thì việc tạo tệp đồng nghĩa với việc đặt con trỏ vào đầu tệp. Nếu thuộc tính bảo vệ của tệp đã tồn tại cho phép ghi thì tệp sẽ bị xoa, chiều dài bằng 0. Nếu tệp đã tồn tại có thuộc tính chỉ đọc thì việc tạo tệp bằng lệnh creat thất bại. 3.4. Mơ tệp Xét ví dụ VD82.C: #include #include #include #include void main () { int n ; char buf[ 2 0] ; int f = open ("Data.byt" , 0_RDONLY I 0_BINARY); while (((n = read (f, bút, 20)) > 0)) write (Ì, bui, n); c Ì o s e ( f) ; } 222
  73. Chương trình này có thêm một dòng #include . Tệp f được mở bằng lệnh open là nguồn dữ liệu để đọc và in kết qua lén màn hình. Ta xét ký lệnh open: Tham số đầu tiên là tên tệp, tham số thứ hai là kiểu truy xuất chi đọc. Tham số thứ 3 xác định tệp được mờ như một tệp nhị phân, tức là dữ liệu bên trong dược coi là các bvte. Điều kiên tiếp tục vòng lặp có hơi khác: Thay vì là n > Ì là n > 0. Vi lần này dữ liệu được đọc từ tệp. Khi hết tệp mà còn cố đọc thi kết quả trả về của lệnh read là 0. Các hãng ký hiệu ()_RDONLY và 0_BINARY được định nghĩa trong tệp ĩcntl.h. 3.5. Cú pháp lệnh open = open( , , [ ]); là một biến kiểu int. là một giá trị kiểu xâu ký tự. Nếu bao gồm cả đường dẫn có các dấu chéo (\) phân cách giữa các tên thư mục thì phải ghi thành hai dấu chéo (\\). một giá trị nguyên trong đó mỗi bít được gọi là một cờ (flag) cho biết kiểu truy xuất tệp, phân biệt tệp là vãn bản hay nhị phân. Các hằng ký hiệu được định nghĩa trong ícntl.h. là tham số không bắt buộc. Thường chi dùng khi trong số các kiểu truy xuất có 0_CREAT. Các hằng ký hiệu tương ứng được định nghĩa trong SYS\STAT.H, chính là các hằng mà ta đã xét khi học lệnh creat. Có nhiều hằng ký hiệu thể hiện cách truy xuất tệp khôns phụ thuộc lẫn nhau. Khi mớ tệp với nhiều hằng như vậy thì cần kết hợp chúng với nhau bằng phép toán cộng bít ( I ). Trong ví dụ trên: int f = open("Data.byt" , 0_RDONLY I 0_BIMARY) ; Tệp được mở để đọc ở mode nhị phân, vì vậy tham số là 0_RDONLY I CLBINARY. Sau đây là một số hằng quan trọng: ;0_RDONLY MỞ chỉ để dóc 0_WRONLY MỞ chỉ để ghi 223
  74. 0_RDWR MỞ để đọc và để ghi MỞ để ghi tiếp vào cuối tạc . Khi 0_APPEND tệp mở, con trỏ định vị sẽ đặt ả cuối tép. Tạo và mở tệp. Nếu tép đã tổn Ị tai thì hằng này không cố tác 0 CREAT dụng. Nếu tệp chưa tồn tại thì tệp sẽ đươc tao ra MỞ tép ở mode nhị phân. Không có; 0 BINARY bất kỳ sư chuyển đối nào. 2 r 2 , . *' ĩ Mớ tệp ớ mcde văn ban. Chuyên đổi cáp ký tư CR-LF (OxODŨA) 0 TEXT thành LF (OxOA) và ngược lại. CTRL-Z (OxlA) chỉ kết thúc tệp. Hình 53. Các hàng xác định mode làm việc của tệp 3.6. Di chuyến con trỏ định vị Khi tệp được mở đế đọc hoặc ghi. con trỏ định vị được đặt ở đầu tệp. tức là trước bvte đầu tiên của tệp. Nếu khi mở đế ghi có dùng hằng 0_APPEND thì con trỏ định vị được đặt ở sau byte cuối cùng của tệp. VỊ trí đầu tệp được mã hoa bằng giá trị 0. Vị trí cuối tệp được mã hoa bằng giá trị 2. Vị trí hiện tại của con trỏ bất kế nó đang ở đâu được mã hoa bằng giá trị Ì. Các giá trị này được định nghĩa thành các hằng ký hiệu trong tệp 10.H như sau: HẰNG GIÁ TRI VỊ TRÍ CON TRỎ SEEK_SET 0 Đầu tệp SEEK_CUR Ì Vị trí hiện tại SEEK_END 2 Cuối tệp Hình 54. Các hàng xác định vị trí con trỏ định vị dữ liệu Quá trình ghi và đọc bằng các lệnh vvrite và read được thực hiện bình thường một cách tuần tự. bắt đầu từ đầu tệp. sau mỗi thao tác ghi hoặc đọc thì tịnh tiến thêm về phía cuối tệp. 224
  75. Nếu ta không muốn tiến hành quá trình một cách tuần tự như vậy thì cần phải biết cách đặt lại con trỏ định vị vào vị trí cần thiết để đọc và ghi. Thao tác này được thực hiện bằng lệnh lseek với cú pháp như sau: lseek( , , ); Trong đó: • : Số nguyên do các hàm creat hoặc open trả về đế xác định tệp. [ i : Số nguyên dài chí độ dịch chuyên tính từ vị trí làm chuẩn. Nếu dương thì chuyển dịch hướng về cuối tệp, nếu độ dịch chuyển âm thi chuyển dịch hướng về đầu tệp. ũ ); 5. Xác định kích thước tệp Hàm íilelength với tham số là số hiệu tệp sẽ cho biết dung lượng của tệp (tính bằng byte). Cú pháp: = filelength( ); 6. Ví dụ Ví dụ VD83.C sau đây nhằm minh hoa cách sử dụng các hàm truy xuất tệp cấp Ì vào việc quản lý các bản ghi, trong đó có các thao tác ghi và đọc các bản ghi, di chuyển trong tệp, cách lấy chiều dài tệp và xác định vị trí hiện tại của con trỏ định vị. #include #include #include #include #include #include #define NMax 100 15-Giáo trình NN lập trình c 225
  76. struct KocSinh { char HoTen[ 2Z\ ; íloai Diêm; ỉ t int r. ; int Nháp (HccSir.h *H) f flush(stdin) ; printf (" nHo va ten : aeis(H->HoTen); if (!strlen(H->HoTen)) re tùm 2 ; float x; printí (" nDiem : ") ; scanf (" %f" , &x) ; H->Diem = x; re tùm 1; } void NhapDS() { int i = rì; do { i = Nháp(D+n)ỉ if (i) n++; } while (i)Ị Ị • void Xuân(HocSinh H) 226
  77. c Ì r s c r 0 ; printf("\nHo va ten li H.HoTen); printf (" \ nDiem %4 . lf " , H.Diêm) ; getch (); } void InDS(HocSinh *Ds) { int i; printf ("\nSTT\ tHo va ten\ t\ tDiem" ) ; printí ("\n " ) ; for (i = 0; i 0) k = write(f, Ds, n*sizeof(HocSinh)); close(ĩ) ỉ return k/sizeof(HocSinh); int Load(char *FName, HocSinh *Ds) { int f = open(FName, 0_RD0NLY I 0_BINARY); n = 0; if (f > 0) Víhile ((read(f, Ds++, sizeof (HocSinh) ) > 0)) n++; close(f); re tùm ri; } 227
  78. int MoTep(char *FName) { int f = open(FName, 0_RDWRI 0_BINARY); if (f < 0) f = creat(FName, S_IWRITE); else Load(FName, D); NhapDS () ; Save(FName, D, n); InDS(D); re tùm f; } int Menu(int k) { char *m[] = { "In danh sách", "Mb tép" , "Xem ban ghi truoc", "Xem ban ghi sau" , "Ban ghi dâu tiên", "Ban ghi cuoi cung", " Thoát"} ; clrscr 0; char c; int i ; do { for (i = 0; i < 7; i++) { gotoxỵ (10, i + 5) ; if (i == k) textattr(YELLOW + (RED « 4)); else textattr(WHITE + (BLUE << 4)); cprintí (" %d. %s" , i, m[ ì] ) ; } 228
  79. c = getch () ; switch (c){ case 72: if (k > 0) k ; break; case 80: if (k sizeof(HocSinh)) { lseek(f,-2L*sizeof(HocSinh) , SEEK_CUR); if (read(f, &H, sizeof(HocSinh))) Xuat (H); } break; case 3: if (tell(f) < filelength (f)) { if (read(f, &H, sizeof(HocSinh))) Xuat (H); } 229
  80. break; case 4: lseek(f, OL, SEEK_SET); if (read(f, &H, sizeof(HocSinh))) Xuat (H)Ị break; case 5: lseek(f, -(long)sizeof(H), SEEK_END); if (read(f, &H, sizeof(H))) Xuat (H); break; } } while (k != 6) ; re tùm 0 ; } Chú ý là các hàm truy xuất cấp Ì không hề biết là nó đang quản lý các bản ghi kiểu học sinh. Chúng chỉ biết làm việc với các byte. Vì vậy, khi ghi hoặc khi đọc một bản ghi nghĩa là ghi hoặc đọc một số byte bằng cỡ lớn của bản ghi. Tương tự, khi di chuyển từ bản ghi này sang bản ghi khác cũng phải nhảy qua từng ấy byte. HI. CÁC HÀM TRUY XUẤT TỆP CẤP 2 1. Đặc điểm của các hàm truy xuất tệp cấp 2 • Có các lệnh đọc và ghi một số loại dữ liệu chuẩn • Có các lệnh đọc và ghi các khối dữ liệu • Trao đổi dữ liệu giữa chương trình và tệp thông qua vùng đệm của hệ thống. • Chương trình tác động lên tệp thông qua một con trỏ kiểu FILE. Kiểu này được định nghĩa trong tệp STDIO.H. Các hàm truy xuất tệp cấp 2 được xây dựng trên cơ sở các hàm truy xuất tệp cấp Ì, Các hàm truy xuất tệp cấp Ì được xây dựng trên cơ sở các lời gọi hệ thống. Do đó các hàm truy xuất tệp cấp 2 gần với người lập trình hơn, che giấu được những rắc rối liên quan đến phần cứng và do đó dễ sử dụng hơn. 230
  81. 2. Mở tệp Việc mở tệp được thực hiện bằng hàm fopen với cú pháp sau: FILE * = fopen( , ); Trong đó: Hàm fopen trả về con trỏ tệp nếu thành công, NƯLL nếu thất bại. 3. Đóng tệp Dùng lệnh íclose với cú pháp sau: int fclose( ); Trả về 0 nếu không có lỗi, - Ì nếu có lỗi. 4. Đọc La. con trò kiêu HLE. Cỉu tróc F1LE bạn ỉè tự tìm hiếu sau. Xâu kí tự. Xêu trong dườnz cán có càu chéo ('-ỏ chì phái Mét thành 2 Câu chéo ("vò: Xâu kỷ tự bío 2ổm các ký hiệu'V". "a", '+ ". 'ỉ", "r" Mỗi ký liêu rrans ý nshĩa nem "r" tệpchi cọc Tạo tệp mói dè chi shi. Nếu tập đà tồn tại thì nói đuna của nó se bị .xoa. Tệp mờ đe shi tiếp ^ào cuối tập. Tạo tệp mới nếu chua "a" tổn tại "r+" Mờ lập đà có đè cập nhật (đọc hoặc chi) Tạo tệp mói đè cập nhặt (cọc hoác chi). NẾU tệp đà có thì bị chi đè. Mờ đe ahi tiếp lào cuối ẹp. Nêu tệp chưa có thì tạo lập mới. V Mờ tệp ở nxxte nhi phán (khem có chuy ên đổi) Mờ tệp ờ mode văn bản (chmtn đổi cập ký tự CR-LF "t" thành CR khi ghi và nsược lại khi đọc). 231
  82. Dùng hàm fread với cú pháp sau: size_t , size_t , size_t ); Hàm ữead cho phép đọc nhiều khối dữ liệu với kích thước các khối cho trước. ấn định kích thước mỗi khối tính bằng byte; là con trỏ kiểu FILE, dĩ nhiên cho biết dữ liệu đọc từ đâu. = fwrite(void * , size_t , size_t , FILE * ); Hàm fwrite cho phép ghi nhiều khối dữ liệu với kích thước các khối cho trước. ấn định kích thước mỗi khối tính bằng byte; cho lệnh fwrite biết nó phải ghi bao nhiêu khối; là con trỏ kiêu FILE cho biết dữ liệu ghi vào đâu. là kết quả ghi. Bình thường bằng . Nếu đang ghi mà đầy đĩa thì nhỏ hơn . size_t là một kiểu dữ liệu định nghĩa trong một số tệp chèn khác nhau. Nó chính là kiểu unsigned. 6. Di chuyển con trỏ định vị Dùng hàm fseek với cú pháp sau: fseek(FILE * , long , long ); Cách dùng giống như hàm lseek ở cấp Ì, ngoại trừ việc dùng con trỏ tệp thay cho số hiệu tệp. 232
  83. 7. Xác định vị trí hiện tại của con trỏ định vị Dùng hàm ftell với cú pháp sau: long ftell(FILE * ); Cách dùng giống như hàm tell ở cấp Ì, ngoại trừ việc dùng con trỏ tệp thay cho số hiệu tệp. 8. Thử lỗi Dùng hàm ferror với cú pháp sau: int ferror(FILE * ); Giá trị trả về 0 nếu không có lỗi, khác 0 nếu có lỗi. 9. Thử hết tệp Muốn biết đã hết tệp chưa ta đùng hàm feof. Cách dùng: int feof(FILE * ); Kết quả trả về 0 nếu chưa hết tệp, khác 0 nếu đã đến cuối tệp, không thể đọc thêm được nữa. 10. Đổi tên tệp Dùng lệnh rename như sau: rename(const char * , const char * ); Ví dụ: rename("fileOl.C", "file02.C"); //đổi tên tép FILE01.C thành FILE02.C 11. Xóa tệp Dùng lệnh unlink như sau: unlink(const char * ); 12. Liên hệ giữa hai cấp Có thể tiếp tục sử dụng các hàm cấp Ì với các tệp đã mở bằng các hàm cấp 2, nhưng trước hết phải lấy được số hiệu của tệp đã mờ được trỏ tới. Để làm được việc đó, ta dùng hàm íileno: 233
  84. int fileno( ); Với số hiệu thu được. ta có thế tác độnglên tệp bằng các hàm cấp Ì. 13. Trỏ lại đầu tệp Đế quay trờ lại điếm đầu của tệp. có thế sử dụng hàm fseek. hoặc dùng lệnh revvind, cú pháp như sau: rewind( ); 14. Làm sạch vùng đệm Các thao tác truy nhập tệp cấp 2 cần có cho mỗi tệp một vùng đệm. Khi cần làm sạch vùng đệm (ép ghi nốt vào tệp đối với tệp mở để ghi) hoặc xoa sạch vùng đệm (đối với các tệp mở để đọc), ta dùng lệnh fflush quen thuộc: fflush ( ); Một số tệp được hệ thống mờ sẵn có các con trỏ sau trò tới. ta chì việc sử dụng: Tên con trỏ Tệp Ị stdin Bàn phím (thiết bị vào chuẩn) stdout Màn hình (thiết bị ra chuẩn) stderr Thiết bị báo lỗi chuẩn stdaux Thiết bị phụ (cổng COM1) stdprt Máy in chuẩn Hình 55. Các con trỏ tệp chuẩn Để minh hoa cho bài này, chí cần sửa lại chương trình ví dụ VD73.C thành VD84.C trong đó chi có các hàm sau đây là có sự thay đổi: int MoTep(char *FName) { FILE *f = fopen(FName, "rb"); if (!f) f = fopen(FName, "wb"); 234
  85. else Load(FName, D); NhapDS()/ Save(FName, D, n); InDS(D); re tùm f; } int Save(char *FName, HocSinh *Ds, int n) { int k; FILE *f = fopen(FName, "wb"); if (f > 0) k = fwrite(Ds, sizeof(HocSinh) , n, f) ; í do se ( f) ; re tùm k; } int Load(char *FName, HocSinh *Ds) { FILE *f = fopen (FName, "rb"); n = fread(Ds + + , sizeof (HocSinh) , NMax, ĩ.) ỉ fclose(f); return n; int main(void) { int n; int k = 0/ FILE * f; 235
  86. do { k = Menu (k) ; HocSinh H; switch (k){ case 0: int m = Load("DanhSach.dát", D) ; InDS (D, m)ỉ break; case 1: f = MoTepCDanhSach.dat"); break; case 2: if (ftell(f) > sizeof(HocSinh)) { fseek (f,-2L*sizeof(HocSinh) , SEEK_CUR) ; if (fread(&H, sizeof(HocSinh), Ì, f)) Xuat (H); } break; case 3: int k = fileno(f); if (ftell(f) < filelength(k)) { if (fread(&H, sizeof(HocSinh), Ì, f)) Xuat CH); } break; case 4: fseek(f, 0L, SEEK_SET); if (fread(&H, sizeof(HocSinh), Ì, f)) Xuat(H); break; case 5: fseek(f, -(long)sizeof(H), SEEK_END); if (fread(&H, sizeof(H), Ì, f)) Xuat (H) ; 236
  87. break } } while (k != fclose(f); re tùm 0 ; }
  88. PHỤ LỤC ì. GỠ RỐI CHƯƠNG TRÌNH 1. Chuẩn bị Thao tác chuẩn bị là mở một cửa sổ Watch, một cửa sổ Output bên cạnh cứa sổ soạn thảo chương trình. Ta lấy ví dụ cụ thế. Đầu tiên gõ chương trìn h VD85.C. Dùng lệnh CTRL-H để thu nhỏ cửa sổ đó lại. Sau đó mở cửa sổ Watch, cửa sổ Output và bố trí như trong hình 56. Đóng hết tất cá các cửa sổ khôns liên quan. . • X h Run Ccrp-"c Dsbug Prciect Cpt"cns .'•ná: -e"p VD85.C tỉnclude tinclude raainO í ịnt a ạ 1,5; Output float b = 5; double c; 1.2000 int í = Ó; ; 858993459 1.2000 858995459 1.2000 c ạ a/b + 1; 858993459 prịnttQnc • XL0.4]f", c); 3.0000 pnnư("\nc ả XLOld", c); 0 3.0000 getchO; 0 return ở; 3.0000 0 RI Help F7 Trace F8 step F9 Hake no Menu Hình 56. Bô trí các cửa số trước khi gỡ rỏi 238
  89. 1.1. Mỏ của sổ watcb Cứa sổ Watch dùng đế theo dõi các biến của chương trình. Mớ cửa sổ này bằng lệnh Window\Watch. Thêm tên các biến cần theo dõi vào cửa sổ này bằng cách: Chọn cửa sổ Watch. ấn phím Insert Đế theo dõi giá trị của một biểu thức thì gõ biểu thức đó vào ô Watch expression. Việc theo dõi giá trị của các biến chi là trường hợp của một biếu thức đơn giản. Sau đó nhấn Enter. Thao tác này được lặp đi lặp lại cho từng biểu thức. Cũng có thể thực hiện việc thêm các biểu thức vào cửa sổ Watch bằng lệnh Debug\Watches, Ạdd watch Phương pháp này cũng dùng đế mớ cửa sổ Watch khi cửa sổ này chưa được mở. Nếu thấy không cần thiết một biêu thức nào đó trong cửa sổ Watch. có thế xoa đi bằng cách chọn biểu thức đó bằng chuột và ấn phím Delcte. 1.2. Mơ cửa sổ Output Cửa sổ Output là nơi xuất hiện kết quả của các lệnh ra như printf. Dùng cửa sổ này, ta có thể thấy ngay kết quả cùa chương trình trong khi nó đang chạy mà không cần dùng tổ hợp phím Alt- Điều quan trọng là đế sử dụng có ích các cứa sổ, cần sắp xếp chúng sao cho chúng không đè lên nhau và đủ rộng để nhìn thấy phần quan trọng nhất trong tất cá các cứa sỗ. 1.3. Chạy từng dòng Bây giờ ta sẽ chạy lại VD85.C, nhưng theo cách sau đây: Bạn hãy ấn phím li để bắt đầu. Bạn sẽ thấy xuất hiện một vạch sáng xanh trên dòng thứ 4, tức là dòng có khai báo main(). Điều đó có nghĩa là chương trình đã bắt đầu chạy và đang đứng chờ quyết định của chúng ta trên dòng đó. Bao giờ chương trình c cũng bắt đầu chạy từ dòng khai báo hàm main(). Lúc này, trong cứa sổ Watch, tất cả các biến đều bị phê là Undeíined symbol, vì cho đến dòng này chưa có khai báo các biến a, b, c, i. Chương trình sẽ tiếp tục chạy nếu ta lại ấn phím 12!. Nếu vạch sáng xanh dừng lại tại một lời gọi hàm thì hàm đó sẽ được thực hiện và vạch sáng chuyến 239
  90. sang dòng tiếp theo sau cú nhấn mu. Nếu muốn tìm hiểu chi tiết xem hàm được thực hiện như thế nào thì thay vì nhấn U|. bạn dùng phím Nếu ta ấn tiếp Si. ta thấy sẽ thấy vạch sáng nhảv qua dòng thứ 5 và chuyến sang dòng thứ 6 (tức là dòng khai báo và khởi tạo giá trị int a = 1.5). Điểu đó cho thấy dòng chỉ có dấu Ị không thế coi là một lệnh. Nếu một dòng chí có khai báo. ví du: int a; Cũng không phải là lệnh thực hiện, và khi chạy. vạch sáng sẽ nhảy qua. Lúc này, đã vào trong hàm main. Các biến a, b. c đều được nhận biết. nhưng mang các giá trị linh tinh. vì các lệnh gán chưa được thực hiện, và vì chúng là các biến tư đồng nên không đươc đát về 0. Nhấn tiếp , giá trị cùa a thav đổi (a : 1) Nhấn tiếp . giá trị của b thav đổi (b : 5.0) Nhấn tiếp , giá trị của i thay đổi (i : 0) Nhấn tiếp 3j, giá trị của c thay đổi (c : 1.2) Nhấn tiếp , vạch sáng nhảy vào trong vòng lặp do Nhấn tiếp . giá trị của i thay đổi (i : 1) Nhấn tiếp , thấy trong cửa sổ Output (c = Ì .2000) Nhấn tiếp , thấy trong cửa sổ Output (c = 858993459) Nhấn tiếp hai lần nữa thì vạch sáng chuyển về dòng lệnh i++ ở đầu vòng lập. Đây là vòng lặp vô tận, nếu chỉ dùng lệnh thì chắc ban khống đủ kiên trì để chạy đến cùng chương trình này. Trong khi đang dừng chương trình tại một dòng nào đó. bạn có thể cho dừng chương trình bằng lệnh Run\Program reset (UI KL-ĩyỊ ). Nếu muốn chạy nhanh qua một đoạn chương trình đến thẳng một dòng nào đó mới dừng lại thì chọn dòng đó và ấn ĨEI. 1.4. Thay đổi giá trị biên Giả sử ta đang dừng vạch sáng xanh trên dòng 17. Giá trị của a lúc này đang là 1. Chọn Debug. trong đó chọn Evaluate/modifv. Gõ vào ô Expresion tên biến a. Gõ Enter Ta thấy trong ô Result giá trị hiện tại là Ì. 240
  91. Chuyến sang ổ New value bằng chuột hoặc ấn Gõ vào đó giá trị mà ta mong muôn ví dụ là 10. Gõ Enter để ra. Giá trị của a đa được sửa thành giá trị mong đợi. Gõ [3t để chạy tiếp. Kết quả in ra màn hình thay đổi: c = 3.000 c= 0 Dòng in thứ hai luôn luôn sai. Lý do là do sự chuyển kiêu từ double sang long int làm mất mát dữ liệu. 2. Đặt và xóa điểm dừng Trong một chương trình lớn, nhiều đoạn đáng tin cậy không cần phải kiểm tra. nhưng một số đoạn đáng ngờ, cần dừng lại để dò từng dòng. Có thế đặt một số điếm dừng trong chươna trình, gọi là các breakpoint. Chương trình sẽ dừng lại tại các điểm đó trong điều kiện nhất định cho trước, hoặc vô điều kiện. 2.1. Đặt điếm dùng vô điều kiện Chọn dòng làm điểm dừng, thực hiện lệnh DebugỴToggle breakpoint hoác CTRL-I |. Dòng được chọn bị tô đó. Nếu một dòng đang là điếm dừng, thực hiện thao tác như trên sẽ là xoa điểm dừng. Dòng được chọn trở lại bình thường. 2.2. Đật điểu kiện cho điểm dừng Thực hiện lệnh Debug\Breakpoints Hộp thoại Breakpoints hiện ra, trong đó có một danh sách các điểm dừng hiện có. Danh sách bao gồm các cột Breakpoints List. Line#. Condition. Pass. Cột đầu tiên ghi tên tệp (vì chương trình lớn có thê là một dự án với nhiều tệp). Cột thứ hai ghi số thứ tự dòng của điếm dừng. Cột thứ ba là điều kiện dừng. Cột thứ tư là số lần điều kiện được thoa mãn trước khi dừng. Hai cột cuối phải được soạn thảo, nếu không có gì thì điều kiện là đúng. và số lần là Ì. 241
  92. ra ri í =1=1! J=i Ị -; ——- watcft ——— tĩnclude í 4' Uwtefi»eđsynbol "Ì* tinclude thì tham số thực phải là một biến cùng kiêu. Thông tin trao đổi được thực hiện bằng cách tham chiếu. Đây là điểm mới thêm vào từ C++. Cách dùng này nhằm hạn chế việc sử dụng con trỏ trons một ngôn ngữ có cấu trúc chắc chấn và an toàn hơn. sự kiểm soát kiểu chặt chẽ hơn. 242
  93. Dưới đây là chương trình VD86.CPP hoàn chỉnh có sử dụng hàm HCN đã viết ở trên: #include #include int HCN(int a, int b, int *DT, int *CV) { if (a < 0 li b < 0) return -1; * DT = a* b ; *cv = 2* (a + b) ; return 0; } main () { int A, B, s, C; printf("\nCho hai canh a và b của hình chữ nhát: "); scanf (" %d%d" , SA, &B) ; int k = HCN(A, B, &s, SO ỉ lí (!k) printí ("XnDiên tích s = %d\tchu vi c = %đ', s, C) ; else printf("\nCác cạnh không hợp lệ"); getch() / return 0; } Trong ví dụ trên, hàm main sau khi nhập giá trị hai cạnh của hình chữ nhật thì gọi hàm HCN. Lệnh: int k = HCN(A, B, &s, &C); gọi là lời gọi tới hàm HCN. Danh sách các tham số thực tế bao gồm các cạnh A, B và các địa chỉ của hai biến s và c. Giá trị trả về của hàm được lưu giữ trong biến k. Sau 243