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:

namespaces.cpp
#include <iostream>

// A namespace of your own
namespace Graphics {
    void drawTriangle() {
        std::cout << "Graphics::drawTriangle" << std::endl;
    }
}

// A different library that happens to use the same function name
namespace Physics {
    void drawTriangle() {
        std::cout << "Physics::drawTriangle (collision shape)" << std::endl;
    }
}

// Nested namespace, C++17 syntax
namespace Graphics::Engine {
    void initRenderer() {
        std::cout << "Graphics::Engine::initRenderer" << std::endl;
    }
}

int main() {
    // The prefix is what makes the two drawTriangle() distinguishable
    Graphics::drawTriangle();
    Physics::drawTriangle();
    Graphics::Engine::initRenderer();
    return 0;
}

Điểm cần thấy là hai hàm drawTriangle() trùng tên hoàn toàn nhưng vẫn sống chung được, vì tiền tố namespace phân biệt chúng. Đó là toàn bộ lý do namespace tồn tại.

⚠️ Không bao giờ thêm gì vào namespace std
Bạn sẽ thấy trên mạng những ví dụ mở lại namespace std để thêm hàm vào đó. Đừng làm theo: chuẩn C++ quy định rõ ở mục [namespace.std] rằng thêm khai báo hay định nghĩa vào namespace std khiến hành vi của cả chương trình trở thành không xác định (undefined behaviour).

Điều làm nó nguy hiểm hơn hẳn một lỗi cú pháp: không compiler nào cảnh báo. Tôi thử cả g++clang++ với -Wall -Wextra — cả hai biên dịch sạch, chương trình chạy ra kết quả đúng như mong đợi. Nó chỉ vỡ vào một ngày bạn nâng phiên bản thư viện chuẩn và tên bạn thêm vào đụng với một tên có thật bên trong std.

Ngoại lệ duy nhất được chuẩn cho phép là đặc tả (specialisation) một template có sẵn của std cho kiểu của riêng bạn — ví dụ std::hash<MyType>. Còn thêm hàm mới thì không.

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:

using_directive.cpp
#include <iostream>

