Con trỏ

Con trỏ là một khái niệm phức tạp nhưng lại có các tính năng vô cùng mạnh mẽ trong ngôn ngữ C/C++. Một khi nắm chắc về con trỏ thì nó sẽ đem lại những lợi ích lớn cho người sử dụng như một vài ví dụ sau đây:

  • Có thể dùng để cấp phát bộ nhớ động.
    int *p = new int[10];
  • Truyền các tham số (các đối tượng, các mảng, các chuỗi …) hiệu quả hơn (thay vì truyền giá trị của một biến, ta có thể truyền địa chỉ của biến đó).
    #include <iostream>
    using namespace std;
     
    void square(int *);
     
    int main() {
       int numb = 8;
       cout <<  "In main(): " << &numb << endl;  //Địa chỉ của biến numb là 0x22ff1c
       cout << numb << endl;   // Giá trị của biến numb là 8
       square(&numb);          // Truyền địa chỉ của biến numb vào hàm square
       cout << numb << endl;   // Giá trị của numb thay đổi và bằng 64 sau khi truyền vào hàm tính bình phương
    }
     
    void square(int * pNumb) {  // Hàm nhận tham số là một con trỏ kiểu int
       cout <<  "In square(): " << pNumb << endl;  // Giá trị của pNumb: 0x22ff1c, để ý là nó trùng với địa chỉ biến numb ở hàm main
       *pNumb *= *pNumb;      // Thao tác tính toán với con trỏ
    }
  • Giúp hàm có thể trả về nhiều hơn một giá trị
    #include <iostream>
    
    using namespace std;
    
    void cv(int* a, int* b) {
        // Hàm khai báo kiểu void và không dùng câu lệnh return để trả về kết quả mà kết quả được cập nhật trực tiếp vào các biến con trỏ a và b, như vậy ta có hai biến được thay đổi giá trị sau khi gọi hàm này
        *a = 1;
        *b = 2;
    }
    
    int main() {
        int a, b;
        a = 0, b = 0;
        cv(&a, &b);
        cout << a << " " << b << endl; //kết quả là 1 2
        return 0;
    }
  • Xây dựng các cấu trúc dữ liệu phức tạp hơn như linked list, tree, và graph.

Ưu điểm nhiều là vậy nhưng nó cũng tồn tại một số khuyết điểm như:

  • Không dễ để học và sử dụng thành thạo con trỏ
  • Làm cho code khó đọc hơn.

Chính vì những khuyết điểm trên mà con trỏ không có mặt trong một số ngôn ngữ như Java.

Để hiểu hơn về con trỏ ta xét thêm các ví dụ dưới đây.

Ví dụ 1

Ta xét biểu thức:

number = 154;

Biểu thức chỉ ra rằng số nguyên 154 được chứa trong biến ‘number’.

Chúng ta sử dụng ‘number’ làm tên của một biến và trình biên dịch tự động liên kết nó tới một địa chỉ nào đó trong bộ nhớ (giả sử là địa chỉ 5572 như ở hình bên dưới).

Với biểu thức ở trên ta có cách diễn đạt khác như sau:

(*5572) = Nội dung của ô nhớ có địa chỉ 5572 = 154

Chú ý: Nếu dấu * được gắn với một địa chỉ, nó có nghĩa là nội dung của địa chỉ đó.

Ví dụ 2

Đoạn code sau thực hiện việc đổi chỗ nội dung của hai biến. Bạn có thể đổi chỗ nội dung của biến ‘a’ và biến ‘b’ bằng cách sử dụng một biến tạm ‘t’.

t = a;
a = b;
b = t;

Ta có thể viết một hàm để thực hiện việc này như sau, và bạn hãy đoán xem nó có thực hiện được ý đồ của chúng ta hay không?

#include<iostream>
using namespace std;

void swap(int n1, int n2)
{
 int t = n1;
 n1 = n2;
 n2 = t;
}

int main() {
    int a, b;
    a = 3, b = 5;
    swap(a, b);
    cout << a << " " << b << endl;
    return 0;
}

Trong đoạn code trên, ta thấy đầu ra là “3 5” tức là nội dung của hai biến a,b không được đổi chỗ cho nhau.

Nguyên nhân:

Khi ta truyền tham số a,b vào hàm swap(), Thực chất các biến này đã được sao chép thành một phiên bản khác, độc lập với phiên bản gốc (a được sao chép thành n1, b được sao chép thành n2) . Do vậy những xử lý trong hàm swap() không hề tác động tới a,b mà chỉ tác động tới các biến cục bộ của chúng là n1 và n2, dẫn đến khi thực hiện xong hàm swap, giá trị của a và b không được thay đổi.

Như vậy làm sao để có thể đổi chỗ được nội dung của a và b?

Để có thể đổi chỗ được nội dung của a và b, ta phải giải quyết được vấn đề ở trên. Tức là khi hàm swap() được gọi, những xử lý bên trong nó phải được cập nhật vào a và b, việc này có thể thực hiện được bằng cách truyền vào địa chỉ của a và b thay vì truyền trực tiếp giá trị của a,b vào hàm swap().

Viết lại đoạn code ở ví dụ trên như sau:

#include<iostream>
using namespace std;

void swap(int* n1, int* n2)
{
 int t;
 t = *n1;
 *n1 = *n2;
 *n2 = t;
}

int main() {
    int a, b;
    a = 3, b = 5;
    swap(&a, &b);
    cout << a << " " << b << endl;
    return 0;
}

Sau khi chạy, đầu ra thu được là “5 3”. Như vậy nội dụng của a và b đã được đổi chỗ cho nhau.

Ta sẽ xem những gì thực sự xảy ra bên trong bộ nhớ khi hàm swap() được gọi bằng hình sau:

Khi vừa gọi hàm swap(&a, &b).

Trong quá trình hàm swap() thực hiện, nội dung của ô nhớ 5574 (chứa nội dung của biến b) và 5575(chứa nội dung của biến a) được đổi chỗ cho nhau.

Sau khi hàm swap() thực hiện xong:

Ở đây giá trị của ô nhớ 5574 và 5575 đã được đổi chỗ cho nhau.

Như vậy ta đã thấy được cách làm việc của con trỏ và các trường hợp mà dùng biến thông thường không thể làm được. Trong các bài học tiếp theo về các loại cấu trúc dữ liệu phức tạp hơn như Linked list, Tree, Graph bạn sẽ thấy được tầm quan trọng của con trỏ trong C/C++.

Chúng tôi luôn cố gắng để đem lại những bài học chất lượng nhất đến người đọc, nhưng cũng khó có thể những sai sót. Nếu có bất kỳ vấn đề gì mong bạn bình luận dưới bài cho chúng tôi biết. Xin cảm ơn!

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *