Con trỏ thao tác với mảng hai chiều trong C | Lập Trình Từ Đầu

1.Nhắc lại kiến thức mảng hai chiều

Một mảng hai chiều là một mảng chứa nhiều mảng một chiều, đôi khi mảng hai chiều còn được ngầm hiểu như một bảng bao gồm các hàng và các cột. Mảng hai chiều còn thường được gọi với cái tên là ma trận có kích thước M x N (M là số hàng, N là số cột).

Truy cập vào phần tử trong mảng một chiều là việc truy cập vào hàng thứ i cột thứ j có trong mảng. Ví dụ mảng a[1][3] (nghĩa là: i = 1, j = 3)

Ví dụ dưới đây tôi có a là mảng 2 chiều (hay còn gọi là ma trận) có kích thước 3 x 3 như sau:

#include <stdio.h>
int main()
{
    //khai bao mang 2 chieu a gom 3 x 3 phan tu
    int a[3][3] = {
        {1,2,3},
        {4,5,6},
        {7,8,9}
    };
}

Chúng ta có thể tưởng tượng mảng hai chiều trên như một bảng như sau:

Như ở trên ta vẫn thường gọi mảng có các hàng và các cột, tuy nhiên bộ nhớ của máy tính là tuyến tính và không có hàng và cột. Vì lý do đó nên các phần tử của mảng 2 chiều thực chất được lưu trữ theo một hàng và được gom nhóm lại theo các cột có trong hàng, xem hình minh họa dưới đây để hiểu rõ hơn:

Dòng trên cùng là số lượng các hàng, vì chúng ta có mảng 2 chiều kích thước 3 x 3 nên có 3 hàng (3 row). Trong mỗi hàng được gom nhóm lại 3 phần tử tương ứng với 3 cột.

Dòng ở giữa là giá trị của mỗi phần tử ở hàng thứ i cột thứ j, ví dụ tại a[1][3] = 3 (nghĩa là i = 1, j = 3) hay a[2][1] = 4 (nghĩa là i = 2, j = 1).

Dòng cuối cùng là địa chỉ của mỗi phần tử ở hàng thứ i cột thứ j, ví dụ a[1][1] lưu trữ tại địa chỉ 1000, a[1][2] lưu trữ tại địa chỉ 1004, a[1][3] lưu trữ tại địa chỉ 1008… và a[3][3] lưu trữ tại địa chỉ 1032

Vì đây là mảng có kiểu int và có kích thước 4 byte nên mỗi địa chỉ liền kề cách nhau 4 đơn vị (4 byte).

Chú ý: Địa chỉ ô nhớ trên chỉ là ví dụ để ta dễ hình dung.

2.Sử dụng con trỏ để thao tác với các phần tử trong mảng hai chiều

2.1 Địa chỉ ô nhớ của các phần tử mảng hai chiều

Mảng hai chiều có thể được định nghĩa như là một con trỏ trỏ tới một nhóm các mảng một chiều liên tiếp nhau.

Ở mảng một chiều ta biết rằng tên của mảng chứa địa chỉ của mảng đó, hay phần tử a[0] (phần tử đầu tiên) cũng chứa địa chỉ của mảng đó.

Trong trường hợp của mảng 2 chiều, phần tử đầu tiên trong mảng 2 chiều lại chính là một mảng 1 chiều và nếu mảng hai chiều a có kích thước (3 x 3) và có địa chỉ tại phần tử đầu tiên (a + 0) là 1000 thì địa chỉ của phần tử thứ hai (a+1) là 1012 , địa chỉ tại phần tử thứ ba (a + 2) là 1024. Xem phần màu đỏ ở hình dưới đây:

Như vậy có nghĩa là:

  • (a + 0) trỏ đến mảng thứ 0
  • (a + 1) trỏ đến mảng thứ 1
  • (a + 2) trỏ đến mảng thứ 2

Tóm lại, ta có thể hiểu (a + i) trỏ đến mảng một chiều thứ i trong mảng 2 chiều.

Tuy nhiên, trong mảng thứ i lại tồn tại thêm các giá trị ở trong đó (vì mảng thứ i này là mảng 1 chiều). Ví dụ (a + 0) thì sẽ là:

Hay (a + 1) thì sẽ là:

Hoặc (a + 2) thì sẽ là:

Chú ý: Địa chỉ ô nhớ trên chỉ là ví dụ để ta dễ hình dung.

2.2 Sử dụng con trỏ để truy cập địa chỉ và giá trị của phần tử mảng hai chiều

Bạn đọc hãy cùng tôi tham khảo hai ví dụ dưới đây để rút ra công thức tổng quát cho việc lấy địa chỉ và giá trị của mảng hai chiều a[i][j]

Ví dụ 1, giả sử tôi muốn lấy địa chỉ của a[0][0], nghĩa là tôi truy cập vào mảng 1 chiều đầu tiên, và truy cập tiếp theo vào phần tử đầu tiên trong mảng đó. Xem hình minh họa bên dưới

Tôi sẽ sử dụng cú pháp: *(a + 0) + 0 để lấy địa chỉ a[0][0]

#include <stdio.h>
int main()
{
    //khai bao mang 2 chieu a gom 3 x 3 phan tu
    int a[3][3] = {
        {1,2,3},
        {4,5,6},
        {7,8,9}
    };
    //lay dia chi a[0][0] bang *(a + 0) + 0
    printf("Dia chi cua a[0][0] la: %x", *(a + 0) + 0);
}

Dia chi cua a[0][0] la: 62fdf0

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

Ví dụ thứ 2 tôi muốn lấy địa chỉ của a[2][1], nghĩa là tôi truy cập vào mảng 1 chiều thứ 2, và truy cập tiếp theo vào phần tử thứ 1 trong mảng đó.

Tôi sẽ sử dụng cú pháp: *(a + 2) + 1 để lấy địa chỉ a[2][1]

#include <stdio.h>
int main()
{
    //khai bao mang 2 chieu a gom 3 x 3 phan tu
    int a[3][3] = {
        {1,2,3},
        {4,5,6},
        {7,8,9},
    };
    //lay dia chi a[2][1] bang *(a + 2) + 1
    printf("Dia chi cua a[2][1] la: %x", *(a + 2) + 1);
}

Dia chi cua a[2][1] la: 62fe0c

Chú ý: Địa chỉ ô nhớ trên mỗi máy tính khi thực thi chương trình trên là khác nhau. Kết quả trên chỉ là minh họa

→ Qua hai ví dụ trên ta có thể rút ra công thức cho địa chỉ và giá trị của phần tử a[i][j] nào đó trong mảng 2 chiều

  1. Công thức cho địa chỉ ô nhớ a[i][j] là: *(a+i)+j
  2. Và ta hoàn toàn lấy được giá trị của a[i][j] bằng cách thêm toán tử * cho toàn bộ công thức trên: *(*(a+i) + j)

Để minh họa về việc lấy giá trị của chúng ta cùng quay lại ví dụ 2, tôi sẽ lấy giá trị tại a[2][1] = 8

Theo công thức *(*(a+2) + 1)

#include <stdio.h>
int main()
{
    //khai bao mang 2 chieu a gom 3 x 3 phan tu
    int a[3][3] = {
        {1,2,3},
        {4,5,6},
        {7,8,9},
    };
    //lay gia tri a[2][1] bang *(*(a+2) + 1)
    printf("Gia tri cua a[2][1] la: %d",*(*(a+2) + 1));
}

Gia tri cua a[2][1] la: 8

Như vậy, ta đã có công thức cho địa chỉ ô nhớ và giá trị của phần tử a[i][j] trong mảng hai chiều.

Tiếp theo ta sẽ áp dụng con trỏ để truy cập vào địa chỉ và giá trị của phần tử mảng a[i][j] đó. Ta nhớ lại rằng mảng 1 chiều khi thao tác với con trỏ ta chỉ việc gán con trỏ p = a (trong đó a là tên mảng) bạn đọc quên có thể quay lại đọc Bài 2: Con trỏ thao tác với mảng một chiều trong C.

Luôn nhớ rằng, mảng hai chiều thực chất cũng là mảng 1 chiều chứa nhiều phần tử và mỗi phần tử trong đó lại là một mảng 1 chiều gồm nhiều cột.

Vì thế để sử dụng con trỏ lưu trữ địa chỉ của mảng 2 chiều đó ta cần một con trỏ đến một mảng có kích thước bằng số cột của mảng 1 chiều.

Ví dụ dưới đây tôi có mảng a[3][3] (có 3 hàng 3 cột) nên tôi sẽ khai báo như sau:

#include <stdio.h>
int main()
{
    //khai bao mang 2 chieu a gom 3 x 3 phan tu
    int a[3][3] = {
        {1,2,3},
        {4,5,6},
        {7,8,9},
    };
    //khoi tao con tro p tro den 3 cot
    int (*p)[3];
    //gan con tro p bang mang a
    p = a;
}

Giả sử tôi có mảng a[3][4] (3 hàng 4 cột) tôi sẽ khai báo: int (*p)[4];

→ Như vậy ta có công thức để có thể gán con trỏ cho mảng 2 chiều có kích thước a[i][j] là: int (*p)[j]

Bây giờ, ta hoàn toàn có thể sử dụng con trỏ để lấy giá trị và địa chỉ của các phần tử trong mảng a bằng cách:

  1. (p + i) để lấy ra địa chỉ của các phần tử trong mảng hai chiều
  2. * ( * ( p + i) + j ) để lấy ra giá trị của các phần tử trong mảng

Ví dụ dưới đây tôi sẽ sử dụng con trỏ (*p)[3] để lấy giá trị và địa chỉ của mảng a[3][3]

#include <stdio.h>
int main()
{
    //khai bao mang 2 chieu a gom 3 x 3 phan tu
    int a[3][3] = {
        {1,2,3},
        {4,5,6},
        {7,8,9},
    };
    //khoi tao con tro p
    int (*p)[3];
    //gan con tro p bang mang a
    p = a;
    
    //duyet cac hang trong mang
    for(int i = 0; i < 3; i ++){
        //hien thi dia chi cua tung phan tu trong mang 2 chieu
        printf("Dia chi phan tu %d la: %x \n",i , p + i);
        //duyet cac cot trong mang
        for(int j = 0; j < 3; j++){
            //hien thi gia tri cua tung phan tu
            printf ("Gia tri: a[%d][%d] =%d \n" ,  i ,  j ,  * (  * ( p  +  i)  +  j ) );
        }   
        printf("\n \n");
    }
}

Dia chi phan tu 0 la: 62fde0

Gia tri: a[0][0] =1

Gia tri: a[0][1] =2

Gia tri: a[0][2] =3

Dia chi phan tu 1 la: 62fdec

Gia tri: a[1][0] =4

Gia tri: a[1][1] =5

Gia tri: a[1][2] =6

Dia chi phan tu 2 la: 62fdf8

Gia tri: a[2][0] =7

Gia tri: a[2][1] =8

Gia tri: a[2][2] =9

Ví dụ 2 tôi sử dụng con trỏ (*p)[3] để lấy giá trị các phần tử là số chẵn có trong mảng a

#include <stdio.h>
int main()
{
    //khai bao mang 2 chieu a gom 3 x 3 phan tu
    int a[3][3] = {
        {1,2,3},
        {4,5,6},
        {7,8,9},
    };
    //khoi tao con tro p cho mang a[3][3]
    int (*p)[3];
    //gan con tro p bang mang a
    p = a;
    
    printf("Cac phan tu trong mang la so chan \n");
    //duyet cac hang trong mang
    for(int i = 0; i < 3; i ++){
        //duyet cac cot trong mang
        for(int j = 0; j < 3; j++){
            if(* (  * ( p  +  i)  +  j ) % 2 == 0){
                //hien thi gia tri cac phan tu la so chan
                printf ("Gia tri: a[%d][%d] =%d \n" ,  i ,  j ,  * (  * ( p  +  i)  +  j ) );
            }
        }   
    }
}

Cac phan tu trong mang la so chan

Gia tri: a[0][1] =2

Gia tri: a[1][0] =4

Gia tri: a[1][2] =6

Gia tri: a[2][1] =8