Chương 1 đã trình bày tổng quan về các phương pháp lập trình hiện nay. Nội dung tập trung vào ba phương pháp lập trình có liên quan trực tiếp đến ngôn ngữ lập trình C++: Lập trình tuyến tính Lập trình hướng cấu trúc Lập trình hướng đối tượng. C++ là ngôn ngữ lập trình được mở rộng từ một ngôn ngữ lập trình cấu trúc C. Do đó, C++ vừa có những đặc trưng của lập trình cấu trúc, vừa có những đặc trưng của lập trình hướng đối tượng.com https://fb.com/tailieudientucntt CHƯƠNG 2 CON TRỎ VÀ MẢNG Nội dung của chương này tập trung trình bày các vấn đề cơ bản liên quan đến các thao tác trên kiểu dữ liệu con trỏ và mảng trong C++: Khái niệm con trỏ, cách khai báo và sử dụng con trỏ.
Mối quan hệ giữa con trỏ và mảng Con trỏ hàm Cấp phát bộ nhớ cho con trỏ 2.1 KHÁI NIỆM CON TRỎ 2.1 Khai báo con trỏ Con trỏ là một biến đặc biệt, nó chứa địa chỉ của một biến khác. Con trỏ có kiểu là kiểu của biến mà nó trỏ tới. Cú pháp khai báo một con trỏ như sau: <Kiểu dữ liệu> *<Tên con trỏ>; Trong đó: Kiểu dữ liệu: Có thể là các kiểu dữ liệu cơ bản của C++, hoặc là kiểu dữ liệu có cấu trúc, hoặc là kiểu đối tượng do người dùng tự định nghĩa. Tên con trỏ: Tuân theo qui tắc đặt tên biến của C++: - Chỉ được bắt đầu bằng một kí tự (chữ), hoặc dấu gạch dưới “_”.
- Bắt đầu từ kí tự thứ hai, có thể có kiểu kí tự số. - Không có dấu trống (space bar) trong tên biến. - Có phân biệt chữ hoa và chữ thường. - Không giới hạn độ dài tên biến.
Ví dụ, để khai báo một biến con trỏ có kiểu là int và tên là pointerInt, ta viết như sau: int *pointerInt; Lưu ý Có thể viết dấu con trỏ “*” ngay sau kiểu dữ liệu, nghĩa là hai cách khai báo sau là tương đương: int *pointerInt; int* pointerInt; Các cách khai báo con trỏ như sau là sai cú pháp: *int pointerInt; // Khai báo sai con trỏ 11 CuuDuongThanCong.com https://fb.com/tailieudientucntt int pointerInt*; // Khai báo sai con trỏ 2.2 Sử dụng con trỏ Con trỏ được sử dụng theo hai phép toán: Lấy địa chỉ con trỏ để thao tác Lấy giá trị của con trỏ để thao tác Lấy địa chỉ con trỏ Bản thân con trỏ sẽ được gán cho địa chỉ của một biến có cùng kiểu dữ liệu với nó. Cú pháp của phép gán như sau: <Tên con trỏ> = &<tên biến>; Lưu ý Trong phép lấy địa chỉ, tên con trỏ không có dấu “*”. Ví dụ: int x, *px; px = &x; sẽ cho con trỏ px có kiểu int trỏ vào địa chỉ của biến x có kiểu nguyên. Phép toán &<Tên biến> sẽ cho địa chỉ của biến tương ứng.
Lấy giá trị của con trỏ Phép lấy giá trị của con trỏ được thực hiện bằng cách gọi tên: *<Tên con trỏ>; Lưu ý Trong phép lấy giá trị, phải có dấu con trỏ “*”. Nếu không có dấu con trỏ, sẽ trở thành phép lấy địa chỉ con trỏ. Ví dụ: int x = 12, y, *px; px = &y; *px = x; con trỏ px vẫn trỏ tới địa chỉ biến y và giá trị của biến y sẽ là 12. Phép gán giữa các con trỏ Các con trỏ cùng kiểu có thể gán cho nhau thông qua phép gán và lấy địa chỉ con trỏ: <Tên con trỏ 1> = <Tên con trỏ 2>; Lưu ý 12 CuuDuongThanCong.com https://fb.com/tailieudientucntt Trong phép gán giữa các con trỏ, bắt buộc phải dùng phép lấy địa chỉ con trỏ (không có dấu “*” trong tên con trỏ) mà không được dùng phép lấy giá trị con trỏ.
Hai con trỏ phải cùng kiểu. Trong trường hợp hai con trỏ khác kiểu, phải dụng các phương thức ép kiểu tương tự như trong phép gán các biến thông thường có kiểu khác nhau. Ví dụ: int x = 12, px, py; px = &x; py = px; con trỏ py cũng trỏ vào địa chỉ của biến x như con trỏ px. Khi đó *py cũng có giá trị 12 giống như *px và giá trị biến x.
Chương trình 2.1 minh hoạ việc dùng con trỏ giữa các biến của một chương trình C++. Chương trình 2.h> void main(void){ int x = 12, *px, *py; cout << ”x = ” << x << endl; px = &x; // Con trỏ px trỏ tới địa chỉ của x cout << ”px = &x, *px = ” << *px << endl; *px = *px + 20; // Nội dung của px là 32 cout << ”*px = *px+20, x = ” << x << endl; py = px; // Cho py trỏ tới chỗ mà px trỏ: địa chỉ của x *py += 15; // Nội dung của py là 47 cout << ”py = px, *py +=15, x = ” << x << endl; } Trong chương trình 2.1, ban đầu biến x có giá trị 12. Sau đó, con trỏ px trỏ vào địa chỉ của biến x nên con trỏ px cũng có giá trị 12. Tiếp theo, ta tăng giá trị của con trỏ px thêm 20, giá trị của con trỏ px là 32.
Vì px đang trỏ đến địa chỉ của x nên x cũng có giá trị là 32. Tiếp theo nữa, ta cho con trỏ py trỏ và chỗ mà px đang trỏ tới, địa chỉ của biến x, nên py cũng có giá trị 32. Cuối cùng, ta tăng giá trị của con trỏ py thêm 15, py sẽ có giá trị 37. Vì py cũng đang trỏ đến địa chỉ của x nên x cũng có giá trị 37.
Do vậy, ví dụ 2.1 sẽ in ra kết quả như sau: 13 CuuDuongThanCong.com https://fb.com/tailieudientucntt x = 12 px = &x, *px = 12 *px = *px + 20, x = 32 py = px, *py += 15, x = 37 2.2 CON TRỎ VÀ MẢNG 2.1 Con trỏ và mảng một chiều Mảng một chiều Trong C++, tên một mảng được coi là một kiểu con trỏ hằng: tên của một mảng được định vị tại một vùng nhớ xác định và địa chỉ của mảng trùng với địa chỉ của phần tử đầu tiên của mảng. Ví dụ khai báo: int A[5]; thì khi đó, địa chỉ của mảng A (cũng viết là A) sẽ trùng với địa chỉ phần tử đầu tiên của mảng A (là &A[0]): A = &A[0]; Quan hệ giữa con trỏ và mảng Vì tên của mảng được coi như một con trỏ hằng, cho nên nó có thể được gán cho một con trỏ có cùng kiểu. Ví dụ khai báo: int A[5] = {5, 10, 15, 20, 25}; int *pa = A; thì con trỏ pa sẽ trỏ đến mảng A, tức là trỏ đến địa chỉ của phần tử A[0], cho nên hai khai báo sau là tương đương: pa = A; pa = &A[0]; Với khai báo này, thì địa chỉ trỏ tới của con trỏ pa là địa chỉ của phần tử A[0] và giá trị của con trỏ pa là giá trị của phần tử A[0], tức là *pa = 5; Phép toán trên con trỏ và mảng Khi một con trỏ trỏ đến mảng, thì các phép toán tăng giảm trên con trỏ sẽ tương ứng với phép dịch chuyển trên mảng. Ví dụ khai báo: int A[5] = {5, 10, 15, 20, 25}; int *pa = &A[2]; thì con trỏ pa sẽ trỏ đến địa chỉ của phần tử A[2] và giá trị của pa là: *pa = A[2] = 15.com https://fb.com/tailieudientucntt Khi đó, phép toán: pa = pa + 1; sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử tiếp theo của mảng A, đó là địa chỉ của A[3].
Tương tự, phép toán: pa = pa – 1; sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử A[1]. Lưu ý: Hai phép toán pa++ và *pa++ có tác dụng hoàn toàn khác nhau trên mảng: pa++ là thao tác trên con trỏ, tức là trên bộ nhớ, nó sẽ đưa con trỏ pa trỏ đến địa chỉ của phần tử tiếp theo của mảng. *pa++ là phép toán trên giá trị, nó tăng giá trị hiện tại của phần tử mảng lên một đơn vị. Ví dụ: int A[5] = {5, 10, 15, 20, 25}; int *pa = &A[2]; thì pa++ là tương đương với pa = &A[3] và *pa = 20.
nhưng *pa++ lại tương đương với pa = &A[2] và *pa = 15+1 = 16, A[2] = 16. Trong trường hợp: int A[5] = {5, 10, 15, 20, 25}; int *pa = &A[4]; thì phép toán pa++ sẽ đưa con trỏ pa trỏ đến một địa chỉ không xác định. Lí do là A[4] là phần tử cuối của mảng A, nên pa++ sẽ trỏ đến địa chỉ ngay sau địa chỉ của A[4], địa chỉ này nằm ngoài vùng chỉ số của mảng A nên không xác định. Tương tự với trường hợp pa=&A[0], phép toán pa-- cũng đưa pa trỏ đến một địa chỉ không xác định.
Vì mảng A là con trỏ hằng, cho nên không thể thực hiện các phép toán trên A mà chỉ có thể thực hiện trên các con trỏ trỏ đến A: các phép toán pa++ hoặc pa--là hợp lệ, nhưng các phép toán A++ hoặc A--là không hợp lệ. Chương trình 2.2a minh hoạ việc cài đặt một thủ tục sắp xếp các phần tử của một mảng theo cách thông thường. Chương trình 2.2a void SortArray(int A[], int n){ int temp; for(int i=0; i<n-1; i++) for(int j=i+1; j<n; j++) if(A[i] > A[j]){ temp = A[i]; 15 CuuDuongThanCong.com https://fb.com/tailieudientucntt A[i] = A[j]; A[j] = temp; } } Chương trình 2.2b cài đặt một thủ tục tương tự bằng con trỏ. Hai thủ tục này có chức năng hoàn toàn giống nhau.
Chương trình 2.2b void SortArray(int *A, int n){ int temp; for(int i=0; i<n-1; i++) for(int j=i+1; j<n; j++) if(*(A+i) > *(A+j)){ temp = *(A+i); *(A+i) = *(A+j); *(A+j) = temp; } } Trong chương trình 2.2b, thay vì dùng một mảng, ta dùng một con trỏ để trỏ đến mảng cần sắp xếp. Khi đó, ta có thể dùng các thao tác trên con trỏ thay vì các thao tác trên các phần tử mảng.2 Con trỏ và mảng nhiều chiều Con trỏ và mảng nhiều chiều Một câu hỏi đặt ra là nếu một ma trận một chiều thì tương đương với một con trỏ, vậy một mảng nhiều chiều thì tương đương với con trỏ như thế nào? Xét ví dụ: int A[3][3] = { {5, 10, 15}, {20, 25, 30}, {35, 40, 45} }; Khi đó, địa chỉ của ma trận A chính là địa chỉ của hàng đầu tiên của ma trận A, và cũng là địa chỉ của phần tử đầu tiên của hàng đầu tiên của ma trận A: 16 CuuDuongThanCong.com https://fb.