Con trỏ trong lập trình là 1 định nghĩa hơi khó nhằn đối với các bạn mới học về C++. Không ngoa khi nói rằng C++ khó vì có con trỏ. Tuy nhiên ví như làm chủ được con trỏ, bạn có thể hiểu và thao tác với dữ liệu trong bộ nhớ máy tính, và những kiến thức liên quan mà bạn học được thông qua con trỏ cũng rất hữu ích cho việc học các ngôn ngữ hướng đối tượng sau này như Java chẳng hạn.
Hãy cùng tìm hiểu con trỏ trong C++ là gì, quan hệ giữa con trỏ và địa chỉ trong C++, cấu trúc, vai trò và cách sử dụng con trỏ sau bài học này nhé.
I. Khai Báo Con Trỏ Trong C++
Để khai báo con trỏ trong C++, chung ta sử dụng với cấu trúc ngữ pháp sau đây:
type *p;
Trong đó type là kiểu dữ liệu của con trỏ, và p là tên con trỏ. Lưu ý là kiểu dữ liệu của con trỏ phải giống với kiểu dữ liệu của dữ liệu cần lưu địa chỉ trong con trỏ.
Ví dụ, chúng ta khai báo con trỏ tên p với kiểu int như sau:
int *p;
Lưu ý là các cách viết sau đây cũng được chấp nhận khi khai báo con trỏ trong C++:
int* p; int * p;
II. Cấp Phát Bộ Nhớ Cho Con Trỏ Trong C++
Ngôn ngữ C++ hỗ trợ ba loại cấp phát bộ nhớ cơ bản, hai loại trong số đó bạn đã được học ở các bài học trước:
1. Cấp phát bộ nhớ tĩnh (Static memory allocation):
- Xảy ra trên các biến tĩnh và biến toàn cục.
- Vùng nhớ của các loại biến này được cấp phát một lần khi chương trình bắt đầu chạy và vẫn tồn tại trong suốt thời gian tồn tại của chương trình.
- Kích thước của biến/mảng nên được biết tại thời điểm biên dịch chương trình.
2. Cấp phát bộ nhớ tự động (Automatic memory allocation):
- Xảy ra trên các tham số hàm và biến cục bộ.
- Vùng nhớ của các loại biến này được cấp phát khi chương trình đi vào khối lệnh và được giải phóng khi khối lệnh bị thoát.
- Kích thước của biến/mảng phải được biết tại thời điểm biên dịch chương trình.
3. Cấp phát bộ nhớ động (Dynamic memory allocation) sẽ được kể tới trong bài học này.
Trong tất cả những trường hợp, cấp phát bộ nhớ tĩnh và tự động có thể đáp ứng tốt các đề nghị của chương trình. Tuy nhiên, ta cùng xem ví dụ bên dưới:
Ví dụ: Chúng ta cần sử dụng một chuỗi để lưu tên của người dùng, nhưng chúng ta không biết tên của họ dài bao nhiêu cho đến khi họ nhập tên. Hoặc chúng ta cần lưu trữ danh sách nhân viên trong một công ty, nhưng chúng ta không biết trước được công ty đó sẽ có bao nhiêu nhân viên.
Đối với cấp phát bộ nhớ tĩnh và tự động, kích thước của biến/mảng phải được biết tại thời điểm biên dịch chương trình. Vì vậy, điều tốt nhất chúng ta có thể làm là cố gắng đoán một kích thước tối đa của các biến đó:
char name[30]; // tên không quá 30 ký tự Staff staff[500]; // công ty không quá 500 nhân viên
Khuyết điểm của cách khai báo trên:
+ Gây lãng phí bộ nhớ nếu các biến không thực sự sử dụng hết kích thước khi khai báo. Ví dụ: nếu công ty chỉ có 100 nhân viên, chúng ta có 400 vùng nhớ nhân viên không được sử dụng tới.
+ Thứ hai, hầu hết các biến thông thường (bao gồm mảng tĩnh) được cấp phát trong một phần bộ nhớ gọi là ngăn xếp (stack). Kích thước bộ nhớ stack cho một chương trình khá nhỏ (khoảng 1Mb với Visual Studio), nếu yêu cầu cấp phát vùng nhớ vượt quá con số này, chương trình sẽ bị đóng bởi hệ điều hành với lỗi stack overflow.
char byte[1000000 * 2]; // khoảng 2Mb bộ nhớ => lỗi stack overflow
+ Thứ ba, điều gì xảy ra nếu công ty có 600 nhân viên, trong khi mảng staff chỉ có 500 phần tử. Lúc này, chương trình sẽ bị giới hạn bởi kích thước được khai báo ban đầu.
Để giải quyết những hạn chế trên, cấp phát bộ nhớ động được ra đời.
III. Con Trỏ Trong C++ Dùng Để Làm Gì
Tác dụng của con trỏ trong c chính là để lưu giữ địa chỉ của dữ liệu trong bộ nhớ máy tính, và bằng cách truy cập vào địa chỉ này, chúng ta có thể lấy được giá trị của dữ liệu tại đó.
Ngoài ra thì giá trị của con trỏ cũng là một số, nên chúng ta cũng có thể thực hiện các phép tính toán với con trỏ, ví dụ như cộng thêm hoặc hoặc trừ đi một số lượng đơn vị.
Do đó, con trỏ trong C sẽ được dùng để làm 1 trong 2 công việc sau đây trong chương trình:
- Thao tác với địa chỉ bằng các phép tính toán với số được lưu trong nó
- Thao tác với giá trị tại địa chỉ mà nó lưu mà thôi.
IV. Mảng Con Trỏ Trong C++
Trước khi chúng ta hiểu về khái niệm mảng các con trỏ, chúng ta xem xét ví dụ sau, mà sử dụng một mảng gồm 3 số integer:
#include <iostream> using namespace std; const int MAX = 3; int main () { int var[MAX] = {10, 100, 200}; for (int i = 0; i < MAX; i++) { cout << "Gia tri cua var[" << i << "] = "; cout << var[i] << endl; } return 0; }
Khi code trên được biên dịch và thực thi, nó cho kết quả sau:
Gia tri cua var[0] = 10 Gia tri cua var[1] = 100 Gia tri cua var[2] = 200
Có một tình huống khi chúng ta muốn duy trì một mảng, mà có thể lưu giữ các con trỏ tới một kiểu dữ liệu int hoặc char hoặc bất kỳ kiểu nào khác. Sau đây là khai báo một mảng của các con trỏ tới một integer:
int *contro[MAX];
Nó khai báo contro như là một mảng các con trỏ MAX kiểu integer. Vì thế, mỗi phần tử trong contro, bây giờ giữ một con trỏ tới một giá trị int. Ví dụ sau sử dụng 3 số integer, mà sẽ được lưu giữ trong một mảng các con trỏ như sau:
#include <iostream> using namespace std; const int MAX = 3; int main () { int var[MAX] = {10, 100, 200}; int *contro[MAX]; for (int i = 0; i < MAX; i++) { contro[i] = &var[i]; // gan dia chi cua so nguyen. } for (int i = 0; i < MAX; i++) { cout << "Gia tri cua var[" << i << "] = "; cout << *contro[i] << endl; } return 0; }
Khi code trên được biên dịch và thực thi, nó cho kết quả sau:
Gia tri cua var[0] = 10 Gia tri cua var[1] = 100 Gia tri cua var[2] = 200
Bạn có thể sử dụng một mảng các con trỏ tới ký tự để lưu giữ một danh sách các chuỗi như sau:
#include <iostream> using namespace std; const int MAX = 4; int main () { char *tensv[MAX] = { "Nguyen Thanh Tung", "Tran Minh Chinh", "Ho Ngoc Ha", "Hoang Minh Hang", }; for (int i = 0; i < MAX; i++) { cout << "Gia tri cua tensv[" << i << "] = "; cout << tensv[i] << endl; } return 0; }
Chạy chương trình C++ trên sẽ cho kết quả như hình sau:
V. Con Trỏ Hàm Trong C++
Con trỏ hàm là một biến lưu trữ địa chỉ của một hàm, thông qua biến đó, ta có thể gọi hàm mà nó trỏ tới.
Cú pháp khai báo con trỏ hàm:
<kiểu trả về> (*<tên con trỏ>)(<danh sách tham số>);
int(*fcnPtr)(int); // con trỏ hàm nhận vào 1 biến kiểu int và trả về kiểu int void(*fcnPtr)(int, int); // con trỏ hàm nhận vào 2 biến kiểu int và trả về kiểu void
Chú ý: Dấu ngoặc () quanh *fcnPtr là bắt buộc.
VI. Con Trỏ Trong Class C++
Một con trỏ tới một lớp trong C++ được thực hiện theo cách giống hệt như một con trỏ tới một cấu trúc; và để truy cập các thành viên của một con trỏ tới một lớp bạn sử dụng toán tử truy cập thành viên trong C++ là toán tử ->, như khi bạn thực hiện với các con trỏ tới cấu trúc. Cũng như với tất cả con trỏ, bạn phải khai báo con trỏ trước khi sử dụng nó.
Bạn thử ví dụ sau để hiểu khái niệm con trỏ tới một lớp trong C++:
#include <iostream> using namespace std; class Box { public: // phan dinh nghia Constructor Box(double dai=1.0, double rong=1.0, double cao=1.0) { cout <<"Constructor duoc goi." << endl; chieudai = dai; chieurong = rong; chieucao = cao; } double theTich() { return chieudai * chieurong * chieucao; } private: double chieudai; // chieu dai cua mot box double chieurong; // chieu rong cua mot box double chieucao; // chieu cao cua mot box }; int main(void) { Box Box1(2.4, 4.2, 2.2); // khai bao box1 Box Box2(4.5, 2.0, 3.2); // khai bao box2 Box *controBox; // khai bao con tro toi mot class. // luu giu dia chi cua doi tuong dau tien controBox = &Box1; // bay gio thu truy cap mot thanh vien boi su dung toan tu truy cap thanh vien cout << "The tich cua Box1 la: " << controBox->theTich() << endl; // luu giu dia chi cua doi tuong thu hai controBox = &Box2; // bay gio thu truy cap mot thanh vien boi su dung toan tu truy cap thanh vien cout << "The tich cua Box2 la: " << controBox->theTich() << endl; return 0; }
Biên dịch và chạy chương trình C++ trên sẽ cho kết quả sau:
VII. Con Trỏ Trong Struct C++
Chào các bạn đang theo dõi khóa học lập trình trực tuyến ngôn ngữ C++.
Chúng ta cùng tiếp tục tìm hiểu về kiểu dữ liệu tự định nghĩa thông qua từ khóa struct mà ngôn ngữ C++ hỗ trợ. Trong bài học này, mình sẽ trình bày về kiểu struct khi sử dụng kết hợp với con trỏ.
Như các bạn đã học trong bài trước, sau khi chúng ta tự định nghĩa một struct, compiler sẽ coi tên gọi của struct đó như là một kiểu dữ liệu. Điều này có nghĩa khi chúng ta sử dụng các kiểu dữ liệu built-in để tạo ra các biến, tham chiếu hoặc con trỏ thì chúng ta cũng có thể sử dụng kiểu struct để tạo ra biến struct, tham chiếu struct và con trỏ kiểu struct (Pointer to struct).
Pointer to struct
Đầu tiên, chúng ta cùng định nghĩa một kiểu dữ liệu theo ý muốn. Dưới đây, mình định nghĩa một kiểu dữ liệu có tên là Letter:
struct Letter { };
Trong struct Letter mình chưa định nghĩa các trường dữ liệu, lúc này Letter là một kiểu dữ liệu rỗng. Nhưng ngôn ngữ C++ vẫn đặt kích thước của kiểu Letter này là 1 bytes.
Mục đích là để đảm bảo địa chỉ của 2 biến được tạo ra sẽ có địa chỉ khác nhau. Tuy nhiên, định nghĩa ra một struct rỗng không có tác dụng gì trong chương trình, chúng ta cùng thêm vào một số trường dữ liệu cho Letter:
struct Letter { char from[50]; char to[50]; };
Một lá thư sẽ có thông tin về người gửi và người nhận, nên mình thêm vào 2 trường dữ liệu kiểu C-style string dùng để lưu thông tin mà người dùng điền vào một lá thư.
Mình vừa định nghĩa xong một kiểu dữ liệu mới để phục vụ cho chương trình của mình. Bây giờ chúng ta cùng tạo ra một đơn vị từ kiểu dữ liệu trên (mình thao tác luôn trong hàm main):
int main() { Letter myLetter; return 0; }
Với mỗi biến kiểu Letter được tạo ra, chương trình sẽ yêu cầu cấp phát 100 bytes (50 bytes cho trường dữ liệu from và 50 bytes cho trường dữ liệu to), và chắc chắn rồi, biến đó sẽ có một địa chỉ xác định được thông qua toán tử address-of.
int main() { Letter myLetter; std::cout << "Address of myLetter: " << &myLetter << std::endl; std::cout << "Address of from field: " << &myLetter.from << std::endl; return 0; }
Ở đoạn chương trình trên, mình in ra địa chỉ của biến myLetter, đồng thời in ra luôn địa chỉ của trường dữ liệu from của biến myLetter. Kết quả cho thấy 2 địa chỉ được in ra có giá trị hoàn toàn giống nhau. Điều này có nghĩa địa chỉ của trường dữ liệu đầu tiên trong một biến struct cũng là địa chỉ của biến struct đó.
Các bạn có thể liên hệ struct với mảng một chiều trong C/C++, khi mảng một chiều mà tập hợp các phần tử có cùng kiểu dữ liệu được bao bọc bởi tên mảng một chiều và địa chỉ của mảng một chiều cũng là địa chỉ của phần tử đầu tiên trong mảng, một biến struct sẽ bao gồm tập hợp các trường dữ liệu mà địa chỉ của biến struct sẽ là địa chỉ của trường dữ liệu được khai báo đầu tiên trong struct.
Và như các bạn cũng đã học về con trỏ (Pointer), kiểu dữ liệu của con trỏ dùng để xác định kiểu dữ liệu của vùng nhớ mà con trỏ có thể trỏ đến. Vậy thì để cho con trỏ trỏ đến một địa chỉ của biến kiểu struct, chúng ta cần có một con trỏ cùng kiểu struct với biến được trỏ đến.
Letter myLetter; Letter *pLetter = &myLetter;
Dù kích thước của kiểu dữ liệu struct có lớn bao nhiêu, biến con trỏ cũng chỉ có kích thước 4 bytes trên hệ điều hành 32 bits và kích thước 8 bytes trên hệ điều hành 64 bits (đủ để trỏ đến toàn bộ địa chỉ trên bộ nhớ ảo).
Access struct members
Trong bài học trước, các bạn đã biết cách truy cập đến các trường dữ liệu của các biến struct thông qua member selection operator (dấu chấm). Nhưng khi sử dụng Pointer to struct, member selection operator được sử dụng dưới cách viết khác. Để phân biệt sự khác nhau khi sử dụng member selection operator cho biến struct thông thường và một Pointer to struct, các bạn cùng xem ví dụ bên dưới:
struct BankAccount { __int64 accountNumber; __int64 balance; }; int main() { BankAccount myAccount = { 123456789, 50 }; // $50 BankAccount *pAccount = &myAccount; std::cout << "My bank account number: " << myAccount.accountNumber << std::endl; std::cout << "My bank account number: " << pAccount->accountNumber << std::endl; std::cout << "My balance: " << myAccount.balance << std::endl; std::cout << "My balance: " << pAccount->balance << std::endl; return 0; }
Như các bạn thấy, kết quả của việc truy xuất giá trị thông qua tên biến struct và con trỏ kiểu struct là hoàn toàn giống nhau, và chúng đều dùng toán tử member selection. Tuy nhiên, để phân biệt biến con trỏ và biến thông thường, biến con trỏ kiểu struct sẽ truy cập đến các trường dữ liệu trong vùng nhớ bằng toán tử (->). Hai toán tử này cùng tên, chỉ khác nhau về cách biểu diễn.
Một số nhầm lần khi sử dụng struct và Pointer to struct
Khi mới tìm hiểu về Pointer to struct, các bạn có thể bị nhầm lẫn giữa cách khởi tạo hoặc gán giá trị cho biến struct thông thường và biến con trỏ struct.
struct BankAccount { __int64 accountNumber; __int64 balance; }; int main() { BankAccount myAccount = { 12345, 50 }; BankAccount *pAccount = { 12345, 50 }; //error return 0; }
Đoạn chương trình trên báo lỗi vì biến con trỏ chỉ nhận giá trị là địa chỉ. Tuy nhiên, lỗi này có thể thấy dễ dàng vì Visual studio đưa ra thông báo lỗi ngay. Dưới đây là cách gán giá trị đúng khi mình sử dụng toán tử dereference cho biến con trỏ struct để thay đổi giá trị bên trong vùng nhớ:
struct BankAccount { __int64 accountNumber; __int64 balance; }; int main() { BankAccount myAccount = { 0, 0 }; BankAccount *pAccount = &myAccount; *pAccount = { 12345, 50 }; std::cout << pAccount->accountNumber << " " << pAccount->balance << std::endl; return 0; }
Hoặc một cách khác là chúng ta cấp phát vùng nhớ cho biến con trỏ struct, và dereference đến đó để gán giá trị cho nó:
BankAccount *pAccount = new BankAccount; *pAccount = { 12345, 50 };
Và các bạn lưu ý khi sử dụng biến kiểu con trỏ struct thì chúng ta sử dụng toán tử member selection này (->). Có một số bạn nhầm lẫn giữa biến con trỏ struct và trường dữ liệu kiểu con trỏ. Ví dụ:
struct BankAccount { char *name; __int64 accountNumber; __int64 balance; };
Mình thêm vào struct một trường dữ liệu kiểu con trỏ char nhưng việc truy xuất đến trường dữ liệu này không có gì thay đổi khi mình sử dụng biến struct thông thường.
BankAccount myAccount = { "Le Tran Dat", 12345, 50 }; std::cout << myAccount.name << std::endl; std::cout << myAccount.accountNumber << std::endl; std::cout << myAccount.balance << std::endl;
Sẽ phức tạp hơn một chút khi các bạn sử dụng các nested struct. Ví dụ:
struct BankAccount { Date registrationDate; __int64 accountNumber; __int64 balance; }; int main() { BankAccount *pAccount = new BankAccount; *pAccount = { {2, 5, 2016}, 12345, 50 }; std::cout << pAccount->registrationDate.year << std::endl; return 0; }
Như các bạn thấy, từ biến con trỏ pAccount truy xuất vào các trường dữ liệu bên trong thì mình dùng toán tử (->), nhưng trường dữ liệu Date trong struct BankAccount là biến thông thường, nên mình dùng dấu chấm để truy xuất dữ liệu ngày đăng kí.
VIII. Con Trỏ Null Trong C++
Con trỏ NULL trong C++ là một hằng với một giá trị là 0 được định nghĩa trong một vài thư viện chuẩn, gồm iostream.
#include <iostream> using namespace std; int main () { int *ptr = NULL; cout << "Gia tri cua contro la " << ptr ; return 0; }
Kết quả:
Gia tri cua contro la 0
Trên hầu hết các hệ điều hành, các chương trình không được phép truy cập bộ nhớ tại địa chỉ 0, vì bộ nhớ đó được dự trữ bởi hệ điều hành. Tuy nhiên, địa chỉ bộ nhớ 0 có ý nghĩa đặc biệt, nó chỉ ra rằng con trỏ không được trỏ tới một vị trí ô nhớ có thể truy cập. Nhưng theo qui ước, nếu một con trỏ chứa giá trị 0, nó được xem như là không trỏ tới bất cứ thứ gì.
Để kiểm tra một con trỏ null trong C++, bạn có thể sử dụng lệnh if như sau:
if(contro) // true neu contro khong la NULL if(!contro) // true neu contro la NULL
IX. Nhập Xuất Mảng Bằng Con Trỏ Trong C++
Bằng cách sử dụng con trỏ mảng, chúng ta có thể chỉ định vị trí các phần tử trong mảng, cũng như là truy cập và lấy giá trị của các phần tử đó.
Ứng dụng điều này, chúng ta cũng có thể nhập xuất mảng bằng con trỏ trong C++ như sau.
Nhập mảng bằng con trỏ trong C++
Trong bài Nhập xuất mảng trong C++ chúng ta đã biết cách tạo hàm nhập trực tiếp các giá trị từ bàn phím vào mảng như sau:
/*Tạo hàm nhập mảng 1 chiều trong C++*/ void input_array(int array[], int length){ //array: tên mảng //length: độ dài mảng for (short i = 0; i < length; i++) cin >> array[i]; }
Trong đó array và length lần lượt là tên và độ dài (số phần tử) của mảng cần nhập.
Để nhập mảng bằng con trỏ trong C++ chúng ta chỉ cần sử dụng giá trị con trỏ thay cho mảng, và thay vì dùng index để chỉ định vị trí nhập dữ liệu, thì chúng ta sẽ sử dụng trực tiếp giá trị con trỏ để chỉ định vị trí cần nhập. Chúng ta viết hàm nhập mảng bằng con trỏ trong C++ như sau:
/*Tạo hàm nhập mảng bằng con trỏ trong C++*/ void input_array(int array[], int length){ //array: tên mảng //length: độ dài mảng for (short i = 0; i < length; i++) cin >> *(array + i); }
Lưu ý tại đây chúng ta sử dụng lệnh cin để nhập dữ liệu vào mảng, nên cần chỉ định bản thân dữ liệu mà con trỏ chỉ đến bằng cách thêm dấu hoa thị * trước con trỏ.
Tuy nhiên nếu sử dụng hàm scanf được kế thừa từ ngôn ngữ C để nhập mảng bằng con trỏ trong C++, thay vì chỉ định dữ liệu thì chúng ta cần chỉ định địa chỉ của dữ liệu đã được lưu vào con trỏ như sau:
/*Tạo hàm nhập mảng bằng con trỏ trong C*/ void input_array(int *array, int length){ //array: tên mảng //length: độ dài mảng for (short i = 0; i < length; i++) scanf("%d", (array + i)); }
Xuất mảng bằng con trỏ trong C++
Trong bài xuất xuất mảng trong C++ chúng ta đã biết cách tạo hàm xuất trực tiếp các giá trị của mảng như sau:
/*Tạo hàm xuất mảng 1 chiều trong C++*/ void show_array(int array[], int length){ //array: tên mảng //length: độ dài mảng for(short i = 0; i < length; i++) cout << array[i] <<' '; cout << endl; }
Trong đó array và length lần lượt là tên và độ dài (số phần tử) của mảng cần xuất.
Để xuất mảng bằng con trỏ trong C++ chúng ta chỉ cần sử dụng con trỏ thay cho mảng, và thay vì truy xuất giá trị các phần tử của mảng bằng index thì chúng ta sẽ dùng tên con trỏ và dấu hoa thị để xuất giá trị đó. Chúng ta viết hàm xuất mảng bằng con trỏ trong C++ như sau:
/*Tạo hàm xuất mảng bằng con trỏ trong C++*/ void show_array(int array[], int length){ //array: tên mảng //length: độ dài mảng for(short i = 0; i < length; i++) cout << *(array + i) <<' '; cout << endl; }
Chương trình mẫu nhập xuất mảng bằng con trỏ trong C++
Dưới đây là chương trình mẫu sử dụng các hàm trên để nhập xuất mảng bằng con trỏ trong C++:
#include <iostream> using namespace std; /*Tạo hàm nhập mảng bằng con trỏ trong C++*/ void input_array(int array[], int length){ //array: tên mảng //length: độ dài mảng for (short i = 0; i < length; i++) cin >> *(array + i); } /*Tạo hàm xuất mảng bằng con trỏ trong C++*/ void show_array(int array[], int length){ //array: tên mảng //length: độ dài mảng for(short i = 0; i < length; i++) cout << *(array + i) <<' '; cout << endl; } int main(){ /*Nhập mảng bằng con trỏ trong C++*/ int n; cout << ">>Nhap so phan tu: "; cin >> n; int array[n], *p; cout << (">>Nhap phan tu:\n"); input_array(array, n); /*Xuất mảng bằng con trỏ trong C++*/ cout << (">>Mang vua nhap:\n"); show_array(array, n); }
Kết quả:
>>Nhap so phan tu: 5 >>Nhap phan tu: 1 2 3 4 5 >>Mang vua nhap: 1 2 3 4 5
X. Con Trỏ Và Tham Chiếu Trong C++
C và C++ hỗ trợ con trỏ cái mà khác với hầu hết các ngôn ngữ lập trình khác. Các ngôn ngữ khác bao gồm C ++, Java, Python, Ruby, Perl và PHP đều hỗ trợ tham chiếu.
Nhìn bề ngoài, cả hai tham chiếu và con trỏ đều rất giống nhau, cả hai đều được sử dụng để có một biến cung cấp quyền truy cập cho một biến khác. Với việc cả hai đều cung cấp nhiều khả năng giống nhau, người ta thường không rõ điều gì khác biệt giữa các cơ chế khác nhau này. Trong bài viết này, tôi sẽ cố gắng minh họa sự khác biệt giữa con trỏ và tham chiếu.
Con trỏ: Con trỏ là một biến chứa địa chỉ bộ nhớ của một biến khác. Một con trỏ cần được deference với toán tử * để truy cập vào vị trí bộ nhớ mà nó trỏ tới.
Tham chiếu: Một biến tham chiếu là một bí danh, tức là một tên khác của một biến đã tồn tại. Một tham chiếu, giống như một con trỏ, cũng được hiện thựcbằng cách lưu trữ địa chỉ của một đối tượng.
Một tham chiếu có thể được coi là một con trỏ hằng (đừng nhầm với một con trỏ đến một giá trị không đổi!) với tính năng tự động chuyển hướng, tức là trình biên dịch sẽ áp dụng toán tử * cho bạn.
int i = 3; // Một con trỏ đến biến i // (lưu trữ địa chỉ của i) int *ptr = &i; // Một tham chiếu (bí danh) của i. int &ref = i;
Sự khác nhau giữa Con trỏ và Tham chiếu
Khởi tạo
Một con trỏ có thế được khai báo và khởi tạo đồng thời hoặc trên nhiều dòng.
C++ int a = 10; int *p = &a; hay int *p; p = &a;
Trong khi tham chiếu phải được khởi tạo cùng lúc với khai báo.
int a=10; int &p=a; //OK nhưng int &p; p=a; //NOK
Chú ý: Sự khác biệt này có thể thay đổi từ trình biên dịch này sang trình biên dịch khác. Sự khác biệt trên là đối với Turbo IDE.
Gán lại giá trị
Một con trỏ có thể được gán lại. Thuộc tính này hữu ích cho việc triển khai các cấu trúc dữ liệu như danh sách liên kết, cây, v.v. Xem ví dụ sau:
int a = 5; int b = 6; int *p; p = &a; p = &b;
Trong khi đó, tham chiếu không gán lại mà phải được gán ngay lúc khởi tạo.
int a = 5; int b = 6; int &p = a; int &p = b; //Tại dòng này sẽ hiện lỗi là "không được khai báo nhiều lần" //Tuy nhiên, lệnh như dưới đây thì ok. int &q=p;
Địa chỉ bộ nhớ
Một con trỏ có địa chỉ bộ nhớ và kích thước riêng của nó trên ngăn xếp trong khi một tham chiếu chia sẻ cùng địa chỉ bộ nhớ (với biến gốc) nhưng cũng chiếm một số không gian trên ngăn xếp.
int &p = a; cout << &p << endl << &a; // Địa chỉ của p và a là như nhau
Giá trị NULL
Con trỏ có thể được gán NULL trực tiếp, trong khi tham chiếu không thể. Các ràng buộc liên quan đến tham chiếu (không NULL, không gán lại) đảm bảo rằng các hoạt động cơ bản không rơi vào trường hợp ngoại lệ.
Con trỏ đến con trỏ
Bạn có thể có các con trỏ đến con trỏ cung cấp nhiều cấp độ chuyển hướng bổ sung. Trong khi đó, các tham chiếu chỉ cung cấp một mức chuyển hướng.
//Con trỏ, int a = 10; int *p; int **q; //Con trỏ đến con trỏ. p = &a; q = &p; //Đối với tham chiếu, int &p = a; int &&q = p; //Lỗi, không có tham chiếu của tham chiếu.
Các phép tính toán học
Các phép toán số học khác nhau có thể được thực hiện trên con trỏ trong khi không có thứ gọi là Số học tham chiếu. (Nhưng bạn có thể lấy địa chỉ của một đối tượng được trỏ bởi một tham chiếu và thực hiện số học con trỏ trên đó như &obj + 5)
Khi nào sử dụng cái nào
Hiệu suất hoàn toàn giống nhau, vì các tham chiếu được triển khai bên trong dưới dạng con trỏ. Tuy nhiên, bạn vẫn có thể ghi nhớ một số điểm để quyết định khi nào sử dụng cái gì:
Sử dụng tham chiếu:
- Trong các tham số hàm và kiểu trả về.
Sử dụng con trỏ:
- Sử dụng con trỏ nếu cấn tính toán con trỏ số học hoặc NULL. Ví dụ đối với mảng (Lưu ý rằng truy cập mảng được thực hiện bằng cách sử dụng số học con trỏ).
- Để triển khai các cấu trúc dữ liệu như danh sách liên kết, cây, v.v. và các thuật toán của chúng vì để trỏ ô khác nhau, chúng ta phải sử dụng khái niệm con trỏ.
XI. Ý Nghĩa Con Trỏ This Trong C++
Con trỏ this trong C++
This là một con trỏ đặc biệt dùng để trỏ đến địa chỉ của đối tượng hiện tại. Như vậy để truy cập đến các thuộc tính, phương thức của đối tượng hiện tại thì ta sẽ sử dụng con trỏ this. Hãy xem ví dụ dưới đây.
Ví dụ
#include <iostream> using namespace std; class NhanVien { int msnv; string ten; int tuoi; public: void setData(int msnv, string ten, int tuoi) { this->msnv = msnv; this->ten = ten; this->tuoi = tuoi; } void showData() { cout << "Ten nhan vien: " << this->ten << endl; cout << "Ma so nhan vien: " << this->msnv << endl; cout << "Tuoi: " << this->tuoi << endl; } };
Trong ví dụ này mình đã tạo ra ba thuộc tính để lưu trữ thông tin của một nhân viên đó là: manv, ten, tuoi. Ngoài ra mình có tạo thêm phương thức setData() dùng để gán dữ liệu cho sinh viên, và showData() dùng để hiển thị dữ liệu.
Trong phương thức setData() mình đã sử dụng từ khóa this->ten_thuoc_tinh để thực hiện phép gán dữ liệu cho các thuộc tính, còn ở phương thức showData() mình cũng sử dụng cú pháp tương tự để hiển thị dữ liệu của các thuộc tính. Như vậy công dụng của từ khóa this chính là một con trỏ và trỏ đến địa chỉ của đối tượng hiện tại.
Câu hỏi đặt ra là đối tượng hiện tại tại là gì? Để hiểu rõ hơn thì hãy xem đoạn code sử dụng class trên như sau:
Ví dụ
int main() { // Nhan vien 1 NhanVien n1 = NhanVien(); n1.setData(111231, "Nguyen Van A", 24); n1.showData(); // Nhan vien 2 NhanVien n2 = NhanVien(); n2.setData(111232, "Nguyen Van B", 25); n2.showData(); return 0; }
Trong ví dụ này mình đã tạo ra hai đối tượng sinh viên đó là n1 và n2, và con trỏ this của n1 sẽ trỏ đến chính đối tượng n1, con trỏ this của n2 sẽ trỏ đến chính đối tượng n2, đây ta gọi là đối tượng hiện tại.
Lưu ý: Trong các phương thức bình thường (không phải hàm khởi tạo) nếu bạn sử dụng tên của biến thì sẽ có hai trường hợp xảy ra.
- Nếu biến đó không tôn tại trong phương thức mà nó lại trùng với tên thuộc tính thì mặc nhiên nó sẽ hiểu đó là thuộc tính.
- Nếu biến đó có khai báo trong phương thức thì ta sẽ hiểu đó là biến bình thường, không phải là thuộc tính.
Một ví dụ khác về con trỏ this
Bạn hãy xem ví dụ dưới đây, đây là một ví dụ mình viết lại ở phần 1 và có một chút thay đổi.
Ví dụ
#include <iostream> using namespace std; class NhanVien { int msnv; string ten; int tuoi; public: NhanVien(int msnv, string ten, int tuoi) { cout << "Trong ham xay dung: " << endl; cout << " msnv: " << msnv << endl; cout << " ten: " << ten << endl; cout << " Tuoi: " << tuoi << endl; msnv = msnv; ten = ten; tuoi = tuoi; } void HienThi() { cout << "Ham in thong tin cua doi tuong nhan vien: " << endl; cout << ten << endl; cout << " Ma so nhan vien: " << msnv << endl; cout << " Tuoi: " << tuoi << endl; } }; int main() { NhanVien n1 = NhanVien(111231, "Nguyen Van A", 25); n1.HienThi(); return 0; }
XII. Con Trỏ Void Trong C++
Để tìm hiều về con trỏ void trong C++, trước hết bạn cần phải nắm vững các kiến thức cơ bản về con trỏ void. Đừng lo lắng vì Techacademy đã chuẩn bị cho bạn trong các bài viết sau đây:
Con trỏ void trong C++ là gì
Trong số các kiểu con trỏ, bạn có thể xác định một con trỏ hơi khác thường được gọi là con trỏ void.
Giống như các loại con trỏ khác trong C++ thì con trỏ void cũng được sử dụng để lưu trữ địa chỉ của một dữ liệu trong bộ nhớ máy tính.
Tuy nhiên điều đặc biệt ở đây là, với kiểu dữ liệu mà con trỏ void lưu giữ địa chỉ thì chương trình có thể truy cập đến địa chỉ của dữ liệu đó, nhưng không xác định được kiểu của nó. Nói cách khác thì con trỏ void được sử dụng để lưu giữ địa chỉ của các kiểu dữ liệu không tồn tại kiểu dữ liệu.
Khai báo con trỏ void trong C++
Cách khai báo con trỏ void trong C++ cũng tương tự như với các loại con trỏ khác, chúng ta viết kiểu void, rồi dấu hoa thị *, và cuối cùng là tên con trỏ void như sau:
void * pdata;
Cách viết này cũng tương tự như với các kiểu con trỏ int hay char chẳng hạn:
char * pCharData; // Con trỏ kiểu char int * pIntData; // Con trỏ kiểu int
Chúng ta cũng có thể thực hiện các thao tác như gán địa chỉ vào con trỏ, hoặc là in địa chỉ được gán vào con trỏ void tương tự như các loại con trỏ khác trong C++. Ví dụ:
#include <iostream> using namespace std; int main() { char data = 'A'; //Khai báo biến data void * pdata = &data; // Khai báo con trỏ void và gán địa chỉ của biến data vào con trỏ cout << "pointer: "<< pdata;//In địa chỉ mà con trỏ lưu giữ return 0; } // pointer: 0x7ffe529fddbf
Tuy nhiên, không giống như với các kiểu con trỏ khác thì chúng ta lại không thể thực hiện các thao tác với giá trị của biến mà con trỏ void trỏ đến, và vì thế cũng không thể biết được kiểu của dữ liệu đó là gì. Ví dụ, chúng ta không thể đọc được giá trị của biến thông qua con trỏ, vì lỗi sau đây sẽ xảy ra:
#include <iostream> using namespace std; int main() { char data = 'A'; //Khai báo biến data void * pdata = &data;// Khai báo con trỏ void và gán địa chỉ của biến data vào con trỏ cout << "data: "<< *pdata;//In giá trị tại địa chỉ mà con trỏ lưu giữ return 0; }
Vậy chẳng phải con trỏ void trong C++ rất là vô dụng hay sao? Tất nhiên là không phải rồi, vì chúng ta sẽ cần tới con trỏ void trong các trường hợp như dưới đây:
Sử dụng con trỏ void trong C++
Con trỏ void trong C++ sẽ được sử dụng trong các trường hợp đặc biệt sau đây:
Con trỏ vạn năng giúp lưu giữ tất cả các loại giữ liệu trong C++
Một điều dễ hiểu là do con trỏ void không tồn tại kiểu của dữ liệu mà nó đang chỉ đến, nên nó có khả năng chấp nhận và lưu giữ địa chỉ của tất cả các loại giữ liệu khác nhau trong C++. Đây là điều mà các con trỏ khác trong C++ không làm được. Ví dụ như con trỏ kiểu int thì chỉ chấp nhận lưu địa chỉ của dữ liệu kiểu int, còn con trỏ kiểu char thì cũng chỉ có thể chấp nhận lưu giữ địa chỉ của kiểu giữ liệu char. Nhưng với con trỏ void, void chấp hết ^_ .
Ví dụ cụ thể, con trỏ void trong C++ dưới đây có thể lưu giữ địa chỉ của tất cả các loại giữ liệu mà không sợ lỗi xảy ra trong chương trình.
#include <iostream> using namespace std; int main(){ //Khai báo biến char data1; short data2; long data3; double data4; // Khai báo con trỏ void void * pdata; //Gán địa chỉ của các loại dữ liệu với nhiều kiểu vào con trỏ void pdata = &data1; // pdata --> data1 pdata = &data2; // pdata --> data2 pdata = &data3; // pdata --> data3 pdata = &data4; // pdata --> data4 return 0; }
Con trỏ giúp cố ý ẩn kiểu dữ liệu
Vì con trỏ C++ không cho phép chúng ta đọc kiểu dữ liệu cũng như truy cập vào dữ liệu tại địa chỉ mà nó lưu giữ, nên con trỏ void có vai trò vô cùng quan trong khi chúng ta muốn ẩn kiểu dữ liệu nào đó trong chương trình.
Đây là một thuật toán vô cùng phức tạp đòi hỏi lượng kiến thức khá cao, chỉ dành cho các bạn thực sự pro và muốn tìm hiểu sâu về C++ mà thôi.
XIII. Bài Tập Về Con Trỏ Trong C++
Trong chủ đề này, chúng ta cùng làm một số bài tập về Con trỏ trong C++.
Bài tập 1
Sử dụng con trỏ trong C++, bạn hãy viết một chương trình C++ để nhận dữ liệu từ người dùng và tìm giá trị lớn nhất của một tập dữ liệu nội bộ.
Lời giải
Dưới đây là chương trình C++ để giải bài tập trên. Mình sử dụng một hàm mà nhận mảng các giá trị dữ liệu và kích cỡ của nó. Hàm này trả về con trỏ mà trỏ tới giá trị lớn nhất.
#include<iostream> #include<conio.h> using namespace std; int *findMax(int arr[],int n); int main(){ int n,i,*p; cout<<"Nhap so du lieu: "; cin>>n; int arr[n]; for(i=0;i<n;i++) { cout<<"Nhap gia tri thu "<<i+1<<" la :"; cin>>arr[i]; } p=findMax(arr,n); cout<<"Gia tri lon nhat la: "<<*p; getch(); return 0; } int *findMax(int data[],int n){ int *max=data; int i; for(i=1;i<n;i++){ if(*max<*(max+i)) *max=*(max+i); } return max; }
Chạy chương trình C++ trên sẽ cho kết quả như hình sau:
Bài tập 2
Viết một chương trình C++ để nhận 5 giá trị nguyên từ bàn phím. 5 giá trị này sẽ được lưu trữ trong một mảng bởi sử dụng một con trỏ. Sau đó, in các phần tử của mảng trên màn hình.
Lời giải
Dưới đây là chương trình C++ để giải bài tập trên.
#include<iostream> #include<conio.h> using namespace std; int main() { int arr[5],i; int *p=arr; cout<<"Nhap 5 so:"; cin>>*p>>*(p+1)>>*(p+2)>>*(p+3)>>*(p+4); cout<<"Cac so ban vua nhap la:\n"; for(i=0;i<5;i++) cout<<arr[i]<<endl; return 0; }
Chạy chương trình C++ trên sẽ cho kết quả như hình sau:
Sửa đổi lời giải trên để in các phần tử của mảng theo thứ tự đảo ngược bởi sử dụng một con trỏ.
#include<iostream> #include<conio.h> using namespace std; int main() { int arr[5],i; int *p=arr; cout<<"Nhap 5 so:"; cin>>*p>>*(p+1)>>*(p+2)>>*(p+3)>>*(p+4); cout<<"Cac so ban vua nhap theo thu tu dao nguoc la:\n"; for(i=4;i>=0;i--) cout<<*(p+i)<<endl; return 0; }
Chạy chương trình C++ trên sẽ cho kết quả như hình sau:
XIV. Ép Kiểu Con Trỏ Trong C++
Ép kiểu trong C++ là việc gán giá trị của một biến có kiểu dữ liệu này tới biến khác có kiểu dữ liệu khác.
Cú pháp:
(type) value;
Ví dụ:
float c = 35.8f; int b = (int)c + 1;
Trong ví dụ trên, đầu tiên giá trị dấu phảy động c được đổi thành giá trị nguyên 35. Sau đó nó được cộng với 1 và kết quả là giá trị 36 được lưu vào b.
Phân loại ép kiểu trong C++
Trong C++, có hai loại ép kiểu dữ liệu:
- Nới rộng (widening): Là quá trình làm tròn số từ kiểu dữ liệu có kích thước nhỏ hơn sang kiểu có kích thước lớn hơn. Kiểu biến đổi này không làm mất thông tin.
- Thu hẹp (narrowwing): Là quá trình làm tròn số từ kiểu dữ liệu có kích thước lớn hơn sang kiểu có kích thước nhỏ hơn. Kiểu biến đổi này có thể làm mất thông tin
1. Nới rộng (widening)
Nới rộng (widening): Là quá trình làm tròn số từ kiểu dữ liệu có kích thước nhỏ hơn sang kiểu có kích thước lớn hơn. Kiểu biến đổi này không làm mất thông tin. Ví dụ chuyển từ int sang float. Chuyển kiểu loại này có thế được thực hiện ngầm định bởi trình biên dịch.
Ví dụ:
#include <iostream> using namespace std; int main() { int i = 100; long l = i; // khong yeu cau chi dinh ep kieu float f = l; // khong yeu cau chi dinh ep kieu cout << "Gia tri Int: " << i << endl; cout << "Gia tri Long: " << l << endl; cout << "Gia tri Float: " << f << endl; return 0; }
Kết quả:
Giá trị Int: 100 Giá trị Long: 100 Giá trị Float: 100
2.Thu hẹp (narrowwing)
Thu hẹp (narrowwing): Là quá trình làm tròn số từ kiểu dữ liệu có kích thước lớn hơn sang kiểu có kích thước nhỏ hơn. Kiểu biến đổi này có thể làm mất thông tin như ví dụ ở trên. Chuyển kiểu loại này không thể thực hiện ngầm định bởi trình biên dịch, người dùng phải thực hiện chuyển kiểu tường minh.
Ví dụ:
#include <iostream> using namespace std; int main() { double d = 100.04; long l = (long) d; // yeu cau chi dinh kieu du lieu (long) int i = (int) l; // yeu cau chi dinh kieu du lieu (int) cout << "Gia tri Double: " << d << endl; cout << "Gia tri Long: " << l << endl; cout << "Gia tri Int: " << i << endl; return 0; }
Kết quả:
Giá trị Double: 100.04 Giá trị Long: 100 Giá trị Int: 100
0 / 5 - (0 Đánh Giá)