วิธีที่แปลกประหลาดในการจัดสรรอาร์เรย์สองมิติ?


110

ในโครงการมีใครบางคนผลักดันบรรทัดนี้:

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

ซึ่งคาดว่าจะสร้างอาร์เรย์สองมิติของ (n + 1) * (n + 1) เป็นสองเท่า

คาดคะเนผมพูดเพราะเพื่อให้ห่างไกลไม่มีใครฉันถามจะบอกฉันว่านี้ไม่ว่าหรือที่มันมาจากหรือทำไมมันควรจะทำงาน (ซึ่งถูกกล่าวหาว่ามันไม่ แต่ฉันยังไม่ได้ซื้อมัน)

บางทีฉันอาจพลาดบางอย่างที่ชัดเจน แต่ฉันจะขอบคุณถ้ามีใครสามารถอธิบายข้างต้นให้ฉัน เพราะโดยส่วนตัวแล้วฉันจะรู้สึกดีขึ้นมากถ้าเราจะใช้สิ่งที่เราเข้าใจจริงๆ


15
แม้ว่ามันจะได้ผลให้กดปุ่มกด
Martin James

22
@MartinJames ทำไม? คุณจะจัดสรรอาร์เรย์ 2D ในหน่วยความจำที่อยู่ติดกันอย่างไร? อาร์เรย์ 1D ที่แหลกเหลว? นั่นคือการเขียนโปรแกรมในปี 1990 ตอนนี้เรามี VLAs แล้ว
Lundin

43
สำหรับเร็กคอร์ดนั่นเป็นวิธีเดียวและวิธีเดียวในการจัดสรรอาร์เรย์ 2D จริงแบบไดนามิก
Quentin

15
@Kninnug ไม่นั่นจะประกาศอาร์เรย์ของพอยน์เตอร์ไม่ใช่อาร์เรย์ 2D หากคุณต้องการอาร์เรย์ 2 มิติไม่มีเหตุผลที่คุณจะต้องการจัดสรรอาร์เรย์ของพอยน์เตอร์ ทำงานช้าเนื่องจากการกระจายตัวของฮีปและการใช้หน่วยความจำแคชที่ไม่ดีและไม่ปลอดภัยเนื่องจากคุณไม่สามารถใช้เป็นอาร์เรย์ได้ (มี memcpy เป็นต้น) แถมโค้ดก็ป่อง คุณจะต้องมีการโทรฟรี () หลายครั้งและจะทำให้หน่วยความจำรั่วไหลได้ง่าย การแพร่กระจายในวงกว้างเช่นรหัสดังกล่าวอาจเป็นเรื่องไม่ดี
Lundin

15
ปัญหานี้จะอธิบาย / ตอบได้ชัดเจนขึ้นหากตัวอย่างไม่ได้ใช้ขนาดที่มีมูลค่าเท่ากันn+1แต่กลับเป็นdouble (*e)[rows] = malloc(columns * sizeof *e);
chux - Reinstate Monica

คำตอบ:


87

ตัวแปรeเป็นตัวชี้ไปยังอาร์เรย์ของต์องค์ประกอบของประเภทn + 1double

การใช้โอเปอเรเตอร์ dereference เปิดeจะให้ประเภทพื้นฐานeซึ่งเป็น "อาร์เรย์ของn + 1องค์ประกอบประเภทdouble"

การmallocเรียกใช้เพียงประเภทพื้นฐานของe(อธิบายไว้ข้างต้น) และรับขนาดของมันคูณด้วยn + 1และส่งผ่านขนาดนั้นไปยังmallocฟังก์ชัน โดยพื้นฐานแล้วการจัดสรรn + 1อาร์เรย์ของn + 1องค์ประกอบของdouble.


3
@MartinJames sizeof(*e)เทียบเท่ากับsizeof(double [n + 1]). คูณด้วยn + 1และคุณจะได้รับเพียงพอ
โปรแกรมเมอร์บางคน

24
@MartinJames: เกิดอะไรขึ้น? ไม่ใช่ว่าการแทงตา แต่เป็นการรับประกันว่าแถวที่จัดสรรนั้นติดกันและคุณสามารถทำดัชนีได้เหมือนกับอาร์เรย์ 2D อื่น ๆ ฉันใช้สำนวนนี้มากในรหัสของฉันเอง
John Bode

3
อาจดูเหมือนชัดเจน แต่ใช้ได้กับอาร์เรย์สี่เหลี่ยม (ขนาดเดียวกัน) เท่านั้น
Jens

18
@Jens: ในแง่ที่ว่าถ้าคุณใส่n+1ทั้งสองมิติผลลัพธ์จะเป็นสี่เหลี่ยมจัตุรัส หากคุณทำdouble (*e)[cols] = malloc(rows * sizeof(*e));ผลลัพธ์จะมีจำนวนแถวและคอลัมน์ที่คุณระบุไว้
user2357112 รองรับ Monica

9
@ user2357112 ตอนนี้ฉันค่อนข้างจะเห็น แม้ว่าจะหมายความว่าคุณต้องเพิ่มint rows = n+1และint cols = n+1. พระเจ้าช่วยเราจากรหัสที่ชาญฉลาด
candied_orange

56

นี่เป็นวิธีทั่วไปที่คุณควรจัดสรรอาร์เรย์ 2D แบบไดนามิก

  • edouble [n+1]เป็นตัวชี้อาร์เรย์ไปยังอาร์เรย์ของพิมพ์
  • sizeof(*e)ดังนั้นจึงให้ประเภทของชนิดปลายแหลมซึ่งมีขนาดเท่ากับdouble [n+1]อาร์เรย์หนึ่งชุด
  • คุณจัดสรรห้องสำหรับn+1อาร์เรย์ดังกล่าว
  • คุณตั้งค่าตัวชี้อาร์เรย์eให้ชี้ไปที่อาร์เรย์แรกในอาร์เรย์ของอาร์เรย์นี้
  • นี้จะช่วยให้คุณใช้eเป็นe[i][j]แต่ละรายการในการเข้าถึงอาร์เรย์ 2 มิติ

โดยส่วนตัวแล้วฉันคิดว่าสไตล์นี้อ่านง่ายกว่ามาก:

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

12
คำตอบที่ดียกเว้นว่าฉันไม่เห็นด้วยกับสไตล์ที่คุณแนะนำเลือก ptr = malloc(sizeof *ptr * count)สไตล์นั้น
chux - คืนสถานะ Monica

คำตอบที่ดีและฉันชอบสไตล์ที่คุณต้องการ การปรับปรุงเล็กน้อยอาจเป็นการชี้ให้เห็นว่าคุณต้องทำด้วยวิธีนี้เนื่องจากอาจมีช่องว่างระหว่างแถวที่ต้องนำมาพิจารณา (อย่างน้อยฉันก็คิดว่านั่นเป็นเหตุผลที่คุณต้องทำแบบนี้) (แจ้งให้เราทราบหากฉันผิด)
davidbak

2
@davidbak นั่นคือสิ่งเดียวกัน ไวยากรณ์ของอาร์เรย์เป็นเพียงโค้ดที่จัดทำเอกสารด้วยตัวเองโดยระบุว่า "จัดสรรพื้นที่สำหรับอาร์เรย์ 2 มิติ" ด้วยซอร์สโค้ดเอง
Lundin

1
@davidbak หมายเหตุ: ข้อเสียเล็กน้อยของ ความคิดเห็น malloc(row*col*sizeof(double))เกิดขึ้นเมื่อมีมากเกินไปrow*col*sizeof()แต่ไม่เกิดขึ้น sizeof()*row*col(เช่น row, col are int)
chux - Reinstate Monica

7
@davidbak: sizeof *e * (n+1)ดูแลรักษาง่ายกว่า; หากคุณเคยตัดสินใจที่จะเปลี่ยนประเภทฐาน (จากdoubleเป็นlong doubleเป็นต้น) คุณจะต้องเปลี่ยนการประกาศeเท่านั้น คุณไม่จำเป็นต้องแก้ไขsizeofนิพจน์ในการmallocโทร (ซึ่งช่วยประหยัดเวลาและปกป้องคุณจากการเปลี่ยนแปลงในที่เดียว แต่ไม่ต้องแก้ไขนิพจน์อื่น ๆ ) sizeof *eมักจะให้ขนาดที่เหมาะสม
John Bode

39

สำนวนนี้หลุดออกจากการจัดสรรอาร์เรย์ 1D โดยธรรมชาติ เริ่มต้นด้วยการจัดสรรอาร์เรย์ 1D ของบางประเภทโดยพลการT:

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

ง่ายใช่มั้ย? แสดงออก *pมีประเภทTเพื่อsizeof *pให้ผลเช่นเดียวsizeof (T)ดังนั้นเราจึงจัดสรรพื้นที่เพียงพอสำหรับอาร์เรย์องค์ประกอบของN Tนี้เป็นจริงสำหรับประเภทใด T

ตอนนี้ให้แทนของกับชนิดอาร์เรย์เช่นT R [10]จากนั้นการจัดสรรของเราจะกลายเป็น

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

ความหมายตรงนี้เหมือนกับวิธีการจัดสรร 1D สิ่งที่เปลี่ยนไปคือประเภทของp. แทนที่จะก็ตอนนี้T * R (*)[10]การแสดงออกที่*pมีประเภทTซึ่งเป็นประเภทR [10]เพื่อให้sizeof *pเทียบเท่ากับซึ่งเทียบเท่ากับsizeof (T) sizeof (R [10])ดังนั้นเรากำลังการจัดสรรพื้นที่เพียงพอสำหรับNโดยอาร์เรย์องค์ประกอบของ 10R

เราสามารถนำสิ่งนี้ไปได้ไกลกว่านี้ถ้าเราต้องการ สมมติว่าเป็นตัวเองชนิดอาร์เรย์R int [5]แทนสิ่งนั้นRและเราได้รับ

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

จัดการกัน - sizeof *pเป็นเช่นเดียวกับsizeof (int [10][5])เราและเราไขลานจัดสรรก้อนที่ต่อเนื่องกันของหน่วยความจำขนาดใหญ่พอที่จะถือNโดย10โดยอาร์เรย์ของ 5int

นั่นคือด้านการจัดสรร แล้วด้านการเข้าถึงล่ะ?

โปรดจำไว้ว่า[]การดำเนินการห้อยถูกกำหนดไว้ในแง่ของการคำนวณตัวชี้: a[i]ถูกกำหนดให้เป็น1*(a + i) ดังนั้นตัวดำเนินการตัวห้อยจึงหมายถึงตัวชี้[] โดยปริยาย หากpเป็นตัวชี้ไปTคุณสามารถเข้าถึงค่าที่ชี้ไปที่ใดก็ได้โดยการยกเลิกการอ้างอิงอย่างชัดเจนด้วยตัวดำเนิน*การยูนารี:

T x = *p;

หรือโดยใช้ตัว[]ดำเนินการตัวห้อย:

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

ดังนั้นหากpชี้ไปที่องค์ประกอบแรกของอาร์เรย์คุณสามารถเข้าถึงองค์ประกอบใด ๆ ของอาร์เรย์นั้นได้โดยใช้ตัวห้อยบนตัวชี้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

ตอนนี้เรามาทำการแทนที่ของเราอีกครั้งและแทนที่Tด้วยประเภทอาร์เรย์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];

ความแตกต่างที่เห็นได้ชัดในทันที เรากำลังอ้างถึงอย่างชัดเจนpก่อนที่จะใช้ตัวดำเนินการตัวห้อย เราไม่ต้องการตัวห้อยpเราต้องการห้อยลงในสิ่งที่p ชี้ไป (ในกรณีนี้คืออาร์เรย์ arr[0] ) ตั้งแต่เอก*มีความสำคัญต่ำกว่าห้อย[]ประกอบการที่เราจะต้องใช้วงเล็บอย่างชัดเจนในกลุ่มด้วยp *แต่จำไว้ว่าจากด้านบนนั้น*pเหมือนกับp[0]เราจึงสามารถแทนที่ด้วย

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

หรือเพียงแค่

R x = p[0][i];

ดังนั้นหากpชี้ไปที่อาร์เรย์ 2 มิติเราสามารถจัดทำดัชนีในอาร์เรย์pนั้นได้ดังนี้:

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

นำสิ่งนี้ไปสู่ข้อสรุปเดียวกันกับด้านบนและแทนที่Rด้วย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];

นี้ทำงานเพียงเดียวกันถ้าชี้ไปยังอาร์เรย์ปกติหรือถ้ามันชี้ไปจัดสรรหน่วยความจำผ่าน pmalloc

สำนวนนี้มีประโยชน์ดังต่อไปนี้:

  1. มันง่ายมาก - โค้ดเพียงบรรทัดเดียวซึ่งต่างจากวิธีการจัดสรรทีละน้อย
    T **arr = malloc( sizeof *arr * N );
    if ( arr )
    {
      for ( size_t i = 0; i < N; i++ )
      {
        arr[i] = malloc( sizeof *arr[i] * M );
      }
    }
  2. แถวทั้งหมดของอาร์เรย์ที่จัดสรรคือ * ต่อเนื่องกัน * ซึ่งไม่ใช่กรณีของวิธีการจัดสรรทีละน้อยข้างต้น
  3. deallocating freeอาร์เรย์เป็นเพียงเป็นเรื่องง่ายด้วยสายเดียวที่จะ อีกครั้งไม่เป็นความจริงกับวิธีการจัดสรรทีละน้อยซึ่งคุณต้องยกเลิกการจัดสรรแต่ละรายการarr[i]ก่อนจึงจะสามารถยกเลิกการจัดสรรarrได้

บางครั้งวิธีการจัดสรรแบบทีละชิ้นจะดีกว่าเช่นเมื่อฮีปของคุณแยกส่วนไม่ดีและคุณไม่สามารถจัดสรรหน่วยความจำของคุณเป็นชิ้นส่วนที่ต่อเนื่องกันได้หรือคุณต้องการจัดสรรอาร์เรย์แบบ "หยัก" ซึ่งแต่ละแถวสามารถมีความยาวต่างกันได้ แต่โดยทั่วไปแล้วนี่เป็นวิธีที่ดีกว่า


1. โปรดจำไว้ว่าอาร์เรย์ไม่ใช่ตัวชี้ แต่นิพจน์อาร์เรย์จะถูกแปลงเป็นนิพจน์พอยน์เตอร์ตามความจำเป็น


4
+1 ฉันชอบวิธีที่คุณนำเสนอแนวคิด: การจัดสรรชุดองค์ประกอบเป็นไปได้สำหรับทุกประเภทแม้ว่าองค์ประกอบเหล่านั้นจะเป็นอาร์เรย์เองก็ตาม
logo_writer

1
คำอธิบายของคุณดีมาก แต่โปรดทราบว่าการจัดสรรหน่วยความจำที่ต่อเนื่องกันไม่ใช่ประโยชน์จนกว่าคุณจะต้องการจริงๆ หน่วยความจำแบบต่อเนื่องมีราคาแพงกว่าหน่วยความจำที่ไม่ติดกัน สำหรับอาร์เรย์ 2 มิติแบบธรรมดาจะไม่มีความแตกต่างในรูปแบบหน่วยความจำสำหรับคุณ (ยกเว้นจำนวนบรรทัดสำหรับการจัดสรรและการจัดสรรตำแหน่ง) ดังนั้นเลือกใช้หน่วยความจำที่ไม่ติดกัน
Oleg Lokshyn
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.