Nói về con trỏ [P. cuối]

Các bài viết trong loạt bài “Nói về con trỏ”

Kết thúc phần 1, mình đã trình bày những thứ cơ bản về con trỏ trong C và theo mình thì nhiêu đó cũng đủ để sử dụng rồi. Ở phần 2 này, mình sẽ tiếp tục về con trỏ trong C++. Tuy nhiên, ở phần này, mình chỉ nêu ra một số điểm khác biệt giữa con trỏ trong C++ và C mà không trình bày chi tiết nữa.

Về bố cục, ở phần này sẽ phân chia giống ở phần 1 để bạn tiện theo dõi và so sánh.

Nếu bạn chưa đọc phần 1 hay vẫn còn điểm gì chưa rõ, hãy quay lại ngâm cứu phần 1 một chút, vì đó là những kiến thức cơ bản và sẽ được sử dụng lại trong phần này.

Và nếu bạn đã sẵn sàng, còn chờ gì nữa, bắt đầu nào.

Phần 2: Con trỏ trong C++

1.1. Khai báo và khởi tạo con trỏ

Mọi thứ trong phần này thực sự giống với trong C nên chẳng có gì đế nói cả.

Tuy nhiên, vẫn có một bổ sung nho nhỏ.

Trong C, việc cấp phát vùng nhớ thực sự lưu giá trị của 1 con trỏ được thực hiện thế này.

int *a = NULL;
a = (int*)malloc(sizeof(int));

Hơi dài dòng, khó nhớ nhỉ? Đừng lo, C++ có thêm một cách nữa cho bạn đây.

int *a = NULL;
a = new int;

Đơn giản và dễ nhớ hơn rất nhiều phải không?

Notes

Chắc bạn sẽ thắc mắc rằng tại sao trong C, bạn vẫn có thể thực hiện câu lệnh a = new int;. Câu trả lời nằm ở trình biên dịch của bạn.

Nếu bạn sử dụng Borland C++ hay IDE nào đó có C++ trong tên của nó thì nó sẽ có khả năng biên dịch cả C và C++. Do đó, nếu bạn sử dụng a = new int; trong C, trình biên dịch vẫn hiểu và chấp nhận.

Thử lùi lại một chút, sử dụng Borland C, bạn sẽ thấy ngay sự khác biệt. Do Borland C chỉ có thể biên dịch C nên nếu bạn sử dụng a = new int;, nó sẽ không hiểu và báo lỗi.

1.2. Khai báo mảng động

Kế thừa việc cấp phát vùng nhớ thực sự lưu giá trị của con trỏ, khai báo mảng trong C++ cũng trở nên gọn gàng hơn.

Mảng 1 chiều

int *a = NULL; //Khai báo
a = new int[10]; //Khởi tạo

Thay cho, trong C:

int *a = NULL; //Khai báo
a = (int*)malloc(sizeof(int[10])); //Khởi tạo

Mảng 2 chiều

int cols, rows;
cols = rows = 2;

//Khai báo
int ** a = NULL;
a = new int*[cols];
for (int i = 0; i < rows; i++)
    *(a + i) = new int[rows];

Thay cho, trong C:

int cols, rows;
cols = rows = 2;

//Khai báo
int ** a = NULL;
a = (int**)malloc(cols * sizeof(int*));
for (int i = 0; i < rows; i++)
    *(a + i) = (int*)malloc(rows * sizeof(int));

1.3. Con trỏ và thủ tục, hàm

Truyền tham chiếu

Bạn còn nhớ ví dụ hoán vị 2 số ở phần 1 chứ? Tóm tắt lại thì nó như thế này, trong C:

void swap(int *a, int *b) //Khai báo thủ tục
{
...
}
...
int a, b;
...
swap(&a, &b); //Gọi thủ tục

Trong C++ cũng như thế, ngoài việc bạn cũng có thể gọi thủ tục như thế này:

int a, b;
...
swap(a, b); //Gọi thủ tục

Caution

Cách gọi thủ tục

swap(&a, &b); hoặc
swap(a, b);

chỉ đúng khi phần khai báo thủ tục là

void swap(<strong>int *a, int *b</strong>)
{
...
}

Đó là nói về khác biệt khi gọi thủ tục, ngoài ra trong C++ còn có thể khai báo thủ tục theo một cách khác:

void swap(<strong>int &a, int &b</strong>)
{
    int temp = a;
    a = b;
    b = temp;
}

Và ứng với cách khai báo này, cách gọi thủ tục như sau:

int a, b;
...
swap(a, b);

Đối với cách khai báo trên, dấu & để chỉ cho trình biên dịch biết rằng bạn đang truyền tham chiếu, còn những việc còn lại như gọi thủ tục, phần thân thủ tục đều được đơn giản hoá. Cụ thể hơn, ở phần gọi thủ tục, bạn chỉ cần truyền vào tham số đúng với kiểu dữ liệu bạn đã khai báo (trong ví dụ là kiểu int) mà không cần phải quan tâm là truyền vào địa chỉ (&a trong C) hay giá trị. Cũng tương tự như vậy, 2 tham số của thủ tục có kiểu dữ liệu giống với kiểu dữ liệu của 2 biến được truyền vào (int a, b;), do đó việc truy xuất các tham số trong phần thân của thủ tục cũng đơn giản hơn, int temp = a; thay cho int temp = *a;.

Theo mình, đây là một đặc điểm khá hay trong C++, giúp đơn giản hoá việc truyền tham chiếu.

Truyền tham số là mảng

Việc truyền tham số là mảng cho C++ cũng giống với C, tức là:

Đối với mảng 1 chiều:

void arr(int *a)
{
    ...
}

Đối với mảng 2 chiều:

void arr(int **a)
{
    ...
}

Trong phần này ở phần 1, mình có nói rằng “Về bản chất, khi truyền tham số là một mảng tức là truyền tham chiếu.”, điều đó là hoàn toàn đúng. Tuy nhiên, nó vẫn chưa phải toàn bộ vấn đề. Hãy xem ví dụ sau:

#include <iostream>

using namespace std;

void input(int *a, int &n)
{
    cout << "Enter n: ";
    cin >> n;

    a = new int[n];
    for (int i = 0; i < n; i++)
        a[i] = i;
}

void output(int *a, int n)
{
    for (int i = 0; i < n; i++)
        cout << a[i] << " ";
}

int main()
{
    int n;
    int *a = NULL;

    input(a, n);
    //a = new int[n]; //Dòng 1
    output(a, n);

    return 0;
}

Chương trình trên sẽ chỉ chạy đến hết phần nhập, sau đó báo lỗi và thoát ra ngoài. Lỗi phát sinh ngay trong phần xuất và lý do là a[i] chưa được khởi tạo nên không thể xuất giá trị của nó. Bạn có thể kiểm chứng điều này bằng cách cho thực thi dòng 1.

Digging deeper

Tại sao mình nói “”Về bản chất, khi truyền tham số là một mảng tức là truyền tham chiếu.”, điều đó là hoàn toàn đúng” và “Tuy nhiên, nó vẫn chưa phải toàn bộ vấn đề”, khó hiểu nhỉ?

Nếu tinh ý, bạn sẽ thấy rằng những ví dụ về truyền tham số là một mảng ở phần 1, chỉ là xuất hoặc thay đổi giá trị của một tham số mảng đã được khởi tạo trước đó. Điều đó đủ cho thấy rằng, truyền tham số là một mảng là truyền tham chiếu vì giá trị trong mảng đã bị tác động.

Tuy nhiên, ở ví dụ trên, vẫn là truyền tham số là một mảng, vẫn là truyền tham chiếu, nhưng tại sao mảng được truyền vào lại không bị tác động? Đó là phần còn lại của vấn đề. Nếu đã nghiên cứu kĩ vấn đề này ở phần 1, bạn có thể trả lời được câu hỏi này.

Tuy nhiên, để nhắc lại một chút, hãy phân tích thủ tục input ở đoạn code trên.

  1. void input(int *a, int &n)

    Như đã nói ở phần trước, dòng khai báo này sẽ tạo ra một con trỏ a cục bộ của thủ tục input và con trỏ a này sẽ trỏ đến cùng vùng nhớ thực sự lưu giá trị của con trỏ a ở hàm main.

  2. a = new int[n];

    Đây là chỗ phát sinh vấn đề. Dòng lệnh nãy sẽ cấp phát một vùng nhớ thực sự lưu giá trị của con trỏ a cục bộ. Từ đây, vùng nhớ thực sự lưu giá trị của con trỏ a cục bộ và vùng nhớ lưu giá trị của con trỏ a ở hàm main hoàn toàn độc lập. Do đó, mọi tác động đến con trỏ a cục bộ này không ảnh hưởng đến con trỏ a ở hàm main.

Nếu vẫn muốn viết như vậy mà chương trình chạy đúng, bạn cần sửa lại như sau:

void input(int <strong>**a</strong>, int &n)
{
    cout << "Enter n: ";
    cin >> n;

    <strong>*a</strong> = new int[n];
    for (int i = 0; i < n; i++)
        (<strong>*a</strong>)[i] = i;
}
...
input(<strong>&a</strong>, n);

Note

Cách trên cũng có thể được áp dụng trong C.

Tuy nhiên, tại sao ta không sử dụng đặc điểm mới trong C++ để đơn giản hoá việc truyền tham số là một mảng này. Cách thức thực hiện như sau:

void input(int <strong>* &a</strong>, int &n)
{
    cout << "Enter n: ";     cin >> n;

    <strong>a</strong> = new int[n];
    for (int i = 0; i < n; i++)
        <strong>a</strong>[i] = i;
}
...
input(<strong>a</strong>, n);

Rõ ràng rằng, việc truyền tham số trong C++ đã đơn giản hơn và bạn cũng đỡ phải đau đầu hơn.

Giá trị trả về là mảng

Vấn đề này thì cũng giống như trong C nên hãy xem lại vấn đề này ở phần 1 nếu cần thiết.

1.4. Tổng kết

Con trỏ trong C++ có rất nhiều đặc điểm giống con trỏ trong C, có thể nói là kế thừa. Tuy nhiên, nó cũng được bổ sung một vài đặc điểm mới giúp cho việc lập trình dễ dàng hơn. Có thể kể đến như:

  • Cách cấp phát vùng nhớ thực sự lưu giá trị của con trỏ
  • Cách truyền tham số, đặc biệt là tham chiếu

Nếu thực sự đã nắm rõ con trỏ trong C, con trỏ trong C++ sẽ rất dễ dàng đối với bạn, thậm chí là dễ dàng hơn với những đặc điểm mới mà C++ hỗ trợ.

Phần kết

Qua loạt bài về con trỏ trong C và C++, hy vọng bạn đã có những kiến thức cơ bản về nó. Bấy nhiêu đó chắc cũng đủ để bạn làm được khá nhiều việc rồi. Qua đó, mình cũng khám phá thêm được nhiều điều mới và càng nắm rõ hơn về vấn đề này.

Hãy tiếp tục khám phá và chia sẻ, bạn sẽ bất ngờ vì những kết quả bạn nhận được đấy.

Và thay lời kết là một câu nói mình rất thích:

Stay hungry, stay foolish

Steve Jobs

Tags: , ,

About ninjapro

It is better to feel by yourself about me

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: