ฉันจะใช้อาร์เรย์ใน C ++ ได้อย่างไร


480

C ++ สืบทอดมาจาก C ที่พวกเขาใช้งานได้ทุกที่ C ++ ให้ abstractions ที่ใช้ง่ายกว่าและมีข้อผิดพลาดน้อยลง ( std::vector<T>ตั้งแต่ C ++ 98 และstd::array<T, n>ตั้งแต่C ++ 11 ) ดังนั้นความต้องการอาร์เรย์ไม่ได้เกิดขึ้นบ่อยเท่าใน C อย่างไรก็ตามเมื่อคุณอ่าน legacy รหัสหรือโต้ตอบกับไลบรารีที่เขียนใน C คุณควรมีความเข้าใจอย่างถ่องแท้เกี่ยวกับวิธีการทำงานของอาร์เรย์

คำถามที่พบบ่อยนี้แบ่งออกเป็นห้าส่วน:

  1. อาร์เรย์ในระดับประเภทและองค์ประกอบการเข้าถึง
  2. การสร้างและการเริ่มต้นอาร์เรย์
  3. การมอบหมายและการส่งพารามิเตอร์
  4. อาร์เรย์หลายมิติและอาร์เรย์ของพอยน์เตอร์
  5. ข้อผิดพลาดทั่วไปเมื่อใช้อาร์เรย์

หากคุณรู้สึกว่าสิ่งสำคัญขาดหายไปในคำถามที่พบบ่อยนี้ให้เขียนคำตอบและเชื่อมโยงที่นี่เป็นส่วนเพิ่มเติม

ในข้อความต่อไปนี้ "อาร์เรย์" หมายถึง "อาร์เรย์ C" std::arrayไม่ใช่แม่แบบชั้นเรียน มีความรู้พื้นฐานเกี่ยวกับไวยากรณ์ของตัวประกาศ C โปรดทราบว่าการใช้งานด้วยตนเองnewและdeleteแสดงให้เห็นด้านล่างเป็นอันตรายอย่างยิ่งในการเผชิญกับข้อยกเว้น แต่ที่เป็นหัวข้อของคำถามที่พบบ่อยอีก

(หมายเหตุ: นี่คือหมายถึงการเป็นรายการที่จะกองมากเกินของ C ++ คำถามที่พบบ่อย . ถ้าคุณต้องการที่จะวิจารณ์ความคิดของการให้คำถามที่พบบ่อยในรูปแบบนี้แล้วโพสต์บนเมตาที่เริ่มต้นทั้งหมดนี้ . จะเป็นสถานที่ที่จะทำเช่นนั้นได้คำตอบ คำถามนั้นได้รับการตรวจสอบในห้องสนทนา C ++ซึ่งแนวคิด FAQ เริ่มต้นตั้งแต่แรกดังนั้นคำตอบของคุณมีโอกาสมากที่จะได้รับการอ่านโดยผู้ที่คิดแนวคิดนี้)


พวกเขาจะดียิ่งขึ้นถ้าชี้เสมอชี้ไปที่จุดเริ่มต้นแทนการอยู่ที่ไหนสักแห่งในช่วงกลางของเป้าหมายของพวกเขาแม้ว่า ...
Deduplicator

คุณควรใช้เวกเตอร์ STL เพราะมันให้ความยืดหยุ่นมากกว่า
Moiz Sajid

2
ด้วยความพร้อมใช้งานร่วมกันของstd::arrays, std::vectors และgsl::spans - ฉันคาดหวังว่าจะมีคำถามที่พบบ่อยเกี่ยวกับวิธีการใช้อาร์เรย์ใน C ++ เพื่อพูดว่า "ตอนนี้คุณสามารถเริ่มพิจารณาได้ด้วยดีไม่ใช้มัน"
einpoklum

คำตอบ:


302

อาร์เรย์ในระดับประเภท

ประเภทอาเรย์จะแสดงเป็นT[n]ตำแหน่งที่Tเป็นประเภทองค์ประกอบและnมีขนาดบวกจำนวนองค์ประกอบในอาเรย์ ประเภทอาเรย์เป็นประเภทผลิตภัณฑ์ของประเภทองค์ประกอบและขนาด หากส่วนผสมหนึ่งหรือทั้งสองอย่างแตกต่างกันคุณจะได้รับประเภทที่แตกต่าง:

#include <type_traits>

static_assert(!std::is_same<int[8], float[8]>::value, "distinct element type");
static_assert(!std::is_same<int[8],   int[9]>::value, "distinct size");

โปรดทราบว่าขนาดเป็นส่วนหนึ่งของประเภทนั่นคือประเภทอาร์เรย์ที่มีขนาดแตกต่างกันเป็นประเภทที่เข้ากันไม่ได้ซึ่งไม่มีส่วนเกี่ยวข้องใด ๆ เทียบเท่ากับsizeof(T[n])n * sizeof(T)

การสลายแบบ Array-to-pointer

เท่านั้น "การเชื่อมต่อ" ระหว่างT[n]และT[m]คือการที่ทั้งสองชนิดโดยปริยายสามารถแปลงไปT*และผลของการแปลงนี้เป็นตัวชี้ไปองค์ประกอบแรกของอาร์เรย์ นั่นคือที่ใดก็ตามที่T*จำเป็นต้องมีคุณสามารถให้ a T[n]และคอมไพเลอร์จะให้ตัวชี้นั้น:

                  +---+---+---+---+---+---+---+---+
the_actual_array: |   |   |   |   |   |   |   |   |   int[8]
                  +---+---+---+---+---+---+---+---+
                    ^
                    |
                    |
                    |
                    |  pointer_to_the_first_element   int*

การแปลงนี้เรียกว่า "การสลายตัวของอาเรย์ต่อตัวชี้" และเป็นแหล่งของความสับสนที่สำคัญ ขนาดของอาเรย์จะหายไปในกระบวนการนี้เนื่องจากไม่ได้เป็นส่วนหนึ่งของประเภท ( T*) อีกต่อไป Pro: การลืมขนาดของอาร์เรย์ในระดับประเภทจะทำให้ตัวชี้ชี้ไปที่องค์ประกอบแรกของอาร์เรย์ทุกขนาด คอนดิชั่น: กำหนดตัวชี้ไปยังองค์ประกอบแรก (หรืออื่น ๆ ) ของอาเรย์ไม่มีวิธีการตรวจสอบว่าอาเรย์นั้นมีขนาดใหญ่เพียงใดหรือที่ตัวชี้พอยน์เตอร์ตรงกับขอบเขตของอาเรย์ พอยน์เตอร์นั้นโง่มาก

อาร์เรย์ไม่ใช่ตัวชี้

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

static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");

บริบทที่สำคัญอย่างหนึ่งที่อาร์เรย์ไม่สลายตัวลงในตัวชี้ไปยังองค์ประกอบแรกคือเมื่อตัว&ดำเนินการถูกนำไปใช้ ในกรณีนั้น&โอเปอเรเตอร์จะให้พอยน์เตอร์ไปยังอาเรย์ทั้งหมดไม่ใช่แค่ตัวชี้ไปยังองค์ประกอบแรก แม้ว่าในกรณีนั้นค่า (ที่อยู่) จะเหมือนกัน แต่ตัวชี้ไปยังองค์ประกอบแรกของอาร์เรย์และตัวชี้ไปยังทั้งอาร์เรย์นั้นเป็นประเภทที่แตกต่างกันโดยสิ้นเชิง:

static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");

ศิลปะ ASCII ต่อไปนี้จะอธิบายความแตกต่างนี้:

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | int[8]
|     | +---+---+---+---+---+---+---+---+ |
|     +---^-------------------------------+
|         |
|         |
|         |
|         |  pointer_to_the_first_element   int*
|
|  pointer_to_the_entire_array              int(*)[8]

โปรดสังเกตว่าตัวชี้ไปยังองค์ประกอบแรกเพียงชี้ไปที่จำนวนเต็มเดียว (แสดงเป็นกล่องเล็ก) ในขณะที่ตัวชี้ไปยังทั้งอาร์เรย์ชี้ไปที่อาร์เรย์จำนวนเต็ม 8 จำนวน (แสดงเป็นกล่องขนาดใหญ่)

สถานการณ์เดียวกันเกิดขึ้นในชั้นเรียนและอาจชัดเจนกว่า ตัวชี้ไปยังวัตถุและตัวชี้ไปยังข้อมูลสมาชิกคนแรกมีค่าเท่ากัน(ที่อยู่เดียวกัน) แต่เป็นชนิดที่แตกต่างกันโดยสิ้นเชิง

หากคุณไม่คุ้นเคยกับไวยากรณ์ C declarator วงเล็บในประเภทint(*)[8]นั้นมีความสำคัญ:

  • int(*)[8] เป็นตัวชี้ไปยังอาร์เรย์ 8 จำนวนเต็ม
  • int*[8]เป็นอาร์เรย์ 8 int*ตัวชี้องค์ประกอบของแต่ละประเภท

การเข้าถึงองค์ประกอบ

C ++ มีการจัดรูปแบบวากยสัมพันธ์สองรูปแบบเพื่อเข้าถึงแต่ละองค์ประกอบของอาร์เรย์ ทั้งสองอย่างนั้นดีกว่ากันและคุณควรทำความคุ้นเคยกับทั้งคู่

เลขคณิตของตัวชี้

กำหนดตัวชี้pไปยังองค์ประกอบแรกของอาร์เรย์นิพจน์p+iให้ผลลัพธ์ตัวชี้ไปยังองค์ประกอบ i-th ของอาร์เรย์ ด้วยการยกเลิกการลงทะเบียนตัวชี้หลังจากนั้นเราสามารถเข้าถึงแต่ละองค์ประกอบ:

std::cout << *(x+3) << ", " << *(x+7) << std::endl;

หากxหมายถึงอาร์เรย์การสลายตัวของอาเรย์กับตัวชี้จะเริ่มขึ้นเนื่องจากการเพิ่มอาร์เรย์และจำนวนเต็มไม่มีความหมาย (ไม่มีการดำเนินการบวกในอาร์เรย์) แต่การเพิ่มตัวชี้และจำนวนเต็มเหมาะสม:

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*

(โปรดทราบว่าตัวชี้ที่สร้างขึ้นโดยนัยไม่มีชื่อดังนั้นฉันจึงเขียนx+0เพื่อระบุตัวตน)

หากในทางกลับกันxแสดงว่าตัวชี้ไปยังองค์ประกอบแรก (หรืออื่น ๆ ) ของอาร์เรย์แล้วการสลายตัวของอาเรย์ต่อตัวชี้ไม่จำเป็นเพราะตัวชี้ที่iจะถูกเพิ่มอยู่แล้ว:

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+

โปรดสังเกตว่าในกรณีที่ปรากฎxเป็นตัวแปรตัวชี้(มองเห็นได้โดยกล่องเล็ก ๆ ถัดจากx) แต่ก็อาจเป็นผลมาจากฟังก์ชั่นที่ส่งกลับตัวชี้ (หรือการแสดงออกประเภทอื่น ๆT*)

ผู้ประกอบการสร้างดัชนี

เนื่องจากไวยากรณ์*(x+i)เป็นบิตที่เงอะงะ C ++ จึงให้ทางเลือกไวยากรณ์x[i]:

std::cout << x[3] << ", " << x[7] << std::endl;

เนื่องจากความจริงที่ว่าการเพิ่มคือการสับเปลี่ยนรหัสต่อไปนี้จึงเหมือนกันทุกประการ:

std::cout << 3[x] << ", " << 7[x] << std::endl;

คำจำกัดความของผู้ดำเนินการจัดทำดัชนีนำไปสู่การเทียบเท่าที่น่าสนใจต่อไปนี้:

&x[i]  ==  &*(x+i)  ==  x+i

แต่&x[0]โดยทั่วไปจะไม่xเทียบเท่ากับ อดีตเป็นตัวชี้หลังอาร์เรย์ เฉพาะเมื่อบริบททริกเกอร์การสลายตัวของอาเรย์ต่อตัวชี้สามารถxและ&x[0]ใช้แทนกันได้ ตัวอย่างเช่น:

T* p = &array[0];  // rewritten as &*(array+0), decay happens due to the addition
T* q = array;      // decay happens due to the assignment

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

ช่วง

อาร์เรย์ของชนิดT[n]มีnองค์ประกอบของการจัดทำดัชนีจาก0ไปn-1; nมีองค์ประกอบไม่ และเพื่อสนับสนุนช่วงเปิดครึ่ง (ที่จุดเริ่มต้นรวมและสิ้นสุดเป็นเอกสิทธิ์ ), C ++ ช่วยให้การคำนวณของตัวชี้ไปยังองค์ประกอบ n-th (ไม่มีอยู่) แต่มันผิดกฎหมายที่จะตรวจสอบตัวชี้นั้น:

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*

ตัวอย่างเช่นหากคุณต้องการเรียงลำดับอาร์เรย์ทั้งสองอย่างต่อไปนี้จะทำงานได้ดีเท่า ๆ กัน:

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);

โปรดทราบว่ามันเป็นสิ่งผิดกฎหมายที่จะให้&x[n]เป็นอาร์กิวเมนต์ที่สองเนื่องจากสิ่งนี้เทียบเท่ากับ&*(x+n)และ sub-expression *(x+n)เทคนิคเรียกพฤติกรรมที่ไม่ได้กำหนดใน C ++ (แต่ไม่ใช่ C99)

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


กรณีที่อาร์เรย์ไม่สลายตัวลงในตัวชี้แสดงไว้ที่นี่เพื่อการอ้างอิง
ตำนาน 2k

@fredoverflow ในส่วน Access หรือ Ranges อาจคุ้มค่าที่จะกล่าวถึงว่า C-arrays ทำงานร่วมกับ C ++ 11 ช่วงสำหรับลูป
gnzlbg

135

โปรแกรมเมอร์มักสับสนอาเรย์หลายมิติกับอาเรย์ของพอยน์เตอร์

อาร์เรย์หลายมิติ

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

อาร์เรย์หลายมิติที่มีชื่อ

เมื่อใช้อาร์เรย์หลายมิติที่มีชื่อจะต้องทราบมิติทั้งหมดในเวลารวบรวม:

int H = read_int();
int W = read_int();

int connect_four[6][7];   // okay

int connect_four[H][7];   // ISO C++ forbids variable length array
int connect_four[6][W];   // ISO C++ forbids variable length array
int connect_four[H][W];   // ISO C++ forbids variable length array

นี่คือลักษณะของอาร์เรย์หลายมิติที่มีชื่อในหน่วยความจำ:

              +---+---+---+---+---+---+---+
connect_four: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+

โปรดทราบว่ากริด 2D เช่นด้านบนเป็นเพียงการสร้างภาพข้อมูลที่เป็นประโยชน์ จากมุมมองของ C ++ หน่วยความจำคือลำดับ "แบน" ของไบต์ องค์ประกอบของอาร์เรย์หลายมิติจะถูกเก็บไว้ในลำดับสำคัญของแถว นั่นคือconnect_four[0][6]และconnect_four[1][0]เป็นเพื่อนบ้านในหน่วยความจำ ในความเป็นจริงconnect_four[0][7]และconnect_four[1][0]แสดงถึงองค์ประกอบเดียวกัน! ซึ่งหมายความว่าคุณสามารถใช้อาร์เรย์หลายมิติและถือว่าเป็นอาร์เรย์ขนาดใหญ่และมิติเดียว:

int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);

อาร์เรย์หลายมิตินิรนาม

ด้วยอาร์เรย์หลายมิติที่ไม่ระบุชื่อมิติทั้งหมดยกเว้นมิติแรกต้องรู้ ณ เวลารวบรวม

int (*p)[7] = new int[6][7];   // okay
int (*p)[7] = new int[H][7];   // okay

int (*p)[W] = new int[6][W];   // ISO C++ forbids variable length array
int (*p)[W] = new int[H][W];   // ISO C++ forbids variable length array

นี่คือลักษณะของอาเรย์หลายมิติแบบไม่ระบุชื่อในหน่วยความจำ:

              +---+---+---+---+---+---+---+
        +---> |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |
      +-|-+
   p: | | |
      +---+

โปรดทราบว่าอาร์เรย์นั้นยังคงถูกจัดสรรเป็นบล็อกเดียวในหน่วยความจำ

อาร์เรย์ของพอยน์เตอร์

คุณสามารถเอาชนะข้อ จำกัด ของความกว้างคงที่โดยแนะนำการอ้อมอีกระดับ

อาร์เรย์ที่มีชื่อของพอยน์เตอร์

นี่คืออาร์เรย์ที่ระบุชื่อของห้าพอยน์เตอร์ซึ่งเริ่มต้นด้วยอาร์เรย์นิรนามที่มีความยาวต่างกัน:

int* triangle[5];
for (int i = 0; i < 5; ++i)
{
    triangle[i] = new int[5 - i];
}

// ...

for (int i = 0; i < 5; ++i)
{
    delete[] triangle[i];
}

และนี่คือลักษณะของหน่วยความจำ:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
          +---+---+---+---+---+

เนื่องจากแต่ละบรรทัดได้รับการจัดสรรแยกกันในขณะนี้การดูอาร์เรย์ 2D เนื่องจาก 1D อาร์เรย์ไม่ทำงานอีกต่อไป

อาร์เรย์พอยน์เตอร์ที่ไม่ระบุชื่อ

นี่คืออาเรย์ที่ไม่ระบุตัวตนของพอยน์เตอร์ 5 ตัว (หรือจำนวนอื่น ๆ ) ที่เริ่มต้นด้วยอาร์เรย์นิรนามที่มีความยาวต่างกัน:

int n = calculate_five();   // or any other number
int** p = new int*[n];
for (int i = 0; i < n; ++i)
{
    p[i] = new int[n - i];
}

// ...

for (int i = 0; i < n; ++i)
{
    delete[] p[i];
}
delete[] p;   // note the extra delete[] !

และนี่คือลักษณะของหน่วยความจำ:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
          | | | | | | | | | | |
          +---+---+---+---+---+
            ^
            |
            |
          +-|-+
       p: | | |
          +---+

การแปลง

การสลายตัวของอาเรย์ - ทู - พอยน์เตอร์จะรวมไปถึงอาเรย์ของอาร์เรย์และพอยน์เตอร์พอยน์เตอร์โดยธรรมชาติ:

int array_of_arrays[6][7];
int (*pointer_to_array)[7] = array_of_arrays;

int* array_of_pointers[6];
int** pointer_to_pointer = array_of_pointers;

อย่างไรก็ตามไม่มีการแปลงโดยปริยายจากไปT[h][w] T**หากการแปลงโดยนัยนั้นมีอยู่ผลลัพธ์จะเป็นตัวชี้ไปยังองค์ประกอบแรกของอาร์เรย์ของพhอยน์เตอร์ไปT(แต่ละรายการชี้ไปที่องค์ประกอบแรกของบรรทัดในอาร์เรย์ 2D ดั้งเดิม) แต่อาร์เรย์ตัวชี้นั้นไม่มีอยู่ที่ใดใน หน่วยความจำ หากคุณต้องการการแปลงเช่นนี้คุณต้องสร้างและเติมอาเรย์ตัวชี้ที่ต้องการด้วยตนเอง:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = connect_four[i];
}

// ...

delete[] p;

โปรดทราบว่าสิ่งนี้จะสร้างมุมมองของอาเรย์หลายมิติดั้งเดิม หากคุณต้องการคัดลอกแทนคุณจะต้องสร้างอาร์เรย์พิเศษและคัดลอกข้อมูลด้วยตัวเอง:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = new int[7];
    std::copy(connect_four[i], connect_four[i + 1], p[i]);
}

// ...

for (int i = 0; i < 6; ++i)
{
    delete[] p[i];
}
delete[] p;

เป็นข้อเสนอแนะ: คุณควรชี้ให้เห็นว่าint connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];เช่นเดียวกับint (*p)[W] = new int[6][W];และint (*p)[W] = new int[H][W];เป็นงบที่ถูกต้องเมื่อHและWเป็นที่รู้จักกันที่รวบรวมเวลา
RobertS สนับสนุน Monica Cellio

88

การมอบหมาย

ด้วยเหตุผลเฉพาะไม่สามารถกำหนดอาร์เรย์ให้กันได้ ใช้std::copyแทน:

#include <algorithm>

// ...

int a[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int b[8];
std::copy(a + 0, a + 8, b);

สิ่งนี้มีความยืดหยุ่นมากกว่าสิ่งที่การจัดสรรอาเรย์ที่แท้จริงสามารถให้ได้เพราะเป็นไปได้ที่จะคัดลอกชิ้นส่วนของอาร์เรย์ขนาดใหญ่ลงในอาร์เรย์ขนาดเล็ก std::copyมักเป็นผู้เชี่ยวชาญสำหรับประเภทดั้งเดิมเพื่อให้มีประสิทธิภาพสูงสุด มันไม่น่าที่จะstd::memcpyทำงานได้ดีขึ้น หากมีข้อสงสัยให้วัด

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

ผ่านพารามิเตอร์

ไม่สามารถส่งค่าอาร์เรย์ได้ คุณสามารถส่งผ่านตัวชี้หรืออ้างอิงได้

ผ่านตัวชี้

เนื่องจากอาร์เรย์ไม่สามารถส่งผ่านค่าได้โดยปกติแล้วตัวชี้ไปยังองค์ประกอบแรกจะถูกส่งผ่านตามค่าแทน ซึ่งมักเรียกว่า "การส่งผ่านตัวชี้" เนื่องจากขนาดของอาร์เรย์ไม่สามารถเรียกคืนได้ผ่านตัวชี้นั้นคุณจะต้องผ่านพารามิเตอร์ตัวที่สองเพื่อระบุขนาดของอาร์เรย์ (ตัวแก้ปัญหาคลาสสิก C) หรือตัวชี้ที่สองที่ชี้ไปหลังจากองค์ประกอบสุดท้ายของอาร์เรย์ (โซลูชันตัววนซ้ำ C ++) :

#include <numeric>
#include <cstddef>

int sum(const int* p, std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

int sum(const int* p, const int* q)
{
    return std::accumulate(p, q, 0);
}

ในฐานะทางเลือกทางไวยากรณ์คุณสามารถประกาศพารามิเตอร์เป็นT p[]และมันหมายถึงสิ่งเดียวกันกับT* p ในบริบทของรายการพารามิเตอร์เท่านั้น :

int sum(const int p[], std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

คุณสามารถคิดว่าคอมไพเลอร์เป็นเขียนT p[]ไปในบริบทของรายการพารามิเตอร์เท่านั้นT *p กฎพิเศษนี้มีส่วนรับผิดชอบต่อความสับสนทั้งหมดเกี่ยวกับอาร์เรย์และพอยน์เตอร์ ในบริบทอื่น ๆ การประกาศบางสิ่งบางอย่างในฐานะอาร์เรย์หรือเป็นตัวชี้ทำให้เกิดความแตกต่างอย่างมาก

น่าเสียดายที่คุณยังสามารถระบุขนาดในพารามิเตอร์อาร์เรย์ซึ่งคอมไพเลอร์จะถูกละเว้น นั่นคือสามลายเซ็นต่อไปนี้เทียบเท่ากันตามที่ระบุโดยข้อผิดพลาดของคอมไพเลอร์:

int sum(const int* p, std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[], std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[8], std::size_t n)   // the 8 has no meaning here

ผ่านการอ้างอิง

อาร์เรย์ยังสามารถส่งผ่านโดยการอ้างอิง:

int sum(const int (&a)[8])
{
    return std::accumulate(a + 0, a + 8, 0);
}

ในกรณีนี้ขนาดอาร์เรย์มีความสำคัญ เนื่องจากการเขียนฟังก์ชั่นที่ยอมรับเฉพาะอาร์เรย์ขององค์ประกอบ 8 ประการนั้นเป็นการใช้เพียงเล็กน้อยโปรแกรมเมอร์จึงมักจะเขียนฟังก์ชันดังกล่าวเป็นเทมเพลต:

template <std::size_t n>
int sum(const int (&a)[n])
{
    return std::accumulate(a + 0, a + n, 0);
}

โปรดทราบว่าคุณสามารถเรียกใช้เทมเพลตฟังก์ชันดังกล่าวที่มีอาร์เรย์จำนวนเต็มจริงเท่านั้นไม่ใช่ด้วยตัวชี้ไปยังจำนวนเต็ม ขนาดของอาเรย์จะถูกอนุมานโดยอัตโนมัติและสำหรับทุกขนาดnฟังก์ชันที่แตกต่างจะถูกสร้างจากแม่แบบ คุณยังสามารถเขียนเทมเพลตฟังก์ชั่นที่มีประโยชน์มากที่เป็นนามธรรมจากทั้งชนิดองค์ประกอบและขนาด


2
อาจจะคุ้มค่าที่จะเพิ่มหมายเหตุว่าแม้สรรพสิ่งในvoid foo(int a[3]) aนั้นจะดูเหมือนว่ามีการส่งผ่านอาร์เรย์ตามค่าการแก้ไขaด้านในของfooจะปรับเปลี่ยนอาร์เรย์เดิม สิ่งนี้ควรมีความชัดเจนเนื่องจากไม่สามารถคัดลอกอาร์เรย์ได้ แต่อาจคุ้มค่าที่จะเสริม
gnzlbg

C ++ 20 มีranges::copy(a, b)
LF

int sum( int size_, int a[size_]);- จาก (ฉันคิดว่า) C99 เป็นต้นไป
Chef Gladiator

73

5. ข้อผิดพลาดทั่วไปเมื่อใช้อาร์เรย์

5.1 Pitfall: เชื่อใจในการเชื่อมโยงแบบไม่ปลอดภัย

ตกลงคุณได้รับการบอกกล่าวหรือพบตัวเองแล้วว่า globals (ตัวแปรขอบเขตเนมสเปซที่สามารถเข้าถึงได้นอกหน่วยการแปล) คือ Evil ™ แต่คุณรู้หรือไม่ว่า Evil ™เป็นอย่างไร พิจารณาโปรแกรมด้านล่างประกอบด้วยสองไฟล์ [main.cpp] และ [numbers.cpp]:

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

ใน Windows 7 จะรวบรวมและเชื่อมโยงกับทั้ง MinGW g ++ 4.4.1 และ Visual C ++ 10.0

เนื่องจากประเภทไม่ตรงกันโปรแกรมจึงขัดข้องเมื่อคุณเรียกใช้

กล่องโต้ตอบขัดข้อง Windows 7

คำอธิบายแบบเป็นทางการ: โปรแกรมมีพฤติกรรมที่ไม่ได้กำหนด (UB) และแทนที่จะล้มเหลวจึงสามารถแขวนหรืออาจจะไม่ทำอะไรเลยหรือส่งอีเมลที่ส่งไปยังประธานาธิบดีของสหรัฐอเมริการัสเซียอินเดีย จีนและสวิตเซอร์แลนด์และทำให้ Nasal Daemons ลอยออกจากจมูกคุณ

คำอธิบายในการปฏิบัติ: ในmain.cppอาร์เรย์จะถือว่าเป็นตัวชี้วางไว้ที่ที่อยู่เดียวกับอาร์เรย์ สำหรับปฏิบัติการแบบ 32 บิตนี่หมายความว่าintค่าแรก ในอาร์เรย์นั้นจะถือว่าเป็นตัวชี้ เช่นในตัวแปรมีหรือดูเหมือนจะมี, สิ่งนี้ทำให้โปรแกรมเข้าถึงหน่วยความจำลงที่ด้านล่างสุดของพื้นที่ที่อยู่ซึ่งสงวนไว้ตามปกติและทำให้เกิดกับดัก ผลลัพธ์: คุณได้รับความผิดพลาดmain.cppnumbers(int*)1

คอมไพเลอร์อยู่ภายใต้สิทธิ์อย่างเต็มที่ที่จะไม่วินิจฉัยข้อผิดพลาดนี้เนื่องจาก C ++ 11 §3.5 / 10 บอกว่าเกี่ยวกับข้อกำหนดของประเภทที่เข้ากันได้สำหรับการประกาศ

[N3290 §3.5 / 10]
การละเมิดกฎนี้กับข้อมูลประจำตัวของประเภทไม่จำเป็นต้องมีการวินิจฉัย

ย่อหน้าเดียวกันมีรายละเอียดเกี่ยวกับรูปแบบที่อนุญาต:

... การประกาศสำหรับวัตถุอาร์เรย์สามารถระบุประเภทของอาร์เรย์ที่แตกต่างกันโดยการมีหรือไม่มีของขอบเขตอาร์เรย์หลัก (8.3.4)

รูปแบบที่อนุญาตนี้ไม่รวมถึงการประกาศชื่อเป็นอาร์เรย์ในหนึ่งหน่วยการแปลและเป็นตัวชี้ในหน่วยการแปลอื่น

5.2 หลุมพราง: ทำการเพิ่มประสิทธิภาพก่อนวัยอันควร ( memset& เพื่อน)

ยังไม่ได้เขียน

5.3 ข้อผิดพลาด: การใช้สำนวน C เพื่อรับจำนวนองค์ประกอบ

ด้วยประสบการณ์ C ลึกเป็นเรื่องธรรมดาที่จะเขียน ...

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

ตั้งแต่arrayสูญสลายไปชี้ไปยังองค์ประกอบแรกที่จำเป็นต้องแสดงออกยังสามารถเขียนเป็นsizeof(a)/sizeof(a[0]) sizeof(a)/sizeof(*a)มันมีความหมายเหมือนกันและไม่ว่ามันจะเขียนอย่างไรมันเป็นสำนวน Cในการค้นหาองค์ประกอบจำนวนของอาร์เรย์

ข้อผิดพลาดหลัก: สำนวน C ไม่ปลอดภัย ตัวอย่างเช่นรหัส ...

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

ส่งผ่านตัวชี้ไปที่N_ITEMSและดังนั้นส่วนใหญ่จะสร้างผลลัพธ์ที่ผิด คอมไพล์เป็นไฟล์ปฏิบัติการแบบ 32 บิตใน Windows 7 มันสร้าง ...

7 องค์ประกอบการโทรแสดง ...
1 องค์ประกอบ

  1. คอมไพเลอร์ปรับเปลี่ยนเพียงแค่int const a[7]int const a[]
  2. คอมไพเลอร์ปรับเปลี่ยนไปint const a[]int const* a
  3. N_ITEMS ถูกเรียกใช้ด้วยตัวชี้
  4. สำหรับปฏิบัติการแบบ 32 บิตsizeof(array)(ขนาดของตัวชี้) เท่ากับ 4
  5. sizeof(*array)เทียบเท่ากับsizeof(int)ซึ่งสำหรับการปฏิบัติการแบบ 32 บิตก็เช่นกัน

ในการตรวจสอบข้อผิดพลาดนี้ในเวลาทำงานคุณสามารถทำ ...

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7 องค์ประกอบการโทรที่แสดง ...
การยืนยันล้มเหลว: ("N_ITEMS ต้องใช้อาร์เรย์จริงเป็นอาร์กิวเมนต์", typeid (a)! = typeid (& * a)), ไฟล์ runtime_detect ion.cpp, บรรทัดที่ 16

แอปพลิเคชั่นนี้ร้องขอให้ Runtime ทำการยกเลิกในลักษณะที่ผิดปกติ
โปรดติดต่อทีมสนับสนุนของแอปพลิเคชันสำหรับข้อมูลเพิ่มเติม

การตรวจจับข้อผิดพลาดรันไทม์นั้นดีกว่าการตรวจจับใด ๆ แต่มันเสียเวลาประมวลผลเพียงเล็กน้อยและอาจทำให้โปรแกรมเมอร์เสียเวลามากขึ้น ดีกว่าด้วยการตรวจจับในเวลารวบรวม! และถ้าคุณยินดีที่จะไม่สนับสนุนอาร์เรย์ประเภทท้องถิ่นด้วย C ++ 98 คุณสามารถทำได้ดังนี้:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

การคอมไพล์คำจำกัดความนี้ถูกแทนที่ด้วยโปรแกรมที่สมบูรณ์แรกด้วย g ++, ฉันได้ ...

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: ในฟังก์ชั่น 'void display (const int *)':
compile_time_detection.cpp: 14: ข้อผิดพลาด: ไม่มีฟังก์ชันที่ตรงกันสำหรับการเรียกไปที่ 'n_items (const int * &)

M: \ count> _

วิธีการทำงาน: อาร์เรย์ถูกส่งผ่านโดยอ้างอิงถึงn_itemsและดังนั้นจึงไม่สลายตัวไปยังตัวชี้ไปยังองค์ประกอบแรกและฟังก์ชั่นก็สามารถคืนจำนวนองค์ประกอบที่ระบุโดยประเภท

ด้วย C ++ 11 คุณสามารถใช้สิ่งนี้ได้เช่นกันสำหรับอาร์เรย์ของชนิดโลคัลและเป็นชนิดที่ปลอดภัย C ++ สำนวนสำหรับการค้นหาจำนวนองค์ประกอบของอาร์เรย์

ข้อผิดพลาด 5.4 C ++ 11 และ C ++ 14 ข้อผิดพลาด: การใช้constexprฟังก์ชันขนาดอาร์เรย์

ด้วย C ++ 11 และใหม่กว่านั้นเป็นเรื่องธรรมดา แต่คุณจะเห็นว่าอันตราย! เพื่อแทนที่ฟังก์ชั่น C ++ 03

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

กับ

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

ที่การเปลี่ยนแปลงที่สำคัญคือการใช้constexprซึ่งจะช่วยให้ฟังก์ชั่นนี้ในการผลิตคงที่รวบรวมเวลา

ตัวอย่างเช่นในทางตรงกันข้ามกับฟังก์ชั่น C ++ 03 ค่าคงที่เวลาการคอมไพล์ดังกล่าวสามารถใช้เพื่อประกาศอาร์เรย์ที่มีขนาดเดียวกันกับอีกขนาดหนึ่ง:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

แต่พิจารณารหัสนี้โดยใช้constexprรุ่น:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

หลุมพราง: ณ เดือนกรกฎาคม 2558 ข้างต้นคอมไพล์ด้วย MinGW-64 5.1.0 ด้วย -pedantic-errorsและการทดสอบกับคอมไพเลอร์ออนไลน์ที่gcc.godbolt.org/ , ด้วย clang 3.0 และ clang 3.2 แต่ไม่ใช่ clang 3.3, 3.4 1, 3.5.0, 3.5.1, 3.6 (rc1) หรือ 3.7 (ทดลอง) และที่สำคัญสำหรับแพลตฟอร์ม Windows นั้นไม่ได้คอมไพล์ด้วย Visual C ++ 2015 เหตุผลคือคำสั่ง C ++ 11 / C ++ 14 เกี่ยวกับการใช้การอ้างอิงในconstexprนิพจน์:

C ++ 11 C ++ 14 $ 5.19 / 2 เก้าTHรีบ

เงื่อนไขการแสดงออก eเป็นแสดงออกคงหลักเว้นแต่การประเมินeตามกฎของเครื่องนามธรรม (1.9) จะประเมินหนึ่งของการแสดงออกดังต่อไปนี้:
        ⋮

  • ID-แสดงออกที่หมายถึงตัวแปรหรือข้อมูลสมาชิกของชนิดการอ้างอิงเว้นแต่อ้างอิงมีการเริ่มต้นก่อนหน้านี้และทั้ง
    • มันเริ่มต้นได้ด้วยการแสดงออกอย่างต่อเนื่องหรือ
    • มันเป็นสมาชิกข้อมูลที่ไม่คงที่ของวัตถุที่อายุการใช้งานเริ่มขึ้นในการประเมิน e;

หนึ่งสามารถเขียน verbose มากขึ้น

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

… แต่สิ่งนี้ล้มเหลวเมื่อCollectionไม่ใช่อาเรย์ดิบ

ในการจัดการกับคอลเลกชันที่ไม่ใช่อาร์เรย์เราต้องการความสามารถในการโอเวอร์โหลดของ n_itemsฟังก์ชั่น แต่สำหรับการคอมไพล์เวลาต้องใช้การรวบรวมเวลาแทนขนาดอาเรย์ และคลาสสิก C ++ 03 วิธีการแก้ปัญหาซึ่งทำงานได้ดียังอยู่ใน C ++ 11 และ C ++ 14 คือการปล่อยให้รายงานผลการทำงานของตนไม่ได้เป็นค่าผ่านทาง แต่ผลการทำงานของประเภท ตัวอย่างเช่นนี้:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

เกี่ยวกับตัวเลือกประเภทส่งคืนสำหรับstatic_n_items: รหัสนี้ไม่ได้ใช้std::integral_constant เนื่องจากstd::integral_constantผลลัพธ์จะแสดงเป็นconstexprค่าโดยตรงเพื่อนำเสนอปัญหาเดิมอีกครั้ง แทนที่จะเป็นSize_carrierคลาสหนึ่งสามารถให้ฟังก์ชันส่งคืนการอ้างอิงไปยังอาร์เรย์ได้โดยตรง อย่างไรก็ตามทุกคนไม่คุ้นเคยกับไวยากรณ์นั้น

เกี่ยวกับการตั้งชื่อ: ส่วนหนึ่งของโซลูชันนี้สำหรับปัญหาconstexpr-invalid-Due-to-Reference คือการเลือกตัวเลือกการรวบรวมเวลาคงที่อย่างชัดเจน

หวังว่าปัญหา oops-there-was-a-reference-related-in-your- constexprจะได้รับการแก้ไขด้วย C ++ 17 แต่จนกระทั่งมาโครดังกล่าวSTATIC_N_ITEMSข้างต้นให้ความสะดวกในการพกพาเช่นคอมไพเลอร์ clang และ Visual C ++ ความปลอดภัย

ที่เกี่ยวข้อง: MYLIB_STATIC_N_ITEMSแมโครไม่เคารพขอบเขตเพื่อที่จะหลีกเลี่ยงการชนชื่อมันอาจจะเป็นความคิดที่ดีที่จะใช้คำนำหน้าชื่อเช่น


1
+1 การทดสอบการเข้ารหัส C ที่ยอดเยี่ยม: ฉันใช้เวลา 15 นาทีใน VC ++ 10.0 และ GCC 4.1.2 พยายามแก้ไขSegmentation fault... ในที่สุดฉันก็พบ / เข้าใจหลังจากอ่านคำอธิบายของคุณ! โปรดเขียนส่วน§5.2ของคุณ :-) ไชโย
โอลิเบอร์

ดี. หนึ่ง nit - ประเภทส่งคืนสำหรับ countOf ควรเป็น size_t แทนที่จะเป็น ptrdiff_t อาจเป็นมูลค่าการกล่าวขวัญว่าใน C ++ 11/14 ควรเป็น constexpr และ noexcept
Ricky65

@ Ricky65: ขอบคุณสำหรับการกล่าวถึงข้อพิจารณา C ++ 11 การสนับสนุนฟีเจอร์เหล่านี้มาช้าสำหรับ Visual C ++ เกี่ยวกับsize_tที่ไม่มีประโยชน์ที่ฉันรู้สำหรับแพลตฟอร์มที่ทันสมัย ​​แต่ก็มีปัญหามากมายเนื่องจากกฎการแปลงประเภทโดยนัยของ C และ C ++ นั่นคือมีการใช้มากจงใจเพื่อหลีกเลี่ยงปัญหาที่มีptrdiff_t size_tอย่างไรก็ตามสิ่งหนึ่งที่ควรทราบคือ g ++ มีปัญหากับการจับคู่ขนาดอาร์เรย์กับพารามิเตอร์เทมเพลตยกเว้นว่าเป็นsize_t(ฉันไม่คิดว่าปัญหาเฉพาะคอมไพเลอร์ที่ไม่มีsize_tความสำคัญ แต่ YMMV)
ไชโยและ hth - Alf

@Alf ใน Standard Working Draft (N3936) 8.3.4 ฉันอ่าน - ขอบเขตของอาร์เรย์คือ ... "นิพจน์คงที่ที่แปลงแล้วของ type std :: size_t และค่าของมันจะมากกว่าศูนย์"
Ricky65

@Ricky: หากคุณอ้างถึงความไม่สอดคล้องกันคำสั่งนี้ไม่มีอยู่ในมาตรฐาน C ++ 11 ปัจจุบันดังนั้นจึงเป็นการยากที่จะคาดเดาบริบท แต่ความขัดแย้ง (อาร์เรย์ที่จัดสรรแบบไดนามิกอาจมีค่าเท่ากับ 0 ต่อ C + +11 §5.3.4 / 7) อาจไม่สิ้นสุดใน C ++ 14 ร่างเป็นเพียง: ร่าง หากคุณถามว่า "มัน" หมายถึงอะไรมันหมายถึงการแสดงออกดั้งเดิมไม่ใช่การแปลง ถ้าในมือที่สามคุณพูดถึงสิ่งนี้เพราะคุณคิดว่าประโยคดังกล่าวอาจหมายความว่าเราควรใช้size_tเพื่อแสดงขนาดของอาร์เรย์ไม่แน่นอน
ไชโยและ hth - Alf

72

การสร้างและการเริ่มต้นอาร์เรย์

เช่นเดียวกับวัตถุ C ++ ชนิดอื่น ๆ อาร์เรย์สามารถจัดเก็บได้โดยตรงในตัวแปรที่มีชื่อ (ขนาดจะต้องเป็นค่าคงที่เวลาคอมไพล์; C ++ ไม่รองรับ VLAs ) หรือสามารถเก็บไว้โดยไม่ระบุชื่อบนฮีปและเข้าถึงทางอ้อมผ่าน พอยน์เตอร์ (สามารถคำนวณขนาดที่รันไทม์ได้เท่านั้น)

อาร์เรย์อัตโนมัติ

อาร์เรย์อัตโนมัติ (อาร์เรย์ที่มีชีวิต "บนสแต็ก") จะถูกสร้างขึ้นในแต่ละครั้งที่การไหลของการควบคุมผ่านคำจำกัดความของตัวแปรอาเรย์ภายในแบบไม่คงที่:

void foo()
{
    int automatic_array[8];
}

การเริ่มต้นจะดำเนินการในลำดับที่น้อยไปมาก โปรดทราบว่าค่าเริ่มต้นขึ้นอยู่กับประเภทองค์ประกอบT:

  • ถ้าTเป็นPOD (เหมือนintในตัวอย่างด้านบน) จะไม่มีการเริ่มต้น
  • มิฉะนั้นตัวสร้างTเริ่มต้นของการเริ่มต้นองค์ประกอบทั้งหมด
  • หากTไม่มีตัวสร้างเริ่มต้นที่สามารถเข้าถึงได้โปรแกรมจะไม่คอมไพล์

อีกทางเลือกหนึ่งค่าเริ่มต้นสามารถระบุได้อย่างชัดเจนในarray initializerซึ่งเป็นรายการที่คั่นด้วยเครื่องหมายจุลภาคล้อมรอบด้วยวงเล็บปีกกา

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};

เนื่องจากในกรณีนี้จำนวนองค์ประกอบใน array initializer เท่ากับขนาดของอาร์เรย์การระบุขนาดด้วยตนเองจึงซ้ำซ้อน คอมไพเลอร์สามารถสรุปได้โดยอัตโนมัติ:

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced

นอกจากนี้ยังเป็นไปได้ที่จะระบุขนาดและจัดเตรียม initializer ของอาร์เรย์ที่สั้นกว่า:

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced

ในกรณีที่องค์ประกอบที่เหลือเป็นศูนย์ที่เริ่ม โปรดทราบว่า C ++ อนุญาตให้อาเรย์เริ่มต้นว่างเปล่า (องค์ประกอบทั้งหมดเป็นศูนย์เริ่มต้น) ในขณะที่ C89 ไม่ (ต้องมีค่าอย่างน้อยหนึ่งค่า) นอกจากนี้โปรดทราบว่าการเริ่มต้นอาร์เรย์สามารถใช้เพื่อเริ่มต้นอาร์เรย์เท่านั้น ไม่สามารถใช้ในภายหลังได้ในการมอบหมาย

อาร์เรย์แบบคงที่

อาร์เรย์แบบคงที่ (อาร์เรย์ที่อาศัยอยู่ "ในส่วนข้อมูล") เป็นตัวแปรอาร์เรย์ภายในที่กำหนดด้วยstaticคำหลักและตัวแปรอาร์เรย์ที่ขอบเขตเนมสเปซ ("ตัวแปรทั่วโลก"):

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}

(โปรดทราบว่าตัวแปรที่ขอบเขตเนมสเปซจะคงที่โดยปริยายการเพิ่มstaticคำหลักลงในคำจำกัดความของพวกเขามีความหมายที่แตกต่างกันอย่างสิ้นเชิงและเลิกใช้แล้ว )

นี่คือวิธีที่อาร์เรย์แบบคงที่ทำงานแตกต่างจากอาร์เรย์อัตโนมัติ:

  • อาร์เรย์แบบสแตติกที่ไม่มีตัวกำหนดค่าเริ่มต้นของอาเรย์จะถูกกำหนดค่าเริ่มต้นเป็นศูนย์ก่อนการกำหนดค่าเริ่มต้นใด ๆ
  • อาร์เรย์ POD แบบสแตติกจะถูกกำหนดค่าเริ่มต้นอย่างแน่นอนหนึ่งครั้งและโดยทั่วไปค่าเริ่มต้นจะถูกนำเข้าสู่การปฏิบัติการซึ่งในกรณีนี้ไม่มีค่าใช้จ่ายในการเริ่มต้นที่รันไทม์ นี่ไม่ใช่วิธีการแก้ปัญหาที่มีประสิทธิภาพในการใช้พื้นที่มากที่สุดเสมอไปและไม่จำเป็นต้องใช้ตามมาตรฐาน
  • อาเรย์ที่ไม่ใช่ POD แบบคงที่จะเริ่มต้นในครั้งแรกที่การไหลของการควบคุมผ่านคำจำกัดความของพวกเขา ในกรณีของอาร์เรย์แบบสแตติกท้องถิ่นอาจไม่เกิดขึ้นหากฟังก์ชันไม่เคยถูกเรียกใช้

(ไม่มีสิ่งใดที่กล่าวมาข้างต้นเฉพาะกับอาร์เรย์กฎเหล่านี้มีผลบังคับใช้กับวัตถุคงที่ประเภทอื่น ๆ อย่างเท่าเทียมกัน)

สมาชิกข้อมูล Array

สมาชิกข้อมูล Array ถูกสร้างขึ้นเมื่อมีการสร้างวัตถุที่เป็นเจ้าของ น่าเสียดายที่ C ++ 03 ไม่ได้หมายถึงการเริ่มต้นอาร์เรย์ในรายการ initializer สมาชิกดังนั้นการเริ่มต้นจะต้องแกล้งด้วยการมอบหมาย:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};

หรือคุณสามารถกำหนดอาเรย์อัตโนมัติในเนื้อความคอนสตรัคเตอร์และคัดลอกองค์ประกอบที่อยู่เหนือ:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        int local_array[] = {2, 3, 5, 7, 11, 13, 17, 19};
        std::copy(local_array + 0, local_array + 8, primes + 0);
    }
};

ใน C ++ 0x อาร์เรย์สามารถเริ่มต้นได้ในรายการ initializer สมาชิกด้วยการกำหนดค่าเริ่มต้นที่สม่ำเสมอ :

class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};

นี่เป็นทางออกเดียวที่ทำงานกับประเภทองค์ประกอบที่ไม่มีตัวสร้างเริ่มต้น

อาร์เรย์แบบไดนามิก

อาร์เรย์แบบไดนามิกไม่มีชื่อดังนั้นวิธีเดียวในการเข้าถึงมันคือผ่านตัวชี้ เนื่องจากพวกเขาไม่มีชื่อฉันจะเรียกพวกเขาว่า "อาร์เรย์ที่ไม่ระบุชื่อ" นับจากนี้เป็นต้นไป

ใน C อาร์เรย์ที่ไม่ระบุตัวตนจะถูกสร้างขึ้นผ่านmallocและเพื่อน ๆ ใน C ++ อาร์เรย์นิรนามจะถูกสร้างขึ้นโดยใช้new T[size]ไวยากรณ์ที่ส่งกลับตัวชี้ไปยังองค์ประกอบแรกของอาร์เรย์ที่ไม่ระบุชื่อ:

std::size_t size = compute_size_at_runtime();
int* p = new int[size];

ASCII ต่อไปนี้แสดงให้เห็นถึงรูปแบบหน่วยความจำหากคำนวณขนาดเป็น 8 ที่รันไทม์:

             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+

เห็นได้ชัดว่าอาร์เรย์ที่ไม่ระบุชื่อนั้นต้องการหน่วยความจำมากกว่าอาร์เรย์ที่ตั้งชื่อเนื่องจากตัวชี้พิเศษที่ต้องจัดเก็บแยกต่างหาก (นอกจากนี้ยังมีค่าใช้จ่ายเพิ่มเติมบางอย่างในร้านค้าฟรี)

โปรดทราบว่าไม่มีการสลายตัวของอาเรย์กับตัวชี้ที่เกิดขึ้นที่นี่ แม้ว่าการประเมินnew int[size]ในความเป็นจริงสร้างอาร์เรย์ของจำนวนเต็มผลของการแสดงออกที่new int[size]เป็นอยู่แล้วชี้ไปยังจำนวนเต็มเดียว (องค์ประกอบแรก) ไม่อาร์เรย์ของจำนวนเต็มหรือตัวชี้ไปยังอาร์เรย์ของจำนวนเต็มขนาดที่ไม่รู้จัก นั่นจะเป็นไปไม่ได้เพราะระบบประเภทคงที่ต้องการขนาดอาร์เรย์ที่จะเป็นค่าคงที่เวลาคอมไพล์ (ดังนั้นฉันไม่ได้ใส่คำอธิบายประกอบอาร์เรย์ที่ไม่ระบุชื่อด้วยข้อมูลประเภทสแตติกในรูปภาพ)

เกี่ยวกับค่าเริ่มต้นสำหรับองค์ประกอบอาร์เรย์ที่ไม่ระบุชื่อจะทำงานคล้ายกับอาร์เรย์อัตโนมัติ โดยปกติแล้วอาร์เรย์ POD ที่ไม่ระบุชื่อจะไม่ได้รับการเริ่มต้น แต่มีไวยากรณ์พิเศษที่ก่อให้เกิดการเริ่มต้นค่า:

int* p = new int[some_computed_size]();

(สังเกตคู่ของวงเล็บที่อยู่ข้างหน้าเครื่องหมายอัฒภาค) อีกครั้ง C ++ 0x ทำให้กฎง่ายขึ้นและอนุญาตให้ระบุค่าเริ่มต้นสำหรับอาร์เรย์ที่ไม่ระบุชื่อด้วยการกำหนดค่าเริ่มต้นที่สม่ำเสมอ:

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };

หากคุณใช้อาเรย์แบบไม่ระบุชื่อคุณต้องปล่อยมันกลับสู่ระบบ:

delete[] p;

คุณต้องปล่อยอาร์เรย์ที่ไม่ระบุตัวตนแต่ละครั้งอย่างแน่นอนและจากนั้นไม่ต้องสัมผัสอีกครั้งในภายหลัง ไม่ปล่อยผลลัพธ์ทั้งหมดในหน่วยความจำรั่ว (หรือโดยทั่วไปขึ้นอยู่กับประเภทองค์ประกอบทรัพยากรรั่ว) และพยายามที่จะปล่อยมันหลายครั้งส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด โดยใช้รูปแบบที่ไม่ใช่อาร์เรย์delete(หรือfree) แทนdelete[]การปล่อยแถวนี้ยังไม่ได้กำหนดพฤติกรรม


2
การเลิกstaticใช้งานในขอบเขตเนมสเปซถูกลบใน C ++ 11
ตำนาน 2k

เพราะnewมันเป็นโอเปอเรเตอร์มันแน่นอนสามารถส่งคืนอาเรย์ที่ได้รับการจัดสรรโดยการอ้างอิง มีเพียงจุดใดกับมัน ...
Deduplicator

@Dupuplicator ไม่มันไม่สามารถทำได้เพราะในอดีตnewนั้นมีอายุมากกว่าการอ้างอิง
fredoverflow

@FredOverflow: ดังนั้นมีเหตุผลที่ไม่สามารถส่งคืนการอ้างอิงเป็นเพียงแตกต่างอย่างสมบูรณ์จากคำอธิบายที่เขียน
Deduplicator

2
@Dupuplicator ฉันไม่คิดว่ามีการอ้างอิงไปยังอาร์เรย์ของขอบเขตที่ไม่รู้จักอยู่แล้ว อย่างน้อย g ++ ปฏิเสธที่จะรวบรวมint a[10]; int (&r)[] = a;
fredoverflow
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.