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:
#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.
namespace stdnamespace 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++ và 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:
#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 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:
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 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 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:
#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
autokhi 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ỹ:
#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;
}
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
#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
#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ệ:
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:
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:
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:
$ 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 x là const 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):
#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:
#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;
}
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 |
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
Bình luận