[C++ OOP Thực Chiến] Bài 13: Phương thức khởi tạo có tham số (Phần 1) - Phân biệt "Gán" và "Khởi tạo" thực sự!
Chào anh em! Ở [Bài 12], chúng ta đã biết cách viết Parameterized Constructor (Hàm khởi tạo có tham số) để đẻ ra một Object mang sẵn số liệu:
Character(string n, int h, int d) {
name = n;
hp = h;
damage = d;
}
Nhìn thì có vẻ rất bình thường và code vẫn chạy đúng. Nhưng nếu đem đoạn code này cho một Senior C++ review, họ sẽ nhăn mặt và nói: "Em đang làm chậm hệ thống đấy, đây là Gán (Assignment), không phải Khởi tạo (Initialization)!".
Tại sao lại như vậy? Hôm nay chúng ta sẽ bóc trần quá trình C++ đúc ra một Object từ trong bộ nhớ.
1. Sự thật mất lòng: Bạn đã tốn gấp đôi công sức!
Hãy nhìn vào đoạn code trên. Khi chương trình chạy đến dấu ngoặc nhọn mở { của Constructor, toàn bộ vùng nhớ cho Object đã được cấp phát xong.
Lúc này, C++ sẽ tự động gọi Default Constructor cho tất cả các thuộc tính bên trong. Đặc biệt với biến name (kiểu std::string - vốn là một Object phức tạp), C++ đã lén tạo ra một chuỗi rỗng "" cho nó.
Khi bạn bước vào bên trong dấu {} và viết name = n;, bạn đang thực hiện thao tác Gán (Assignment). Tức là C++ phải:
- Đập bỏ cái chuỗi rỗng vừa tạo.
- Copy dữ liệu từ biến
nđè vào biếnname.
Kết luận: Bạn mất 2 lần chi phí (Tạo rỗng -> Gán đè). Nếu bạn làm một hệ thống có hàng triệu Object khởi tạo liên tục, server của bạn sẽ lãng phí tài nguyên CPU và RAM một cách vô ích!
2. Vũ khí tối thượng: Danh sách khởi tạo (Initializer List)
Để giải quyết bài toán trên, C++ cung cấp một cú pháp gọi là Initializer List. Nó ép C++ lấy thẳng cái giá trị bạn truyền vào để đúc thành Object ngay từ ban đầu, bỏ qua bước "tạo rỗng".
Cú pháp: Thêm dấu hai chấm : sau ngoặc tròn, theo sau là tên thuộc tính và giá trị khởi tạo nằm trong ngoặc ().
// CÁCH CỦA SENIOR: KHỞI TẠO THỰC SỰ
Character(string n, int h, int d) : name(n), hp(h), damage(d) {
// Bên trong này không cần làm gì nữa, hoặc chỉ viết logic log ra màn hình
cout << "Da khoi tao " << name << " sieu toc!\n";
}
Viết như thế này, name được đúc thẳng ra bằng n. Chi phí chỉ tốn ĐÚNG 1 LẦN. Nhanh gọn và chuẩn xác!
3. Những trường hợp BẮT BUỘC phải dùng Initializer List
Tối ưu hiệu năng là một chuyện, nhưng có những trường hợp C++ cấm bạn dùng phép gán = trong dấu {}, mà bắt buộc phải dùng Initializer List. Đó là khi Class của bạn có:
- Biến hằng số (
const): Hằng số sinh ra là không được phép thay đổi. Bạn phải cung cấp giá trị cho nó ngay tại thời điểm nó chào đời. Nếu để nó chui vào{}rồi mới gán thì đã quá muộn! - Biến tham chiếu (
&): Tương tự hằng số, tham chiếu khi sinh ra phải trỏ ngay vào một vùng nhớ cụ thể.
4. Code Demo: Thiết lập cấu hình Server
Để thấy rõ sức mạnh của Initializer List, chúng ta hãy viết một Class ServerConfig chứa địa chỉ Host (là hằng số không được đổi) và số lượng Port.
#include <iostream>
#include <string>
using namespace std;
class ServerConfig {
private:
const string HOST_IP; // Hằng số: Không bao giờ thay đổi sau khi tạo
int port;
public:
// NẾU VIẾT THẾ NÀY SẼ LỖI COMPILER NGAY:
/*
ServerConfig(string ip, int p) {
HOST_IP = ip; // LỖI: Không thể gán giá trị cho biến const
port = p;
}
*/
// PHẢI DÙNG INITIALIZER LIST:
ServerConfig(string ip, int p) : HOST_IP(ip), port(p) {
cout << "[SYSTEM] Server da khoi dong tai "
<< HOST_IP << ":" << port << "\n";
}
void showConfig() const {
cout << "--- SERVER INFO ---\n"
<< "IP: " << HOST_IP << "\n"
<< "Port: " << port << "\n"
<< "-------------------\n";
}
};
int main() {
// Khởi tạo một cấu hình server
ServerConfig myDatabase("192.168.1.100", 5432); // 5432 là port quen thuộc của PostgreSQL
myDatabase.showConfig();
return 0;
}
Nhận xét: Việc dùng Initializer List không chỉ là một thủ thuật tối ưu, mà nó thể hiện bạn là một người thực sự hiểu về vòng đời (Lifecycle) của một biến trong bộ nhớ.
Tạm kết & Gợi mở
Tuyệt vời! Từ nay trở đi, hãy rèn luyện thói quen sử dụng Initializer List cho mọi Constructor mà bạn viết. Code của bạn sẽ chạy mượt mà và chuẩn mực hơn rất nhiều.
Nhưng câu chuyện về Constructor chưa dừng lại ở đó. Nếu một Class có tận 5-6 cái Constructor khác nhau (overloading) để phục vụ cho nhiều tình huống, liệu chúng ta có phải viết đi viết lại đoạn Initializer List dài dằng dặc không? Và có cách nào ngăn chặn việc C++ tự động ép kiểu bừa bãi khi ta lỡ truyền nhầm tham số vào Constructor không?
Đã đến lúc gom tất cả các kỹ năng từ đầu series lại và thực hành. Hẹn gặp lại anh em ở Bài 14: Phương thức khởi tạo có tham số (Phần 2) + Bài tập thực chiến. Hãy chuẩn bị sẵn IDE để code nhé!
All rights reserved