Tại sao tôi nên sử dụng một con trỏ chứ không phải chính đối tượng?


1602

Tôi đến từ nền Java và đã bắt đầu làm việc với các đối tượng trong C ++. Nhưng một điều xảy ra với tôi là mọi người thường sử dụng các con trỏ tới các đối tượng hơn là chính các đối tượng, ví dụ như tuyên bố này:

Object *myObject = new Object;

thay vì:

Object myObject;

Hoặc thay vì sử dụng một chức năng, hãy nói testFunc(), như thế này:

myObject.testFunc();

chúng ta phải viết:

myObject->testFunc();

Nhưng tôi không thể hiểu tại sao chúng ta nên làm theo cách này. Tôi cho rằng nó phải liên quan đến hiệu quả và tốc độ vì chúng ta có quyền truy cập trực tiếp vào địa chỉ bộ nhớ. Tôi có đúng không


405
Kudos cho bạn để đặt câu hỏi về thực hành này thay vì chỉ theo dõi nó. Hầu hết thời gian, con trỏ được sử dụng quá mức.
Luchian Grigore

120
Nếu bạn không thấy lý do để sử dụng con trỏ, thì đừng. Thích đồ vật. Thích các đối tượng trước unique_ptr trước shared_ptr trước các con trỏ thô.
stefan

113
lưu ý: trong java, mọi thứ (trừ các loại cơ bản) là một con trỏ. Vì vậy, bạn nên hỏi ngược lại: tại sao tôi cần các đối tượng đơn giản?
Karoly Horvath

119
Lưu ý rằng, trong Java, các con trỏ được ẩn theo cú pháp. Trong C ++, sự khác biệt giữa một con trỏ và một con trỏ không được thể hiện rõ ràng trong mã. Java sử dụng con trỏ ở mọi nơi.
Daniel Martín

216
Đóng như quá rộng ? Nghiêm túc? Xin mọi người, lưu ý rằng cách lập trình Java ++ này rất phổ biến và là một trong những vấn đề quan trọng nhất trên cộng đồng C ++ . Nó nên được điều trị nghiêm túc.
Manu343726 ngày

Câu trả lời:


1574

Thật không may khi bạn thấy phân bổ động thường xuyên như vậy. Điều đó chỉ cho thấy có bao nhiêu lập trình viên C ++ xấu.

Theo một nghĩa nào đó, bạn có hai câu hỏi được gói lại thành một. Đầu tiên là khi nào chúng ta nên sử dụng phân bổ động (sử dụng new)? Thứ hai là khi nào chúng ta nên sử dụng con trỏ?

Thông điệp mang về nhà quan trọng là bạn nên luôn luôn sử dụng công cụ thích hợp cho công việc . Trong hầu hết các tình huống, có một cái gì đó phù hợp và an toàn hơn là thực hiện phân bổ động thủ công và / hoặc sử dụng các con trỏ thô.

Phân bổ động

Trong câu hỏi của bạn, bạn đã trình bày hai cách tạo đối tượng. Sự khác biệt chính là thời gian lưu trữ của đối tượng. Khi thực hiện Object myObject;trong một khối, đối tượng được tạo với thời lượng lưu trữ tự động, có nghĩa là nó sẽ bị hủy tự động khi đi ra khỏi phạm vi. Khi bạn làm như vậy new Object(), đối tượng có thời lượng lưu trữ động, có nghĩa là nó vẫn tồn tại cho đến khi bạn rõ ràng delete. Bạn chỉ nên sử dụng thời lượng lưu trữ động khi bạn cần. Đó là, bạn nên luôn luôn thích tạo các đối tượng với thời gian lưu trữ tự động khi bạn có thể .

Hai tình huống chính mà bạn có thể yêu cầu phân bổ động:

  1. Bạn cần đối tượng tồn tại lâu hơn phạm vi hiện tại - đối tượng cụ thể đó tại vị trí bộ nhớ cụ thể đó, không phải là bản sao của nó. Nếu bạn ổn với việc sao chép / di chuyển đối tượng (hầu hết thời gian bạn nên làm), bạn nên chọn một đối tượng tự động.
  2. Bạn cần phân bổ rất nhiều bộ nhớ , có thể dễ dàng lấp đầy ngăn xếp. Sẽ thật tuyệt nếu chúng ta không phải lo lắng về điều này (hầu hết thời gian bạn không cần phải làm), vì nó thực sự nằm ngoài phạm vi của C ++, nhưng thật không may, chúng ta phải đối phó với thực tế của các hệ thống chúng tôi đang phát triển cho.

Khi bạn hoàn toàn yêu cầu phân bổ động, bạn nên gói nó trong một con trỏ thông minh hoặc một số loại khác thực hiện RAII (như các thùng chứa tiêu chuẩn). Con trỏ thông minh cung cấp ngữ nghĩa sở hữu của các đối tượng được phân bổ động. Hãy xem std::unique_ptrstd::shared_ptr, ví dụ. Nếu bạn sử dụng chúng một cách thích hợp, bạn gần như hoàn toàn có thể tránh thực hiện quản lý bộ nhớ của riêng mình (xem Quy tắc về Không ).

Con trỏ

Tuy nhiên, có những cách sử dụng chung khác cho con trỏ thô ngoài phân bổ động, nhưng hầu hết có các lựa chọn thay thế mà bạn nên thích. Như trước đây, luôn luôn thích các lựa chọn thay thế trừ khi bạn thực sự cần con trỏ .

  1. Bạn cần tham khảo ngữ nghĩa . Đôi khi bạn muốn truyền một đối tượng bằng một con trỏ (bất kể nó được phân bổ như thế nào) bởi vì bạn muốn chức năng mà bạn chuyển qua nó có quyền truy cập vào đối tượng cụ thể đó (không phải là bản sao của đối tượng đó). Tuy nhiên, trong hầu hết các tình huống, bạn nên ưu tiên các loại tham chiếu cho con trỏ, bởi vì đây cụ thể là những gì chúng được thiết kế cho. Lưu ý điều này không nhất thiết là kéo dài thời gian tồn tại của đối tượng vượt quá phạm vi hiện tại, như trong tình huống 1 ở trên. Như trước đây, nếu bạn ổn khi chuyển một bản sao của đối tượng, bạn không cần ngữ nghĩa tham khảo.

  2. Bạn cần đa hình . Bạn chỉ có thể gọi các hàm đa hình (nghĩa là theo kiểu động của một đối tượng) thông qua một con trỏ hoặc tham chiếu đến đối tượng. Nếu đó là hành vi bạn cần, thì bạn cần sử dụng con trỏ hoặc tham chiếu. Một lần nữa, tài liệu tham khảo nên được ưu tiên.

  3. Bạn muốn thể hiện rằng một đối tượng là tùy chọn bằng cách cho phép nullptrvượt qua khi đối tượng bị bỏ qua. Nếu đó là một đối số, bạn nên sử dụng các đối số mặc định hoặc quá tải hàm. Mặt khác, tốt nhất bạn nên sử dụng một loại đóng gói hành vi này, chẳng hạn như std::optional(được giới thiệu trong C ++ 17 - với các tiêu chuẩn C ++ trước đó, sử dụng boost::optional).

  4. Bạn muốn tách các đơn vị biên dịch để cải thiện thời gian biên dịch . Thuộc tính hữu ích của một con trỏ là bạn chỉ yêu cầu khai báo chuyển tiếp của kiểu trỏ (để thực sự sử dụng đối tượng, bạn sẽ cần một định nghĩa). Điều này cho phép bạn tách rời các phần của quá trình biên dịch, điều này có thể cải thiện đáng kể thời gian biên dịch. Xem thành ngữ Pimpl .

  5. Bạn cần giao tiếp với thư viện C hoặc thư viện kiểu C. Tại thời điểm này, bạn buộc phải sử dụng con trỏ thô. Điều tốt nhất bạn có thể làm là đảm bảo rằng bạn chỉ để con trỏ thô của bạn lỏng lẻo vào thời điểm cuối cùng có thể. Bạn có thể lấy một con trỏ thô từ một con trỏ thông minh, ví dụ, bằng cách sử dụng gethàm thành viên của nó . Nếu một thư viện thực hiện một số phân bổ cho bạn mà nó hy vọng bạn sẽ phân bổ thông qua một tay cầm, bạn có thể thường bọc tay cầm trong một con trỏ thông minh với một deleter tùy chỉnh sẽ phân bổ đối tượng một cách thích hợp.


83
"Bạn cần đối tượng để tồn tại lâu hơn phạm vi hiện tại." - Một lưu ý bổ sung về điều này: có những trường hợp có vẻ như bạn cần đối tượng tồn tại lâu hơn phạm vi hiện tại, nhưng bạn thực sự không. Ví dụ, nếu bạn đặt đối tượng của mình vào một vectơ, đối tượng sẽ được sao chép (hoặc di chuyển) vào vectơ và đối tượng ban đầu an toàn để hủy khi phạm vi của nó kết thúc.

25
Hãy nhớ rằng s / copy / move / ở nhiều nơi bây giờ. Trả lại một đối tượng chắc chắn không ngụ ý một động thái. Bạn cũng nên lưu ý rằng việc truy cập một đối tượng thông qua một con trỏ là trực giao với cách nó được tạo.
Cún con

15
Tôi bỏ lỡ một tài liệu tham khảo rõ ràng cho RAII về câu trả lời này. C ++ là tất cả (hầu hết tất cả) về quản lý tài nguyên và RAII là cách thực hiện trên C ++ (Và vấn đề chính mà các con trỏ thô tạo ra: Phá vỡ RAII)
Manu343726

11
Con trỏ thông minh tồn tại trước C ++ 11, ví dụ: boost :: shared_ptr và boost :: scoped_ptr. Các dự án khác có tương đương riêng của họ. Bạn không thể có được ngữ nghĩa di chuyển và bài tập của std :: auto_ptr là thiếu sót, vì vậy C ++ 11 cải thiện mọi thứ, nhưng lời khuyên vẫn tốt. (Và một nitpick buồn, đó là chưa đủ để có quyền truy cập vào một C ++ 11 trình biên dịch, nó là cần thiết rằng tất cả các trình biên dịch, bạn có thể có thể muốn mã của bạn để làm việc với sự hỗ trợ C ++ 11. Vâng, Oracle Solaris Studio, Tôi nhìn vào bạn.)
armb

7
@ MDMoore313 Bạn có thể viếtObject myObject(param1, etc...)
user000001

173

Có nhiều trường hợp sử dụng cho con trỏ.

Hành vi đa hình . Đối với các loại đa hình, con trỏ (hoặc tham chiếu) được sử dụng để tránh cắt:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Ngữ nghĩa tham khảo và tránh sao chép . Đối với các loại không đa hình, một con trỏ (hoặc tham chiếu) sẽ tránh sao chép một đối tượng đắt tiền

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Lưu ý rằng C ++ 11 đã di chuyển ngữ nghĩa có thể tránh nhiều bản sao của các đối tượng đắt tiền thành đối số hàm và làm giá trị trả về. Nhưng sử dụng một con trỏ chắc chắn sẽ tránh được những thứ đó và sẽ cho phép nhiều con trỏ trên cùng một đối tượng (trong khi một đối tượng chỉ có thể được di chuyển từ một lần).

Mua lại tài nguyên . Tạo một con trỏ tới tài nguyên bằng newtoán tử là một mô hình chống trong C ++ hiện đại. Sử dụng một lớp tài nguyên đặc biệt (một trong các thùng chứa Tiêu chuẩn) hoặc một con trỏ thông minh ( std::unique_ptr<>hoặc std::shared_ptr<>). Xem xét:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

so với

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Một con trỏ thô chỉ nên được sử dụng như một "khung nhìn" và không phải là bất kỳ cách nào liên quan đến quyền sở hữu, thông qua việc tạo trực tiếp hoặc ngầm thông qua các giá trị trả về. Xem thêm Q & A này từ C ++ FAQ .

Kiểm soát thời gian sống chi tiết hơn Mỗi khi một con trỏ dùng chung được sao chép (ví dụ như là một đối số chức năng), tài nguyên mà nó trỏ đến đang được giữ nguyên. Các đối tượng thông thường (không được tạo bởi new, trực tiếp bởi bạn hoặc bên trong một lớp tài nguyên) sẽ bị hủy khi đi ra khỏi phạm vi.


17
"Tạo một con trỏ tới tài nguyên bằng toán tử mới là một kiểu chống mẫu" Tôi nghĩ rằng bạn thậm chí có thể nâng cao điều đó để có một con trỏ thô sở hữu thứ gì đó là một mô hình chống . Không chỉ việc tạo, mà việc chuyển các con trỏ thô thành đối số hoặc trả về giá trị ngụ ý chuyển quyền sở hữu IMHO không được chấp nhận kể từ khi unique_ptr/ di chuyển ngữ nghĩa
dyp

1
@dyp tnx, được cập nhật và tham khảo Câu hỏi thường gặp về C ++ về chủ đề này.
TemplateRex

4
Sử dụng con trỏ thông minh ở khắp mọi nơi là một mô hình chống. Có một vài trường hợp đặc biệt được áp dụng, nhưng hầu hết thời gian, cùng một lý do tranh luận về phân bổ động (suốt đời tùy ý) cũng chống lại bất kỳ con trỏ thông minh thông thường nào.
James Kanze

2
@JamesKanze Tôi không có ý ám chỉ rằng con trỏ thông minh nên được sử dụng ở mọi nơi, chỉ để sở hữu và cũng không nên sử dụng con trỏ thô để làm chủ, mà chỉ cho các lượt xem.
TemplateRex

2
@TemplateRex Điều đó có vẻ hơi ngớ ngẩn được đưa ra hun(b)cũng đòi hỏi kiến ​​thức về chữ ký trừ khi bạn không biết rằng bạn đã cung cấp sai loại cho đến khi biên dịch. Mặc dù vấn đề tham chiếu thường sẽ không bị bắt vào thời gian biên dịch và sẽ mất nhiều nỗ lực hơn để gỡ lỗi, nếu bạn đang kiểm tra chữ ký để đảm bảo các đối số là đúng, bạn cũng có thể xem liệu có bất kỳ đối số nào là tham chiếu không vì vậy bit tham chiếu trở thành một vấn đề không phải là vấn đề (đặc biệt là khi sử dụng IDE hoặc trình soạn thảo văn bản hiển thị chữ ký của các chức năng được chọn). Ngoài ra, const&.
JAB

130

Có nhiều câu trả lời tuyệt vời cho câu hỏi này, bao gồm các trường hợp sử dụng quan trọng của khai báo chuyển tiếp, đa hình, v.v. nhưng tôi cảm thấy một phần "linh hồn" của câu hỏi của bạn không được trả lời - cụ thể là các cú pháp khác nhau có nghĩa gì trên Java và C ++.

Hãy xem xét tình huống so sánh hai ngôn ngữ:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

Tương đương gần nhất với điều này, là:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Hãy xem cách thay thế C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

Cách tốt nhất để nghĩ về nó là - ít nhiều - Java (ngầm) xử lý các con trỏ tới các đối tượng, trong khi C ++ có thể xử lý các con trỏ tới các đối tượng hoặc chính các đối tượng. Có các ngoại lệ cho điều này - ví dụ: nếu bạn khai báo các kiểu "nguyên thủy" của Java, thì chúng là các giá trị thực được sao chép và không phải là con trỏ. Vì thế,

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Điều đó nói rằng, sử dụng con trỏ KHÔNG nhất thiết là cách đúng hay sai để xử lý mọi việc; tuy nhiên các câu trả lời khác đã đáp ứng thỏa đáng. Mặc dù vậy, ý tưởng chung là trong C ++, bạn có nhiều quyền kiểm soát hơn đối với tuổi thọ của các đối tượng và nơi chúng sẽ sống.

