Sự khác nhau giữa con trỏ và mảng

contro

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

Contents

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:

1
2
3
printf( "Dia chi cua bien mang arr              = %p", &arr );
printf( "Dia chi phan tu dau tien cua mang arr  = %p", &arr[0] );

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?

1
2
3
4
5
6
7
// Đị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] );

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):

1
2
3
4
5
6
7
8
9
10
11
12
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;

Đị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: arrarr + 1arr + 2arr + 3arr + 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:

1
2
3
4
5
6
7
8
9
10
11
12
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;

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:

1
2
3
4
5
6
7
8
9
10
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] << " ";
//}

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:

1
2
3
4
5
6
7
8
9
10
11
12
p = arr;
for ( p = &arr[0]; p <= &arr[4]; p++ )
{
    cout << *p << " ";
}
// Hoặc:
for ( p = arr; p <= arr + 4; p++ )
{
    cout << *p << " ";
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) << " ";
}

 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:

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < 5; i++)
{
    des[i] = src[i];
}
// Hoặc:
for (int i = 0; i < 5; i++)
{
    *(des + i) = *(src + i);
}

■ Đố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ụ:

1
2
3
4
5
6
7
8
9
// 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;

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:

1
2
3
char *pName = myName;
cout << "Hello " << pName << endl;

– 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ụ:

1
2
3
char *p_str = "This is an example string";
cout << p_str << endl;

*** 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

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.

1
2
3
4
5
6
int arr[5];
int *p = arr;
cout << "Size of arr = " << sizeof( arr ) << endl;
cout << "Size of p   = " << sizeof( p ) << endl;

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ớ.

Trả lời