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

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

Dạo này cứ C rồi C++, nhảy qua nhảy lại chóng mặt luôn. Lại đụng đến con trỏ, một vấn đề không khó để hiểu nhưng lại không dễ để kiểm soát hoàn toàn. Thế nên nghiên cứu, viết loạt bài này để nắm rõ vấn đề hơn, cũng như là tài liệu tham khảo cho bạn nào cũng đang gặp vướng mắt này.

Bài viết này sẽ gồm 2 phần. Phần 1 là con trỏ trong C và phần 2 là con trỏ trong C++. Lý do chia làm 2 phần này vì trước mắt thấy sử dụng con trỏ trong C và C++ có chút khác biệt nên chia làm 2 phần cho rõ ràng.

Loạt bài viết này chỉ tập trung vào việc sử dụng con trỏ trong C, C++, không trình bày sâu về cách tổ chức dữ liệu bên dưới bộ nhớ.

Phần 1: Con trỏ trong C

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

Trước tiên, để sử dụng được con trỏ thì cần phải khai báo và khởi tạo con trỏ và có chút hiểu biết cơ bản về nó.

Để khai báo và khởi tạo con trỏ trong C có nhiều cách khác nhau và tương nhiên cũng sẽ có chút khác biệt.

Cách 1:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *a; //Khai báo

    *a = 100; //Khởi tạo

    printf("Address: %p\n", a);
    printf("Value: %d\n", *a);

    return 0;
}

Output:

Address: 0x7fff5fbff9a8
Value: 100

Với cách này, sau khi khai báo, a sẽ tự trỏ đến một vùng nhớ bất kì với một giá trị bất kì (có thể kiểm tra điều này bằng cách xoá hay chú thích dòng khởi tạo).

Tiếp đến, khởi tạo giá trị cho a bằng toán tử * (dereference operator). Nếu bỏ * đi sẽ phát sinh lỗivì a chỉ chứa địa trỏ đến vùng nhớ lưu giá trị thực sự, có thể truy xuất bằng *a. Việc này có thể dễ dàng nhận thấy qua 2 dòng lệnh printf.

Caution

Đây thực sự không phải là một cách tốt và bạn nên tránh sử dụng nó.
Còn lý do tại? So sánh kết quả của đoạn code này với đoạn code bên trên, bạn sẽ nhận ra được vấn đề.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *a;
    int *c;

    printf("%p\n", a);
    printf("%p\n", c);

    return 0;
}

Output:

0x7fff5fbff9a8
0x0

Tiếp nhé, hãy xem đoạn code sau:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *a = 100; //Khởi tạo ngay lúc khai báo

    printf("Address: %p\n", a);
    printf("Value: %d\n", *a);

    return 0;
}

Đoạn code này vẫn cho kết quả đúng như đoạn code trên chứ?

Câu trả lời là không! Vấn đề nằm ở chỗ khởi tạo ngay lúc khai báo. Chắc chắn bạn sẽ hỏi rằng “Sử dụng *a để truy xuất đến vùng nhớ lưu giá trị thực sự, sau đó gán giá trị cho nó, vậy tại sao sai chứ?”.

Yeah, nghe có vẻ hợp lý nhưng thực tế lại là không bạn à!

Có chút sự khác biệt ở đây mà bạn cần lưu ý để tránh hiểu nhầm. int *a; dấu * ở đây không mang ý nghĩa là truy xuất đến vùng nhớ lưu giá trị thực sự của con trỏ a mà chỉ để trình biên dịch nhận biết đó là một biến con trỏ, chứ không phải một biến bình thường. Do đó, việc dùng int *a = 10; là không đúng vì thực sự *a lúc đó cũng chỉ có thể gán bằng một địa chỉ mà thôi.

Để hiểu rõ hơn về vấn đề này, bạn hãy xem tiếp đoạn code sau:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int b = 100;
    int *a = &b;

    printf("Address: %p\n", a);
    printf("Value: %d\n", *a);

    return 0;
}

Output:

Address: 0x7fff5fbff998
Value: 100

Đến đây, chắc bạn đã hiểu rõ hơn, phân biệt được lúc nào * là toán tử, lúc nào không và kiểm soát con trỏ hơn được một chút.

Digging deeper

%p là chuỗi định dùng để xuất địa chỉ. Ở đây, mình chỉ nói chung chung là xuất địa chỉ mà không nói rõ là xuất địa chỉ của biến nào. Có lý do đấy!

Và lý do là đây:

...
printf("Address: %p\n", a);
printf("Address: %p\n", &a);
...

Output:

Address: 0x7fff5fbff9a8
Address: 0x7fff5fbff990

Bạn thấy chứ, thật sự thì a không lưu một chút dữ liệu nào cả, nó chỉ lưu địa chỉ của vùng nhớ lưu giá trị thực sự thôi.

Nhưng a cũng có địa chỉ của riêng nó nữa chứ và địa chỉ đó có thể được truy xuất qua &a (&: address operator).

Nếu bạn vẫn còn nghi ngờ hay lờ mờ việc này. Hãy để một ví dụ nữa làm sáng tỏ.

...
int a = 100;

printf("Address: %p\n", a); //Dòng 1
printf("Address: %p\n", &a);
printf("Value: %d\n", a)
...

Output:

Address: 0x64 //Dòng 2
Address: 0x7fff5fbff98c //Dòng 3
Value: 100

Kết quả có vẻ không ổn lắm.

Ở ví dụ trên, a là một biến bình thường và có giá trị là 100, một số kiểu int.

Khi dòng 1 được thực thi, chương trình sẽ hiểu là xuất ra giá trị của biến a với định dạng là một địa chỉ. Tuy nhiên, thực sự a chỉ lưu giá trị của một số kiểu int, không lưu bất kì địa chỉ của một vùng nhớ nào cả nên việc yêu cầu xuất giá trị của a với định dạng là một địa chỉ là không hợp lý và kết quả ta nhận được không giống với dòng 3.

Thử một chút nữa ta sẽ thấy kết quả ở dòng 2 là 64 (hệ 16) bằng 100 (hệ 10), tương đương với giá trị ta gán vào biến a. Điều này một lần nữa khẳng định rằng, a không trỏ đi đâu hết, nhưng do ta bắt buộc xuất ra giá trị của a với định dạng %p nên chương trình phải xuất giá trị của nó ở hệ 16 (hệ biểu diễn của địa chỉ).

Cách 2:

Cách này có nhiều cách viết khác nhau nhưng về cơ bản, nó khác cách 1 ở chỗ, khai báo tường minh việc cấp phát vùng nhớ lưu giá trị thực sự của con trỏ. Để hiểu thêm về điều này, hãy xem qua đoạn code sau:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *a = NULL; //Khai báo

    *a = 100; //Khởi tạo

    printf("Address: %p\n", a);
    printf("Value: %d\n", *a);

    return 0;
}

Theo bạn thì kết quả của đoạn code trên là gì?

Dù câu trả lời của bạn là gì, phần giải thích bên dưới sẽ giúp bạn nhận ra vấn đề và hiểu rõ hơn.

Đoạn code trên chẳng chạy được đâu!

Nếu để ý, bạn sẽ thấy có chút khác biệt ở dòng khai báo ở ví dụ trên và những ví dụ ở cách 1. Sự khác biệt nhỏ nhưng kết quả lại rất lớn đấy! Trong ví dụ trên, ngay khi khai báo, bạn đã gán cho con trỏ a bằng NULL. Nói cách khác, chưa hề tồn tại một vùng nhớ lưu giá trị thực sự của a, việc gọi *a vô nghĩa là vô nghĩa và gây ra lỗi.

Vậy làm thế nào để cấp phát vùng nhớ lưu giá trị thực sự của con trỏ a. Bạn có thể dùng cách sau đây:

//Hàm malloc sẽ trả về con trỏ đến vùng nhớ có kích thước được truyền vào (ở đây là sizeof(int), có nghĩa là kích thước của kiểu int - 4 bytes)

//Nhưng nó sẽ trả về con trỏ chung <strong>void*</strong> do đó cần ép về kiểu <strong>int*</strong>

//Câu lệnh bên dưới có ý nghĩa là, lấy địa chỉ của vùng nhớ mới cấp phát gán cho <strong>a</strong>. Tức là lúc này <strong>a</strong> đã có một vùng nhớ lưu giá trị thực sự và sẵn sàng để khởi tạo
<pre>a = (int*)malloc(sizeof(int));

Chú ý

Để sử dụng được hàm malloc cần có thư viện stdlib.h

Và đây là một đoạn hoàn chỉnh:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *a = NULL;

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

    printf("Address: %p\n", a);
    printf("Value: %d\n", *a);

    return 0;
}

Output:

Address: 0x100100a20
Value: 100

Đến đây chắc bạn đã có thể khai báo, khởi tạo con trỏ một cách ngon lành rồi. Nhưng đừng quên là hãy tự mình thực hành, kiểm nghiệm lại đấy, biết đâu bạn lại khám phá ra một thứ gì đó mới, thú vị hơn thì sao và cũng đừng quên chia sẻ lại cho mọi người nhé!

Stop

Chỉ riêng về khai báo và khởi tạo con trỏ, cũng còn kha khá chuyện để bàn luận. Nhưng nổi lên từ những ví dụ trên có 2 vấn đề chính:

  1. Nếu tinh ý, bạn sẽ nhận thấy sự khác biệt ở địa chỉ mà con trỏ trỏ đến ở cách 1 và cách 2. Cụ thể thì như thế này:

    Address: 0x7fff5fbff9a8 //Cách 1
    Address: 0x100100a20 //Cách 2

    Điều này chứng tỏ, cách cấp phát và tổ chức bộ nhớ ở 2 cách là khác nhau.

    Nhưng như mình đã nói ở đầu bài viết, mình sẽ không trình bày sâu về việc tổ chức bộ nhớ thế nào trong bài viết này.

    Đây cũng xem như một thử thách thú vị cho bạn tự tìm hiểu. Và hẹn lại bạn ở một bài viết khác mình sẽ trình bày chi tiết hơn về việc này.

  2. Khai báo mảng động. Vấn đề nãy sẽ được trình bày trong phần tiếp theo.

Còn tiếp…

Tags: , ,

About ninjapro

It is better to feel by yourself about me

5 responses to “Nói về con trỏ [P. 1]”

  1. Đăng Quang says :

    Cảm ơn bạn Vinh rất nhiều!

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: