Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 10.118 phút đọc

Ma trận

Sau bài này bạn sẽ làm được

  • Khai báo và khởi tạo mảng hai chiều đúng cách
  • Giải thích bố cục lưu theo hàng và công thức địa chỉ
  • Hiểu vì sao tham số mảng hai chiều bắt buộc ghi số cột
  • Chọn đúng cách truyền ma trận vào hàm

Mảng hai chiều là mảng của mảng. Câu đó nghe đơn giản nhưng giải thích được toàn bộ những chỗ khó của chương này: vì sao chiều thứ hai bắt buộc phải ghi khi truyền vào hàm, vì sao thứ tự vòng lặp ảnh hưởng tốc độ, và vì sao m[1][2] lại nằm ở ô thứ sáu.

#Khai báo và khởi tạo

int m[3][4];                 /* 3 hàng, mỗi hàng 4 cột, tổng 12 phần tử */

int a[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };     /* rõ ràng nhất */
int b[2][3] = { 1, 2, 3, 4, 5, 6 };             /* tương đương, điền theo hàng */
int c[][3]  = { { 1, 2, 3 }, { 4, 5, 6 } };     /* chiều đầu tự suy ra là 2 */
int d[2][3] = { { 1 }, { 4 } };                 /* {1,0,0} và {4,0,0} */
int e[2][3] = { 0 };                            /* cả 6 ô bằng 0 */

Khởi tạo có chỉ định cũng dùng được

/* Ma trận đơn vị 3x3, chỉ ghi những ô khác 0 */
int don_vi[3][3] = {
    [0][0] = 1,
    [1][1] = 1,
    [2][2] = 1,
};

/* Bảng khoảng cách, chỉ ghi các cặp có đường đi */
int kc[4][4] = {
    [0][1] = 5, [0][2] = 3,
    [1][3] = 2,
    [2][3] = 7,
};

#Bố cục trong bộ nhớ

Lưu theo hàng
C xếp toàn bộ hàng 0 trước, rồi tới hàng 1, rồi hàng 2. Trong bộ nhớ, mảng hai chiều là một dãy phẳng liên tục, không có con trỏ nào ở giữa.
Bên trên là cách bạn nghĩ về ma trận. Bên dưới là cách nó thật sự nằm trong bộ nhớ: ba hàng nối đuôi nhau.
/* Công thức địa chỉ của mảng hai chiều */
&m[i][j]  ==  (char *)m + (i * SO_COT + j) * sizeof m[0][0]
phang.c
#include <stdio.h>

int main(void)
{
    int m[3][4] = {
        {  1,  2,  3,  4 },
        {  5,  6,  7,  8 },
        {  9, 10, 11, 12 },
    };

    /* Duyệt như mảng hai chiều */
    for (size_t i = 0; i < 3; ++i)
        for (size_t j = 0; j < 4; ++j)
            printf("%d ", m[i][j]);

    printf("\n");

    /* Duyệt như mảng một chiều 12 phần tử, cho ra đúng thứ tự đó */
    int *p = &m[0][0];

    for (size_t k = 0; k < 12; ++k)
        printf("%d ", p[k]);

    printf("\n");
    printf("sizeof m       = %zu\n", sizeof m);
    printf("sizeof m[0]    = %zu\n", sizeof m[0]);
    printf("sizeof m[0][0] = %zu\n", sizeof m[0][0]);

    return 0;
}
terminal
./phang
1 2 3 4 5 6 7 8 9 10 11 12 
1 2 3 4 5 6 7 8 9 10 11 12 
sizeof m       = 48
sizeof m[0]    = 16
sizeof m[0][0] = 4
Biểu thứcKiểuÝ nghĩasizeof
mint (*)[4] sau phân rãCon trỏ tới hàng đầu48 khi chưa phân rã
m[0]int[4] sau phân rã thành int *Hàng thứ nhất16
m[0][0]intMột phần tử4
&m[0][0]int *Địa chỉ phần tử đầu8

#Truyền ma trận vào hàm

void f(int m[3][4]);      /* thực chất là int (*m)[4] */
void g(int m[][4]);       /* giống hệt f */
void h(int (*m)[4]);      /* giống hệt, và đây là sự thật */

void sai(int m[][]);      /* lỗi biên dịch */
void sai2(int **m);       /* biên dịch được nhưng SAI NGHĨA, xem cảnh báo dưới */
truyen.c
enum { COT = 4 };

void in_ma_tran(size_t hang, const int m[][COT])
{
    for (size_t i = 0; i < hang; ++i) {
        for (size_t j = 0; j < COT; ++j)
            printf("%4d", m[i][j]);

        putchar('\n');
    }
}

