This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Sau khi thiết lập môi trường lập trình và viết chương trình đầu tiên, bạn cần nắm vững các khái niệm nền tảng của Modern C++. Bài học này sẽ giới thiệu các tính năng cốt lõi: Namespaces (quản lý không gian tên), Type Modifiers (const, constexpr, volatile), Auto Keyword (suy luận kiểu tự động), iostream I/O Basics, và các kỹ thuật Type Casting an toàn. Những khái niệm này là nền móng cho việc viết code C++ sạch, an toàn và hiệu năng cao.

1. Namespaces: Quản lý không gian tên trong dự án lớn

Namespace là một cơ chế cho phép bạn tổ chức mã nguồn thành các "vùng tên" riêng biệt, tránh xung đột tên gọi khi làm việc với nhiều thư viện. Ví dụ, cả OpenGL Graphics Library và một thư viện Graphics tự viết của bạn đều có thể có hàm drawTriangle(), nhưng bằng cách sử dụng namespace, bạn có thể phân biệt chúng:

C++ Snippet
// Namespace std (Standard Library)
namespace std {
    void cout_example() {
        std::cout << "Hello from std namespace" << std::endl;
    }
}

// Namespace tự định nghĩa
namespace Graphics {
    void drawTriangle() {
        std::cout << "Drawing triangle..." << std::endl;
    }
}

// Namespace lồng nhau
namespace Graphics::Engine {
    void initRenderer() {
        std::cout << "Initializing graphics engine..." << std::endl;
    }
}

int main() {
    // Gọi hàm từ namespace Graphics
    Graphics::drawTriangle();

    // Gọi hàm từ namespace lồng
    Graphics::Engine::initRenderer();

    return 0;
}

Using Directive & Using Declaration

Thay vì viết std::cout mỗi lần, bạn có thể sử dụng using để "nhập" hàm vào scope hiện tại. Tuy nhiên, cần cẩn thận vì việc dùng using namespace std; ở mức global có thể gây xung đột tên:

C++ Snippet
// Using declaration: chỉ nhập một hàm cụ thể
using std::cout;
using std::endl;

int main() {
    cout << "Safer than using namespace std" << endl;

    // Trong phạm vi hàm
    {
        using std::cin;
        int x;
        cin >> x;
    }

    return 0;
}

Namespace Alias

Khi tên namespace quá dài (ví dụ: boost::asio::ssl::stream), bạn có thể tạo alias ngắn hơn:

C++ Snippet
namespace fs = std::filesystem;  // Alias
namespace ba = boost::asio;      // Alias

// Bây giờ có thể sử dụng ngắn hơn
fs::path my_path = "/tmp/file.txt";
ba::io_context io;

2. Type Modifiers: const, constexpr & volatile

Các từ khóa này giúp bạn kiểm soát cách dữ liệu có thể bị thay đổi, giúp compiler phát hiện lỗi logic sớm.

const: Hằng số không thay đổi

const chỉ ra rằng một biến không thể bị thay đổi sau khi khởi tạo. Nó có thể áp dụng cho biến, con trỏ, hoặc phương thức class:

C++ Snippet
int main() {
    // Biến const - giá trị không thay đổi
    const int MAX_SIZE = 100;
    // MAX_SIZE = 200;  // LỖI: không thể thay đổi const

    // Con trỏ const - con trỏ không thể thay đổi địa chỉ
    int x = 10;
    int* const ptr = &x;  // Địa chỉ không đổi, nhưng *ptr có thể thay
    *ptr = 20;              // OK
    // ptr = &y;            // LỖI

    // Const pointer - con trỏ có thể thay đổi, nhưng dữ liệu không
    const int* const_ptr = &x;
    // *const_ptr = 30;     // LỖI
    const_ptr = &y;        // OK

    return 0;
}

constexpr: Hằng số tính toán lúc biên dịch

constexpr yêu cầu giá trị phải được tính toán lúc biên dịch (compile-time), cho phép compiler tối ưu hóa. Điều này hữu ích cho các hằng số phức tạp:

C++ Snippet
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    // Tính toán lúc biên dịch
    constexpr int result = factorial(5);  // Compiler tính 5! = 120

    // Mảng const
    constexpr int arr[] = {1, 2, 3, 4, 5};

    return 0;
}

volatile: Chỉ định giá trị có thể thay đổi ngoài kiểm soát chương trình

volatile báo cho compiler rằng giá trị có thể thay đổi ngoài dự kiến (ví dụ: do phần cứng, signal handler, hay luồng khác), nên không được tối ưu hóa:

C++ Snippet
// Volatile thường dùng cho hardware registers hoặc shared variables
volatile int hardware_counter = 0;  // Có thể bị thay đổi bởi phần cứng

// Compiler sẽ không cache giá trị này, mà luôn đọc từ bộ nhớ
while (hardware_counter < 100) {
    // Compiler sẽ đọc lại hardware_counter mỗi lần
}

// Const volatile: không thể thay qua con trỏ, nhưng giá trị có thể thay
const volatile int* hw_ptr = &hardware_counter;

3. Auto Keyword: Suy luận kiểu tự động

auto yêu cầu compiler tự động suy luận kiểu dữ liệu dựa vào giá trị gán. Điều này giúp code ngắn gọn hơn, đặc biệt khi làm việc với STL containers:

C++ Snippet
#include <vector>
#include <map>

int main() {
    // Auto với biến thông thường
    auto x = 42;           // int
    auto name = "John";    // const char*
    auto pi = 3.14159;     // double

    // Auto với STL containers
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // Thay vì: std::vector<int>::iterator it = numbers.begin();
    // Viết ngắn hơn:
    auto it = numbers.begin();

    // Range-based for loop với auto
    for (auto num : numbers) {
        std::cout << num << " ";  // auto sẽ là int
    }

    // Map iterator: thường phức tạp, auto rất hữu ích
    std::map<std::string, int> scores;
    for (const auto& [name, score] : scores) {  // Structured binding (C++17)
        std::cout << name << ": " << score << std::endl;
    }

    return 0;
}

Cẩn thận: Auto không phải lúc nào cũng là tốt

Mặc dù tiện lợi, quá sử dụng auto có thể làm code khó đọc. Nên dùng auto khi:

  • Kiểu dữ liệu rõ ràng từ giá trị (ví dụ: auto x = std::make_unique<MyClass>();).
  • Làm việc với iterator hoặc template types phức tạp.
  • Không dùng auto khi kiểu dữ liệu không rõ ràng để tránh nhầm lẫn.

4. iostream & I/O Basics: Xuất nhập dữ liệu an toàn

C++ cung cấp iostream (Input/Output Stream) thay thế an toàn cho scanf/printf của C. Stream là một luồng dữ liệu hai chiều giữa chương trình và thiết bị (terminal, file, mạng).

std::cout: Xuất dữ liệu

C++ Snippet
#include <iostream>
#include <iomanip>

int main() {
    std::cout << "Hello, C++!" << std::endl;

    // Xuất nhiều giá trị cùng lúc
    int age = 25;
    double height = 1.75;
    std::cout << "Age: " << age << ", Height: " << height << std::endl;

    // Định dạng số
    std::cout << std::fixed << std::setprecision(2) << height << std::endl;

    // Hexadecimal và octal
    std::cout << std::hex << 255 << std::endl;      // ff
    std::cout << std::oct << 255 << std::endl;      // 377
    std::cout << std::dec << 255 << std::endl;      // 255

    return 0;
}

std::cin: Nhập dữ liệu từ người dùng

C++ Snippet
#include <iostream>
#include <string>

int main() {
    // Nhập số nguyên
    int x;
    std::cout << "Enter an integer: ";
    std::cin >> x;

    // Nhập một từ (dừng ở space)
    std::string word;
    std::cout << "Enter a word: ";
    std::cin >> word;

    // Nhập cả dòng (bao gồm space)
    std::string line;
    std::cout << "Enter a sentence: ";
    std::getline(std::cin, line);

    std::cout << "You entered: " << line << std::endl;

    return 0;
}

5. Type Casting: Chuyển đổi kiểu an toàn vs không an toàn

Type casting là việc chuyển đổi từ kiểu dữ liệu này sang kiểu khác. C++ cung cấp hai cách: C-style (nguy hiểm) và C++-style (an toàn hơn).

C-style Cast (không khuyến nghị)

Cú pháp: (type) value. Cách này mạnh mẽ nhưng rất nguy hiểm vì compiler không kiểm tra tính hợp lệ:

C++ Snippet
int main() {
    double d = 3.14159;
    int i = (int)d;  // Truncate to 3 (mất dữ liệu)

    // Nguy hiểm: cast địa chỉ thành địa chỉ không liên quan
    int* ptr = (int*) "dangerous string cast";  // UB!

    return 0;
}

static_cast: Chuyển đổi kiểu tương thích

Cú pháp: static_cast<type>(value). Được dùng khi bạn chắc chắn rằng hai kiểu tương thích:

C++ Snippet
int main() {
    double d = 3.14159;
    int i = static_cast<int>(d);  // Explicit conversion, loss of precision OK

    // Chuyển đổi giữa numeric types
    float f = static_cast<float>(42);

    // Chuyển đổi con trỏ trong hierarchy class
    class Base {};
    class Derived : public Base {};

    Derived d_obj;
    Base* base_ptr = static_cast<Base*>(&d_obj);  // Derived -> Base (safe)

    return 0;
}

const_cast: Loại bỏ const qualifier

Cú pháp: const_cast<type>(value). Được dùng khi bạn biết rằng dữ liệu thực sự không phải const nhưng được khai báo const:

C++ Snippet
void modifyData(int* ptr) {
    *ptr = 999;
}

int main() {
    const int x = 10;

    // const_cast để loại bỏ const
    modifyData(const_cast<int*>(&x));

    // CẢNH BÁO: Nếu x thực sự nằm trong read-only memory,
    // việc này sẽ dẫn đến Undefined Behavior!

    return 0;
}

reinterpret_cast: Cast bất cứ con trỏ nào sang con trỏ khác

Cú pháp: reinterpret_cast<type>(value). Cách cast nguy hiểm nhất, chỉ dùng khi thực sự cần thiết (ví dụ: tương tác với APIs cổ điển hoặc hardware):

C++ Snippet
int main() {
    int x = 42;

    // Cast địa chỉ thành unsigned long long
    unsigned long long addr = reinterpret_cast<unsigned long long>(&x);

    std::cout << "Address: 0x" << std::hex << addr << std::endl;

    // Cast ngược lại
    int* ptr = reinterpret_cast<int*>(addr);

    // RẤT NGUY HIỂM: chỉ dùng khi thực sự biết mình làm gì!

    return 0;
}

Tóm tắt Type Casting

Cast Type Mục đích Độ an toàn
(type) value Cast kiểu cũ (C-style) Thấp - tránh sử dụng
static_cast<T>() Chuyển đổi kiểu tương thích (int↔float, Base→Derived) Cao
const_cast<T>() Loại bỏ const/volatile qualifier Trung bình
reinterpret_cast<T>() Cast bất kỳ con trỏ sang con trỏ khác Thấp - chỉ dùng khi cần thiết
dynamic_cast<T>() Runtime type checking cho polymorphic types Cao

Câu hỏi ôn tập kiến thức

Khi nào bạn nên sử dụng const auto& thay vì auto trong range-based for loop?

A. Luôn luôn, vì nó là best practice không có ngoại lệ.
B. Khi bạn không cần sửa đổi phần tử và muốn tránh copy không cần thiết (đặc biệt với các object lớn).
C. Vì nó làm code biên dịch nhanh hơn.

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 cpp_fundamentals.cpp