Khi nào tôi nên sử dụng từ khóa mới trong C ++?


272

Tôi đã sử dụng C ++ được một thời gian ngắn và tôi đã tự hỏi về từ khóa mới . Đơn giản, tôi có nên sử dụng nó hay không?

1) Với từ khóa mới ...

MyClass* myClass = new MyClass();
myClass->MyField = "Hello world!";

2) Không có từ khóa mới ...

MyClass myClass;
myClass.MyField = "Hello world!";

Từ góc độ thực hiện, chúng dường như không khác biệt (nhưng tôi chắc chắn là như vậy) ... Tuy nhiên, ngôn ngữ chính của tôi là C #, và tất nhiên phương pháp đầu tiên là cách tôi sử dụng.

Khó khăn dường như là phương thức 1 khó sử dụng hơn với các lớp std C ++.

Tôi nên sử dụng phương pháp nào?

Cập nhật 1:

Gần đây tôi đã sử dụng từ khóa mới cho bộ nhớ heap (hoặc cửa hàng miễn phí ) cho một mảng lớn nằm ngoài phạm vi (nghĩa là được trả về từ một hàm). Trường hợp trước khi tôi sử dụng ngăn xếp, khiến một nửa các phần tử bị hỏng ngoài phạm vi, việc chuyển sang sử dụng heap đảm bảo rằng các phần tử nằm trong chiến thuật. Yay!

Cập nhật 2:

Một người bạn của tôi gần đây đã nói với tôi rằng có một quy tắc đơn giản để sử dụng newtừ khóa; mỗi khi bạn gõ new, gõ delete.

Foobar *foobar = new Foobar();
delete foobar; // TODO: Move this to the right place.

Điều này giúp ngăn chặn rò rỉ bộ nhớ, vì bạn luôn phải đặt xóa ở đâu đó (nghĩa là khi bạn cắt và dán nó vào một hàm hủy hoặc nếu không).


6
Câu trả lời ngắn gọn là, hãy sử dụng phiên bản ngắn khi bạn có thể thoát khỏi nó. :)
jalf

11
Một kỹ thuật tốt hơn là luôn luôn viết một xóa tương ứng - sử dụng các thùng chứa STL và các con trỏ thông minh như std::vectorstd::shared_ptr. Chúng bao bọc các cuộc gọi đến newdeletecho bạn, do đó bạn thậm chí ít có khả năng rò rỉ bộ nhớ. Hãy tự hỏi mình, ví dụ: bạn có luôn nhớ đặt một tương ứng deleteở mọi nơi một ngoại lệ có thể được ném không? Đưa vào deletes bằng tay khó hơn bạn tưởng.
AshleyBrain

Re: CẬP NHẬT 1 - Một trong những điều hay của C ++ là nó cho phép bạn lưu trữ các loại Xác định người dùng trên ngăn xếp, trong khi rác thu thập các lang như C # buộc bạn phải lưu trữ dữ liệu trên đống . Lưu trữ dữ liệu trên heap tiêu tốn nhiều tài nguyên hơn lưu trữ dữ liệu trên ngăn xếp , do đó bạn nên ưu tiên ngăn xếp cho heap , ngoại trừ khi UDT của bạn yêu cầu một lượng lớn bộ nhớ để lưu trữ dữ liệu của nó. (Điều này cũng có nghĩa là các đối tượng được truyền theo giá trị theo mặc định). Một giải pháp tốt hơn cho vấn đề của bạn sẽ là truyền mảng cho hàm bằng cách tham chiếu .
Charles Addis

Câu trả lời:


303

Phương pháp 1 (sử dụng new)

  • Phân bổ bộ nhớ cho đối tượng trên cửa hàng miễn phí (Điều này thường giống với heap )
  • Yêu cầu bạn phải rõ ràng deleteđối tượng của bạn sau này. (Nếu bạn không xóa nó, bạn có thể tạo rò rỉ bộ nhớ)
  • Bộ nhớ được phân bổ cho đến khi bạn deletenó. (tức là bạn có thể returnmột đối tượng mà bạn đã tạo bằng cách sử dụng new)
  • Ví dụ trong câu hỏi sẽ rò rỉ bộ nhớ trừ khi con trỏ là deleted; và nó phải luôn luôn bị xóa , bất kể đường dẫn điều khiển nào được thực hiện, hoặc nếu ngoại lệ được ném.

Phương pháp 2 (không sử dụng new)

  • Phân bổ bộ nhớ cho đối tượng trên ngăn xếp (nơi tất cả các biến cục bộ đi) Nhìn chung có ít bộ nhớ hơn cho ngăn xếp; nếu bạn phân bổ quá nhiều đối tượng, bạn có nguy cơ tràn ngăn xếp.
  • Bạn sẽ không cần deletenó sau này.
  • Bộ nhớ không còn được phân bổ khi đi ra khỏi phạm vi. (tức là bạn không nên returntrỏ đến một đối tượng trên ngăn xếp)

Theo như một trong những để sử dụng; bạn chọn phương pháp phù hợp nhất với bạn, với các ràng buộc ở trên.

Một số trường hợp dễ dàng:

  • Nếu bạn không muốn lo lắng về việc gọi điện delete, (và khả năng gây rò rỉ bộ nhớ ), bạn không nên sử dụng new.
  • Nếu bạn muốn trả về một con trỏ tới đối tượng của bạn từ một hàm, bạn phải sử dụng new

4
Một nitpick - Tôi tin rằng nhà điều hành mới phân bổ bộ nhớ từ "cửa hàng miễn phí", trong khi malloc phân bổ từ "heap". Những điều này không được đảm bảo là điều tương tự, mặc dù trong thực tế chúng thường như vậy. Xem gotw.ca/gotw/009.htm .
Fred Larson 17/03/2016 lúc

4
Tôi nghĩ rằng câu trả lời của bạn có thể rõ ràng hơn để sử dụng. (99% thời gian, sự lựa chọn rất đơn giản. Sử dụng phương pháp 2, trên một đối tượng trình bao bọc gọi new / xóa trong hàm tạo / hàm hủy)
jalf

4
@jalf: Phương thức 2 là phương pháp không sử dụng cái mới: - / Trong mọi trường hợp, nhiều lần mã của bạn sẽ đơn giản hơn nhiều (ví dụ: xử lý các trường hợp lỗi) bằng cách sử dụng Phương thức 2 (không có cái mới)
Daniel LeCheminant

Một cú đánh khác ... Bạn nên làm rõ hơn rằng ví dụ đầu tiên của Nick làm rò rỉ bộ nhớ, trong khi lần thứ hai của anh ta thì không, ngay cả khi đối mặt với các ngoại lệ.
Arafangion

4
@Fred, Arafangion: Cảm ơn vì sự sáng suốt của bạn; Tôi đã kết hợp ý kiến ​​của bạn vào câu trả lời.
Daniel LeCheminant

118

Có một sự khác biệt quan trọng giữa hai.

Mọi thứ không được phân bổ newgiống như các loại giá trị trong C # (và mọi người thường nói rằng các đối tượng đó được phân bổ trên ngăn xếp, đây có thể là trường hợp phổ biến / rõ ràng nhất, nhưng không phải lúc nào cũng đúng. Chính xác hơn, các đối tượng được phân bổ mà không sử dụng newlưu trữ tự động thời lượng Mọi thứ được phân bổ vớinew được phân bổ trên heap và một con trỏ tới nó được trả về, giống như các kiểu tham chiếu trong C #.

Bất cứ thứ gì được phân bổ trên ngăn xếp đều phải có kích thước không đổi, được xác định tại thời gian biên dịch (trình biên dịch phải đặt con trỏ ngăn xếp chính xác hoặc nếu đối tượng là thành viên của lớp khác, nó phải điều chỉnh kích thước của lớp khác) . Đó là lý do tại sao mảng trong C # là loại tham chiếu. Chúng phải như vậy, vì với các kiểu tham chiếu, chúng ta có thể quyết định trong thời gian chạy cần bao nhiêu bộ nhớ. Và tương tự áp dụng ở đây. Chỉ các mảng có kích thước không đổi (kích thước có thể được xác định tại thời gian biên dịch) mới có thể được phân bổ với thời gian lưu trữ tự động (trên ngăn xếp). Các mảng có kích thước động phải được phân bổ trên heap, bằng cách gọi new.

(Và đó là nơi tương tự với C # dừng lại)

Bây giờ, mọi thứ được phân bổ trên ngăn xếp đều có thời lượng lưu trữ "tự động" (bạn thực sự có thể khai báo một biến là auto, nhưng đây là mặc định nếu không có loại lưu trữ nào khác được chỉ định để từ khóa không thực sự được sử dụng trong thực tế, nhưng đây là nơi nó được sử dụng đến từ)

Thời lượng lưu trữ tự động có nghĩa là chính xác những gì nó nghe, thời lượng của biến được xử lý tự động. Ngược lại, bất cứ thứ gì được phân bổ trên heap đều phải được bạn xóa bằng tay. Đây là một ví dụ:

void foo() {
  bar b;
  bar* b2 = new bar();
}

Hàm này tạo ra ba giá trị đáng xem xét:

Trên dòng 1, nó khai báo một biến bloại bartrên ngăn xếp (thời lượng tự động).

Trên dòng 2, nó khai báo một barcon trỏ b2trên ngăn xếp (thời lượng tự động) gọi mới, phân bổ một barđối tượng trên heap. (thời lượng động)

Khi hàm trả về, điều sau đây sẽ xảy ra: Đầu tiên, b2đi ra khỏi phạm vi (thứ tự phá hủy luôn trái ngược với thứ tự xây dựng). Nhưng b2chỉ là một con trỏ, vì vậy không có gì xảy ra, bộ nhớ mà nó chiếm giữ chỉ đơn giản là được giải phóng. Và quan trọng, bộ nhớ mà nó trỏ đến ( barví dụ trên heap) KHÔNG được chạm vào. Chỉ con trỏ được giải phóng, vì chỉ con trỏ có thời lượng tự động. Thứ hai, bđi ra khỏi phạm vi, vì vậy nó có thời lượng tự động, nên hàm hủy của nó được gọi và bộ nhớ được giải phóng.

barví dụ về heap? Có lẽ nó vẫn ở đó. Không ai bận tâm để xóa nó, vì vậy chúng tôi đã bị rò rỉ bộ nhớ.

Từ ví dụ này, chúng ta có thể thấy rằng bất cứ thứ gì có thời lượng tự động đều được đảm bảo có hàm hủy của nó được gọi khi nó đi ra khỏi phạm vi. Điều đó hữu ích. Nhưng bất cứ điều gì được phân bổ trên heap đều kéo dài miễn là chúng ta cần nó và có thể có kích thước động, như trong trường hợp mảng. Điều đó cũng hữu ích. Chúng ta có thể sử dụng điều đó để quản lý phân bổ bộ nhớ của chúng tôi. Điều gì sẽ xảy ra nếu lớp Foo cấp phát một số bộ nhớ trên heap trong hàm tạo của nó và xóa bộ nhớ đó trong hàm hủy của nó. Sau đó, chúng ta có thể có được những điều tốt nhất của cả hai thế giới, phân bổ bộ nhớ an toàn được đảm bảo sẽ được giải phóng một lần nữa, nhưng không có giới hạn buộc mọi thứ phải ở trên ngăn xếp.

Và đó là khá chính xác cách mà hầu hết mã C ++ hoạt động. Nhìn vào thư viện tiêu chuẩn std::vectorchẳng hạn. Điều đó thường được phân bổ trên ngăn xếp, nhưng có thể được kích thước động và thay đổi kích thước. Và nó thực hiện điều này bằng cách phân bổ nội bộ bộ nhớ trên heap khi cần thiết. Người dùng của lớp không bao giờ thấy điều này, vì vậy không có cơ hội rò rỉ bộ nhớ hoặc quên làm sạch những gì bạn đã phân bổ.

Nguyên tắc này được gọi là RAII (Thu nhận tài nguyên là khởi tạo) và nó có thể được mở rộng cho bất kỳ tài nguyên nào phải được mua và phát hành. (ổ cắm mạng, tệp, kết nối cơ sở dữ liệu, khóa đồng bộ hóa). Tất cả chúng có thể được mua trong hàm tạo và được phát hành trong hàm hủy, vì vậy bạn được đảm bảo rằng tất cả các tài nguyên bạn có được sẽ được giải phóng trở lại.

Theo nguyên tắc chung, không bao giờ sử dụng mới / xóa trực tiếp từ mã cấp cao của bạn. Luôn luôn bọc nó trong một lớp có thể quản lý bộ nhớ cho bạn và điều đó sẽ đảm bảo nó được giải phóng trở lại. (Vâng, có thể có ngoại lệ cho quy tắc này. Đặc biệt, con trỏ thông minh yêu cầu bạn gọi newtrực tiếp và chuyển con trỏ đến hàm tạo của nó, sau đó tiếp quản và đảm bảo deleteđược gọi chính xác. Nhưng đây vẫn là một quy tắc rất quan trọng )


2
"Mọi thứ không được phân bổ mới đều được đặt trên ngăn xếp" Không phải trong các hệ thống tôi đã làm việc ... thường là dữ liệu toàn cầu (và chưa được xác định) toàn cầu (tĩnh) được đặt trong các phân đoạn của riêng chúng. Ví dụ: các phân đoạn liên kết .data, .bss, v.v .... Pedantic, tôi biết ...
Dan

Tất nhiên, bạn đúng. Tôi đã không thực sự nghĩ về dữ liệu tĩnh. Xấu của tôi, tất nhiên. :)
jalf

