Bài giảng Lập trình nâng cao - Bài 2+3: Hàm trong C/C++ - Trương Xuân Nam

pdf 46 trang Gia Huy 17/05/2022 2071
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng Lập trình nâng cao - Bài 2+3: Hàm trong C/C++ - Trương Xuân Nam", để 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_lap_trinh_nang_cao_bai_23_ham_trong_cc_truong_xuan.pdf

Nội dung text: Bài giảng Lập trình nâng cao - Bài 2+3: Hàm trong C/C++ - Trương Xuân Nam

  1. LẬP TRÌNH NÂNG CAO Bài 2+3: Hàm trong C/C++ TRƯƠNG XUÂN NAM 1
  2. Nội dung chính 1. Cấu chúc chung của hàm 2. Hiểu về cách hàm hoạt động 3. Các hàm có sẵn 4. Phạm vi của biến và của hàm 5. Truyền tham số trong hàm 6. Nạp chồng hàm 7. Hàm đệ quy 8. Bài tập Trương Xuân Nam - Khoa CNTT 2
  3. Phần 1 Cấu chúc chung của hàm TRƯƠNG XUÂN NAM 3
  4. Cấu chúc chung của hàm #include ▪ Đã học trong Nhập môn Lập trình using namespace std; ▪ Định nghĩa hàm (function // hàm mu3: tính a^3 definition) gồm 2 phần: int mu3(int a) ▪ Phần khai báo (function { declaration / function int b = a * a * a; prototype) return b; ▪ Phần thân (function body) } ▪ Gọi hàm: int main() { ▪ Thông qua tên cout << mu3(20); ▪ Truyền đối số phù hợp } TRƯƠNG XUÂN NAM 4
  5. Cấu chúc chung của hàm #include ▪ Phần khai báo hàm có thể using namespace std; tách riêng // hàm mu3: tính a^3 ▪ Thường viết ở phần đầu int mu3(int a); của file hoặc tách riêng thành một file (gọi là file int main() { header) cout << mu3(20); } ▪ Có thể gọi hàm ngay cả khi chưa biết thân hàm int mu3(int a) { viết thế nào int b = a * a * a; return b; ▪ Phần định nghĩa hàm viết } đầu đủ sau đó TRƯƠNG XUÂN NAM 5
  6. Cấu chúc chung của hàm #include ▪ Phần khai báo hàm không using namespace std; cần viết tên tham số // hàm mu3: tính a^3 ▪ Vẫn phải viết kiểu trả về và int mu3(int); tên hàm. Riêng phần tham số chỉ cần viết kiểu và bỏ int main() { qua phần tên cout << mu3(20); } ▪ Có thể gọi hàm ngay cả khi chưa biết thân hàm int mu3(int a) { viết thế nào int b = a * a * a; return b; ▪ Phần định nghĩa hàm viết } đầu đủ sau đó TRƯƠNG XUÂN NAM 6
  7. Cấu chúc chung của hàm #include ▪ Phần khai báo hàm không using namespace std; cần viết tên tham số // hàm mu3: tính x^3 ▪ Thậm chí tên tham số ở int mu3(int x); trên viết một đằng ở dưới viết một nẻo vẫn được int main() { chấp nhận cout << mu3(20); } ▪ Có thể gọi hàm ngay cả khi chưa biết thân hàm int mu3(int a) { viết thế nào int b = a * a * a; return b; ▪ Phần định nghĩa hàm viết } đầu đủ sau đó TRƯƠNG XUÂN NAM 7
  8. Cấu trúc của một chương trình C/C++ #include ▪ Tiền xử lý: using namespace std; ▪ #include const int MAX = 100; ▪ #define double PI = 3.1415; ▪ Khai báo, định nghĩa: int mu3(int); ▪ Hằng số int main() { ▪ Biến cout << mu3(20); ▪ Nguyên mẫu hàm } ▪ Nguyên mẫu lớp int mu3(int a) { ▪ Mã chính: int b = a * a * a; ▪ Các hàm return b; ▪ Các lớp } TRƯƠNG XUÂN NAM 8
  9. Quy tắc ▪ Khai báo hàm: cung cấp thông tin nguyên mẫu của hàm ▪ Mô tả đủ thông tin để có thể phát lời gọi hàm ▪ Phải viết trước bất kỳ lời gọi hàm nào ▪ Phải có kiểu trả về của hàm ▪ Phải có tên hàm ▪ Phải có kiểu của từng tham số ▪ Không nhất thiết phải có tên tham số ▪ double mu_x(int, double); ▪ double mu_x(int a, double d); ▪ Gọi hàm: gọi tên hàm và các đối số cần thiết ▪ d = mu_x(3, 0.5); ▪ Những giá trị thực sự được dùng trong lời gọi hàm được gọi là đối số (argument) hoặc tham số thực (actual parameter) TRƯƠNG XUÂN NAM 9
  10. Quy tắc ▪ Định nghĩa hàm: viết đầy đủ cả phần khai báo và phần thân hàm ▪ Tất nhiên phải viết đầy đủ tên các tham số (parameter) để có thể sử dụng được chúng trong phần thân hàm ▪ Còn gọi là các tham số hình thức (formal parameter) ▪ Trả về kết quả thông qua lệnh return ▪ Nếu hàm không có kết quả tính toán (chẳng hạn hàm in N số ra màn hình), thì khai báo kiểu void và không cần return nữa ▪ double mu_x(int a, double d) { double k = 1; for (int i = 0; i < a; i++) k *= d; return k } TRƯƠNG XUÂN NAM 10
  11. Thảo luận ▪ Mục đích của việc sử dụng hàm? ▪ Tái sử dụng: Mã được viết một lần, sử dụng nhiều lần ▪ Giảm chi phí: Sửa lỗi, nâng cấp ở một đoạn mã ▪ Dễ phát triển: Chia chương trình phức tạp thành nhiều đơn thể, giảm độ phức tạp khi viết các khối mã ▪ Tại sao phải tách phần nguyên mẫu và phần thân hàm? ▪ Tập trung vào các chức năng ▪ Phát triển song song ▪ Hàm: hiện thực hóa ý tưởng “trừu tượng hóa chức năng” (functional abstraction) ▪ Người dùng chỉ cần biết đến chức năng của mã ▪ Người dùng không cần quan tâm đến chi tiết của mã TRƯƠNG XUÂN NAM 11
  12. Phần 2 Hiểu về cách hàm hoạt động TRƯƠNG XUÂN NAM 12
  13. Hiểu về cách hàm hoạt động #include ①Phát lời gọi hàm using namespace std; ②Gán giá trị thực cho double mu_x(int, double); tham số: a = 3, d = 0.5 int main() { ③Vào thân hàm cout << mu_x(3, 0.5); ④Khai báo biến k = 1 } ⑤Thực hiện vòng for double mu_x(int a, double d) { ⑥Trả về kết quả qua lệnh double k = 1; for (int i = 0; i < a; i++) return và thoát khỏi hàm k *= d; return k; } Vấn đề: hàm không thể thay đổi giá trị của đối số TRƯƠNG XUÂN NAM 13
  14. Phần 3 Các hàm có sẵn TRƯƠNG XUÂN NAM 14
  15. Các hàm có sẵn ▪ Các hàm có sẵn do các lập trình viên khác viết ra và cung cấp cho chúng ta sử dụng ▪ Hàm chuẩn của C/C++ đi kèm với trình biên dịch ▪ Hàm do các đồng nghiệp trong cùng dự án viết cho chúng ta ▪ Cung cấp ở dạng thư viện, chỉ việc khai báo và sử dụng ▪ Thư viện thường gồm 2 loại file: • File header: chỉ chứa các khai báo hàm (.h, .hpp hoặc không đuôi) • File source: chứa phần thân hàm (.c, .cpp) ▪ Khai báo thư viện thông qua phát biểu #include ▪ Phát biểu #include phải chỉ ra file header sẽ sử dụng ▪ #include ← tìm file trong thư mục chuẩn ▪ #include "mylib" ← tìm file trong thư mục hiện tại TRƯƠNG XUÂN NAM 15
  16. Các hàm có sẵn TRƯƠNG XUÂN NAM 16
  17. Các hàm có sẵn TRƯƠNG XUÂN NAM 17
  18. Tạo số ngẫu nhiên ▪ Một trong những vấn đề thú vị nhất ▪ Tạo các tình huống ngẫu nhiên trong chương trình, trò chơi ▪ Tạo các biến ngẫu nhiên trong tính toán khoa học ▪ Chỉ là giả-ngẫu-nhiên ▪ Một số kĩ thuật lưu ý: ▪ rand(): trả về giá trị nguyên giữa 0 & RAND_MAX • RAND_MAX tùy thuộc vào từng thư viện và trình biên dịch ▪ Thu hẹp phạm vi: rand() % 6 • Trả về số ngẫu nhiên giữa 0 & 5 ▪ Tịnh tiến: rand() % 6 + k • Trả về số ngẫu nhiên giữa k & 5+k ▪ Số thực ngẫu nhiên: rand() / (double) RAND_MAX ▪ Khởi tạo nhân cho việc tạo số ngẫu nhiên: srand(time(0)) TRƯƠNG XUÂN NAM 18
  19. Phần 4 Phạm vi của biến và của hàm TRƯƠNG XUÂN NAM 19
  20. Quy tắc ▪ Biến chỉ có thể truy cập sau khi đã khai báo ▪ Biến được khai báo bên trong khối nào (giữa cặp ngoặc {} nào) thì chỉ được truy cập bên trong khối đó ▪ Biến không nằm trong bất kỳ cặp ngoặc nào: biến toàn cục (global variable) ▪ Biến nằm trong hàm: biến cục bộ (local variable) ▪ Biến tham số của hàm được sử dụng trong hàm ▪ Biến global: ▪ Có thể truy cập từ bất kỳ đâu trong chương trình ▪ Chú ý: một chương trình có thể gồm nhiều file ▪ Có thể truy cập biến ở trong file khác: từ khóa extern ▪ Rất cẩn thận khi sử dụng TRƯƠNG XUÂN NAM 20
  21. Từ khóa static ▪ Từ khóa này có thể dụng cho cả hàm, biến toàn cục và biến cục bộ ▪ Dùng với hàm: quy định rằng hàm này chỉ được dùng trong phạm vi của file hiện tại ▪ Dùng với biến toàn cục: quy định rằng biến này chỉ được truy cập trong phạm vi của file hiện tại ▪ Dùng với biến cục bộ: quy định rằng biến này là “tĩnh”, không bị hủy đi khi kết thúc hàm ▪ Chỉ khởi tạo một lần ▪ Sống theo vòng đời của chương trình, không bị hủy với hàm ▪ Hàm lần sau sử dụng giá trị còn tồn lại từ lần gọi trước TRƯƠNG XUÂN NAM 21
  22. Ví dụ về quy tắc phạm vi // biến toàn cục, truy cập từ bất kỳ đâu, kể cả từ file khác long long g = 0; // biến toàn cục, được khai báo ở file khác extern double d; // biến module, chỉ được truy cập từ đoạn mã cùng file static int c = 1; // hàm toàn cục, có thể gọi từ bất kỳ đâu, kể cả từ file khác void change(int a) { static int x = 0; // biến module, nhưng phạm vi hàm // hàm module, chỉ gọi được từ đoạn mã cùng file static void dosmt(bool a) { bool b = true; // biến cục bộ, phạm vị hàm // hàm toàn cục, định nghĩa hàm được viết ở file khấc extern int somefuction(int n); TRƯƠNG XUÂN NAM 22
  23. Ví dụ về biến static, hãy chạy thử xem nào! #include using namespace std; void change(int a) { static int x = 0; cout << "X = " << ++x << endl; a = 10; } int main() { int b = 5; change(b); change(b); change(b); } TRƯƠNG XUÂN NAM 23
  24. Phần 5 Truyền tham số trong hàm TRƯƠNG XUÂN NAM 24
  25. Trở lại ví dụ về cách hàm hoạt động #include ①Phát lời gọi hàm using namespace std; ②Gán giá trị thực (đối số) double mu_x(int, double); cho tham số: a = 3, d = 0.5 int main() { ③Khai báo biến k = 1 cout << mu_x(3, 0.5); ④Thực hiện vòng for } ⑤Trả về kết quả qua lệnh double mu_x(int a, double d) { return và thoát khỏi hàm double k = 1; for (int i = 0; i < a; i++) k *= d; Vấn đề: Biến a và d là biến return k; } của hàm, việc thay đổi giá trị chỉ có tác dụng nội bộ TRƯƠNG XUÂN NAM 25
  26. Một ví dụ rõ ràng hơn #include ①In ra b (b = 5) using namespace std; ②Phát lời gọi hàm ③Gán giá trị thực (đối số) void change(int a) { a = 10; cho tham số: a = b (a = 5) cout << "a = " << a << endl; ④In ra a (a = 10) } ⑤Thoát khỏi hàm int main() { ⑥In ra b (b = 5) int b = 5; cout << "b = " << b << endl; change(b); Việc thay đổi biến a trong cout << "b = " << b << endl; } hàm không tác động gì đến biến b ở hàm chính TRƯƠNG XUÂN NAM 26
  27. Cách giải quyết: tham chiếu (&) ▪ Đây là kiến thức có trong môn học Nhập môn Lập trình ▪ Biến tham chiếu: ▪ Alias (nickname) của một biến khác ▪ Khai báo như biến, nhưng thêm dấu & vào trước tên biến ▪ Tác động vào tham chiếu cũng như tác động vào biến ▪ Ví dụ: int x; // biến x int & y = x, z = y; // tham chiếu y, biến z x = 10; // y = 10 z = 9 // x và y vẫn là 10 y = 20; // x = 20 TRƯƠNG XUÂN NAM 27
  28. Ví dụ viết lại bằng tham chiếu #include ①In ra b (b = 5) using namespace std; ②Phát lời gọi hàm, gán tham số: a = b (a ≈ b) void change(int & a) { a = 10; ③Thay đổi a = 10 (b = 10) cout << "a = " << a << endl; ④In ra a (a = 10) } ⑤Thoát khỏi hàm int main() { ⑥In ra b (b = 10) int b = 5; cout << "b = " << b << endl; change(b); Muốn tham số bị thay đổi cout << "b = " << b << endl; } giá trị trong hàm: khai báo nó ở dạng tham chiếu TRƯƠNG XUÂN NAM 28
  29. Tham chiếu có ưu điểm và có điểm bất tiện #include ▪ Tiết kiệm bộ nhớ (biến tham chiếu luôn có kích using namespace std; thước 4 byte – tùy OS) void change(int & a) { ▪ Nhanh (vì kích thước nhỏ) a = 10; cout << "a = " << a << endl; ▪ Chỉ sử dụng được với } biến, không làm việc với dữ liệu trực trị (giá trị viết int main() { int b = 5; trực tiếp vào đối số) cout << "b = " << b << endl; ▪ Trả về tham chiếu đến change(b); cout << "b = " << b << endl; biến cục bộ có thể gây change(100); // lỗi những lỗi bộ nhớ } TRƯƠNG XUÂN NAM 29
  30. Quy tắc chung ▪ Chú ý: Những quy tắc này không phải luôn luôn đúng ▪ Muốn thay đổi giá trị đối số thì hãy sử dụng tham chiếu (thêm dấu & vào trước tên biến) ▪ Muốn giảm bớt yêu cầu bộ nhớ và tăng tốc độ của hàm cũng có thể sử dụng tham chiếu ▪ Muốn ngăn chặn việc vô ý thay đổi dữ liệu tham chiếu thì thêm từ khóa const vào trước tham chiếu ▪ Ví dụ: // dùng khi cần thay đổi d int change(string & d) // dùng khi cần hàm nhanh hơn int change(string & d) // dùng khi cần hàm nhanh hơn và không thay đổi d int change(const string & d) TRƯƠNG XUÂN NAM 30
  31. Phần 6 Nạp chồng hàm TRƯƠNG XUÂN NAM 31
  32. Khái niệm ▪ Nạp chồng hàm (function overloading) chỉ việc có thể viết nhiều hàm cùng tên nhau trong một chương trình ▪ Ví dụ: ta viết ba hàm đều tên là trungbinh dùng để tính trung bình cộng của 2, 3 hoặc 4 số thực double trungbinh(double a, double b) { } double trungbinh(double a, double b, double c) { } double trungbinh(double a, double b, double c, double d) { } TRƯƠNG XUÂN NAM 32
  33. Khái niệm ▪ Tất nhiên ta có thể đặt tên hàm khác đi, chẳng hạn như trungbinh2, trungbinh3 và trungbinh4 ▪ Nhưng cách làm này có tính đối phó ▪ Làm mất ý nghĩa của tên hàm: trungbinh2 có thể hiểu là trung bình bình phương? ▪ Một số ngôn ngữ lập trình không cho hàm trùng tên ▪ C/C++ thì việc này là được phép: trình dịch sẽ tự tìm ra hàm hợp lý nhất trong số các hàm trùng tên ▪ Dựa trên số lượng tham số của hàm ▪ Dựa trên kiểu dữ liệu của các tham số của hàm ▪ Không dựa trên kết quả trả về của hàm ▪ Cơ chế này gọi là tự động phân giải nạp chồng (automatic overload resolution) TRƯƠNG XUÂN NAM 33
  34. Nạp chồng hàm: tình huống đơn giản #include using namespace std; void print(int a, int b) { cout << "0" << endl; } void print(int a, double b) { cout << "1" << endl; } void print(double a, int b) { cout << "2" << endl; } int main() { print(10, 20); // print(int, int) print(0.5, 100); // print(double, int) print(5, 0.5); // print(int, double) print(1.5, 0.5); // Lỗi } TRƯƠNG XUÂN NAM 34
  35. Nạp chồng hàm: tình huống chuyển đổi kiểu #include using namespace std; void print(long a, long b) { cout << "0" << endl; } void print(long a, double b) { cout << "1" << endl; } void print(double a, long b) { cout << "2" << endl; } void print(double a, double b) { cout << "3" << endl; } int main() { print(10, 20); // Lỗi print(0.5, 100L); // print(double, long) print(5L, 0.5); // print(long, double) print(1.5, 0.5); // print(double, double) } TRƯƠNG XUÂN NAM 35
  36. Nạp chồng hàm: tham số mặc định #include using namespace std; double area(double dai, double rong = 1) { return dai * rong; } int main() { cout << area(10, 20) << endl; // dai = 10, rong = 20 cout << area(10) << endl; // dai = 10, rong = 1 } TRƯƠNG XUÂN NAM 36
  37. Nạp chồng hàm giúp thiết kế linh hoạt hơn // Nhập n rồi nhập mảng a void nhap(int & n, int a[]) { } // Nhập mảng a có n phần tử (n biết trước) void nhap(int a[], int n) { } // Nhập n, nhập mảng a rồi trả n về int nhap(int a[]) { } TRƯƠNG XUÂN NAM 37
  38. Phần 7 Hàm đệ quy TRƯƠNG XUÂN NAM 38
  39. Khái niệm đệ quy ▪ Hàm đệ quy = Hàm trong lúc thực thi có gọi lại chính nó ▪ Đệ quy trực tiếp: gọi lại chính nó ngay trong thân hàm ▪ Đệ quy gián tiếp: gọi lại chính nó thực hiện trong các hàm con ▪ Một số ngôn ngữ lập trình không cho phép đệ quy ▪ Phù hợp với lối suy nghĩ top-down (từ trên xuống), rất phổ biến trong toán học, tin học, vật lý, ▪ Chương trình viết rất ngắn ▪ Khá giống với tư duy quy nạp: ▪ Giải bài toán với trường hợp nhỏ nhất ▪ Giải bài toán lớn dựa trên lời giải từ bài toán con ▪ Chạy chậm!!! TRƯƠNG XUÂN NAM 39
  40. Ví dụ: tính n! ▪ Công thức toán: n! = 1 x 2 x x (n-1) x n ▪ Trường hợp n = 1: n! = 1 ▪ Trường hợp n > 1: biến đổi công thức ▪ n! = 1 x 2 x x (n-1) x n = (1 x 2 x x (n-1)) x n = (n-1)! x n ▪ Viết mã: // phiên bản dài dòng int giaithua(int n) { if (n == 1) return 1; return giaithua(n - 1) * n; } // phiên bản ngắn hơn int gt(int n) { return n == 1 ? 1 : gt(n - 1) * n; } TRƯƠNG XUÂN NAM 40
  41. Hàm đệ quy thực hiện như thế nào? #include ①Phát lời gọi gt(4) using namespace std; ②Vào hàm gt(n=4) int gt(int n) { ③Bỏ qua lệnh if if (n == 1) return 1; return gt(n - 1) * n; ④Tính công thức gt(3)*4 } ⑤Vào hàm gt(n=3) ⑥Bỏ qua lệnh if int main() { cout << gt(4); ⑦Tính công thức gt(2)*3 } ⑧Vào hàm gt(n=2) ⑨Bỏ qua lệnh if ⑩Tính công thức gt(1)*2 ⑪Vào hàm gt(n=1) ⑫Chạy lệnh if, trả về 1 TRƯƠNG XUÂN NAM 41
  42. Hàm đệ quy thực hiện như thế nào? #include using namespace std; int gt(int n) { if (n == 1) return 1; return gt(n - 1) * n; } int main() { cout << gt(4); } TRƯƠNG XUÂN NAM 42
  43. Phần 8 Bài tập TRƯƠNG XUÂN NAM 43
  44. Bài tập TRƯƠNG XUÂN NAM 44
  45. Bài tập TRƯƠNG XUÂN NAM 45
  46. Bài tập 푛 TRƯƠNG XUÂN NAM 46