Câu hỏi Tại sao tôi nên sử dụng con trỏ thay vì chính đối tượng?


Tôi đến từ mộ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 con trỏ tới các đối tượng thay vì 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 hàm, 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ó có liên quan đến hiệu quả và tốc độ kể từ khi chúng tôi truy cập trực tiếp vào địa chỉ bộ nhớ. Tôi có đúng không?


1335
2018-03-03 11:54


gốc


Kudos cho bạn để đặt câu hỏi 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
Nếu bạn không thấy lý do để sử dụng con trỏ, thì đừng. Ưu tiên các đối tượng. Ưu tiên các đối tượng trước unique_ptr trước shared_ptr trước các con trỏ thô. - stefan
lưu ý: trong java, mọi thứ (ngoại trừ kiểu 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
Lưu ý rằng, trong Java, 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ực hiện rõ ràng trong mã. Java sử dụng con trỏ ở mọi nơi. - Daniel Martín
Đó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 là 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 xử lý nghiêm túc. - Manu343726


Các câu trả lời:


Rất tiếc là bạn thấy phân bổ động quá thường xuyên. Điều đó chỉ cho thấy có bao nhiêu người lập trình C ++ xấu.

Theo nghĩa nào đó, bạn có hai câu hỏi được nhóm lại thành một câu hỏi. Đầ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 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ì đó thích hợp hơn và an toàn hơn so với thực hiện phân bổ động thủ công và / hoặc sử dụng con trỏ thô.

Phân bổ động

Trong câu hỏi của bạn, bạn đã chứng minh 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 làm Object myObject; trong một khối, đối tượng được tạo với thời gian lưu trữ tự động, điều đó có nghĩa là đối tượng sẽ tự động bị hủy khi nó nằm ngoài phạm vi. Khi bạn làm new Object(), đối tượng có thời lượng lưu trữ động, có nghĩa là nó vẫn còn sống cho đến khi bạn rõ ràng delete nó. Bạn chỉ nên sử dụng thời lượng lưu trữ động khi cần. Đó là, bạn nên luôn luôn thích tạo đối tượng với thời lượng 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 để sống lâu hơn phạm vi hiện tại - đối tượng cụ thể ở vị trí bộ nhớ cụ thể đó, không phải bản sao của nó. Nếu bạn đồng ý với việc sao chép / di chuyển đối tượng (phần lớn thời gian bạn nên có), bạn nên chọn một đối tượng tự động.
  2. Bạn cần phân bổ 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 bận tâm đến vấn đề 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 tầm nhìn của C ++, nhưng tiếc là chúng ta phải đối phó với thực tế của các hệ thống chúng ta đang phát triển cho.

Khi bạn thực sự yêu cầu phân bổ động, bạn nên đóng 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 (giống như các thùng chứa tiêu chuẩn). Con trỏ thông minh cung cấp ngữ nghĩa quyền sở hữu của các đối tượng được phân bổ động. Hãy xem std::unique_ptr và std::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 bạn (xem Quy tắc Zero).

Con trỏ

Tuy nhiên, có nhiều cách sử dụng tổng quát hơn cho các 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 chọn. Như trước, 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 ngữ nghĩa tham chiếu. Đôi khi bạn muốn truyền một đối tượng bằng cách sử dụng một con trỏ (bất kể nó được phân bổ như thế nào) bởi vì bạn muốn hàm mà bạn đang truyền nó để có quyền truy cập đối tượng cụ thể đó (không phải bản sao của nó). Tuy nhiên, trong hầu hết các trường hợp, bạn nên chọn các kiểu tham chiếu đến con trỏ, bởi vì đây là những gì chúng được thiết kế cho. Lưu ý rằng điều này không nhất thiết phải kéo dài tuổi thọ của đối tượng vượt quá phạm vi hiện tại, như trong trường hợp 1 ở trên. Như trước đây, nếu bạn đồng ý với việc truyền một bản sao của đối tượng, bạn không cần ngữ nghĩa tham chiếu.

  2. Bạn cần đa hình. Bạn chỉ có thể gọi các hàm đa hình (tức 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 ưa thích.

  3. Bạn muốn đại diện cho một đối tượng là tùy chọn bằng cách cho phép nullptr được truyền 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. Nếu không, bạn nên sử dụng 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 đây, 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 một khai báo chuyển tiếp của kiểu được trỏ tới (để 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 phải giao tiếp với một 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 mất đi vào giây phút 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 nó get chức năng thành viê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 deallocate thông qua một xử lý, bạn thường có thể quấn tay cầm lên trong một con trỏ thông minh với một deleter tùy chỉnh sẽ deallocate đối tượng một cách thích hợp.


1348
2018-03-03 12:06



"Bạn cần đối tượng để sống 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 để sống lâu hơn phạm vi hiện tại, nhưng bạn thực sự không. Nếu bạn đặt đối tượng của mình bên trong một vectơ, ví dụ, đối tượng sẽ được sao chép (hoặc di chuyển) vào vectơ và đối tượng ban đầu sẽ an toàn để hủy khi phạm vi của nó kết thúc. - hvd
Hãy nhớ rằng s / copy / move / ở nhiều nơi bây giờ. Trả về 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 ra. - Puppy
Tôi nhớ một tham chiếu rõ ràng với RAII về câu trả lời này. C ++ là tất cả (gần như tất cả) về quản lý tài nguyên, và RAII là cách để làm điều đó trên C ++ (Và vấn đề chính mà con trỏ thô tạo ra: Breaking RAII) - Manu343726
Các 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 với nhau. Bạn không thể di chuyển ngữ nghĩa, và std :: gán của 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, nó không đủ để có quyền truy cập vào một Trình biên dịch C ++ 11, cần thiết tất cả các trình biên dịch bạn 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 đang nhìn bạn.) - armb
@ MDMoore313 Bạn có thể viết Object myObject(param1, etc...) - user000001


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 chiếu và tránh sao chép. Đối với các loại không đa hình, một con trỏ (hoặc một tham chiếu) sẽ tránh sao chép một đối tượng có khả nă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 được nhiều bản sao của các đối tượng đắt tiền vào đối số hàm và làm giá trị trả về. Nhưng bằng cách sử dụng một con trỏ chắc chắn sẽ tránh những người 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).

Thu nhận tài nguyên. Tạo một con trỏ tới một tài nguyên bằng cách sử dụng new toán tử là một chống mẫu 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 là 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
}

Con trỏ thô chỉ nên được sử dụng làm "chế độ xem" chứ không phải theo bất kỳ cách nào liên quan đến quyền sở hữu, có thể là thông qua tạo trực tiếp hoặc ngầm qua các giá trị trả lại. Xem thêm câu hỏi này từ Câu hỏi thường gặp về C ++.

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


156
2018-03-06 18:40



"Tạo một con trỏ tới một tài nguyên bằng toán tử mới là một mẫu chống" Tôi nghĩ bạn thậm chí có thể nâng cao điều đó để có một con trỏ thô sở hữu một cái gì đó là một mô hình chống. Không chỉ tạo ra, mà còn chuyển các con trỏ thô làm đối số hoặc trả về các giá trị ngụ ý chuyển quyền sở hữu IMHO không được chấp nhận vì unique_ptr/ di chuyển ngữ nghĩa - dyp
@dyp tnx, cập nhật và tham chiếu đến C ++ FAQ Q & A về chủ đề này. - TemplateRex
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 mà nó được áp dụng, nhưng hầu hết thời gian, cùng một lý do mà tranh luận cho phân bổ động (tùy ý đời) tranh luận chống lại bất kỳ con trỏ thông minh thông thường là tốt. - James Kanze
@ JamesKanze Tôi không ngụ ý ngụ ý rằng con trỏ thông minh nên được sử dụng ở khắp mọi nơi, chỉ cho quyền sở hữu, và cũng có thể rằng con trỏ thô không nên được sử dụng cho quyền sở hữu, nhưng chỉ cho quan điểm. - TemplateRex
@TemplateRex Điều đó có vẻ hơi ngớ ngẩn cho rằng hun(b) cũng đòi hỏi kiến ​​thức về chữ ký trừ khi bạn tốt với không biết rằng bạn cung cấp loại sai cho đến khi biên dịch. Mặc dù vấn đề tham chiếu thường không bị bắt vào thời gian biên dịch và sẽ cố gắng nhiều hơn để gỡ lỗi, nếu bạn đang kiểm tra chữ ký để đảm bảo các đối số đúng, bạn cũng sẽ có thể xem có đối số nào là tham chiếu hay không do đó 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 các IDE hoặc trình soạn thảo văn bản hiển thị chữ ký của một hàm được chọn). Cũng thế, const&. - JAB


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 các khai báo, đa hình, nhưng tôi cảm thấy một phần của "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 trên Java và C ++.