2
Tại sao bất cứ thứ gì được phân bổ trên ngăn xếp phải có kích thước không đổi?
dùng541686

Không phải lúc nào cũng vậy , có một vài cách để phá vỡ nó, nhưng trong trường hợp chung thì có, bởi vì nó nằm trên một chồng. Nếu nó ở trên cùng của ngăn xếp, thì có thể thay đổi kích thước của nó, nhưng một khi một thứ khác được đẩy lên trên nó, nó sẽ "bị chặn", bao quanh bởi các vật thể ở hai bên, vì vậy nó thực sự không thể thay đổi kích thước . Vâng, nói rằng nó luôn luôn để có một kích thước cố định là một chút của một đơn giản, nhưng nó truyền tải ý tưởng cơ bản (và tôi sẽ không khuyên bạn nên rối tung lên với các chức năng C mà cho phép bạn quá sáng tạo với đống phân bổ)
jalf

14

Tôi nên sử dụng phương pháp nào?

Điều này gần như không bao giờ được xác định bởi sở thích gõ của bạn mà bởi bối cảnh. Nếu bạn cần giữ đối tượng trên một vài ngăn xếp hoặc nếu nó quá nặng cho ngăn xếp bạn phân bổ nó trên cửa hàng miễn phí. Ngoài ra, vì bạn đang phân bổ một đối tượng, bạn cũng có trách nhiệm giải phóng bộ nhớ. Tra cứu người deletevận hành.

Để giảm bớt gánh nặng sử dụng quản lý cửa hàng miễn phí, mọi người đã phát minh ra những thứ như auto_ptrunique_ptr. Tôi thực sự khuyên bạn nên xem những thứ này. Họ thậm chí có thể giúp đỡ cho các vấn đề đánh máy của bạn ;-)


10

Nếu bạn đang viết bằng C ++ thì có lẽ bạn đang viết cho hiệu suất. Sử dụng cửa hàng mới và cửa hàng miễn phí chậm hơn nhiều so với sử dụng ngăn xếp (đặc biệt là khi sử dụng chủ đề) vì vậy chỉ sử dụng nó khi bạn cần.

Như những người khác đã nói, bạn cần mới khi đối tượng của bạn cần sống bên ngoài phạm vi hàm hoặc đối tượng, đối tượng thực sự lớn hoặc khi bạn không biết kích thước của một mảng tại thời điểm biên dịch.

Ngoài ra, cố gắng tránh sử dụng xóa. Thay vào đó, bọc cái mới của bạn thành một con trỏ thông minh. Hãy để con trỏ thông minh xóa cuộc gọi cho bạn.

Có một số trường hợp con trỏ thông minh không thông minh. Không bao giờ lưu trữ std :: auto_ptr <> bên trong thùng chứa STL. Nó sẽ xóa con trỏ quá sớm vì các hoạt động sao chép bên trong container. Một trường hợp khác là khi bạn có một thùng chứa con trỏ STL thực sự lớn đến các đối tượng. boost :: shared_ptr <> sẽ có hàng tấn tốc độ khi nó tăng số lượng tham chiếu lên xuống. Cách tốt hơn để đi trong trường hợp đó là đặt thùng chứa STL vào một đối tượng khác và cung cấp cho đối tượng đó một hàm hủy sẽ gọi xóa trên mọi con trỏ trong vùng chứa.


10

Câu trả lời ngắn gọn là: nếu bạn là người mới bắt đầu sử dụng C ++, bạn không bao giờ nên sử dụng newhoặc deletechính mình.

Thay vào đó, bạn nên sử dụng các con trỏ thông minh như std::unique_ptrstd::make_unique(hoặc ít thường xuyên hơn, std::shared_ptrstd::make_shared). Bằng cách đó, bạn không phải lo lắng nhiều về rò rỉ bộ nhớ. Và ngay cả khi bạn tiến bộ hơn, cách tốt nhất thường là gói gọn cách tùy chỉnh bạn đang sử dụng newdeletevào một lớp nhỏ (như con trỏ thông minh tùy chỉnh) dành riêng cho các vấn đề về vòng đời của đối tượng.

Tất nhiên, đằng sau hậu trường, những con trỏ thông minh này vẫn đang thực hiện phân bổ động và phân bổ động, do đó, mã sử dụng chúng vẫn có chi phí thời gian chạy liên quan. Các câu trả lời khác ở đây đã đề cập đến những vấn đề này và cách đưa ra quyết định thiết kế khi nào nên sử dụng con trỏ thông minh so với việc chỉ tạo các đối tượng trên ngăn xếp hoặc kết hợp chúng với tư cách là thành viên trực tiếp của một đối tượng, đủ để tôi sẽ không lặp lại chúng. Nhưng tóm tắt điều hành của tôi sẽ là: không sử dụng con trỏ thông minh hoặc phân bổ động cho đến khi một cái gì đó buộc bạn phải làm.


thú vị để xem câu trả lời có thể thay đổi như thế nào khi thời gian trôi qua;)
Wolf


2

Câu trả lời đơn giản là có - new () tạo một đối tượng trên heap (với tác dụng phụ không may là bạn phải quản lý vòng đời của nó (bằng cách gọi rõ ràng là xóa trên nó), trong khi dạng thứ hai tạo ra một đối tượng trong ngăn xếp trong hiện tại phạm vi và đối tượng đó sẽ bị phá hủy khi nó đi ra khỏi phạm vi.


1

Nếu biến của bạn chỉ được sử dụng trong ngữ cảnh của một hàm duy nhất, tốt hơn hết bạn nên sử dụng biến ngăn xếp, nghĩa là Tùy chọn 2. Như những người khác đã nói, bạn không phải quản lý vòng đời của các biến ngăn xếp - chúng được xây dựng và phá hủy tự động. Ngoài ra, việc phân bổ / giải quyết một biến trên heap là chậm khi so sánh. Nếu chức năng của bạn được gọi thường xuyên đủ, bạn sẽ thấy một sự cải thiện hiệu suất rất lớn nếu sử dụng biến ngăn xếp so với biến heap.

Điều đó nói rằng, có một vài trường hợp rõ ràng trong đó các biến stack là không đủ.

Nếu biến ngăn xếp có dung lượng bộ nhớ lớn, thì bạn có nguy cơ tràn ngăn xếp. Theo mặc định, kích thước ngăn xếp của mỗi luồng là 1 MB trên Windows. Không chắc là bạn sẽ tạo biến ngăn xếp có kích thước 1 MB, nhưng bạn phải nhớ rằng việc sử dụng ngăn xếp là tích lũy. Nếu hàm của bạn gọi một hàm gọi hàm khác gọi hàm khác là ..., các biến ngăn xếp trong tất cả các hàm này chiếm không gian trên cùng một ngăn xếp. Các hàm đệ quy có thể chạy vào vấn đề này một cách nhanh chóng, tùy thuộc vào mức độ đệ quy sâu như thế nào. Nếu đây là một vấn đề, bạn có thể tăng kích thước của ngăn xếp (không được khuyến nghị) hoặc phân bổ biến trên heap bằng toán tử mới (được khuyến nghị).

Một điều kiện khác, nhiều khả năng là biến của bạn cần "sống" ngoài phạm vi chức năng của bạn. Trong trường hợp này, bạn sẽ phân bổ biến trên heap để có thể đạt được bên ngoài phạm vi của bất kỳ hàm đã cho nào.


1

Bạn đang chuyển myClass ra khỏi một chức năng, hoặc hy vọng nó tồn tại bên ngoài chức năng đó? Như một số người khác nói, tất cả chỉ là về phạm vi khi bạn không phân bổ trên heap. Khi bạn rời khỏi chức năng, nó sẽ biến mất (cuối cùng). Một trong những sai lầm kinh điển của người mới bắt đầu là cố gắng tạo một đối tượng cục bộ của một số lớp trong một hàm và trả về nó mà không phân bổ nó trên heap. Tôi có thể nhớ gỡ lỗi kiểu này trong những ngày đầu làm c ++.


0

Phương thức thứ hai tạo ra thể hiện trên ngăn xếp, cùng với những thứ như một thứ được khai báo intvà danh sách các tham số được truyền vào hàm.

Phương thức đầu tiên nhường chỗ cho một con trỏ trên ngăn xếp, mà bạn đã đặt vào vị trí trong bộ nhớ nơi một cái mới MyClassđã được phân bổ trên heap - hoặc cửa hàng miễn phí.

Phương thức đầu tiên cũng yêu cầu deletebạn tạo ra những gì bạn tạo ra new, trong khi đó trong phương thức thứ hai, lớp sẽ tự động bị hủy và giải phóng khi nó nằm ngoài phạm vi (thường là dấu ngoặc đóng tiếp theo).


-1

Câu trả lời ngắn gọn là có, từ khóa "mới" cực kỳ quan trọng vì khi bạn sử dụng nó, dữ liệu đối tượng được lưu trữ trên heap trái ngược với ngăn xếp, điều quan trọng nhất!

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.