Bilangan complex pada chapter sebelumnya adalah vektor dua komponen yang matematikanya sudah siap pakai; sekarang kita generalisasi ke ukuran berapa pun — vektor n komponen dan matriks baris × kolom — dan membahas pertanyaan yang sering dijawab tergesa-gesa: bagaimana menyimpannya di C++. Ada tiga kandidat utama, dan pilihan Anda menentukan bukan sekadar gaya kode, melainkan kecepatan juga: std::array untuk dimensi kecil yang tetap, std::vector<std::vector<double>> yang nyaman tetapi menyebar di memori, dan flat vector — satu std::vector panjang dengan indeks dua dimensi yang dihitung manual lewat rumus i*cols + j. Chapter ini membangun ketiganya, menghitung dot product dan perkalian matriks pada representasi flat, lalu menjelaskan mengapa pustaka numerik sungguhan (BLAS, LAPACK, dan kawan-kawannya) hampir selalu memilih larik datar.
Tiga Kandidat Representasi #
| Representasi | Cocok untuk | Kelebihan | Kelemahan |
|---|---|---|---|
std::array<double, N> | dimensi kecil yang tetap (3D, kuaternion) | tanpa alokasi dinamis, ukuran diketahui saat kompilasi, se-kontigu | ukuran terkunci di tipe |
vector<vector<double>> | prototyping, baris yang panjangnya berbeda (jagged) | sintaks m[i][j] familiar, tiap baris bisa dibuang/ganti | banyak alokasi, data tercecer, baris tidak harus konsisten |
flat vector<double> + indeks | kode numerik serius, data besar | satu blok memori kontigu, satu alokasi, siap dikirim ke pustaka C/Fortran | indeks manual i*cols + j harus disiplin |
Konvensi penyimpanan flat di atas adalah row-major: elemen baris 0 diletakkan lebih dulu, lalu baris 1, dan seterusnya — persis cara C dan C++ menyimpan array multi-dimensi bawaan. Indeks (i, j) menerjemahkan ke posisi i * cols + j, dan fungsi kecil bernama at akan membungkus rumus itu supaya sisa kode tetap terbaca seperti matematika.
Dot Product, Indeks Flat, dan Perkalian Matriks #
#include <array>
#include <iostream>
#include <vector>
int main() {
// 1. Vektor kecil berukuran tetap: std::array
std::array<double, 3> a{1.0, 2.0, 3.0};
std::array<double, 3> b{4.0, 5.0, 6.0};
double dot = 0.0;
for (std::size_t i = 0; i < a.size(); ++i) {
dot += a[i] * b[i];
}
std::cout << "dot(a,b) = " << dot << "\n";
// 2. Matriks nested: setiap baris adalah vector tersendiri
std::vector<std::vector<double>> nested = {
{1.0, 2.0, 3.0},
{4.0, 5.0, 6.0},
};
std::cout << "nested = " << nested[1][0] << " " << nested[1][1]
<< " " << nested[1][2] << "\n";
// 3. Matriks flat: satu vector, indeks dua dimensi dihitung manual
const int rows = 2;
const int cols = 3;
std::vector<double> flat = {
1.0, 2.0, 3.0, // baris 0
4.0, 5.0, 6.0, // baris 1
};
auto at = [&](int i, int j) { return flat[i * cols + j]; };
std::cout << "flat(1,0) = " << at(1, 0) << ", flat(0,2) = "
<< at(0, 2) << "\n";
// 4. Perkalian matriks 2x3 * 3x2 = 2x2 pada representasi flat
std::vector<double> bMatriks = {
7.0, 8.0,
9.0, 10.0,
11.0, 12.0,
};
std::vector<double> c(4, 0.0);
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 2; ++j) {
for (int l = 0; l < cols; ++l) {
c[i * 2 + j] += at(i, l) * bMatriks[l * 2 + j];
}
}
}
std::cout << "A x B = " << c[0] << " " << c[1] << "\n";
std::cout << " " << c[2] << " " << c[3] << "\n";
return 0;
}Output yang Dihasilkan:
dot(a,b) = 32
nested = 4 5 6
flat(1,0) = 4, flat(0,2) = 3
A x B = 58 64
139 154Bedah baris-per-baris:
- Baris 7-13 (
std::array<double, 3> a ... dot += a[i] * b[i];): dot product menjumlahkan hasil kali komponen yang seposisi: 1×4 + 2×5 + 3×6 = 32.std::arraymenampung enamdoubleini di stack tanpa satu pun alokasi heap — pilihan alami untuk vektor 3D grafika dan fisika. - Baris 16-19 (
std::vector<std::vector<double>> nested = {...}): matriks nested sesungguhnya adalah vector berisi dua vector baris. Aksesnested[1][0]berarti dua lompatan: ambil baris ke-1 (sebuah objek vector), lalu elemen ke-0 di dalamnya. - Baris 24-30 (
const int cols = 3; ... auto at = [&](int i, int j) ...): data yang sama disimpan sebagai enamdoubleberurutan, dan lambdaatmenerapkan rumus row-majori * cols + j. Uji kecilnya konsisten:flat(1,0)= posisi 3 bernilai 4,flat(0,2)= posisi 2 bernilai 3 — sama dengan matriks aslinya. - Baris 35-39 (
std::vector<double> bMatriks = {...}): matriks B berukuran 3×2 juga flat — tiga baris, dua kolom, diakses denganl * 2 + j(dua adalah jumlah kolom B). - Baris 40-47 (
std::vector<double> c(4, 0.0); ... tiga loop ...): inilah algoritma perkalian matriks klasik: setiap selC[i][j]menjumlahkan hasil kali barisidari A dengan kolomjdari B. Tiga loop luar-dalam-luar menghasilkan 2×2 sel, masing-masing tiga kali kali-tambah. - Baris 48-49 (
std::cout << "A x B = " ...): hasil mengikuti hitungan manual — 1×7+2×9+3×11 = 58, 1×8+2×10+3×12 = 64, 4×7+5×9+6×11 = 139, 4×8+5×10+6×12 = 154. Pencetakan dua baris menjaga bentuk matriksnya tetap terlihat.
Flat vs Nested: Cerita Cache Locality #
Perbedaan dua representasi itu bukan soal selera, melainkan soal cache locality. CPU modern mengambil data dari memori utama dalam blok-blok berurutan (cache line); ketika enam double matriks flat terletak berdempetan, pembacaan berurutan mendapat blok-blok itu “gratis” dari prefetcher hardware. Matriks nested memaksa tiap baris mengalokasi sendiri di lokasi yang bisa berjauhan — akses [i][j] adalah pointer chasing: dereferensi baris dulu, baru elemen. Ditambah lagi, nested butuh rows + 1 alokasi dinamis, sementara flat cukup satu. Untuk matriks ribuan × ribuan, perbedaan ini bisa berkali lipat kecepatannya, dan itulah alasan pustaka numerik dunia (BLAS/LAPACK) menyimpan semuanya sebagai satu larik datar dengan stride. Kita hanya menyentuh dasarnya di sini; pengukuran dan teknik mempercepat urutan loop dibahas tuntas di Section Performa dan Optimasi.
Setelah data terstruktur rapi, lengkaplah sudah bekalnya: sekarang kita tinggal memanggil fungsi matematika standar. Chapter Operasi Numerik berikutnya menelusuri <cmath> — dari sqrt sampai remainder — beserta cara aman menangani keluaran tak terdefinisi dan konversi antar tipe angka.
Rujukan resmi untuk topik Chapter ini: std::array , std::vector , dan std::mdspan di cppreference.