Trong C++, hàm tạo (constructor) được dùng để khởi tạo đối tượng khi nó được tạo ra. Có nhiều loại hàm tạo phổ biến:
- Hàm tạo mặc định (không tham số)
- Hàm tạo có tham số
- Hàm tạo sao chép (copy constructor)
- Hàm tạo di chuyển (move constructor, sử dụng tham chiếu rvalue)
- Hàm tạo ủy quyền (delegating constructor)
- Hàm tạo chuyển đổi (conversion constructor)
Ví dụ minh họa các loại hàm tạo
#include <iostream>
using namespace std;
class Student {
public:
// Hàm tạo mặc định
Student() : age(20), num(1000) {}
// Hàm tạo có tham số
Student(int a, int n) : age(a), num(n) {}
// Hàm tạo sao chép
Student(const Student& other) : age(other.age), num(other.num) {}
// Hàm tạo chuyển đổi từ int
explicit Student(int value) : age(value), num(1002) {}
~Student() = default;
public:
int age;
int num;
};
int main() {
Student s1; // Gọi hàm tạo mặc định
Student s2(18, 1001); // Gọi hàm tạo có tham số
Student s3 = Student(10); // Gọi hàm tạo chuyển đổi (explicit tránh ép kiểu ngầm)
Student s4(s3); // Gọi hàm tạo sao chép
printf("s1 age:%d, num:%d\n", s1.age, s1.num);
printf("s2 age:%d, num:%d\n", s2.age, s2.num);
printf("s3 age:%d, num:%d\n", s3.age, s3.num);
printf("s4 age:%d, num:%d\n", s4.age, s4.num);
return 0;
}
Hàm tạo ủy quyền
Hàm tạo ủy quyền cho phép một hàm tạo gọi đến hàm tạo khác trong cùng lớp:
class Person {
public:
Person() : Person(1, 'a') {}
Person(int id) : Person(id, 'a') {}
Person(char c) : Person(1, c) {}
private:
Person(int id, char c) : type(id), name(c) {}
int type{1};
char name{'a'};
};
Lưu ý về khởi tạo thành viên lớp
Có hai cách chính để khởi tạo thành viên trong lớp:
- Danh sách khởi tạo (initializer list): Khuyến khích sử dụng vì hiệu quả hơn.
- Gán giá trị trong thân hàm tạo: Thực chất là gán sau khi đã khởi tạo mặc định.
Danh sách khởi tạo trực tiếp gọi hàm tạo của thành viên, trong khi gán trong thân hàm tạo sẽ gọi toán tử gán sau khi thành viên đã được khởi tạo.
Bắt buộc phải dùng danh sách khởi tạo trong các trường hợp sau:
- Thành viên là tham chiếu
- Thành viên là hằng (
const) - Gọi hàm tạo của lớp cơ sở với tham số
- Gọi hàm tạo của đối tượng thành viên với tham số
Khởi tạo mặc định tại nơi khai báo (C++11)
Từ C++11, có thể gán giá trị mặc định trực tiếp khi khai báo thành viên:
class Example {
public:
int x = 1; // Hợp lệ
// B y(2); // Không hợp lệ – gây nhập nhằng với khai báo hàm
B y{2}; // Hợp lệ – dùng cú pháp uniform initialization
};
Lưu ý: Không được dùng dấu ngoặc tròn () để gán giá trị mặc định tại nơi khai báo vì cú pháp này bị hiểu là khai báo hàm thành viên.
Về tối ưu giá trị trả về (RVO)
Khi trả về đối tượng từ hàm, trình biên dịch thường áp dụng tối ưu giá trị trả về (Return Value Optimization - RVO) để tránh gọi hàm tạo sao chép không cần thiết. Ví dụ:
class A {
public:
int a;
A(int i) : a(i) { cout << "Tạo A(" << i << ")\n"; }
A(const A& other) : a(other.a) { cout << "Sao chép A(" << a << ")\n"; }
A& operator=(const A& other) { a = other.a; cout << "Gán A(" << a << ")\n"; return *this; }
~A() { cout << "Hủy A(" << a << ")\n"; }
};
A makeA(int k) {
cout << "Trong makeA\n";
return A(k); // RVO có thể bỏ qua copy constructor
}
int main() {
A obj1 = makeA(5); // Có thể chỉ gọi constructor, không gọi copy
return 0;
}