Khởi đầu hành trình chinh phục C++ hiện đại (C++17 trở lên), bài viết này sẽ hướng dẫn bạn cách thiết lập môi trường lập trình tối giản, chuyên nghiệp trên macOS, Linux và Windows. Đồng thời, chúng ta sẽ viết và biên dịch chương trình đầu tiên, tìm hiểu cách hoạt động của không gian tên (Namespaces) và làm rõ lý do tại sao các dự án siêu lớn, yêu cầu tốc độ tối đa như Google V8 JavaScript Engine lại bắt buộc phải viết bằng C++.

1. Tại sao V8 — bộ não của Chrome và Node.js — lại viết bằng C++?

Trước khi cài đặt bất cứ thứ gì, hãy trả lời câu hỏi đáng giá nhất: tại sao người ta vẫn chọn C++ cho những phần mềm khó nhất, trong khi đã có hàng chục ngôn ngữ dễ viết hơn? Ví dụ rõ nhất là V8 — bộ máy chạy JavaScript bên trong Chrome và Node.js. Bản thân V8 gần như toàn bộ là C++.

Lý do gói gọn trong ba điều, và cả ba đều là những thứ bạn sẽ tự tay làm trong series này:

  • Không có runtime chen giữa bạn và CPU. Khi bạn viết JavaScript, có sẵn một chương trình khác (chính là V8) đứng ra dịch và chạy hộ. Còn chương trình C++ được dịch thẳng thành mã máy trước khi chạy — không ai đứng giữa. Chính vì thế V8 phải viết bằng thứ gì đó nhanh hơn JavaScript: nó là cái chạy JavaScript.
  • Bạn quyết định bộ nhớ được cấp phát và thu hồi lúc nào. JavaScript có bộ thu gom rác (garbage collector) tự dọn bộ nhớ giúp bạn — nhưng bộ thu gom rác đó là một đoạn code C++ do người ta viết ra. Muốn viết được nó thì phải kiểm soát bộ nhớ ở mức thấp nhất. Đây là chủ đề của Bài 7 (move semantics) và Bài 8 (smart pointer).
  • Trừu tượng hoá mà không mất tốc độ (zero-cost abstraction). Đây là lời hứa đặc trưng của C++: bạn được viết code gọn gàng bằng lớp (class), khuôn mẫu (template), thuật toán có sẵn — và trình biên dịch dịch chúng ra mã máy nhanh gần bằng viết tay. Bài 10 và Bài 13 sẽ cho bạn thấy điều đó cụ thể.
📌 Chưa hiểu hết cũng không sao
Các cụm như "cấp phát bộ nhớ", "lớp", "khuôn mẫu" vừa xuất hiện ở trên đều có nguyên một bài dành riêng cho chúng ở phía sau. Ở bài này bạn chưa cần hiểu chúng — chỉ cần biết đó là những thứ C++ trao vào tay bạn, và đó là lý do nó vẫn được chọn cho các hệ thống đòi hỏi tốc độ cao nhất. Việc của bài 1 là làm cho máy bạn biên dịch được C++ và chạy được chương trình đầu tiên.

2. Vòng đời biên dịch (C++ Compilation Pipeline) & Name Mangling

Tương tự như C, mã nguồn C++ (file .cpp) muốn chạy được trên phần cứng phải đi qua 4 giai đoạn chính của Compiler Pipeline:

Mã nguồn C++ (.cpp)
       │
       ▼ [Giai đoạn 1: Preprocessor] (Xử lý các chỉ thị #include, #define)
Mã nguồn đã mở rộng (.ii)
       │
       ▼ [Giai đoạn 2: Compiler] (Dịch mã nguồn thành Hợp ngữ Assembly)
Hợp ngữ Assembly (.s)
       │
       ▼ [Giai đoạn 3: Assembler] (Dịch hợp ngữ thành mã máy Object Code)
Mã máy Object (.o / .obj)
       │
       ▼ [Giai đoạn 4: Linker] (Liên kết các Object file và Thư viện tĩnh/động)
File chạy thực thi (Executable)
              

Hiện tượng Name Mangling (Mã hóa tên ký hiệu):

Một điểm khác biệt lớn giữa trình biên dịch C và C++ là cách quản lý Symbol Table (Bảng ký hiệu). C++ hỗ trợ Nạp chồng hàm (Function Overloading) - cho phép các hàm trùng tên nhưng khác kiểu tham số. Để làm được điều này, trình biên dịch C++ thực hiện kỹ thuật Name Mangling: tự động mã hóa tên hàm kèm theo kiểu tham số của nó thành một ký hiệu duy nhất trong file Object.

Ví dụ: Hai hàm sau trùng tên trong C++:

void print(int x);     // Compiled to the symbol: _Z5printi
void print(double x);  // Compiled to the symbol: _Z5printd

Ký hiệu _Z5printi không phải chuỗi ngẫu nhiên, nó đọc được: _Z là dấu hiệu tên đã bị mã hoá, 5 là độ dài của tên gốc, print là tên gốc, và chữ cuối là kiểu tham số — i cho int, d cho double. Bạn tự kiểm chứng được bằng hai lệnh sau (trên macOS ký hiệu có thêm một dấu gạch dưới ở đầu: __Z5printi, do định dạng Mach-O; trên Linux/ELF thì đúng như bài viết):

Terminal
# Compile to an object file, then list the symbols inside it
printf 'void print(int x){}\nvoid print(double x){}\n' > nm_demo.cpp
g++ -c nm_demo.cpp -o nm_demo.o
nm nm_demo.o | grep print
# macOS: __Z5printd / __Z5printi   (Linux: _Z5printd / _Z5printi)

# Turn a mangled symbol back into a readable signature
echo '_Z5printi' | c++filt
# => print(int)

Trong khi đó, ngôn ngữ C không có Name Mangling, tên hàm được giữ nguyên làm ký hiệu. Do đó, nếu bạn muốn viết thư viện C++ tương thích và gọi được từ code C, bạn phải sử dụng từ khóa extern "C" để báo cho compiler C++ không được thực hiện Name Mangling trên khối code đó:

extern "C" {
    void print_c_compatible(int x); // Not mangled - the symbol keeps this exact name
}

3. Cài đặt Trình biên dịch C++ hiện đại

Trên macOS (Sử dụng Clang)

Trình biên dịch mặc định của Apple là Clang (được tích hợp sẵn trong Command Line Tools). Để cài đặt, bạn mở Terminal lên và gõ lệnh sau:

Terminal
xcode-select --install

Lệnh này không cài cả bộ Xcode nặng vài chục GB — nó chỉ cài Command Line Tools, trong đó có clang++, makegit. Một hộp thoại sẽ hiện lên, bấm Install rồi chờ vài phút. Nếu máy đã có sẵn, terminal báo command line tools are already installed — đó là kết quả tốt, không phải lỗi.

Kiểm tra phiên bản bằng lệnh: clang++ --version. Yêu cầu phiên bản hỗ trợ tốt chuẩn C++17.

Trên Linux (Sử dụng GCC)

Trên các hệ điều hành dựa trên Debian hoặc Ubuntu, bạn có thể cài đặt bộ biên dịch g++ thông qua câu lệnh sau:

Terminal
sudo apt update
sudo apt install build-essential

build-essential là một gói gộp: nó kéo về g++, gcc, make và các thư viện chuẩn cần để biên dịch. Cài riêng lẻ từng thứ cũng được, nhưng gói này là cách nhanh và chắc chắn nhất.

Xác thực phiên bản bằng lệnh: g++ --version.

Trên Windows (WSL2 hoặc MSYS2)

Windows không có sẵn trình biên dịch C++ trên dòng lệnh. Có hai hướng, và với series này tôi khuyên hướng thứ nhất:

  • WSL2 (khuyến nghị). Cài một bản Ubuntu chạy ngay trong Windows, rồi từ đó dùng đúng mọi lệnh Linux ở phần trên — kể cả apt ở mục 4. Đây là cách để mọi lệnh trong series chạy y hệt như trên máy Linux, không phải dịch lại.
  • MSYS2. Cho bạn g++ chạy nguyên bản (native) trên Windows, tạo ra file .exe chạy được mà không cần WSL. Đổi lại, các lệnh cài thư viện dùng pacman chứ không phải apt.
PowerShell / MSYS2
# Option 1 - WSL2: run this in PowerShell as Administrator, then reboot
wsl --install -d Ubuntu
# After the reboot, open "Ubuntu" and follow the Linux instructions above

# Option 2 - MSYS2: install from https://www.msys2.org, then in the MSYS2 UCRT64 shell
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake
g++ --version

Dù chọn hướng nào, phần còn lại của bài vẫn dùng được: lệnh biên dịch g++ -std=c++17 ... giống hệt nhau ở cả ba hệ điều hành. Chỉ khác ở chỗ tên file chạy — trên Windows bản MSYS2 thì thêm đuôi .exe và gọi bằng .\hello_cpp.exe.

4. Quản lý gói (Package Managers)

Khi phát triển dự án C++ lớn, bạn sẽ cần sử dụng các thư viện bên ngoài (như OpenSSL, Boost, CURL, v.v.). Thay vì tải xuống và biên dịch từng thư viện thủ công, hãy sử dụng các trình quản lý gói để tự động hóa quá trình này.

Trên macOS (Homebrew)

Homebrew là trình quản lý gói phổ biến nhất trên macOS. Nếu chưa cài đặt, hãy cài đặt trước:

Terminal
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Sau đó, cài đặt các thư viện C++ phổ biến:

Terminal
# Install Boost (a very widely used utility library)
brew install boost

# Install OpenSSL (cryptography)
brew install openssl

# Install CMake (the build tool)
brew install cmake

Trên Linux (APT, Debian/Ubuntu)

Trên các hệ điều hành dựa trên Debian, sử dụng apt để quản lý gói:

Terminal
# Refresh the package list
sudo apt update

# Install Boost
sudo apt install libboost-all-dev

# Install OpenSSL and its development headers
sudo apt install libssl-dev

# Install CMake
sudo apt install cmake

5. CMake: Hệ thống build chuyên nghiệp cho C++

Khi dự án của bạn có nhiều file nguồn, thư viện bên ngoài, hoặc cần xây dựng trên nhiều nền tảng khác nhau, việc sử dụng lệnh g++ hoặc clang++ thủ công trở nên bất khả thi. CMake là một công cụ build đa nền tảng cho phép bạn xác định cách biên dịch dự án một lần, sau đó CMake sẽ tự động sinh ra các Makefile, Visual Studio Projects, hoặc Xcode Projects tùy theo hệ điều hành.

Cấu trúc dự án CMake cơ bản

Tạo một dự án C++ với cấu trúc sau:

my_project/
├── CMakeLists.txt          (Tệp cấu hình CMake)
├── src/
│   ├── main.cpp
│   └── utils.cpp
├── include/
│   └── utils.h
└── build/                   (Thư mục build - tạo ra sau khi chạy CMake)
              

Mẫu CMakeLists.txt

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(my_project VERSION 1.0.0 LANGUAGES CXX)

# Set the C++ standard (C++17 or newer)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Per-configuration compiler flags
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -Wextra")
set(CMAKE_CXX_FLAGS_DEBUG "-g -Wall -Wextra")

# Add the include directory (where the .h files live)
include_directories(${CMAKE_SOURCE_DIR}/include)

# List the source files (.cpp)
add_executable(my_program
    src/main.cpp
    src/utils.cpp
)

# Link against external libraries (Boost, for example)
# target_link_libraries(my_program PRIVATE Boost::boost)

Quy trình build với CMake

Terminal
# 1. Create a build directory
mkdir build
cd build

# 2. Run CMake to generate the Makefile
cmake ..

# 3. Compile with make
make

# 4. Run the program
./my_program

# (Optional) Build in Release mode, with optimisation on
cmake -DCMAKE_BUILD_TYPE=Release ..
make

Chú ý bước 2 chỉ chạy một lần: CMake đọc CMakeLists.txt rồi sinh ra Makefile trong thư mục build/. Những lần sửa code sau đó bạn chỉ cần gõ make, và nó chỉ biên dịch lại đúng file bạn vừa đổi. Sở dĩ phải tạo thư mục build/ riêng (out-of-source build) là để mọi file trung gian nằm gọn một chỗ, xoá cả thư mục là sạch, không lẫn vào mã nguồn.

6. Bảng Compiler Flags - Hướng dẫn tối ưu hóa & Debugging

Dưới đây là bảng các compiler flags quan trọng nhất để tối ưu hóa hiệu năng và phát hiện lỗi sớm:

Compiler Flag Mục đích Ví dụ
-std=c++17 Yêu cầu tuân thủ tiêu chuẩn C++17 g++ -std=c++17 main.cpp
-std=c++20 Tuân thủ tiêu chuẩn C++20 (Concepts, Modules) g++ -std=c++20 main.cpp
-O0 Không tối ưu hóa (mặc định, tốc độ biên dịch nhanh nhất) g++ -O0 -g main.cpp
-O1 Tối ưu hóa cơ bản, vẫn giữ thời gian biên dịch chấp nhận được g++ -O1 main.cpp
-O2 Tối ưu hóa cân bằng (khuyến nghị cho sản xuất) g++ -O2 main.cpp
-O3 Tối ưu hóa tối đa (SIMD Vectorization, inlining tích cực) g++ -O3 main.cpp
-Ofast Aggressive: bỏ qua tiêu chuẩn IEEE floating-point g++ -Ofast main.cpp
-g Thêm debug symbols (cần thiết để debug với GDB/LLDB) g++ -g -O2 main.cpp
-Wall Bật tất cả các cảnh báo cơ bản g++ -Wall main.cpp
-Wextra Bật thêm các cảnh báo bổ sung g++ -Wall -Wextra main.cpp
-Werror Coi cảnh báo là lỗi (buộc fix tất cả warnings) g++ -Wall -Werror main.cpp
-fsanitize=address AddressSanitizer: phát hiện buffer overflow, use-after-free g++ -fsanitize=address main.cpp
-fPIC Position Independent Code (cần cho shared libraries) g++ -fPIC -shared lib.cpp -o lib.so
-static Link thống kê (tất cả thư viện nhúng trong executable) g++ -static main.cpp

Lệnh build được khuyến nghị cho Development vs Release

Terminal
# Development: fast to build, easy to debug
g++ -std=c++17 -O0 -g -Wall -Wextra main.cpp -o main_debug

# Release: optimised for speed, no debug symbols
g++ -std=c++17 -O3 -Wall -Wextra main.cpp -o main_release

# CI/CD: strictest possible checking
g++ -std=c++17 -O2 -Wall -Wextra -Werror -fsanitize=address main.cpp -o main_ci

Ba dòng trên là ba mục đích khác nhau, đừng dùng lẫn. Bản debug để -O0 vì tối ưu hoá sẽ xoá và trộn các dòng lệnh khiến trình gỡ lỗi nhảy lung tung. Bản release bỏ -g cho file nhỏ lại. Còn bản CI thêm -Werror (coi mọi cảnh báo là lỗi) và -fsanitize=address — chậm hơn nhiều nhưng bắt được lỗi tràn bộ nhớ ngay lúc chạy test.

7. Hướng dẫn chi tiết chương trình đầu tiên: hello_cpp.cpp

Hãy phân tích kỹ lưỡng đoạn mã chương trình Hello World dưới đây để hiểu cách mã C++ hoạt động từng bước:

Tạo file hello_cpp.cpp

hello_cpp.cpp
#include <iostream>
#include <string>

// A namespace of our own
namespace Engine {
    void printV8Info() {
        // std::string is a safe, self-managing string type - unlike a raw C char array
        std::string engine = "Google V8";
        std::cout << engine << " is written mostly in C++, which is why it can run JS this fast!" << std::endl;
    }
}

int main() {
    // std::cout lives in namespace std, and writes to the terminal
    std::cout << "Welcome to the C++ series on js-tools.org!" << std::endl;

    // Call the function from namespace Engine
    Engine::printV8Info();

    return 0;
}

Giải thích chi tiết từng dòng mã

Dòng 1-2: Include Headers

  • #include <iostream>: Tải thư viện tiêu chuẩn của C++ để sử dụng std::cout (xuất dữ liệu ra terminal) và std::endl (xuống dòng).
  • #include <string>: Tải thư viện xử lý chuỗi ký tự an toàn kiểu std::string.

Dòng 4-11: Định nghĩa Namespace

Namespace là một "không gian tên" riêng biệt giúp tránh xung đột khi có hai hàm cùng tên từ các thư viện khác nhau. Ở đây, chúng ta tạo namespace Engine chứa hàm printV8Info(). Để gọi hàm này từ main, ta phải dùng tiền tố: Engine::printV8Info().

Dòng 13: Hàm main()

Mọi chương trình C++ đều bắt đầu từ hàm main(). Kiểu trả về int có nghĩa chương trình sẽ trả về một mã thoát (exit code) cho hệ điều hành:

  • return 0;: Chỉ ra rằng chương trình thực thi thành công.
  • return 1; (hoặc giá trị khác): Chỉ ra có lỗi xảy ra.

Dòng 15: std::cout (Standard Output)

std::cout << "..." << std::endl; có nghĩa là "ghi chuỗi ký tự ra terminal và xuống dòng". Toán tử << gọi là "stream insertion operator".

Biên dịch và chạy chương trình

Sau khi tạo file hello_cpp.cpp, hãy biên dịch nó:

Terminal
# Compile against the C++17 standard
g++ -std=c++17 -Wall -Wextra hello_cpp.cpp -o hello_cpp

# Run it
./hello_cpp

# Expected output:
# Welcome to the C++ series on js-tools.org!
# Google V8 is written mostly in C++, which is why it can run JS this fast!

Giải thích lệnh biên dịch:

  • g++: Trình biên dịch C++.
  • -std=c++17: Sử dụng tiêu chuẩn C++17.
  • -Wall -Wextra: Bật tất cả các cảnh báo để phát hiện lỗi.
  • hello_cpp.cpp: File nguồn cần biên dịch.
  • -o hello_cpp: Tên file thực thi sau biên dịch.
📝 Kiểm tra kiến thức bài 1
Tại sao chúng ta nên viết std::cout thay vì dùng thẳng cout, khi chưa khai báo using namespace std;?

Tải mã nguồn mẫu bài học

Bạn có thể tải file mã nguồn C++ mẫu hoàn chỉnh của bài học này để tiến hành thực hành trực tiếp trên máy tính cá nhân.

Tải hello_cpp.cpp