Tại sao các lập trình viên C ++ nên giảm thiểu việc sử dụng 'mới'?


873

Tôi vấp phải câu hỏi Stack Overflow Rò rỉ bộ nhớ với std :: string khi sử dụng std :: list <std :: string> , và một trong những ý kiến nói rằng:

Ngừng sử dụng newquá nhiều. Tôi không thể thấy bất kỳ lý do nào bạn sử dụng mới bất cứ nơi nào bạn đã làm. Bạn có thể tạo các đối tượng theo giá trị trong C ++ và đó là một trong những lợi thế lớn khi sử dụng ngôn ngữ.
Bạn không phải phân bổ mọi thứ trên đống.
Ngừng suy nghĩ như một lập trình viên Java .

Tôi không thực sự chắc chắn những gì anh ấy có nghĩa là.

Tại sao các đối tượng nên được tạo bởi giá trị trong C ++ càng thường xuyên càng tốt, và nó tạo ra sự khác biệt nào trong nội bộ?
Tôi đã giải thích sai câu trả lời?

Câu trả lời:


1037

Có hai kỹ thuật phân bổ bộ nhớ được sử dụng rộng rãi: phân bổ tự động và phân bổ động. Thông thường, có một vùng bộ nhớ tương ứng cho mỗi vùng: ngăn xếp và đống.

Cây rơm

Ngăn xếp luôn phân bổ bộ nhớ theo kiểu liên tiếp. Nó có thể làm như vậy bởi vì nó yêu cầu bạn giải phóng bộ nhớ theo thứ tự ngược lại (First-In, Last-Out: FILO). Đây là kỹ thuật cấp phát bộ nhớ cho các biến cục bộ trong nhiều ngôn ngữ lập trình. Nó rất, rất nhanh bởi vì nó đòi hỏi sổ sách tối thiểu và địa chỉ tiếp theo để phân bổ là ẩn.

Trong C ++, đây được gọi là lưu trữ tự động vì lưu trữ được yêu cầu tự động ở cuối phạm vi. Ngay khi thực hiện khối mã hiện tại (được phân định bằng cách sử dụng {}), bộ nhớ cho tất cả các biến trong khối đó sẽ tự động được thu thập. Đây cũng là thời điểm mà hàm hủy được viện dẫn để các nguồn lực dọn dẹp.

Đống

Heap cho phép chế độ cấp phát bộ nhớ linh hoạt hơn. Sổ sách kế toán phức tạp hơn và phân bổ chậm hơn. Vì không có điểm phát hành ngầm, bạn phải giải phóng bộ nhớ theo cách thủ công, sử dụng deletehoặc delete[]( freebằng C). Tuy nhiên, sự vắng mặt của một điểm phát hành ngầm là chìa khóa cho tính linh hoạt của heap.

Lý do nên sử dụng phân bổ động

Ngay cả khi việc sử dụng heap chậm hơn và có khả năng dẫn đến rò rỉ bộ nhớ hoặc phân mảnh bộ nhớ, vẫn có những trường hợp sử dụng hoàn toàn tốt để phân bổ động, vì nó ít bị hạn chế hơn.

Hai lý do chính để sử dụng phân bổ động:

  • Bạn không biết bạn cần bao nhiêu bộ nhớ trong thời gian biên dịch. Chẳng hạn, khi đọc tệp văn bản thành một chuỗi, bạn thường không biết tệp có kích thước như thế nào, vì vậy bạn không thể quyết định phân bổ bao nhiêu bộ nhớ cho đến khi bạn chạy chương trình.

  • Bạn muốn phân bổ bộ nhớ sẽ tồn tại sau khi rời khỏi khối hiện tại. Chẳng hạn, bạn có thể muốn viết một hàm string readfile(string path)trả về nội dung của tệp. Trong trường hợp này, ngay cả khi ngăn xếp có thể chứa toàn bộ nội dung tệp, bạn không thể trả về từ một hàm và giữ khối bộ nhớ được phân bổ.

Tại sao phân bổ động thường không cần thiết

Trong C ++, có một cấu trúc gọn gàng được gọi là hàm hủy . Cơ chế này cho phép bạn quản lý tài nguyên bằng cách sắp xếp thời gian tồn tại của tài nguyên với thời gian tồn tại của một biến. Kỹ thuật này được gọi là RAII và là điểm phân biệt của C ++. Nó "bọc" tài nguyên vào các đối tượng. std::stringlà một ví dụ hoàn hảo. Đoạn trích này:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

thực sự phân bổ một lượng bộ nhớ thay đổi. Đối std::stringtượng phân bổ bộ nhớ bằng cách sử dụng heap và giải phóng nó trong hàm hủy của nó. Trong trường hợp này, bạn không cần phải quản lý thủ công bất kỳ tài nguyên nào và vẫn nhận được lợi ích của việc cấp phát bộ nhớ động.

Cụ thể, nó ngụ ý rằng trong đoạn trích này:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

có sự phân bổ bộ nhớ động không cần thiết. Chương trình yêu cầu nhập nhiều hơn (!) Và đưa ra nguy cơ quên mất việc sắp xếp bộ nhớ. Nó làm điều này không có lợi ích rõ ràng.

Tại sao bạn nên sử dụng lưu trữ tự động càng thường xuyên càng tốt

Về cơ bản, đoạn cuối tổng hợp nó. Sử dụng lưu trữ tự động thường xuyên nhất có thể làm cho các chương trình của bạn:

  • gõ nhanh hơn;
  • nhanh hơn khi chạy;
  • ít bị rò rỉ bộ nhớ / tài nguyên.

Điểm thưởng

Trong câu hỏi tham khảo, có những mối quan tâm bổ sung. Đặc biệt, lớp sau:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

Thực sự có nhiều rủi ro hơn khi sử dụng so với cách sau:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

Lý do là std::stringxác định đúng một hàm tạo sao chép. Hãy xem xét chương trình sau:

int main ()
{
    Line l1;
    Line l2 = l1;
}

Sử dụng phiên bản gốc, chương trình này có thể sẽ gặp sự cố, vì nó sử dụng deletetrên cùng một chuỗi hai lần. Sử dụng phiên bản sửa đổi, mỗi Linedụ sẽ sở hữu chuỗi riêng của mình Ví dụ , mỗi bộ nhớ riêng của mình và cả hai sẽ được phát hành vào cuối chương trình.

Ghi chú khác

Việc sử dụng rộng rãi RAII được coi là một cách thực hành tốt nhất trong C ++ vì tất cả các lý do trên. Tuy nhiên, có một lợi ích bổ sung không rõ ràng ngay lập tức. Về cơ bản, nó tốt hơn tổng số các bộ phận của nó. Toàn bộ cơ chế sáng tác . Nó vảy.

Nếu bạn sử dụng Linelớp làm khối xây dựng:

 class Table
 {
      Line borders[4];
 };

Sau đó

 int main ()
 {
     Table table;
 }

phân bổ bốn std::stringtrường hợp, bốn Linetrường hợp, một Tablethể hiện và tất cả nội dung của chuỗi và mọi thứ được giải phóng tự động .


55
+1 để đề cập đến RAII ở cuối, nhưng cần có một cái gì đó về các trường hợp ngoại lệ và ngăn chặn.
Tobu

7
@Tobu: vâng, nhưng bài đăng này đã khá dài và tôi muốn giữ nó khá tập trung vào câu hỏi của OP. Tôi sẽ kết thúc việc viết một bài đăng trên blog hoặc một cái gì đó và liên kết với nó từ đây.
André Caron

15
Sẽ là một bổ sung tuyệt vời để đề cập đến nhược điểm của phân bổ ngăn xếp (ít nhất là cho đến C ++ 1x) - bạn thường cần sao chép những thứ không cần thiết nếu bạn không cẩn thận. ví dụ như Monsterphun ra từ a Treasuređến Worldkhi nó chết. Trong Die()phương pháp của nó, nó thêm kho báu cho thế giới. Nó phải sử dụng world->Add(new Treasure(/*...*/))khác để bảo quản kho báu sau khi nó chết. Các lựa chọn thay thế là shared_ptr(có thể là quá mức cần thiết), auto_ptr(ngữ nghĩa kém để chuyển quyền sở hữu), chuyển qua giá trị (lãng phí) và move+ unique_ptr(chưa được triển khai rộng rãi).
kizzx2

7
Những gì bạn nói về các biến cục bộ được phân bổ ngăn xếp có thể là một chút sai lệch. "Ngăn xếp" dùng để chỉ ngăn xếp cuộc gọi, nơi lưu trữ các khung ngăn xếp . Đây là những khung ngăn xếp được lưu trữ theo kiểu LIFO. Các biến cục bộ cho một khung cụ thể được phân bổ như thể chúng là thành viên của một cấu trúc.
someguy

7
@someguy: Thật vậy, lời giải thích không hoàn hảo. Việc thực hiện có tự do trong chính sách phân bổ của nó. Tuy nhiên, các biến được yêu cầu phải được khởi tạo và hủy theo kiểu LIFO, do đó, sự tương tự giữ. Tôi không nghĩ rằng nó làm việc phức tạp hơn câu trả lời nữa.
André Caron

171

Bởi vì ngăn xếp nhanh hơn và không bị rò rỉ

Trong C ++, chỉ cần một lệnh duy nhất để phân bổ không gian - trên ngăn xếp - cho mọi đối tượng phạm vi cục bộ trong một chức năng nhất định và không thể rò rỉ bất kỳ bộ nhớ nào. Nhận xét đó dự định (hoặc nên có ý định) để nói điều gì đó như "sử dụng ngăn xếp chứ không phải heap".


18
"phải mất một hướng dẫn duy nhất để phân bổ không gian" - oh, vô nghĩa. Chắc chắn chỉ cần một hướng dẫn để thêm vào con trỏ ngăn xếp, nhưng nếu lớp có bất kỳ cấu trúc bên trong thú vị nào, sẽ có nhiều hơn nhiều so với việc thêm vào con trỏ ngăn xếp đang diễn ra. Cũng có giá trị như nhau khi nói rằng trong Java, không cần hướng dẫn để phân bổ không gian, bởi vì trình biên dịch sẽ quản lý các tham chiếu tại thời gian biên dịch.
Charlie Martin

31
@Charlie là chính xác. Biến tự động là nhanh và hoàn hảo sẽ chính xác hơn.
Oliver Charlesworth

28
@Charlie: Các lớp bên trong cần phải được thiết lập một trong hai cách. Việc so sánh đang được thực hiện trên việc phân bổ không gian cần thiết.
Oliver Charlesworth

51
ho int x; return &x;
peterchen

16
nhanh có Nhưng chắc chắn không thể đánh lừa được. Không có gì là ngu ngốc. Bạn có thể nhận được StackOverflow :)
rxantos

107

Lý do tại sao phức tạp.

Đầu tiên, C ++ không phải là rác được thu thập. Do đó, đối với mỗi cái mới, phải có một xóa tương ứng. Nếu bạn không thể xóa phần xóa này, thì bạn sẽ bị rò rỉ bộ nhớ. Bây giờ, đối với một trường hợp đơn giản như thế này:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

Cái này đơn giản. Nhưng chuyện gì sẽ xảy ra nếu "Do Stuff" ném ngoại lệ? Rất tiếc: rò rỉ bộ nhớ. Điều gì xảy ra nếu vấn đề "Làm công cụ" returnsớm? Rất tiếc: rò rỉ bộ nhớ.

Và đây là trường hợp đơn giản nhất . Nếu bạn tình cờ trả lại chuỗi đó cho ai đó, bây giờ họ phải xóa nó. Và nếu họ vượt qua nó như một đối số, người nhận nó có cần xóa nó không? Khi nào họ nên xóa nó?

Hoặc, bạn chỉ có thể làm điều này:

std::string someString(...);
//Do stuff

Không delete. Đối tượng được tạo trên "ngăn xếp" và nó sẽ bị phá hủy một khi nó vượt ra khỏi phạm vi. Bạn thậm chí có thể trả về đối tượng, do đó chuyển nội dung của nó sang chức năng gọi. Bạn có thể truyền đối tượng cho các hàm (thường là tham chiếu hoặc tham chiếu const : void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis). Và vv.

Tất cả không có newdelete. Không có câu hỏi ai là người sở hữu bộ nhớ hoặc ai chịu trách nhiệm xóa nó. Nếu bạn làm:

std::string someString(...);
std::string otherString;
otherString = someString;

Điều này được hiểu rằng otherStringcó một bản sao của dữ liệu của someString. Nó không phải là một con trỏ; nó là một đối tượng riêng biệt Chúng có thể có cùng một nội dung, nhưng bạn có thể thay đổi một nội dung mà không ảnh hưởng đến nội dung khác:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

Xem ý tưởng?


1
Trên lưu ý đó ... Nếu một đối tượng được phân bổ động main(), tồn tại trong suốt thời gian của chương trình, không thể dễ dàng tạo trên ngăn xếp do tình huống và con trỏ tới nó được chuyển đến bất kỳ chức năng nào yêu cầu quyền truy cập vào nó , điều này có thể gây rò rỉ trong trường hợp sự cố chương trình, hoặc nó sẽ an toàn? Tôi sẽ giả sử điều thứ hai, vì HĐH sắp xếp lại tất cả bộ nhớ của chương trình cũng sẽ xử lý nó một cách hợp lý, nhưng tôi không muốn thừa nhận bất cứ điều gì khi nói đến new.
Thời gian của Justin - Tái lập lại

4
@JustinTime Bạn không cần phải lo lắng về việc giải phóng bộ nhớ của các đối tượng được phân bổ động sẽ tồn tại suốt đời của chương trình. Khi một chương trình thực thi, HĐH sẽ tạo ra một tập bản đồ bộ nhớ vật lý hoặc Bộ nhớ ảo cho nó. Mọi địa chỉ trong không gian bộ nhớ ảo được ánh xạ tới một địa chỉ của bộ nhớ vật lý và khi chương trình thoát ra, tất cả những gì được ánh xạ tới bộ nhớ ảo sẽ được giải phóng. Vì vậy, miễn là chương trình thoát hoàn toàn, bạn không cần lo lắng về việc bộ nhớ được phân bổ sẽ không bao giờ bị xóa.
Aiman ​​Al-Eryani

75

Các đối tượng được tạo bởi newcuối cùng phải deletesợ rằng chúng bị rò rỉ. Hàm hủy sẽ không được gọi, bộ nhớ sẽ không được giải phóng, toàn bộ bit. Vì C ++ không có bộ sưu tập rác, nên đó là một vấn đề.

Các đối tượng được tạo bởi giá trị (tức là trên ngăn xếp) sẽ tự động chết khi chúng đi ra khỏi phạm vi. Cuộc gọi hàm hủy được trình biên dịch chèn vào và bộ nhớ được tự động giải phóng khi trả về hàm.

Con trỏ thông minh thích unique_ptr, shared_ptrgiải quyết vấn đề tham chiếu lơ lửng, nhưng chúng đòi hỏi kỷ luật mã hóa và có các vấn đề tiềm năng khác (khả năng sao chép, vòng lặp tham chiếu, v.v.).

Ngoài ra, trong các kịch bản đa luồng rất nhiều, newlà một điểm bất đồng giữa các luồng; có thể có một tác động hiệu suất cho việc lạm dụng new. Tạo đối tượng ngăn xếp là theo định nghĩa luồng-cục bộ, vì mỗi luồng có ngăn xếp riêng.

Nhược điểm của các đối tượng giá trị là chúng chết khi hàm máy chủ trả về - bạn không thể chuyển tham chiếu đến các đối tượng đó cho người gọi, chỉ bằng cách sao chép, trả lại hoặc di chuyển theo giá trị.


9
+1. Re "Các đối tượng được tạo bởi newcuối cùng phải deletesợ rằng chúng bị rò rỉ." - tệ hơn nữa, new[]phải được khớp bởi delete[]và bạn có hành vi không xác định nếu bạn delete new[]nhớ bộ nhớ hoặc delete[] newbộ nhớ -ed - rất ít trình biên dịch cảnh báo về điều này (một số công cụ như Cppcheck làm khi có thể).
Tony Delroy

3
@TonyDelroy Có những tình huống mà trình biên dịch không thể cảnh báo điều này. Nếu một hàm trả về một con trỏ, nó có thể được tạo nếu mới (một phần tử đơn) hoặc mới [].
fbafelipe

32
  • C ++ không sử dụng bất kỳ trình quản lý bộ nhớ nào. Các ngôn ngữ khác như C #, Java có trình thu gom rác để xử lý bộ nhớ
  • Việc triển khai C ++ thường sử dụng các thường trình của hệ điều hành để phân bổ bộ nhớ và quá nhiều mới / xóa có thể làm phân mảnh bộ nhớ khả dụng
  • Với bất kỳ ứng dụng nào, nếu bộ nhớ thường xuyên được sử dụng, bạn nên phân bổ trước và giải phóng khi không cần thiết.
  • Quản lý bộ nhớ không đúng cách có thể dẫn đến rò rỉ bộ nhớ và thực sự rất khó để theo dõi. Vì vậy, sử dụng các đối tượng ngăn xếp trong phạm vi chức năng là một kỹ thuật đã được chứng minh
  • Nhược điểm của việc sử dụng các đối tượng ngăn xếp là, nó tạo ra nhiều bản sao của các đối tượng khi quay trở lại, chuyển đến các chức năng, v.v. Tuy nhiên, trình biên dịch thông minh nhận thức rõ các tình huống này và chúng đã được tối ưu hóa tốt cho hiệu suất
  • Thật sự tẻ nhạt trong C ++ nếu bộ nhớ được phân bổ và phát hành ở hai nơi khác nhau. Trách nhiệm phát hành luôn là một câu hỏi và chủ yếu chúng tôi dựa vào một số con trỏ thường truy cập, các đối tượng ngăn xếp (tối đa có thể) và các kỹ thuật như auto_ptr (các đối tượng RAII)
  • Điều tốt nhất là, bạn đã kiểm soát bộ nhớ và điều tồi tệ nhất là bạn sẽ không có bất kỳ quyền kiểm soát nào đối với bộ nhớ nếu chúng tôi sử dụng quản lý bộ nhớ không phù hợp cho ứng dụng. Các sự cố gây ra do hỏng bộ nhớ là nhanh nhất và khó theo dõi.

5
Trên thực tế, bất kỳ ngôn ngữ phân bổ bộ nhớ đều có trình quản lý bộ nhớ, bao gồm c. Hầu hết chỉ rất đơn giản, tức là int * x = malloc (4); int * y = malloc (4); ... Cuộc gọi đầu tiên sẽ phân bổ bộ nhớ, còn gọi os cho bộ nhớ, (thường là trong các khối 1k / 4k) để cuộc gọi thứ hai, không thực sự phân bổ bộ nhớ, nhưng cung cấp cho bạn một đoạn của đoạn cuối được phân bổ. IMO, người thu gom rác không phải là người quản lý bộ nhớ, vì nó chỉ xử lý việc tự động phân bổ bộ nhớ. Để được gọi là trình quản lý bộ nhớ, nó không chỉ xử lý việc phân bổ mà còn phân bổ bộ nhớ.
Rahly

1
Các biến cục bộ sử dụng stack để trình biên dịch không phát ra cuộc gọi đến malloc()hoặc bạn bè của nó để phân bổ bộ nhớ cần thiết. Tuy nhiên, ngăn xếp không thể giải phóng bất kỳ mục nào trong ngăn xếp, cách duy nhất bộ nhớ ngăn xếp được phát hành là giải phóng khỏi đỉnh ngăn xếp.
Mikko Rantalainen

C ++ không "sử dụng các thói quen của hệ điều hành"; Đó không phải là một phần của ngôn ngữ, nó chỉ là một cách thực hiện phổ biến. C ++ thậm chí có thể chạy mà không cần bất kỳ hệ điều hành.
einpoklum

22

Tôi thấy rằng một vài lý do quan trọng để thực hiện càng ít cái mới càng tốt đã bị bỏ qua:

Toán tử newcó thời gian thực hiện không xác định

Việc gọi newcó thể hoặc không thể khiến HĐH phân bổ một trang vật lý mới cho quy trình của bạn, điều này có thể khá chậm nếu bạn thực hiện thường xuyên. Hoặc nó có thể đã có sẵn một vị trí bộ nhớ phù hợp, chúng tôi không biết. Nếu chương trình của bạn cần có thời gian thực hiện nhất quán và có thể dự đoán được (như trong hệ thống thời gian thực hoặc mô phỏng trò chơi / vật lý), bạn cần tránh newtrong các vòng lặp quan trọng trong thời gian của mình.

Toán tử newlà một đồng bộ hóa chủ đề ngầm

Có bạn đã nghe tôi nói, hệ điều hành của bạn cần đảm bảo các bảng trang của bạn nhất quán và vì cách gọi như vậy newsẽ khiến luồng của bạn có được một khóa mutex ẩn. Nếu bạn liên tục gọi newtừ nhiều luồng, bạn thực sự đang nối tiếp các luồng của mình (Tôi đã thực hiện việc này với 32 CPU, mỗi lần nhấn newđể nhận được vài trăm byte mỗi luồng, ouch! Đó là một pita hoàng gia để gỡ lỗi)

Phần còn lại như chậm, phân mảnh, dễ bị lỗi, vv đã được đề cập bởi các câu trả lời khác.


2
Cả hai có thể tránh được bằng cách sử dụng vị trí mới / xóa và phân bổ bộ nhớ trước khi sử dụng. Hoặc bạn có thể tự phân bổ / giải phóng bộ nhớ và sau đó gọi hàm tạo / hàm hủy. Đây là cách std :: vector thường hoạt động.
rxantos

1
@rxantos Vui lòng đọc OP, câu hỏi này là về việc tránh phân bổ bộ nhớ không cần thiết. Ngoài ra, không có vị trí xóa.
Emily L.

@Emily Đây là ý nghĩa của OP, tôi nghĩ:void * someAddress = ...; delete (T*)someAddress
xryl669

1
Sử dụng stack cũng không xác định trong thời gian thực hiện. Trừ khi bạn gọi mlock()hoặc một cái gì đó tương tự. Điều này là do hệ thống có thể sắp hết bộ nhớ và không có trang bộ nhớ vật lý sẵn sàng cho ngăn xếp nên HĐH có thể cần phải trao đổi hoặc ghi một số bộ nhớ cache (xóa bộ nhớ bẩn) vào đĩa trước khi thực hiện có thể tiến hành.
Mikko Rantalainen

1
@mikkorantalainen điều đó đúng về mặt kỹ thuật nhưng trong tình huống bộ nhớ thấp, tất cả các cược đều bị tắt dù sao thì hiệu suất của bạn cũng bị đẩy vào đĩa nên bạn không thể làm gì được. Dù sao, nó không làm mất hiệu lực lời khuyên để tránh các cuộc gọi mới khi hợp lý để làm như vậy.
Emily L.

21

Tiền C ++ 17:

Bởi vì nó dễ bị rò rỉ tinh tế ngay cả khi bạn bọc kết quả trong một con trỏ thông minh .

Hãy xem xét một người dùng "cẩn thận", người nhớ bọc các đối tượng trong các con trỏ thông minh:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

Mã này rất nguy hiểm vì không có gì đảm bảo rằng một trong hai shared_ptrđược xây dựng trước khi một trong hai T1hoặc T2. Do đó, nếu một trong hai new T1()hoặc new T2()thất bại sau khi người kia thành công, thì đối tượng đầu tiên sẽ bị rò rỉ vì không shared_ptrtồn tại để phá hủy và giải phóng nó.

Giải pháp: sử dụng make_shared.

Sau C ++ 17:

Đây không còn là vấn đề nữa: C ++ 17 áp đặt một ràng buộc về thứ tự của các hoạt động này, trong trường hợp này đảm bảo rằng mỗi cuộc gọi new()phải được thực hiện ngay lập tức bằng cách xây dựng con trỏ thông minh tương ứng, không có thao tác nào khác ở giữa. Điều này ngụ ý rằng, tại thời điểm thứ hai new()được gọi, nó được đảm bảo rằng đối tượng đầu tiên đã được bọc trong con trỏ thông minh của nó, do đó ngăn chặn mọi rò rỉ trong trường hợp ném ngoại lệ.

Một lời giải thích chi tiết hơn về thứ tự đánh giá mới được giới thiệu bởi C ++ 17 đã được Barry cung cấp trong một câu trả lời khác .

Cảm ơn @Remy Lebeau đã chỉ ra rằng đây vẫn là một vấn đề trong C ++ 17 (mặc dù ít hơn): hàm shared_ptrtạo có thể không phân bổ khối điều khiển và ném, trong trường hợp đó, con trỏ chuyển đến nó không bị xóa.

Giải pháp: sử dụng make_shared.


5
Giải pháp khác: Không bao giờ tự động phân bổ nhiều hơn một đối tượng trên mỗi dòng.
Antimon

3
@Antimony: Vâng, sẽ rất hấp dẫn khi phân bổ nhiều hơn một đối tượng khi bạn đã phân bổ một đối tượng, so với khi bạn chưa phân bổ bất kỳ đối tượng nào.
dùng541686

1
Tôi nghĩ rằng một câu trả lời tốt hơn là smart_ptr sẽ bị rò rỉ nếu một ngoại lệ được gọi và không có gì bắt được nó.
Natalie Adams

2
Ngay cả trong trường hợp sau C ++ 17, rò rỉ vẫn có thể xảy ra nếu newthành công và sau đó việc shared_ptrxây dựng tiếp theo thất bại. std::make_shared()cũng sẽ giải quyết điều đó
Remy Lebeau 15/03/19

1
@Mehrdad hàm shared_ptrtạo trong câu hỏi phân bổ bộ nhớ cho khối điều khiển lưu trữ con trỏ và deleter được chia sẻ, vì vậy, về mặt lý thuyết, nó có thể gây ra lỗi bộ nhớ. Chỉ các nhà xây dựng sao chép, di chuyển và răng cưa là không ném. make_sharedphân bổ đối tượng được chia sẻ bên trong khối điều khiển, do đó chỉ có 1 phân bổ thay vì 2.
Remy Lebeau 15/03/19

17

Ở một mức độ lớn, đó là một người nâng cao điểm yếu của họ thành một quy tắc chung. Có gì sai cho mỗi gia nhập với tạo các đối tượng sử dụng các newnhà điều hành. Điều có một số tranh luận là bạn phải làm như vậy với một số kỷ luật: nếu bạn tạo một đối tượng, bạn cần chắc chắn rằng nó sẽ bị phá hủy.

Cách dễ nhất để làm điều đó là tạo đối tượng trong bộ lưu trữ tự động, vì vậy C ++ biết cách phá hủy nó khi nó vượt ra khỏi phạm vi:

 {
    File foo = File("foo.dat");

    // do things

 }

Bây giờ, quan sát rằng khi bạn rơi ra khỏi khối đó sau khi kết thúc, foonằm ngoài phạm vi. C ++ sẽ gọi dtor của nó tự động cho bạn. Không giống như Java, bạn không cần phải chờ đợi GC tìm thấy nó.

Bạn đã viết chưa

 {
     File * foo = new File("foo.dat");

bạn muốn kết hợp nó rõ ràng với

     delete foo;
  }

hoặc thậm chí tốt hơn, phân bổ của bạn File *như là một "con trỏ thông minh". Nếu bạn không cẩn thận về điều đó có thể dẫn đến rò rỉ.

Câu trả lời tự đưa ra giả định sai lầm rằng nếu bạn không sử dụng, newbạn không phân bổ vào đống; Thực tế, trong C ++ bạn không biết điều đó. Nhiều nhất, bạn biết rằng một lượng nhỏ bộ nhớ, giả sử một con trỏ, chắc chắn được phân bổ trên ngăn xếp. Tuy nhiên, hãy xem xét việc triển khai Tệp có giống như không

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

sau đó vẫnFileImpl sẽ được phân bổ trên ngăn xếp.

Và vâng, bạn nên chắc chắn có

     ~File(){ delete fd ; }

trong lớp cũng vậy; không có nó, bạn sẽ bị rò rỉ bộ nhớ từ heap ngay cả khi bạn rõ ràng không phân bổ cho heap.


4
Bạn nên xem mã trong câu hỏi được tham khảo. Chắc chắn có rất nhiều điều đi sai trong mã đó.
André Caron

7
Tôi đồng ý không có gì sai khi sử dụng new per se , nhưng nếu bạn nhìn vào mã gốc, bình luận có liên quan đến, newđang bị lạm dụng. Mã được viết giống như Java hoặc C #, trong đó newthực tế được sử dụng cho mọi biến số, khi mọi thứ có ý nghĩa hơn nhiều trong ngăn xếp.
luke

5
Điểm công bằng. Nhưng các quy tắc chung thường được thi hành để tránh những cạm bẫy thông thường. Cho dù đây có phải là điểm yếu của cá nhân hay không, quản lý bộ nhớ đủ phức tạp để đảm bảo quy tắc chung như thế này! :)
Robben_Ford_Fan_boy

9
@Charlie: bình luận không nói rằng bạn không bao giờ nên sử dụng new. Nó nói rằng nếu bạn sự lựa chọn giữa phân bổ động và lưu trữ tự động, hãy sử dụng lưu trữ tự động.
André Caron

8
@Charlie: không có gì sai khi sử dụng new, nhưng nếu bạn sử dụng delete, bạn đang làm sai!
Matthieu M ..

16

new()không nên được sử dụng ít nhất có thể. Nó nên được sử dụng càng cẩn thận càng tốt. Và nó nên được sử dụng thường xuyên khi cần thiết theo chủ nghĩa thực dụng.

Phân bổ các đối tượng trên ngăn xếp, dựa vào sự phá hủy ngầm của chúng, là một mô hình đơn giản. Nếu phạm vi yêu cầu của một đối tượng phù hợp với mô hình đó thì không cần sử dụng new(), với việc liên kết delete()và kiểm tra các con trỏ NULL. Trong trường hợp bạn có nhiều phân bổ đối tượng tồn tại ngắn trên ngăn xếp thì sẽ giảm các vấn đề về phân mảnh heap.

Tuy nhiên, nếu thời gian tồn tại của đối tượng của bạn cần vượt ra ngoài phạm vi hiện tại thì đó new()là câu trả lời đúng. Chỉ cần đảm bảo rằng bạn chú ý đến thời điểm và cách bạn gọi delete()và khả năng của con trỏ NULL, sử dụng các đối tượng đã xóa và tất cả các vấn đề khác đi kèm với việc sử dụng con trỏ.


9
"Nếu thời gian tồn tại của đối tượng của bạn cần vượt ra ngoài phạm vi hiện tại thì new () là câu trả lời đúng" ... tại sao không ưu tiên trả về theo giá trị hoặc chấp nhận biến trong phạm vi người gọi bằng cách không tham chiếu consthoặc con trỏ ...?
Tony Delroy

2
@Tony: Vâng, vâng! Tôi rất vui khi nghe ai đó ủng hộ tài liệu tham khảo. Chúng được tạo ra để ngăn chặn vấn đề này.
Nathan Osman

@TonyD ... hoặc kết hợp chúng: trả về một con trỏ thông minh theo giá trị. Bằng cách đó, người gọi và trong nhiều trường hợp (nghĩa make_shared/_uniquelà có thể sử dụng được), callee không bao giờ cần newhoặc delete. Câu trả lời này bỏ lỡ các điểm thực: (A) C ++ cung cấp những thứ như RVO, di chuyển ngữ nghĩa và các tham số đầu ra - điều này thường có nghĩa là việc xử lý việc tạo đối tượng và mở rộng trọn đời bằng cách trả lại bộ nhớ được cấp phát động trở nên không cần thiết và bất cẩn. (B) Ngay cả trong các tình huống bắt buộc phải phân bổ động, stdlib cung cấp các trình bao bọc RAII giúp người dùng giảm bớt các chi tiết bên trong xấu xí.
gạch dưới

14

Khi bạn sử dụng mới, các đối tượng được phân bổ cho heap. Nó thường được sử dụng khi bạn dự đoán mở rộng. Khi bạn khai báo một đối tượng như,

Class var;

nó được đặt trên ngăn xếp

Bạn sẽ luôn phải gọi tiêu diệt trên đối tượng mà bạn đã đặt trên heap với mới. Điều này mở ra khả năng rò rỉ bộ nhớ. Các đối tượng được đặt trên ngăn xếp không dễ bị rò rỉ bộ nhớ!


2
+1 "[heap] thường được sử dụng khi bạn dự đoán mở rộng" - như nối thêm vào std::stringhoặc std::map, vâng, cái nhìn sâu sắc. Phản ứng ban đầu của tôi là "nhưng cũng rất phổ biến để tách rời vòng đời của một đối tượng khỏi phạm vi của mã tạo", nhưng thực sự trả về bằng giá trị hoặc chấp nhận các giá trị trong phạm vi của người gọi bằng cách không consttham chiếu hoặc con trỏ sẽ tốt hơn cho điều đó, trừ khi có "mở rộng" liên quan quá. Có một số cách sử dụng âm thanh khác như phương pháp nhà máy mặc dù ....
Tony Delroy

12

Một lý do đáng chú ý để tránh lạm dụng heap là về hiệu năng - cụ thể là liên quan đến hiệu suất của cơ chế quản lý bộ nhớ mặc định được sử dụng bởi C ++. Mặc dù phân bổ có thể khá nhanh trong trường hợp tầm thường, nhưng thực hiện rất nhiều newdeletetrên các đối tượng có kích thước không đồng đều mà không có trật tự nghiêm ngặt không chỉ dẫn đến phân mảnh bộ nhớ, mà còn làm phức tạp thuật toán phân bổ và hoàn toàn có thể phá hủy hiệu suất trong một số trường hợp nhất định.

Đó là vấn đề mà bộ nhớ nơi tạo ra để giải quyết, cho phép giảm thiểu những nhược điểm cố hữu của việc triển khai heap truyền thống, trong khi vẫn cho phép bạn sử dụng heap khi cần thiết.

Tuy nhiên, tốt hơn hết là để tránh vấn đề hoàn toàn. Nếu bạn có thể đặt nó trên ngăn xếp, sau đó làm như vậy.


Bạn luôn có thể phân bổ một lượng bộ nhớ hợp lý và sau đó sử dụng vị trí mới / xóa nếu tốc độ là một vấn đề.
rxantos

Các nhóm bộ nhớ là để tránh sự phân mảnh, để tăng tốc độ phân bổ (một lần phân bổ cho hàng ngàn đối tượng) và làm cho việc phân bổ an toàn hơn.
Lothar

10

Tôi nghĩ rằng poster có nghĩa là để nói You do not have to allocate everything on theheapchứ không phải là stack.

Về cơ bản các đối tượng được phân bổ trên ngăn xếp (tất nhiên nếu kích thước đối tượng cho phép) vì chi phí phân bổ ngăn xếp rẻ, thay vì phân bổ dựa trên heap liên quan đến khá nhiều công việc của người cấp phát và thêm tính chi tiết vì sau đó bạn phải quản lý dữ liệu được phân bổ trên heap.


10

Tôi có xu hướng không đồng ý với ý tưởng sử dụng "quá nhiều" mới. Mặc dù người gửi ban đầu sử dụng mới với các lớp hệ thống là một chút vô lý. ( int *i; i = new int[9999];? thực sự? int i[9999];rõ ràng hơn nhiều.) Tôi nghĩ đó là những gì đã nhận được dê bình luận.

Khi bạn làm việc với các đối tượng hệ thống, rất hiếm khi bạn cần nhiều hơn một tham chiếu đến cùng một đối tượng. Miễn là giá trị là như nhau, đó mới là vấn đề. Và các đối tượng hệ thống thường không chiếm nhiều dung lượng trong bộ nhớ. (một byte cho mỗi ký tự, trong một chuỗi). Và nếu có, các thư viện nên được thiết kế để tính đến việc quản lý bộ nhớ đó (nếu chúng được viết tốt). Trong những trường hợp này, (tất cả trừ một hoặc hai tin tức trong mã của anh ấy), thực tế mới là vô nghĩa và chỉ phục vụ để giới thiệu sự nhầm lẫn và tiềm năng cho lỗi.

Tuy nhiên, khi bạn làm việc với các lớp / đối tượng của riêng bạn (ví dụ: lớp Line của người gửi bài gốc), thì bạn phải bắt đầu suy nghĩ về các vấn đề như dấu chân bộ nhớ, sự tồn tại của dữ liệu, v.v. Tại thời điểm này, cho phép nhiều tham chiếu đến cùng một giá trị là vô giá - nó cho phép các cấu trúc như danh sách, từ điển và biểu đồ được liên kết, trong đó nhiều biến không chỉ có cùng giá trị mà còn tham chiếu chính xác cùng một đối tượng trong bộ nhớ. Tuy nhiên, lớp Line không có bất kỳ yêu cầu nào trong số đó. Vì vậy, mã của người gửi ban đầu thực sự hoàn toàn không có nhu cầu new.


thông thường mới / xóa sẽ được sử dụng nó khi bạn không biết trước khi sử dụng kích thước của mảng. Tất nhiên std :: vector ẩn mới / xóa cho bạn. Bạn vẫn sử dụng chúng, nhưng máng std :: vector. Vì vậy, ngày nay nó sẽ được sử dụng khi bạn không biết kích thước của mảng và vì một lý do nào đó, hãy tránh sử dụng std :: vector (tuy nhỏ nhưng vẫn tồn tại).
rxantos

When you're working with your own classes/objects... Bạn thường không có lý do để làm như vậy! Một tỷ lệ nhỏ Qs nằm trên chi tiết thiết kế container của các lập trình viên lành nghề. Ngược lại, một tỷ lệ đáng buồn về sự nhầm lẫn của những người mới không biết stdlib tồn tại - hoặc chủ động được giao những nhiệm vụ khủng khiếp trong 'khóa học' ', trong đó một gia sư yêu cầu họ vô tình phát minh lại bánh xe - trước khi họ thậm chí còn đã học được một bánh xe là gì và tại sao nó hoạt động. Bằng cách thúc đẩy phân bổ trừu tượng hơn, C ++ có thể cứu chúng ta khỏi 'segfault vô tận với danh sách được liên kết' của C; xin vui lòng, chúng ta hãy để cho nó .
gạch dưới

" việc sử dụng mới của người gửi bài mới với các lớp hệ thống là hơi vô lý. ( int *i; i = new int[9999];? thực sự? int i[9999];rõ ràng hơn nhiều.)" Vâng, rõ ràng hơn, nhưng để chơi người ủng hộ của quỷ, loại này không nhất thiết là một cuộc tranh luận tồi. Với 9999 phần tử, tôi có thể tưởng tượng một hệ thống nhúng chặt chẽ không có đủ ngăn xếp cho 9999 phần tử: 9999x4 byte là ~ 40 kB, x8 ~ 80 kB. Vì vậy, các hệ thống như vậy có thể cần sử dụng phân bổ động, giả sử họ thực hiện nó bằng bộ nhớ thay thế. Tuy nhiên, điều đó chỉ có thể biện minh cho phân bổ động, không new; a vectorsẽ là sửa chữa thực sự trong trường hợp đó
underscore_d

Đồng ý với @underscore_d - đó không phải là một ví dụ tuyệt vời. Tôi sẽ không thêm 40.000 hoặc 80.000 byte vào ngăn xếp của mình như thế. Tôi thực sự có thể sẽ phân bổ chúng trên đống (với std::make_unique<int[]>()tất nhiên).
einpoklum

3

Hai lý do:

  1. Nó không cần thiết trong trường hợp này. Bạn đang làm cho mã của bạn phức tạp hơn không cần thiết.
  2. Nó phân bổ không gian trên heap, và điều đó có nghĩa là bạn phải nhớ deletenó sau, nếu không nó sẽ gây rò rỉ bộ nhớ.

2

newlà mới goto.

Hãy nhớ lại lý do tại sao lại gotobị chê như vậy: mặc dù nó là một công cụ mạnh mẽ, ở mức độ thấp để kiểm soát luồng, mọi người thường sử dụng nó theo những cách phức tạp không cần thiết khiến mã khó theo dõi. Hơn nữa, các mẫu hữu ích nhất và dễ đọc nhất được mã hóa trong các câu lệnh lập trình có cấu trúc (ví dụ forhoặc while); hiệu quả cuối cùng là mã ở đâu gotolà cách thích hợp để khá hiếm, nếu bạn muốn viết goto, có lẽ bạn đang làm việc rất tệ (trừ khi bạn thực sự biết bạn đang làm gì).

newlà tương tự - nó thường được sử dụng để làm cho những thứ phức tạp không cần thiết và khó đọc hơn, và các mẫu sử dụng hữu ích nhất có thể được mã hóa đã được mã hóa thành các lớp khác nhau. Hơn nữa, nếu bạn cần sử dụng bất kỳ mẫu sử dụng mới nào chưa có các lớp tiêu chuẩn, bạn có thể viết các lớp của riêng mình mã hóa chúng!

Tôi thậm chí sẽ tranh luận rằng điều đó newcòn tồi tệ hơn goto, do nhu cầu ghép nối newdeletetuyên bố.

Giống như goto, nếu bạn từng nghĩ rằng bạn cần sử dụng new, có lẽ bạn đang làm việc rất tệ - đặc biệt là nếu bạn đang làm như vậy bên ngoài việc thực hiện một lớp học có mục đích trong cuộc sống là gói gọn bất kỳ phân bổ động nào bạn cần làm.


Và tôi sẽ thêm: "Về cơ bản, bạn không cần nó".
einpoklum

1

Lý do cốt lõi là các đối tượng trên heap luôn khó sử dụng và quản lý hơn các giá trị đơn giản. Viết mã dễ đọc và duy trì luôn là ưu tiên hàng đầu của bất kỳ lập trình viên nghiêm túc nào.

Một kịch bản khác là thư viện chúng tôi đang sử dụng cung cấp ngữ nghĩa giá trị và phân bổ động không cần thiết. Std::stringlà một ví dụ tốt.

Tuy nhiên, đối với mã hướng đối tượng, sử dụng một con trỏ - có nghĩa là sử dụng newđể tạo nó trước - là điều bắt buộc. Để đơn giản hóa sự phức tạp của quản lý tài nguyên, chúng tôi có hàng tá công cụ để làm cho nó đơn giản nhất có thể, chẳng hạn như con trỏ thông minh. Mô hình dựa trên đối tượng hoặc mô hình chung giả định ngữ nghĩa giá trị và yêu cầu ít hoặc khôngnew , giống như các áp phích ở nơi khác đã nêu.

Các mẫu thiết kế truyền thống, đặc biệt là các mẫu được đề cập trong sách GoF , sử dụng newrất nhiều, vì chúng là mã OO điển hình.


4
Đây là một câu trả lời sâu sắc . For object oriented code, using a pointer [...] is a must: vô nghĩa . Nếu bạn đang phá giá 'OO' bằng cách chỉ tham khảo một tập hợp nhỏ, tính đa hình - cũng vô nghĩa: các tài liệu tham khảo cũng hoạt động. [pointer] means use new to create it beforehand: đặc biệt là vô nghĩa : các tham chiếu hoặc con trỏ có thể được lấy để tự động phân bổ các đối tượng & được sử dụng đa hình; xem tôi . [typical OO code] use new a lot: có thể trong một số cuốn sách cũ, nhưng ai quan tâm? Bất kỳ eschews new/ con trỏ thô hiện đại nào của C ++ hiện đại bất cứ khi nào có thể - & không có cách nào khác với OO bằng cách làm như vậy
underscore_d

1

Thêm một điểm nữa cho tất cả các câu trả lời đúng ở trên, nó phụ thuộc vào loại chương trình bạn đang làm. Ví dụ, việc phát triển kernel trong Windows -> Ngăn xếp bị hạn chế nghiêm trọng và bạn có thể không gặp phải lỗi trang như trong chế độ người dùng.

Trong các môi trường như vậy, các lệnh gọi API mới hoặc C giống như được ưa thích và thậm chí là bắt buộc.

Tất nhiên, đây chỉ là một ngoại lệ cho quy tắc.


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.