Lập trình C/C++

Mối liên hệ giữa con trỏ và mảng một chiều trong C/C++

Mối liên hệ giữa con trỏ và mảng một chiều
Được viết bởi Minh Hoàng

Series lập trình C/C++, ngôn ngữ lập trình hệ thống mạnh mẽ.

Tiếp tục loạt bài trong series về con trỏ, trong bài này chúng ta sẽ tìm hiểu những nội dung sau:

  1. Mối liên hệ giữa con trỏ và mảng một chiều
  2. Địa chỉ của mảng một chiều và các phần tử trong mảng
  3. Con trỏ trỏ đến mảng một chiều
  4. Sự khác nhau khi sử dụng mảng một chiều và con trỏ trỏ đến mảng một chiều
1. Mối liên hệ giữa con trỏ và mảng một chiều
1. Mối liên hệ giữa con trỏ và mảng một chiều

– Nhắc lại khái niệm về mảng một chiều:
Mảng một chiều là tập hợp các phần tử có cùng kiểu dữ liệu được lưu trữ liên tiếp nhau trên bộ nhớ ảo, nếu mảng một chiều có một hoặc nhiều hơn một phần tử, thì địa chỉ của phần tử đầu tiên cũng chính là địa chỉ của mảng một chiều.

– Ở bài viết trước khái niệm và cách sử dụng của con trỏ trong ngôn ngữ C/C++, chúng ta biết rằng con trỏ có khả năng lưu trữ một địa chỉ của một vùng nhớ trên bộ nhớ ảo (virtual memory), tận dụng sức mạnh của con trỏ chúng ta có thể dùng nó để quản lý vùng nhớ tại địa chỉ mà con trỏ đang giữ, kích thước vùng nhớ đó là bao nhiêu còn tùy thuộc vào kiểu dữ liệu chúng ta khai báo cho con trỏ.

=> Chính vì con trỏ có thể thao tác trực tiếp với bộ nhớ ảo, nên chúng ta cũng có thể sử dụng con trỏ để thao tác trực tiếp với mảng một chiều. Trong C/C++ có mối quan hệ chặt chẽ giữa con trỏ và mảng. Các phần tử của mảng có thể được xác định được bằng chỉ số hoặc thông qua con trỏ.

2. Địa chỉ của mảng một chiều và các phần tử trong mảng
2. Địa chỉ của mảng một chiều và các phần tử trong mảng

– Ví dụ chúng ta có một mảng một chiều được khai báo 5 phần tử:

int arr[] = { 32, 13, 66, 11, 22 };

– Như chúng ta đã biết, địa chỉ của mảng một chiều cũng là địa chỉ của phần tử đầu tiên, vì thế, đoạn chương trình bên dưới sẽ in ra 2 giá trị địa chỉ giống nhau:

[code language=”c”] printf( "Dia chi cua bien mang arr = %p", &arr );

printf( "Dia chi phan tu dau tien cua mang arr = %p", &arr[0] );
[/code]

2 giá trị địa chỉ của &arr , &arr[0] là giống nhau.

2 giá trị địa chỉ của &arr , &arr[0] là giống nhau.

– Có một điểm đặc biệt của mảng một chiều trong C/C++, nếu ta lấy địa chỉ của biến mảng arr mà không sử dụng &arr, mà ghi trực tiếp là arr thì sẽ thế nào?

[code language=”c”] // Địa chỉ của arr trong bộ nhớ ảo (virtual memory)
printf( "arr = %p\n", arr );
printf( "===========================\n" );
printf( "&arr = %p\n", &arr );

// Địa chỉ của phần tử đầu tiên của mảng arr
printf( "&arr[0] = %p\n", &arr[0] );
[/code]

3 giá trị địa chỉ của arr, &arr , &arr[0] là giống nhau.

3 giá trị địa chỉ của arr, &arr , &arr[0] là giống nhau.

=> Điều này chứng tỏ rằng việc sử dụng tên mảng một chiều cũng chính là chúng ta đang sử dụng địa chỉ của mảng một chiều (&arr tương đương với arr).

Vì thế, chúng ta có thể in ra địa chỉ của cả 5 phần tử của mảng arr bằng cách sau (Mình viết bằng C++ cho dễ nhìn):

[code language=”c”] cout << arr << endl;
cout << arr + 1 << endl;
cout << arr + 2 << endl;
cout << arr + 3 << endl;
cout << arr + 4 << endl;

// Hoặc theo cách thông thường:
//cout << &arr[0] << endl;
//cout << &arr[1] << endl;
//cout << &arr[2] << endl;
//cout << &arr[3] << endl;
//cout << &arr[4] << endl;
[/code]

Địa chỉ liên tiếp nhau trên bộ nhớ ảo

Địa chỉ liên tiếp nhau trên bộ nhớ ảo

– Từ kết quả trên ta thấy: arr, arr + 1, arr + 2, arr + 3, arr + 4 chính là địa chỉ của 5 phần tử của mảng arr. Các bạn có thấy nó giống như các con trỏ không ;) vì vậy chúng ta có thể sử dụng toán tử * để truy xuất giá trị của chúng:

[code language=”c”] cout << *(arr) << endl;
cout << *(arr + 1) << endl;
cout << *(arr + 2) << endl;
cout << *(arr + 3) << endl;
cout << *(arr + 4) << endl;

// Hoặc theo cách thông thường:
//cout << arr[0] << endl;
//cout << arr[1] << endl;
//cout << arr[2] << endl;
//cout << arr[3] << endl;
//cout << arr[4] << endl;
[/code]

Các giá trị của mảng

Các giá trị của mảng

=> Từ cách viết trên, ta thấy ngoài việc dùng vòng for() in ra các giá trị của mảng bằng cách dùng chỉ số như thông thường thì ta cũng có thể làm như sau:

[code language=”c”] for (int i = 0; i < 5; i++)
{
cout << *(arr + i) << " ";
}

// Hoặc theo cách thông thường:
//for (int i = 0; i < 5; i++)
//{
// cout << arr[i] << " ";
//}
[/code]

Các giá trị của mảng

Các giá trị của mảng

Tổng kết:
– Tên mảng là một hằng địa chỉ (hằng con trỏ), nó chính là địa chỉ của phần tử đầu tiên của mảng.
– Địa chỉ:

  • arr <=> &arr[0] <=> &arr
  • (arr + i) <=> &arr[i]

– Giá trị:

  • *arr <=> arr[0]
  • *(arr + i) <=> arr[i]
Con trỏ và mảng một chiều trong C/C++

Con trỏ và mảng một chiều trong C/C++

3. Con trỏ trỏ đến mảng một chiều
3. Con trỏ trỏ đến mảng một chiều

– Chúng ta sẽ tiếp tục làm việc với mảng một chiều trên:

int arr[] = { 32, 13, 66, 11, 22 };

– Mỗi phần tử bên trong mảng đều có kiểu int, do đó, chúng ta sẽ sử dụng 1 con trỏ có kiểu dữ liệu tương ứng (int *) để thao tác với mảng arr

int *p;

– Vì tên mảng và con trỏ bản chất đều là địa chỉ của vùng nhớ, nên ta có thể gán địa chỉ của hằng con trỏ arr cho con trỏ p

p = arr;

// Hoặc:
p = &arr[0];

– Lúc này thì với các code minh họa ở mục 2. phía trên, ở tất cả những chỗ có chữ arr, chúng ta thay bằng p khi chạy chương trình thì kết quả là hoàn toàn giống nhau ;).
Và để truy cập từng phần tử trong mảng arr , thì 4 cách sau là tương đương nhau:

  1.  arr[i]
  2.  *(arr + i) // Lấy giá trị trong ô địa chỉ (arr + i)
  3.  p[i]
  4.  *(p + i)
Con trỏ trỏ đến mảng

Con trỏ trỏ đến mảng

Notes: Đến đây các bạn thử nghĩ xem khi chạy code bên dưới địa chỉ của *p và p sẽ như thế nào?

printf("Address in *p: %p\n", *p);
printf("Address in  p: %p\n",  p);

– Bây giờ chúng ta sẽ cho con trỏ p thao tác với mảng arr, mình cho con trỏ p trỏ đến phần tử thứ 3 trong mảng arr.

p = &arr[2];

// Hoặc:
p = arr + 2;
Con trỏ trỏ đến phần tử thứ 3 của mảng

Con trỏ trỏ đến phần tử thứ 3 của mảng

– Lúc này, chúng ta sử dụng toán tử * để lấy giá trị của con trỏ p sẽ được giá trị 66.

cout << *p << endl;

- Từ địa chỉ của arr[2] mà con trỏ p đang nắm giữ, chúng ta cũng có thể sử dụng toán tử「+」hoặc「-」để truy xuất đến tất cả các phần tử còn lại trong mảng arr vì các phần tử của mảng có địa chỉ nối tiếp nhau trên bộ nhớ ảo.

// Dịch chuyển con trỏ lên trước 1 ô nhớ
cout << *(p - 1) << endl; // access the second element(13) of arr
Dùng toán tử「-」thao tác địa chỉ con trỏ

Dùng toán tử「-」thao tác địa chỉ con trỏ

// Dịch chuyển con trỏ lùi ra sau 2 ô nhớ
cout << *(p + 2) << endl; // access the last element(22) of arr
Dùng toán tử「+」thao tác địa chỉ con trỏ

Dùng toán tử「+」thao tác địa chỉ con trỏ

- Tương tự như trên, chúng ta cũng có thể sử dụng toán tử「++」hoặc「--」 để truy xuất đến phần tử tiếp theo hoặc phần tử đứng trước đó:

p++;
cout << *p << endl; // Output: 11
Dùng toán tử「++」thao tác địa chỉ con trỏ

Dùng toán tử「++」thao tác địa chỉ con trỏ

p--;
cout << *p << endl; // Output: 13
Dùng toán tử「--」thao tác địa chỉ con trỏ

Dùng toán tử「--」thao tác địa chỉ con trỏ

- Như các bạn thấy, chỉ với một con trỏ có kiểu dữ liệu tương ứng với kiểu của mảng một chiều, chúng ta có thể quản lý được toàn bộ phần tử trong mảng:

[code language="c"] p = arr;

for ( p = &arr[0]; p <= &arr[4]; p++ )
{
cout << *p << " ";
}

// Hoặc:
for ( p = arr; p <= arr + 4; p++ )
{
cout << *p << " ";
}
[/code]

Các giá trị của mảng

Các giá trị của mảng

Vòng lặp for() trên: ban đầu khởi tạo bằng cách gán địa chỉ phần tử đầu tiên của mảng arr cho con trỏ p (p = &arr[0]), khi nào địa chỉ mà p nắm giữ vẫn còn nhỏ hơn hoặc bằng địa chỉ của phần tử cuối cùng (p <= &arr[4]) thì tiếp tục in giá trị mà p trỏ đến, cứ thế cho p dịch chuyển đến ô nhớ tiếp theo (p++) trong mảng.

- Cũng là in ra toàn bộ giá trị của các phần tử trong mảng arr, nhưng sử dụng con trỏ chúng ta có rất nhiều cách viết khác nhau:

[code language="c"] p = arr;

for (int i = 0; i < 5; i++)
{
cout << p[i] << " ";
}

// Hoặc:
for (int i = 0; i < 5; i++)
{
cout << *(p + i) << " ";
}

// Hoặc:
for (int i = 0; i < 5; i++)
{
cout << *(arr + i) << " ";
}
[/code]

Bên lề:
■ Giả sử chúng ta có 2 mảng một chiều kiểu int có cùng kích thước như sau:

int src[5] = { 3, 1, 5, 7, 4 };
int des[5];

- Việc copy dữ liệu từ mảng src sang mảng des có thể thực hiện như sau:

[code language="c"] for (int i = 0; i < 5; i++)
{
des[i] = src[i];
}

// Hoặc:
for (int i = 0; i < 5; i++)
{
*(des + i) = *(src + i);
}
[/code]

■ Đối với mảng kí tự bản chất nó cũng tương tự như mảng một chiều, chúng ta có thể trực tiếp in nội dung của chuỗi kí tự. Ví dụ:

[code language="c"] // Khai báo mảng ký tự
char myName[50];

// Nhập chuỗi
cout << "Enter your name: ";
gets_s( myName );

// Xuất chuỗi
cout << "Hello " << myName << endl;
[/code]

Nhập, xuất mảng ký tự

Nhập, xuất mảng ký tự

- Và nếu chúng ta sử dụng một con trỏ kiểu (char *) để trỏ đến mảng myName, chúng ta có thể dùng tên con trỏ để in mảng đó ra màn hình:

[code language="c"] char *pName = myName;

cout << "Hello " << pName << endl;
[/code]

- Bên cạnh đó, chúng ta có thể cho con trỏ kiểu (char *) trỏ đến một chuỗi kí tự cố định nào đó, và vẫn có thể in nội dung mà con trỏ đó đang trỏ đến. Ví dụ:

[code language="c"] char *p_str = "This is an example string";

cout << p_str << endl;
[/code]

*** Nhưng vùng nhớ của chuỗi kí tự này được xem là hằng số (const) nên chúng ta chỉ có thể xem nội dung mà p_str trỏ đến chứ không thể thay đổi kí tự bên trong chuỗi. Chúng ta sẽ tìm hiểu về vấn đề này trong các bài viết tiếp theo.

4. Sự khác nhau khi sử dụng mảng một chiều và con trỏ trỏ đến mảng một chiều
4. Sự khác nhau khi sử dụng mảng một chiều và con trỏ trỏ đến mảng một chiều

Sau khi con trỏ trỏ đến mảng một chiều, chúng ta có thể sử dụng tên con trỏ thay vì sử dụng tên mảng. Tuy vậy, giữa chúng vẫn có một số điểm khác biệt.

#1. Mảng một chiều sau khi khai báo thì sẽ có ĐỊA CHỈ CỐ ĐỊNH trên bộ nhớ ảo, con trỏ sau khi trỏ đến mảng một chiều, nếu muốn chúng ta vẫn có thể cho nó trỏ đi nơi khác được.

[code language="c" highlight="6,14"] int arr[5] = { 32, 13, 66, 11, 22 };

int *p;

// Cho con trỏ p trỏ đến mảng arr
p = arr;

// In giá trị con trỏ p
cout << "*p = " << *p << endl;

int a = 9;

// Cho con trỏ p trỏ đến biến a
p = &a;

// In giá trị con trỏ p
cout << "*p = " << *p << endl;
[/code]

Con trỏ có thể trỏ đi lung tung trong bộ nhớ

Con trỏ có thể trỏ đi lung tung trong bộ nhớ

#2. Khi sử dụng toán tử sizeof() thì:

=> Như vậy, sử dụng mảng một chiều chúng ta có thể biết được chính xác số lượng phần tử chúng ta cần quản lý trong khi con trỏ không làm được điều này.

[code language="c"] int arr[5];

int *p = arr;

cout << "Size of arr = " << sizeof( arr ) << endl;
cout << "Size of p = " << sizeof( p ) << endl;
[/code]

Kích thước của mảng và con trỏ đến mảng

Kích thước của mảng và con trỏ đến mảng

#3. Con trỏ khi khai báo thì chưa được cấp phát bộ nhớ, còn mảng thì được cấp phát bộ nhớ.

Cảm ơn bạn đã theo dõi. Đừng ngần ngại hãy cùng thảo luận với chúng tôi!

Giới thiệu

Minh Hoàng

Xin chào, tôi là Hoàng Ngọc Minh, hiện đang làm BrSE, tại công ty Toyota, Nhật Bản. Những gì tôi viết trên blog này là những trải nghiệm thực tế tôi đã đúc rút ra được trong cuộc sống, quá trình học tập và làm việc. Các bài viết được biên tập một cách chi tiết, linh hoạt để giúp bạn đọc có thể tiếp cận một cách dễ dàng nhất. Hi vọng nó sẽ có ích hoặc mang lại một góc nhìn khác cho bạn[...]

2 bình luận

Translate »