Lấy điểm chính - Object * object = new Object()cấu trúc thực sự là thứ gần nhất với ngữ nghĩa điển hình của Java (hoặc C # cho vấn đề đó).


7
Object2 is now "dead": Tôi nghĩ bạn có nghĩa là myObject1hoặc chính xác hơn the object pointed to by myObject1.
Clément

2
Thật! Đọc lại một chút.
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();là mã rất xấu. Trình xây dựng đối tượng thứ hai mới hoặc thứ hai có thể ném và bây giờ đối tượng1 bị rò rỉ. Nếu bạn đang sử dụng news thô , bạn nên bọc newcác đối tượng ed trong trình bao bọc RAII càng sớm càng tốt.
PSkocik

8
Thật vậy, nó sẽ là nếu đây là một chương trình, và không có gì khác đang diễn ra xung quanh nó. Rất may, đây chỉ là một đoạn giải thích cho thấy cách một Con trỏ trong C ++ hoạt động - và một trong số ít nơi mà một đối tượng RAII không thể được thay thế cho một con trỏ thô, đang nghiên cứu và tìm hiểu về các con trỏ thô ...
Gerasimos R

80

Một lý do tốt để sử dụng con trỏ sẽ là cho khai báo chuyển tiếp . Trong một dự án đủ lớn, họ thực sự có thể tăng tốc thời gian biên dịch.


7
điều này thực sự bổ sung vào hỗn hợp thông tin hữu ích, rất vui vì bạn đã đưa ra câu trả lời!
TemplateRex

3
std :: shared_ptr <T> cũng hoạt động với các khai báo chuyển tiếp của T. (std :: unique_ptr <T> không )
berkus

13
@berkus: std::unique_ptr<T>không hoạt động với khai báo chuyển tiếp của T. Bạn chỉ cần đảm bảo rằng khi hàm hủy của std::unique_ptr<T>được gọi, Tlà một kiểu hoàn chỉnh. Điều này thường có nghĩa là lớp của bạn chứa std::unique_ptr<T>khai báo hàm hủy của nó trong tệp tiêu đề và thực hiện nó trong tệp cpp (ngay cả khi việc triển khai trống).
David Stone

Các mô-đun sẽ khắc phục điều này?
Trevor Hickey

@TrevorHickey Nhận xét cũ tôi biết, nhưng dù sao cũng phải trả lời. Các mô-đun sẽ không loại bỏ sự phụ thuộc, nhưng sẽ bao gồm cả sự phụ thuộc rất rẻ, gần như miễn phí về chi phí hiệu suất. Ngoài ra, nếu việc tăng tốc chung từ các mô-đun sẽ đủ để đưa thời gian biên dịch của bạn trong một phạm vi chấp nhận được thì đó cũng không còn là vấn đề nữa.
Aidiakapi

79

Lời nói đầu

Java không giống như C ++, trái với sự cường điệu. Máy thổi phồng Java muốn bạn tin rằng vì Java có cú pháp giống C ++, nên các ngôn ngữ tương tự nhau. Không có gì có thể được thêm từ sự thật. Thông tin sai lệch này là một phần lý do tại sao các lập trình viên Java tìm đến C ++ và sử dụng cú pháp giống như Java mà không hiểu ý nghĩa của mã của họ.

Trở đi chúng tôi đi

Nhưng tôi không thể hiểu tại sao chúng ta nên làm theo cách này. Tôi cho rằng nó phải liên quan đến hiệu quả và tốc độ vì chúng ta có quyền truy cập trực tiếp vào địa chỉ bộ nhớ. Tôi có đúng không

Ngược lại, thực sự. Heap chậm hơn nhiều so với stack, vì stack rất đơn giản so với heap. Biến lưu trữ tự động (còn gọi là biến ngăn xếp) có hàm hủy của chúng được gọi khi chúng đi ra khỏi phạm vi. Ví dụ:

{
    std::string s;
}
// s is destroyed here

Mặt khác, nếu bạn sử dụng một con trỏ được phân bổ động, hàm hủy của nó phải được gọi thủ công. deletegọi hàm hủy này cho bạn.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Điều này không liên quan gì đến newcú pháp phổ biến trong C # và Java. Chúng được sử dụng cho các mục đích hoàn toàn khác nhau.

Lợi ích của phân bổ động

1. Bạn không cần phải biết kích thước của mảng trước

Một trong những vấn đề đầu tiên mà nhiều lập trình viên C ++ gặp phải là khi họ chấp nhận đầu vào tùy ý từ người dùng, bạn chỉ có thể phân bổ kích thước cố định cho biến stack. Bạn cũng không thể thay đổi kích thước của mảng. Ví dụ:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Tất nhiên, nếu bạn đã sử dụng std::stringthay thế, std::stringnội bộ sẽ tự thay đổi kích thước để không gặp sự cố. Nhưng thực chất giải pháp cho vấn đề này là phân bổ động. Bạn có thể phân bổ bộ nhớ động dựa trên đầu vào của người dùng, ví dụ:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Lưu ý bên lề : Một sai lầm mà nhiều người mới bắt đầu mắc phải là việc sử dụng mảng có chiều dài thay đổi. Đây là một phần mở rộng GNU và cũng là một phần mở rộng trong Clang vì chúng phản ánh nhiều phần mở rộng của GCC. Vì vậy, những điều sau đây int arr[n]không nên dựa vào.

Bởi vì heap lớn hơn nhiều so với stack, người ta có thể tùy ý phân bổ / phân bổ lại nhiều bộ nhớ theo nhu cầu của mình, trong khi stack có giới hạn.

2. Mảng không phải là con trỏ

Làm thế nào đây là một lợi ích bạn yêu cầu? Câu trả lời sẽ trở nên rõ ràng khi bạn hiểu được sự nhầm lẫn / huyền thoại đằng sau mảng và con trỏ. Người ta thường cho rằng chúng giống nhau, nhưng chúng không giống nhau. Huyền thoại này xuất phát từ thực tế là các con trỏ có thể được đăng ký giống như các mảng và do các mảng phân rã thành các con trỏ ở cấp cao nhất trong một khai báo hàm. Tuy nhiên, một khi một mảng phân rã thành một con trỏ, con trỏ sẽ mất sizeofthông tin của nó . Vì vậy, sizeof(pointer)sẽ cho kích thước của con trỏ theo byte, thường là 8 byte trên hệ thống 64 bit.

Bạn không thể gán cho mảng, chỉ khởi tạo chúng. Ví dụ:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

Mặt khác, bạn có thể làm bất cứ điều gì bạn muốn với con trỏ. Thật không may, vì sự khác biệt giữa con trỏ và mảng được vẫy tay trong Java và C #, người mới bắt đầu không hiểu sự khác biệt.

3. Đa hình

Java và C # có các phương tiện cho phép bạn coi các đối tượng như một đối tượng khác, ví dụ như sử dụng astừ khóa. Vì vậy, nếu ai đó muốn coi một Entityđối tượng là một Playerđối tượng, người ta có thể làm Player player = Entity as Player;Điều này rất hữu ích nếu bạn có ý định gọi các hàm trên một thùng chứa đồng nhất chỉ nên áp dụng cho một loại cụ thể. Các chức năng có thể đạt được theo cách tương tự dưới đây:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Vì vậy, giả sử nếu chỉ có Tam giác có chức năng Xoay, thì đó sẽ là lỗi trình biên dịch nếu bạn cố gọi nó trên tất cả các đối tượng của lớp. Sử dụng dynamic_cast, bạn có thể mô phỏng astừ khóa. Để rõ ràng, nếu một cast thất bại, nó trả về một con trỏ không hợp lệ. Vì vậy, !testvề cơ bản là một tốc ký để kiểm tra nếu testlà NULL hoặc một con trỏ không hợp lệ, điều đó có nghĩa là việc truyền không thành công.

Lợi ích của biến tự động

Sau khi thấy tất cả những điều tuyệt vời mà phân bổ động có thể làm, có lẽ bạn sẽ tự hỏi tại sao mọi người KHÔNG sử dụng phân bổ động mọi lúc? Tôi đã nói với bạn một lý do, đống là chậm. Và nếu bạn không cần tất cả bộ nhớ đó, bạn không nên lạm dụng nó. Vì vậy, đây là một số nhược điểm không theo thứ tự cụ thể:

  • Nó dễ bị lỗi. Cấp phát bộ nhớ thủ công là nguy hiểm và bạn dễ bị rò rỉ. Nếu bạn không thành thạo sử dụng trình gỡ lỗi hoặc valgrind(một công cụ rò rỉ bộ nhớ), bạn có thể nhổ tóc ra khỏi đầu. May mắn thay, thành ngữ RAII và con trỏ thông minh làm giảm bớt điều này một chút, nhưng bạn phải làm quen với các thực tiễn như Quy tắc ba và Quy tắc năm. Đó là rất nhiều thông tin để đưa vào, và những người mới bắt đầu không biết hoặc không quan tâm sẽ rơi vào cái bẫy này.

  • Nó không phải là cần thiết. Không giống như Java và C # nơi sử dụng newtừ khóa ở mọi nơi, trong C ++, bạn chỉ nên sử dụng nó nếu bạn cần. Các cụm từ phổ biến đi, mọi thứ trông giống như một cái đinh nếu bạn có một cái búa. Trong khi những người mới bắt đầu với C ++ sợ các con trỏ và học cách sử dụng các biến stack theo thói quen, các lập trình viên Java và C # bắt đầu bằng cách sử dụng các con trỏ mà không hiểu nó! Đó là nghĩa đen bước trên chân sai. Bạn phải từ bỏ mọi thứ bạn biết vì cú pháp là một chuyện, học ngôn ngữ là chuyện khác.

1. (N) RVO - Aka, (Được đặt tên) Tối ưu hóa giá trị trả về

Một tối ưu hóa nhiều trình biên dịch thực hiện những điều gọi là sự bỏ bớttối ưu hóa giá trị trả về . Những thứ này có thể làm mờ các bản sao không cần thiết, hữu ích cho các đối tượng rất lớn, chẳng hạn như một vectơ chứa nhiều phần tử. Thông thường, thông lệ là sử dụng các con trỏ để chuyển quyền sở hữu thay vì sao chép các đối tượng lớn để di chuyển chúng xung quanh. Điều này đã dẫn đến sự khởi đầu của ngữ nghĩa di chuyểncon trỏ thông minh .

Nếu bạn đang sử dụng con trỏ, (N) RVO KHÔNG xảy ra. Sẽ có lợi hơn và ít bị lỗi hơn khi tận dụng (N) RVO thay vì trả lại hoặc chuyển con trỏ nếu bạn lo lắng về tối ưu hóa. Rò rỉ lỗi có thể xảy ra nếu người gọi hàm có trách nhiệm lấy deletemột đối tượng được phân bổ động và như vậy. Có thể khó theo dõi quyền sở hữu của một đối tượng nếu con trỏ được truyền xung quanh như một củ khoai tây nóng. Chỉ cần sử dụng biến stack vì nó đơn giản và tốt hơn.


"Vì vậy, kiểm tra về cơ bản là một tốc ký để kiểm tra xem kiểm tra là NULL hay một con trỏ không hợp lệ, điều đó có nghĩa là việc truyền không thành công." Tôi nghĩ rằng câu này phải được viết lại cho rõ ràng.
berkus

4
"Máy thổi phồng Java muốn bạn tin tưởng" - có thể vào năm 1997, nhưng giờ đây đã lỗi thời, không còn động lực để so sánh Java với C ++ vào năm 2014.
Matt R

15
Câu hỏi cũ, nhưng trong đoạn mã { std::string* s = new std::string; } delete s; // destructor called.... chắc chắn điều này deletesẽ không hoạt động vì trình biên dịch sẽ không biết cái gì snữa?
badger5000

2
Tôi KHÔNG cho -1, nhưng tôi không đồng ý với các tuyên bố mở đầu như được viết. Đầu tiên, tôi không đồng ý có bất kỳ "cường điệu" nào - có thể đã xảy ra xung quanh Y2K, nhưng bây giờ cả hai ngôn ngữ đều được hiểu rõ. Thứ hai, tôi sẽ lập luận rằng chúng khá giống nhau - C ++ là con của C kết hôn với Simula, Java thêm Virtual Machine, Garbage Collector và HEAVILY cắt giảm các tính năng và C # sắp xếp hợp lý và giới thiệu lại các tính năng bị thiếu cho Java. Đúng, điều này làm cho các mô hình và cách sử dụng hợp lệ HUGELY khác nhau, nhưng có ích khi hiểu cơ sở hạ tầng / mong muốn chung để người ta có thể thấy sự khác biệt.
Gerasimos R

1
@James Matta: Tất nhiên bạn đúng rằng bộ nhớ là bộ nhớ và cả hai đều được phân bổ từ cùng một bộ nhớ vật lý, nhưng một điều cần xem xét là rất phổ biến để có được các đặc tính hiệu suất tốt hơn khi làm việc với các đối tượng được cấp phát ngăn xếp vì ngăn xếp - hoặc ít nhất là mức cao nhất của nó - có khả năng rất "nóng" trong bộ đệm khi các chức năng vào và thoát, trong khi heap không có lợi ích như vậy nên nếu bạn đang đuổi theo con trỏ trong heap, bạn có thể nhận được nhiều lỗi nhớ cache. bạn có thể sẽ không ở trên ngăn xếp. Nhưng tất cả "sự ngẫu nhiên" này thường ủng hộ ngăn xếp.
Gerasimos R

23

C ++ cung cấp cho bạn ba cách để vượt qua một đối tượng: theo con trỏ, theo tham chiếu và theo giá trị. Java giới hạn bạn với cái sau (ngoại lệ duy nhất là các kiểu nguyên thủy như int, boolean, v.v.). Nếu bạn muốn sử dụng C ++ không giống như một món đồ chơi kỳ lạ, thì tốt hơn bạn nên tìm hiểu sự khác biệt giữa ba cách này.

Java giả vờ rằng không có vấn đề nào như 'ai và khi nào nên phá hủy điều này?'. Câu trả lời là: The Garbage Collector, Great and Awful. Tuy nhiên, nó không thể cung cấp bảo vệ 100% chống rò rỉ bộ nhớ (vâng, java có thể rò rỉ bộ nhớ ). Trên thực tế, GC cung cấp cho bạn một cảm giác sai lầm về an toàn. Chiếc SUV của bạn càng lớn, đường đến với người sơ tán càng dài.

C ++ giúp bạn đối mặt với quản lý vòng đời của đối tượng. Chà, có nhiều cách để đối phó với điều đó ( gia đình con trỏ thông minh , QObject trong Qt, v.v.), nhưng không ai trong số chúng có thể được sử dụng theo cách 'cháy và quên' như GC: bạn nên luôn luôn ghi nhớ xử lý bộ nhớ. Bạn không chỉ quan tâm đến việc tiêu diệt một đối tượng, bạn cũng phải tránh phá hủy cùng một đối tượng nhiều lần.

Chưa sợ à? Ok: tài liệu tham khảo theo chu kỳ - tự xử lý chúng, con người. Và hãy nhớ rằng: tiêu diệt chính xác từng đối tượng một lần, chúng tôi C ++ thời gian không thích những kẻ gây rối với xác chết, để những người chết một mình.

Vì vậy, trở lại câu hỏi của bạn.

Khi bạn truyền đối tượng của mình xung quanh theo giá trị, không phải bằng con trỏ hoặc tham chiếu, bạn sao chép đối tượng (toàn bộ đối tượng, cho dù đó là một vài byte hoặc kết xuất cơ sở dữ liệu khổng lồ - bạn đủ thông minh để tránh sau này, aren ' t bạn?) mỗi khi bạn làm '='. Và để truy cập các thành viên của đối tượng, bạn sử dụng '.' (chấm).

Khi bạn truyền đối tượng của mình bằng con trỏ, bạn chỉ sao chép một vài byte (4 trên hệ thống 32 bit, 8 trên hệ thống 64 bit), cụ thể là - địa chỉ của đối tượng này. Và để hiển thị điều này cho mọi người, bạn sử dụng toán tử '->' lạ mắt này khi bạn truy cập các thành viên. Hoặc bạn có thể sử dụng kết hợp '*' và '.'.

Khi bạn sử dụng tài liệu tham khảo, sau đó bạn nhận được con trỏ giả vờ là một giá trị. Đó là một con trỏ, nhưng bạn truy cập các thành viên thông qua '.'.

Và, để thổi tâm trí của bạn một lần nữa: khi bạn khai báo một số biến được phân tách bằng dấu phẩy, sau đó (xem tay):

  • Loại được trao cho tất cả mọi người
  • Công cụ sửa đổi giá trị / con trỏ / tham chiếu là riêng lẻ

Thí dụ:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrkhông được dùng nữa, xin vui lòng không sử dụng nó.
Neil

2
Khá chắc chắn rằng bạn không thể có một tham chiếu là thành viên mà không cung cấp cho nhà xây dựng một danh sách khởi tạo bao gồm biến tham chiếu. (Một tham chiếu phải được khởi tạo ngay lập tức. Ngay cả phần thân của nhà xây dựng cũng quá muộn để đặt nó, IIRC.)
cHao

20

Trong C ++, các đối tượng được phân bổ trên ngăn xếp (sử dụng Object object;câu lệnh trong một khối) sẽ chỉ sống trong phạm vi chúng được khai báo. Khi khối mã kết thúc thực thi, đối tượng được khai báo bị hủy. Trong khi đó, nếu bạn phân bổ bộ nhớ trên heap, sử dụng Object* obj = new Object(), chúng sẽ tiếp tục sống trong heap cho đến khi bạn gọi delete obj.

Tôi sẽ tạo một đối tượng trên heap khi tôi muốn sử dụng đối tượng không chỉ trong khối mã đã khai báo / cấp phát nó.


6
Object objkhông phải luôn luôn trên ngăn xếp - ví dụ như các biến toàn cục hoặc biến thành viên.
10

2
@LightnessRacesinOrbit Tôi chỉ đề cập về các đối tượng được phân bổ trong một khối, không phải về các biến toàn cục và thành viên. Điều đó là không rõ ràng, bây giờ đã sửa nó - thêm "trong một khối" trong câu trả lời. Hy vọng thông tin không sai của nó bây giờ :)
Karthik Kalyanasundaram

20

Nhưng tôi không thể hiểu tại sao chúng ta nên sử dụng nó như thế này?

Tôi sẽ so sánh cách nó hoạt động bên trong cơ thể chức năng nếu bạn sử dụng:

Object myObject;

Bên trong chức năng, bạn myObjectsẽ bị hủy khi chức năng này trở lại. Vì vậy, điều này rất hữu ích nếu bạn không cần đối tượng bên ngoài chức năng của mình. Đối tượng này sẽ được đưa vào ngăn xếp luồng hiện tại.

Nếu bạn viết bên trong cơ thể chức năng:

 Object *myObject = new Object;

sau đó, đối tượng lớp Object được chỉ bởi myObjectsẽ không bị hủy khi hàm kết thúc và cấp phát nằm trên heap.

Bây giờ nếu bạn là lập trình viên Java, thì ví dụ thứ hai gần hơn với cách phân bổ đối tượng hoạt động theo java. Dòng này: Object *myObject = new Object;tương đương với java : Object myObject = new Object();. Sự khác biệt là trong java myObject sẽ nhận được rác được thu thập, trong khi theo c ++, nó sẽ không được giải phóng, bạn phải gọi một cách rõ ràng là `xóa myObject; ' nếu không bạn sẽ giới thiệu rò rỉ bộ nhớ.

Vì c ++ 11, bạn có thể sử dụng các cách phân bổ động an toàn : new Object, bằng cách lưu trữ các giá trị trong shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

Ngoài ra, các đối tượng thường được lưu trữ trong các thùng chứa, như map-s hoặc vector-s, chúng sẽ tự động quản lý vòng đời của các đối tượng của bạn.


1
then myObject will not get destroyed once function endsNó hoàn toàn sẽ.
Các cuộc đua Lightness trong Orbit

6
Trong trường hợp con trỏ, myObjectvẫn sẽ bị hủy, giống như bất kỳ biến cục bộ nào khác. Sự khác biệt là giá trị của nó là một con trỏ tới một đối tượng, không phải chính đối tượng đó và sự phá hủy của một con trỏ câm không ảnh hưởng đến điểm của nó. Vì vậy, các đối tượng sẽ tồn tại nói hủy diệt.
cHao

Đã sửa lỗi đó, tất nhiên các biến cục bộ (bao gồm con trỏ) sẽ được giải phóng - chúng nằm trên ngăn xếp.
marcinj

13

Về mặt kỹ thuật, đây là vấn đề cấp phát bộ nhớ, tuy nhiên đây là hai khía cạnh thực tế hơn của vấn đề này. Nó phải thực hiện với hai điều: 1) Phạm vi, khi bạn xác định một đối tượng không có con trỏ, bạn sẽ không thể truy cập vào nó sau khối mã được xác định, trong khi nếu bạn xác định một con trỏ có "mới" thì bạn có thể truy cập nó từ bất cứ nơi nào bạn có một con trỏ tới bộ nhớ này cho đến khi bạn gọi "xóa" trên cùng một con trỏ. 2) Nếu bạn muốn truyền đối số cho hàm bạn muốn truyền con trỏ hoặc tham chiếu để hiệu quả hơn. Khi bạn truyền một đối tượng thì đối tượng được sao chép, nếu đây là một đối tượng sử dụng nhiều bộ nhớ thì điều này có thể tiêu tốn CPU (ví dụ: bạn sao chép một vectơ đầy dữ liệu). Khi bạn vượt qua một con trỏ, tất cả những gì bạn vượt qua là một int (tùy thuộc vào việc thực hiện nhưng hầu hết chúng là một int).

Ngoài ra, bạn cần hiểu rằng "mới" phân bổ bộ nhớ trên heap cần được giải phóng tại một số điểm. Khi bạn không phải sử dụng "mới", tôi khuyên bạn nên sử dụng định nghĩa đối tượng thông thường "trên ngăn xếp".


6

Vâng, câu hỏi chính là tại sao tôi nên sử dụng một con trỏ chứ không phải chính đối tượng? Và câu trả lời của tôi, bạn (hầu như) không bao giờ sử dụng con trỏ thay vì đối tượng, bởi vì C ++ có các tham chiếu , nó an toàn hơn sau đó con trỏ và đảm bảo hiệu suất tương tự như con trỏ.

Một điều khác mà bạn đề cập trong câu hỏi của bạn:

Object *myObject = new Object;

Làm thế nào nó hoạt động? Nó tạo con trỏ Objectkiểu, phân bổ bộ nhớ để phù hợp với một đối tượng và gọi hàm tạo mặc định, nghe có vẻ tốt, phải không? Nhưng thực ra nó không tốt lắm, nếu bạn được cấp phát bộ nhớ (từ khóa đã sử dụng new), bạn cũng phải giải phóng bộ nhớ theo cách thủ công, điều đó có nghĩa là trong mã bạn nên có:

delete myObject;

Điều này gọi hàm hủy và giải phóng bộ nhớ, trông có vẻ dễ dàng, tuy nhiên trong các dự án lớn có thể khó phát hiện nếu một luồng giải phóng bộ nhớ hay không, nhưng với mục đích đó, bạn có thể thử dùng con trỏ chia sẻ , những hiệu suất này giảm nhẹ, nhưng làm việc với nó dễ hơn nhiều họ


Và bây giờ một số giới thiệu đã kết thúc và quay trở lại câu hỏi.

Bạn có thể sử dụng con trỏ thay vì các đối tượng để có hiệu suất tốt hơn trong khi truyền dữ liệu giữa các chức năng.

Hãy xem, bạn có std::string(nó cũng là đối tượng) và nó chứa rất nhiều dữ liệu, ví dụ XML lớn, bây giờ bạn cần phân tích nó, nhưng đối với bạn có chức năng void foo(...)có thể được khai báo theo các cách khác nhau:

  1. void foo(std::string xml); Trong trường hợp này, bạn sẽ sao chép tất cả dữ liệu từ biến của bạn sang ngăn xếp hàm, phải mất một thời gian, do đó hiệu suất của bạn sẽ thấp.
  2. void foo(std::string* xml); Trong trường hợp này, bạn sẽ truyền con trỏ đến đối tượng, cùng tốc độ với size_tbiến truyền , tuy nhiên khai báo này dễ bị lỗi, bởi vì bạn có thể truyền NULLcon trỏ hoặc con trỏ không hợp lệ. Con trỏ thường được sử dụng Cvì nó không có tài liệu tham khảo.
  3. void foo(std::string& xml); Ở đây bạn truyền tham chiếu, về cơ bản nó giống như chuyển con trỏ, nhưng trình biên dịch thực hiện một số nội dung và bạn không thể chuyển tham chiếu không hợp lệ (thực tế có thể tạo tình huống với tham chiếu không hợp lệ, nhưng đó là trình biên dịch lừa).
  4. void foo(const std::string* xml); Đây là giống như thứ hai, chỉ có thể thay đổi giá trị con trỏ.
  5. void foo(const std::string& xml); Ở đây giống như thứ ba, nhưng giá trị đối tượng không thể thay đổi.

Hơn nữa tôi muốn đề cập đến, bạn có thể sử dụng 5 cách này để truyền dữ liệu cho dù bạn đã chọn cách phân bổ nào (có newhoặc thường xuyên ).


Một điều khác cần đề cập, khi bạn tạo đối tượng theo cách thông thường , bạn phân bổ bộ nhớ trong ngăn xếp, nhưng trong khi bạn tạo nó với newbạn phân bổ heap. Việc phân bổ stack nhanh hơn rất nhiều, nhưng nó khá nhỏ đối với các mảng dữ liệu thực sự lớn, vì vậy nếu bạn cần đối tượng lớn, bạn nên sử dụng heap, vì bạn có thể bị tràn stack, nhưng thường thì vấn đề này được giải quyết bằng cách sử dụng các container STL và nhớ std::stringcũng là container, một số kẻ quên nó :)


5

Giả sử bạn có class Achứa class BKhi bạn muốn gọi một số chức năng class Bbên ngoài, class Abạn sẽ chỉ cần lấy một con trỏ tới lớp này và bạn có thể làm bất cứ điều gì bạn muốn và nó cũng sẽ thay đổi ngữ cảnh class Btrongclass A

Nhưng hãy cẩn thận với đối tượng năng động


5

Có nhiều lợi ích của việc sử dụng con trỏ để phản đối -

  1. Hiệu quả (như bạn đã chỉ ra). Truyền đối tượng cho các chức năng có nghĩa là tạo các bản sao mới của đối tượng.
  2. Làm việc với các đối tượng từ các thư viện bên thứ ba. Nếu đối tượng của bạn thuộc về mã của bên thứ ba và các tác giả có ý định sử dụng các đối tượng của họ thông qua các con trỏ (không có các hàm tạo sao chép, v.v.), cách duy nhất bạn có thể vượt qua đối tượng này là sử dụng các con trỏ. Đi qua giá trị có thể gây ra vấn đề. (Bản sao sâu / vấn đề sao chép nông).
  3. nếu đối tượng sở hữu một tài nguyên và bạn muốn rằng quyền sở hữu không nên bị phá hủy với các đối tượng khác.

3

Điều này đã được thảo luận về chiều dài, nhưng trong Java mọi thứ đều là một con trỏ. Nó không phân biệt giữa phân bổ stack và heap (tất cả các đối tượng được phân bổ trên heap), vì vậy bạn không nhận ra mình đang sử dụng con trỏ. Trong C ++, bạn có thể kết hợp cả hai, tùy thuộc vào yêu cầu bộ nhớ của bạn. Hiệu suất và việc sử dụng bộ nhớ mang tính quyết định nhiều hơn trong C ++ (duh).


3
Object *myObject = new Object;

Làm điều này sẽ tạo một tham chiếu đến một Object (trên heap) phải được xóa rõ ràng để tránh rò rỉ bộ nhớ .

Object myObject;

Làm điều này sẽ tạo ra một đối tượng (myObject) thuộc loại tự động (trên ngăn xếp) sẽ tự động bị xóa khi đối tượng (myObject) đi ra khỏi phạm vi.


1

Một con trỏ trực tiếp tham chiếu vị trí bộ nhớ của một đối tượng. Java không có gì như thế này. Java có các tham chiếu tham chiếu vị trí của đối tượng thông qua các bảng băm. Bạn không thể làm bất cứ điều gì như số học con trỏ trong Java với các tham chiếu này.

Để trả lời câu hỏi của bạn, đó chỉ là sở thích của bạn. Tôi thích sử dụng cú pháp giống như Java.


Bảng băm? Có thể trong một số JVM nhưng đừng tin vào điều đó.
Zan Lynx

Điều gì về JVM đi kèm với Java? Tất nhiên bạn có thể triển khai BẤT CỨ điều gì bạn có thể nghĩ giống như một JVM sử dụng con trỏ trực tiếp hoặc một phương thức thực hiện toán con trỏ. Điều đó giống như nói rằng "mọi người không chết vì cảm lạnh thông thường" và nhận được phản hồi "Có thể hầu hết mọi người không nhưng không tin vào điều đó!" Ha ha.
RioRicoRick

2
@RioRicoRick HotSpot triển khai các tham chiếu Java như các con trỏ riêng, xem docs.oracle.com/javase/7/docs/technotes/guides/vm/. Theo như tôi có thể thấy, JRockit cũng làm như vậy. Cả hai đều hỗ trợ nén OOP, nhưng không bao giờ sử dụng bảng băm. Hậu quả hiệu suất có thể sẽ là thảm họa. Ngoài ra, "đó chỉ là sở thích của bạn" dường như ngụ ý rằng cả hai chỉ là những cú pháp khác nhau cho hành vi tương đương, tất nhiên là không phải vậy.
Max Barraclough


0

Với con trỏ ,

  • Có thể nói chuyện trực tiếp với bộ nhớ.

  • có thể ngăn chặn nhiều rò rỉ bộ nhớ của chương trình bằng cách thao tác con trỏ.


4
" Trong C ++, sử dụng các con trỏ, bạn có thể tạo một trình thu gom rác tùy chỉnh cho chương trình của riêng bạn " nghe có vẻ là một ý tưởng tồi tệ.
lượng

0

Một lý do để sử dụng con trỏ là để giao diện với các chức năng C. Một lý do khác là để tiết kiệm bộ nhớ; ví dụ: thay vì truyền một đối tượng chứa nhiều dữ liệu và có một hàm tạo sao chép chuyên sâu cho bộ xử lý cho một hàm, chỉ cần truyền một con trỏ tới đối tượng, tiết kiệm bộ nhớ và tốc độ, đặc biệt nếu bạn đang ở trong một vòng lặp, tuy nhiên tham chiếu sẽ tốt hơn trong trường hợp đó, trừ khi bạn đang sử dụng mảng kiểu C.


0

Trong các lĩnh vực mà việc sử dụng bộ nhớ là cao cấp, con trỏ trở nên tiện dụng. Ví dụ, hãy xem xét một thuật toán minimax, trong đó hàng ngàn nút sẽ được tạo bằng thói quen đệ quy và sau đó sử dụng chúng để đánh giá bước đi tốt nhất tiếp theo trong trò chơi, khả năng giải quyết hoặc đặt lại (như trong con trỏ thông minh) làm giảm đáng kể mức tiêu thụ bộ nhớ. Trong khi đó, biến không phải con trỏ tiếp tục chiếm không gian cho đến khi cuộc gọi đệ quy trả về một giá trị.


0

Tôi sẽ bao gồm một trường hợp sử dụng quan trọng của con trỏ. Khi bạn đang lưu trữ một số đối tượng trong lớp cơ sở, nhưng nó có thể là đa hình.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

Vì vậy, trong trường hợp này, bạn không thể khai báo bObj là đối tượng trực tiếp, bạn phải có con trỏ.


-5

"Cần thiết là mẹ của sáng chế." Sự khác biệt quan trọng nhất mà tôi muốn chỉ ra là kết quả của trải nghiệm mã hóa của riêng tôi. Đôi khi bạn cần truyền đối tượng cho các hàm. Trong trường hợp đó, nếu đối tượng của bạn thuộc một lớp rất lớn thì việc chuyển nó thành đối tượng sẽ sao chép trạng thái của nó (mà bạn có thể không muốn ..AND CÓ THỂ LỚN LÊN TUYỆT VỜI) do đó dẫn đến việc sao chép đối tượng sao chép. Kích thước 4 byte (giả sử 32 bit). Những lý do khác đã được đề cập ở trên ...


14
bạn nên ưu tiên vượt qua bằng cách tham khảo
bolov

2
Tôi khuyên bạn nên chuyển bằng tham chiếu không đổi như biến std::string test;chúng ta có void func(const std::string &) {}nhưng trừ khi hàm cần thay đổi đầu vào trong trường hợp tôi khuyên bạn nên sử dụng con trỏ (để bất kỳ ai đọc mã đều chú ý &và hiểu hàm có thể thay đổi đầu vào của nó)
Top- Thầy

-7

Có rất nhiều câu trả lời xuất sắc, nhưng hãy để tôi cho bạn một ví dụ:

Tôi có một lớp Item đơn giản:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

Tôi làm một vector để giữ một loạt chúng.

std::vector<Item> inventory;

Tôi tạo một triệu đối tượng Item và đẩy chúng trở lại vào vector. Tôi sắp xếp các vectơ theo tên, và sau đó thực hiện tìm kiếm nhị phân lặp đơn giản cho một tên mục cụ thể. Tôi kiểm tra chương trình, và phải mất hơn 8 phút để hoàn thành việc thực hiện. Sau đó, tôi thay đổi vector hàng tồn kho của mình như vậy:

std::vector<Item *> inventory;

... và tạo hàng triệu đối tượng Item của tôi thông qua mới. Các thay đổi CHỈ tôi thực hiện cho mã của mình là sử dụng các con trỏ tới Mục, ngoại trừ một vòng lặp tôi thêm để dọn dẹp bộ nhớ ở cuối. Chương trình đó chạy trong vòng dưới 40 giây, hoặc tốt hơn là tăng tốc độ gấp 10 lần. EDIT: Mã có tại http://pastebin.com/DK24SPeW Với tối ưu hóa trình biên dịch, nó chỉ hiển thị mức tăng 3,4 lần trên máy tôi vừa thử, vẫn còn đáng kể.


2
Bạn đang so sánh các con trỏ sau đó hay bạn vẫn so sánh các đối tượng thực tế? Tôi rất nghi ngờ rằng một mức độ gián tiếp khác có thể cải thiện hiệu suất. Vui lòng cung cấp mã! Bạn có làm sạch đúng cách sau đó?
stefan

1
@stefan Tôi so sánh dữ liệu (cụ thể là trường tên) của các đối tượng cho cả sắp xếp và tìm kiếm. Tôi dọn dẹp đúng cách, như tôi đã đề cập trong bài viết. việc tăng tốc có thể là do hai yếu tố: 1) std :: vector push_back () sao chép các đối tượng, vì vậy phiên bản con trỏ chỉ cần sao chép một con trỏ trên mỗi đối tượng. Điều này có nhiều tác động đến hiệu suất, vì không chỉ ít dữ liệu được sao chép, mà bộ cấp phát bộ nhớ lớp vectơ cũng bị giảm bớt.
Darren

2
Đây là mã hiển thị thực tế không có sự khác biệt cho ví dụ của bạn: sắp xếp. Mã con trỏ nhanh hơn 6% so với mã không phải con trỏ cho riêng loại, nhưng nhìn chung nó chậm hơn 10% so với mã không phải con trỏ. ideone.com/G0c7zw
stefan

3
Từ khóa : push_back. Tất nhiên bản sao này. Bạn nên có emplacetại chỗ khi tạo các đối tượng của mình (trừ khi bạn cần lưu trữ chúng ở nơi khác).
gạch dưới

1
Các vectơ của con trỏ hầu như luôn luôn sai. Vui lòng không đề xuất chúng mà không giải thích chi tiết, hãy cẩn thận và những ưu và nhược điểm. Bạn dường như đã tìm thấy một pro, đó chỉ là hậu quả của một ví dụ phản biện kém, và trình bày sai về nó
Các cuộc đua Lightness trong Orbit
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.