Mảng, đống và ngăn xếp và các loại giá trị


134
int[] myIntegers;
myIntegers = new int[100];

Trong đoạn mã trên, int mới [100] có tạo ra mảng trên heap không? Từ những gì tôi đã đọc trên CLR qua c #, câu trả lời là có. Nhưng những gì tôi không thể hiểu, là những gì xảy ra với int thực tế bên trong mảng. Vì chúng là các loại giá trị, tôi đoán rằng chúng sẽ phải được đóng hộp, ví dụ như tôi có thể chuyển các myIntegers của mình sang các phần khác của chương trình và nó sẽ làm lộn xộn ngăn xếp nếu chúng luôn ở trên đó . Hoặc là tôi sai? Tôi đoán họ sẽ được đóng hộp và sẽ sống trong đống cho đến khi mảng tồn tại.

Câu trả lời:


289

Mảng của bạn được phân bổ trên heap và ints không được đóng hộp.

Nguồn gốc của sự nhầm lẫn của bạn có thể là do mọi người đã nói rằng các loại tham chiếu được phân bổ trên heap và các loại giá trị được phân bổ trên ngăn xếp. Đây không phải là một đại diện hoàn toàn chính xác.

Tất cả các biến và tham số cục bộ được phân bổ trên ngăn xếp. Điều này bao gồm cả loại giá trị và loại tham chiếu. Sự khác biệt giữa hai chỉ là những gì được lưu trữ trong biến. Không có gì đáng ngạc nhiên, đối với một loại giá trị, giá trị của loại được lưu trữ trực tiếp trong biến và đối với loại tham chiếu, giá trị của loại được lưu trữ trên heap và tham chiếu đến giá trị này là giá trị được lưu trữ trong biến.

Điều tương tự giữ cho các lĩnh vực. Khi bộ nhớ được cấp phát cho một thể hiện của loại tổng hợp (a classhoặc a struct), nó phải bao gồm lưu trữ cho mỗi trường đối tượng của nó. Đối với các trường loại tham chiếu, bộ lưu trữ này chỉ giữ một tham chiếu đến giá trị, chính nó sẽ được phân bổ trên heap sau này. Đối với các trường loại giá trị, bộ lưu trữ này giữ giá trị thực.

Vì vậy, đưa ra các loại sau:

class RefType{
    public int    I;
    public string S;
    public long   L;
}

struct ValType{
    public int    I;
    public string S;
    public long   L;
}

Các giá trị của mỗi loại này sẽ cần 16 byte bộ nhớ (giả sử kích thước từ 32 bit). Trường Itrong mỗi trường hợp mất 4 byte để lưu trữ giá trị của nó, trường Smất 4 byte để lưu trữ tham chiếu của nó và trường Lmất 8 byte để lưu trữ giá trị của nó. Vì vậy, bộ nhớ cho giá trị của cả hai RefTypeValTypetrông như thế này:

 0 ┌───────────────────
   Tôi
 4 ├───────────────────
   │ S
 8 ├───────────────────
   │ L
   │ │
16 └───────────────────

Bây giờ nếu bạn có ba biến cục bộ trong một hàm, các loại RefType, ValTypeint[], như thế này:

RefType refType;
ValType valType;
int[]   intArray;

sau đó ngăn xếp của bạn có thể trông như thế này:

 0 ┌───────────────────
   │ refType
 4 ├───────────────────
   │ valType │
   │ │
   │ │
   │ │
20 ├───────────────────
   IntArray
24 ───────────────────

Nếu bạn đã gán giá trị cho các biến cục bộ này, như vậy:

refType = new RefType();
refType.I = 100;
refType.S = "refType.S";
refType.L = 0x0123456789ABCDEF;

valType = new ValType();
valType.I = 200;
valType.S = "valType.S";
valType.L = 0x0011223344556677;

intArray = new int[4];
intArray[0] = 300;
intArray[1] = 301;
intArray[2] = 302;
intArray[3] = 303;

Sau đó, ngăn xếp của bạn có thể trông giống như thế này:

 0 ┌───────────────────
   0x4A963B68 - địa chỉ heap của `refType`
 4 ├───────────────────
   200 │ - giá trị của `valType.I`
   │ 0x4A984C10 - địa chỉ heap của `valType.S`
   0x44556677 - 32 bit thấp của `valType.L`
   0x00112233 - 32 bit cao của `valType.L`
20 ├───────────────────
   0x4AA4C288 - địa chỉ heap của `intArray`
24 ───────────────────

Bộ nhớ tại địa chỉ 0x4A963B68(giá trị của refType) sẽ giống như:

 0 ┌───────────────────
   │ 100 │ - giá trị của `refType.I`
 4 ├───────────────────
   0x4A984D88 - địa chỉ heap của `refType.S`
 8 ├───────────────────
   │ 0x89ABCDEF - 32 bit của `refType.L`
   0x01234567 - cao 32 bit của `refType.L`
16 └───────────────────

Bộ nhớ tại địa chỉ 0x4AA4C288(giá trị của intArray) sẽ giống như:

 0 ┌───────────────────
   4 │ - chiều dài của mảng
 4 ├───────────────────
   │ 300 │ - `intArray [0]`
 8 ├───────────────────
   │ 301 │ - `intArray [1]`
12 ├───────────────────
   │ 302 │ - `intArray [2]`
16 ├───────────────────
   │ 303 │ - `intArray [3]`
20 └───────────────────

Bây giờ, nếu bạn chuyển intArrayđến một hàm khác, giá trị được đẩy lên ngăn xếp sẽ là 0x4AA4C288địa chỉ của mảng chứ không phải là bản sao của mảng.


52
Tôi lưu ý rằng tuyên bố rằng tất cả các biến cục bộ được lưu trữ trên ngăn xếp là không chính xác. Các biến cục bộ là các biến ngoài của hàm ẩn danh được lưu trữ trên heap. Các biến cục bộ của các khối lặp được lưu trữ trên heap. Các biến cục bộ của khối async được lưu trữ trên heap. Các biến cục bộ được đăng ký được lưu trữ trên cả stack và heap. Các biến cục bộ được tách biệt được lưu trữ trên cả stack và heap.
Eric Lippert

5
LOL, luôn luôn là người chọn nit, ông Lippert. :) Tôi cảm thấy buộc phải chỉ ra rằng ngoại trừ hai trường hợp sau của bạn, cái gọi là "người địa phương" không còn là người địa phương tại thời điểm biên dịch. Việc thực hiện nâng chúng lên trạng thái của các thành viên trong lớp, đó là lý do duy nhất chúng được lưu trữ trên heap. Vì vậy, nó chỉ là một chi tiết thực hiện (cười thầm). Tất nhiên, lưu trữ đăng ký là một chi tiết triển khai ở cấp độ thấp hơn và elision không được tính.
P Daddy

3
Tất nhiên, toàn bộ bài viết của tôi là chi tiết triển khai, nhưng, như tôi chắc chắn bạn nhận ra, tất cả đều cố gắng tách các khái niệm về biếngiá trị . Một biến (gọi nó là cục bộ, trường, tham số, bất cứ thứ gì) có thể được lưu trữ trên ngăn xếp, heap hoặc một số nơi được xác định thực hiện khác, nhưng đó không thực sự là điều quan trọng. Điều quan trọng, là liệu biến đó trực tiếp lưu trữ giá trị mà nó đại diện hay chỉ đơn giản là một tham chiếu đến giá trị đó, được lưu trữ ở nơi khác. Điều này quan trọng bởi vì nó ảnh hưởng đến ngữ nghĩa sao chép: liệu sao chép biến đó sao chép giá trị hoặc địa chỉ của nó.
P Daddy

16
Rõ ràng bạn có một ý tưởng khác về ý nghĩa của "biến cục bộ" so với tôi. Bạn dường như tin rằng một "biến cục bộ" được đặc trưng bởi các chi tiết thực hiện . Niềm tin này không được chứng minh bằng bất cứ điều gì tôi biết trong đặc tả C #. Thực tế, một biến cục bộ là một biến được khai báo bên trong một khối có tên chỉ nằm trong phạm vi không gian khai báo được liên kết với khối đó. Tôi đảm bảo với bạn rằng các biến cục bộ, như một chi tiết triển khai, được kéo lên các trường của một lớp đóng, vẫn là các biến cục bộ theo các quy tắc của C #.
Eric Lippert

15
Điều đó nói rằng, tất nhiên câu trả lời của bạn nói chung là tuyệt vời; điểm mà các giá trị khác nhau về mặt khái niệm với các biến là một điểm cần được thực hiện thường xuyên và càng lớn càng tốt, vì nó là cơ bản. Nhưng rất nhiều người tin rằng những huyền thoại kỳ lạ nhất về họ! Vì vậy, tốt cho bạn để chiến đấu chiến đấu tốt.
Eric Lippert

23

Có mảng sẽ nằm trên heap.

Các int bên trong mảng sẽ không được đóng hộp. Chỉ vì một loại giá trị tồn tại trên heap, không nhất thiết có nghĩa là nó sẽ được đóng hộp. Quyền anh sẽ chỉ xảy ra khi một loại giá trị, chẳng hạn như int, được gán cho tham chiếu của đối tượng loại.

Ví dụ

Không hộp:

int i = 42;
myIntegers[0] = 42;

Hộp:

object i = 42;
object[] arr = new object[10];  // no boxing here 
arr[0] = 42;

Bạn cũng có thể muốn xem bài viết của Eric về chủ đề này:


1
Nhưng tôi không hiểu. Các loại giá trị không nên được phân bổ trên ngăn xếp? Hoặc cả hai loại giá trị và tham chiếu có thể được phân bổ cả trên heap hoặc stack và chỉ là chúng thường chỉ được lưu trữ ở nơi này hay nơi khác?
nuốt chửng elysium

4
@Jorge, một loại giá trị không có trình bao bọc / vùng chứa tham chiếu sẽ nằm trên ngăn xếp. Tuy nhiên, một khi nó được sử dụng trong một loại thùng chứa tham chiếu, nó sẽ sống trong đống. Mảng là một kiểu tham chiếu và do đó bộ nhớ cho int phải nằm trong heap.
JaredPar

2
@Jorge: các loại tham chiếu chỉ sống trong heap, không bao giờ trên stack. Ngược lại, không thể (trong mã xác minh) không thể lưu trữ một con trỏ đến vị trí ngăn xếp vào một đối tượng của kiểu tham chiếu.
Anton Tykhyy

1
Tôi nghĩ rằng bạn có nghĩa là gán i cho mảng [0]. Việc gán liên tục vẫn sẽ gây ra quyền anh của "42", nhưng bạn đã tạo i, vì vậy bạn cũng có thể sử dụng nó ;-)
Marcus Griep

@AntonTykhyy: Không có quy tắc nào tôi biết khi nói CLR không thể phân tích thoát. Nếu nó phát hiện ra rằng một đối tượng sẽ không bao giờ được tham chiếu trong suốt vòng đời của hàm đã tạo ra nó, thì nó hoàn toàn hợp pháp - và thậm chí tốt hơn - để xây dựng đối tượng trên ngăn xếp, cho dù đó là loại giá trị hay không. "Loại giá trị" và "loại tham chiếu" về cơ bản mô tả những gì trong bộ nhớ được đưa lên bởi biến, không phải là quy tắc cứng và nhanh về nơi đối tượng sống.
cHao

21

Để hiểu những gì đang xảy ra, đây là một số sự thật:

  • Đối tượng luôn được phân bổ trên đống.
  • Heap chỉ chứa các đối tượng.
  • Các loại giá trị được phân bổ trên ngăn xếp hoặc một phần của đối tượng trên heap.
  • Một mảng là một đối tượng.
  • Một mảng chỉ có thể chứa các loại giá trị.
  • Một tham chiếu đối tượng là một loại giá trị.

Vì vậy, nếu bạn có một mảng các số nguyên, mảng được phân bổ trên heap và các số nguyên mà nó chứa là một phần của đối tượng mảng trên heap. Các số nguyên nằm trong đối tượng mảng trên heap, không phải là các đối tượng riêng biệt, vì vậy chúng không được đóng hộp.

Nếu bạn có một chuỗi các chuỗi, nó thực sự là một mảng các tham chiếu chuỗi. Vì các tham chiếu là các loại giá trị, chúng sẽ là một phần của đối tượng mảng trên heap. Nếu bạn đặt một đối tượng chuỗi trong mảng, bạn thực sự đặt tham chiếu đến đối tượng chuỗi trong mảng và chuỗi là một đối tượng riêng biệt trên heap.


Có, các tham chiếu hoạt động chính xác như các loại giá trị nhưng tôi nhận thấy chúng thường không được gọi theo cách đó hoặc được bao gồm trong các loại giá trị. Xem ví dụ (nhưng có nhiều hơn như thế này) msdn.microsoft.com/en-us/l Library / s1ax56ch.aspx
Henk Holterman

@Henk: Vâng, bạn đúng rằng các tham chiếu không được liệt kê trong số các biến loại giá trị, nhưng khi nói đến cách phân bổ bộ nhớ cho chúng, chúng thuộc mọi loại giá trị tôn trọng và rất hữu ích để nhận ra rằng để hiểu cách phân bổ bộ nhớ Tất cả phù hợp với nhau. :)
Guffa

Tôi nghi ngờ điểm thứ 5, "Một mảng chỉ có thể chứa các loại giá trị." Còn mảng chuỗi thì sao? chuỗi [] chuỗi = chuỗi mới [4];
Sunil Purushothaman

9

Tôi nghĩ rằng cốt lõi của câu hỏi của bạn nằm ở một sự hiểu lầm về các loại tham chiếu và giá trị. Đây là điều mà có lẽ mọi nhà phát triển .NET và Java phải vật lộn với.

Một mảng chỉ là một danh sách các giá trị. Nếu đó là một mảng của kiểu tham chiếu (giả sử a string[]) thì mảng đó là danh sách các tham chiếu đến các stringđối tượng khác nhau trên heap, vì tham chiếu là giá trị của kiểu tham chiếu. Trong nội bộ, các tham chiếu này được thực hiện như con trỏ đến một địa chỉ trong bộ nhớ. Nếu bạn muốn hình dung điều này, một mảng như vậy sẽ trông như thế này trong bộ nhớ (trên heap):

[ 00000000, 00000000, 00000000, F8AB56AA ]

Đây là một mảng stringchứa 4 tham chiếu đến stringcác đối tượng trên heap (các số ở đây là thập lục phân). Hiện tại, chỉ cuối cùng stringthực sự trỏ đến bất cứ điều gì (bộ nhớ được khởi tạo cho tất cả các số 0 khi được phân bổ), về cơ bản mảng này sẽ là kết quả của mã này trong C #:

string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR

Mảng trên sẽ là một chương trình 32 bit. Trong một chương trình 64 bit, các tham chiếu sẽ lớn gấp đôi ( F8AB56AAsẽ là 00000000F8AB56AA).

Nếu bạn có một mảng các loại giá trị (giả sử int[]) thì mảng đó là một danh sách các số nguyên, vì giá trị của một loại giá trị chính giá trị (do đó là tên). Hình dung của một mảng như vậy sẽ là thế này:

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Đây là một mảng gồm 4 số nguyên, trong đó chỉ có int thứ hai được gán một giá trị (tới 1174352571, là biểu diễn thập phân của số thập lục phân đó) và phần còn lại của các số nguyên sẽ là 0 (như tôi đã nói, bộ nhớ được khởi tạo bằng 0 và 00000000 theo hệ thập lục phân là 0 ở dạng thập phân). Mã tạo ra mảng này sẽ là:

 int[] integers = new int[4];
 integers[1] = 1174352571; // integers[1] = 0x45FF32BB would be valid too

Đây int[]mảng cũng sẽ được lưu trữ trên heap.

Một ví dụ khác, bộ nhớ của một short[4]mảng sẽ trông như thế này:

[ 0000, 0000, 0000, 0000 ]

giá trị của a shortlà số 2 byte.

Khi một loại giá trị được lưu trữ, chỉ là một chi tiết triển khai như Eric Lippert giải thích rất rõ ở đây , không phải là sự khác biệt giữa các loại giá trị và loại tham chiếu (đó là sự khác biệt trong hành vi).

Khi bạn truyền một cái gì đó cho một phương thức (có thể là loại tham chiếu hoặc loại giá trị) thì một bản sao của giá trị của loại thực sự được truyền cho phương thức. Trong trường hợp của kiểu tham chiếu, giá trị là tham chiếu (nghĩ về điều này như một con trỏ tới một phần bộ nhớ, mặc dù đó cũng là một chi tiết thực hiện) và trong trường hợp của một loại giá trị, giá trị là chính nó.

// Calling this method creates a copy of the *reference* to the string
// and a copy of the int itself, so copies of the *values*
void SomeMethod(string s, int i){}

Quyền anh chỉ xảy ra nếu bạn chuyển đổi loại giá trị thành loại tham chiếu. Hộp mã này:

object o = 5;

Tôi tin rằng "một chi tiết triển khai" phải là cỡ chữ: 50px. ;)
sisve

2

Đây là những hình ảnh minh họa mô tả câu trả lời trên của @P Daddy

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

Và tôi đã minh họa các nội dung tương ứng trong phong cách của tôi.

nhập mô tả hình ảnh ở đây


@P Bố ơi mình làm tranh minh họa. Vui lòng kiểm tra nếu có phần sai. Và tôi có một số câu hỏi bổ sung. 1. Khi tôi tạo mảng 4 chiều dài int, thông tin độ dài (4) cũng luôn được lưu trong bộ nhớ?
Công viên YoungMin

2. Trên hình minh họa thứ hai, địa chỉ mảng được sao chép được lưu trữ ở đâu? Có phải cùng một khu vực ngăn xếp trong đó địa chỉ intArray được lưu trữ không? Có phải là stack khác nhưng cùng loại stack? Có phải là loại ngăn xếp khác nhau? 3. 32 bit thấp / 32 bit cao có nghĩa là gì? 4. Giá trị trả về là gì khi tôi phân bổ loại giá trị (trong ví dụ này, cấu trúc) trên ngăn xếp bằng cách sử dụng từ khóa mới? Có phải đó cũng là địa chỉ? Khi tôi kiểm tra bằng câu lệnh này Console.WriteLine (valType), nó sẽ hiển thị tên đủ điều kiện như đối tượng như ConsoleApp.ValType.
Công viên YoungMin

5. valType.I = 200; Có phải câu lệnh này có nghĩa là tôi lấy địa chỉ của valType, bởi địa chỉ này tôi truy cập vào I và ngay tại đó tôi lưu trữ 200 nhưng "trên ngăn xếp".
Công viên YoungMin

1

Một mảng các số nguyên được phân bổ trên heap, không hơn, không kém. myIntegers tham chiếu đến phần bắt đầu của phần ints được phân bổ. Tham chiếu đó nằm trên ngăn xếp.

Nếu bạn có một mảng các đối tượng kiểu tham chiếu, như kiểu Object, myObjects [], nằm trên ngăn xếp, sẽ tham chiếu đến bó các giá trị tham chiếu đến chính các đối tượng.

Tóm lại, nếu bạn chuyển myIntegers cho một số hàm, bạn chỉ chuyển tham chiếu đến nơi phân bổ số nguyên thực sự.


1

Không có quyền anh trong mã ví dụ của bạn.

Các loại giá trị có thể sống trên heap như chúng làm trong mảng ints của bạn. Mảng được phân bổ trên heap và nó lưu ints, đó là các loại giá trị. Nội dung của mảng được khởi tạo thành mặc định (int), điều này xảy ra bằng không.

Hãy xem xét một lớp có chứa một loại giá trị:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

Biến h đề cập đến một thể hiện của HasAnInt sống trên đống. Nó chỉ xảy ra để chứa một loại giá trị. Điều đó hoàn toàn ổn, 'tôi' tình cờ sống trong đống vì nó được chứa trong một lớp. Không có quyền anh trong ví dụ này.


1

Mọi người đã nói đủ, nhưng nếu ai đó đang tìm kiếm một tài liệu và tài liệu rõ ràng (nhưng không chính thức) về heap, stack, biến cục bộ và biến tĩnh, hãy tham khảo bài viết đầy đủ của Jon Skeet về Bộ nhớ trong .NET - điều gì sẽ xảy ra Ở đâu

Trích đoạn:

  1. Mỗi biến cục bộ (tức là một biến được khai báo trong một phương thức) được lưu trữ trên ngăn xếp. Điều đó bao gồm các biến loại tham chiếu - chính biến đó nằm trên ngăn xếp, nhưng hãy nhớ rằng giá trị của biến loại tham chiếu chỉ là tham chiếu (hoặc null), không phải là chính đối tượng. Các tham số của phương thức cũng được tính là các biến cục bộ, nhưng nếu chúng được khai báo bằng công cụ sửa đổi ref, chúng không có vị trí riêng, mà chia sẻ một vị trí với biến được sử dụng trong mã gọi. Xem bài viết của tôi về thông số đi qua để biết thêm chi tiết.

  2. Các biến sơ thẩm cho một kiểu tham chiếu luôn nằm trong heap. Đó là nơi mà đối tượng tự "sống".

  3. Các biến sơ thẩm cho một loại giá trị được lưu trữ trong cùng bối cảnh với biến khai báo loại giá trị. Vị trí bộ nhớ cho ví dụ có hiệu quả chứa các vị trí cho từng trường trong thể hiện. Điều đó có nghĩa (được đưa ra hai điểm trước đó) rằng một biến cấu trúc được khai báo trong một phương thức sẽ luôn nằm trên ngăn xếp, trong khi đó một biến cấu trúc là trường thể hiện của một lớp sẽ nằm trong heap.

  4. Mỗi biến tĩnh được lưu trữ trên heap, bất kể nó được khai báo trong loại tham chiếu hay loại giá trị. Tổng cộng chỉ có một vị trí cho dù có bao nhiêu trường hợp được tạo. (Không cần phải có bất kỳ trường hợp nào được tạo cho một vị trí đó tồn tại.) Các chi tiết về chính xác các biến số sống trên đó rất phức tạp, nhưng được giải thích chi tiết trong một bài viết về MSDN về chủ đề này.

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.