Sử dụng stack và heap đúng cách trong C ++?


122

Tôi đã lập trình được một thời gian nhưng chủ yếu là Java và C #. Tôi thực sự chưa bao giờ phải tự mình quản lý bộ nhớ. Gần đây tôi đã bắt đầu lập trình trong C ++ và tôi hơi bối rối khi nào nên lưu trữ mọi thứ trên ngăn xếp và khi nào nên lưu trữ chúng trên đống.

Tôi hiểu rằng các biến được truy cập rất thường xuyên nên được lưu trữ trên ngăn xếp và các đối tượng, các biến hiếm khi được sử dụng và các cấu trúc dữ liệu lớn nên được lưu trữ trên heap. Điều này đúng hay tôi không đúng?


Câu trả lời:


242

Không, sự khác biệt giữa stack và heap không phải là hiệu suất. Đó là tuổi thọ: bất kỳ biến cục bộ nào bên trong hàm (bất cứ thứ gì bạn không malloc () hoặc mới) đều sống trên ngăn xếp. Nó biến mất khi bạn trở về từ chức năng. Nếu bạn muốn một cái gì đó sống lâu hơn chức năng đã khai báo nó, bạn phải phân bổ nó trên heap.

class Thingy;

Thingy* foo( ) 
{
  int a; // this int lives on the stack
  Thingy B; // this thingy lives on the stack and will be deleted when we return from foo
  Thingy *pointerToB = &B; // this points to an address on the stack
  Thingy *pointerToC = new Thingy(); // this makes a Thingy on the heap.
                                     // pointerToC contains its address.

  // this is safe: C lives on the heap and outlives foo().
  // Whoever you pass this to must remember to delete it!
  return pointerToC;

  // this is NOT SAFE: B lives on the stack and will be deleted when foo() returns. 
  // whoever uses this returned pointer will probably cause a crash!
  return pointerToB;
}

Để hiểu rõ hơn về ngăn xếp là gì, hãy đến từ đầu kia - thay vì cố gắng hiểu ngăn xếp đó làm gì theo ngôn ngữ cấp cao, hãy tra cứu "ngăn xếp cuộc gọi" và "quy ước gọi" và xem những gì Máy thực sự làm khi bạn gọi một chức năng. Bộ nhớ máy tính chỉ là một chuỗi các địa chỉ; "Heap" và "stack" là những phát minh của trình biên dịch.


7
Sẽ an toàn khi thêm thông tin có kích thước thay đổi thường đi vào đống. Các ngoại lệ duy nhất tôi biết là VLA trong C99 (có hỗ trợ hạn chế) và hàm alloca () thường bị hiểu lầm ngay cả bởi các lập trình viên C.
Dan Olson

10
Giải thích tốt, mặc dù trong một kịch bản đa luồng với sự phân bổ và / hoặc thỏa thuận thường xuyên, heap một điểm gây tranh cãi, do đó ảnh hưởng đến hiệu suất. Tuy nhiên, Phạm vi gần như luôn luôn là yếu tố quyết định.
peterchen

18
Chắc chắn, và new / malloc () tự nó hoạt động chậm và stack có nhiều khả năng bị dcache hơn là một dòng heap tùy ý. Đây là những cân nhắc thực sự, nhưng thường là thứ yếu cho câu hỏi về tuổi thọ.
Crashworks

1
Có đúng không "Bộ nhớ máy tính chỉ là một chuỗi các địa chỉ;" heap "và" stack "là những phát minh của trình biên dịch" ?? Tôi đã đọc ở nhiều nơi ngăn xếp đó là một vùng đặc biệt trong bộ nhớ máy tính của chúng tôi.
Vineeth Chitteti

2
@kai Đó là một cách để hình dung nó, nhưng không nhất thiết phải nói thật. HĐH có trách nhiệm phân bổ ngăn xếp và đống ứng dụng. Trình biên dịch cũng chịu trách nhiệm, nhưng chủ yếu nó dựa vào HĐH để làm việc đó. Stack bị giới hạn, và heap thì không. Điều này là do cách HĐH xử lý việc sắp xếp các địa chỉ bộ nhớ này thành một thứ có cấu trúc chặt chẽ hơn để nhiều ứng dụng có thể chạy trên cùng một hệ thống. Heap và stack không phải là những cái duy nhất, nhưng chúng thường là hai thứ duy nhất mà hầu hết các nhà phát triển quan tâm.
tsturzl

42

Tôi sẽ nói:

Lưu trữ nó trên ngăn xếp, nếu bạn CÓ THỂ.

Lưu trữ nó trên đống, nếu bạn CẦN.

Do đó, thích ngăn xếp để đống. Một số lý do có thể khiến bạn không thể lưu trữ thứ gì đó trên ngăn xếp là:

  • Nó quá lớn - trên các chương trình đa luồng trên HĐH 32 bit, ngăn xếp có kích thước nhỏ và cố định (ít nhất là tại thời điểm tạo luồng) (thường chỉ vài megs. Điều này để bạn có thể tạo nhiều luồng mà không làm cạn kiệt địa chỉ Đối với các chương trình 64 bit hoặc các chương trình đơn luồng (dù sao cũng là Linux), đây không phải là vấn đề chính. Trong Linux 32 bit, các chương trình đơn luồng thường sử dụng các ngăn xếp động có thể tiếp tục phát triển cho đến khi chúng đạt đến đỉnh.
  • Bạn cần truy cập nó bên ngoài phạm vi của khung ngăn xếp ban đầu - đây thực sự là lý do chính.

Có thể, với các trình biên dịch hợp lý, để phân bổ các đối tượng kích thước không cố định trên heap (thường là các mảng có kích thước không được biết tại thời điểm biên dịch).


1
Bất cứ điều gì nhiều hơn một vài KB thường được đưa vào heap. Tôi không biết chi tiết cụ thể nhưng tôi không nhớ là đã từng làm việc với một ngăn xếp "vài megs".
Dan Olson

2
Đó là điều mà tôi sẽ không quan tâm đến người dùng ngay từ đầu. Đối với người dùng, vectơ và danh sách dường như được phân bổ trên ngăn xếp ngay cả khi STL không lưu trữ nội dung trên heap. Câu hỏi dường như nhiều hơn trên dòng quyết định khi nào rõ ràng gọi mới / xóa.
David Rodríguez - dribeas

1
Dan: Tôi đã đặt 2 hợp đồng biểu diễn (Có, G như trong GIGS) lên ngăn xếp dưới linux 32 bit. Giới hạn ngăn xếp phụ thuộc vào hệ điều hành.
Mr.Ree

6
mrree: Ngăn xếp Nintendo DS là 16 kilobyte. Một số giới hạn ngăn xếp phụ thuộc vào phần cứng.
Kiến

Ant: Tất cả các ngăn xếp đều phụ thuộc vào phần cứng, phụ thuộc hệ điều hành và phụ thuộc vào trình biên dịch.
Viliami

24

Nó tinh tế hơn những câu trả lời khác cho thấy. Không có sự phân chia tuyệt đối giữa dữ liệu trên ngăn xếp và dữ liệu trên heap dựa trên cách bạn khai báo nó. Ví dụ:

std::vector<int> v(10);

Trong phần thân của hàm, khai báo một vector(mảng động) gồm mười số nguyên trên ngăn xếp. Nhưng lưu trữ được quản lý bởi vectorkhông phải trên ngăn xếp.

À, nhưng (các câu trả lời khác cho thấy) thời gian lưu trữ bị giới hạn bởi thời gian tồn tại của vectorchính nó, ở đây là dựa trên ngăn xếp, vì vậy nó không có gì khác biệt khi thực hiện - chúng ta chỉ có thể coi nó là một đối tượng dựa trên ngăn xếp với giá trị ngữ nghĩa.

Không phải vậy. Giả sử hàm là:

void GetSomeNumbers(std::vector<int> &result)
{
    std::vector<int> v(10);

    // fill v with numbers

    result.swap(v);
}

Vì vậy, bất cứ thứ gì có swaphàm (và bất kỳ loại giá trị phức tạp nào cũng cần có) đều có thể đóng vai trò là một loại tham chiếu có thể đảo ngược đối với một số dữ liệu heap, trong một hệ thống đảm bảo một chủ sở hữu duy nhất của dữ liệu đó.

Do đó, cách tiếp cận C ++ hiện đại là không bao giờ lưu trữ địa chỉ của dữ liệu heap trong các biến con trỏ cục bộ. Tất cả các phân bổ heap phải được ẩn trong các lớp.

Nếu bạn làm điều đó, bạn có thể nghĩ về tất cả các biến trong chương trình của mình như thể chúng là các loại giá trị đơn giản và quên hoàn toàn heap (ngoại trừ khi viết một lớp bao bọc giống như giá trị mới cho một số dữ liệu heap, điều này không bình thường) .

Bạn chỉ cần giữ lại một chút kiến ​​thức đặc biệt để giúp bạn tối ưu hóa: khi có thể, thay vì gán một biến cho một biến khác như thế này:

a = b;

trao đổi chúng như thế này:

a.swap(b);

bởi vì nó nhanh hơn nhiều và nó không ném ngoại lệ. Yêu cầu duy nhất là bạn không cần btiếp tục giữ cùng một giá trị ( athay vào đó sẽ nhận được giá trị thay vào đó, sẽ được chuyển vào a = b).

Nhược điểm là cách tiếp cận này buộc bạn phải trả về giá trị từ các hàm thông qua các tham số đầu ra thay vì giá trị trả về thực tế. Nhưng họ đang sửa nó trong C ++ 0x với các tham chiếu rvalue .

Trong những tình huống phức tạp nhất, bạn sẽ đưa ý tưởng này đến mức cực đoan chung và sử dụng một lớp con trỏ thông minh như shared_ptrđã có trong tr1. (Mặc dù tôi cho rằng nếu bạn có vẻ cần nó, bạn có thể đã di chuyển ra ngoài điểm áp dụng ngọt ngào của Standard C ++.)


6

Bạn cũng sẽ lưu trữ một mục trên heap nếu nó cần được sử dụng ngoài phạm vi của chức năng mà nó được tạo. Một thành ngữ được sử dụng với các đối tượng ngăn xếp được gọi là RAII - điều này liên quan đến việc sử dụng đối tượng dựa trên ngăn xếp làm trình bao bọc cho tài nguyên, khi đối tượng bị phá hủy, tài nguyên sẽ được dọn sạch. Các đối tượng dựa trên ngăn xếp sẽ dễ dàng theo dõi hơn khi bạn có thể đưa ra các ngoại lệ - bạn không cần phải lo lắng về việc xóa một đối tượng dựa trên đống trong một trình xử lý ngoại lệ. Đây là lý do tại sao các con trỏ thô thường không được sử dụng trong C ++ hiện đại, bạn sẽ sử dụng một con trỏ thông minh có thể là một trình bao bọc dựa trên ngăn xếp cho một con trỏ thô đến một đối tượng dựa trên heap.


5

Để thêm vào các câu trả lời khác, nó cũng có thể là về hiệu suất, ít nhất là một chút. Không phải là bạn nên lo lắng về điều này trừ khi nó phù hợp với bạn, nhưng:

Phân bổ trong heap đòi hỏi phải tìm một khối bộ nhớ theo dõi, đây không phải là hoạt động liên tục (và mất một số chu kỳ và chi phí chung). Điều này có thể trở nên chậm hơn khi bộ nhớ bị phân mảnh và / hoặc bạn đang tiến gần đến việc sử dụng 100% không gian địa chỉ của mình. Mặt khác, phân bổ ngăn xếp là các hoạt động không đổi, về cơ bản là "miễn phí".

Một điều khác cần xem xét (một lần nữa, thực sự chỉ quan trọng nếu nó trở thành một vấn đề) là thông thường kích thước ngăn xếp được cố định và có thể thấp hơn nhiều so với kích thước heap. Vì vậy, nếu bạn đang phân bổ các đối tượng lớn hoặc nhiều đối tượng nhỏ, có lẽ bạn muốn sử dụng heap; nếu bạn hết dung lượng ngăn xếp, bộ thực thi sẽ ném ngoại lệ trang web. Không thường là một vấn đề lớn, nhưng một điều khác để xem xét.


Cả heap & stack đều là bộ nhớ ảo phân trang. Thời gian tìm kiếm heap rất nhanh so với những gì nó cần để lập bản đồ trong bộ nhớ mới. Trong Linux 32 bit, tôi có thể đặt> 2gig vào ngăn xếp của mình. Trong máy Mac, tôi nghĩ rằng ngăn xếp này bị giới hạn ở mức 65Meg.
Mr.Ree

3

Stack hiệu quả hơn và dễ dàng hơn để quản lý dữ liệu phạm vi.

Nhưng heap nên được sử dụng cho bất cứ thứ gì lớn hơn vài KB (thật dễ dàng trong C ++, chỉ cần tạo một boost::scoped_ptrngăn xếp để giữ một con trỏ tới bộ nhớ được phân bổ).

Hãy xem xét một thuật toán đệ quy tiếp tục gọi vào chính nó. Rất khó để giới hạn và hoặc đoán tổng mức sử dụng ngăn xếp! Trong khi trên heap, bộ cấp phát ( malloc()hoặc new) có thể chỉ ra bộ nhớ ngoài bằng cách quay lại NULLhoặc throwing.

Nguồn : Linux Kernel có stack không lớn hơn 8KB!


Để tham khảo các độc giả khác: (A) "Nên" ở đây hoàn toàn là ý kiến ​​cá nhân của người dùng, được rút ra từ tối đa 1 trích dẫn và 1 kịch bản mà nhiều người dùng không thể gặp phải (đệ quy). Ngoài ra, (B) thư viện tiêu chuẩn cung cấp std::unique_ptr, nên được ưu tiên cho bất kỳ thư viện bên ngoài nào như Boost (mặc dù điều đó cung cấp mọi thứ theo tiêu chuẩn theo thời gian).
gạch dưới


1

Lựa chọn phân bổ trên heap hay trên stack là một lựa chọn dành cho bạn, tùy thuộc vào cách biến của bạn được phân bổ. Nếu bạn phân bổ một cái gì đó một cách linh hoạt, bằng cách sử dụng một cuộc gọi "mới", bạn đang phân bổ từ heap. Nếu bạn phân bổ một cái gì đó như một biến toàn cục hoặc như một tham số trong hàm thì nó được phân bổ trên ngăn xếp.


4
Tôi nghi ngờ anh ta hỏi khi nào nên bỏ đồ vào đống, chứ không phải thế nào.
Steve Rowe

0

Theo tôi có hai yếu tố quyết định

1) Scope of variable
2) Performance.

Tôi muốn sử dụng stack trong hầu hết các trường hợp nhưng nếu bạn cần truy cập vào phạm vi ngoài phạm vi, bạn có thể sử dụng heap.

Để tăng cường hiệu suất trong khi sử dụng heap, bạn cũng có thể sử dụng chức năng để tạo khối heap và điều đó có thể giúp đạt được hiệu suất thay vì phân bổ từng biến ở vị trí bộ nhớ khác nhau.


0

có lẽ điều này đã được trả lời khá tốt. Tôi muốn chỉ cho bạn loạt bài viết dưới đây để hiểu sâu hơn về các chi tiết cấp thấp. Alex Darby có một loạt các bài báo, nơi anh ta dẫn bạn đi qua với một trình sửa lỗi. Đây là Phần 3 về Stack. http://www.altdevblogaday.com/2011/12/14/cc-low-level-curemony-part-3-the-stack/


Liên kết dường như đã chết, nhưng việc kiểm tra Internet Archive Wayback Machine chỉ ra rằng nó chỉ nói về ngăn xếp và do đó không có gì để trả lời câu hỏi cụ thể ở đây về stack so với heap . -1
gạch dưới
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.