Cách phân bổ mảng hai chiều kỳ lạ?


110

Trong một dự án, ai đó đã đẩy dòng này:

double (*e)[n+1] = malloc((n+1) * sizeof(*e));

Điều này được cho là tạo ra một mảng hai chiều (n + 1) * (n + 1) nhân đôi.

Giả sử , tôi nói, bởi vì cho đến nay, không ai hỏi tôi có thể cho tôi biết điều này không, chính xác, và cũng không nơi nó có nguồn gốc từ hoặc lý do tại sao nó phải làm việc (trong đó bị cáo buộc, nó làm, nhưng tôi chưa mua nó).

Có lẽ tôi đang thiếu một cái gì đó rõ ràng, nhưng tôi sẽ đánh giá cao nếu ai đó có thể giải thích dòng trên cho tôi. Bởi vì về mặt cá nhân, tôi sẽ cảm thấy tốt hơn nhiều nếu chúng tôi sử dụng thứ mà chúng tôi thực sự hiểu.


15
Ngay cả khi nó hoạt động, hãy bắn bộ đẩy.
Martin James

22
@MartinJames Tại sao? Nếu không, bạn sẽ phân bổ một mảng 2D trong bộ nhớ liền kề như thế nào? Mảng 1D bị lệch? Đó là lập trình những năm 1990, chúng tôi có VLA bây giờ.
Lundin

43
Đối với bản ghi, đó cách duy nhất và duy nhất để phân bổ động một mảng 2D thực tế.
Quentin

15
@Kninnug Không, điều đó sẽ khai báo một mảng con trỏ, không phải một mảng 2D. Nếu bạn muốn có một mảng 2D, không có lý do gì bạn muốn phân bổ một mảng con trỏ. Nó chậm do phân mảnh heap và sử dụng bộ nhớ đệm kém và không an toàn vì bạn không thể sử dụng nó như một mảng (với memcpy, v.v.). Thêm vào đó, mã bị phình ra. Bạn sẽ cần nhiều cuộc gọi miễn phí () và sẽ dễ gây rò rỉ bộ nhớ. Mã như vậy có thể lan rộng, nó rõ ràng là tệ.
Lundin

15
Vấn đề này có thể đã được rõ ràng hơn để giải thích / câu trả lời đã mẫu không sử dụng cùng một kích thước có giá trị n+1mà thay vào đódouble (*e)[rows] = malloc(columns * sizeof *e);
Chux - Khôi phục Monica

Câu trả lời:


87

Biến elà một con trỏ đến một mảng các n + 1phần tử có kiểu double.

Sử dụng toán tử tham chiếu trên ecung cấp cho bạn kiểu cơ sở elà "mảng các n + 1phần tử của kiểu double".

Lệnh mallocgọi chỉ đơn giản là lấy kiểu cơ sở của e(giải thích ở trên) và lấy kích thước của nó, nhân nó với n + 1và truyền kích thước đó cho mallochàm. Về cơ bản phân bổ một mảng các n + 1mảng các n + 1phần tử của double.


3
@MartinJames sizeof(*e)tương đương với sizeof(double [n + 1]). Nhân với n + 1và bạn nhận được đủ.
Một số lập trình viên dude

24
@MartinJames: Có gì sai với nó? Nó không phải là điều bắt mắt, nó đảm bảo rằng các hàng được phân bổ liền kề và bạn có thể lập chỉ mục nó giống như bất kỳ mảng 2D nào khác. Tôi sử dụng thành ngữ này rất nhiều trong mã của riêng tôi.
John Bode

3
Nó có vẻ hiển nhiên, nhưng điều này chỉ hoạt động đối với các mảng hình vuông (cùng kích thước).
Jens

18
@Jens: Chỉ theo nghĩa là nếu bạn đặt n+1cho cả hai chiều, kết quả sẽ là hình vuông. Nếu bạn làm như vậy double (*e)[cols] = malloc(rows * sizeof(*e));, kết quả sẽ có bất kỳ số hàng và cột nào bạn đã chỉ định.
user2357112 hỗ trợ Monica

9
@ user2357112 Bây giờ tôi muốn xem hơn. Ngay cả khi nó có nghĩa là bạn phải thêm int rows = n+1int cols = n+1. Chúa cứu chúng ta khỏi mã thông minh.
candied_orange

56

Đây là cách điển hình mà bạn nên phân bổ động các mảng 2D.

  • elà một con trỏ mảng đến một kiểu mảng double [n+1].
  • sizeof(*e)do đó cung cấp cho kiểu của kiểu trỏ vào, có kích thước bằng một double [n+1]mảng.
  • Bạn phân bổ chỗ cho n+1các mảng như vậy.
  • Bạn đặt con trỏ mảng eđể trỏ vào mảng đầu tiên trong mảng mảng này.
  • Điều này cho phép bạn sử dụng enhư e[i][j]truy cập từng mục trong mảng 2D.

Cá nhân tôi nghĩ phong cách này dễ đọc hơn nhiều:

double (*e)[n+1] = malloc( sizeof(double[n+1][n+1]) );

12
Câu trả lời tốt, ngoại trừ tôi không đồng ý với phong cách đề xuất của bạn, thích ptr = malloc(sizeof *ptr * count)phong cách hơn.
chux - Phục hồi Monica

Câu trả lời hay, và tôi thích phong cách ưa thích của bạn. Một cải tiến nhỏ có thể chỉ ra rằng bạn cần làm theo cách này vì có thể có khoảng đệm giữa các hàng cần được tính đến. (Ít nhất, tôi nghĩ đó là lý do bạn cần phải làm điều đó theo cách này.) (Hãy cho tôi biết nếu tôi sai.)
davidbak

2
@davidbak Điều tương tự. Cú pháp mảng chỉ đơn thuần là mã tự ghi lại: nó cho biết "phân bổ chỗ cho mảng 2D" với chính mã nguồn.
Lundin

1
@davidbak Lưu ý: Một nhược điểm nhỏ của nhận xét malloc(row*col*sizeof(double)) xảy ra khi row*col*sizeof()tràn, nhưng không sizeof()*row*colphải không. (ví dụ: row, col are int)
chux - Reinstate Monica

7
@davidbak: sizeof *e * (n+1)dễ bảo trì hơn; nếu bạn quyết định thay đổi kiểu cơ sở (từ doublesang long doublechẳng hạn), thì bạn chỉ cần thay đổi khai báo của e; bạn không cần phải sửa đổi sizeofbiểu thức trong malloccuộc gọi (điều này giúp tiết kiệm thời gian và bảo vệ bạn khỏi việc thay đổi nó ở một nơi mà không phải nơi khác). sizeof *esẽ luôn cung cấp cho bạn kích thước phù hợp.
John Bode

39

Thành ngữ này tự nhiên nằm ngoài phân bổ mảng 1D. Hãy bắt đầu với việc cấp phát một mảng 1D của một số kiểu tùy ý T:

T *p = malloc( sizeof *p * N );

Đơn giản, phải không? Các biểu hiện *p có kiểu T, vì vậy sizeof *pcho kết quả tương tự như sizeof (T), vì vậy chúng tôi đang phân bổ đủ không gian cho một Nmảng -element của T. Điều này đúng cho bất kỳ loại nàoT .

Bây giờ, hãy thay thế Tbằng một kiểu mảng như R [10]. Sau đó, phân bổ của chúng tôi trở thành

R (*p)[10] = malloc( sizeof *p * N);

Ngữ nghĩa ở đây hoàn toàn giống với phương pháp cấp phát 1D; tất cả những gì đã thay đổi là loại p. Thay vì T *, nó là bây giờ R (*)[10]. Biểu thức *pcó kiểu Tlà kiểu R [10], vì vậy sizeof *ptương đương với sizeof (T)tương đương với sizeof (R [10]). Vì vậy, chúng tôi đang phân bổ đủ không gian cho một Nbằng 10mảng yếu tố R.

Chúng ta có thể tiến xa hơn nữa nếu chúng ta muốn; giả sử Rbản thân nó là một kiểu mảng int [5]. Thay thế cho Rvà chúng tôi nhận được

int (*p)[10][5] = malloc( sizeof *p * N);

Tương tự đối phó - sizeof *pcũng giống như sizeof (int [10][5]), và chúng tôi gió lên bố trí một đoạn tiếp giáp bộ nhớ đủ lớn để tổ chức một Nbằng 10bởi 5mảng int.

Vì vậy, đó là phía phân bổ; những gì về phía truy cập?

Hãy nhớ rằng các []hoạt động subscript được định nghĩa về con trỏ số học: a[i]được định nghĩa là *(a + i)1 . Do đó, toán tử chỉ số phụ [] ngầm định tham chiếu đến một con trỏ. Nếu plà con trỏ tới T, bạn có thể truy cập giá trị trỏ đến bằng cách tham chiếu rõ ràng với toán *tử một ngôi:

