Bài giảng Lập trình nâng cao - Bài 7+8+9: Con trỏ và bộ nhớ trong C/C++ - Trương Xuân Nam

pdf 43 trang Gia Huy 17/05/2022 2961
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 7+8+9: Con trỏ và bộ nhớ 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_789_con_tro_va_bo_nho_trong.pdf

Nội dung text: Bài giảng Lập trình nâng cao - Bài 7+8+9: Con trỏ và bộ nhớ trong C/C++ - Trương Xuân Nam

  1. LẬP TRÌNH NÂNG CAO Bài 7+8+9: Con trỏ và bộ nhớ trong C/C++ TRƯƠNG XUÂN NAM 1
  2. Nội dung chính 1. Bộ nhớ máy tính 2. Biến và địa chỉ của biến 3. Biến con trỏ 4. Mảng và con trỏ 5. Bộ nhớ động 6. Con trỏ hàm 7. Bài tập Trương Xuân Nam - Khoa CNTT 2
  3. Phần 1 Bộ nhớ máy tính TRƯƠNG XUÂN NAM 3
  4. Các kiểu lưu trữ thông tin trên máy tính TRƯƠNG XUÂN NAM 4
  5. RAM ▪ RAM (random access memory) ▪ Một dãy các byte liên tiếp (một mảng byte khổng lồ) ▪ Mọi thứ đều nằm trên đó • Hệ điều hành • Các trình điều khiển thiết bị • Các chương trình • Mã chương trình • Hằng số, trực trị • Biến • ▪ Do tất cả đều nằm trên bộ nhớ, về lý thuyết: ▪ Có thể biết chính xác “địa chỉ” của chúng? ▪ Có thể “tóm” được chúng và đọc / ghi giá trị? TRƯƠNG XUÂN NAM 5
  6. Bộ nhớ vật lý và bộ nhớ bảo vệ TRƯƠNG XUÂN NAM 6
  7. Bộ nhớ của chương trình C/C++ TRƯƠNG XUÂN NAM 7
  8. Phần 2 Biến và địa chỉ của biến TRƯƠNG XUÂN NAM 8
  9. Biến và địa chỉ của biến ▪ Biến nằm trong bộ nhớ, nó phải nằm ở một (vài) ô nhớ nào đó, vị trí này gọi là địa chỉ (address) của biến ▪ Phép toán địa chỉ: & ▪ Trả về địa chỉ của biến ▪ Thường là một số 32 bit (tùy vào CPU, OS và kiểu chương trình) ▪ In ra màn hình ở dạng hexadecima ▪ Ví dụ: int a[] = { 1, 3, 2, 4, 2 }; cout << &a << endl; cout << &a[0] << endl; cout << &a[1] << endl; cout << (long) &a[2] << endl; ▪ Có lấy được địa chỉ của thứ khác trong bộ nhớ không? TRƯƠNG XUÂN NAM 9
  10. Phần 3 Biến con trỏ TRƯƠNG XUÂN NAM 10
  11. Biến con trỏ ▪ Con trỏ = Kết quả của phép lấy địa chỉ & ▪ Ta có thể lưu trữ kết quả này vào một biến hay không? ▪ Có, sử dụng biến có kiểu “con trỏ” ▪ Khai báo như biến bình thường, thêm dấu * trước tên biến ▪ Ví dụ: int a = 10; int *pa = &a; // con trỏ tới biến a cout << "A = " << a << endl; cout << "PA (con tro) = " << pa << endl; cout << "PA (int) = " << (int) pa << endl; ▪ Thậm chí có thể khai báo lẫn lộn: int a, *b, c, d; TRƯƠNG XUÂN NAM 11
  12. Khai báo và khởi tạo con trỏ ▪ Khai báo: như một biến thông thường, hơi rối hơn chút int *p1; // con trỏ đến giá trị int double *p2; // con trỏ đến giá trị thực bool *p3; // con trỏ đến giá trị logic int p4; // con trỏ đến con trỏ kiểu nguyên ▪ Khởi tạo: int n; int *p1 = &n; // con trỏ đến n double *p2; // con trỏ đến đâu??? bool *p3 = NULL; // con trỏ NULL ▪ Nếu không khởi tạo thì sao? Có thể gây lỗi ▪ Khởi tạo có chắc chắn an toàn? Không chắc ▪ NULL là một giá trị đặc biệt, bằng 0 (nullptr từ C++11) TRƯƠNG XUÂN NAM 12
  13. Sử dụng con trỏ ▪ Con trỏ thì có ích gì? ▪ Máy tính dùng con trỏ trong thao tác bộ nhớ, đoạn mã dùng con trỏ sẽ có tốc độ cao hơn do dễ dàng dịch thành các mã máy tương ứng (lý do ngôn ngữ lập trình C/C++ chạy nhanh) ▪ Biết địa chỉ của biến, biết biến đó nằm ở đâu trong bộ nhớ ▪ Thông qua con trỏ, có thể truy cập vào biến để đọc/ghi giá trị ▪ Phép toán truy cập với con trỏ: * ▪ Ví dụ: int n = 1, m = 2; int *p = &n; // p trỏ đến n *p = 100; // n = 100 p = &m; // p trỏ đến m *p = 200; // m = 200 TRƯƠNG XUÂN NAM 13
  14. Con trỏ làm tham số của hàm: có gì đặc biệt? #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: sử dụng con trỏ TRƯƠNG XUÂN NAM 14
  15. Quy tắc sử dụng con trỏ ▪ Con trỏ là khái niệm quan trọng và khó trong C/C++ ▪ Nhiều công ty phần mềm đánh giá mức độ thành thạo C/C++ qua khả năng hiểu và sử dụng con trỏ của ứng viên ▪ Hai phép toán đối lập: & và * ▪ Phép & trả về địa chỉ của biến ▪ Phép * trả về biến từ địa chỉ ▪ Quy tắc cặp đôi: int a, *pa = &a; ▪ *pa và a đều chỉ nội dung của biến a • *pa còn được gọi là truy cập gián tiếp vào a ▪ pa và &a đều là địa chỉ của biến a ▪ Con trỏ phải được khởi tạo, nếu chưa biết trỏ đến biến nào thì gán bằng NULL / nullptr TRƯƠNG XUÂN NAM 15
  16. Phép toán trên con trỏ ▪ Phép lấy biến (đã học): *pa ▪ Phép gán: p1 = p2 ▪ Hai con trỏ bằng nhau, trỏ đến cùng một chỗ TRƯƠNG XUÂN NAM 16
  17. Phép toán trên con trỏ ▪ Phép cộng với số nguyên: pa + n ▪ Tăng / Giảm địa chỉ, trả về con trỏ đến biến tiếp theo trong bộ nhớ (chú ý!) ▪ Giá trị n có thể âm ▪ Có thể dùng ++ , += hoặc , -= TRƯƠNG XUÂN NAM 17
  18. Phép toán trên con trỏ ▪ Tính khoảng cách giữa hai con trỏ: pa-pb ▪ Đối ngẫu với phép cộng con trỏ với số nguyên ▪ Kết quả trả về là số nguyên ▪ Là khoảng cách giữa hai con trỏ, tính bằng số lượng biến cùng kiểu với con trỏ ▪ Rất cẩn thận khi sử dụng ▪ Chú ý ví dụ sau: int a; int b; int *pa = &a, *pb = &b; cout << pb-pa << endl; short *ppa = (short *) pa; short *ppb = (short *) pb; cout << ppb-ppa << endl; TRƯƠNG XUÂN NAM 18
  19. Phép toán trên con trỏ ▪ Giữa các con trỏ có thể sử dụng các phép so sánh thông thường, vì bản chất chúng là các giá trị số ▪ So sánh địa chỉ 2 ô nhớ ▪ Thực hiện được cả 6 phép so sánh • == • != • > • >= • < • <= ▪ Không có nhiều ý nghĩa, ngoại trừ phép == và phép != TRƯƠNG XUÂN NAM 19
  20. Phần 4 Mảng và con trỏ TRƯƠNG XUÂN NAM 20
  21. Mảng và con trỏ ▪ Mảng (một chiều) và con trỏ trong C/C++ có một số đặc điểm giống nhau về cách sử dụng, thậm chí sử dụng có phần lẫn lộn ▪ Vì lý do đó nên một số tài liệu xem mảng là hằng con trỏ (tức là một con trỏ nhưng trỏ đến một vị trí cố định trong bộ nhớ), điều này không hoàn toàn chính xác ▪ Cách tốt nhất là hãy phân biệt rạch ròi giữa mảng và con trỏ, cho dù chúng có nhiều đặc điểm chung ▪ Mảng có thể gán cho con trỏ, có vẻ như chúng là một int a[5], *p1, *p2; p1 = a; // p trỏ đến đầu của a, tức a[0] p2 = &a[0]; // p trỏ đến a[0] cout << p1 << endl; // in ra địa chỉ của a cout << p2 << endl; // in ra địa chỉ của a[0], giống p1 TRƯƠNG XUÂN NAM 21
  22. Mảng và con trỏ ▪ Mặc dù có thể dùng biến mảng như biến con trỏ int a[5] = { 1, 2, 3, 4, 5 }, *p = a; cout << a[2] << endl; // 3, bình thường cout << *(a+2) << endl; // 3, dùng a như con trỏ cout << *(p+2) << endl; // 3, bình thường cout << p[2] << endl; // 3, dùng p như mảng cout << *(2+p) << endl; // 3, lạ chưa? ▪ Nhưng có vẻ C/C++ không xem hai biến là giống nhau int a[5], *p = a; cout << sizeof(a) << endl; // 20 cout << sizeof(p) << endl; // 4 ▪ Dưới đây là cái gì? int *a[5]; int p; TRƯƠNG XUÂN NAM 22
  23. Mảng và con trỏ ▪ Thậm chí mảng trong C/C++ đôi khi cũng không giống chính nó: ▪ Mảng trong hàm main là hằng số ▪ Nhưng mảng a là tham số của hàm print thì không ▪ Bạn có lời giải thích nào không? void print(int a[], int n) { for (int i = 0; i < n; i++) cout << *(a++) << " "; } int main() { int n = 9, a[] = { 6, 5, 9, -1, 100, 7, 5, 5, 2 }; print(a, n); for (int i = 0; i < n; i++) cout << *(a++) << " "; // lỗi } TRƯƠNG XUÂN NAM 23
  24. Phần 5 Bộ nhớ động TRƯƠNG XUÂN NAM 24
  25. Bộ nhớ động ▪ Cơ chế bộ nhớ: ▪ Vùng code: chứa mã thực thi ▪ Vùng data (static memory): chứa các dữ liệu được khởi tạo từ ban đầu, thường là các biến global ▪ Vùng stack: chứa các biến địa phương • Vùng này sẽ tăng giảm theo độ sâu gọi hàm (call stack) ▪ Vùng heap: chứa các biến sẽ được “cấp phát động” • Chẳng hạn như dữ liệu của vector, có thể lúc ít phần tử, lúc khác lại chứa rất nhiều phần tử TRƯƠNG XUÂN NAM 25
  26. Bộ nhớ động ▪ Cấp phát các biến trong vùng nhớ heap gọi là “cấp phát động” – khi nào cần mới yêu cầu cấp ▪ Hãy tưởng tưởng phần mềm Microsoft Word cần những biến nào để hiển thị và soạn thảo một file? ▪ Không thể biết trước được, có những file rất ít dữ liệu, có những file cực nhiều dữ liệu ▪ Trong trường hợp này phù hợp nhất là dùng cơ chế cấp phát động, xin bộ nhớ theo nhu cầu của phần mềm ▪ Có 2 cơ chế cấp phát động thông dụng ▪ Cấp phát theo khối nhớ, sử dụng các hàm cũ của C ▪ Cấp phát theo đối tượng, dùng cơ chế tạo đối tượng của C++ ▪ Một số tài liệu nói C cấp phát ở “heap” còn C++ cấp phát ở “free store”, thực chất hai vùng nhớ này là một TRƯƠNG XUÂN NAM 26
  27. Cấp phát động kiểu C: cấp theo khối nhớ ▪ Sử dụng thư viện: ▪ Bốn hàm chính: ▪ malloc(N) – cấp một khối nhớ cỡ N byte ▪ calloc(N, S) – cấp một khối nhớ cỡ N x S byte, điền số 0 vào mọi ô dữ liệu được cấp phát ▪ free(p) – hủy khối nhớ được cấp cho con trỏ p ▪ realloc(p, S) – chỉnh kích cỡ khối nhớ được cấp bởi con trỏ p thành cỡ S byte, giữ lại dữ liệu cũ đã được khởi tạo ▪ Nguyên tắc: ▪ Các hàm cấp phát trả về con trỏ void (void *) ▪ Cần chuyển kiểu để dễ sử dụng ▪ Cấp phát không thành công sẽ trả về con trỏ nullptr ▪ Cấp phát xong, không dùng thì nên hủy TRƯƠNG XUÂN NAM 27
  28. Cấp phát động kiểu C: cấp theo khối nhớ #include #include using namespace std; const int N = 100; int main() { double *ptr; ptr = (double *) malloc(N * sizeof(double)); if (ptr == NULL) cout << "Lỗi cấp phát động"; else // in ra xem dữ liệu được khởi tạo thế nào for (int i = 0; i < N; ++i) cout << *(ptr++) << endl; // thực ra không cần lắm free(ptr); } TRƯƠNG XUÂN NAM 28
  29. Cấp phát động kiểu C++: cơ chế tạo đối tượng ▪ Không cần thư viện ▪ Sử dụng các phép toán thay vì lệnh ▪ Toán tử new: tạo đối tượng (biến) ▪ Toán tử new[]: tạo mảng đối tượng ▪ Toán tử delete: hủy đối tượng ▪ Toán tử delete[]: hủy mảng đối tượng ▪ Chú ý: ▪ Cấp phát xong không dùng thì nên hủy ▪ Cấp phát mảng thì phải dùng hủy mảng ▪ Cấp phát kiểu C++ chậm hơn so với kiểu của C vì toán tử new sau khi cấp phát bộ nhớ thì luôn luôn khởi tạo giá trị mặc định cho biến TRƯƠNG XUÂN NAM 29
  30. Cấp phát động kiểu C++: cơ chế tạo đối tượng #include using namespace std; const int N = 5; int main() { double *ptr = new double [N]; if (ptr == NULL) cout << "Lỗi cấp phát động"; else // in ra xem dữ liệu được khởi tạo thế nào for (int i = 0; i < N; ++i) cout << *(ptr++) << endl; // thực ra không cần lắm delete [] ptr; } TRƯƠNG XUÂN NAM 30
  31. Phần 6 Con trỏ hàm TRƯƠNG XUÂN NAM 31
  32. Con trỏ hàm ▪ Câu hỏi ở slide 9: ngoài việc lấy địa chỉ của biến, còn có thể lấy được địa chỉ của “các thứ khác” không? ▪ Và nếu lấy được thì dùng vào việc gì? ▪ C/C++ cho phép ta lấy địa chỉ của một hàm, và lưu nó vào biến con trỏ, biến này gọi là “con trỏ hàm” ▪ Có thể gọi hàm thông qua con trỏ hàm ▪ Khai báo: (* )( ); // haingoi: con trỏ đến một hàm có 2 tham số kiểu // nguyên và trả về kiểu nguyên int (*haingoi) (int a, int b); // inketqua: con trỏ đến một hàm có tham số kiểu mảng // và số nguyên n, hàm không trả về giá trị void (*inketqua) (int a[], int n); TRƯƠNG XUÂN NAM 32
  33. Con trỏ hàm: gán qua tên hàm ▪ Sau khi khai báo xong, ta dùng con trỏ hàm như một biến con trỏ thông thường, nhưng nhận kết quả là địa chỉ một hàm, hàm này phải khớp với khai báo ở biến // khai báo hàm int foo(); double goo(); int hoo(int x); // gán thử con trỏ hàm int (*p1)() = foo; // ok int (*p2)() = goo; // lỗi, sai kiểu trả về double (*p3)() = &goo; // ok, viết thế cũng được p1 = hoo; // lỗi, sai tham số int (*p4)(int) = hoo; // ok p2 = nullptr; // ok TRƯƠNG XUÂN NAM 33
  34. Con trỏ hàm: gọi hàm qua tên biến #include using namespace std; double tong(double a, double b) { return a + b; } double tich(double a, double b) { return a * b; } int main() { double (*p) (double, double) = tong; cout << "Ket qua 1 = " << p(10, 20) << endl; p = tich; cout << "Ket qua 2 = " << (*p)(10, 20) << endl; } TRƯƠNG XUÂN NAM 34
  35. Con trỏ hàm: dùng làm tham số của hàm khác #include using namespace std; int tong(int a, int b) { return a + b; } int tich(int a, int b) { return a * b; } void inketqua(int a, int b, int (*p) (int, int)) { cout << "Ket qua = " << p(a, b) << endl; } int main() { inketqua(10, 20, tong); inketqua(10, 20, tich); } TRƯƠNG XUÂN NAM 35
  36. Con trỏ hàm: hàm trả về con trỏ hàm #include using namespace std; int tong(int a, int b) { return a + b; } int tich(int a, int b) { return a * b; } int (*chonham(int n)) (int, int) { if (n == 0) return tong; else return tich; } void inketqua(int a, int b, int (*p) (int, int)) { cout << "Ket qua = " << p(a, b) << endl; } int main() { inketqua(10, 20, chonham(1)); inketqua(10, 20, chonham(0)); } TRƯƠNG XUÂN NAM 36
  37. Phần 7 Bài tập TRƯƠNG XUÂN NAM 37
  38. Bài tập double d, *p; d = 123.45; p = &d; a. d b. &d c. *d d. *&d e. &*d f. p g. &p h. *p i. *&p j. &*p TRƯƠNG XUÂN NAM 38
  39. Bài tập int *pi; float f; char c; double *pd; a. f = *pi; b. c = *pd; c. *pi = *pd; d. *pd = f; e. pd = f; f. *pd = *pi; g. *pi = c; h. *pi = pd; TRƯƠNG XUÂN NAM 39
  40. Bài tập TRƯƠNG XUÂN NAM 40
  41. Bài tập TRƯƠNG XUÂN NAM 41
  42. Bài tập TRƯƠNG XUÂN NAM 42
  43. Bài tập TRƯƠNG XUÂN NAM 43