Chuyển đến nội dung

Hàm tạo, hàm hủy và quy tắc 0/3/5

Nắm vững toàn bộ vòng đời của các đối tượng C++ — từ xây dựng đến phá hủy — và tìm hiểu khi nào nên viết (hoặc không viết) các hàm thành viên đặc biệt.

Vòng đời đối tượng trong C++

Trong C++, bạn có quyền kiểm soát trực tiếp thời điểm và cách thức các đối tượng được tạo và hủy. Trình biên dịch có thể tự động tạo tối đa sáu hàm thành viên đặc biệt: hàm tạo mặc định, hàm tạo sao chép, toán tử gán sao chép, hàm tạo di chuyển, toán tử gán di chuyển và hàm hủy. Hiểu khi nào nên tự viết - và khi nào để trình biên dịch thực hiện việc đó - là một trong những kỹ năng quan trọng nhất trong C++.

Giống xây dựng

Các nhà xây dựng khởi tạo một đối tượng. C++ hỗ trợ một số loại: mặc định (không có đối số), được tham số, chuyển đổi (đối số đơn, chuyển đổi ngầm định) và ủy quyền (một hàm tạo gọi một hàm tạo khác).

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

class Temperature {
    double celsius_;
    std::string label_;

public:
    // Default constructor
    Temperature() : celsius_{0.0}, label_{"unknown"} {}

    // Parameterized constructor
    Temperature(double c, const std::string& label)
        : celsius_{c}, label_{label} {}

    // Converting constructor — allows implicit conversion from double
    Temperature(double c) : celsius_{c}, label_{"sensor"} {}

    // Delegating constructor — reuses another constructor
    Temperature(const std::string& label)
        : Temperature{0.0, label} {}  // delegates to parameterized

    void print() const {
        std::cout << label_ << ": " << celsius_ << "°C\n";
    }
};

int main() {
    Temperature t1;                   // default
    Temperature t2{36.6, "body"};     // parameterized
    Temperature t3 = 100.0;           // converting (implicit)
    Temperature t4{"outdoor"};        // delegating

    t1.print();  // unknown: 0°C
    t2.print();  // body: 36.6°C
    t3.print();  // sensor: 100°C
    t4.print();  // outdoor: 0°C
    return 0;
}

Từ khóa rõ ràng

Hàm tạo đối số đơn cho phép chuyển đổi ngầm, điều này có thể gây ra các lỗi nhỏ. Sử dụng explicit để ngăn các chuyển đổi ngoài ý muốn — đây hầu như luôn là điều bạn mong muốn.

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

class UserId {
    int id_;
public:
    explicit UserId(int id) : id_{id} {}
    int get() const { return id_; }
};

void process(UserId uid) {
    std::cout << "Processing user #" << uid.get() << '\n';
}

int main() {
    UserId u1{42};       // OK: direct initialization
    // UserId u2 = 42;   // ERROR: implicit conversion blocked by explicit
    // process(42);      // ERROR: would require implicit conversion
    process(UserId{42}); // OK: explicit construction
    return 0;
}

Danh sách khởi tạo thành viên & Thứ tự khởi tạo

danh sách khởi tạo thành viên là cách ưu tiên để khởi tạo thành viên. Nó bắt buộc đối với các thành viênconst, thành viên tham chiếu và thành viên không có hàm tạo mặc định. Các thành viên được khởi tạo theo thứ tự khai báo, không phải thứ tự chúng xuất hiện trong danh sách khởi tạo.

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

class Connection {
    const int max_retries_;      // const: MUST use init list
    std::string& log_target_;   // reference: MUST use init list
    std::string host_;
    int port_;

public:
    Connection(std::string& log, const std::string& host, int port)
        : max_retries_{3}          // const member
        , log_target_{log}         // reference member
        , host_{host}              // initialized in declaration order
        , port_{port}
    {
        // Constructor body runs AFTER all members are initialized
        log_target_ += "[Connection to " + host_ + " created]\n";
    }

    void print() const {
        std::cout << host_ << ':' << port_
                  << " (max retries: " << max_retries_ << ")\n";
    }
};

int main() {
    std::string log;
    Connection conn{log, "db.example.com", 5432};
    conn.print();
    std::cout << log;
    return 0;
}

Destructors & Rule of Zero / Three / Five

Trình hủy (~ClassName()) chạy khi một đối tượng bị hủy — nó giải phóng tài nguyên và không được đưa ra ngoại lệ. Đối với hầu hết các lớp, hàm hủy do trình biên dịch tạo ra là đủ.

Quy tắc số 0: ưu tiên các trình bao bọc RAII (std::string,std::vector,std::unique_ptr) để bạn không bao giờ cần phải viết bất kỳ hàm thành viên đặc biệt nào. Nếu bạn phải quản lý tài nguyên thô, Quy tắc ba nói rằng bạn cần một hàm hủy, hàm tạo sao chép và toán tử gán sao chép. Quy tắc năm mở rộng điều này để bao gồm hàm tạo di chuyển và toán tử gán di chuyển.

rule_of_five.cpp
#include <iostream>
#include <cstring>

// === Rule of Zero: the GOAL ===
// No special members needed — std::string handles everything
class Person {
    std::string name_;
    int age_;
public:
    Person(const std::string& name, int age)
        : name_{name}, age_{age} {}
    // Compiler generates correct copy/move/destructor automatically!
};

// === Rule of Five: when managing a raw resource ===
class CharBuffer {
    char* data_;
    std::size_t size_;

public:
    explicit CharBuffer(const char* str)
        : size_{std::strlen(str)}
        , data_{new char[std::strlen(str) + 1]}
    {
        std::strcpy(data_, str);
    }

    // Destructor
    ~CharBuffer() { delete[] data_; }

    // Copy constructor (deep copy)
    CharBuffer(const CharBuffer& other)
        : size_{other.size_}
        , data_{new char[other.size_ + 1]}
    {
        std::strcpy(data_, other.data_);
    }

    // Copy assignment (copy-and-swap idiom)
    CharBuffer& operator=(CharBuffer other) {  // note: by value
        swap(*this, other);
        return *this;
    }

    // Move constructor
    CharBuffer(CharBuffer&& other) noexcept
        : data_{other.data_}, size_{other.size_}
    {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // Move assignment
    CharBuffer& operator=(CharBuffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    friend void swap(CharBuffer& a, CharBuffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    void print() const {
        std::cout << (data_ ? data_ : "(null)") << '\n';
    }
};

int main() {
    CharBuffer a{"Hello"};
    CharBuffer b = a;           // copy constructor
    CharBuffer c{"World"};
    c = a;                      // copy assignment
    CharBuffer d = std::move(a); // move constructor
    d.print();  // Hello
    a.print();  // (null) — moved-from state
    return 0;
}

=default and =delete

You can explicitly request the compiler-generated version of a special member with =default, or explicitly suppress it with =delete. This makes your intent clear and documents the class's copying/moving semantics.

default_delete.cpp
#include <memory>
#include <string>

// Non-copyable, movable resource handle
class FileHandle {
    std::string path_;
    int fd_;

public:
    FileHandle(const std::string& path, int fd)
        : path_{path}, fd_{fd} {}

    // Explicitly default the destructor
    ~FileHandle() = default;

    // Delete copy operations — file handles should not be duplicated
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Default move operations
    FileHandle(FileHandle&&) = default;
    FileHandle& operator=(FileHandle&&) = default;
};

int main() {
    FileHandle fh{"data.txt", 3};
    // FileHandle copy = fh;         // ERROR: copy deleted
    FileHandle moved = std::move(fh); // OK: move is allowed
    return 0;
}
Lỗi thường gặp

Members are initialized in the order they are declared in the class, not the order they appear in the initializer list. If one member depends on another, make sure the dependency is declared first. Compilers warn about this with-Wreorder— never ignore that warning. A classic bug: initializingsize_from a parameter but then usingsize_to allocatedata_, withdata_declared beforesize_.

Điểm chính
  • Useexpliciton single-argument constructors to prevent unintended implicit conversions
  • The member initializer list is required forconstand reference members, and more efficient for all members
  • Members initialize in declaration order, not initializer-list order
  • Aim for the Rule of Zero — use RAII types so the compiler-generated special members are correct
  • If you write any of destructor/copy-ctor/copy-assign, write all five (Rule of Five)
  • Mục đích của tài liệu=default;=deletenghiêm cấm một hoạt động

Câu đố — Kiểm tra kiến thức

(20 XP)

1. What does the`explicit`keyword on a constructor prevent?

2. In what order are class members initialized?

3. What is the Rule of Zero?