Menu

Array 2D di C: array multidimensi, tata letak memori, dan matriks

Cara mendeklarasikan, menginisialisasi, dan melakukan perulangan pada array 2D di C, apa arti tata letak row-major di memori, kenapa mengirim array 2D ke fungsi butuh jumlah kolom, dan contoh matriks yang lengkap.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Array multidimensi adalah array yang elemen-elemennya juga berupa array. int grid[3][4]; bukan tipe grid khusus: itu tiga elemen, masing-masing berupa array berisi empat int, yang disimpan berurutan. Begitu ini dipahami, sisa topiknya mengikuti: tata letaknya, aritmetika indeksnya, dan aturan yang tadinya membingungkan soal mengirimnya ke fungsi.

Dua dimensi sudah mencakup hampir semua kebutuhan praktis, seperti grid, tabel, matriks, papan permainan, dan gambar, jadi itulah yang dipakai di halaman ini.

Deklarasi dan inisialisasi

int grid[3][4];        // 3 rows, 4 columns - 12 ints

Angka pertama adalah jumlah baris, yang kedua jumlah kolom. Inisialisasinya bisa ditulis datar atau dengan kurung kurawal di dalam; kurung kurawal itu layak dipakai karena memperlihatkan bentuknya.

Bentuk int e[][3] itu penting: kamu boleh mengosongkan jumlah baris dan membiarkan inisialisasinya yang menentukan, tapi jumlah kolom tidak pernah opsional. Bagian berikutnya menjelaskan alasannya.

Tata letak row-major

C menyimpan array 2D dalam urutan row-major: seluruh baris 0, lalu seluruh baris 1, dan seterusnya, dalam satu blok memori yang tidak terputus. Tidak ada array berisi pointer-pointer baris di belakang layar.

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

how you picture it            how it actually sits in memory
+----+----+----+----+
|  1 |  2 |  3 |  4 |   row 0   +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+           | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
|  5 |  6 |  7 |  8 |   row 1   +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+            \__ row 0 __/\__ row 1 __/\_ row 2 _/
|  9 | 10 | 11 | 12 |   row 2
+----+----+----+----+           grid[i][j] is at element index i*4 + j

Rumus itu, i * columns + j, adalah seluruh mekanismenya, dan karena itulah compiler harus tahu jumlah kolom untuk mengindeks apa pun. Jumlah baris tidak pernah ikut dalam perhitungannya.

Kamu bisa melihat tata letaknya langsung dengan mencetak alamatnya:

Alamatnya naik sebesar sizeof(int) tanpa celah, termasuk di tempat satu baris berakhir dan baris berikutnya dimulai. Loop yang diratakan membuktikannya: flat[k] menelusuri kedua belas elemen sebagai satu deretan.

Tata letak ini juga punya konsekuensi performa yang perlu diketahui: melakukan perulangan per baris lalu per kolom menyentuh memori secara berurutan, dan itu disukai cache CPU. Menukar urutan loop sehingga loop dalam menuruni sebuah kolom akan melompat sejauh satu baris penuh setiap kali, dan bisa berjalan beberapa kali lebih lambat untuk array yang besar.

Nested loop

Dua dimensi butuh dua for loop: loop luar memilih baris, loop dalam menyapu kolom-kolom di baris itu.

Beri nama penghitungnya sesuai artinya (i/row untuk baris, j/col untuk kolom) dan jaga urutannya tetap konsisten: grid[row][col] di mana-mana. Separuh dari semua bug array 2D adalah pasangan indeks yang tertukar.

Ukuran yang ditulis dengan #define juga bukan hiasan: batas loop dan deklarasinya sekarang tidak bisa jadi tidak sinkron saat kamu mengubah bentuknya.

Mengirim array 2D ke fungsi

Inilah aturan yang menjebak semua orang: parameter fungsi harus menyebutkan jumlah kolom.

Alasannya adalah decay. Mengirim grid mengubahnya menjadi pointer ke elemen pertamanya, dan elemennya adalah baris, jadi tipenya int (*)[4]: pointer ke array berisi 4 int. Agar grid[i][j] punya arti, compiler harus tahu seberapa panjang satu baris, dan itulah angka 4. Jumlah baris memang tidak ada di tipenya, dan karena itulah jumlah baris dikirim sebagai argumen terpisah.

