Chuyển tới nội dung chính

Trình biên dịch g++

g++ là trình biên dịch ngôn ngữ C++ nằm trong bộ công cụ GCC (GNU Compiler Collection). Đây là một trong những trình biên dịch phổ biến nhất, được dùng nhiều trên các hệ điều hành dựa trên Unix (Linux, BSD, macOS), và có thể dùng trên Windows thông qua các hệ thống như MinGW, MSYS2, hoặc WSL.

Khi bạn chạy g++ với một file nguồn .cpp, trình biên dịch này sẽ tự động thực hiện cả hai giai đoạn:

  • Biên dịch file .cpp thành file .o (object)
  • Gọi trình liên kết ld để tạo file thực thi .exe (Windows) hoặc .out (Linux)

Cú pháp tổng quát

g++ [file nguồn] [cờ tiền xử lý] [cờ cấu hình biên dịch] [cờ cảnh báo] [cờ tối ưu hóa] [cờ include] [cờ liên kết] [cờ đầu ra]
Gợi ý thứ tự
  • [file nguồn] đặt ở đầu để dễ đọc
  • Nhóm cờ được nhóm theo chức năng, giúp dễ debug và mở rộng
  • Có thể thay đổi thứ tự, nhưng khuyến nghị theo nhóm rõ ràng

Các nhóm cờ và giải thích