Hãy kiểm tra 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à - nhiều hơn hoặc ít hơn - 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ó những ngoại lệ cho điều này - ví dụ, nếu bạn khai báo các kiểu Java "nguyên thuỷ", 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à đúng hoặc sai cách để xử lý mọi thứ; tuy nhiên các câu trả lời khác đã bao hàm một cách 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à về nơi chúng sẽ sống.

Mang về nhà - Object * object = new Object() xây dựng thực sự là những gì gần nhất với ngữ nghĩa Java điển hình (hoặc C # cho vấn đề đó).


111
2018-03-03 14:34



Object2 is now "dead": Tôi nghĩ bạn muốn nói myObject1 hoặc chính xác hơn the object pointed to by myObject1. - Clément
Thật! Rephrased một chút. - Gerasimos R
Object object1 = new Object(); Object object2 = new Object(); là mã rất xấu. Phương thức khởi tạo mới thứ hai hoặc đối tượng Object thứ hai có thể ném, và bây giờ object1 bị rò rỉ. Nếu bạn đang sử dụng nguyên news, bạn nên quấn newđối tượng ed trong trình bao bọc RAII càng sớm càng tốt. - PSkocik
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 xảy ra xung quanh nó. Rất may, đây chỉ là một đoạn giải thích cho thấy một Pointer trong C ++ hoạt động như thế nào - và một trong số ít nơi mà đố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ề con trỏ thô ... - Gerasimos R


Một lý do khác để sử dụng con trỏ sẽ là cho tờ khai 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.


73
2018-03-07 07:30



điều này thực sự thêm vào sự pha trộn của thông tin hữu ích, vì vậy vui vì bạn đã làm cho nó một câu trả lời! - TemplateRex
Tài liệu tham khảo làm việc cho điều này quá. - Zan Lynx
std :: shared_ptr <T> cũng làm việc với các khai báo về phía trước của T. (std :: unique_ptr <T> không) - berkus
@berkus: std::unique_ptr<T> làm việc với các khai báo chuyển tiếp của T. Bạn chỉ cần đảm bảo rằng khi destructor của std::unique_ptr<T> được gọi là, T là một loại hoàn chỉnh. Điều này thường có nghĩa là lớp học của bạn có chứa std::unique_ptr<T> tuyên bố hủy của nó trong tập tin tiêu đề và thực hiện nó trong tập tin cpp (ngay cả khi việc thực hiện có sản phẩm nào). - David Stone
@ DavidStone Cảm ơn, nó đã hoạt động! - berkus


Lời nói đầu

Java là không có gì giống như C + +, trái với hype. Máy hype Java muốn bạn tin rằng vì Java có cú pháp giống C ++, nên các ngôn ngữ giống 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 chuyển sang C ++ và sử dụng cú pháp giống như Java mà không hiểu các hàm ý của mã của họ.

Về sau chúng ta đ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 sẽ giả sử nó   phải làm với hiệu quả và tốc độ kể từ khi chúng tôi 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, bởi vì stack rất đơn giản so với heap. Các biến lưu trữ tự động (hay còn gọi là các biến stack) có các hàm hủy của chúng được gọi một khi chúng nằm ngoà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, thì hàm hủy của nó phải được gọi thủ công. deletegọi destructor 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 new cú 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ể cấp phát một kích thước cố định cho một biến ngăn xếp. Bạn 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::string thay thế, std::string tự thay đổi kích thước nội bộ để không phải là vấn đề. Nhưng về cơ bản giải pháp cho vấn đề này là phân bổ động. Bạn có thể cấp phát 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];

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

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

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

Làm thế nào là một lợi ích mà 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ỏ. Nó thường được giả định rằng họ là như nhau, nhưng họ không. Huyền thoại này xuất phát từ thực tế rằng con trỏ có thể được subscripted giống như mảng và vì mảng phân rã để con trỏ ở cấp cao nhất trong một tuyên bố chức năng. Tuy nhiên, khi một mảng phân rã thành một con trỏ, con trỏ sẽ mất sizeof thông tin. Vì thế sizeof(pointer) sẽ cung cấp 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, bởi 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 cơ sở cho phép bạn xử lý các đối tượng như một đối tượng khác, ví dụ bằng cách sử dụng as từ khóa. Vì vậy, nếu ai đó muốn điều trị Entity đối tượng như 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 dự định gọi các hàm trên một vù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 trong một thời trang 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, nếu chỉ tam giác có một chức năng xoay, nó sẽ là một lỗi trình biên dịch nếu bạn đã cố gắng 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 as từ khóa. Để được rõ ràng, nếu một diễn viên thất bại, nó trả về một con trỏ không hợp lệ. Vì thế !test về cơ bản là viết tắt để kiểm tra xem test là NULL hoặc một con trỏ không hợp lệ, có nghĩa là các diễn viên thất bại.

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

Sau khi nhìn thấy tất cả những điều tuyệt vời mà phân bổ động có thể làm, bạn có thể tự hỏi tại sao không ai KHÔNG sử dụng phân bổ động tất cả thời gian? Tôi đã nói với bạn một lý do, đống này 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ó là dễ bị lỗi. Phân bổ 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 khi sử dụng trình gỡ rối hoặc valgrind (một công cụ rò rỉ bộ nhớ), bạn có thể kéo tóc ra khỏi đầu. May mắn 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 quen thuộc với các thực hành như Quy tắc Ba và Quy tắc Năm. Nó là rất nhiều thông tin để đưa vào, và người mới bắt đầu hoặc là 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 nó là thành ngữ để sử dụng new từ khóa ở khắp mọi nơi, trong C ++, bạn chỉ nên sử dụng nó nếu bạn cần. Cụm từ chung đi, mọi thứ trông giống như móng tay nếu bạn có cây búa. Trong khi những người mới bắt đầu bắt đầu với C ++ thì sợ con trỏ và học cách sử dụng các biến ngăn xếp theo thói quen, lập trình viên Java và C # khởi đầu bằng cách sử dụng 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 thứ, học ngôn ngữ là một thứ khác.

1. (N) RVO - Aka, (được đặt tên)

Một tối ưu hóa mà nhiều trình biên dịch tạo ra là những thứ được gọi là cắt bỏ và tối ưu hóa giá trị trả về. Những thứ này có thể làm giảm bớt các bản sao không cần thiết rấ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ực tế phổ biến là sử dụng con trỏ để chuyển nhượng 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ự ra đời của di chuyển ngữ nghĩa và con trỏ thông minh.

Nếu bạn đang sử dụng con trỏ, (N) RVO không KHÔNG PHẢI xảy ra. Nó có lợi hơn và ít bị lỗi hơn để tận dụng lợi thế của (N) RVO hơn là trả lại hoặc đi qua con trỏ nếu bạn lo lắng về tối ưu hóa. Lỗi rò rỉ có thể xảy ra nếu người gọi hàm có trách nhiệm deleteing mộ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ỏ đang được truyền đi xung quanh giống như khoai tây nóng. Chỉ cần sử dụng các biến stack vì nó đơn giản và tốt hơn.


62
2018-03-07 10:00



"Vì vậy, kiểm tra cơ bản là một cách viết tắt để kiểm tra nếu thử nghiệm là NULL hoặc một con trỏ không hợp lệ, có nghĩa là diễn viên thất bại." Tôi nghĩ câu này phải được viết lại để rõ ràng. - berkus
"Máy hype Java muốn bạn tin" - có thể vào năm 1997, nhưng điều này bây giờ là lỗi thời, không còn động lực để so sánh Java với C ++ trong năm 2014. - Matt R
Câu hỏi cũ, nhưng trong phân đoạn mã { std::string* s = new std::string; } delete s; // destructor called .... chắc chắn điều này delete sẽ không hoạt động vì trình biên dịch sẽ không biết s còn nữa? - badger5000
"... sự khác biệt giữa các con trỏ và mảng được vẫy tay trong Java ..." Không hề. Đặt sang một bên rằng Java không có con trỏ (nó có tham chiếu nullable), mảng là một kiểu như bất kỳ khác. Bạn không thể "handwave" tham chiếu bằng cách loại bỏ cú pháp mảng và giả vờ nó là một tham chiếu đến phần tử đầu tiên của nó. int[] nums = { 0 }; int zero = nums; sẽ không biên dịch. - Justin
@Justin Con trỏ là gì nếu không tham chiếu nullable? - Shoe


C ++ cung cấp cho bạn ba cách để truyền một đối tượng: theo con trỏ, bằng tham chiếu và theo giá trị. Java giới hạn bạn với cái thứ hai (ngoại lệ duy nhất là các kiểu nguyên thủy như int, boolean, vv). Nếu bạn muốn sử dụng C ++ không giống như một món đồ chơi kỳ lạ, thì 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 đề 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 100% bảo vệ chống lại rò rỉ bộ nhớ (có, java có thể bộ nhớ bị rò rỉ). Trên thực tế, GC cung cấp cho bạn cảm giác an toàn giả. Chiếc SUV của bạn càng lớn, con đường của bạn càng xa càng tốt.

C ++ giúp bạn đối mặt trực tiếp với quản lý vòng đời của đối tượng. Vâng, có phương tiện để đối phó với điều đó (con trỏ thông minh gia đình, QObject trong Qt và vv), nhưng không ai trong số họ có thể được sử dụng trong 'lửa và quên' cách như GC: bạn nên luôn luônhãy ghi nhớ xử lý bộ nhớ. Không chỉ bạn nên quan tâm đến việc phá hủy 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.

Không sợ? Ok: tham khảo tuần hoàn - tự xử lý chúng, con người. Và hãy nhớ: giết từng đối tượng một cách chính xác một lần, chúng tôi C ++ runtimes không thích những người mess với xác chết, để lại những người chết một mình.

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

Khi bạn truyền đối tượng của bạn 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 một cơ sở dữ liệu khổng lồ - bạn đủ thông minh để tránh việc sau này, aren ' t bạn?) mỗi khi bạn làm '='. Và để truy cập vào các thành viên của đối tượng, bạn sử dụng '.' (dấu chấm).

Khi bạn truyền đối tượng của bạn bằng con trỏ, bạn chỉ sao chép một vài byte (4 trên các hệ thống 32 bit, 8 trên các 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ử '->' ưa thích 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 của '*' 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ách nhau bằng dấu phẩy, sau đó (xem tay):

  • Loại được cung cấp cho mọi người
  • Giá trị / con trỏ / công cụ sửa đổi tham chiếu là cá nhân

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

21
2018-03-03 12:00



std::auto_ptr không được chấp nhận, vui lòng không sử dụng. - Neil
Cảm ơn std :: chỉ ra điều này, tôi đã chỉnh sửa câu trả lời - Kirill Gamazkov
Khá chắc chắn bạn không thể có tham chiếu làm thành viên mà không cung cấp một hàm tạo với một danh sách khởi tạo bao gồm biến tham chiếu. (Tham chiếu phải được khởi tạo ngay lập tức. Ngay cả phần thân của hàm tạo quá trễ để thiết lập nó, IIRC.) - cHao


Trong C ++, các đối tượng được cấp phát trên stack (sử dụng Object object; câu lệnh trong một khối) sẽ chỉ nằm trong phạm vi mà chúng được khai báo. Khi khối mã kết thúc thực thi, đối tượng được khai báo sẽ bị hủy. Trong khi đó nếu bạn phân bổ bộ nhớ trên heap, sử dụng Object* obj = new Object(), họ 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 các đối tượng không chỉ trong khối mã mà tuyên bố / phân bổ nó.


19
2018-03-03 12:19



Object obj không phải luôn luôn trên ngăn xếp - ví dụ như các hình cầu hoặc biến thành viên. - tenfour
@tenfour Vâng. Tôi biết điều đó :) - Karthik Kalyanasundaram
Sau đó, bạn cố ý đã viết thông tin sai lệch! - Lightness Races in Orbit
@LightnessRacesinOrbit Tôi đã đề cập chỉ 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ầu và thành viên. Thing là nó không rõ ràng, bây giờ sửa chữ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


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 hàm, myObject sẽ bị phá hủy khi hàm này trả về. Vì vậy, điều này rất hữu ích nếu bạn không cần đối tượng của bạn bên ngoài chức năng của bạn. Đối tượng này sẽ được đặt trên 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;

thì đối tượng của lớp Object được chỉ bởi myObject sẽ không bị phá hủy khi chức năng kết thúc, và phân bổ là trên heap.

Bây giờ nếu bạn là lập trình viên Java, thì ví dụ thứ hai là gần hơn với cách phân bổ đối tượng hoạt động trong java. Đường thẳng này: Object *myObject = new Object; tương đương với java: Object myObject = new Object();. Sự khác biệt là dưới java myObject sẽ nhận được rác thu thập được, trong khi dưới c ++ nó sẽ không được giải phóng, bạn phải ở đâu đó một cách rõ ràng gọi `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 Objectbằ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ý toàn bộ thời gian của các đối tượng của bạn.


18
2018-03-03 12:05



then myObject will not get destroyed once function ends Nó hoàn toàn sẽ. - Lightness Races in Orbit
Trong trường hợp con trỏ, myObjectsẽ vẫn 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à con trỏ với một vật thể, không phải đối tượng, và sự hủy diệt của một con trỏ câm không ảnh hưởng đến con trỏ của nó. Nên vật sẽ sống sót sau khi hủy diệt. - cHao
Cố định rằng, các biến cục bộ (bao gồm cả con trỏ) tất nhiên sẽ được giải phóng - chúng nằm trên stack. - marcinj