Menu

Biên dịch C++: từ mã nguồn thành tệp thực thi với g++

Cách C++ biến mã nguồn .cpp thành tệp thực thi gốc: biên dịch bằng g++ (hoặc clang++/MSVC), chạy tệp nhị phân, và đọc lỗi biên dịch khi có sự cố.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Biên dịch, rồi chạy

Giờ bạn đã cài trình biên dịch, việc chạy một chương trình C++ gồm hai bước. Khác với ngôn ngữ kịch bản, nơi trình thông dịch đọc tệp của bạn từng dòng một, C++ trước hết biên dịch (compile) mã nguồn thành một tệp thực thi gốc, tức một tệp nhị phân mã máy độc lập, rồi bạn chạy trực tiếp tệp nhị phân đó. Không có trình thông dịch nào đứng giữa chương trình và CPU lúc chạy.

Sự tách biệt đó là lý do C++ chạy nhanh, và cũng là lý do một lỗi cú pháp chặn bạn ngay lúc biên dịch thay vì giữa chừng khi đang chạy. Trình biên dịch kiểm tra toàn bộ tệp trước khi tạo ra bất cứ thứ gì.

Bạn có thể chạy mọi ví dụ độc lập ngay trên trang này: trình soạn thảo bên dưới biên dịch và chạy giúp bạn. Nhưng vẫn nên biết chuyện gì xảy ra trên máy của bạn, vì đó là nơi các dự án thật được thực hiện.

Chu trình biên dịch và chạy trên máy của bạn

Giả sử bạn đã lưu đoạn này thành main.cpp:

#include <iostream>
using namespace std;

int main() {
    cout << "Hello from the terminal" << endl;
    return 0;
}

Mở terminal trong thư mục chứa tệp và gọi trình biên dịch. Trình biên dịch phổ biến nhất là g++ (thuộc GCC); clang++ hoạt động y hệt cho mọi thứ ở đây:

g++ main.cpp -o main

Nếu code biên dịch không lỗi, g++ không in ra gì và tạo một tệp mới cạnh mã nguồn: một tệp thực thi tên là main (hoặc main.exe trên Windows). Tệp nhị phân đó là mã máy, không phải văn bản đọc được, và gắn với hệ điều hành và CPU của bạn. Giờ hãy chạy nó:

./main
Hello from the terminal

Trên Windows, bạn chạy nó bằng main.exe (hoặc chỉ main) từ cùng terminal đó. ./ trên macOS và Linux cho shell biết "chương trình nằm ngay trong thư mục này, không phải ở đâu đó trên PATH". Người mới hay quên bước này, rồi thắc mắc vì sao main báo command not found.

-o dùng để làm gì (và a.out)

Cờ -o đặt tên cho đầu ra. Bỏ nó đi thì g++ vẫn biên dịch, nhưng ghi tệp thực thi ra một tên mặc định: a.out trên macOS/Linux, a.exe trên Windows.

g++ main.cpp        # produces a.out, not main
./a.out

Điều này liên tục khiến người học vấp: họ biên dịch, không thấy lỗi, chạy ./main và nhận No such file or directory, vì tệp nhị phân thật ra tên là a.out. Luôn truyền -o để bạn biết chính xác mình đang chạy gì.

Bạn biên dịch một lần cho mỗi lần sửa mã nguồn. Sau đó, tệp thực thi là độc lập: bạn có thể chạy ./main cả trăm lần mà không cần biên dịch lại. Bạn chỉ chạy lại g++ sau khi sửa tệp .cpp.

Chọn chuẩn C++

C++ có nhiều phiên bản, C++11, C++14, C++17, C++20, C++23, mỗi bản bổ sung thêm tính năng ngôn ngữ. Vấn đề là trình biên dịch chọn một chuẩn mặc định có thể cũ hơn bạn nghĩ, nên code hiện đại có thể không biên dịch được mà chẳng rõ lý do. Hãy chỉ định rõ chuẩn bằng -std:

g++ -std=c++17 main.cpp -o main
g++ -std=c++20 main.cpp -o main

Đây là code dùng một tính năng của C++17 (structured binding). Nó biên dịch bình thường trong trình soạn thảo, nhưng trên máy của bạn thì cần -std=c++17 hoặc mới hơn:

Nếu bạn gặp lỗi kiểu 'structured bindings' only available with '-std=c++17', cách sửa không nằm trong code mà là thêm đúng cờ -std. Trong suốt khóa học này, hãy mặc định là C++17 trở lên.

Bật cảnh báo

Một chương trình C++ có thể biên dịch không lỗi mà vẫn sai. Nếu bạn yêu cầu, trình biên dịch sẽ chỉ ra những đoạn code đáng ngờ trước khi chúng gây hại lúc chạy. Thêm -Wall -Wextra:

g++ -std=c++17 -Wall -Wextra main.cpp -o main

Xét chương trình này. Nó biên dịch được khi không có -Wall, nhưng chứa một bug thật: đọc một biến chưa từng được gán giá trị, tức là undefined behavior:

#include <iostream>
using namespace std;

int main() {
    int count;                 // never initialized
    cout << count << endl;     // reads garbage - undefined behavior
    return 0;
}

Khi bật cảnh báo, trình biên dịch sẽ chỉ ra lỗi này:

warning: 'count' is used uninitialized [-Wuninitialized]

Hãy tập thói quen biên dịch với -Wall -Wextra ngay từ ngày đầu. Cảnh báo là trình biên dịch đang review code miễn phí cho bạn; phớt lờ chúng là cách các bug khó thấy sống sót. Cách sửa ở đây đơn giản là int count = 0;.

Đọc lỗi biên dịch

Khi g++ từ chối code của bạn, nó nêu tên tệp, dòng và chuyện gì đã sai. Biết đọc những thông báo đó là đã gỡ được một nửa vấn đề. Đây là lỗi kinh điển, thiếu dấu chấm phẩy:

#include <iostream>
using namespace std;

int main() {
    cout << "Oops"      // no semicolon
    return 0;
}
main.cpp:5:5: error: expected ';' before 'return'
    5 |     return 0;
      |     ^~~~~~

Có vài điều cần để ý. Lỗi báo ở dòng 5, nhưng sai sót nằm ở dòng 4: trình biên dịch chỉ nhận ra thiếu dấu chấm phẩy khi đã đọc tới token kế tiếp. Vì vậy khi lỗi chỉ vào một dòng trông có vẻ ổn, hãy kiểm tra dòng ngay trước nó. main.cpp:5:5 là tệp, dòng, rồi cột. Sửa đúng một thứ được nêu tên rồi biên dịch lại, đừng đoán mò.

Lỗi biên dịch nghĩa là chưa có gì chạy cả. Chúng khác với lỗi lúc chạy, khi chương trình đã bắt đầu rồi mới bị crash. Bắt được lỗi lúc biên dịch, trước khi chương trình chạy, là một trong những thế mạnh lớn nhất của C++.

Một chương trình kiểm tra nhanh

Chạy đoạn này trong trình soạn thảo, hoặc lưu thành main.cpp rồi chạy g++ -std=c++17 -Wall main.cpp -o main và sau đó ./main trên máy của bạn. Nếu cả ba dòng đều hiện ra, bộ công cụ của bạn hoạt động trọn vẹn:

Ở đây có ba thứ bạn sẽ sớm tìm hiểu kỹ: một biến int, một vector (mảng có thể thay đổi kích thước của C++), và cout để in đầu ra. Lúc này chỉ cần chương trình biên dịch được và in đủ ba dòng theo thứ tự.

Tiếp theo: cú pháp C++

Bạn đã biên dịch và chạy vài chương trình, nhưng chúng ta mới lướt qua phần dấu câu: các dòng #include, dấu ngoặc nhọn, dấu chấm phẩy, int main(), và vì sao mỗi dòng trông như vậy. Trang tiếp theo phân tích cú pháp C++ từng phần một, để cấu trúc này không còn là code mẫu khó hiểu mà bắt đầu trở nên có nghĩa.

Câu hỏi thường gặp

Làm sao biên dịch và chạy một chương trình C++?

Lưu code thành main.cpp, mở terminal trong thư mục đó và chạy g++ main.cpp -o main để tạo tệp thực thi. Sau đó chạy nó bằng ./main trên macOS/Linux hoặc main.exe trên Windows. Bạn chỉ biên dịch một lần; tệp nhị phân tạo ra có thể chạy bao nhiêu lần tùy thích.

Cờ -o trong g++ dùng để làm gì?

-o đặt tên cho tệp đầu ra. g++ main.cpp -o hello tạo ra một tệp thực thi tên là hello. Bỏ -o đi thì g++ mặc định dùng tên a.out (hoặc a.exe trên Windows), và đó là lý do người mới hay chạy nhầm một tệp mà họ không hề biết mình đã tạo ra.

Làm sao biên dịch C++ với một chuẩn cụ thể như C++17 hay C++20?

Truyền cờ -std: g++ -std=c++17 main.cpp -o main hoặc -std=c++20. Nếu không có cờ này, trình biên dịch dùng chuẩn mặc định của riêng nó, có thể cũ hơn bạn nghĩ, nên các tính năng mới như structured binding hay <ranges> có thể không biên dịch được cho đến khi bạn chỉ định rõ chuẩn.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu