Bagaimana cara menggunakan array di C ++?


480

C ++ array yang diwarisi dari C di mana mereka digunakan hampir di mana-mana. C ++ menyediakan abstraksi yang lebih mudah digunakan dan lebih sedikit rawan kesalahan ( std::vector<T>sejak C ++ 98 dan std::array<T, n>karena C ++ 11 ), sehingga kebutuhan untuk array tidak muncul sesering seperti di C. Namun, ketika Anda membaca legacy kode atau berinteraksi dengan perpustakaan yang ditulis dalam C, Anda harus memiliki pemahaman yang kuat tentang cara kerja array.

FAQ ini dibagi menjadi lima bagian:

  1. array pada level tipe dan elemen mengakses
  2. pembuatan dan inisialisasi array
  3. penugasan dan melewati parameter
  4. array multidimensi dan array pointer
  5. perangkap umum saat menggunakan array

Jika Anda merasa ada sesuatu yang penting tidak ada dalam FAQ ini, tulis jawaban dan tautkan di sini sebagai bagian tambahan.

Dalam teks berikut, "larik" berarti "larik C", bukan templat kelas std::array. Pengetahuan dasar tentang sintaks deklarator C diasumsikan. Perhatikan bahwa penggunaan manual newdan deleteseperti yang ditunjukkan di bawah ini sangat berbahaya dalam menghadapi pengecualian, tetapi itulah topik dari FAQ lainnya .

(Catatan: Ini dimaksudkan sebagai entri untuk FAQ C ++ Stack Overflow . Jika Anda ingin mengkritik gagasan memberikan FAQ dalam formulir ini, maka posting pada meta yang memulai semua ini akan menjadi tempat untuk melakukan itu. Jawaban untuk pertanyaan itu dipantau di chatroom C ++ , di mana ide FAQ dimulai sejak awal, jadi jawaban Anda sangat mungkin untuk dibaca oleh mereka yang mengemukakan ide itu.)


Mereka akan lebih baik jika pointer selalu menunjuk ke awal daripada di suatu tempat di tengah target mereka ...
Deduplicator

Anda harus menggunakan STL Vector karena memberikan Anda fleksibilitas yang lebih besar.
Moiz Sajid

2
Dengan kombinasi ketersediaan std::arrays, std::vectors dan gsl::spans - saya akan terus terang berharap FAQ tentang cara menggunakan array di C ++ untuk mengatakan "Sekarang, Anda dapat mulai mempertimbangkan, yah, tidak menggunakannya."
einpoklum

Jawaban:


302

Array pada tingkat jenis

Sebuah array tipe dilambangkan sebagai T[n]mana Tadalah jenis elemen dan nmerupakan positif ukuran , jumlah elemen dalam array. Jenis array adalah jenis produk dari jenis elemen dan ukurannya. Jika salah satu atau kedua bahan tersebut berbeda, Anda mendapatkan jenis yang berbeda:

#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");

Perhatikan bahwa ukuran adalah bagian dari tipe, yaitu, tipe array dengan ukuran berbeda adalah tipe yang tidak kompatibel yang sama sekali tidak ada hubungannya satu sama lain. sizeof(T[n])setara dengan n * sizeof(T).

Array-to-pointer decay

Satu-satunya "koneksi" antara T[n]dan T[m]adalah bahwa kedua jenis secara implisit dapat dikonversi menjadi T*, dan hasil konversi ini adalah penunjuk ke elemen pertama dari array. Artinya, di mana saja T*diperlukan, Anda dapat memberikan T[n], dan kompiler akan secara diam-diam memberikan pointer itu:

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

Konversi ini dikenal sebagai "peluruhan array-ke-pointer", dan merupakan sumber utama kebingungan. Ukuran array hilang dalam proses ini, karena bukan lagi bagian dari tipe ( T*). Pro: Melupakan ukuran array pada level tipe memungkinkan penunjuk untuk menunjuk ke elemen pertama array ukuran apa pun . Con: Diberikan pointer ke elemen array pertama (atau lainnya), tidak ada cara untuk mendeteksi seberapa besar array itu atau di mana tepatnya pointer menunjuk ke relatif terhadap batas-batas array. Pointer sangat bodoh .

Array bukan pointer

Compiler akan secara diam-diam menghasilkan pointer ke elemen pertama dari array kapan pun dianggap berguna, yaitu, setiap kali operasi akan gagal pada array tetapi berhasil pada pointer. Konversi ini dari array ke pointer adalah sepele, karena pointer yang dihasilkan nilai hanya alamat dari array. Perhatikan bahwa pointer tidak disimpan sebagai bagian dari array itu sendiri (atau tempat lain di memori). Array bukan pointer.

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

Satu konteks penting di mana array tidak membusuk menjadi pointer ke elemen pertamanya adalah ketika &operator diterapkan padanya. Dalam hal ini, &operator menghasilkan pointer ke seluruh array, bukan hanya pointer ke elemen pertamanya. Meskipun dalam hal ini nilai - nilai (alamat) adalah sama, sebuah penunjuk ke elemen pertama dari sebuah array dan sebuah penunjuk ke seluruh array adalah tipe yang sama sekali berbeda:

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

Seni ASCII berikut menjelaskan perbedaan ini:

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

Perhatikan bagaimana pointer ke elemen pertama hanya menunjuk ke integer tunggal (digambarkan sebagai kotak kecil), sedangkan pointer ke seluruh array menunjuk ke array 8 integer (digambarkan sebagai kotak besar).

Situasi yang sama muncul di kelas dan mungkin lebih jelas. Sebuah pointer ke objek dan pointer ke data anggota pertama memiliki yang sama nilai (alamat yang sama), namun mereka benar-benar jenis yang berbeda.

Jika Anda tidak terbiasa dengan sintaks deklarator C, tanda kurung dalam jenis int(*)[8]ini penting:

  • int(*)[8] adalah pointer ke array 8 bilangan bulat.
  • int*[8]adalah array 8 pointer, masing-masing elemen tipe int*.

Mengakses elemen

C ++ menyediakan dua variasi sintaksis untuk mengakses elemen individual dari sebuah array. Tak satu pun dari mereka lebih unggul dari yang lain, dan Anda harus membiasakan diri dengan keduanya.

Aritmatika petunjuk

Diberikan pointer pke elemen pertama array, ekspresi p+imenghasilkan pointer ke elemen ke-i dari array. Dengan mendereferensi pointer itu sesudahnya, seseorang dapat mengakses elemen individual:

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

Jika xmenunjukkan sebuah array , maka peluruhan array-to-pointer akan muncul, karena menambahkan array dan integer tidak ada artinya (tidak ada operasi plus pada array), tetapi menambahkan pointer dan integer masuk akal:

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

(Perhatikan bahwa pointer yang dihasilkan secara implisit tidak memiliki nama, jadi saya menulis x+0untuk mengidentifikasinya.)

Jika, di lain pihak, xmenunjukkan sebuah pointer ke elemen pertama (atau elemen lainnya) dari sebuah array, maka peluruhan array-to-pointer tidak diperlukan, karena pointer yang iakan ditambahkan sudah ada:

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

Perhatikan bahwa dalam kasus yang digambarkan, xadalah variabel pointer (dapat dilihat oleh kotak kecil di sebelah x), tetapi bisa juga hasil dari fungsi yang mengembalikan pointer (atau ekspresi tipe lainnya T*).

Operator pengindeksan

Karena sintaksnya *(x+i)agak canggung, C ++ menyediakan sintaksis alternatif x[i]:

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

Karena fakta bahwa penambahan bersifat komutatif, kode berikut tidak persis sama:

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

Definisi operator pengindeksan mengarah pada kesetaraan menarik berikut:

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

Namun, &x[0]umumnya tidak setara dengan x. Yang pertama adalah pointer, yang terakhir adalah array. Hanya ketika konteks memicu peluruhan array-ke-pointer dapat xdan &x[0]digunakan secara bergantian. Sebagai contoh:

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

Pada baris pertama, kompiler mendeteksi tugas dari pointer ke pointer, yang berhasil. Pada baris kedua, ini mendeteksi tugas dari array ke pointer. Karena ini adalah berarti (tapi pointer ke pointer tugas masuk akal), array-to-pointer pembusukan tendangan seperti biasa.

Kisaran

Array tipe T[n]memiliki nelemen, diindeks dari 0ke n-1; tidak ada elemen n. Namun, untuk mendukung rentang setengah terbuka (di mana permulaan inklusif dan ujungnya eksklusif ), C ++ memungkinkan perhitungan pointer ke elemen ke-n (tidak ada), tetapi ilegal untuk meringkas pointer itu:

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

Misalnya, jika Anda ingin mengurutkan array, kedua hal berikut ini akan berfungsi sama baiknya:

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

Perhatikan bahwa memberikan &x[n]argumen kedua adalah ilegal karena hal ini setara dengan &*(x+n), dan sub-ekspresi secara *(x+n)teknis memanggil perilaku yang tidak terdefinisi dalam C ++ (tetapi tidak dalam C99).

Perhatikan juga bahwa Anda cukup memberikan xargumen pertama. Itu agak terlalu singkat untuk seleraku, dan itu juga membuat pengurangan argumen template sedikit lebih sulit untuk kompiler, karena dalam kasus itu argumen pertama adalah sebuah array tetapi argumen kedua adalah sebuah pointer. (Sekali lagi, peluruhan array-ke-pointer mulai.)


Kasus di mana array tidak membusuk menjadi pointer diilustrasikan di sini untuk referensi.
legends2k

@fredoverflow Di bagian Access atau Ranges mungkin perlu disebutkan bahwa array C bekerja dengan rentang berbasis C ++ 11 untuk loop.
gnzlbg

135

Programmer sering membingungkan array multidimensi dengan array pointer.

Array multidimensi

Sebagian besar programmer akrab dengan array multidimensi bernama, tetapi banyak yang tidak menyadari fakta bahwa array multidimensi juga dapat dibuat secara anonim. Array multidimensi sering disebut sebagai "array array" atau " array multidimensi sejati ".

Disebut array multidimensi

Saat menggunakan array multidimensi bernama, semua dimensi harus diketahui pada waktu kompilasi:

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

Beginilah bentuk array multidimensi terlihat di memori:

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

Perhatikan bahwa kisi 2D seperti di atas hanyalah visualisasi yang bermanfaat. Dari sudut pandang C ++, memori adalah urutan "flat" dari byte. Elemen-elemen dari array multidimensi disimpan dalam urutan baris-utama. Yaitu, connect_four[0][6]dan connect_four[1][0]tetangga dalam ingatan. Bahkan, connect_four[0][7]dan connect_four[1][0]menunjukkan elemen yang sama! Ini berarti Anda dapat mengambil array multi dimensi dan memperlakukannya sebagai array besar satu dimensi:

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

Array multidimensi anonim

Dengan array multidimensi anonim, semua dimensi kecuali yang pertama harus diketahui pada waktu kompilasi:

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

Beginilah tampilan array multidimensi anonim dalam memori:

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

Perhatikan bahwa array itu sendiri masih dialokasikan sebagai satu blok di memori.

Array pointer

Anda dapat mengatasi pembatasan lebar tetap dengan memperkenalkan tingkat tipuan lainnya.

Bernama array pointer

Berikut adalah array bernama dari lima pointer yang diinisialisasi dengan array anonim dengan panjang yang berbeda:

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];
}

Dan inilah tampilannya di memori:

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

Karena setiap baris dialokasikan secara individual sekarang, melihat array 2D sebagai array 1D tidak berfungsi lagi.

Array pointer anonim

Berikut adalah array anonim dari 5 (atau jumlah lainnya) pointer yang diinisialisasi dengan array anonim dengan panjang berbeda:

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[] !

Dan inilah tampilannya di memori:

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

Konversi

Array-to-pointer decay secara alami meluas ke array array dan array pointer:

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;

Namun, tidak ada konversi implisit dari T[h][w]ke T**. Jika konversi implisit seperti itu memang ada, hasilnya akan menjadi pointer ke elemen pertama dari array hpointer ke T(masing-masing menunjuk ke elemen pertama dari sebuah garis dalam array 2D asli), tetapi array pointer tidak ada di mana pun di memori belum. Jika Anda ingin konversi seperti itu, Anda harus membuat dan mengisi array pointer yang diperlukan secara manual:

int connect_four[6][7];

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

// ...

delete[] p;

Perhatikan bahwa ini menghasilkan tampilan array multidimensi asli. Jika Anda memerlukan salinan, Anda harus membuat array tambahan dan menyalin data sendiri:

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;

Sebagai saran: Anda harus menunjukkan, bahwa int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];serta int (*p)[W] = new int[6][W];dan int (*p)[W] = new int[H][W];merupakan pernyataan yang valid, kapan Hdan Wdiketahui pada saat kompilasi.
RobertS mendukung Monica Cellio

88

Tugas

Tanpa alasan tertentu, array tidak dapat ditugaskan satu sama lain. Gunakan std::copysebaliknya:

#include <algorithm>

// ...

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

