Back to blog

Deep Dive: Quản Lý Bộ Nhớ & Smart Pointers

cppc++memory-managementsmart-pointersmodern-cpp
Deep Dive: Quản Lý Bộ Nhớ & Smart Pointers

Quản lý bộ nhớ là thử thách cốt lõi của C++. Đây là nguồn gốc sức mạnh lớn nhất của ngôn ngữ — và cũng là nguyên nhân của những bug khét tiếng nhất. Làm đúng thì code chạy nhanh như phần cứng cho phép, không overhead. Làm sai thì crash, leak, lỗ hổng bảo mật, và hàng giờ debug.

Modern C++ đã giải quyết phần lớn những vấn đề này. Các kỹ thuật trong bài — RAII, smart pointers, và ownership semantics — loại bỏ nguyên cả class bug ngay từ compile time. Nếu bạn vẫn đang viết new và delete bằng tay, đã đến lúc dừng lại.

Thời gian học: 1-2 tuần, 1-2 giờ mỗi ngày
Điều kiện tiên quyết: Phase 3: Tính Năng Modern C++

Đọc thêm: Về các chủ đề nền tảng, xem C++ Stack vs Heap Memory và C++ Pointers: Hướng Dẫn Toàn Diện.

Những Gì Bạn Sẽ Học

✅ Hiểu C++ memory model: stack, heap, và object lifetime
✅ Áp dụng RAII để quản lý mọi loại resource tự động
✅ Sử dụng std::unique_ptr cho exclusive ownership
✅ Sử dụng std::shared_ptr cho shared ownership với reference counting
✅ Sử dụng std::weak_ptr để phá vỡ circular references
✅ Viết custom deleters cho non-standard resources
✅ Áp dụng ownership semantics để design API rõ ràng
✅ Tránh các memory safety pitfalls phổ biến nhất trong C++


1. C++ Memory Model

Mỗi object trong C++ có một storage duration quyết định khi nào nó được tạo và hủy:

Storage DurationỞ ĐâuLifetimeVí Dụ
AutomaticStackDựa trên scopeBiến cục bộ
DynamicHeapThủ công / smart pointerObject cấp phát bằng new
StaticData segmentSuốt chương trìnhBiến static và global
ThreadThread-localSuốt threadBiến thread_local

Stack vs Heap — Phiên Bản Nhanh

#include <memory>
 
void example() {
    // Stack: cấp phát nhanh, tự dọn dẹp
    int x = 42;                    // hủy khi hết scope
    double arr[100];               // kích thước cố định, trên stack
 
    // Heap: kích thước linh hoạt, phải quản lý lifetime
    int* p = new int(42);          // BẠN phải delete cái này
    delete p;                      // dọn dẹp thủ công — dễ lỗi
 
    // Heap với smart pointer: tự dọn dẹp
    auto sp = std::make_unique<int>(42);  // tự delete
}   // x, arr, sp đều được dọn ở đây. Nếu quên 'delete p' → leak.

Insight quan trọng: Stack allocation gần như miễn phí (chỉ là dịch pointer). Heap allocation đi qua memory allocator và chậm hơn hàng chục lần. Ưu tiên stack khi có thể — dùng heap khi cần dynamic lifetime, polymorphism, hoặc data có kích thước lớn/thay đổi.

Object Lifetime

Mỗi C++ object tuân theo một lifecycle nghiêm ngặt:

Với stack objects, compiler lo hết cả 5 bước. Với heap objects, bạn chịu trách nhiệm destruction và deallocation — trừ khi bạn dùng RAII.


2. RAII: Nền Tảng Của C++ An Toàn

RAII (Resource Acquisition Is Initialization) là idiom quan trọng nhất trong C++. Ý tưởng rất đơn giản: gắn mọi resource vào lifetime của object. Acquire resource trong constructor, release trong destructor. Khi object ra khỏi scope, cleanup tự động xảy ra.

Vấn Đề RAII Giải Quyết

// KHÔNG CÓ RAII — quản lý resource thủ công
void processFile(const std::string& path) {
    FILE* f = fopen(path.c_str(), "r");
    if (!f) return;
 
    char* buffer = new char[1024];
 
    // Nếu hàm này throw thì sao?
    processData(f, buffer);
 
    delete[] buffer;    // bị skip nếu exception!
    fclose(f);          // bị skip nếu exception!
}

Code này có ba bug:

  1. Nếu processData throw, buffer bị leak
  2. Nếu processData throw, f không bao giờ được close
  3. Nếu bạn thêm return sớm, có thể quên cleanup

Giải Pháp RAII

#include <fstream>
#include <vector>
#include <string>
 
// CÓ RAII — quản lý resource tự động
void processFile(const std::string& path) {
    std::ifstream file(path);       // mở file trong constructor
    if (!file) return;
 
    std::vector<char> buffer(1024); // cấp phát trong constructor
 
    processData(file, buffer);
 
}   // file đóng, buffer giải phóng — TỰ ĐỘNG
    // Kể cả khi processData throw — destructors LUÔN chạy

Không leak. Không quên cleanup. Không bug từ early returns hay exceptions. RAII làm cho quản lý resource đúng ngay từ thiết kế.

Viết RAII Class Của Riêng Bạn

Bất kỳ resource nào cần cleanup đều có thể wrap trong RAII:

#include <cstdio>
#include <stdexcept>
#include <string>
 
class FileHandle {
public:
    explicit FileHandle(const std::string& path, const char* mode)
        : file_(fopen(path.c_str(), mode))
    {
        if (!file_) {
            throw std::runtime_error("Không mở được: " + path);
        }
    }
 
    // Destructor — luôn chạy khi object ra khỏi scope
    ~FileHandle() {
        if (file_) {
            fclose(file_);
        }
    }
 
    // Xóa copy operations — file handles không copy được
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
 
    // Cho phép move operations — chuyển ownership
    FileHandle(FileHandle&& other) noexcept : file_(other.file_) {
        other.file_ = nullptr;
    }
 
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (file_) fclose(file_);
            file_ = other.file_;
            other.file_ = nullptr;
        }
        return *this;
    }
 
    FILE* get() const { return file_; }
 
    void write(const std::string& data) {
        fwrite(data.c_str(), 1, data.size(), file_);
    }
 
private:
    FILE* file_;
};
 
// Sử dụng
void writeReport() {
    FileHandle report("report.txt", "w");
    report.write("Hello, RAII!\n");
}   // file tự động đóng ở đây

RAII Cho Non-Memory Resources

RAII hoạt động với mọi loại resource — không chỉ memory:

#include <mutex>
#include <iostream>
 
// Mutex locking
std::mutex mtx;
 
void safeOperation() {
    std::lock_guard<std::mutex> lock(mtx);  // lock mutex
    // ... critical section ...
}   // mutex tự động unlock
 
// Database transactions
class Transaction {
public:
    explicit Transaction(Database& db) : db_(db) {
        db_.begin();
    }
    ~Transaction() {
        if (!committed_) {
            db_.rollback();   // tự rollback nếu chưa commit
        }
    }
    void commit() {
        db_.commit();
        committed_ = true;
    }
private:
    Database& db_;
    bool committed_ = false;
};

Quy tắc RAII: Nếu bạn acquire nó, hãy wrap nó. Nếu nó có close(), release(), unlock(), hay delete — nó thuộc về destructor.


3. std::unique_ptr — Exclusive Ownership

std::unique_ptr là con ngựa chiến của memory management trong modern C++. Nó sở hữu một heap-allocated object một cách độc quyền — chỉ đúng một unique_ptr trỏ đến object tại bất kỳ thời điểm nào. Khi unique_ptr bị hủy, object được delete.

Cách Dùng Cơ Bản

#include <memory>
#include <iostream>
#include <string>
 
struct User {
    std::string name;
    int age;
 
    User(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "User created: " << name << "\n";
    }
    ~User() {
        std::cout << "User destroyed: " << name << "\n";
    }
};
 
int main() {
    // Tạo với make_unique (ưu tiên — exception-safe)
    auto alice = std::make_unique<User>("Alice", 30);
 
    std::cout << alice->name << " is " << alice->age << "\n";
 
    // Truy cập raw pointer (không chuyển ownership)
    User* raw = alice.get();
    std::cout << raw->name << "\n";
 
    // Kiểm tra có sở hữu gì không
    if (alice) {
        std::cout << "alice is not null\n";
    }
 
}   // alice bị hủy ở đây → User destroyed: Alice

Chuyển Ownership Với std::move

unique_ptr không thể copy — ownership là exclusive. Nhưng bạn có thể move nó:

#include <memory>
#include <iostream>
#include <vector>
 
auto createUser(std::string name, int age) {
    return std::make_unique<User>(std::move(name), age);
}
 
int main() {
    auto user = createUser("Bob", 25);
 
    // Chuyển ownership
    auto user2 = std::move(user);
 
    // user giờ là nullptr!
    if (!user) {
        std::cout << "user is null after move\n";
    }
 
    // Lưu trong containers
    std::vector<std::unique_ptr<User>> users;
    users.push_back(std::make_unique<User>("Charlie", 35));
    users.push_back(std::move(user2));
 
}   // tất cả users bị hủy khi vector bị hủy

unique_ptr và Polymorphism

unique_ptr hoàn hảo cho polymorphic objects:

#include <memory>
#include <iostream>
#include <vector>
#include <string>
 
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};
 
class Circle : public Shape {
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
    std::string name() const override { return "Circle"; }
private:
    double radius_;
};
 
class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double area() const override { return width_ * height_; }
    std::string name() const override { return "Rectangle"; }
private:
    double width_, height_;
};
 
int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5.0));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
 
    for (const auto& shape : shapes) {
        std::cout << shape->name() << ": area = "
                  << shape->area() << "\n";
    }
}

unique_ptr Với Arrays

#include <memory>
#include <iostream>
 
int main() {
    // Phiên bản array — tự gọi delete[]
    auto arr = std::make_unique<int[]>(100);
 
    for (int i = 0; i < 100; ++i) {
        arr[i] = i * i;
    }
 
    std::cout << "arr[10] = " << arr[10] << "\n"; // 100
}
 
// Nhưng hầu hết trường hợp nên dùng std::vector:
// std::vector<int> arr(100);

Custom Deleters

Đôi khi bạn cần cleanup khác ngoài delete. Custom deleters lo được:

#include <memory>
#include <cstdio>
#include <iostream>
 
// Custom deleter cho FILE*
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            std::cout << "Đang đóng file\n";
            fclose(f);
        }
    }
};
 
// Type bao gồm cả deleter
using FilePtr = std::unique_ptr<FILE, FileDeleter>;
 
FilePtr openFile(const char* path, const char* mode) {
    return FilePtr(fopen(path, mode));
}
 
int main() {
    auto file = openFile("data.txt", "w");
    if (file) {
        fprintf(file.get(), "Hello from unique_ptr!\n");
    }
}   // file được đóng bởi FileDeleter
 
// Lambda deleter (ngắn gọn hơn)
auto closerLambda = [](FILE* f) {
    if (f) fclose(f);
};
 
// Lưu ý: lambda deleters thay đổi unique_ptr type
std::unique_ptr<FILE, decltype(closerLambda)> file2(
    fopen("data.txt", "r"), closerLambda
);

Khi Nào Dùng unique_ptr

Dùng unique_ptr làm lựa chọn mặc định cho heap allocation:

  • Factory functions tạo objects
  • Class members với polymorphic types
  • Containers chứa polymorphic objects
  • Pimpl (Pointer to Implementation) idiom
  • Bất kỳ khi nào bạn sẽ viết new — wrap nó trong make_unique

Chi phí: Zero overhead so với raw new/delete. Cùng size với raw pointer (trừ khi bạn có stateful deleter). Cùng performance.


4. std::shared_ptr — Shared Ownership

Đôi khi nhiều phần trong code cần chia sẻ ownership của một object. std::shared_ptr dùng reference counting: object bị hủy khi shared_ptr cuối cùng trỏ đến nó bị hủy.

Cách Dùng Cơ Bản

#include <memory>
#include <iostream>
#include <string>
 
struct Config {
    std::string database_url;
    int max_connections;
 
    Config(std::string url, int max)
        : database_url(std::move(url)), max_connections(max) {}
    ~Config() { std::cout << "Config destroyed\n"; }
};
 
int main() {
    // Tạo với make_shared (single allocation, cache-friendly)
    auto config = std::make_shared<Config>("postgres://localhost/db", 10);
 
    std::cout << "ref count: " << config.use_count() << "\n"; // 1
 
    {
        // Copy — tăng reference count
        auto config2 = config;
        std::cout << "ref count: " << config.use_count() << "\n"; // 2
 
        auto config3 = config;
        std::cout << "ref count: " << config.use_count() << "\n"; // 3
 
    }   // config2, config3 bị hủy → ref count trở về 1
 
    std::cout << "ref count: " << config.use_count() << "\n"; // 1
 
}   // config bị hủy → ref count 0 → Config destroyed

make_shared vs shared_ptr Constructor

Luôn ưu tiên std::make_shared:

#include <memory>
 
struct Widget {
    int x, y;
};
 
// TỐT: single allocation (object + control block cùng nhau)
auto w1 = std::make_shared<Widget>();
 
// KÉM HƠN: hai allocations (object + control block riêng)
std::shared_ptr<Widget> w2(new Widget());
 
// NGUY HIỂM: có thể leak trong function arguments trước C++17
// process(std::shared_ptr<Widget>(new Widget()), mightThrow());
// Nếu mightThrow() throw sau new nhưng trước shared_ptr constructor,
// Widget bị leak. make_shared tránh hoàn toàn vấn đề này.

Tại sao make_shared tốt hơn:

  1. Một allocation thay vì hai (object + control block trong cùng block memory)
  2. Exception safe — không có cửa sổ leak giữa new và smart pointer construction
  3. Cache locality tốt hơn — object và reference count nằm kề nhau trong memory

Chia Sẻ Giữa Các Objects

#include <memory>
#include <iostream>
#include <string>
#include <vector>
 
class Logger {
public:
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << "\n";
    }
};
 
class Service {
public:
    explicit Service(std::string name, std::shared_ptr<Logger> logger)
        : name_(std::move(name)), logger_(std::move(logger)) {}
 
    void doWork() {
        logger_->log(name_ + " doing work");
    }
 
private:
    std::string name_;
    std::shared_ptr<Logger> logger_;  // shared ownership
};
 
int main() {
    auto logger = std::make_shared<Logger>();
 
    // Nhiều services chia sẻ cùng một logger
    Service auth("AuthService", logger);
    Service data("DataService", logger);
    Service cache("CacheService", logger);
 
    std::cout << "Logger ref count: " << logger.use_count() << "\n"; // 4
 
    auth.doWork();
    data.doWork();
    cache.doWork();
}
// Logger chỉ bị hủy sau khi TẤT CẢ services và 'logger' biến mất

Aliasing Constructor

Tính năng ít biết: tạo shared_ptr chia sẻ ownership của một object nhưng trỏ đến member:

#include <memory>
#include <iostream>
#include <string>
 
struct Pair {
    std::string first;
    std::string second;
};
 
int main() {
    auto pair = std::make_shared<Pair>(Pair{"hello", "world"});
 
    // Aliasing constructor: chia sẻ ownership của 'pair',
    // nhưng trỏ đến pair->first
    std::shared_ptr<std::string> first(pair, &pair->first);
 
    pair.reset();  // release handle của chúng ta
 
    // 'first' giữ cả Pair sống!
    std::cout << *first << "\n"; // "hello" — vẫn valid!
    std::cout << "ref count: " << first.use_count() << "\n"; // 1
}

Performance Considerations

shared_ptr không miễn phí:

// Chi phí cụ thể:
// 1. Allocation thêm cho control block (tránh được với make_shared)
// 2. Atomic reference count increment/decrement (thread-safe nhưng ~10-50ns)
// 3. Hai pointers mỗi shared_ptr (16 bytes trên 64-bit vs 8 cho raw/unique)
// 4. Control block lưu: strong count, weak count, deleter, allocator
 
// Khi nào nó quan trọng:
// - Tight loops với hàng triệu copies → overhead đo được
// - Single-threaded code → atomic ops là overhead không cần thiết
// - Collections lớn → 2x pointer storage cộng dồn

Quy tắc ngón cái: Bắt đầu với unique_ptr. Chỉ dùng shared_ptr khi bạn thực sự cần shared ownership. Nếu không chắc, có lẽ bạn không cần nó.


5. std::weak_ptr — Non-Owning Observer

std::weak_ptr là non-owning reference đến object được quản lý bởi shared_ptr. Nó có thể observe object mà không kéo dài lifetime. Điều này rất quan trọng để phá vỡ circular references.

Vấn Đề Circular Reference

#include <memory>
#include <iostream>
#include <string>
 
struct Node {
    std::string name;
    std::shared_ptr<Node> next;  // sở hữu node tiếp theo
    std::shared_ptr<Node> prev;  // VẤN ĐỀ: circular ownership!
 
    Node(std::string n) : name(std::move(n)) {
        std::cout << "Node " << name << " created\n";
    }
    ~Node() {
        std::cout << "Node " << name << " destroyed\n";
    }
};
 
int main() {
    auto a = std::make_shared<Node>("A");
    auto b = std::make_shared<Node>("B");
 
    a->next = b;   // A sở hữu B
    b->prev = a;   // B sở hữu A → CIRCULAR REFERENCE!
 
    std::cout << "a ref count: " << a.use_count() << "\n"; // 2
    std::cout << "b ref count: " << b.use_count() << "\n"; // 2
}
// a ra khỏi scope → ref count giảm xuống 1 (b->prev vẫn giữ)
// b ra khỏi scope → ref count giảm xuống 1 (a->next vẫn giữ)
// KHÔNG CÁI NÀO bị hủy! MEMORY LEAK!

Cách Sửa: weak_ptr Cho Back-References

#include <memory>
#include <iostream>
#include <string>
 
struct Node {
    std::string name;
    std::shared_ptr<Node> next;   // sở hữu node tiếp theo
    std::weak_ptr<Node> prev;     // SỬA: non-owning back-reference
 
    Node(std::string n) : name(std::move(n)) {
        std::cout << "Node " << name << " created\n";
    }
    ~Node() {
        std::cout << "Node " << name << " destroyed\n";
    }
};
 
int main() {
    auto a = std::make_shared<Node>("A");
    auto b = std::make_shared<Node>("B");
 
    a->next = b;   // A sở hữu B
    b->prev = a;   // weak reference — không ảnh hưởng ref count
 
    std::cout << "a ref count: " << a.use_count() << "\n"; // 1
    std::cout << "b ref count: " << b.use_count() << "\n"; // 2
 
    // Truy cập qua weak_ptr — phải lock trước
    if (auto prev = b->prev.lock()) {
        std::cout << "B's prev: " << prev->name << "\n";
    }
}
// a bị hủy → a ref count 0 → Node A destroyed
// → a->next (shared_ptr đến B) bị hủy → b ref count giảm xuống 1
// b bị hủy → b ref count 0 → Node B destroyed
// Không leak!

Sử Dụng weak_ptr An Toàn

weak_ptr không đảm bảo object còn sống. Bạn phải kiểm tra trước khi truy cập:

#include <memory>
#include <iostream>
 
void useWeakPtr(std::weak_ptr<int> weak) {
    // Cách 1: lock() — trả về shared_ptr hoặc nullptr
    if (auto shared = weak.lock()) {
        std::cout << "Value: " << *shared << "\n";
    } else {
        std::cout << "Object đã bị hủy\n";
    }
 
    // Cách 2: expired() — kiểm tra mà không lock
    if (weak.expired()) {
        std::cout << "Object đã mất\n";
    }
}
 
int main() {
    std::weak_ptr<int> weak;
 
    {
        auto shared = std::make_shared<int>(42);
        weak = shared;
 
        useWeakPtr(weak); // in "Value: 42"
    }   // shared bị hủy ở đây
 
    useWeakPtr(weak);     // in "Object đã bị hủy"
}

Observer Pattern Với weak_ptr

Use case kinh điển — observers không giữ subjects sống:

#include <memory>
#include <vector>
#include <algorithm>
#include <iostream>
#include <string>
 
class EventEmitter {
public:
    using Listener = std::shared_ptr<std::function<void(const std::string&)>>;
 
    void subscribe(Listener listener) {
        listeners_.push_back(listener);
    }
 
    void emit(const std::string& event) {
        // Dọn dẹp listeners hết hạn và notify các active
        listeners_.erase(
            std::remove_if(listeners_.begin(), listeners_.end(),
                [](const std::weak_ptr<std::function<void(const std::string&)>>& w) {
                    return w.expired();
                }),
            listeners_.end()
        );
 
        for (auto& weak : listeners_) {
            if (auto listener = weak.lock()) {
                (*listener)(event);
            }
        }
    }
 
private:
    std::vector<std::weak_ptr<std::function<void(const std::string&)>>> listeners_;
};

Cache Với weak_ptr

#include <memory>
#include <unordered_map>
#include <string>
#include <iostream>
 
class ImageCache {
public:
    std::shared_ptr<Image> load(const std::string& path) {
        // Kiểm tra image còn trong memory không
        auto it = cache_.find(path);
        if (it != cache_.end()) {
            if (auto img = it->second.lock()) {
                std::cout << "Cache hit: " << path << "\n";
                return img;  // vẫn sống — tái sử dụng
            }
            // hết hạn — xóa entry cũ
            cache_.erase(it);
        }
 
        // Load image mới
        std::cout << "Cache miss: " << path << "\n";
        auto img = std::make_shared<Image>(path);
        cache_[path] = img;  // lưu weak reference
        return img;
    }
 
private:
    std::unordered_map<std::string, std::weak_ptr<Image>> cache_;
};

6. So Sánh Smart Pointers

Đây là khi nào dùng cái nào:

PointerOwnershipOverheadDùng Khi
unique_ptrExclusiveZero (như raw new/delete)Lựa chọn mặc định cho heap allocation
shared_ptrSharedAtomic ref count + control blockThực sự cần nhiều owners
weak_ptrNone (observer)Lock check trước khi accessPhá cycles, caches, observers
Raw pointer T*None (non-owning)NoneObserve, optional references
Reference T&None (non-owning)NoneGuaranteed non-null access

Flowchart Quyết Định


7. Ownership Semantics Trong APIs

Cách bạn truyền smart pointers thể hiện ý định ownership. Đây là một trong những design signals quan trọng nhất trong C++.

Function Parameters

#include <memory>
#include <string>
 
class Widget {
public:
    std::string name;
};
 
// 1. Nhận bằng raw pointer hoặc reference — "Tôi chỉ dùng, không sở hữu"
void inspect(const Widget& w) {
    // read-only access, caller giữ ownership
    std::cout << w.name << "\n";
}
 
void modify(Widget& w) {
    // read-write access, caller giữ ownership
    w.name = "modified";
}
 
// 2. Nhận unique_ptr bằng value — "Cho tôi ownership (sink parameter)"
void consume(std::unique_ptr<Widget> w) {
    // Tôi giờ sở hữu widget này. Nó bị hủy khi tôi xong.
    std::cout << "Consumed: " << w->name << "\n";
}
 
// 3. Nhận unique_ptr bằng reference — "Tôi có thể lấy ownership"
void maybeConsume(std::unique_ptr<Widget>& w) {
    if (w->name == "disposable") {
        w.reset();  // lấy và hủy
    }
}
 
// 4. Nhận shared_ptr bằng value — "Tôi cần shared ownership"
void share(std::shared_ptr<Widget> w) {
    // Tôi giờ đồng sở hữu widget này
    // (ref count tăng khi gọi, giảm khi function return)
}
 
// 5. Nhận shared_ptr bằng const ref — "Tôi có thể share ownership"
void maybeShare(const std::shared_ptr<Widget>& w) {
    // Rẻ khi gọi — không thay đổi ref count
    // Có thể copy nếu quyết định giữ lại
}

Return Values

#include <memory>
#include <string>
 
// Factory — return unique_ptr (caller nhận ownership)
std::unique_ptr<Widget> createWidget(std::string name) {
    auto w = std::make_unique<Widget>();
    w->name = std::move(name);
    return w;  // tự động move (NRVO hoặc implicit move)
}
 
// Shared factory — khi callers sẽ share kết quả
std::shared_ptr<Widget> getSharedWidget() {
    static auto instance = std::make_shared<Widget>();
    return instance;
}

Class Members

#include <memory>
#include <string>
#include <vector>
 
class Engine {
public:
    void run() { /* ... */ }
};
 
class Car {
public:
    // Exclusive ownership — Car sở hữu Engine
    Car() : engine_(std::make_unique<Engine>()) {}
 
    // Cung cấp non-owning access
    Engine& engine() { return *engine_; }
 
private:
    std::unique_ptr<Engine> engine_;
};
 
class Team {
public:
    void addMember(std::shared_ptr<Player> player) {
        members_.push_back(std::move(player));
    }
 
private:
    // Shared ownership — players có thể ở nhiều teams
    std::vector<std::shared_ptr<Player>> members_;
};

Core Guidelines

Từ C++ Core Guidelines:

GuidelineQuy Tắc
R.30Chỉ truyền smart pointers làm parameters để thể hiện ownership
R.32Nhận unique_ptr<Widget> để thể hiện function nhận ownership
R.33Nhận unique_ptr<Widget>& để thể hiện function có thể reseat widget
R.34Nhận shared_ptr<Widget> để thể hiện shared ownership
R.36Nhận const shared_ptr<Widget>& để thể hiện có thể giữ reference
F.7Cho general use, nhận T* hoặc T& thay vì smart pointers

Lỗi phổ biến nhất: Truyền shared_ptr khắp nơi "phòng khi cần." Điều này làm ô nhiễm API với reference counting không cần thiết. Nếu function chỉ đọc hoặc modify object, nhận nó bằng reference.


8. Các Memory Safety Pitfalls Thường Gặp

Pitfall 1: Dangling Pointers

#include <memory>
#include <iostream>
 
// BUG: return reference đến local object
int& dangling() {
    int x = 42;
    return x;  // x bị hủy khi hết scope!
}
 
// BUG: giữ raw pointer sau khi unique_ptr bị move
void danglingFromMove() {
    auto ptr = std::make_unique<int>(42);
    int* raw = ptr.get();
 
    auto ptr2 = std::move(ptr);  // ownership đã chuyển
 
    // raw giờ DANGLING — ptr không còn sở hữu object
    // *raw là undefined behavior!
}
 
// SỬA: không sống lâu hơn owner
void safe() {
    auto ptr = std::make_unique<int>(42);
    int* raw = ptr.get();
 
    // Chỉ dùng raw khi ptr còn sống và chưa move
    std::cout << *raw << "\n"; // OK
 
    // Không move ptr khi raw đang được dùng
}

Pitfall 2: Double Deletion

#include <memory>
 
// BUG: hai shared_ptrs từ cùng raw pointer
void doubleFree() {
    int* raw = new int(42);
 
    std::shared_ptr<int> sp1(raw);
    std::shared_ptr<int> sp2(raw);  // NGUY HIỂM: control blocks riêng!
 
    // Khi sp1 bị hủy → delete raw
    // Khi sp2 bị hủy → delete raw LẦN NỮA → crash!
}
 
// SỬA: luôn dùng make_shared, hoặc tạo từ shared_ptr có sẵn
void safe() {
    auto sp1 = std::make_shared<int>(42);
    auto sp2 = sp1;  // cùng control block, ref count = 2
}

Pitfall 3: Circular References Với shared_ptr

#include <memory>
 
struct Parent;
struct Child;
 
struct Parent {
    std::shared_ptr<Child> child;
    ~Parent() { std::cout << "Parent destroyed\n"; }
};
 
struct Child {
    std::shared_ptr<Parent> parent;  // BUG: circular!
    ~Child() { std::cout << "Child destroyed\n"; }
};
 
// SỬA: dùng weak_ptr cho back-reference
struct ChildFixed {
    std::weak_ptr<Parent> parent;    // non-owning
    ~ChildFixed() { std::cout << "ChildFixed destroyed\n"; }
};

Pitfall 4: Dùng shared_ptr Từ this

#include <memory>
#include <vector>
#include <iostream>
 
// BUG: tạo shared_ptr từ raw 'this'
class BadWidget {
public:
    void addToList(std::vector<std::shared_ptr<BadWidget>>& list) {
        // NGUY HIỂM: tạo control block mới từ raw pointer!
        list.push_back(std::shared_ptr<BadWidget>(this));
    }
};
 
// SỬA: kế thừa từ enable_shared_from_this
class GoodWidget : public std::enable_shared_from_this<GoodWidget> {
public:
    void addToList(std::vector<std::shared_ptr<GoodWidget>>& list) {
        // An toàn: dùng control block có sẵn
        list.push_back(shared_from_this());
    }
};
 
// Sử dụng — PHẢI được quản lý bởi shared_ptr trước
auto widget = std::make_shared<GoodWidget>();
std::vector<std::shared_ptr<GoodWidget>> list;
widget->addToList(list);  // an toàn!

Pitfall 5: Slicing Với Smart Pointers

#include <memory>
#include <iostream>
 
class Base {
public:
    virtual ~Base() = default;
    virtual void speak() { std::cout << "Base\n"; }
};
 
class Derived : public Base {
public:
    void speak() override { std::cout << "Derived\n"; }
    int extra_data = 42;
};
 
// OK: smart pointers đến base class hoạt động đúng
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
ptr->speak();  // in "Derived" — polymorphism hoạt động!
 
// BUG: value-type slicing (không phải lỗi smart pointer, nhưng hay nhầm)
Base base = Derived();  // SLICED! extra_data mất, speak() gọi Base::speak

9. Advanced Memory Patterns

Pimpl Idiom (Pointer to Implementation)

Ẩn implementation details sau một unique_ptr:

// widget.h — public header
#pragma once
#include <memory>
#include <string>
 
class Widget {
public:
    Widget(std::string name);
    ~Widget();  // phải khai báo trong header, define trong .cpp
 
    // Move operations
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
 
    // Không copy
    Widget(const Widget&) = delete;
    Widget& operator=(const Widget&) = delete;
 
    void doSomething();
    std::string name() const;
 
private:
    struct Impl;                  // forward declaration
    std::unique_ptr<Impl> pimpl_; // pointer đến implementation
};
// widget.cpp — implementation (có thể thay đổi mà không recompile users)
#include "widget.h"
#include <iostream>
#include <vector>
 
struct Widget::Impl {
    std::string name;
    std::vector<int> data;
    // ... bất kỳ private members nào, kể cả heavy headers
};
 
Widget::Widget(std::string name)
    : pimpl_(std::make_unique<Impl>()) {
    pimpl_->name = std::move(name);
}
 
Widget::~Widget() = default;  // phải ở .cpp nơi Impl là complete type
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
 
void Widget::doSomething() {
    pimpl_->data.push_back(42);
    std::cout << pimpl_->name << " did something\n";
}
 
std::string Widget::name() const {
    return pimpl_->name;
}

Lợi ích: Compilation nhanh hơn (thay đổi Impl không recompile users), ABI stability, public headers sạch.

Object Pool

Tái sử dụng objects thay vì allocate/deallocate liên tục:

#include <memory>
#include <vector>
#include <iostream>
#include <functional>
 
template<typename T>
class ObjectPool {
public:
    using Deleter = std::function<void(T*)>;
    using Ptr = std::unique_ptr<T, Deleter>;
 
    explicit ObjectPool(size_t initial_size = 10) {
        for (size_t i = 0; i < initial_size; ++i) {
            pool_.push_back(std::make_unique<T>());
        }
    }
 
    Ptr acquire() {
        if (pool_.empty()) {
            // Pool cạn — tạo object mới
            return Ptr(new T(), [this](T* obj) {
                pool_.push_back(std::unique_ptr<T>(obj));
            });
        }
 
        // Lấy từ pool — return với custom deleter để recycle
        auto obj = pool_.back().release();
        pool_.pop_back();
 
        return Ptr(obj, [this](T* obj) {
            pool_.push_back(std::unique_ptr<T>(obj));
        });
    }
 
    size_t available() const { return pool_.size(); }
 
private:
    std::vector<std::unique_ptr<T>> pool_;
};
 
// Sử dụng
struct Connection {
    void reset() { /* reset state để tái sử dụng */ }
};
 
int main() {
    ObjectPool<Connection> pool(5);
    std::cout << "Available: " << pool.available() << "\n"; // 5
 
    {
        auto conn1 = pool.acquire();
        auto conn2 = pool.acquire();
        std::cout << "Available: " << pool.available() << "\n"; // 3
    }   // conn1, conn2 trả về pool
 
    std::cout << "Available: " << pool.available() << "\n"; // 5
}

std::pmr Polymorphic Allocators (C++17)

Cho performance-critical code, bạn có thể kiểm soát nơi memory được cấp phát:

#include <memory_resource>
#include <vector>
#include <iostream>
 
int main() {
    // Buffer trên stack — hoàn toàn không heap allocation
    char buffer[4096];
    std::pmr::monotonic_buffer_resource pool(buffer, sizeof(buffer));
 
    // Vector allocate từ buffer của chúng ta
    std::pmr::vector<int> vec(&pool);
    for (int i = 0; i < 100; ++i) {
        vec.push_back(i);
    }
 
    std::cout << "Size: " << vec.size() << "\n";
    // Tất cả allocations đến từ 'buffer' — zero heap allocations!
}

aligned_alloc và Alignment

Cho SIMD hoặc yêu cầu hardware cụ thể:

#include <memory>
#include <cstdlib>
#include <iostream>
 
// Custom deleter cho aligned memory
struct AlignedDeleter {
    void operator()(void* ptr) const {
        std::free(ptr);
    }
};
 
// Allocate aligned memory với smart pointer
auto makeAligned(size_t alignment, size_t size) {
    void* ptr = std::aligned_alloc(alignment, size);
    return std::unique_ptr<void, AlignedDeleter>(ptr);
}
 
int main() {
    // 64-byte aligned buffer (cache line aligned)
    auto buffer = makeAligned(64, 1024);
    std::cout << "Aligned: "
              << (reinterpret_cast<uintptr_t>(buffer.get()) % 64 == 0)
              << "\n"; // 1 (true)
}
 
// C++17: alignas trong classes
struct alignas(64) CacheAligned {
    int data[16];
};
 
auto ptr = std::make_unique<CacheAligned>();
// Lưu ý: make_unique tôn trọng alignas từ C++17

10. Công Cụ Debug Memory

Dù cẩn thận đến đâu, memory bugs vẫn xảy ra. Các công cụ này bắt chúng:

AddressSanitizer (ASan)

Công cụ quan trọng nhất để bắt memory bugs lúc runtime:

# Compile với ASan
g++ -fsanitize=address -g -O1 main.cpp -o main
clang++ -fsanitize=address -g -O1 main.cpp -o main
 
# Chạy — ASan báo lỗi với stack traces
./main

ASan phát hiện:

  • Use after free
  • Heap buffer overflow
  • Stack buffer overflow
  • Memory leaks
  • Double free
// Ví dụ: ASan bắt use-after-free
#include <memory>
 
int main() {
    auto ptr = std::make_unique<int>(42);
    int* raw = ptr.get();
    ptr.reset();       // giải phóng memory
    return *raw;       // ASan: "use-after-free"
}

Valgrind

# Chạy dưới Valgrind (chậm hơn nhưng kỹ hơn)
valgrind --leak-check=full --show-leak-kinds=all ./main

LeakSanitizer (LSan)

# Thường đi kèm ASan, hoặc standalone:
g++ -fsanitize=leak -g main.cpp -o main
./main

Compiler Warnings

# Bật hết warnings
g++ -Wall -Wextra -Wpedantic -Werror -std=c++20 main.cpp
clang++ -Wall -Wextra -Wpedantic -Werror -std=c++20 main.cpp

11. Tóm Tắt Best Practices

Các Quy Tắc Memory Trong Modern C++

  1. Đừng bao giờ viết new/delete trực tiếp — dùng make_unique hoặc make_shared
  2. unique_ptr là mặc định — chỉ dùng shared_ptr khi thực sự cần
  3. Functions không sở hữu nên nhận T& hoặc T* — không phải smart pointers
  4. Dùng weak_ptr để phá circular references trong shared ownership graphs
  5. Mọi resource đều RAII — files, locks, connections, transactions, tất cả
  6. Ưu tiên stack allocation — heap chỉ khi cần dynamic lifetime hoặc size
  7. Dùng ASan trong development — compile với -fsanitize=address và chạy tests

Code Review Checklist

Khi review C++ code về memory safety:

✅ Không raw new/delete ngoài RAII wrappers
✅ Không raw pointer ownership (raw pointers chỉ là observers)
✅ shared_ptr cycles được phá bằng weak_ptr
✅ enable_shared_from_this dùng khi objects cần shared_from_this()
✅ Factory functions return unique_ptr (không phải raw pointers)
✅ Không dùng get() pointer ngoài lifetime của smart pointer
✅ Virtual destructors trên base classes
✅ Move semantics implemented cho resource-holding classes

Migration Path: Legacy Code → Modern C++

Nếu bạn đang làm việc với legacy code dùng raw new/delete:

// Bước 1: Wrap raw pointers có sẵn trong unique_ptr khi tạo
// TRƯỚC:
Widget* w = new Widget();
// ... 200 dòng sau ...
delete w;
 
// SAU:
auto w = std::make_unique<Widget>();
// ... 200 dòng sau ...
// (tự động cleanup)
 
// Bước 2: Thay raw pointer parameters bằng references
// TRƯỚC:
void process(Widget* w) { w->doThing(); }
// SAU:
void process(Widget& w) { w.doThing(); }
 
// Bước 3: Thay ownership transfers bằng unique_ptr
// TRƯỚC:
Widget* create() { return new Widget(); }  // caller phải delete
// SAU:
std::unique_ptr<Widget> create() { return std::make_unique<Widget>(); }

Bài Tập Thực Hành

Bài 1: RAII Socket Wrapper

Viết RAII wrapper cho network socket:

class Socket {
public:
    explicit Socket(int port);  // mở socket
    ~Socket();                  // đóng socket
 
    Socket(Socket&&) noexcept;
    Socket& operator=(Socket&&) noexcept;
 
    // Không copy
    Socket(const Socket&) = delete;
    Socket& operator=(const Socket&) = delete;
 
    void send(const std::string& data);
    std::string receive();
 
private:
    int fd_ = -1;  // file descriptor
};

Bài 2: Tree Với Smart Pointers

Xây dựng tree trong đó:

  • Parent sở hữu children (unique_ptr)
  • Children reference parent (weak_ptr hoặc raw pointer)
struct TreeNode {
    int value;
    std::vector<std::unique_ptr<TreeNode>> children;
    TreeNode* parent = nullptr;  // non-owning back reference
 
    void addChild(int val) {
        auto child = std::make_unique<TreeNode>();
        child->value = val;
        child->parent = this;
        children.push_back(std::move(child));
    }
};

Bài 3: Type-Erased Callback

Dùng shared_ptr và weak_ptr để build callback system tự unregister khi listener bị hủy:

class Button {
public:
    using Callback = std::function<void()>;
 
    void onClick(std::weak_ptr<Callback> cb) {
        callbacks_.push_back(cb);
    }
 
    void click() {
        // Xóa expired, gọi active
        // ... implement cái này
    }
 
private:
    std::vector<std::weak_ptr<Callback>> callbacks_;
};

Tóm Tắt

✅ RAII là nền tảng — mọi resource đều có destructor
✅ unique_ptr cho exclusive ownership — zero overhead, lựa chọn mặc định
✅ shared_ptr cho shared ownership — dùng tiết kiệm, luôn make_shared
✅ weak_ptr để observe mà không sở hữu — phá cycles, caches, observers
✅ Ownership semantics truyền đạt ý định — signal quan trọng nhất của API
✅ Đừng bao giờ viết raw new/delete — compiler và smart pointers lo
✅ Dùng ASan và Valgrind — trust but verify

Tiếp Theo Là Gì?

Tài Nguyên Bổ Sung


Series: Lộ Trình Học Modern C++
Trước: Phase 3: Tính Năng Modern C++
Tiếp theo: Deep Dive: STL Containers & Algorithms

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.