Dưới đây sẽ liệt kê các cờ cơ bản và phổ biến đại diện cho mỗi nhóm cờ trong cú pháp tổng quát.

  1. Nhóm cờ tiền xử lý (Preprocessing)

    • -D<macro>: Định nghĩa macro trước khi biên dịch (giống như #define). Ví dụ: -DDEBUG.
    • -U<macro>: Gỡ bỏ macro đã định nghĩa.
    • -include file.h: Ép buộc include một file header từ đầu, trước cả các include khác.
    • -E: Thực hiện tiền xử lý và in kết quả ra stdout. Không biên dịch.
  2. Nhóm cờ cấu hình biên dịch

    • -std=c++20: Chỉ định phiên bản chuẩn C++ để biên dịch. Thường dùng c++11, c++17, c++20, v.v.
    • -g: Thêm thông tin debug vào file thực thi (sử dụng với gdb).
    • -fno-exceptions: Tắt hệ thống try-catch, giảm kích thước chương trình nếu không dùng exception.
    • -fno-rtti: Tắt RTTI (Run-time Type Information), giúp giảm kích thước chương trình nếu không dùng dynamic_cast hay typeid.
    • -nostdlib: Không tự động liên kết với thư viện chuẩn như libstdc++, libc. Dùng khi bạn muốn kiểm soát hoàn toàn liên kết.
    • -nostdinc: Không thêm đường dẫn thư viện tiêu chuẩn như /usr/include.
    • -nodefaultlibs: Không liên kết với bất kỳ thư viện mặc định nào (như libgcc, libstdc++, libc,...).
  3. Nhóm cờ cảnh báo

    • -Wall: Bật tập hợp cảnh báo phổ biến. Đây là cờ nên luôn bật.
    • -Wextra: Bật các cảnh báo bổ sung mà -Wall chưa bao gồm. Nên bật để phát hiện lỗi tiềm ẩn.
    • -pedantic: Tuân thủ nghiêm ngặt tiêu chuẩn C++ (báo lỗi những gì không chuẩn).
    • -Wconversion: Cảnh báo khi có chuyển kiểu ngầm định có thể gây mất dữ liệu.
    • -Wshadow: Cảnh báo khi biến khai báo mới che khuất (shadow) biến cũ.
    • -Wsign-conversion: Cảnh báo khi có chuyển đổi giữa số có dấu và không dấu.
    • -Werror: Chuyển tất cả cảnh báo thành lỗi, dừng biên dịch nếu có cảnh báo.
    • -fsanitize=address, -fsanitize=undefined: Kích hoạt AddressSanitizer hoặc UndefinedBehaviorSanitizer để phát hiện lỗi bộ nhớ hoặc hành vi không xác định.
  4. Nhóm cờ tối ưu hóa

    • -O0 / -O1 / -O2 / -O3: Tối ưu mã ở các mức độ khác nhau. -O0 là không tối ưu, -O2 là tối ưu thông thường, -O3 tối ưu mạnh.
    • -flto: Bật Link-Time Optimization, cải thiện hiệu năng.
  5. Nhóm cờ include (Header search path)

    • -I<dir>: Thêm thư mục vào danh sách tìm kiếm file header. Áp dụng cho cả #include "..."<...>.
    • -iquote <dir>: Chỉ thêm thư mục này cho #include "...", không áp dụng cho <...>.
    • -isystem <dir>: Thêm thư mục hệ thống (thường là SDK, thư viện chuẩn); cảnh báo trong các file header ở đây sẽ bị bỏ qua.
    • -idirafter <dir>: thêm thư mục tìm kiếm header nhưng ưu tiên thấp hơn -I.
  6. Nhóm cờ liên kết (Linker flags)

    • -L<dir>: Thêm thư mục vào đường dẫn tìm thư viện .a hoặc .so.
    • -l<name>: Link với thư viện lib<name>.a hoặc lib<name>.so. Ví dụ -lmlibm.a.
    • -pthread: Thêm cả cờ biên dịch và liên kết để hỗ trợ đa luồng POSIX.
    • -static: Yêu cầu liên kết thư viện tĩnh thay vì thư viện động.
    • -fuse-ld=lld: Dùng linker lld thay thế cho ld. Cần cài thêm lld.
    • -Wl,<option>: Truyền trực tiếp tùy chọn cho linker ld. Ví dụ: -Wl,--as-needed để giảm liên kết thư viện không cần thiết.
    • -shared-libgcc: Liên kết với thư viện GCC động thay vì tĩnh.
  7. Nhóm cờ đầu ra

    • -o <file>: Đặt tên file đầu ra. Nếu không chỉ định, mặc định là a.out (Linux) hoặc a.exe (Windows).
    • -c: Chỉ biên dịch, không liên kết. Tạo .o thay vì .exe.
    • -S: Sinh mã hợp ngữ (.s) thay vì mã máy.
    • -MMD: Tạo file .d chứa dependency để hỗ trợ build tự động (dùng cho makefile).
    • -shared: Biên dịch và liên kết để sinh thư viện động (.so trên Linux hoặc .dll trên Windows).
      • Khi sử dụng cờ -shared, g++ sẽ hiểu rằng bạn đang tạo một thư viện động và tự động chọn phần mở rộng đầu ra phù hợp, chẳng hạn .so trên Linux hoặc .dll trên Windows (với MinGW).
      • Nếu không có -shared, định dạng đầu ra mặc định là .out hoặc .exe.
    • -M: Tạo danh sách phụ thuộc (dependencies) mà không tạo file .d như -MMD.
    • -fPIC: Tạo mã máy độc lập vị trí (Position-Independent Code)
      • Bắt buộc khi build thư viện động (.so) trên Linux. Trên Windows (MinGW), có thể không cần, nhưng vẫn nên dùng để tăng tính tương thích và ổn định khi biên dịch thư viện .dll.
      • Không cần dùng khi build file thực thi .exe hoặc thư viện tĩnh .a.

Trình liên kết của g++

  • g++ sử dụng ld (GNU linker) làm trình liên kết mặc định.
  • Thông thường, người dùng không cần quan tâm đến ldg++ sẽ gọi nó tự động.
  • Có thể thay thế bằng linker khác như gold hoặc lld bằng cờ -fuse-ld=gold hoặc -fuse-ld=lld.
  • lld thường nhanh hơn ld và được ưa chuộng trong các dự án lớn, nhưng cần cài đặt thêm.
  • Khi biên dịch có nhiều file .o, g++ sẽ tự gọi ld để tạo file thực thi hoặc thư viện (nếu có cờ -shared).

Ví dụ

Biên dịch file thực thi .exe:

g++ src/main.cpp src/lib.cpp -std=c++20 -Wall -Wextra -Wconversion -O2 -Iinclude/ -Llib/ -lmylib -o build/myapp.exe

Giải thích:

  • File nguồn: src/main.cpp, src/lib.cpp
  • Cờ cấu hình biên dịch: -std=c++20
  • Cờ cảnh báo: -Wall, -Wextra, -Wconversion
  • Cờ tối ưu hóa: -O2
  • Cờ include: -Iinclude/
  • Cờ liên kết: -Llib/, -lmylib
  • Cờ đầu ra: -o build/myapp.exe

Biên dịch và sử dụng thư viện tĩnh (.a)

1. Biên dịch mã nguồn thành file đối tượng (.o)

  • Mục đích: Biên dịch các file mã nguồn (.cpp) thành file đối tượng (.o) mà không liên kết, để chuẩn bị cho việc tạo thư viện tĩnh.

  • Cú pháp tổng quát:

    g++ -c [-fPIC] <file.cpp> -o <file.o> [cờ biên dịch khác]
  • Ví dụ áp dụng cơ bản:

    g++ -c mylib.cpp -o mylib.o
    • -c: Chỉ biên dịch, không liên kết. Tạo file .o từ .cpp.
    • -o mylib.o: Chỉ định tên file đối tượng đầu ra.

    lưu ý
    • Nếu có nhiều file mã nguồn (ví dụ: file1.cpp, file2.cpp), lặp lại lệnh cho từng file.
    • Đảm bảo file .cpp đã include .h tương ứng để kiểm tra tính nhất quán của khai báo prototype và định nghĩa.

2. Đóng gói file đối tượng thành thư viện tĩnh (.a)

  • Mục đích: Dùng ar để đóng gói các file .o thành thư viện tĩnh .a, tạo điều kiện cho việc liên kết với chương trình chính.
  • Cú pháp tổng quát:
    ar rcs <libname.a> <file1.o> [file2.o ...]
  • Ví dụ áp dụng cơ bản:
    ar rcs libmylib.a mylib.o
    • ar: Công cụ GNU Binutils để tạo/quản lý file lưu trữ.
    • r: Thêm hoặc thay thế file .o vào thư viện.
    • c: Tạo thư viện mới nếu chưa tồn tại.
    • s: Tạo chỉ mục (symbol table) để linker tìm kiếm nhanh (tương đương ranlib).
    • libmylib.a: Tên thư viện tĩnh theo quy ước GNU. Khai báo tên thư viện .a ngay sau rcs là bắt buộc.
    • mylib.o: File đối tượng được đóng gói.
    • Nếu có nhiều file .o:
      ar rcs libmylib.a file1.o file2.o
    Quy tắc đặt tên thư viện (GNU)
    • Tên thư viện tĩnh phải bắt đầu bằng lib (ví dụ: libmylib.a).
    • Khi liên kết, chỉ cần dùng -lmylib (bỏ lib.a).
    • Đặt thư viện trong thư mục chuẩn như lib/ để dễ quản lý.
  • Kiểm tra thư viện sau khi đóng gói:
    • Liệt kê các file .o trong .a:
      ar -t libmylib.a
    • Xem symbol table (các ký hiệu như hàm/biến):
      nm -s libmylib.a
    • Trích xuất file .o từ .a (nếu cần):
      • Trích xuất tất cả file
        ar -x libmylib.a
      • Hoặc trích xuất file cụ thể
        ar -x libmylib.a mylib.o

3. Liên kết thư viện .a với chương trình chính

  • Mục đích: Biên dịch chương trình chính (.cpp) và liên kết với libmylib.a để tạo file thực thi.
  • Cú pháp tổng quát:
    g++ <main.cpp> -I<dir_include> -L<dir_lib> -l<libname> -o <output[.exe]> [cờ biên dịch khác]
  • Ví dụ áp dụng cơ bản:
    g++ main.cpp -Iinclude -Llib -lmylib -o myprogram
    • main.cpp: File mã nguồn chính, phải include mylib.h để sử dụng các hàm/biến từ thư viện.
    • -Iinclude: Chỉ định thư mục chứa mylib.h (nếu không ở cùng thư mục với main.cpp).
    • -Llib: Chỉ định thư mục chứa libmylib.a.
    • -lmylib: Liên kết với libmylib.a (bỏ lib.a theo quy ước GNU).
    • -o myprogram: Tên file thực thi đầu ra.

Biên dịch và sử dụng thư viện động (.dll)

Thư viện động .dll (Dynamic-Link Library) trong hệ sinh thái GNU (dùng g++ với MinGW/MSYS2 trên Windows) cho phép chia sẻ mã thực thi giữa nhiều chương trình, giảm kích thước file thực thi (.exe) và hỗ trợ tải động tại runtime. Quy trình dưới đây mô tả cách biên dịch và sử dụng .dll theo hai phương pháp:

  • Liên kết tĩnh (implicit linking): Gọi hàm trực tiếp qua file .a (import library).

  • Tải động (explicit linking): Tự nạp .dll tại runtime bằng API hệ thống (LoadLibrary/GetProcAddress).

    thông tin

    Cả hai cách đều chỉ yêu cầu phân phối .exe.dll khi triển khai.

1. Biên dịch mã nguồn thành file đối tượng (.o)

  • Mục đích: Biên dịch các file mã nguồn (.cpp) thành file đối tượng (.o) mà không liên kết, chuẩn bị cho việc tạo thư viện động. Và cho mục đích khác là tái sử dụng .o.

  • Cú pháp tổng quát:

    g++ -c [-fPIC] <file.cpp> -o <file.o> [cờ biên dịch khác]
  • Ví dụ áp dụng cơ bản:

    g++ -c -fPIC mylib.cpp -o mylib.o
    • -c: Chỉ biên dịch, không liên kết.
    • -fPIC: Tạo mã độc lập vị trí (Position-Independent Code), bắt buộc nếu build thư viện động trên Linux.
    • -o mylib.o: Tên file đối tượng đầu ra.
    Các cờ bắt buộc và cần thiết
    • -cbắt buộc để chỉ biên dịch, không liên kết.
    • -ocần thiết để đặt tên file đầu ra rõ ràng, nhất là khi có nhiều file, tránh dùng mặc định a.o.
    • -fPIC:
      • Bắt buộc nếu build .so trên Linux, đặc biệt khi dùng nhiều .o hoặc xuất symbol.
      • Không bắt buộc trên Windows, vì định dạng .dll dùng PE/COFF, cơ chế khác với ELF.
      • g++ sẽ tự bỏ qua -fPIC trên Windows, vẫn nên thêm để đảm bảo tính tương thích nếu chuyển thư viện sang Linux.
  • File header:

    • Cần tạo file header (.h) chứa khai báo hàm/biến với chỉ thị __declspec(dllexport)/__declspec(dllimport) để xuất/nhập ký hiệu. Để tương thích với ngôn ngữ khác, dùng extern "C" để tránh name mangling.
    • Ví dụ với include/mylib.h
      #pragma once

      #ifdef _WIN32
      #ifdef BUILD_MYLIB
      #define MYLIB_API __declspec(dllexport)
      #else
      #define MYLIB_API __declspec(dllimport)
      #endif
      #else
      #ifdef BUILD_MYLIB
      #define MYLIB_API __attribute__((visibility("default")))
      #else
      #define MYLIB_API
      #endif
      #endif

      #ifdef __cplusplus
      extern "C" {
      #endif

      MYLIB_API int add(int a, int b);

      #ifdef __cplusplus
      }
      #endif
    • Ví dụ mã nguồn src/mylib.cpp
      #include "mylib.h"

      int add(int a, int b) {
      return a + b;
      }
    lưu ý
    • extern "C" đảm bảo tên hàm không bị name mangling (ví dụ: add thay vì _Z3addii), cần cho tải động.
    • Đảm bảo mylib.cpp có include mylib.h để kiểm tra tính nhất quán.

2. Tạo thư viện động (.dll) và import library (.a)

  • Mục đích: Liên kết các file .o thành .dll và tạo import library .a (dùng cho liên kết tĩnh).

  • Cú pháp tổng quát:

    g++ -shared <file.o> -o <file.dll> [-Wl,--out-implib,<file.a>] [-D<macro>]
  • Ví dụ áp dụng cơ bản:

    g++ -shared mylib.o -o lib/mylib.dll -Wl,--out-implib,lib/libmylib.a -DBUILD_MYLIB
    • -shared: Tạo thư viện động .dll thay vì file thực thi.
    • -o lib/mylib.dll: Tên file thư viện động đầu ra.
    • -Wl,--out-implib,lib/libmylib.a: Tạo import library libmylib.a cho liên kết tĩnh.
    • -DBUILD_MYLIB: Định nghĩa macro để sử dụng __declspec(dllexport) khi biên dịch.
    • Nếu có nhiều file .o:
      g++ -shared file1.o file2.o -o lib/mylib.dll -Wl,--out-implib,lib/libmylib.a -DBUILD_MYLIB
    lưu ý
    • -Wl,--out-implib,lib/libmylib.a: Là một cờ duy nhất, dùng để yêu cầu linker (ld) tạo import library .a phục vụ liên kết tĩnh (implicit linking).
      • -Wl,: Chuyển tiếp toàn bộ phần phía sau dấu phẩy , xuống cho linker (ld).
      • --out-implib,lib/libmylib.a: Là một tùy chọn đơn lẻ của linker, yêu cầu linker xuất file .a (lib/libmylib.a) chứa thông tin liên kết tới .dll.
  • Quy tắc đặt tên:

    • Thư viện động: <ten>.dll (ví dụ: mylib.dll), không bắt buộc tiền tố lib.
    • Import library: lib<ten>.a (ví dụ: libmylib.a), dùng với -l<ten> (như -lmylib) khi liên kết.
    • Đặt .dll.a trong thư mục lib/:
      project/
      ├── include/mylib.h
      ├── lib/libmylib.a
      ├── lib/mylib.dll
  • Kiểm tra sau khi tạo:

    • Xem ký hiệu xuất trong .dll
      nm --defined-only lib/mylib.dll
      Kết quả: Hiển thị tên hàm như add (nếu dùng extern "C").
    • Xem thông tin chi tiết .dll
      objdump -p lib/mylib.dll
    • Kiểm tra import library
      nm -s lib/libmylib.a
    • Trích xuất .o từ .a (nếu cần)
      ar -x lib/libmylib.a
  • Lưu ý:

    • File .a chỉ cần cho liên kết tĩnh, không cần cho tải động.
    • Có thể tạo .def để:
      • Kiểm soát ký hiệu xuất
      • Kiểm soát chính xác tên hàm export
      • Tránh bị name mangling hoặc export hàm không mong muốn
      • Hữu ích khi không dùng __declspec(dllexport) hoặc muốn giảm phụ thuộc
      • Nội dung .def:
        LIBRARY "mylib"
        EXPORTS
        add
        • LIBRARY là tên DLL (không cần đuôi .dll)
        • EXPORTS liệt kê các hàm public
      • Biên dịch với .def:
        g++ -shared -o lib/mylib.dll mylib.o -Wl,--output-def,lib/mylib.def -DBUILD_MYLIB
    • Nếu chỉ cần tải động, có thể bỏ --out-implib:
      g++ -shared -o lib/mylib.dll mylib.o -DBUILD_MYLIB

3. Sử dụng thư viện động .dll

  1. Liên kết tĩnh (Implicit linking)
    • Mục đích: Biên dịch chương trình chính, liên kết với libmylib.a để gọi hàm từ .dll như các hàm thông thường.
    • Yêu cầu:
      • libmylib.a: Import library chứa symbol để linker tạo lời gọi tới .dll.
      • mylib.h: Khai báo hàm với __declspec(dllimport) (qua macro nếu dùng header dùng chung export/import).
      • mylib.dll: Cần có mặt khi chạy chương trình.
    • Cú pháp tổng quát:
      g++ <main.cpp> -I<dir_include> -L<dir_lib> -l<libname> -o <output[.exe]> [cờ khác]
    • Ví dụ áp dụng cơ bản:
      g++ main.cpp -Iinclude -Llib -lmylib -o myapp.exe
      • main.cpp: Gọi các hàm được khai báo trong mylib.h.
      • -Iinclude: Thư mục chứa mylib.h.
      • -Llib: Thư mục chứa libmylib.a.
      • -lmylib: Liên kết với libmylib.a.
      • -o myapp.exe: File thực thi đầu ra.
    • Lưu ý quan trọng:
      • Cần mylib.h với __declspec(dllimport) để chương trình biết khai báo các hàm từ .dll.
      • Nhiều chương trình .exe khác nhau có thể dùng chung mylib.dll, miễn là chúng đều được biên dịch với libmylib.a.
      • Tại runtime, file mylib.dll phải nằm cùng thư mục với myapp.exe, hoặc:
        • Được thêm vào biến môi trường PATH
        • Chỉ định rõ đường dẫn thư mục chứa .dll bằng linker: -Wl,-rpath,<dir> (trên Linux) hoặc sử dụng công cụ riêng cho Windows (như . manifest hoặc SetDllDirectory).
      • Cấu trúc thư mục thường dùng:
        bin/myapp.exe
        bin/mylib.dll
        lib/libmylib.a
        include/mylib.h
  2. Tải động (Explicit linking)
    • Mục đích: Tải .dll tại runtime, không cần libmylib.a, phù hợp cho plugin, tình huống cần linh hoạt, hoặc sử dụng từ ngôn ngữ khác như Python, C#, v.v.
    • Ưu điểm:
      • Cho phép kiểm soát lỗi khi không tìm thấy .dll, linh hoạt (plugin, module).
      • Nếu .dll được xây dựng đúng tiêu chuẩn (sử dụng extern "C" và chuẩn gọi hàm rõ ràng như __cdecl hoặc __stdcall), nó có thể được gọi từ nhiều ngôn ngữ khác.
    • Yêu cầu:
      • Tên hàm (như hàm "add" trong ví dụ bên dưới) phải khớp tuyệt đối với tên xuất trong .dll.
      • Trong thư viện, hàm phải khai báo với extern "C" để tránh name mangling.
      • Cần include <windows.h> trên Windows, <dlfcn.h> trên Linux/macOS.
    • Ví dụ minh hoạ:
      src/main.cpp (Windows)
      #include <windows.h>
      #include <iostream>

      typedef int (*AddFunc)(int, int);

      int main() {
      HINSTANCE hDLL = LoadLibrary("mylib.dll");
      if (!hDLL) {
      std::cerr << "Không thể tải mylib.dll\n";
      return 1;
      }

      AddFunc add = (AddFunc)GetProcAddress(hDLL, "add");
      if (!add) {
      std::cerr << "Không tìm thấy hàm add\n";
      return 1;
      }

      std::cout << "2 + 3 = " << add(2, 3) << "\n";
      FreeLibrary(hDLL);
      return 0;
      }
      src/main.cpp (Linux/macOS)
      #include <dlfcn.h>
      #include <iostream>

      int main() {
      void* handle = dlopen("libmylib.so", RTLD_LAZY);
      if (!handle) {
      std::cerr << dlerror() << "\n";
      return 1;
      }

      typedef int (*AddFunc)(int, int);
      AddFunc add = (AddFunc)dlsym(handle, "add");

      if (!add) {
      std::cerr << dlerror() << "\n";
      return 1;
      }

      std::cout << "2 + 3 = " << add(2, 3) << "\n";
      dlclose(handle);
      }
    • Biên dịch chương trình:
      g++ src/main.cpp -o myapp.exe
    lưu ý
    • Không cần libmylib.a hoặc mylib.h, nhưng bạn cần:
      • Biết chính xác tên hàm export ("add") và prototype của nó.
      • Biết chuẩn gọi hàm (thường là __cdecl mặc định trên Windows).
    • Có thể dùng nm --defined-only lib/mylib.dll để liệt kê symbol nếu .dll không bị strip.
    • Nên cung cấp tài liệu .dll (header hoặc PDF) cho người dùng thư viện: tên hàm, kiểu dữ liệu, calling convention, v.v.