int m[3][COT] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } };

in_ma_tran(3, m);

Ba cách truyền ma trận, chọn cách nào

CáchNguyên mẫuƯu và nhược
Số cột cố địnhvoid f(size_t h, int m[][COT])Đơn giản, an toàn, nhưng chỉ dùng được với đúng số cột đó
Mảng phẳng kèm hai kích thướcvoid f(int *m, size_t h, size_t c)Linh hoạt mọi kích thước, nhưng phải tự viết m[i * c + j]
Tham số mảng độ dài thay đổivoid f(size_t h, size_t c, int m[h][c])Cú pháp đẹp nhất, nhưng là tính năng tùy chọn của C11
phang-kem-kich-thuoc.c
/* Cách linh hoạt nhất mà vẫn di động: coi ma trận là mảng phẳng. */
void in_ma_tran(const int *m, size_t hang, size_t cot)
{
    for (size_t i = 0; i < hang; ++i) {
        for (size_t j = 0; j < cot; ++j)
            printf("%4d", m[i * cot + j]);

        putchar('\n');
    }
}

int m[3][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } };

in_ma_tran(&m[0][0], 3, 4);      /* truyền địa chỉ phần tử đầu */

#Vì sao bắt buộc ghi số cột

Câu trả lời nằm gọn trong công thức địa chỉ: i * SO_COT + j. Trình biên dịch không thể sinh mã cho m[i][j] nếu không biết SO_COT. Số hàng thì không cần, vì nó không xuất hiện trong công thức.

Hàm không biết số cột
void f(int m[][]);        /* lỗi: array type has incomplete element type */

void g(int **m);          /* biên dịch được nhưng hiểu sai bố cục */
Số cột nằm trong kiểu, hoặc trong tham số
void f(size_t hang, int m[][4]);           /* số cột nằm trong kiểu */

void g(int *m, size_t hang, size_t cot);   /* số cột là tham số */
terminal
# Kiểm chứng: hai hàng cách nhau đúng COT * sizeof(int) byte
./khoang-cach
&m[0][0] = 0x7ffd1c2a3e10
&m[1][0] = 0x7ffd1c2a3e20
chenh lech = 16 byte = 4 cot * 4 byte

#Ba chiều trở lên

int khoi[2][3][4];       /* 2 lớp, mỗi lớp 3 hàng, mỗi hàng 4 cột */

/* Công thức địa chỉ mở rộng tự nhiên */
&khoi[i][j][k]  ==  (char *)khoi + ((i * 3 + j) * 4 + k) * sizeof(int)

/* Quy tắc truyền vào hàm: chỉ chiều ĐẦU TIÊN được lược bỏ */
void f(int k[][3][4]);   /* hợp lệ */
void g(int k[2][3][4]);  /* hợp lệ, con số 2 bị bỏ qua */
void h(int k[][][4]);    /* lỗi */
/* Mảng lớn phải là toàn cục hoặc static, hoặc cấp phát động ở Chương 14 */
static double luoi[100][100][100];      /* nằm ở .bss, an toàn */

int main(void)
{
    double luoi2[100][100][100];        /* 8 MB trên ngăn xếp, sập ngay */

    return 0;
}

Tự làm thử

  1. In địa chỉ của mọi phần tử một ma trận ba nhân bốn, xác nhận chúng liên tiếp theo đúng thứ tự hàng.
  2. Duyệt cùng một ma trận bằng hai cặp ngoặc vuông và bằng con trỏ phẳng một chiều, so sánh kết quả.
  3. In sizeof của m, m[0]m[0][0], giải thích ba con số.
  4. Viết ba phiên bản hàm in ma trận theo ba cách trong bảng, so sánh chỗ gọi và tính linh hoạt.
  5. Thử truyền int m[3][4] vào hàm nhận int **, đọc cảnh báo, rồi ép kiểu và quan sát chương trình sập.
  6. Khai báo double a[100][100][100] làm biến cục bộ và quan sát chương trình sập, sau đó thêm static và chạy lại.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Mảng hai chiều lưu theo hàng: toàn bộ hàng 0, rồi hàng 1, rồi hàng 2, liên tục không đứt quãng.
  • Địa chỉ tính bằng (i * số_cột + j) * sizeof(phần tử), nên số cột là thông tin bắt buộc còn số hàng thì không.
  • Khi truyền vào hàm, chỉ chiều đầu tiên được lược bỏ. int m[][] là lỗi.
  • int ** không phải ma trận. Nó hiểu bố cục hoàn toàn khác và làm chương trình sập.
  • Ma trận lớn phải khai báo static hoặc cấp phát động, vì ngăn xếp chỉ có vài megabyte.