Pada Chapter Separate Compilation kita mengompilasi file per file, menyimpan object file, lalu menyuruh linker menyambungnya — tanpa benar-benar melihat apa yang disambungkan. Kali ini kita membuka kotaknya: mata uang linker adalah simbol, nama internal untuk setiap fungsi dan objek yang satu object file sediakan dan object file lain butuhkan. Memahami simbol membuat pesan undefined reference to ... yang tadinya tidak terbaca berubah menjadi peta rute yang jelas — dan tool nm akan membuat kita bisa membacanya langsung dari object file.

Simbol: Nama yang Dilihat Linker #

Setiap fungsi dan variabel global yang kita tulis menjadi simbol di object file, dan setiap simbol berada dalam salah satu dari dua kondisi: defined — kode atau data sesungguhnya ada di file itu; undefined — file itu memakai nama tersebut, tetapi implementasinya harus dicari di tempat lain. Tugas linker persis mencocokkan keduanya: setiap simbol undefined di satu file harus menemukan satu definisi di file lain, pustaka, atau ditolak dengan error. Perhatikan pula bahwa nama simbol tidak persis sama dengan nama C++ kita: karena C++ mendukung overload — banyak fungsi bernama sama dengan parameter berbeda — compiler menyandikan tipe parameter ke dalam nama simbol, teknik yang disebut name mangling. Fungsi ringkas tadi tersimpan sebagai _Z7ringkasB5cxx11RKSt6vectorI8ItemNotaSaIS0_EE, bukan ringkas.

Membaca Simbol dengan nm #

Tool standar untuk mengintip simbol adalah nm dari binutils. Kita pakai pada modul nota tiga file berikut:

cpp nota.h
#include <string>
#include <vector>

struct ItemNota {
    std::string nama;
    int harga;
    int jumlah;
};

int totalNota(const std::vector<ItemNota>& items);
std::string ringkas(const std::vector<ItemNota>& items);
void cetakNota(const std::vector<ItemNota>& items);
cpp nota.cpp
#include <iostream>
#include "nota.h"

int totalNota(const std::vector<ItemNota>& items) {
    int total = 0;
    for (const ItemNota& item : items) {
        total += item.harga * item.jumlah;
    }
    return total;
}

std::string ringkas(const std::vector<ItemNota>& items) {
    return std::to_string(items.size()) + " item, total Rp"
         + std::to_string(totalNota(items));
}

void cetakNota(const std::vector<ItemNota>& items) {
    for (const ItemNota& item : items) {
        std::cout << "- " << item.nama << " x" << item.jumlah
                  << " = " << item.harga * item.jumlah << "\n";
    }
    std::cout << "Total: " << totalNota(items) << "\n";
}
cpp utama.cpp
#include <iostream>
#include "nota.h"

int main() {
    std::vector<ItemNota> belanja {
        {"Buku", 15000, 2},
        {"Pulpen", 5000, 3}
    };

    std::cout << ringkas(belanja) << "\n";
    cetakNota(belanja);
    return 0;
}

Kompilasi keduanya menjadi object file, lalu tanya utama.o — output nyata dari lingkungan praktikum, dengan nama main disaring bersama dua fungsi yang kita cari:

bash
g++ -std=c++23 -Wall -Wextra -c utama.cpp
g++ -std=c++23 -Wall -Wextra -c nota.cpp
nm utama.o | grep -E "main|ringkas|cetakNota"

Output yang Dihasilkan:

text
                 U _Z7ringkasB5cxx11RKSt6vectorI8ItemNotaSaIS0_EE
                 U _Z9cetakNotaRKSt6vectorI8ItemNotaSaIS0_EE
0000000000000000 T main

Huruf di tiap baris adalah status simbol: T berarti defined pada bagian kode (text section), sedangkan U berarti undefined — utama.o memanggil ringkas dan cetakNota dan menyerahkan pencariannya ke linker. Adapun nota.o yang menampung definisinya bisa kita baca dengan opsi -C untuk men-demangle nama:

bash
nm -C nota.o | grep -E "totalNota|ringkas|cetakNota"

Output yang Dihasilkan:

text
000000000000008c T ringkas[abi:cxx11](std::vector<ItemNota, std::allocator<ItemNota> > const&)
0000000000000175 T cetakNota(std::vector<ItemNota, std::allocator<ItemNota> > const&)
0000000000000000 T totalNota(std::vector<ItemNota, std::allocator<ItemNota> > const&)
Huruf nmArti
TDefined — berada di bagian kode (text)
UUndefined — butuh definisi dari file lain
DDefined — berada di bagian data terinisialisasi
RDefined — berada di bagian data read-only

Tiga T di atas berarti ketiga fungsi memang terdefinisi di nota.o — simbol undefined di utama.o sudah punya pasangan.

Saat Simbol Tidak Ketemu #

Apa yang terjadi bila ada simbol yang tidak menemukan pasangannya? Kita uji: nota.cpp versi yang kehilangan definisi cetakNota (deklarasi di header tetap ada, sehingga kompilasi utama.cpp tetap lolos):

cpp nota-tanpa-cetak.cpp — cetakNota tidak pernah didefinisikan
#include "nota.h"

int totalNota(const std::vector<ItemNota>& items) {
    int total = 0;
    for (const ItemNota& item : items) {
        total += item.harga * item.jumlah;
    }
    return total;
}

std::string ringkas(const std::vector<ItemNota>& items) {
    return std::to_string(items.size()) + " item, total Rp"
         + std::to_string(totalNota(items));
}

// cetakNota sengaja TIDAK didefinisikan di file ini
bash
g++ -std=c++23 -Wall -Wextra -c nota-tanpa-cetak.cpp -o nota-salah.o
g++ utama.o nota-salah.o -o nota-salah
text
/usr/bin/ld: utama.o: in function `main':
utama.cpp:(.text+0x198): undefined reference to `cetakNota(std::vector<ItemNota, std::allocator<ItemNota> > const&)'
collect2: error: ld returned 1 exit status

Inilah error undefined reference dalam bentuk paling murni — dan sekarang kita bisa membacanya baris demi baris: linker (ld) sedang menyelesaikan utama.o, tepatnya di fungsi main, dan menabrak simbol cetakNota yang tidak punya definisi di mana pun. Versi kurang ramah yang sering Anda lihat di internet menampilkan nama mangled panjang — itu simbol yang sama, hanya belum di-demangle.

Setelah semua simbol bertemu pasangannya, executable siap berjalan. Namun linker ternyata punya trik lebih dari sekadar menyambung potongan kita sendiri: ia juga tahu cara mengambil kode dari kumpulan simbol yang sudah dikemas — library. Chapter berikutnya membuka pintu itu: static library .a dan dynamic library .so, cara membuat keduanya dari nol.

Referensi: Link Options di dokumentasi GCC .