T x = *p;

hoặc bằng cách sử dụng []toán tử chỉ số dưới:

T x = p[0]; // identical to *p

Do đó, nếu ptrỏ đến phần tử đầu tiên của một mảng , bạn có thể truy cập bất kỳ phần tử nào của mảng đó bằng cách sử dụng chỉ số con trên con trỏ p:

T arr[N];
T *p = arr; // expression arr "decays" from type T [N] to T *
...
T x = p[i]; // access the i'th element of arr through pointer p

Bây giờ, hãy thực hiện lại thao tác thay thế và thay thế Tbằng kiểu mảng R [10]:

R arr[N][10];
R (*p)[10] = arr; // expression arr "decays" from type R [N][10] to R (*)[10]
...
R x = (*p)[i];

Một sự khác biệt rõ ràng ngay lập tức; chúng tôi đang tham khảo rõ ràng ptrước khi áp dụng toán tử chỉ số con. Chúng tôi không muốn chỉ số dưới vào p, chúng tôi muốn chỉ số phụ vào những gì p trỏ đến (trong trường hợp này là mảng arr[0] ). Vì một ngôi *có quyền ưu tiên thấp hơn []toán tử chỉ số con, chúng ta phải sử dụng dấu ngoặc đơn để nhóm rõ ràng pvới *. Nhưng hãy nhớ từ phía trên, *pnó giống như p[0], vì vậy chúng ta có thể thay thế nó bằng

R x = (p[0])[i];

hoặc chỉ

R x = p[0][i];

Do đó, nếu ptrỏ đến một mảng 2D, chúng ta có thể lập chỉ mục vào mảng đó thông qua pnhư sau:

R x = p[i][j]; // access the i'th element of arr through pointer p;
               // each arr[i] is a 10-element array of R

Lấy điều này đến kết luận tương tự như trên và thay thế Rbằng int [5]:

int arr[N][10][5];
int (*p)[10][5]; // expression arr "decays" from type int [N][5][10] to int (*)[10][5]
...
int x = p[i][j][k];

Điều này hoạt động giống nhau nếu ptrỏ đến một mảng thông thường hoặc nếu nó trỏ đến bộ nhớ được cấp phát qua malloc.

Thành ngữ này có những lợi ích sau:

  1. Nó đơn giản - chỉ một dòng mã, trái ngược với phương pháp phân bổ từng phần
    T **arr = malloc( sizeof *arr * N );
    if ( arr )
    {
      for ( size_t i = 0; i < N; i++ )
      {
        arr[i] = malloc( sizeof *arr[i] * M );
      }
    }
    
  2. Tất cả các hàng của mảng được phân bổ là * liền kề *, điều này không đúng với phương pháp phân bổ từng phần ở trên;
  3. Việc phân bổ mảng cũng dễ dàng bằng một lệnh gọi tới free. Một lần nữa, không đúng với phương pháp phân bổ từng phần, trong đó bạn phải phân bổ từng thứ arr[i]trước khi có thể phân bổ arr.

Đôi khi phương pháp phân bổ từng phần được ưu tiên hơn, chẳng hạn như khi heap của bạn bị phân mảnh nghiêm trọng và bạn không thể phân bổ bộ nhớ của mình dưới dạng một đoạn liền kề hoặc bạn muốn phân bổ một mảng "răng cưa" trong đó mỗi hàng có thể có độ dài khác nhau. Nhưng nói chung, đây là cách tốt hơn để đi.


1. Hãy nhớ rằng mảng không phải là con trỏ - thay vào đó, biểu thức mảng được chuyển đổi thành biểu thức con trỏ khi cần thiết.


4
+1 Tôi thích cách bạn trình bày khái niệm: việc phân bổ một loạt các phần tử có thể thực hiện được cho bất kỳ kiểu nào, ngay cả khi các phần tử đó là mảng.
logo_writer

1
Lời giải thích của bạn thực sự tốt, nhưng lưu ý rằng việc phân bổ bộ nhớ liền kề không phải là một lợi ích cho đến khi bạn thực sự cần nó. Bộ nhớ liền kề đắt hơn bộ nhớ không liền kề. Đối với các mảng 2D đơn giản, không có sự khác biệt nào về cách bố trí bộ nhớ đối với bạn (ngoại trừ số dòng để phân bổ và phân bổ), vì vậy bạn nên sử dụng bộ nhớ không liền kề.
Oleg Lokshyn
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.