Ini lebih fleksibel daripada yang diberikan oleh penugasan array yang sebenarnya karena dimungkinkan untuk menyalin irisan array yang lebih besar ke dalam array yang lebih kecil. std::copybiasanya dikhususkan untuk tipe primitif untuk memberikan kinerja maksimal. Kecil kemungkinannya std::memcpyberkinerja lebih baik. Jika ragu, ukurlah.

Meskipun Anda tidak dapat menetapkan array secara langsung, Anda dapat menetapkan struct dan kelas yang berisi anggota array. Itu karena anggota array disalin secara anggota oleh operator penugasan yang disediakan sebagai default oleh kompiler. Jika Anda menetapkan operator penugasan secara manual untuk jenis struct atau kelas Anda sendiri, Anda harus kembali ke menyalin manual untuk anggota array.

Melewati parameter

Array tidak bisa dilewati oleh nilai. Anda dapat melewati mereka dengan pointer atau dengan referensi.

Lewati dengan pointer

Karena array itu sendiri tidak dapat dilewati oleh nilai, biasanya pointer ke elemen pertama dilewatkan oleh nilai. Ini sering disebut "pass by pointer". Karena ukuran array tidak dapat diambil melalui pointer itu, Anda harus melewatkan parameter kedua yang menunjukkan ukuran array (solusi C klasik) atau pointer kedua yang menunjuk setelah elemen terakhir array (solusi C ++ iterator) :

#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);
}

Sebagai alternatif sintaksis, Anda juga dapat mendeklarasikan parameter sebagai T p[], dan itu berarti hal yang sama persis seperti T* p dalam konteks daftar parameter saja :

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

Anda dapat menganggap compiler sebagai menulis ulang T p[]untuk T *p dalam konteks daftar parameter hanya . Aturan khusus ini sebagian bertanggung jawab atas seluruh kebingungan tentang array dan pointer. Dalam setiap konteks lain, mendeklarasikan sesuatu sebagai array atau sebagai pointer membuat perbedaan besar .

Sayangnya, Anda juga bisa memberikan ukuran dalam parameter array yang diabaikan oleh kompiler secara diam-diam. Yaitu, tiga tanda tangan berikut ini persis sama, seperti yang ditunjukkan oleh kesalahan kompilator:

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

Lewati dengan referensi

Array juga dapat dilewatkan dengan referensi:

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

Dalam hal ini, ukuran array signifikan. Karena menulis fungsi yang hanya menerima array dengan tepat 8 elemen tidak banyak berguna, programmer biasanya menulis fungsi seperti templat:

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

Perhatikan bahwa Anda hanya dapat memanggil templat fungsi tersebut dengan array integer yang sebenarnya, bukan dengan pointer ke integer. Ukuran array secara otomatis disimpulkan, dan untuk setiap ukuran n, fungsi yang berbeda dipakai dari template. Anda juga dapat menulis templat fungsi yang cukup berguna yang abstrak dari jenis elemen dan dari ukuran.


2
Mungkin layak untuk menambahkan catatan bahwa meskipun void foo(int a[3]) aitu terlihat seperti seseorang sedang melewati nilai, memodifikasi abagian dalam fooakan mengubah array asli. Ini harus jelas karena array tidak dapat disalin, tetapi mungkin layak untuk memperkuatnya.
gnzlbg

C ++ 20 memilikiranges::copy(a, b)
LF

int sum( int size_, int a[size_]);- dari (saya pikir) C99 dan seterusnya
Chef Gladiator

73

5. Kesalahan umum saat menggunakan array.

5.1 Jebakan: Mempercayai tautan jenis tidak aman.

OK, Anda telah diberitahu, atau telah mengetahui sendiri, bahwa global (variabel lingkup namespace yang dapat diakses di luar unit terjemahan) adalah Evil ™. Tapi apakah Anda tahu betapa sebenarnya Evil ™ mereka? Pertimbangkan program di bawah ini, yang terdiri dari dua file [main.cpp] dan [angka.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};

Pada Windows 7 ini mengkompilasi dan menghubungkan dengan baik dengan MinGW g ++ 4.4.1 dan Visual C ++ 10.0.

Karena tipe tidak cocok, program macet ketika Anda menjalankannya.

Dialog crash Windows 7

Penjelasan di dalam-formal: program ini memiliki Perilaku Tidak Terdefinisi (UB), dan alih-alih menabraknya, itu hanya dapat hang, atau mungkin tidak melakukan apa-apa, atau dapat mengirim threating email ke presiden Amerika Serikat, Rusia, India, Cina dan Swiss, dan membuat Nasal Daemon terbang keluar dari hidung Anda.

Dalam prakteknya penjelasan: dalam main.cpparray diperlakukan sebagai pointer, ditempatkan pada alamat yang sama dengan array. Untuk 32-bit yang dapat dieksekusi ini berarti bahwa nilai pertama intdalam array, diperlakukan sebagai pointer. Yaitu, di main.cppdalam numbersvariabel mengandung, atau muncul untuk mengandung, (int*)1. Ini menyebabkan program mengakses memori di bagian paling bawah ruang alamat, yang secara konvensional disediakan dan menyebabkan jebakan. Hasil: Anda mengalami kerusakan.

Para penyusun sepenuhnya dalam hak mereka untuk tidak mendiagnosis kesalahan ini, karena C ++ 11 §3.5 / 10 mengatakan, tentang persyaratan jenis yang kompatibel untuk deklarasi,

[N3290 §3.5 / 10]
Pelanggaran aturan ini tentang identitas tipe tidak memerlukan diagnostik.

Paragraf yang sama merinci variasi yang diizinkan:

... deklarasi untuk objek array dapat menentukan tipe array yang berbeda dengan ada atau tidak adanya batasan array utama (8.3.4).

Variasi yang dibolehkan ini tidak termasuk mendeklarasikan nama sebagai array dalam satu unit terjemahan, dan sebagai penunjuk dalam unit terjemahan lain.

5.2 Jebakan: Melakukan optimasi prematur ( memset& teman).

Belum ditulis

5.3 Pitfall: Menggunakan idiom C untuk mendapatkan sejumlah elemen.

Dengan pengalaman C yang dalam, wajar untuk menulis ...

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

Karena arraypeluruhan untuk menunjuk ke elemen pertama di mana diperlukan, ekspresi sizeof(a)/sizeof(a[0])juga dapat ditulis sebagai sizeof(a)/sizeof(*a). Artinya sama, dan tidak peduli bagaimana ditulisnya, ini adalah ungkapan C untuk menemukan elemen-elemen bilangan array.

Perangkap utama: idiom C tidak aman untuk huruf. Misalnya, kode ...

#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 );
}

meneruskan pointer ke N_ITEMS, dan karena itu kemungkinan besar menghasilkan hasil yang salah. Dikompilasi sebagai executable 32-bit pada Windows 7 yang dihasilkannya ...

7 elemen, tampilan panggilan ...
1 elemen.

  1. Kompiler menulis ulang int const a[7]menjadi adil int const a[].
  2. Kompiler menulis ulang int const a[]untuk int const* a.
  3. N_ITEMS Oleh karena itu dipanggil dengan pointer.
  4. Untuk 32-bit yang dapat dieksekusi sizeof(array)(ukuran pointer) maka 4.
  5. sizeof(*array)setara dengan sizeof(int), yang untuk executable 32-bit juga 4.

Untuk mendeteksi kesalahan ini pada saat dijalankan Anda dapat melakukan ...

#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 elemen, memanggil tampilan ...
Pernyataan gagal: ("N_ITEMS membutuhkan array aktual sebagai argumen", typeid (a)! = Typeid (& * a)), file runtime_detect ion.cpp, line 16

Aplikasi ini telah meminta Runtime untuk menghentikannya dengan cara yang tidak biasa.
Silakan hubungi tim dukungan aplikasi untuk informasi lebih lanjut.

Deteksi kesalahan runtime lebih baik daripada tidak ada deteksi, tetapi membuang-buang sedikit waktu prosesor, dan mungkin lebih banyak waktu programmer. Lebih baik dengan deteksi pada waktu kompilasi! Dan jika Anda senang tidak mendukung array tipe lokal dengan C ++ 98, maka Anda dapat melakukannya:

#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 )

Kompilasi definisi ini diganti dengan program lengkap pertama, dengan g ++, saya dapat…

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: Dalam fungsi 'void display (const int *)':
compile_time_detection.cpp: 14: error: tidak ada fungsi yang cocok untuk panggilan ke 'n_items (const int * &)'

M: \ hitung> _

Cara kerjanya: array dilewatkan dengan referensi untuk n_items, dan sehingga tidak membusuk untuk pointer ke elemen pertama, dan fungsi hanya bisa mengembalikan jumlah elemen yang ditentukan oleh jenis.

Dengan C ++ 11 Anda dapat menggunakan ini juga untuk larik tipe lokal, dan ini adalah tipe aman C ++ idiom untuk menemukan jumlah elemen array.

5.4 C ++ 11 & C ++ 14 pitfall: Menggunakan constexprfungsi ukuran array.

Dengan C ++ 11 dan kemudian itu wajar, tetapi karena Anda akan melihat berbahaya !, untuk menggantikan fungsi C ++ 03

typedef ptrdiff_t   Size;

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

dengan

using Size = ptrdiff_t;

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

di mana perubahan signifikan adalah penggunaan constexpr, yang memungkinkan fungsi ini menghasilkan konstanta waktu kompilasi .

Misalnya, berbeda dengan fungsi C ++ 03, konstanta waktu kompilasi seperti itu dapat digunakan untuk mendeklarasikan array dengan ukuran yang sama dengan yang lain:

// 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.
}

Tetapi pertimbangkan kode ini menggunakan constexprversi:

// 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 );
}

Perangkap: pada Juli 2015 di atas mengkompilasi dengan MinGW-64 5.1.0 dengan -pedantic-errors, dan, pengujian dengan kompiler online di gcc.godbolt.org/ , juga dengan dentang 3.0 dan dentang 3.2, tetapi tidak dengan dentang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) atau 3.7 (percobaan). Dan penting untuk platform Windows, itu tidak dikompilasi dengan Visual C ++ 2015. Alasannya adalah pernyataan C ++ 11 / C ++ 14 tentang penggunaan referensi dalam constexprekspresi:

C ++ 11 C ++ 14 $ 5,19 / 2 dasbor ke sembilan

Sebuah bersyarat ekspresi e adalah konstan ekspresi inti kecuali evaluasi e, mengikuti aturan dari mesin abstrak (1,9), akan mengevaluasi salah satu dari ungkapan berikut:
        ⋮

  • sebuah id-ekspresi yang mengacu pada variabel atau data anggota dari tipe referensi kecuali referensi memiliki inisialisasi sebelumnya dan baik
    • ini diinisialisasi dengan ekspresi konstan atau
    • itu adalah anggota data non-statis dari suatu objek yang masa pakainya dimulai dalam evaluasi e;

Seseorang selalu dapat menulis lebih banyak 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
}

... tapi ini gagal ketika Collectionbukan array mentah.

Untuk menangani koleksi yang bisa berupa non-array kita perlu kelebihan n_itemsfungsi, tetapi juga, untuk waktu kompilasi, kita perlu representasi waktu kompilasi dari ukuran array. Dan solusi klasik C ++ 03, yang berfungsi dengan baik juga di C ++ 11 dan C ++ 14, adalah membiarkan fungsi melaporkan hasilnya bukan sebagai nilai melainkan melalui tipe hasil fungsinya . Misalnya seperti ini:

// 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 );
}

Tentang pilihan jenis pengembalian untuk static_n_items: kode ini tidak digunakan std::integral_constant karena dengan std::integral_constanthasilnya direpresentasikan secara langsung sebagai constexprnilai, memperkenalkan kembali masalah asli. Alih-alih Size_carrierkelas satu dapat membiarkan fungsi langsung mengembalikan referensi ke array. Namun, tidak semua orang akrab dengan sintaksis itu.

Tentang penamaan: bagian dari solusi ini untuk masalah constexpr-invalid-karena-referensi adalah membuat pilihan waktu kompilasi konstan.

Semoga oops-ada-ada-referensi-yang terlibat-dalam- constexprmasalah Anda akan diperbaiki dengan C ++ 17, tetapi sampai saat itu makro seperti STATIC_N_ITEMSportabilitas menghasilkan di atas, misalnya untuk dentang dan kompiler Visual C ++, tipe penahan keamanan.

Terkait: makro tidak menghormati cakupan, jadi untuk menghindari tabrakan nama, sebaiknya menggunakan awalan nama, mis MYLIB_STATIC_N_ITEMS.


1
+1 Uji C coding hebat: Saya telah menghabiskan 15 menit di VC ++ 10.0 dan GCC 4.1.2 mencoba untuk memperbaiki Segmentation fault... Saya akhirnya menemukan / mengerti setelah membaca penjelasan Anda! Silakan tulis bagian §5.2 Anda :-) Cheers
olibre

Baik. Satu nit - tipe kembali untuk countOf harus size_t bukan ptrdiff_t. Mungkin perlu disebutkan bahwa dalam C ++ 11/14 itu haruslah constexpr dan noexcept.
Ricky65

