Bài giảng Cơ sở lập trình (Phần 2) - Nguyễn Văn Huân

pdf 70 trang ngocly 1160
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng Cơ sở lập trình (Phần 2) - Nguyễn Văn Huân", để 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:

  • pdfbai_giang_co_so_lap_trinh_phan_2_nguyen_van_huan.pdf

Nội dung text: Bài giảng Cơ sở lập trình (Phần 2) - Nguyễn Văn Huân

  1. "e>dit an address", "d>isplay an address" }; static char tieude[40]="main menu\n\n"; thucdon(tieude,chon,3); getch(); return 0; } void thucdon(char *tieude,char *lua[],int kichthuoc) { int i; printf(tieude); for (i=0;i<kichthuoc;i++) printf("%s\n",*(lua +i)); } 2.5. Con trỏ 2.5.1. Khái niệm con trỏ Con trỏ là một biến chứa địa chỉ của một biến. Vì có nhiều loại địa chỉ nên cũng có nhiều kiểu con trỏ tương ứng. Con trỏ int chứa địa chỉ của các biến kiểu int, con trỏ kiểu float, kiểu double, Cũng tương tự như vậy. Trong C, con trỏ thường được sử dụng vì với con trỏ thì chương trình có thể được viết ngắn gọn hơn và hiệu quả hơn những cấu trúc khác. 2.5.2. Khai báo con trỏ Cũng như các biến khác, một con trỏ phải được khai báo trước khi sử dụng. Người lập trình phải chỉ ra kiểu của biến được trỏ tới. Mẫu khai báo kiểu_của_biến_được_trỏ *tên_biến_trỏ; 64
  2. ví dụ: int *p; tên biến trỏ chỉ rằng đây là biến trỏ kiểu biến được trỏ *p là dữ liệu được chứa trong địa chỉ là p. Nói cách khác p là vùng nhớ chứa địa chỉ của *p. Hay p là con trỏ trỏ tới biến chứa giá trị *p. Khi ta khai báo một biến con trỏ, máy sẽ cấp phát một vùng bộ nhớ là 2 byte hoặc 4 byte tuỳ theo kiểu của biến được trỏ. Ví dụ: khai báo int *p; thì máy sẽ cấp phát một vùng nhớ 2 byte liên tiếp cho biến trỏ p. Khai báo float x=90.8; - máy sẽ cấp phát một vùng nhớ là 4 byte liên tiếp cho biến x. Con trỏ và địa chỉ của biến Địa chỉ của biến là số thứ tự của byte đầu tiên được cấp phát trong dãy các byte cấp phát cho biến (các byte được đánh số từ 0) Như đã trình bày trên, con trỏ là một biến gồm một nhóm các ô nhớ (2 hoặc 4 byte) để lưu trữ các địa chỉ. Phép toán một ngôi & xác định địa chỉ của đối tượng mà con trỏ trỏ tới. Phép toán một ngôi * sử dụng với biến trỏ để xác định giá trị ở địa chỉ mà con trỏ trỏ tới. Ví dụ: giả sử ta khai báo các biến x, p và phép gán như sau: int x=12; // x là biến có giá trị bằng 15 int *p; // con trỏ p trỏ tới dữ liệu kiểu số nguyên p=&x; // p nhận giá trị địa chỉ của x. Đầu tiên biến trỏ p chứa có một giá trị xác định, sau khi có phép gán p=&x; thì biến p chứa địa chỉ của ô nhớ x, và tất nhiên nó cũng chứa địa chỉ của bản thân nó. Khi biến trỏ không chứa bất kì một địa chỉ nào thì giá trị của nó là null. Cách dùng con trỏ Ta có thể sử dụng tên con trỏ hoặc khai báo của nó trong các biểu thức. Không nhập giá trị cho con trỏ từ bàn phím. Quy cách in con trỏ và địa chỉ là: *p 65
  3. + Sử dụng tên con trỏ: con trỏ cũng là một biến nên có thể dùng trong biểu thức vì giá trị của nó sẽ được dùng trong biểu thức này (nhưng chú ý rằng giá trị của con trỏ là địa chỉ của biến nào đó). Khi tên con trỏ đứng ở vế trái của biểu thức gán thì vế phải (phải là địa chỉ của biến) được gán cho con trỏ. Ví dụ: float a, *p, *q; // khai báo a là một số thực, p và q là hai con trỏ p=&a; // địa chỉ của biến a được gán cho con trỏ p q=p; // giá trị của con trỏ p được gán cho con trỏ q Cũng như các biến khác, nội dung của con trỏ có thể được thay đổi và ta có thể sử dụng quy tắc này để biến đổi địa chỉ. Sử dụng dạng khai báo của con trỏ giả sử ta có các khai báo int x,y,z,*px,*py; px=&x; py=&y; Thì con trỏ px trỏ tới x (hay còn nói px chứa địa chỉ của x) và con trỏ py trỏ tới y (py chứa địa chỉ của y). Khi đó ta có các cách viết x và *px là như nhau, nghĩa là *px=x; *py=y *px, *py là giá trị mà con trỏ px, py trỏ tới ví dụ: kiểm định các giá trị của con trỏ #include #include void main() { clrscr(); int so; int *contro; so=10; printf(“\ndia chi cua so:%p “,&so); printf(“\ngia tri cua so:%d “,so); contro=&so; 66
  4. printf(“\ndia chi cua con tro:%p “,&contro); printf(“\ngia tri cua con tro:%p “,contro); printf(“\ng_tri duoc con tro tro toi:%d “,*contro); getch(); return; } Giả sử địa chỉ của biến so là fff4 và địa chỉ của contro là fff2, khi đó chương trình sẽ cho kết quả là: dia chi cua so: fff4 gia tri cua so: 10 dia chi cua con tro: fff2 gia tri cua con tro: fff4 gia tri duoc con tro tro toi: 10 2.5.3. Các phép toán trên con trỏ Phép toán một ngôi & xác định địa chỉ của đối tượng mà con trỏ trỏ tới. Phép toán một ngôi * sử dụng với biến trỏ để xác định giá trị ở địa chỉ mà con trỏ trỏ tới. Các phép toán số học: Phép cộng một con trỏ với một số nguyên được một con trỏ có cùng kiểu Phép trừ một con trỏ với một số nguyên được một con trỏ có cùng kiểu Phép trừ hai con trỏ có cùng kiểu sẽ được một số nguyên giả sử ta có khai báo: int *p, i=5; thế thì: - phép ++p là: tăng giá trị của p lên một đơn vị - phép p là: giảm giá trị của p một đơn vị Đơn vị tăng hay giảm của một con trỏ luôn luôn có kích thước của biến được trỏ vào. Nếu giá trị của biến được trỏ vào thuộc kiểu int thì một đơn vị tăng giảm là 2 byte. Nếu giá trị của biến được trỏ vào thuộc kiểu float thì một đơn vị tăng giảm là 4 byte . 67
  5. Các phép giữa các biến con trỏ: Phép gán: = Các phép so sánh: = (bằng nhau), != (khác nhau) Lưu ý: Phép toán & chỉ thực hiện cho các đối tượng trong bộ nhớ, nghĩa là chỉ thực hiện đối với các biến và các phần tử của mảng (mà thực chất là các biến có cùng tên), không được sử dụng được với các hằng, biểu thức và các biến thanh ghi. Nếu p là con trỏ trỏ tới một biến nguyên a thì p có thể xuất hiện trong các biểu thức, các câu lệnh giống như x. Ví dụ: *p=10+ ++*p; Con trỏ kiểu tổng quát: Con trỏ kiểu tổng quát là con trỏ có thể trỏ tới mọi kiểu con trỏ, nhưng ngược lại thì không được mà phải dùng phép ép kiểu. Con trỏ kiểu tổng quát là con trỏ khai báo kiểu void. Các phép tăng, giảm địa chỉ và phép so sánh không dùng với con trỏ tổng quát. ví dụ: void *tongquat; int *nguyen; char *kitu; tongquat = nguyen; //phộp gỏn hợp lệ *nguyen = *tongquat; //khụng hợp lệ kitu = (char)tongquat; //sử dụng đúng phép ép kiểu 2.5.4. Con trỏ và xâu ký tự Về thực chất xâu ký tự chính là một mảng có các phần tử là các ký tự, do vậy việc mối quan hệ giữa xâu ký tự và con trỏ chính là mối quan hệ giữa mảng một chiều với con trỏ, và điều này ta đã nghiên cứu ở phần trước. Tên xâu ký tự là một hằng địa chỉ biểu thị địa chỉ phần tử đầu của mảng chứa xâu ký tự đó. Khai báo char *tên; Ví dụ: 68
  6. char *thong_bao; Thì phép gán : thong_bao=”tam dung chuong trinh” Là hoàn toàn có nghĩa. Lúc này con trỏ thong_bao sẽ chứa địa chỉ đầu của mảng kiểu char đang chứa xâu ký tự “tam dung chuong trinh”. Khi đó câu lệnh puts(“tam dung chuong trinh”); hoàn toàn như lệnh puts(thong_bao) vì đều có tác dụng đưa ra màn hình dòng chữ: Tam dung chuong trinh Cần phân biệt giữa mảng kiểu char và con trỏ kiểu char Nếu ta có khai báo: char *tb,mang[15]; thì các câu lệnh sau là đúng tb=”chao cac ban”; (1) gets(mang); (2) còn các câu lệnh sau không thực hiện được mang=”chao cac ban”; (3) gets(tb); (4) Câu lệnh (3) không thực hiện được bởi vì tên mảng là một hằng địa chỉ, do vậy ta không thể gán cho nó một hằng địa chỉ khác. Câu lệnh (4) chưa thực hiện được bởi tb là con trỏ, nên trước khi sử dụng nó cần phải khởi tạo cho nó một giá trị nhất định, vì vậy muốn thực hiện dược câu lệnh này thì cần phải cho con trỏ tb trỏ tới một vùng nhớ xác định. Sau đây là một ví dụ về xử lý xâu ký tự với con trỏ. Ví dụ: Nhập và một xâu ký tự, đưa ra màn hình mã ASCII của các ký tự đã gõ vào. Công việc kết thúc khi nhập và xâu “ket thuc” #include #include #include void main(){ clrscr(); char *p; //con tro kieu ky tu char s[80]; do { 69
  7. p=s; gets(s); while (*p) printf("%10d",*p++); } while (strcmp(s,"ket thuc")); getch(); return; } 2.6. Liên hệ giữa con trỏ và mảng Con trỏ và mảng có mối quan hệ chặt chẽ vì vậy các con trỏ thường được sử dụng khi xử lý các mảng. 2.6.1. Con trỏ và mảng một chiều Phép toán lấy địa chỉ áp dụng được cho các phần tử của mảng một chiều Với khai báo: int x[50]; khi đó phép toán &x[i] cho địa chỉ của phần tử x[i] với i nằm trong khoảng từ 0 đến 49. Tên mảng là một hằng địa chỉ với khai báo trên về thực chất x là địa chỉ phần tử đầu tiên của mảng, nghĩa là x tương đương với &x[0] và x+i tương đương với &x[i], *(x+i) tương đương với x[i]. Tuy nhiên cần lưu ý rằng chỉ có tên mảng mới mang giá trị địa chỉ tức là con trỏ, còn khi viết tên mảng kèm theo dấu ngoặc vuông có chỉ số thì đó là một biến bình thường. Nghĩa là với khai báo trên thì x hoặc x[] là con trỏ, còn x[0], x[1], Lại là các giá trị biến. Nếu p là con trỏ trỏ tới một phần tử x[k] nào đó thì: p+i trỏ tới phần tử x[k+i] p-i trỏ tới phần tử x[k-i] *(p+i) tương đương với x[k+i] xét ví dụ sau: #include #include void main(){ clrscr(); int a[10],*pa,x; a[0]=11; a[1]=22;a[3]=33;a[4]=44; 70
  8. pa=&a[0]; x=*pa; pa++; x=*pa; x=*pa+1; x=*(pa+1); x=*++pa; x=*pa++; x=++*pa; getch(); return; } 2.6.2. Con trỏ và mảng hai chiều Như đã biết các phần tử của mảng hai chiều sau khi khai báo được sắp xếp trong bộ nhớ với các địa chỉ liên tiếp liền nhau theo thứ tự trên một hàng. ví dụ: Với khai báo : float a[2][3]; Thì các phần tử của mảng a được sắp xếp như sau phần tử a[0][0], a[0][1], a[0][2], a[1][0], a[1][1], a[1][2] địa chỉ 1 2 3 4 5 6 Ta cũng đã biết mảng hai chiều là mảng của mảng một chiều và tên biến mảng biểu thị địa chỉ của phần tử đầu tiên của mảng, nên với khai báo trên ta có a là một mảng mà mỗi phần tử của nó gồm 3 số thực (một hàng của bảng), trong đó: a trỏ tới phần tử đầu tiên của hàng đầu là a[0][0] a + 1 trỏ tới phần tử đầu tiên của hàng thứ hai là a[1][0]. Để lần lượt duyệt trên các phần tử của mảng hai chiều, ta vẫn có thể dùng con trỏ theo cách sau: float *p,a[2][3]; p= (float*)a; Khi đó : p trỏ tới a[0][0] p+1 trỏ tới a[0][1] 71
  9. p+2 trỏ tới a[0][2] p+3 trỏ tới a[1][0] p+4 trỏ tới a[1][1] p+5 trỏ tới a[1][2] Ví dụ: để nhập dữ liệu cho một mảng hai chiều với các phần tử của mảng thuộc kiểu float ta thực hiện như sau: #include #include void main(){ clrscr(); float a[2][3],*p; int i; p=(float*)a; //nhap mang for (i=0;i #include void main(){ 72
  10. clrscr(); float a[50][50]; int i,j,m,n; printf("so dong:"); scanf("%d",&m); printf("\nso cot:"); scanf("%d",&n); //nhap mang for (i=0;i #include void main(){ clrscr(); float a[50][50],x; 73
  11. int i,j,m,n; printf("so dong:"); scanf("%d",&m); printf("\nso cot:"); scanf("%d",&n); //nhap mang for (i=0;i<m;++i) for (j=0;j<n;j++){ printf("a[%d][%d]=",i,j); scanf("%f",&x); a[i][j]=x; } //in mang for (i=0;i<m;++i){ printf("\n"); for (j=0;j<n;j++) printf("%10.2f",a[i][j]); } getch(); return; } 2.6.5. Mảng các con trỏ Khái niệm: Mảng con trỏ là một mảng mà mỗi phần tử của nó là một con trỏ (mỗi phần tử của nó có thể chứa một địa chỉ nào đó). Cũng giống như con trỏ, mảng con trỏ có nhiều kiểu. Khai báo: kiểu *tên [n]; kiểu: là kiểu đã được định nghĩa n: số phần tử của mảng. Khi gặp khai báo trên, máy sẽ cấp phát “số phần tử ” khoảng nhớ liên tiếp cho các phần tử của mảng. 74
  12. ví dụ: khai báo double * x [50]; số phần tử tên mảng để chỉ mảng con trỏ kiểu của phần tử được trỏ tới Cách dùng Khi sử dụng mảng con trỏ cần phải gán cho mỗi phần tử của mảng một giá trị, giá trị này phải là địa chỉ của một biến hoặc của một phần tử mảng. Các phần tử của mảng con trỏ kiểu char có thể khởi đầu bằng các xâu ký tự. Bản thân con trỏ không thể dùng để lưu trữ số liệu, nhưng mảng con trỏ cho phép sử dụng các mảng khác để lưu trữ số liệu một cách có hiệu quả hơn theo cách: chia mảng thành các thành phần và ghi nhớ địa chỉ đầu của mỗi phần vào một phần tử của mảng con trỏ. ví dụ: #include #include #include void tim_ng(int ma){ static char *ds[]={ "ma sai", "ten a", "ten binh", "ten minh", "ten xau", "tan ", "chung", "hoa", "dung", "khoa", "hfvjk" }; 75
  13. printf("\n\n ma:%d",ma); printf("\n%s\n",(ma 10)?ds[0]:ds[ma]); } void main(){ int i; tt:printf("can tim nguoi thu: "); scanf("%d",&i); tim_ng(i); printf("co tiep tuc khong? "); if (toupper(getch())=='c') goto tt; getch(); return; } 2.6.5. Cấp phát động con trỏ 2.6.5.1. Biến động Tất cả các biến có kiểu dữ liệu mà ta đã nghiên cứu như mảng, int float, Được gọi là tĩnh vì chúng được khai báo trước một cách rõ ràng lúc mô tả kiểu và khai báo biến, sau đó chúng được sử dụng thông qua tên của chúng. Thời gian tồn tại của biến cũng là thời gian tồn tại của khối chương trình chứa có chứa khai báo biến này. Ví dụ các biến tĩnh được mô tả và khai báo trong chương trình chính sẽ tồn tại trong suốt thời gian chương trình chạy, còn các biến tĩnh được mô tả và khai báo trong một hàm sẽ tồn tại mỗi khi hàm được gọi. Các biến động (dynamic) là các biến tạo ra trong lúc chạy chương trình tuỳ theo nhu cầu. số các biến động hoàn toàn không xác định được từ trước. Các biến động không có tên (vì việc đặt tên thực chất là gán cho nó một địa chỉ xác định). Việc truy nhập các biến động được thực hiện nhờ các biến con trỏ, các biến con trỏ được định nghĩa như là các biến tĩnh (nghĩa là có tên và được khai báo ngay từ đầu) được dùng để chứa địa chỉ các biến động. Các hàm xử lý với con trỏ Hàm malloc Cú pháp khái báo 76
  14. void *malloc(size_t size); Hàm malloc dùng để xin cấp phát một vùng bộ nhớ có kích thước size bytes từ vùng nhớ heap. Hàm này cho phép một chương trình xin cấp phát một vùng bộ nhớ đúng với kích thước mà chương trình cần. Giá trị trả về Trong trường hợp cấp phát thành công, hàm malloc trả về một con trỏ tới khối nhớ mới được cung cấp. Trong trường hợp có lỗi (không đủ bộ nhớ để cấp phát, malloc trả về giá trị null). Nếu tham số size=0, malloc trả về con trỏ null. Hãy xem đoạn code sau đây: #include #include #include #include main(){ char str; char *s; /* cap phat bo nho */ if ((str == (char ) malloc(10)) ==null) { printf(" khong du bo nho "); exit(1); } /* ket thuc chuong trinh cap phat bo nho /* sao chep o chao ban vao xau*/ strcpy(s,"chao ban"); /*hien xau*/ printf("xau la %s", s); /* giai phong bo nho*/ free(s); } Hàm calloc Cú pháp khai báo void *calloc(size*nitems, size_t size); 77
  15. Hàm calloc xin cấp phát một vùng nhớ kích thước (nitems*size) bytes và xóa trắng vùng nhớ này. muốn xin cấp phát vùng nhớ có kích thước lớn hơn 64k, phải sử dụng hàm farcalloc() (cấp phát xa). Giá trị trả về Trong trường hợp thành công, calloc trả về con trỏ tới vùng nhớ mới được cấp phát. Khi có lỗi cấp phát (không đủ bộ nhớ hoặc một trong hai tham số nitems hoặc size bằng 0) thì hàm trả về null. ví dụ #include #include #include int main(void) { char *str = null; /* dia chi chuoi trong bo nho */ str = (char *) calloc(10, sizeof(char)); /* copy "hello" vao chuoi str */ strcpy(str, "hello"); /* hien thi chuoi */ printf("string is %s\n", str); /* giai phong bo nho */ free(str); return 0; } Hàm farrealloc, realloc Cú pháp khai báo void *realloc(void *khốidl, size_t size); void far *farrealloc(void far *oldblock, unsigned long nbytes); Hàm farrealloc chỉnh lại kích thước của khối nhớ block thành nbytes, sao chép nội dung của vùng nhớ cũ vào vùng nhớ mới nếu vùng nhớ mới được cấp phát lại không cùng địa chỉ với vùng trước cấp phát. 78
  16. Hàm realloc điều chỉnh lại kích thước của khối nhớ block thành size, sao chép nội dung vùng nhớ cũ vào vùng mới nếu thấy cần thiết. Tham số Chức năng block trỏ đến một vùng nhớ đã được cấp phát trước đó bằng cách gọi hàm malloc, calloc, hoặc realloc. Nếu block bằng null, realloc làm việc giống như malloc. oldblock trỏ đến vùng nhớ đã được cấp phát từ trước bằng cách gọi hàm farmalloc, farcalloc, hoặc farrealloc nbytes. size kích thước mới của vùng nhớ được trỏ bởi block nbyte kích thước mới của vùng nhớ được trỏ bởi old_block Bộ nhớ heap và cơ chế tạo biến động Các biến động tạo ra được C xếp vào một vùng ô nhớ tự do theo kiểu xếp chồng được gọi là heap (bộ nhớ cấp phát động). Ngôn ngữ C quản lý bộ nhớ heap thông qua một con trỏ của heap gọi là heapptr. Con trỏ heap luôn luôn trỏ vào ô nhớ đầu tiên còn tự do của vùng nhớ tự do của heap. mỗi lần gọi hàm malloc() con trỏ lại được dịch chuyển về phía đỉnh của vùng nhớ tự do một số byte tương ứng với kích thước của biến động được tạo ra. Ngược lại mỗi khi giải phóng bộ nhớ biến động, bộ nhớ biến động được thu hồi. Tuy nhiên việc thu hồi và tạo ra không phải là quá trình liên tục và kế cận thì bản thân bộ nhớ heap cũng bị chia nát ra 79
  17. địa chỉ cao con trỏ của heap bộ nhớ heap còn tự do địa chỉ gốc của heap bộ nhớ heap đã được cấp phát cho biến động overlay buffer sseg : 0000 stack segment đã dùng sseg:ptr con trỏ của stack stack segment chưa dùng bộ nhớ chương địa chỉ thấp trình và các biến tĩnh Hình ảnh khái quát về việc sử dụng bộ nhớ heap. ví dụ: tìm số các nguyên tố với số lượng các số được nhập từ bàn phím. #include #include #include main(){ clrscr(); long *nguyento=null,*batdau=null,*mo=null,thu=0; int i=0,timthay=0,tong=0; printf("bao nhieu so ban can tim? "); scanf("%d",&tong); nguyento=(long*)malloc(tong*sizeof(long)); if (nguyento==null) 80
  18. { printf("\nkhong du bo nho!"); return 0; } *nguyento=2; *(nguyento+1)=3; *(nguyento+2)=5; mo=nguyento+3; thu=5; do { thu+=2; batdau=nguyento; timthay=0; for (i=0;i<mo-nguyento;i++) if (timthay=(thu % *batdau++)==0) break; if (timthay==0) *mo++=thu; }while (mo-nguyento<=tong); for (i=0;i<5* (tong/5);i+=5) printf("\n%12ld%12ld%12ld%12ld%12ld",*(nguyento +i), *(nguyento+i+1),*(nguyento+i+2),*(nguyento+i+3), *(nguyento+i+4)); printf("\n"); for (i=tong-(tong%5);i<tong;i++) printf("\n%12ld",(nguyento+i)); free(nguyento); getch(); return 0; } 81
  19. Ghi nhớ C là một danh sách các giá trị trong mảng. một mảng là một nhóm các phần tử có cùng kiểu giá trị được lưu trữ kế tiếp nhau trong bộ nhớ. Truy nhập đến các thành phần mảng bằng tên biến mảng và chỉ số. Chỉ số các phần tử tính từ 0. Chỉ số của phần tử mảng có thể là giá trị của một biến nguyên, một hằng nguyên hay một biểu thức cho giá trị nguyên. Một mảng ký tự có thể được sử dụng để lưu trữ xâu ký tự. xâu ký tự là một mảng ký tự đặc biệt có chứa ký tự ‘\0’ ở cuối xâu. Các thành phần của mảng có thể được khởi tạo theo 3 cách cùng với khai báo, bằng các câu lệnh gán, bằng câu câu lệnh nhập dữ liệu. C không tự động kiểm tra sự quá giới hạn kích thước của mảng. một xâu có thể đươc nhập bằng scanf() với mô tả %s hoặc bằng gets(), và có thể được hiển thị bởi hàm printf() với mô tả %s hoặc puts(). Con trỏ là biến chứa địa chỉ của các biến khác. Các con trỏ cần phải được khai báo trước khi sử dụng. Tương ứng với các kiểu dữ liệu khác nhau, có các loại biến con trỏ khác nhau. Có ba giá trị có thể sử dụng để khởi tạo cho một biến con trỏ: 0, null, hoặc một địa chỉ. Khởi tạo một con trỏ bằng 0 đồng nghĩa với khởi tạo null cho con trỏ. Chỉ có một số nguyên có thể gán được cho con trỏ đó là 0. Toán tử & trả về địa chỉ của toán hạng của nó. Toán hạng ở đây là một biến nào đó và không phải là biến kiểu thanh ghi (register). Toán tử * tham chiếu nội dung của một của đối tượng mà toán hạng của * trỏ tới trong bộ nhớ. Các thao tác số học có thể thực hiện trên con trỏ là ++ , ( trên các con trỏ ) , + , -, += , -= (giữa con trỏ và một số nguyên ). Khi một số nguyên được thêm vào hay trừ vào một con trỏ con trỏ được tăng hoặc giảm số nguyên đó lần kích thước của đối tượng được trỏ tới Các thao tác số học trên con trỏ chỉ được thực hiện trên một phần bộ nhớ liên tục chẳng hạn như một mảng. Khi thực hiện các thao tác số học con trỏ trên mảng ký tự kết quả thu được giống như đối với các thao tác số học thông thường bởi vì mỗi một ký tự được lưu trữ một byte trong bộ nhớ. Có thể gán giá trị của một con trỏ cho một con trỏ khác cùng kiểu. 82
  20. Một con trỏ void không áp dụng được phép toán *. Các con trỏ có thể được so sánh với nhau theo các toán tử so sánh. ý nghĩa của các phép so sánh giống như các phép so sánh thông thường. Một con trỏ có thể được đánh chỉ số như mảng. Tên mảng là một hằng con trỏ chỉ phần tử đầu tiên của mảng (thứ tự 0). Độ lệch con trỏ có cùng khái niệm như chỉ số đối với mảng. Có thể khai báo một mảng các con trỏ. Các lỗi hay gặp khi lập trình Truy nhập tới một phần tử nằm ngoài giới hạn của mảng. Truy nhập tới các phần tử của mảng nhiều chiều sai quy cách, chẳng hạn a[2,4] thay vì a[2][4]. Nhầm lẫn giữa “phần tử thứ bảy của mảng” và “phần tử mảng có số thứ tự 7”. Quên toán tử * trong khai báo các biến con trỏ. mỗi biến con trỏ cần phải được khai báo với một * như phần tiền tố. Tham chiếu tới một con trỏ chưa được khởi tạo giá trị. Điều này có thể gây ra lỗi thực hiện chương trình. Sử dụng các thao tác số học con trỏ trên một con trỏ không có liên hệ với một mảng cụ thể nào đó. Dùng toán tử * để lấy nội dung của một con trỏ void. Sử dụng tên mảng trong các phép toán nhằm thay đổi giá trị của tên đó. sai vì tên mảng là một hằng con trỏ. Các thói quen lập trình tốt cần học tập Khi duyệt qua các thành phần của một mảng, chỉ số không được bé hơn 0 và phải bé hơn kích thước của mảng. Thêm ptr ở phần đầu tên của các con trỏ để phân biệt với các biến thông thường khác. Sử dụng ký pháp mảng khi thao tác trên mảng thay vì ký pháp con trỏ. 2.7. Kiểu cấu trúc 2.7.1. Khái niệm và định nghĩa cấu trúc Trong thực tế lập trình, chúng ta có thể sẽ cần đến những kiểu dữ liệu phức tạp hơn được tạo thành từ những kiểu dữ liệu đơn giản mà chúng ta đã biết. những kiểu dữ liệu này cho ta một khả năng kết hợp một nhóm các biến cùng thể hiện một 83
  21. đối tượng chung. chẳng hạn để lưu giữ những thông tin liên quan đến một đối tượng nhân viên, ta có thể cần thiết một biến nào đó có khả năng lưu trữ được cả tên, địa chỉ, ngày sinh lẫn mã số nhân viên, mức lương .v.v để có thể xử lý biến này như một phần tử thống nhất, thể hiện thông tin của một nhân viên cụ thể, ngôn ngữ c cho phép chúng ta tự xây dựng những kiểu dữ liệu phức hợp và sử dụng những kiểu dữ liệu này để khai báo cho các biến sử dụng sau đó. chúng ta gọi những kiểu dữ liệu như vậy là các cấu trúc Cấu trúc (struct) là kiểu dữ liệu gồm nhiều mục dữ liệu khác nhau nhưng có liên quan với nhau, hay nói cách khác, để mô tả cùng một đối tượng bằng các phần tử dữ liệu có mô tả kiểu khác nhau chúng ta dùng cấu trúc kiểu struct. 2.7.2. Khai báo cấu trúc Để khai báo cấu trúc ta cần thực hiện 2 giai đoạn  Khai báo kiểu cấu trúc (định nghĩa kiểu)  Khai báo biến Mẫu 1: struct tênkiểu{ kiểu1 thành_phần_1; kiểu 1 thành_phần_2; } danh_sách_biến; Mẫu 2: struct{ kiểu1 thành_phần_1; kiểu 1 thành_phần_2; } danh_sách_biến; Trong đó: kiểu1, kiểu 2, là tên các kiểu dữ liệu đã được định nghĩa. Nếu kiểu lại là tên một kiểu cấu trúc đã được định nghĩa trước thì ta có các cấu trúc lồng nhau. thành_phần1, thành_phần2, là tên các thành phần của kiểu cấu trúc đang được định nghĩa. 84
  22. Ví dụ: thông tin về một sinh viên bao gồm họ tên, lớp, điểm môn1, điểm môn2, điểm trung bình. Như vậy thông tin về mỗi sinh viên bao gồm nhiều thành phần khác nhau, mỗi thành phần thuộc một kiểu dữ liệu khác nhau, ta gom các thông tin đó lại thành một cấu trúc với tên sinh_viên như sau: struct sinh_vien{ char ten[20]; char lop[10]; int diem1,diem2; float dtb; }sv[max]; Như vậy cứ hai sinh viên khác nhau thì có ít nhất 1 thuộc tính khác nhau, điều đó xác định tính duy nhất của mỗi sinh viên. Sự khác nhau giữa 2 mẫu khai báo: Với mẫu khai báo thứ nhất có tên kiểu cấu trúc, như vậy ngoài các biến có tên trong danh sách đã đưa ra, ta còn có thể khai báo thêm các biến khác cũng có cùng cấu trúc như vậy, mẫu khai báo các biến cấu trúc như sau: struct tên_kiểu danh_sách biến; Với mẫu khai báo thứ 2 thì chỉ có các biến có tên trong danh sách biến mới có cấu trúc như vậy mà thôi, muốn thêm các biến khác ta phải xây dựng một cấu trúc khác. Lưu ý: khi xây dựng các cấu trúc lồng nhau bắt buộc cấu trúc được lồng vào phải được định nghĩa trước. Ví dụ Thông tin về ngày bao gồm: ngày, tháng, năm cách 2 struct sinh_vien { char ten[20]; struct { int ngay,thang,nam; }ngay_sinh; int diem1,diem2; float dtb; Thông tin về một sinh viên bao gồm họ tên, ngay sinh, điểm 1, điểm 2, điểm trung bình. Khi đó ta xây dựng các cấu trúc như sau: 85
  23. cách 1 struct date { int ngay,thang,nam; }; struct sinh_vien { char ten[20]; struct date ngay_sinh; int diem1,diem2; float dtb; }sv[max]; 2.7.3. Truy cập các phần tử của cấu trúc Việc xử lý một cấu trúc bao giờ cũng phải thông qua các thành phần cơ bản của nó. Mẫu truy cập: Khi biến hoặc mảng là thành phần trực tiếp của cấu trúc: tên_cấu_trúc.tên_thành_phần Nếu biến hoặc mảng là thành phần trực tiếp của một cấu trúc mà bảnthân cấu trúc lại là thành phần của cấu trúc lớn hơn: tên_cấu_trúc.tên_cấu_trúc.tên_thành_phần Ví dụ: với khai báo struct dia_chi { int so_nha; char pho[20]; char thanh_pho[20]; }ong_a,ba_b; Ta có thể truy cập vào các phần tử của struct với các biến như sau: ong_a.so_nha=222; ong_a.pho=”hoang van thu”; ong_a.thanh_pho=”thai nguyen”; Hoặc dùng các hàm: printf(“hay cho biet ten pho: ”); gets(ong_a.pho); 86
  24. Trong những trường hợp này các thành phần ten_pho và thanh_pho là các mảng nên ta có thể truy cập vào từng phần tử của chúng. Ví dụ sau in ra từng ký tự của ten_pho for (i=0; i #include #include #define max 100 struct dia_chi{ char ten[20]; int so_nha; 87
  25. char pho[20]; char thanh_pho[15]; }addr[max]; void init_list(void); void enter(void); int menu_select(void); void xoa(void); void list(void); int find_free(void); void main(){ clrscr(); char choice; init_list(); for (;;){ choice=menu_select(); switch (choice){ case 1: enter();break; case 2: xoa();break; case 3: list();break; case 4: exit(0); } } } //khoi tao danh sach dia chi void init_list(void){ register int t; for (t=0;t<max;++t) addr[t].ten[0]='\0'; } //tao menu de chon menu_select(){ char s[80]; int c; printf("\n\n"); printf(" \n1. nhap ten"); 88
  26. printf(" \n2. xoa ten"); printf(" \n3. danh sach"); printf(" \n4. ket thuc"); do { printf("\nhay cho biet lua chon cua ban! "); gets(s); c=atoi(s); }while (c 4); return c; } // nhap dia chi vao danh sach void enter(void){ int slot,sn; char tiep; char s[80]; do { slot=find_free(); if (slot==-1){ printf("\nhet cho"); return; } printf("ten:"); gets(addr[slot].ten); printf("so nha: "); scanf("%d",&sn); addr[slot].so_nha=sn; while (getchar()!='\n'); printf("ten pho:"); gets(addr[slot].pho); printf("thanh pho:"); gets(addr[slot].thanh_pho); printf("\nban co tiep khong? "); tiep=getch(); }while (tiep=='c'); 89
  27. } find_free(void){ register int t; for (t=0;addr[t].ten[0] && t =0 && slot<max) addr[slot].ten[0]='\0'; } // hien thi void list(void){ register int t; for (t=0;t<max;++t) if (addr[t].ten[0]) { printf("%s\n",addr[t].ten); printf("%s\n",addr[t].so_nha); printf("%s\n",addr[t].pho); printf("%s\n",addr[t].thanh_pho); } printf("\n\n"); } 2.7.5. Con trỏ cấu trúc Ngôn ngữ C cũng cho phép sử dụng con trỏ trỏ tới struct cũng như các con trỏ trỏ tới các kiểu dữ liệu khác. Con trỏ cấu trúc dùng để lưu trữ địa chỉ của biến cấu trúc và mảng cấu trúc. 90
  28. ví dụ: Xét các kiểu cấu trúc ngay và nhan_cong trong các định nghĩa sau: struct ngay{ int ngay_thu; char thang[10]; int nam; }; struct nhan_cong{ char ten[20]; double luong; struct ngay ngay_sinh; struct ngay ngay_den; }; Khi đó con trỏ cấu trúc kiểu nhan_cong có thể được khai báo cùng với biến cấu trúc và mảng cấu trúc như sau: struct nhan_cong, *p, *p1, *p2, nc1, nc2, danhsach[100]; Trong đó: p, p1, p2 là các con trỏ cấu trúc; nc1, nc2 là các biến cấu trúc danhsach là mảng cấu trúc. Truy nhập đến các thành phần của cấu trúc thông qua con trỏ: cách 1: tên_con_trỏ tên_thành_phần cách 2: (*tên_con_trỏ).tên_thành_phần Toán tử thường được gọi là toán tử mũi tên Khi khai báo một con trỏ cấu trúc thì lúc đó bản thân con trỏ chưa có giá trị cụ thể, muốn sử dụng cần phải thực hiện phép gán giá trị cho nó. Trong khai báo trên ta có thể dùng p=danhsach; p1=&danhsach[2]; p2=&nc1; Khi đó ta có thể dùng *p1 thay cho danhsach[2], p2 thay cho nc1, và p thay cho danhsach[0]. 91
  29. Và đương nhiên ta cũng có thể thực hiện thao tác cộng địa chỉ: p=p+10; // con trỏ p trỏ tới danhsach[10] p1=p1-1; // con trỏ p1 trỏ tới danhsach[1] Như vậy nếu p là con trỏ trỏ tới đầu mảng danhsach (danhsach[100] là mảng cấu trúc) thì ta có: - Để truy nhập đến một thành phần của danhsach, ta có thể dùng: danhsach[i].thành_phần p[i].thành_phần (p+i) thành_phần - Khi sử dụng cả cấu trúc thì các cách viết sau là tương đương: danhsach[i], p[i], *(p+i); 2.8. Hàm và chương trình con 2.8.1. Giới thiệu Hầu hết chương trình máy tính giải quyết các vấn đề trong thực tế đều lớn hơn rất nhiều so với các ví dụ được giới thiệu trong cuốn sách này. Kinh nghiệm cho thấy rằng cách tốt nhất để phát triển và bảo trì các chương trình lớn là xây dựng chúng từ những mảnh nhỏ hơn hay các module, quản lý dễ hơn chương trình ban đầu. Kỹ thuật này được gọi là chia để trị. Chương này mô tả các đặc tính của ngôn ngữ lập trình c trong quá trình thiết kế, cài đặt, hoạt động và bảo trì các chương trình lớn. Trong khi lập trình chúng ta thường gặp những đoạn chương trình lặp đi lặp lại nhiều lần ớ những chỗ khác nhau. Để tránh rườm rà, những đoạn chương trình này được thay thế bằng các chương tình con tương ứng và khi cần chỉ việc làm thủ tục gọi chương trình con đó ra với các tham số cần thiết mà không phải viết lại các chương trình đó. Trong quá trình giải quyết các vấn đề, một vấn đề lớn phức tạp sẽ tương ứng với một chương trình có thể rất lớn, rất dài, do đó việc nhìn tổng thể cả chương trình cũng như gỡ rối, hiệu chỉnh rất khó khăn, việc phân tách một chương trình lớn thành các chương trình con để có cái nhìn tổng quan hơn, dễ kiểm tra, gỡ rối từng thành phần sau đó ghép lại thành chương trình lớn. việc chi nhỏ bài toán thành những modul giống như nguyên tắc chia để trị, chia để dễ điều khiển. 92
  30. 2.8.2. Các module chương trình trong C Các module trong C được gọi là hàm. Các chương trình C nói chung được viết bằng cách kết hợp các hàm mà lập trình viên tự viết với các hàm chuẩn trong thư viện của c. Chúng ta sẽ bàn cả 2 loại hàm trên trong chương này. Các hàm chuẩn trong thư viện của c cung cấp rất nhiều các hàm tính toán, thao tác xâu, thao tác ký tự, vào/ra và nhiều thao tác thông dụng khác. Chúng làm cho công việc của các lập trình viên dễ dàng hơn vì các hàm này đưa ra rất nhiều khả năng cho các lập trình viên. Mặc dù các hàm trong thư viện chuẩn thực ra không phải là một phần trong ngôn ngữ lập trình c, tuy nhiên theo chuẩn ansi, các hàm này không thay đổi trong các chương trình dịch c khác nhau. Các hàm printf hay scanf mà chúng ta dùng trong các chương trước đây chính là các hàm thư viện chuẩn. Lập trình viên có thể viết các hàm thao tác các công việc xác định dùng tại nhiều nơi trong chương trình. Các hàm loại này đôi khi còn được gọi là hàm người dùng định nghĩa. Các câu lệnh cài đặt cụ thể hàm này chỉ phải viết một lần, chúng được ẩn bên trong và có thể dùng lại nhiều lần. Các hàm được gọi thực hiện bằng các lời gọi hàm, các lời gọi hàm xác định tên của hàm và cung cấp các thông tin (hay còn gọi là các tham số) mà hàm được gọi theo đúng trình tự khi khai báo hàm đó. 2.8.3. Thư viện các hàm chuẩn Cũng như các ngôn ngữ khác, c có rất nhiều hàm được lập sẵn và chúng được phân loại thành các nhóm và để chúng vào các tệp tiêu đề có phần mở rộng là .h. Sau đây là tên các tệp tiêu đề và nội dung chủ yếu của nó: tên thư viện đối tượng ql tt quản lý vào ra quản lý ký tự quản lý lỗi giới hạn của số dấu phấy động giới hạn của số nguyên bản địa hoá 93
  31. các hàm toán học nhảy không điều kiện quản lý tín hiệu các định nghĩa chung chẩn đoán các tiện ích chung quản lý bộ nhớ và xâu ký tự ngày tháng và giờ Thư viện các hàm chuẩn được đặt trong thư mục include . muốn xem nguyên mẫu của một hàm ta mở tệp *.h chứa nó trong thư mục include. Thư viện các hàm toán học được khai báo trong tệp tiêu đề math.h cho phép người lập trình thực hiện các thoa tác toán học cơ bản. Bảng sau đây là một số hàm cơ bản: tên hàm mô tả sqrt(x) cho giá trị là căn bậc 2 của số x exp(x) giá trị của e mũ x (ex) log(x) logarithm tự nhiên của số x log10(x) logarithm thập phân của số x fabs(x) giá trị tuyệt đối của x ceit(x) làm tròn số nguyên nhỏ nhất không lớn hơn x floor(x) làm trong số nguyên lớn nhất không nhỏ hơn x pow(x,y) giá trị x mũ y (xy) fmod(x,y) dư của phép chia số thực x cho số thực y sin(x) sin của x tính theo radian cos(x) cosin của x tính theo radian 94
  32. tan(x) tang của x tính theo radian 2.8.4. Các hàm a. Giới thiệu Các hàm cho phép lập trình viên module hoá chương trình. nói chung, các biến định nghĩa trong hàm là các biến cục bộ, chúng chỉ được biết trong bản thân hàm mà chúng được định nghĩa. Hầu hết các hàm đều có các tham số, các tham số cung cấp giá trị thông tin truyền giữa các hàm. Các tham số của hàm nói chung cũng là các biến cục bộ. Thực hiện module hoá chương trình có nhiều ưu điểm. trước tiên, “chia để trị” giúp cho việc phát triển các ứng dụng dễ dàng hơn. Một ưu điểm nữa là tính sử dụng lại, nghĩa là khả năng dùng các hàm có sẵn như các khối cơ bản để xây dựng lên các chương trình mới. ưu điểm thứ ba là tránh được việc lặp đi lặp lại các đoạn mã trong chương trình. việc gói các đoạn mã vào trong hàm cho phép cả đoạn mã đó thực hiện ở nhiều nơi trong chương trình chỉ với một lời gọi hàm đơn giản. b. Cách định nghĩa hàm Trong C chỉ có một loại chương trình con duy nhất là hàm. Hàm nhận các thông số và trả lại kết quả cho tên hàm. Giá trị của hàm có thể không cần dùng đến như hàm printf() hay scanf(), giá trị trả lại của hai hàm này là số nguyên mà ta ít khi dùng đến. Hàm có thể không có giá trị nào, đó là hàm kiểu void. Hàm có thể cung cấp các giá trị không phải là vô hướng. Hàm có thể thay đổi các giá trị đối số của nó Mẫu khai báo hàm: kiểu_giá_trị_trả_lại tên_hàm(danh sách các đối) ví dụ: float f_vi_du(float x, int b, int c) danh sách các đối tên hàm kiểu giá trị trả lại Định nghĩa hàm kiểu_giá_trị_trả_lại tên_hàm(danh sách các đối){ 95
  33. } Khi định nghĩa hàm cần qua hai bước: Dòng tiêu đề: chứa các thông tin về kiểu giá trị trả lại của hàm, tên hàm, dang sách các đối (kiểu và tên của mỗi đối). Thân hàm: là nội dung chính của hàm được bắt đầu bởi dấu { và kết thúc bởi dấu }. trong thân hàm chứa các câu lẹnh cần thiết để thực hiện một yêu cầu nào đó đã đề ra cho hàm. trong thân hàm có thể sử dụng một hay nhiều câu lênh return ở những chỗ khác nhau. Dạng tổng quát của câu lệnh này là: return biểu_thức; Giá trị của biểu_thức sau câu lệnh return sẽ được gán cho tên hàm. nếu hàm có kiểu void thì sau return không có biểu_thức hoặc không có return. Lưu ý: khi khai báo hàm không có dấu chấm phẩy ở cuối hàm các đối được viết phân cách nhau bởi dấu phảy. Ví dụ #include #include int binhphuong(int); // khai báo nguyên mẫu void main(){ clrscr(); int i; for (i=0;i<100;i++) printf("%5d",binhphuong(i)); getch(); return; lời gọi hàm } int binhphuong(int x){ return x*x; } Hàm giúp cho người lập trình tìm lỗi cú pháp như kiểu trả về sai hoặc tham số truyền vào sai. 96
  34. Hàm nguyên mẫu: hàm nguyên mẫu thông báo cho chương trình dịch biết kiểu dữ liệu hàm trả lại, số lượng, kiểu và thứ tự của các tham số được truyền cho hàm. Chương trình biên dịch dùng hàm nguyên mẫu để kiểm tra các lời gọi hàm. Có thể khai báo nguyên mẫu trước hàm main() còn hàm thì được định nghĩa sau hàm main(). Ví dụ: Tính giai thừa của một số n nhập từ bàn phím. #include #include long int giai_thua(int n); void main(){ clrscr(); int n; printf("n= "); scanf("%d",&n); printf("\n n!=%ld",giai_thua(n)); getch(); return; } long int giai_thua(int n){ int i; long int gt=1; if (n>1) for (i=2;i #include long int giai_thua(int n){ int i; long int gt=1; 97
  35. if (n>1) for (i=2;i<=n;i++) gt*=i; return (gt); } void main(){ clrscr(); int n; printf("n= "); scanf("%d",&n); printf("\n n!=%ld",giai_thua(n)); getch(); return; } c. Sử dụng hàm, nguyên tắc hoạt động của hàm Sử dụng hàm: việc sử dụng một hàm thông qua lời gọi nó. Cách viết một lời gọi hàm như sau tên_hàm(danh sách các tham số thực) Số các tham số thực truyền cho hàm phải bằng số các đối trong nguyên mẫu hàm. Kiểu của các tham số thực sự phải phù hợp với kiểu của các tham số hình thức tương ứng. Nguyên tắc hoạt động của hàm: khi gặp lời gọi hàm thì hàm bắt đầu được thực hiện. nghĩa là khi chương trình đang chạy, máy gặp lời gọi hàm thì máy sẽ tạm rời khỏi chỗ đó để chuyển đến hàm tương ứng. Các thao tác thực hiện tuần tự theo các bước như sau: Cấp phát bộ nhớ cho các đối và các biến cục bộ Gán giá trị của các tham số thực sự cho các đối tương ứng. Thực hiện các câu lệnh trong thân hàm Khi gặp dấu return hay dấu } cuối cùng của thân hàm thì máy sẽ xoá các đối, các biến cục bộ và thoát khỏi hàm 98
  36. 2.8.5. Các cơ chế truyền tham số Việc truyền tham số trong c được thực hiện theo một kiểu duy nhất đó là truyền giá trị, nghĩa là giá trị của tham số thực sự trước và sau khi gọi hàm là không thay đổi. Kiểu truyền tham số này sẽ hạn chế sự thay đổi một hay vài giá trị. ví dụ: chương trình hoán vị hai số #include #include void main(){ clrscr(); void hoan_vi(int a,int b); int n=10,p=20; printf("truoc khi goi ham: n=%d p=%d ",n,p); hoan_vi(n,p); printf("\nsau khi goi ham: n=%d p=%d ",n,p); getch(); return; } void hoan_vi(int a,int b){ int t; printf("\ntruoc khi hoan vi: n=%d p=%d",a,b); t=a; a=b; b=t; printf("\nsau khi hoan vi: n=%d p=%d",a,b); return; } Việc thay đổi giá trị của nhiều tham số thông qua các hàm có thể được thực hiện thông qua việc truyền giá trị địa chỉ của tham số đó cho hàm (địa chỉ của tham số chứ không phải giá trị tham số). Để thực hiện được như vậy, ta cần phải sử dụng con trỏ để truyền địa chỉ, khi đó giá trị địa chỉ của biến vẫn không thay đổi song nội dung chứ trong đó sẽ được thay đổi. Ví dụ: quay trở lại ví dụ trên 99
  37. #include #include void main(){ clrscr(); void hoan_vi(int *a,int *b); int n=10,p=20; printf("truoc khi goi ham: n=%d p=%d ",n,p); hoan_vi(&n,&p); printf("\nsau khi goi ham: n=%d p=%d ",n,p); getch(); return; } void hoan_vi(int *a,int *b){ int t; printf("\ntruoc khi hoan vi: n=%d p=%d",*a,*b); t=*a; *a=*b; *b=t; printf("\nsau khi hoan vi: n=%d p=%d",*a,*b); return; } Một phương án khác để thay đổi giá trị là dùng biến toàn cục. Tóm lại: vì truyền tham số theo giá trị nên có thể thay đổi tham số thực sự là một biểu thức. nếu ta dùng cách truyền tham số theo địa chỉ thì chỉ lấp vào đó tham số thực sự là một biến chứ không phải là một biểu thức. 2.8.6. Cấp lưu trữ và phạm vi hoạt động của các đối tượng a. Biến toàn cục + Biến toàn cục là những biến được sử dụng ở mọi nơi trong chương trình + Biến toàn cục được khai báo bên ngoài các hàm, kể cả hàm chính main(). + Tầm tác dụng của biến là phạm vi hoạt động của nó. vậy tầm tác dụng của biến toàn cục là chương trình nguồn theo sau khai báo biến đó. 100
  38. + Cấp phát bộ nhớ cho các biến toàn cục: các biến toàn cục tồn tại trong suốt quá trình chứa nó hoạt động. việc cấp phát bộ nhớ cho các biến cũng được xác định rõ ràng ngay sau khi kết nối với các modun. việc cấp phát bộ nhớ ngay sau khi kết nối được gọi là lớp cấp phát bộ nhớ tĩnh, các biến này được gọi là biến tĩnh và cấp phát tĩnh. Hơn nữa các biến này được khởi tạo bằng 0 khi khởi đầu chương trình trừ trường hợp được khởi tạo giá trị ban đầu. b. Biến địa phương + Biến địa phương là biến chỉ có tác dụng trong thời gian hàm hoạt động. sau khi hàm kết thúc thì những biến khai báo bên trong hàm đó cũng như các tham số của nó cũng được giải phóng luôn. + Biến địa phương được khai báo trong thân hàm. + Tầm tác dụng của biến địa phương chỉ hạn chế trong hàm mà nó được khai báo. Các biến địa phương cũng không có mối liên hệ nào với biến toàn cục có cùng tên và cùng kiểu. Các biên cùng tên được sử dụng theo phương châm “phép vua thua lệ làng”. + Việc cấp phát bộ nhớ cho các biến địa phương của hàm được gọi là cấp phát tự động, cần đến đâu cấp phát đến đó, mỗi lần gọi hàm là một lần cấp phát. Giá trị của các biến địa phương không được lưu trữ lại cho các lần gọi sau. + Biến địa phương tĩnh static: để các biến địa phương được cấp phát bộ nhớ ở vị trí cố định, tĩnh tại, luôn luôn được dùng cho hàm ta có thể khai báo các biến đó với từ khoá static. Các giá trị của biến static có thể tồn tại ngay cả khi hàm đã kết thúc hoạt động. Biến địa phương thanh ghi register (kiểu bộ nhớ thanh ghi) Ta đã biết các bộ vi xử lý cpu đều có một số thanh ghi, các thanh ghi này do nằm trong cpu nên tốc độ truy nhập rất nhanh. nó không có địa chỉ riêng biệt như các ô nhớ khác. vì vậy nếu có những biến int nào đó sử dụng rất nhiều thì có thể khai báo kiểu bố trí ô nhớ là register. tuy nhiên điều này là hạn chế vì số thanh ghi không có nhiều, vì vậy các biến thanh ghi thường là các biến đếm trong các vòng lặp nào đó. 2.8.7. Con trỏ hàm Các con trỏ trỏ tới hàm cho phép gọi một hàm bằng giá trị thay cho bằng tên của hàm. Nó rất thuận tiện khi thực hiện các bảng quyết định khai báo con trỏ hàm: kiểu_giá_trị_trả_lại (*tên_con_trỏ)(d/s_các_biến_và_kiểu_giá_trị); 101
  39. Con trỏ hàm dùng để chứa địa chỉ của hàm. muốn vậy ta thực hiện phép gán tên hàm cho con trỏ hàm, đương nhiên để phép gán có thể thực hiện được thì kiểu của hàm và kiểu của con trỏ hàm phải tương thích và sau phép gán có thể dùng tên con trỏ thay cho tên hàm. ví dụ: #include #include void main(){ int i; void f1(void), f2(void),f3(void); void f4(int); void (*g)(int); clrscr(); static struct { int nb; void (*f)(); } tab[]={ {1,f1}, {2,f2}, {3,f3}, }; g=f4; (*g)(3); printf("chon"); scanf("%d",&i); (*tab[i-1].f)(); return; } void f1(void){ puts("ham f1"); getch(); 102
  40. } void f2(void){ puts("ham f2"); getch(); } void f3(void){ puts("ham f3"); getch(); } void f4(int j){ printf("ham f4: %d\n",j); getch(); } 2.8.8. Hàm đệ qui Một hàm có thể có lời gọi đến chính nó được gọi là hàm đệ quy Ví dụ: chương trình tính giai thừa của một số sử dụng hàm giai_thua(int): #include #include long int giai_thua(int n){ if(n==0) return(1); else return(n*giai_thua(n-1)) } void main(){ int n; printf(“n= ”); scanf(“%d”,&n); printf(“%d != %ld”,n,giai_thua(n)); getch(); return; } 103
  41. Ví dụ 2: tìm giá trị lớn nhất của mảng dùng giải thuật đệ quy #include #include #include #include int a[100],n; int max1(int a,int b); int max(int n); main(){ clrscr(); int i,*p; printf("\nnhap n= "); scanf("%d",&n); p=a; p=(int *)malloc(n*sizeof(int)); if (a!=null) for (i=0;i b)?a:b); 104
  42. } int max(int n){ //int mx; if (n==0) return a[0]; else return max1(a[n],max(n-1)); //return mx; } Các thói quen tốt khi lập trình Cần hiểu rõ các hàm trong thư viện ansi chuẩn đặt các dòng trắng, các lời chú thích ở những nơi thích hợp làm cho chương trình dễ đọc hơn. mặc dù có thể bỏ qua kiểu trả về của hàm có kiểu trả về là int nhưng vẫn nên viết tường minh ra, tuy nhiên riêng đối vời hàm main thì không cần thiết ghi rõ các kiểu của tham số, kể cả trong trường hợp là kiểu ngầm định, kiểu int. Cên dùng các danh biểu khác tên nhau, tránh nhầm lẫn không đáng có. Chọn tên hàm, tên các tham số gọi nhớ, làm cho chương trình dễ đọc hơn, tránh phải viết nhiều chú thích không cần thiết. viết các hàm nguyên mẫu để sử dụng khả năng kiểm tra kiểu chặt chẽ của ansi c các biến chỉ dùng trong hàm nên đặt là biến cục bộ thì tốt hơn là đặt biến toàn cục. Tránh viết lại các chức năng đã có, nếu có thể sử dụng các hàm trong thư viện chuẩn của ansi c thay cho giải pháp viết mới. Mỗi hàm chỉ thực hiện một công việc xác định, tên của hàm cần diễn đạt công việc này. Khi một hàm thực hiện nhiều công việc, chia nhỏ nó thành các hàm nhỏ hơn, các hàm không nên dài quá l trang. Chương trình nên viết thành nhiều hàm nhỏ. Điều đó làm cho chương trình dễ viết, kiểm tra, gỡ rối, bảo trì và thay đổi, ngoài ra nó nâng cao khả năng dùng lại của các hàm con. Hàm nguyên mẫu, định nghĩa hàm và các lời gọi hàm cần phải có cùng số lượng, kiểu và trình tự các tham số, phải cùng kiểu trả về. Các hàm nguyên mẫu cần đặt trước định nghĩa và các lời gọi hàm. 2.9. Các thao tác trên file văn bản File văn bản là file mà các phần tử của nó là các kí tự và văn bản được tổ chức thành từng dòng với độ dài của mỗi dòng khác nhau nhờ có các dấu hết dòng (end of line) hay dấu chấm xuống dòng. Đó là cặp kí tự điều khiển cr( carriage return với mã số là 13 ‘\r’) và lf (line feed với mã số là 10 ‘\n’) hoặc đôi lúc chỉ 105
  43. dùng ‘\n’ là đủ để cách hai dòng. Do file được tổ chức thành từng dòng nên việc ghi, đọc file văn bản cũng có thêm thủ tục ghi và đọc theo dòng là gets() và puts(). 2.9.1. Mở file văn bản mới để cất dữ liệu Các hàm ghi vào file văn bản: putc(kí_tự, file_văn_bản): ghi kí tự vào file văn bản fputs(kí_tự, file_văn_bản): ghi xâu kí tự vào file văn bản fprintf(file_văn_bản, “dãy mã quy cách”, dãy biểu thức): giống hàm printf() nhưng thông tin được đưa vào file văn bản. mở file văn bản để cất dữ liệu: fopen(tên_file,”w”); Ví dụ: ghi một dòng chữ hoa vào file: #include #include #include main(){ clrscr(); file *f; char c; f=fopen("c:\\thkt\\ng_nguc\\tep\\van_ban.txt","w"); do putc(toupper(c=getchar()),f); while (c!='\n'); fclose(f); getch(); return 0; } 2.9.2. Đọc một file văn bản đã có Các hàm đọc các phần tử từ file văn bản: getc(file): đọc một kí tự từ file văn bản. fgets(xâu, file): đọc một xâu kí tự từ file văn bản. fscanf() : tương tự lệnh scanf() Mở file văn bản để đọc dữ liệu: 106
  44. fopen(tên_file,”r”); Ví dụ: mở file văn bản đã có trên đĩa và đọc nội dung của file #include #include main(){ clrscr(); file *f; char c; if ((f=fopen("c:\\thkt\\ng_nguc\\tep\\van_ban.txt","r"))==null ) printf("\n co loi khong mo tep duoc! "); else do putchar(c=getc(f)); while (c!='\n'); fclose(f); getch(); return 0; } Ví dụ: đếm số kí tụ của file #include #include #include int main(){ int ch; char tentep[40]; file *fp; long dem=0; printf("ten tep: "); gets(tentep); fp=fopen(tentep,"r"); while ((ch=getc(fp)) !=eof) { 107
  45. putc(ch,stdout); dem++; } fclose(fp); printf("tep %s co %ld ki tu \n",tentep,dem); getch(); return 0; } Ví dụ: mở một file văn bản đã có trên đĩa và cho biết số dòng của nó. #include #include #include #include int main(){ char tentep[10]; int i=0; char d[256]; file *f; printf("\ncho biet ten tep: "); gets(tentep); f=fopen(tentep,"r"); if (f==null) {printf("khong mo duoc!"); exit(1); } while (!feof(f)){ ++i; fgets(d,256,f); printf("\n dong %d: %s",i,d); } fclose(f); 108
  46. getch(); return 0; } 2.9.3. Các thao tác vào/ra file mức thấp Truy nhập ở mức thấp là truy nhập không qua vùng nhớ đệm nên sẽ nhanh hơn, hiệu quả hơn nhưng cũng phức tạp hơn. Khi đó việc kiểm tra lỗi, chuyển dữ liệu là nhiệm vụ của người lập trình chứ không phải là công việc của các hàm có sẵn trong thư viện chuẩn. Các hàm truy nhập file mức thấp chức năng hàm mức thấp mở file open() đọc file read() ghi file write() đóng file close() truy nhập trực tiếp lseek() tìm vị trí con trỏ tell() Một số hàm vào ra mức thấp thông dụng Hàm int open(char *tên_file, int kiểu ); trong đó: tên_file là một tên hợp lệ của một file có thể bao gồm cả tên ổ đĩa, đường dẫn truy nhập. Kiểu là một trong các giá trị sau dùng để thiết lập chế độ mở file: - o_ronly (giá trị 0): mở file chỉ để đọc o_wronly (giá trị 1): mở file để chỉ ghi o_rdwr (giá trị 2) : mở file vừa đọc vừa ghi o_creat : mở file mới o_text: mở file kiểu văn bản o_binary: mở file kiểu nhị phân o_append : mở file để bổ sung o_excl : mở file ngoại lệ (chỉ được sử dụng với o_creat) 109
  47. o_trunc : xoa nội dung file Hàm close(int thẻ_file) Hàm dùng để đóng file, thành công hàm trả lại giá trị 0, ngược lại cho giá trị –1. Hàm int write(int thẻ_file, void *buf, unsigned độ_dài) Trong đó: thẻ_file là kết quả nhận được từ thao tác open(), hoặc creat() đã thực hiện độ_dài là số byte mà hàm muốn ghi buf là con trỏ trỏ đến vùng đệm mà hàm muốn ghi Hàm dùng để ghi dữ liệu ra file được xác định bởi thẻ file. Nếu thành công hàm trả về số byte thực sự được ghi, ngược lại hàm trả về giá trị –1. Nếu sso byte thực sự được ghi nhỏ hơn độ_dài thì việc ghi đã xảy ra lỗi. Hàm int read(int thẻ_file, void *buf, unsigned độ_dài) Trong đó thẻ_file là kết quả nhận được từ thao tác open(), hoặc creat() đã thực hiện độ_dài là số byte mà hàm thử đọc vào bộ đệm buf buf là con trỏ trỏ đến vùng đệm đọc dữ liệu vào Hàm dùng để đọc dữ liệu từ file được xác định bởi thẻ_file. Nếu thành công, hàm trả về số byte thực sự đã đặt vào buf, khi truy nhập đến cuối file, hàm trả về giá trị 0. Ngược lại hàm trả lịa giá trị –1. hàm long lseek(int thẻ_file, int số_byte, int bắt_đầu) Trong đó thẻ_file là kết quả nhận được từ thao tác open(), hoặc creat() đã thực hiện số_byte là khoảng cách tính theo byte kể từ vị trí bắt đầu đến vị trí mới bắt_đầu là một trong các hằng giá trị sau: seek_set (giá trị 0): từ đầu file seek_cur (giá trị 1): từ vị trí hiện thời seek_end (giá trị 2): từ cuối file Hàm dùng để truy xuất ngẫu nhiên file. 2.9.4. Tạo file nhị phân để ghi Mẫu lệnh 110
  48. tên_biến_file = fopen(tên_file_trên_đĩa, “wb”) Khi mở file xong, file sẽ rỗng vì chưa có phần tử nào, cửa sổ file sẽ không có giá trị xác định vì nó trỏ vào cuối file. Nếu trên đĩa đã có file trùng tên với file được mở thì file cũ sẽ bị xoá đi. ghi dữ liệu vào file nhị phân Mẫu lệnh: fwrite(đc, kt, sl,tt); Trong đó đc: địa chỉ của khối dữ liệu được đưa vào kt: kích thước của khối dữ liệu được đưa vào sl: số khối dữ liệu đưa vào tt: con trỏ cấu trúc file Hàm fwrite() cho phép ghi nhiều khối dữ liệu, tuy nhiên các khói phải có cùng kích thước. ví dụ: tạo file chứa 100 số nguyên đầu tiên, tên file được gõ vào từ bàn phím. #include #include main(){ clrscr(); char ten_tep[21]; int i; long so; file *f1; printf("cho biet ten tep can mo: "); scanf("%20s",ten_tep); f1=fopen(ten_tep,"wb"); for (i=1;i<=100;i++) fwrite(&i,sizeof(int),1,f1); fclose(f1); getch(); return 0; } 111
  49. 2.9.5. Đọc một file dữ liệu nhị phân Để đọc dữ liệu từ một file nhị phân trước hết cần phải kiểm tra xem con trỏ đã ở cuối file chưa. Để biết được thông tin này C cung cấp cho ta hàm chuẩn feof, nếu cửa sổ trỏ vào cuối file thì hàm có giá trị true, ngược lại cho giá trị false. Quy tắc đọc file: Đọc một phần tử rồi kiểm tra xem nó có là eof không theo mẫu lệnh: if ((ch=getc(f)) !=eof) Trong đó hàm getc(f) là hàm đọc một ký tự hay một byte của file có con trỏ là f, con trỏ này đã được khai báo trước đó. Hoặc có thể dùng lệnh while để quét hết các phần tử của file như sau: while ((ch=getc(f)) !=eof) Nghĩa là đọc các phần tử của file chừng nào kí tự đó chưa phải là kí hiẹu kết thúc file. Đọc dữ liệu Mẫu lệnh fread (đc, kt, sl,tt); đc: địa chỉ của khối dữ liệu được đưa vào kt: kích thước của khối dữ liệu được đưa vào sl: số khối dữ liệu đưa vào tt: con trỏ cấu trúc file Ví dụ 1: đọc dữ liệu từ một file nhị phân mà tên file được gõ vào từ bàn phím máy tính. #include #include main(){ clrscr(); char ten_tep[21]; char i; file *f; printf("ten tep can doc: "); scanf("%20s",ten_tep); f=fopen(ten_tep,"rb"); 112
  50. while (fread(&i,sizeof(int),1,f),!feof(f)) printf("%3d",i); fclose(f); getch(); return 0; } 2.9.6. Ghi file dữ liệu có cấu trúc (struct) #include #include #include #define true 1 char ss[88]; typedef struct date { int ngay; int thang; int nam; }; typedef struct maunhansu { char ten[20]; int so_nha; char pho[80]; char thanh_pho[30]; float luong; date ngaysinh; }; /* khai bao nguyen mau ham*/ maunhansu doc(maunhansu nhanvien); file *f; main() { clrscr(); 113
  51. int co=true; maunhansu nguoi; f=fopen("nhan_su.txt","wb"); nguoi.luong=0; while (co) { printf("ten: "); gets(nguoi.ten); if (nguoi.ten[0]=='\0') break; nguoi=doc(nguoi); fwrite(&nguoi,sizeof(maunhansu),1,f); } fclose(f); getch(); return 0; } maunhansu doc(maunhansu nhanvien) { printf("hay nhap ngay thang nam sinh: "); scanf("%d/%d/%d",&nhanvien.ngaysinh.ngay,&nhanvien .ngaysinh.thang,&nhanvien.ngaysinh.nam); printf("so nha: "); scanf("%d",&nhanvien.so_nha); gets(ss); printf("tinh: "); gets(nhanvien.thanh_pho); printf("phuong: "); gets(nhanvien.pho); printf("luong: "); scanf("%f",&nhanvien.luong); gets(ss); 114
  52. return (nhanvien); } 2.9.7. Đọc file dữ liệu cấu trúc #include #include #include #define true 1 char ss[88]; typedef struct date { int ngay; int thang; int nam; }; typedef struct maunhansu { char ten[20]; int so_nha; char pho[80]; char thanh_pho[30]; float luong; date ngaysinh; }; /* khai bao nguyen mau ham*/ void in(maunhansu nhanvien); file *f; main() { clrscr(); maunhansu nguoi; f=fopen("nhan_su.txt","rb"); 115
  53. while (fread(&nguoi,sizeof(maunhansu),1,f),!feof(f)) { in(nguoi); } fclose(f); getch(); return 0; } void in(maunhansu nhanvien){ printf("ten: %s\n",nhanvien.ten); printf("ngay sinh: %2d/%2d/%4d\n", nhanvien.ngaysinh.ngay,nhanvien.ngaysinh.thang,nhanvien. ngaysinh.nam); printf("so nha: %d\n",nhanvien.so_nha); printf("phuong: %s\n",nhanvien.pho); printf("tinh: %s\n",nhanvien.thanh_pho); printf("luong: %f",nhanvien.luong); return ; } 2.9.8. Truy nhập trực tiếp file dữ liệu nhị phân Ngôn ngữ C chỉ định nghĩa một kiểu truy nhập là truy nhập tuần tự. Tuy nhiên các bộ nhớ ngoài như đĩa mềm, đĩa cứng Cho phép có thể tính toán toạ độ của một phần tử bất kỳ của file vì độ dài của các phần tử trong file là như nhau. Điều đó cho phép truy nhập trực tiếp vào file mặc dù cấu tạo logic của file vẫn là dạng tuần tự tức là phần tử nọ xếp sau phần tử kia. Mẫu hàm: fseek(fptr, vị_trí*kich_thước_1_phần_tử, mốc); Trong đó: + fptr: con trỏ file + Vị_trí: là vị trí số thứ tự của phần tử cần truy nhập trong file (các phần tử trong file được đánh số từ 0, nhưng khi sử dụng ta lại tính từ 1). 116
  54. + Kích_thước_của_1_phần_tử: thường được thực hiện bằng hàm sizeof(). Vì vậy vị_trí*kích_thước_1_phần_tử sẽ cho toạ độ tính theo byte của phần tử cần truy nhập. + Mốc: chỉ ra điểm xuất phát để tính offset, nó nhận một trong các giá trị sau: seek_set (hoặc 0): di chuyển vị trí truy nhập bắt đầu từ đầu file. seek_cur (hoặc 1): di chuyển từ vị trí hiện tại của cửa sổ file seek_end (hoặc 2): di chuyển từ toạ độ cuối của file. Ví dụ: #include"stdio.h" #include"conio.h" #include"alloc.h" typedef struct { char ten[8]; int tuoi; }cautruc; const cautruc a[6]={"mai",23,"dao",21,"lan",18, "hue",20,"cuc",19,"hong",22}; file *f; int k; void main(){ cautruc *buffer; f=fopen("dulieu1.dbf","w+b"); /*mo tap tin, ghi nhi phan*/ for (k=0; k ten,buffer->tuoi); } clrscr(); printf("\n\tminh hoa di chuyen con tro taptin"); printf("\nmuon di chuyen toi nguoi thu may ?: "); scanf("%d",&k); fseek(f,(k-1)*sizeof(cautruc),seek_set); fread(buffer,sizeof(cautruc),1,f); printf("\n ten : %s tuoi: %d ",buffer->ten, buffer->tuoi); fseek(f,sizeof(cautruc),seek_cur); fread(buffer,sizeof(cautruc),1,f); 117
  55. printf("\n ten : %s tuoi : %d ",buffer- >ten,buffer->tuoi); fseek(f,0,seek_set); fread(buffer,sizeof(cautruc),1,f); printf("\n ten : %s tuoi: %d ",buffer- >ten,buffer->tuoi); free(buffer); fclose(f); printf("\nbam phim bat ky de ket thuc"); getch(); } 2.9.9. So sánh và chọn phương án sử dụng Các thao vào ra file mức thấp là những thao tác được ứng dụng trong giai đoạn đầu của sự hình thành và phát triển của hệ điều hành Unix. Ngày nay ngôn ngữ lập trình C đã trở nên thông dụng, phát triển mạnh mẽ trên nhiều hệ điều hành, nhưng để duy trì tính kế thừa và khả năng tương thích của các chương trình viết bằng C dưới hệ điều hành Unix, hệ thống các thao tác vào ra mức thấp vẫn được đưa ra và các thao tác vào ra qua vùng đệm chủ yếu cũng dựa trên các thao tác vào ra mức thấp này. Tuy nhiên, ngày nay các thao tác vào ra mức thấp không còn được ưa chuộng nữa và sau này chắc nó không còn được sử dụng, nhưng tuy nhiên các thao tác này còn được sử dụng trong một thời gian nữa. Trong hệ thống vào ra qua vùng đệm, khi làm việc với file văn bản nên sử dụng các hàm getc(), putc() ở chế độ văn bản. Khi thao tác với các file nhị phân hoặc các file có kiểu dữ liệu phức tạp nên sử dụng các file nhị phân với các hàm fread(), fwrite(). Không bao giờ được sử dụng lẫn lộn các thao tác vào ra thuộc hai hệ thống khác nhau vào trong một chương trình vì chúng dễ gây rối loạn lẫn nhau. Các lỗi thường gặp khi lập trình Quên không #include file header của thư viện các hàm đã dùng Quên mở file trước khi thực hiện các thao tác xử lý Qùng con trỏ file qui chiếu đến một file không hợp lệ Mở một file chưa tồn tại cho thao tác đọc Mở một file để đọc, ghi mà quên không gán các quyền truy nhập tương ứng cho nó (các quyền này tuỳ vào từng hệ điều hành) Mở một file để ghi mà không còn đủ vùng trống trên đĩa. 118
  56. Mở một file đã tồn tại cho cả thao tác ghi và đọc trong khi mục đích sử dụng chỉ cần đọc, khi đó nội dung của file có thể bị thay đổi do không để ý mà chẳng có một lưu ý gì cho người dùng Các thói quen tốt khi lập trình Luôn chắc chắn rằng con trỏ file trong các lời gọi hàm là hợp lý. Đóng file ngay mỗi khi chương trình không cần đến nó nữa phải đóng hết tất cả các file trước khi thật ra khỏi chương trình Mở file theo chế độ chỉ cho phép đọc nếu nội dung file cần được giữ nguyên, điều này tránh các thay đổi nội dung file ngoài mong muốn. 119
  57. Chương 3 LẬP TRÌNH TRONG KINH TẾ 3.1. Bài toán lãi suất 3.1.1. Phát biểu bài toán a. Bài toán lãi đơn Lãi đơn: Là hình thức tính lãi mà tiền lãi không được gộp vốn để tính lãi cho kì sau. Bài toán: Đầu tư số tiền là V0 với lãi suất r%/năm. Kì tính lãi là tháng, hình thức tính lãi là lãi đơn. Hỏi sau n kì lãi số tiền thu được là bao nhiêu? r% V V n. V n 012 0 b. Bài toán lãi kép Lãi kép: Là hình thức tính lãi mà tiền lãi được gộp vốn để tính lãi cho kì sau Bài toán: Đầu tư số tiền là V với lãi suất r%/năm. Kì tính lãi là tháng, hình 0 thức tính lãi là lãi kép. Hỏi sau n kì lãi số tiền thu được là bao nhiêu? n r% Vn V 0 1 12 3.1.2. Xây dựng chương trình a. Bài toán lãi đơn - Đầu vào: số tiền gửi/số tiền vay (V0); lãi suất gửi/lãi suất vay (r); thời gian gửi/thời gian vay (n). - Đầu ra: Số tiền lãi được nhận/phải trả sau khoảng thời gian gửi/vay - Công thức tính: +Tiền lãi tại thời điểm n: V0*(r/(100*12))*n (n tính theo tháng, lãi suất tính theo năm); V0*(r/100)*n (n tính theo năm, lãi suất tính theo năm). + Tiền gốc và lãi thu được sau khoảng thời gian (n): Vn n =V0(1+n/100) . Chương trình: #include 120
  58. float tinhlai(float so_tien, float lai_suat, int thoi_gian_vay { float a; a=so_tien*(lai_suat/100)*thoi_gian_vay; return(a); } float tongtien(float so_tien, float lai_suat, int thoi_gian_vay) { float a; a=so_tien+so_tien*(lai_suat/100)*thoi_gian_vay; return(a); } void main(){ float a,b,c,d,f,g; int e; printf ("Nhap so tien gui: "); scanf("%f",&a); printf("Lai suat %: "); scanf ("%f",&d); printf("Thoi gian gui: "); scanf ("%d",&e); f=tinhlai(a,d,e); g=tongtien(a,d,e); printf("\nSo tien lai nhan duoc la: %f",f); printf("\nTong so tien nhan duoc: %f",g); } b. Bài toán lãi kép - Đầu vào: số tiền gửi/số tiền vay (V0k); lãi suất gửi/lãi suất vay (rk); thời gian gửi/thời gian vay (nk). - Đầu ra: Số tiền lãi được nhận/phải trả sau khoảng thời gian gửi/vay 121
  59. - Công thức tính: n +Tiền lãi tại thời điểm n: V0k*(rk/(100*12)) (n tính theo tháng, r tính n lãi suất năm); V0k*(rk/(100)) (n tính theo năm, lãi suất tính theo năm). + Tiền gốc và lãi thu được sau khoảng thời gian (n): Vnk n =V0k(1+rk/100) . #include float tinhlai(float so_tien, float lai_suat, int thoi_gian_vay) { float a; a=so_tien*(lai_suat/100)*thoi_gian_vay; return(a); } float tinhl(float so_tien, float lai_suat, int thoi_gian_gui) { int i=0; float a,tien; a=1+lai_suat/100; while(i<thoi_gian_gui) { a=a*(1+lai_suat/100); i++; } tien=so_tien*a; return(tien); } void main(){ float a,b,c,d,f,g; int e; printf ("Nhap so tien gui: "); scanf("%f",&a); 122
  60. printf("Lai suat %: "); scanf ("%f",&d); printf("Thoi gian gui: "); scanf ("%d",&e); f=tinhlai(a,d,e); g=tinhl(a,d,e); printf("\nSo tien lai nhan duoc la: %f",f); printf("\nTong so tien nhan duoc: %f",g); } 3.2. Bài toán dự trữ 3.2.1. Mô hình quản lý dự trữ Wilson (tiêu thụ đều, bổ sung tức thời) Mô hình quản lý dự trữ theo mô hình Wilson là dạng bài toán dự trữ có tính chất tiêu thụ đều, bổ sung tức thời hàng hóa, sản phẩn dự trữ. Mô hình bài toán cụ thể như sau: a. Mô hình bài toán. Môt hệ thống kinh tế có nhu cầu loại hàng hoá nào đó là Q đơn vị trong khoảng thời gian là T (T = 1 đơn vị). Việc tiêu thụ loại hàng hoá này là điều đặn và thời gian bổ sung hàng vào kho dự trữ là tức thời. Chi phí cho mỗi lần đặt hàng là A, giá một đơn vị hàng là C, hệ số chi phí dự trữ là I, thời gian đặt hàng là T0. Hãy xác định số lần đặt hàng và lượng hàng đặt mỗi lần sao cho tổng chi phí là bé nhất. b. Phân tích bài toán. Hệ thống có đặt mua hàng dự trữ nhiều lần hoặc một lần trong khoảng thời gian T. Giả sử ta chia T thành n kỳ dự trữ và tiêu thụ, trong mỗi kỳ i đặt mua lượng hàng tương ứng là Si, (i = i, n ). Mỗi chu kỳ nhập mộtlượng hàng là Si, (i = i, n ), tiêu thụ hết chu kỳ thì phải có hàng bổ sung dự trữ. Như vậy không xảy ra thiếu thừa dự trữ, do đó không phát sinh ra các chi phí tương ứng. Hệ thống có thể dự trữ bằng cách mua ngay một lần tất cả khối lượng hàng Q cần thiết vào đầu năm (n = 1). Khi đó giả sử khối lượng hàng dự trữ được chở đến hệ thống một lần thì mức dự trữ trung bình trong kho là: 1 T Q() t dt T 1 123
  61. Nếu hệ thống đặt mua hàng dự trữ 2 lần vào đầu năm và giữa năm với khối lượng mỗi lần là Q/2 thì mức dự trữ trung bình trong kho bằng Q/4. Nếu hệ thống đặt mua hàng dự trữ 4 lần vào đầu mỗi quý với khối lượng mỗi lần là Q/4 thì mức dự trữ trung bình trong kho là Q/8 v.v Một cách tổng quát nếu hệ thống thực hiện việc mua hàng bằng nhau và bằng T/n thì lượng hàng dự trữ trung bình trong kho sẽ bằng Z = Q/2n. Đặt S=Q/n ta có n=Q/S. Khi đó chi phí để thực hiện n hợp đồng đặt hàng là An + CQ,chi phí bảo quản dự trữ trong thời gian T là ICZ. Do đó tổng chi phí tạo ra dự trữ cho hệ thống (bao gồm chi phí đặt hàng và chi phí quản lý bảo quản dự trữ) là: F (Z,n) = CQ + An + ICZ. Thì khi đó ta có: - Khối lượng hàng đặt mua mỗi lần S = Q/n sẽ là tối ưu nếu chi phí giới hạn cho việc bảo quản bằng giá trị tuyệt đối của chi phí giới hạn cho việc đặt hàng. Nói cách khác là giá trị của S sẽ tối ưu nếu việc giảm chi phí bảo quản do khối lượng hàng đặt mua mỗi lần giảm đi một đơn vị bằng việc tăng chi phí đặt hàng số lần đặt hàng tăng lên. - Nếu ký hiệu S* là khối lượng đặt hàng tối ưu cho mỗi lần, thì S* là: 2AQ s IC - Số lần đặt hàng tối ưu: n*=Q/S*. - Chu kỳ dự trữ - tiêu thụ tối ưu: t*=1/n*. Điểm đặt hàng tối ưu: Việc xác định điểm đặt hàng tối ưu sẽ đơn giản nếu * * thời gian đặt hàng T0 < t , song thực tế có thể T0 ≥ t như vậy cần đặt hàng trước một hay một số chu kỳ. Điểm đặt hàng là mốc khối lượng trong kho khi lượng hàng trong kho cần bổ sung một lượng hàng đúng bằng lượng hàng tiêu thụ trong khoảng thời gian T0. Muốn vậy thì hợp đồng đặt hàng dự trữ phải được ký kết vào lúc trong kho còn lượng hàng dự trữ là: * * * B = Q [T0 – t . int (T0 /t )] * * (Trong đó int (T0 /t ) là phần nguyên của T0/t ). c. Xây dựng Chuong trinh - Đầu vào : Nhu cầu hàng Q, Thời gian T, Chi phí mỗi lần đặt hàng A, Giá hàng C, chi phí dữ trữ I. - Đầu ra : Số lần đặt hàng tối ưu (n) ; Lượng đặt hàng tối ưu cho mỗi lần (S) ; Chu kỳ dự trữ (t). 124
  62. 2AQ - Công thức : s ; n*=Q/S*; t*=1/n*. IC 3.2.2. Mô hình dự trữ tiêu thụ đều, bổ sung dần Trong mô hình dự trữ Wilson việc bổ sung hàng dự trữ là tức thời, nhưng trong thực tế sản xuất kinh doanh việc bổ sung hàng dự trữ là dần dần. Người ta vừa sản xuất vừa tiêu thụ, vừa nhập hàng vừa tiêu thụ. Đó là những mô hình dự trữ tiêu thụ đều, bổ sung dần dần. Trong mô hình này nếu cường độ cung cấp bằng cường độ tiêu thụ thì sẽ không có dự trữ, vì vậy ta chỉ xét các hệ thống kinh tế mà cường độ cung cấp lớn hơn đáng kể so với cường độ tiêu thụ. a. Mô hình bài toán dự trữ: - Xét lớp hệ thống kinh tế mà nhu cầu một loại hàng trong thời kỳ T (T=1) là Q đơn vị. Việc tiêu thụ hàng đều đặn và thời gian bổ sung hàng vào kho được tiến hành với cường độ không đổi K đơn vị trong suốt thời gian T = 1. Ta giả thiết rằng K >> Q (vì nếu K ≤ Q thì không cần đặt vấn đề dự trữ). Chi phí cho mỗi lần đặt hàng là A, giá đơn vị hàng là C, hệ số chi phí dự trữ là I, thời gian đặt hàng là T0. Hãy xác định số lần đặt hàng và lượng hàng đặt mỗi lần sao cho tổng chi phí tạo ra dự trữ. b. Phân tích bài toán Tương tự mô hình Wilson, ta có thể xem số lượng hàng đặt mỗi lần bằng nhau và bằng S, số lần đặt hàng là n =Q/S. Vì Q << K nên trong mỗi chu kỳ dự trữ - tiêu thụ T có hai khoảng thời gian ts và tr , trong đó ts là thời gian có bổ sung vào kho và trlà thời gian chỉ tiến hành tiêu thụ. Gọi lượng hàng dự trữ tối đa trong kho là Z, khi đó nếu trong thời gian T=1, nhập hàng lien tục thì lượng hàng dư là K - Q, trong thời gian ts lượng hàng dư cũng chính là Z, vậy: Z/ts=(K-Q)/T hay hay Z = ts(K-Q), (vì T=1) Tương tự mô hình Wilson ta có : - Lượng đặt hàng tối ưu cho mỗi lần đặt là : 2AQ s Q IC 1 K - Số lần đặt hàng tối ưu là n*: Q ICQ KQ n SAK2 - Chu kỳ dự trữ - tiêu thụ tối ưu là t*: 1 t n 125
  63. Điểm đặt hàng tối ưu trong trường hợp này ta xét khi đang làm đầy kho và khi đang làm vơi kho. * * Trước tiên tính B = Q [T0- t .int (T0/t )]. Khi đó: - Nếu B ≤ Z*= S*(1-Q/K)thì B* lấy bằng B và đặt hàng khi đang làm vơi kho hàng đang xét. - Nếu B > Z thì B* lấy bằng (K-Q)(t*-B/Q) và đặt hàng khi đang làm đầy kho hàng đang xét. c. Xây dựng chương trình - Đầu vào : Nhu cầu hàng Q, Thời gian T, Chi phí mỗi lần đặt hàng A, Giá hàng C, chi phí dữ trữ I. - Đầu ra : Số lần đặt hàng tối ưu (n) ; Lượng đặt hàng tối ưu cho mỗi lần (S) ; Chu kỳ dự trữ (t). 2AQ * * * * 1 - Công thức : s ; n =Q/S ; t =1/n ; t Q IC 1 n K 3.3. Bài toán quy hoạch tuyến tính Trong toán học, quy hoạch tuyến tính (QHTT) (linear programming - LP) là bài toán tối ưu hóa, trong đó hàm mục tiêu (objective function) – f(x) và các điều kiện ràng buộc đều là tuyến tính. f(x1,x2, ,xn) = a1x1+a2x2 + .+anxn+b a. Phát biểu bài toán Để hiểu rõ bài toán này chúng ta xét ví dụ sau: Một xí nghiệp cần sản xuất 3 loại bánh: bánh đậu xanh, bánh thập cẩm và bánh dẻo. Lượng nguyên liệu đường, đậu cho một bánh mỗi loại, lượng dự trữ nguyên liệu, tiền lãi cho một bánh mỗi loại được cho trong bảng sau: Nguyên liệu Bánh đậu xanh Bánh thập cẩm Bánh dẻo Lượng dự trữ Đường 0,04kg 0,06kg 0,05kg 500kg Đậu 0,07kg 0kg 0,02kg 300kg Bột mì 0,19kg 0,24kg 0,13kg 600kg Lãi 3000 2000 2500 Hãy lập mô hình bài toán tìm số lượng mỗi loại bánh cần sản xuất sao cho không bị động về nguyên liệu mà lãi đạt được cao nhất. 126
  64. Để hiểu rõ cách xây dựng và tìm phương án cho bài toán trên ta tìm hiểu cách giải bài toán trên dưới đây: Gọi x1, x2 , x3 lần lượt là số bánh đậu xanh, bánh thập cẩm, bánh dẻo cần phải sản xuất. Điều kiện: xj 0 , j =1,2,3. Khi đó: 1) Tiền lãi thu được là: f (x) = f (x1 , x2 , x3 ) = 3x1 + 2x2 + 2,5x3 (ngàn). 2) Lượng đường được sử dụng là: 0,04x1 + 0,06x2 + 0,05x3 (kg) Để không bị động về nguyên liệu thì: 0,04x1 + 0,06x2 + 0,05x3 ≤500 . 3) Lượng đậu được sử dụng là: 0,07x1 + 0,02x3 (kg) Để không bị động về nguyên liệu thì: 0,07x1 + 0,02x3 ≤ 300. 4) Lượng bột mì được sử dụng là: 0,19x1 + 0,24x2 + 0,13x3 (kg) Để không bị động về nguyên liệu thì: 0,19x1 + 0,24x2 + 0,13x3 ≤600 . Vậy ta có mô hình bài toán (1) f (x) = f (x1 , x2 , x3 ) = 3x1 + 2x2 + 2,5x3 max (2) 0,04x1 + 0,06x2 + 0,05x3 ≤500 0,07x1 + 0,02x3 ≤ 300 0,19x1 + 0,24x2 + 0,13x3 ≤600 (3) xj 0, j =1,2,3. Ta nói đây là bài toán quy hoạch tuyến tính 3 ẩn tìm max của hàm mục tiêu. b. Phân tích bài toán Dựa trên ví dụ trên ta có mô hình tổng quát của bài toán như sau : - Dạng tổng quát n Tìm x = (x1, x2 xi, xn) ∈IR . n Sao cho: f(x) = Cj x j → max (min) (1) j 1 Thỏa mãn điều kiện: m aij x j (≤, = ≥ ) bi ( i=1,m ) (2) j 1 xj≥ 0 (j = 1, n ) (3) - Dạng chuẩn tắc n Tìm x = (x1 , , xj , xn) ∈ IR 127
  65. Sao cho: f(x) = c1x1 + +ci xi + + cn xn → max (7) Thoả mãn a11 x1 + + a1ixi + +a1nxn ≤ b1 a21 x1 + + a2ixi + +a2nxn ≤ b2 ai1 x1 + + aiixi + +ainxn ≤ bi (8) am1 x1 + + amixi + +amnxn ≤ bm xi ≤ 0 ( i = 1, n ) (9) - Dạng rút gọn. n f(x) = ci x i → max/min i 1 m aji x j ≤ (>=) bi ( i=1,m ) j 1 xi ≥ 0 ( i = 1, n ) Tính chất của hàm mục tiêu (7) và dạng bất phương trình của hệ ràng buộc (8) xuất phát từ ý nghĩa thực tiễn của bài toán đặt ra. Chẳng hạn như bài toán lập kế hoạch sản xuất để hiệu quả kinh tế tổng cộng lớn nhất, khi phải hạn chế chi tiết nguyên liệu sử dụng. Ngược lại, trong bài toán xác định vốn đầu tư cho sản xuất phải khai thác tối đa trang bị kỹ thuật - công nghệ để sao cho đạt được yêu cầu về giá trị sản phẩm làm ra mà vốn đầu tư ít nhất. c. Xây dựng chương trình - Đầu vào: Hệ số hàm mục tiêu CT; Ma trận ràng buộc AT; * - Đầu ra: phương án tối ưu: X = (x1 x2 x3); - Cách tính: Thêm các biến phụ; chuyển các bất phương trình ràng buộc về dạng phương trình: Tính Det(A); Det (A1); Det(A2); Det(A3). Khi đó: x1=Det(A1)/Det(A); x2=Det(A2)/Det(A); x3=Det(A3)/Det(A). 3.4. Bài toán phục vụ công cộng a. Mô tả bài toán - Hệ thống phục vụ công cộng từ chối cổ điển (Elang) Hệ thống phục vụ công cộng có n kênh phục vụ, năng suất các kênh bằng nhau và bằng , dòng yêu cầu đến hệ thống là dòng poát xông dừng mật 128
  66. độ  (qui luật về sự xuất hiện các yêu cầu theo thời gian). Thời gian phục vụ 1 yêu cầu của kênh tuân theo qui luật chỉ số. Một yêu cầu đến hệ thống gặp lúc có ít nhất 1 kênh rỗi thì được nhận phục vụ cho đến thoả mãn tại 1 trong các kênh rỗi đó. Ngược lại nếu tất cả các kênh đều bận thì phải ra khỏi hệ thống. Cần xác định các chỉ tiêu phân tích hệ thống. - Hệ thống chờ với độ dài hàng chờ hạn chế và thời gian chờ không hạn chế Hệ thống phục vụ công cộng có n kênh phục vụ, năng suất các kênh bằng nhau và bằng , dòng yêu cầu đến hệ thống là dòng poát xông dừng mật độ  (qui luật về sự xuất hiện các yêu cầu theo thời gian). Thời gian phục vụ 1 yêu cầu của kênh tuân theo qui luật chỉ số. Một yêu cầu đến hệ thống gặp lúc có ít nhất 1 kênh rỗi thì được nhận phục vụ cho đến thoả mãn tại 1 trong các kênh rỗi đó. Ngược lại nếu tất cả các kênh đều bận thì xếp hàng chờ nếu số yêu cầu chờ bé hơn m. Cần xác định các chỉ tiêu phân tích hệ thống. - Hệ thống chờ thuần nhất Hệ thống phục vụ công cộng có n kênh phục vụ, năng suất các kênh bằng nhau và bằng , dòng yêu cầu đến hệ thống là dòng poát xông dừng mật độ  (qui luật về sự xuất hiện các yêu cầu theo thời gian). Thời gian phục vụ 1 yêu cầu của kênh tuân theo qui luật chỉ số. Một yêu cầu đến hệ thống gặp lúc có ít nhất 1 kênh rỗi thì được nhận phục vụ cho đến thoả mãn tại 1 trong các kênh rỗi đó. Ngược lại nếu tất cả các kênh đều bận thì xếp hàng chờ. Cần xác định các chỉ tiêu phân tích hệ thống. b. Phân tích bài toán Trong quá trình hoạt động, dưới tác động của dòng vào và dòng phục vụ, hệ thống phục vụ chuyển từ trạng thái này sang trạng thái khác. Ta gọi xác suất của quá trình đó là xác suất chuyển trạng thái. Nguyên nhân gây ra sự chuyển trạng thái là do tác động của dòng vào và dòng phục vụ, số yêu cầu và số kênh bận trong hệ thống thay đổi, nghĩa là dưới tác động của dòng vào λi(t) và dòng phục vụ μi(t) tại thời điểm t, hệ thống sẽ biến đổi từ trạng thái này sang trạng thái khác. Ta thiết lập quan hệ giữa xác suất xuất hiện trạng thái xk(t): Pk(t), với các tác nhân gây ra sự biến đổi trạng thái đó. Mối quan hệ này được hiển thị bởi các phương trình toán học chứa các xác suất Pk(t) và cường độ dòng chuyển trạng thái của hệ thống. 129
  67. k Ta đặt = / từ (2) ta có Pk = P ! 0 Thay vào điều kiện chuẩn ta có: n n k  Pk  P0 1 k 0 k 0 K! 1 => P = 0 n k  k 0 ! Bằng cách nhân cả tử số và mẫu số trong công thức trên với e- ta có: e 0 / 0! P = 0 n k  e k 0 ! Ký hiệu: P( , k) = e k /! - đây là xác suất đại lượng ngẫu nhiên phân phối poát xông nhận giá trị k. k R( , k) =  P( ,i) - là xác suất tích luỹ tương ứng ta có: i 0 e 0 / 0! P( ,0) P0 = n k R( , n)  e k 0 ! k P( , k) Từ đó: Pk = P (4) k! 0 R( , n) Các giá trị P( , k) và R( , k) được tính trong bảng phân phối poátxông. Các chỉ tiêu đánh giá hoạt động của hệ thống Đối với hệ thống này các chỉ tiêu cơ bản đánh giá hệ thống là: (1). Xác suất hệ thống có n kênh rỗi: Pr 0 P( ,0) Pr = P0 = P 0! 0 R( ,n) Chỉ tiêu này cho biết tỷ lệ thời gian hệ thống rỗi hoàn toàn, thời gian rỗi hoàn toàn tồn tại ở mọi hệ thống poát xông nói riêng và các hệ ngẫu nhiên nói chung, dù ta có giảm tối thiểu đến số kênh phục vụ hay tăng tối đa cường độ dòng yêu cầu. 130
  68. (2). Xác suất hệ thống có n kênh bận (hay xác suất một yêu cầu đến hệ thống bị từ chối Ptc): n P( , n) Pn = P n! 0 R( , n) Đây cũng là hiệu suất lý thuyết tối đa của hệ thống, như vậy trong trường hợp hệ ngẫu nhiên không có khả năng thiết kế một hệ thống khai thác toàn bộ công suất kỹ thuật của các kênh. (3). Xác suất phục vụ (xác suất một yêu cầu đến hệ thống được nhận phục vụ) là: Ppv = 1-Ptc = 1-Pn Đó cũng là tỷ lệ của các đối tượng được hệ thống tiếp nhận và phục vụ đối với hệ thống được phục vụ công cộng, đây là một trong số ít các chỉ tiêu quan trọng nhất, với cùng một tiềm năng kỹ thuật như nhau có thể chọn chỉ tiêu này làm mục tiêu thiết kế hệ thống. Sau đây là một số chỉ tiêu tính toán ở mức trung bình, vì vậy các công thức dựa trên cơ sở tính kỳ vọng toán học của các đại lượng ngẫu nhiên. (4). Số kênh bận trung bình (hay số yêu cầu trung bình có trong hệ thống): n n k n 1 k N b = kPk  P0  P0 k 0 k 1 k! k 0 k! P( ,n) = (1 P ) [1 ] .P n R( ,n) pv (5). Số kênh rỗi trung bình: N r n N b N (6). Hệ số bận (rỗi): H b b n Hr = 1 - Hb (7). Hiệu quả chung: F Tuỳ thuộc vào các đánh giá lợi ích hay thiệt hại trong quá trình phục vụ và việc tận dụng công suất hệ thống cũng như các loại lợi ích khác, người ta có thể lập một chỉ tiêu tổng hợp đánh giá hiệu quả chung của hệ thống. Việc phục vụ một yêu cầu mang lại một lợi ích là Cpv; mỗi yêu cầu bị từ chối gây thiệt là Cct; mỗi kênh rỗi gây lãng phí Ckr; thì trong một đơn vị thời gian có thể tính được chỉ tiêu hiệu quả chung là: 131
  69. F= ppvCpv - N r ckr- pnctc Trên cơ sở các chỉ tiêu đó ta có thể chọn một hay một vài chỉ tiêu để tối ưu hoá hệ thống. c. Xây dựng chương trình - Đầu vào: Số kênh phục vụ; Cường độ dòng vào; Năng suất phục vụ. - Đầu ra: Xác suất kênh đều bận; Xác suất kênh đều rỗi; Xác suất yêu cầu vào được phục vụ; Xác suất yêu cầu vào bị từ chối; Số kênh bận trung bình; Số kênh rỗi trung bình; Số kênh bận trung bình; Hệ số bận; Hệ số rỗi. - Công thức xác định: 1 + Xác suất kênh đều rỗi: P = 0 n k  k 0 ! n P( , n) + Xác suất kênh đều bận: Pn = P n! 0 R( , n) + Xác suất yêu cầu vào được phục vụ: Ppv = 1-Ptc = 1-Pn + Xác suất yêu cầu vào bị từ chối: Ptc =1-Ppv + Số kênh bận trung bình: Nb= .Ppv + Số kênh rỗi trung bình: Nr=n-Nb N + Hệ số bận: H b b n + Hệ số rỗi: Hr = 1 - Hb 132
  70. TÀI LIỆU THAM KHẢO [1] Nguyễn Quảng, Nguyễn Thượng Hải, (2007), Giáo trình Toán kinh tế, Học viện Bưu chính viễn thông. [2] Phạm Văn Ất, (2009), Giáo trình kỹ thuật lập trình C căn bản và nâng cao, NXB Hồng Đức. [3] Quách Tuấn Ngọc, (1998), Ngôn ngữ lập trình C, NXB Giáo dục [4] Nguyễn Hữu Tuấn, (1997), Ngôn ngữ lập trình C, Nhà xuất bản Khoa học kỹ thuật. [5] Quách Tuấn Ngọc (2003), Ngôn ngữ lập trình C++, NXB Giáo dục [6] Khoa HTTT Kinh tế - HV Ngân hàng, (2011), Bài giảng Cơ sở lập trình [7] Bình Minh Trí, (2006), Giáo trình lý thuyết tối ưu kinh tế, NXB Khoa học và Kỹ thuật. 133