Làm thế nào các mảng đa chiều được định dạng trong bộ nhớ?


185

Trong C, tôi biết rằng tôi có thể tự động phân bổ một mảng hai chiều trên heap bằng mã sau:

int** someNumbers = malloc(arrayRows*sizeof(int*));

for (i = 0; i < arrayRows; i++) {
    someNumbers[i] = malloc(arrayColumns*sizeof(int));
}

Rõ ràng, điều này thực sự tạo ra một mảng các con trỏ một chiều cho một loạt các số nguyên một chiều riêng biệt và "Hệ thống" có thể hiểu ý tôi muốn nói khi tôi yêu cầu:

someNumbers[4][2];

Nhưng khi tôi khai báo tĩnh một mảng 2D, như trong dòng sau ...:

int someNumbers[ARRAY_ROWS][ARRAY_COLUMNS];

... một cấu trúc tương tự có được tạo trên ngăn xếp không, hay nó có dạng hoàn toàn khác không? (tức là nó là một mảng con trỏ 1D? Nếu không, nó là gì và làm thế nào để các tham chiếu đến nó được tìm ra?)

Ngoài ra, khi tôi nói, "Hệ thống", cái gì thực sự chịu trách nhiệm cho việc tìm ra điều đó? Hạt nhân? Hoặc trình biên dịch C sắp xếp nó trong khi biên dịch?


8
Tôi sẽ cung cấp nhiều hơn +1 nếu tôi có thể.
Rob Lachlan

1
Cảnh báo : Không có mảng 2D trong mã này!
quá trung thực cho trang web này

@toohonestforthissite Thật vậy. Để mở rộng về điều đó: Vòng lặp và gọi malloc()không dẫn đến một mảng N chiều. . Nó dẫn đến các mảng của con trỏ [đến các mảng của con trỏ [...]] để tách các mảng một chiều hoàn toàn . Xem Phân bổ chính xác các mảng đa chiều để xem cách phân bổ mảng N chiều TRUE .
Andrew Henle

Câu trả lời:


145

Một mảng hai chiều tĩnh trông giống như một mảng các mảng - nó chỉ được đặt liền kề trong bộ nhớ. Mảng không giống như con trỏ, nhưng vì bạn có thể thường xuyên sử dụng chúng thay thế cho nhau nên đôi khi có thể gây nhầm lẫn. Trình biên dịch theo dõi đúng, mặc dù, làm cho mọi thứ xếp hàng độc đáo. Bạn phải cẩn thận với các mảng 2D tĩnh như bạn đề cập, vì nếu bạn cố truyền một hàm cho một hàm lấy int **tham số, điều tồi tệ sẽ xảy ra. Đây là một ví dụ nhanh:

int array1[3][2] = {{0, 1}, {2, 3}, {4, 5}};

Trong bộ nhớ trông như thế này:

0 1 2 3 4 5

chính xác giống như:

int array2[6] = { 0, 1, 2, 3, 4, 5 };

Nhưng nếu bạn cố gắng chuyển array1sang chức năng này:

void function1(int **a);

bạn sẽ nhận được cảnh báo (và ứng dụng sẽ không truy cập được mảng chính xác):

warning: passing argument 1 of function1 from incompatible pointer type

Bởi vì một mảng 2D không giống như int **. Việc phân rã tự động của một mảng thành một con trỏ chỉ có thể nói "sâu một cấp". Bạn cần khai báo hàm là:

void function2(int a[][2]);

hoặc là

void function2(int a[3][2]);

Để làm cho mọi thứ hạnh phúc.

Khái niệm tương tự này mở rộng đến các mảng n chiều. Tuy nhiên, việc tận dụng lợi thế của loại hình kinh doanh hài hước này trong ứng dụng của bạn chỉ khiến bạn khó hiểu hơn. Vì vậy, hãy cẩn thận khi ở ngoài đó.


Cảm ơn đã giải thích. Vì vậy, "void function2 (int a [] [2]);" sẽ chấp nhận cả 2D và tĩnh? Và tôi đoán rằng vẫn còn thực hành tốt / thiết yếu để vượt qua độ dài của mảng nếu chiều thứ nhất còn lại là []?
Chris Cooper

1
@Chris Tôi không nghĩ vậy - bạn sẽ gặp khó khăn khi biến C xáo trộn một mảng - hoặc phân bổ toàn cầu thành một loạt các con trỏ.
Carl Norum

6
@JasonK. - không Mảng không phải là con trỏ. Mảng "phân rã" thành con trỏ trong một số bối cảnh, nhưng chúng hoàn toàn không giống nhau.
Carl Norum

1
Để rõ ràng: Có Chris "vẫn còn thực hành tốt để vượt qua độ dài của mảng" như một tham số riêng, nếu không thì sử dụng std :: Array hoặc std :: vector (là C ++ không phải C cũ). Tôi nghĩ rằng chúng tôi đồng ý @CarlNorum cả về mặt khái niệm cho người dùng mới và thực tế, để trích dẫn Anders Kaseorg trên Quora: Hồi Bước đầu tiên để học C là hiểu rằng con trỏ và mảng là cùng một thứ. Bước thứ hai là hiểu rằng con trỏ và mảng là khác nhau.
Jason K.

2
@JasonK. "Bước đầu tiên để học C là hiểu rằng con trỏ và mảng là cùng một thứ." - Câu nói này rất sai và sai! Đây thực sự là bước quan trọng nhất để hiểu chúng không giống nhau, nhưng các mảng được chuyển đổi thành một con trỏ thành phần tử đầu tiên cho hầu hết các toán tử! sizeof(int[100]) != sizeof(int *)(trừ khi bạn tìm thấy một nền tảng có 100 * sizeof(int)byte / int, nhưng đó là một điều khác.
quá trung thực cho trang web này vào

85

Câu trả lời dựa trên ý tưởng rằng C không thực sự mảng 2D - nó có mảng - của - mảng. Khi bạn tuyên bố điều này:

int someNumbers[4][2];

Bạn đang yêu cầu someNumberstrở thành một mảng gồm 4 phần tử, trong đó mỗi phần tử của mảng đó thuộc loại int [2](bản thân nó là một mảng gồm 2 ints).

Phần khác của câu đố là các mảng luôn được đặt liền kề trong bộ nhớ. Nếu bạn yêu cầu:

sometype_t array[4];

sau đó sẽ luôn như thế này:

| sometype_t | sometype_t | sometype_t | sometype_t |

(4 sometype_tđối tượng được đặt cạnh nhau, không có khoảng trắng ở giữa). Vì vậy, trong someNumbersmảng của bạn , nó sẽ trông như thế này:

| int [2]    | int [2]    | int [2]    | int [2]    |

Và mỗi int [2]phần tử là một mảng, trông như thế này:

| int        | int        |

Vì vậy, tổng thể, bạn nhận được điều này:

| int | int  | int | int  | int | int  | int | int  |

1
nhìn vào bố cục cuối cùng làm tôi nghĩ rằng int a [] [] có thể được truy cập dưới dạng int * ... phải không?
Narcisse Doudieu Siewe

2
@ user3238855: Các loại không tương thích, nhưng nếu bạn nhận được một con trỏ đến đầu tiên inttrong mảng của mảng (ví dụ: bằng cách đánh giá a[0]hoặc &a[0][0]) thì có, bạn có thể bù nó để truy cập tuần tự mỗi int).
phê

28
unsigned char MultiArray[5][2]={{0,1},{2,3},{4,5},{6,7},{8,9}};

trong bộ nhớ bằng:

unsigned char SingleArray[10]={0,1,2,3,4,5,6,7,8,9};

5

Trả lời cho bạn cũng: Cả hai, mặc dù trình biên dịch đang thực hiện hầu hết các công việc nặng.

Trong trường hợp mảng được phân bổ tĩnh, "Hệ thống" sẽ là trình biên dịch. Nó sẽ dự trữ bộ nhớ như đối với bất kỳ biến stack nào.

Trong trường hợp mảng malloc'd, "Hệ thống" sẽ là người triển khai malloc (thường là kernel). Tất cả các trình biên dịch sẽ phân bổ là con trỏ cơ sở.

Trình biên dịch sẽ luôn xử lý kiểu như những gì chúng được khai báo ngoại trừ trong ví dụ Carl đưa ra nơi nó có thể tìm ra cách sử dụng có thể hoán đổi cho nhau. Đây là lý do tại sao nếu bạn chuyển một [] [] cho một hàm thì nó phải giả sử rằng đó là một mặt phẳng được phân bổ tĩnh, trong đó ** được coi là con trỏ tới con trỏ.


@Jon L. Tôi sẽ không nói rằng malloc được triển khai bởi kernel, nhưng bởi libc trên đỉnh của các nguyên hàm kernel (chẳng hạn như brk)
Manuel Selva

@ManuelSelva: Trường hợp và cách thức malloctriển khai không được chỉ định bởi tiêu chuẩn và còn lại để thực hiện, resp. Môi trường. Đối với các môi trường tự do, nó là tùy chọn giống như tất cả các phần của thư viện tiêu chuẩn yêu cầu các chức năng liên kết (đó là những yêu cầu thực sự dẫn đến, không phải là những gì các trạng thái tiêu chuẩn). Đối với một số môi trường được lưu trữ hiện đại, nó thực sự dựa vào các hàm kernel, hoặc là toàn bộ nội dung, hoặc (ví dụ Linux) như bạn đã viết bằng cả hai, stdlib và kernel-primitive. Đối với các hệ thống xử lý đơn bộ nhớ không ảo, nó chỉ có thể là stdlib.
quá trung thực cho trang web này

2

Giả sử, chúng ta có a1a2định nghĩa và khởi tạo như dưới đây (c99):

int a1[2][2] = {{142,143}, {144,145}};
int **a2 = (int* []){ (int []){242,243}, (int []){244,245} };

a1là một mảng 2D đồng nhất với bố cục liên tục đơn giản trong bộ nhớ và biểu thức (int*)a1được ước tính cho một con trỏ tới phần tử đầu tiên của nó:

a1 --> 142 143 144 145

a2được khởi tạo từ một mảng 2D không đồng nhất và là một con trỏ tới một giá trị của kiểu int*, tức là biểu thức dereference ước tính *a2thành một giá trị của kiểu int*, bố cục bộ nhớ không phải liên tục:

a2 --> p1 p2
       ...
p1 --> 242 243
       ...
p2 --> 244 245

Mặc dù bố cục bộ nhớ và ngữ nghĩa truy cập hoàn toàn khác nhau, ngữ pháp ngôn ngữ C cho các biểu thức truy cập mảng trông giống hệt nhau cho cả mảng 2D đồng nhất và không đồng nhất:

  • biểu thức a1[1][0]sẽ lấy giá trị 144ra khỏi a1mảng
  • biểu thức a2[1][0]sẽ lấy giá trị 244ra khỏi a2mảng

Trình biên dịch biết rằng biểu thức truy cập cho a1hoạt động theo loại int[2][2], khi biểu thức truy cập cho a2hoạt động trên loại int**. Mã lắp ráp được tạo sẽ tuân theo ngữ nghĩa truy cập đồng nhất hoặc không đồng nhất.

Mã thường gặp sự cố khi chạy khi mảng kiểu int[N][M]được tạo kiểu và sau đó được truy cập dưới dạng kiểu int**, ví dụ:

((int**)a1)[1][0]   //crash on dereference of a value of type 'int'

1

Để truy cập vào một mảng 2D cụ thể, hãy xem xét bản đồ bộ nhớ cho một khai báo mảng như được hiển thị trong mã dưới đây:

    0  1
a[0]0  1
a[1]2  3

Để truy cập từng phần tử, chỉ cần vượt qua mảng nào bạn quan tâm làm tham số cho hàm. Sau đó sử dụng offset cho cột để truy cập từng phần tử riêng lẻ.

int a[2][2] ={{0,1},{2,3}};

void f1(int *ptr);

void f1(int *ptr)
{
    int a=0;
    int b=0;
    a=ptr[0];
    b=ptr[1];
    printf("%d\n",a);
    printf("%d\n",b);
}

int main()
{
   f1(a[0]);
   f1(a[1]);
    return 0;
}
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.