@ Ricky65: Terima kasih telah menyebutkan pertimbangan C ++ 11. Dukungan untuk fitur-fitur ini telah terlambat datang untuk Visual C ++. Mengenai size_t, itu tidak memiliki kelebihan yang saya tahu untuk platform modern, tetapi memiliki sejumlah masalah karena aturan konversi tipe implisit dari C dan C ++. Artinya, ptrdiff_tdigunakan dengan sangat sengaja, untuk menghindari masalah dengan size_t. Namun orang harus menyadari bahwa g ++ memiliki masalah dengan ukuran array yang cocok dengan parameter templat kecuali jika itu size_t(saya tidak berpikir masalah khusus kompiler ini dengan yang tidak size_tpenting, tetapi YMMV).
Ceria dan hth. - Alf

@Alf. Dalam Draft Kerja Standar (N3936) 8.3.4 saya membaca - Batas array adalah ... "ekspresi konstan yang dikonversi dari tipe std :: size_t dan nilainya akan lebih besar dari nol".
Ricky65

@Ricky: Jika Anda merujuk pada inkonsistensi, pernyataan ini tidak ada dalam standar C ++ 11 saat ini sehingga sulit untuk menebak konteksnya, tetapi kontradiksinya (array yang dialokasikan secara dinamis dapat memiliki batas 0, per C + +11 §5.3.4 / 7) mungkin tidak akan berakhir di C ++ 14. Draf hanya itu: draf. Jika Anda bertanya tentang apa yang dimaksud dengan "nya", itu merujuk pada ekspresi asli, bukan yang dikonversi. Jika di pihak ketiga Anda menyebutkan ini karena Anda berpikir bahwa mungkin kalimat seperti itu berarti bahwa seseorang harus menggunakan size_tuntuk menunjukkan ukuran array, tentu saja tidak.
Ceria dan hth. - Alf

72

Array pembuatan dan inisialisasi

Seperti halnya objek C ++ lainnya, array dapat disimpan secara langsung dalam variabel bernama (maka ukurannya harus berupa konstanta waktu kompilasi; C ++ tidak mendukung VLA ), atau array dapat disimpan secara anonim di heap dan diakses secara tidak langsung melalui pointer (hanya kemudian ukurannya dapat dihitung pada saat runtime).

Array otomatis

Array otomatis (array yang hidup "pada tumpukan") dibuat setiap kali aliran kontrol melewati definisi variabel array lokal non-statis:

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

Inisialisasi dilakukan dalam urutan menaik. Perhatikan bahwa nilai awal tergantung pada jenis elemen T:

  • Jika Tadalah POD (seperti intdalam contoh di atas), tidak ada inisialisasi berlangsung.
  • Kalau tidak, konstruktor-default Tmenginisialisasi semua elemen.
  • Jika Ttidak menyediakan konstruktor default yang dapat diakses, program tidak dapat dikompilasi.

Atau, nilai awal dapat secara eksplisit ditentukan dalam penginisialisasi array , daftar yang dipisahkan koma yang dikelilingi oleh kurung keriting:

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

Karena dalam hal ini jumlah elemen dalam penginisialisasi array sama dengan ukuran array, menentukan ukuran secara manual berlebihan. Secara otomatis dapat disimpulkan oleh kompiler:

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

Dimungkinkan juga untuk menentukan ukuran dan menyediakan penginisialisasi array yang lebih pendek:

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

Dalam hal ini, elemen yang tersisa diinisialisasi nol . Perhatikan bahwa C ++ memungkinkan penginisialisasi array kosong (semua elemen diinisialisasi nol), sedangkan C89 tidak (setidaknya diperlukan satu nilai). Perhatikan juga bahwa inisialisasi array hanya dapat digunakan untuk menginisialisasi array; nanti tidak bisa digunakan dalam penugasan.

Array statis

Array statis (array yang hidup "di segmen data") adalah variabel array lokal yang didefinisikan dengan statickata kunci dan variabel array pada lingkup namespace ("variabel global"):

int global_static_array[8];

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

(Perhatikan bahwa variabel di lingkup namespace secara implisit statis. Menambahkan statickata kunci ke definisi mereka memiliki arti yang sama sekali berbeda, usang .)

Berikut adalah bagaimana array statis berperilaku berbeda dari array otomatis:

  • Array statis tanpa penginisialisasi array nol diinisialisasi sebelum potensi inisialisasi lebih lanjut.
  • Array POD statis diinisialisasi tepat sekali , dan nilai-nilai awal biasanya dimasukkan ke dalam executable, dalam hal ini tidak ada biaya inisialisasi saat runtime. Namun, ini tidak selalu merupakan solusi yang paling hemat ruang, dan tidak diperlukan oleh standar.
  • Array non-POD statis diinisialisasi saat pertama kali aliran kontrol melewati definisi mereka. Dalam kasus array statis lokal, itu mungkin tidak pernah terjadi jika fungsi tidak pernah dipanggil.

(Tidak ada satu pun di atas yang khusus untuk array. Aturan-aturan ini berlaku juga untuk jenis objek statis lainnya.)

Array anggota data

Anggota data array dibuat ketika objek mereka dibuat. Sayangnya, C ++ 03 tidak menyediakan sarana untuk menginisialisasi array dalam daftar penginisialisasi anggota , jadi inisialisasi harus dipalsukan dengan tugas:

class Foo
{
    int primes[8];

public:

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

Sebagai alternatif, Anda dapat menetapkan larik otomatis di badan konstruktor dan menyalin elemen-elemennya:

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);
    }
};

Dalam C ++ 0x, array dapat diinisialisasi dalam daftar penginisialisasi anggota berkat inisialisasi seragam :

class Foo
{
    int primes[8];

public:

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

Ini adalah satu-satunya solusi yang berfungsi dengan tipe elemen yang tidak memiliki konstruktor default.

Array dinamis

Array dinamis tidak memiliki nama, karenanya satu-satunya cara mengaksesnya adalah melalui pointer. Karena mereka tidak memiliki nama, saya akan menyebutnya sebagai "array anonim" mulai sekarang.

Di C, array anonim dibuat via mallocdan teman. Di C ++, array anonim dibuat menggunakan new T[size]sintaks yang mengembalikan pointer ke elemen pertama dari array anonim:

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

Seni ASCII berikut menggambarkan tata letak memori jika ukurannya dihitung sebagai 8 pada saat runtime:

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

Jelas, array anonim membutuhkan lebih banyak memori daripada array bernama karena pointer tambahan yang harus disimpan secara terpisah. (Ada juga beberapa overhead tambahan di toko gratis.)

Perhatikan bahwa tidak ada peluruhan array-ke-pointer yang terjadi di sini. Meskipun mengevaluasi new int[size]apakah sebenarnya membuat berbagai bilangan bulat, hasil dari ekspresi new int[size]adalah sudah pointer ke bilangan bulat (elemen pertama), tidak array bilangan bulat atau pointer ke array bilangan bulat ukuran yang tidak diketahui. Itu tidak mungkin, karena sistem tipe statis memerlukan ukuran array untuk menjadi konstanta kompilasi-waktu. (Oleh karena itu, saya tidak membubuhi keterangan anonim array dengan jenis informasi statis dalam gambar.)

Mengenai nilai default untuk elemen, array anonim berperilaku mirip dengan array otomatis. Biasanya, array POD anonim tidak diinisialisasi, tetapi ada sintaks khusus yang memicu inisialisasi nilai:

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

(Perhatikan pasangan tanda kurung tepat sebelum tanda titik koma.) Sekali lagi, C ++ 0x menyederhanakan aturan dan memungkinkan menentukan nilai awal untuk array anonim berkat inisialisasi seragam:

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

Jika Anda selesai menggunakan array anonim, Anda harus melepaskannya kembali ke sistem:

delete[] p;

Anda harus melepaskan setiap array anonim tepat sekali dan kemudian tidak pernah menyentuhnya lagi sesudahnya. Tidak melepaskannya sama sekali menghasilkan kebocoran memori (atau lebih umum, tergantung pada jenis elemen, kebocoran sumber daya), dan mencoba melepaskannya berkali-kali menghasilkan perilaku yang tidak terdefinisi. Menggunakan bentuk non-array delete(atau free) alih-alih delete[]melepaskan array juga perilaku yang tidak terdefinisi .


2
Penghentian staticpenggunaan dalam lingkup namespace telah dihapus di C ++ 11.
legends2k

Karena newsaya operator, tentu saja dapat mengembalikan array yang diberi alokasi dengan referensi. Tidak ada gunanya ...
Deduplicator

@Deduplicator Tidak itu tidak bisa, karena secara historis, newjauh lebih tua daripada referensi.
fredoverflow

@ FredOverflow: Jadi ada alasan tidak bisa mengembalikan referensi, hanya saja berbeda dari penjelasan tertulis.
Deduplicator

2
@Dupuplikator Saya tidak berpikir referensi ke array batas yang tidak diketahui ada. Setidaknya g ++ menolak dikompilasiint a[10]; int (&r)[] = a;
fredoverflow
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.