// using declaration: pull in ONE name only
using std::cout;
using std::endl;

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

    // Scoped to this block only - the safest form
    {
        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:

namespace_alias.cpp
namespace fs = std::filesystem;  // Alias
namespace ba = boost::asio;      // Alias

// Now the call sites stay short
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:

const_demo.cpp
int main() {
    // A const variable: cannot be reassigned after initialisation
    const int MAX_SIZE = 100;
    // MAX_SIZE = 200;      // ERROR: cannot assign to a const

    int x = 10, y = 20;

    // CONST POINTER: the pointer is fixed, the value it points at is not.
    // Read it right-to-left: "ptr is a const pointer to int".
    int* const ptr = &x;
    *ptr = 30;              // OK - writing through it is allowed
    // ptr = &y;            // ERROR: the pointer itself cannot be repointed

    // POINTER TO CONST: the opposite. The pointer moves, the value is read-only.
    // "to_const is a pointer to const int".
    const int* to_const = &x;
    // *to_const = 40;      // ERROR: cannot write through a pointer-to-const
    to_const = &y;          // OK - repointing is allowed

    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:

constexpr_demo.cpp
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    // Evaluated at COMPILE time, not at run time
    constexpr int result = factorial(5);  // The compiler computes 5! = 120

    // A compile-time constant array
    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:

volatile_demo.cpp
// volatile is for hardware registers and memory written from outside
volatile int hardware_counter = 0;  // May change without this code touching it

// The compiler may not cache it in a register - every read hits memory
while (hardware_counter < 100) {
    // hardware_counter is re-read on every iteration
}

// const volatile: this code may not write it, but something else may
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:

auto_demo.cpp
#include <iostream>
#include <map>
#include <vector>

int main() {
    // auto with ordinary values
    auto x = 42;           // int
    auto name = "John";    // const char*
    auto pi = 3.14159;     // double

    // auto with STL containers
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // Instead of: std::vector<int>::iterator it = numbers.begin();
    // just write:
    auto it = numbers.begin();

    // Range-based for loop with auto
    for (auto num : numbers) {
        std::cout << num << " ";  // auto deduces int here
    }

    // Map iterators get verbose fast - this is where auto really pays
    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.

Vòng lặp range-based for và const auto&

Chỗ auto đáng giá nhất là trong vòng lặp duyệt container. Nhưng cách bạn viết auto ở đây quyết định chương trình có sao chép dữ liệu hay không — và với object lớn thì đó là khác biệt thật, không phải chuyện thẩm mỹ:

range_for.cpp
#include <iostream>
#include <string>
#include <vector>

struct Report {
    std::string title;
    std::string body;   // imagine this is a few hundred KB
    Report(std::string t, std::string b) : title(std::move(t)), body(std::move(b)) {}
    Report(const Report& other) : title(other.title), body(other.body) {
        std::cout << "  [COPY " << title << "]\n";   // count every copy
    }
};

int main() {
    std::vector<Report> reports;
    reports.reserve(2);   // Without reserve the vector reallocates and copies once by itself
    reports.emplace_back("A", "...");
    reports.emplace_back("B", "...");

    std::cout << "auto (copies every element):\n";
    for (auto r : reports) {
        std::cout << "  read " << r.title << "\n";
    }

    std::cout << "const auto& (no copy):\n";
    for (const auto& r : reports) {
        std::cout << "  read " << r.title << "\n";
    }

    std::cout << "auto& (no copy, AND writable):\n";
    for (auto& r : reports) {
        r.title += "!";
    }
    for (const auto& r : reports) std::cout << "  " << r.title << "\n";

    return 0;
}
Kết quả chạy thật
auto (copies every element):
  [COPY A]
  read A
  [COPY B]
  read B
const auto& (no copy):
  read A
  read B
auto& (no copy, AND writable):
  A!
  B!

Dòng [COPY …] chỉ xuất hiện ở vòng lặp đầu. Viết auto r là bảo compiler "cho tôi một bản sao" — với Report chứa vài trăm KB thì mỗi vòng lặp là một lần cấp phát và copy toàn bộ, làm không vì lý do gì.

Ba dạng, ba mục đích rõ ràng:

  • const auto& — mặc định nên dùng. Không sao chép, và compiler chặn luôn nếu bạn vô tình sửa phần tử.
  • auto& — khi bạn muốn sửa phần tử ngay trong container.
  • auto (không dấu &) — chỉ khi bạn thật sự cần một bản sao để chỉnh mà không ảnh hưởng bản gốc, hoặc phần tử nhỏ như int.

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

cout_formatting.cpp
#include <iostream>
#include <iomanip>

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

    // Chaining several values in one statement
    int age = 25;
    double height = 1.75;
    std::cout << "Age: " << age << ", Height: " << height << std::endl;

    // Number formatting
    std::cout << std::fixed << std::setprecision(2) << height << std::endl;

    // Hexadecimal and 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

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

int main() {
    // Read an integer
    int x;
    std::cout << "Enter an integer: ";
    std::cin >> x;

    // Read one word (stops at whitespace)
    std::string word;
    std::cout << "Enter a word: ";
    std::cin >> word;

    // Read a whole line, spaces included
    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_style_cast.cpp
int main() {
    double d = 3.14159;
    int i = (int)d;  // Truncates to 3 - data lost, and nothing warns you

    // Dangerous: reinterprets a string literal's address as an int*
    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:

static_cast_demo.cpp
int main() {
    double d = 3.14159;
    int i = static_cast<int>(d);  // Explicit conversion, loss of precision OK

    // Between numeric types
    float f = static_cast<float>(42);

    // Up a class hierarchy: Derived* -> Base* is always safe
    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:

const_cast_demo.cpp
void modifyData(int* ptr) {
    *ptr = 999;
}

int main() {
    const int x = 10;

    // const_cast strips the const qualifier
    modifyData(const_cast<int*>(&x));

    // UNDEFINED BEHAVIOUR - and not only "if x sits in read-only memory".
    // Writing to an object that was DECLARED const is UB, full stop.
    // See what actually happens, measured, right below this block.

    return 0;
}

Bài viết trước đây nói "nếu x nằm trong read-only memory thì mới lỗi". Không phải: ghi vào một object được khai báo const là undefined behaviour bất kể nó nằm ở đâu. Và nó không nổ — nó nói dối. Thêm vài dòng in ra rồi chạy:

Kết quả chạy thật — cùng một biến, hai giá trị
$ g++ -std=c++17 -O2 constcast.cpp && ./a.out
x doc truc tiep    = 10
x doc qua con tro  = 999
x doc qua p        = 999

Cùng một biến x, trong cùng một lần chạy, đọc thẳng ra 10 còn đọc qua con trỏ ra 999. Compiler thấy xconst nên nó thay luôn giá trị 10 vào chỗ đọc trực tiếp — một tối ưu hoàn toàn hợp lệ, vì theo chuẩn thì x không thể đổi. Bộ nhớ thì đã bị ghi 999 thật. Kết quả là chương trình mang hai sự thật mâu thuẫn cùng lúc, và không có cảnh báo nào ở cả -O0 lẫn -O2.
const_cast chỉ an toàn khi object gốc không phải const — ví dụ một API cũ nhận char* trong khi bạn đang giữ const char* trỏ tới bộ đệm bạn tự tạo.

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):

reinterpret_cast_demo.cpp
#include <iostream>

int main() {
    int x = 42;

    // Turn an address into an integer
    unsigned long long addr = reinterpret_cast<unsigned long long>(&x);

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

    // ...and back again
    int* ptr = reinterpret_cast<int*>(addr);

    // Only reach for this when nothing else can express what you need

    return 0;
}

dynamic_cast: kiểm tra kiểu lúc chạy

Bốn cách trên đều được quyết định lúc biên dịch. dynamic_cast khác hẳn: nó kiểm tra lúc chạy xem object có thật sự thuộc kiểu bạn nghĩ hay không, và trả về nullptr nếu không. Nó chỉ dùng được với các lớp có hàm ảo (polymorphic) — chi tiết về virtual và VTable nằm ở Bài 6, ở đây chỉ cần thấy nó khác gì static_cast:

dynamic_cast_demo.cpp
#include <iostream>

struct Shape {
    virtual ~Shape() = default;   // Needs a virtual function, or dynamic_cast will not compile
};
struct Circle : Shape {
    void area() { std::cout << "  Circle::area\n"; }
};
struct Square : Shape {};

void handle(Shape* shape) {
    // Ask directly: is this object actually a Circle?
    if (Circle* c = dynamic_cast<Circle*>(shape)) {
        std::cout << "yes, it is a Circle\n";
        c->area();
    } else {
        std::cout << "NOT a Circle -> dynamic_cast returned nullptr\n";
    }
}

int main() {
    Circle c;
    Square sq;
    handle(&c);
    handle(&sq);

    // static_cast checks NOTHING: it takes your word for it, and stays silent.
    Circle* wrong = static_cast<Circle*>(static_cast<Shape*>(&sq));
    std::cout << "static_cast gave a non-null pointer? " << (wrong != nullptr) << "\n";

    return 0;
}
Kết quả chạy thật
yes, it is a Circle
  Circle::area
NOT a Circle -> dynamic_cast returned nullptr
static_cast gave a non-null pointer? 1

Dòng cuối là điểm đáng nhớ. Square không phải Circle, nhưng static_cast vẫn trả về một con trỏ khác null — nó không kiểm tra gì cả. Dùng con trỏ đó để gọi hàm của Circle là undefined behaviour. Đó chính là cái giá của "nhanh hơn": dynamic_cast tốn một phép tra bảng lúc chạy, và đổi lại nó nói thật cho bạn.

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
📝 Kiểm tra kiến thức bài 2
Khi nào nên dùng const auto& thay vì auto trong vòng lặp range-based for?

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