Perhatikan bahwa int (*grid)[COLS] dan int grid[][COLS] adalah parameter yang sama yang ditulis dengan dua cara. Tanda kurungnya wajib, karena int *grid[COLS] justru berarti array berisi pointer. Perbedaan itu dibahas di halaman pointer dan array.

Kalau jumlah kolom baru diketahui saat runtime, parameter variably-modified dari C99 memungkinkanmu mengirimnya lebih dulu:

void print_any(int rows, int cols, int grid[rows][cols]);

rows dan cols harus dideklarasikan sebelum parameter array yang memakainya. Kalau fitur itu tidak tersedia, alternatif yang umum adalah array 1D datar ditambah aritmetika indeks manual:

data[i * cols + j] persis seperti yang ditulis compiler untukmu di kasus ukuran tetap. Melakukannya sendiri hanya butuh satu baris dan bekerja untuk bentuk apa pun yang ditentukan saat runtime.

Contoh matriks

Perkalian matriks menyatukan seluruh isi halaman ini: tiga nested loop di atas penyimpanan row-major.

Ada dua detail yang layak ditiru. Loop k di bagian dalam memasangkan a[i][k] dengan b[k][j]: satu indeks menelusuri baris, yang lain menelusuri kolom. Lalu transpose memulai loop dalamnya dari j = i + 1: kalau dimulai dari 0, setiap pasangan akan ditukar dua kali dan matriksnya tidak berubah.

Tiga dimensi dan seterusnya

Polanya berlanjut, begitu juga aturan soal parameter fungsi: setiap dimensi kecuali yang pertama harus dideklarasikan.

Dalam praktiknya, tiga dimensi adalah titik di mana array berukuran tetap mulai terasa merepotkan, dan kebanyakan kode beralih ke blok datar dengan indeks yang dihitung, atau ke array berisi struct yang menamai arti setiap sumbunya.

Kesalahan umum

  • Menulis grid[i, j]. Operator koma mengevaluasi i, membuangnya, lalu mengindeks dengan j. Kodenya bisa dikompilasi. Hasilnya salah. Pakai grid[i][j].
  • Indeks yang tertukar. grid[col][row] membaca elemen sungguhan dari tempat yang salah, jadi tidak ada error yang bisa menangkapnya. Jaga urutan [row][col] di mana-mana.
  • Tidak menulis ukuran kolom di parameter. void f(int grid[][]) tidak bisa dikompilasi, dan di situ compiler sedang menyelamatkanmu.
  • Keluar dari batas. Seperti array lainnya, tidak ada pemeriksaan batas. grid[0][5] pada grid [3][4] diam-diam membaca grid[1][1], karena tata letaknya bersambung dan aritmetikanya tidak peduli.

Pertanyaan yang sering diajukan

Bagaimana cara mendeklarasikan array 2D di C?

Tulis dua ukuran di dalam kurung siku: int grid[3][4]; mendeklarasikan 3 baris dengan 4 kolom, total 12 int. Bacalah sebagai "array berisi 3 elemen, masing-masing berupa array berisi 4 int", dan memang persis begitulah C menyimpannya.

Bagaimana array 2D disimpan di memori di C?

Dalam urutan row-major: semua elemen baris 0, lalu semua elemen baris 1, dan seterusnya, dalam satu blok yang bersambung. grid[i][j] berada di offset i * columns + j elemen dari awal, dan karena itulah jumlah kolom adalah angka yang dibutuhkan compiler.

Bagaimana cara mengirim array 2D ke fungsi di C?

Parameternya harus menyebutkan jumlah kolom: void print(int grid[][4], int rows) atau yang setara, void print(int (*grid)[4], int rows). Jumlah baris boleh dihilangkan karena array berubah (decay) menjadi pointer ke sebuah baris, tapi tanpa ukuran kolom compiler tidak bisa menghitung di mana sebuah baris dimulai.

Bisakah array 2D diinisialisasi dengan nol semua?

Bisa: int grid[3][4] = {0}; mengisi setiap elemen dengan nol, karena elemen yang tidak kamu sebutkan diinisialisasi dengan nol. int grid[3][4] = {{1, 2}}; mengisi dua elemen pertama baris 0 dan membiarkan sepuluh lainnya bernilai nol.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai