raw, yếu_ptr, unique_ptr, shared_ptr v.v ... Làm thế nào để chọn chúng một cách khôn ngoan?


33

Có rất nhiều con trỏ trong C ++ nhưng thành thật mà nói trong 5 năm hoặc lâu hơn trong lập trình C ++ (cụ thể là với Khung Qt) tôi chỉ sử dụng con trỏ thô cũ:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Tôi biết có rất nhiều gợi ý "thông minh" khác:

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Nhưng tôi không có ý tưởng nhỏ nhất về việc phải làm gì với họ và những gì họ có thể cung cấp cho tôi khi so sánh với con trỏ thô.

Ví dụ tôi có tiêu đề lớp này:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Điều này rõ ràng là không đầy đủ nhưng đối với mỗi 3 con trỏ này thì có thể để chúng ở dạng "thô" hay tôi nên sử dụng một cái gì đó phù hợp hơn?

Và trong lần thứ hai, nếu một nhà tuyển dụng sẽ đọc mã, anh ta sẽ nghiêm khắc về loại con trỏ tôi sử dụng hay không?


Chủ đề này có vẻ rất thích hợp cho SO. này là vào năm 2008 . Và đây là loại con trỏ nào tôi sử dụng khi nào? . Tôi chắc chắn bạn có thể tìm thấy các trận đấu thậm chí tốt hơn. Đây chỉ là lần đầu tiên tôi thấy
sehe

imo đường biên giới này vì nó nhiều về ý nghĩa / ý định khái niệm của các lớp này cũng như về các chi tiết kỹ thuật về hành vi và việc thực hiện của chúng. Vì câu trả lời được chấp nhận nghiêng về câu hỏi trước, tôi rất vui khi đây là "phiên bản PSE" của câu hỏi SO đó.
Ixrec

Câu trả lời:


70

Một con trỏ "thô" không được quản lý. Đó là, dòng sau:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... sẽ rò rỉ bộ nhớ nếu việc đi kèm deletekhông được thực thi vào thời điểm thích hợp.

auto_ptr

Để giảm thiểu những trường hợp này, std::auto_ptr<>đã được giới thiệu. Tuy nhiên, do những hạn chế của C ++ trước tiêu chuẩn 2011, tuy nhiên, vẫn rất dễ auto_ptrbị rò rỉ bộ nhớ. Nó là đủ cho các trường hợp hạn chế, chẳng hạn như điều này, tuy nhiên:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Một trong những trường hợp sử dụng yếu nhất của nó là trong các thùng chứa. Điều này là do nếu một bản sao của một auto_ptr<>được tạo ra và bản sao cũ không được thiết lập lại cẩn thận, thì container có thể xóa con trỏ và mất dữ liệu.

unique_ptr

Để thay thế, C ++ 11 đã giới thiệu std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Như vậy unique_ptr<>sẽ được làm sạch chính xác, ngay cả khi nó được chuyển giữa các chức năng. Nó thực hiện điều này bằng cách đại diện cho "quyền sở hữu" của con trỏ - "chủ sở hữu" làm sạch nó. Điều này làm cho nó lý tưởng để sử dụng trong các thùng chứa:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

Không giống như auto_ptr<>, unique_ptr<>được xử lý tốt ở đây và khi vectorthay đổi kích thước, không có đối tượng nào sẽ vô tình bị xóa trong khi các vectorbản sao lưu trữ sao lưu của nó.

shared_ptrweak_ptr

unique_ptr<>chắc chắn là hữu ích, nhưng có những trường hợp bạn muốn hai phần cơ sở mã của mình có thể tham chiếu đến cùng một đối tượng và sao chép con trỏ xung quanh, trong khi vẫn được đảm bảo dọn dẹp đúng cách. Ví dụ, một cây có thể trông như thế này, khi sử dụng std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Trong trường hợp này, chúng ta thậm chí có thể giữ nhiều bản sao của một nút gốc và cây sẽ được dọn sạch đúng cách khi tất cả các bản sao của nút gốc bị phá hủy.

Điều này hoạt động bởi vì mỗi cái shared_ptr<>giữ không chỉ con trỏ đến đối tượng, mà còn đếm số tham chiếu của tất cả các shared_ptr<>đối tượng tham chiếu đến cùng một con trỏ. Khi một cái mới được tạo ra, số lượng tăng lên. Khi một người bị phá hủy, số lượng đi xuống. Khi số đếm bằng 0, con trỏ là deleted.

Vì vậy, điều này đưa ra một vấn đề: Các cấu trúc liên kết đôi kết thúc với các tham chiếu tròn. Nói rằng chúng tôi muốn thêm một parentcon trỏ vào cây của chúng tôi Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Bây giờ, nếu chúng ta loại bỏ một Node, có một tham chiếu theo chu kỳ cho nó. Nó sẽ không bao giờ là deleted vì số tham chiếu của nó sẽ không bao giờ bằng không.

Để giải quyết vấn đề này, bạn sử dụng std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Bây giờ, mọi thứ sẽ hoạt động chính xác và việc xóa một nút sẽ không để lại các tham chiếu bị kẹt cho nút cha. Nó làm cho việc đi trên cây phức tạp hơn một chút, tuy nhiên:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

Bằng cách này, bạn có thể khóa một tham chiếu đến nút và bạn có một đảm bảo hợp lý rằng nó sẽ không biến mất trong khi bạn đang làm việc với nó, vì bạn đang giữ một shared_ptr<>nút.

make_sharedmake_unique

Bây giờ, có một số vấn đề nhỏ với shared_ptr<>unique_ptr<>cần được giải quyết. Hai dòng sau có một vấn đề:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Nếu thrower()ném một ngoại lệ, cả hai dòng sẽ rò rỉ bộ nhớ. Và hơn thế nữa, shared_ptr<>giữ số tham chiếu cách xa đối tượng mà nó trỏ đến và điều này có thể có nghĩa là phân bổ thứ hai). Điều đó thường không được mong muốn.

C ++ 11 cung cấp std::make_shared<>()và C ++ 14 cung cấp std::make_unique<>()để giải quyết vấn đề này:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Bây giờ, trong cả hai trường hợp, ngay cả khi thrower()ném ngoại lệ, sẽ không có rò rỉ bộ nhớ. Là một phần thưởng, make_shared<>()có cơ hội tạo số tham chiếu của nó trong cùng một không gian bộ nhớ với đối tượng được quản lý của nó, vừa có thể nhanh hơn vừa có thể tiết kiệm một vài byte bộ nhớ, đồng thời mang lại cho bạn sự đảm bảo an toàn ngoại lệ!

Ghi chú về Qt

Tuy nhiên, cần lưu ý rằng Qt, phải hỗ trợ các trình biên dịch trước C ++ 11, có mô hình thu gom rác riêng: Nhiều người QObjectcó một cơ chế trong đó chúng sẽ bị phá hủy đúng cách mà không cần người dùng sử dụng deletechúng.

Tôi không biết QObjects sẽ hành xử như thế nào khi được quản lý bởi các con trỏ được quản lý C ++ 11, vì vậy tôi không thể nói đó shared_ptr<QDialog>là một ý tưởng hay. Tôi không có đủ kinh nghiệm với Qt để nói chắc chắn, nhưng tôi tin rằng Qt5 đã được điều chỉnh cho trường hợp sử dụng này.


1
@Zilators: Xin lưu ý nhận xét thêm của tôi về Qt. Câu trả lời cho câu hỏi của bạn về việc có nên quản lý cả ba con trỏ hay không phụ thuộc vào việc các đối tượng Qt có cư xử tốt hay không.
greyfade

2
"Cả hai thực hiện phân bổ riêng biệt để giữ con trỏ"? Không, unique_ptr không bao giờ phân bổ thêm bất cứ thứ gì, chỉ chia sẻ_ptr phải phân bổ một đối tượng đếm-tham chiếu + phân bổ-đối tượng. "cả hai dòng sẽ rò rỉ bộ nhớ"? không, chỉ có thể, thậm chí không đảm bảo cho hành vi xấu.
Ded

1
@Ded repeatator: Từ ngữ của tôi phải không rõ ràng: Đây shared_ptrlà một đối tượng riêng biệt - phân bổ riêng biệt - từ newđối tượng ed. Chúng tồn tại ở các địa điểm khác nhau. make_sharedcó khả năng kết hợp chúng ở cùng một vị trí, giúp cải thiện cục bộ bộ đệm, trong số những thứ khác.
greyfade

2
@greyfade: Nononono. shared_ptrlà một đối tượng. Và để quản lý một đối tượng, nó phải phân bổ một tham chiếu (đếm tham chiếu (yếu + mạnh) + hủy diệt). make_sharedcho phép phân bổ đó và đối tượng được quản lý như một mảnh. unique_ptrkhông sử dụng chúng, do đó không có lợi thế tương ứng, ngoài việc đảm bảo đối tượng luôn được sở hữu bởi con trỏ thông minh. Là một sang một bên, người ta có thể có một shared_ptrsở hữu một đối tượng cơ bản và đại diện cho một nullptrhoặc mà không sở hữu và đại diện cho một tổ chức phi nullpointer.
Ded

1
Tôi đã xem xét nó và dường như có một sự nhầm lẫn chung về những gì a shared_ptrlàm: 1. Nó chia sẻ quyền sở hữu của một số đối tượng (được đại diện bởi một đối tượng được phân bổ động bên trong có số tham chiếu yếu và mạnh, cũng như một deleter) . 2. Nó chứa một con trỏ. Hai phần đó là độc lập. make_uniquemake_sharedcả hai đều đảm bảo đối tượng được phân bổ được đặt an toàn vào một con trỏ thông minh. Ngoài ra, make_sharedcho phép phân bổ đối tượng sở hữu và con trỏ được quản lý cùng nhau.
Ded
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.