C ++ 'da dizileri nasıl kullanabilirim?


480

C ++, neredeyse her yerde kullanıldığı C'den miras alınan dizilerdir. C ++, kullanımı daha kolay ve daha az hataya açık soyutlamalar sağlar ( std::vector<T>C ++ 98 ve C ++ 11'denstd::array<T, n> beri ), bu nedenle dizilere duyulan ihtiyaç C'de olduğu kadar sık ​​ortaya çıkmaz. C ile yazılmış bir kütüphaneyi kodlayın veya etkileşimde bulunun.

Bu SSS beş bölüme ayrılmıştır:

  1. tür düzeyinde diziler ve erişim elemanları
  2. dizi oluşturma ve başlatma
  3. atama ve parametre geçişi
  4. çok boyutlu diziler ve işaretçi dizileri
  5. diziler kullanılırken yaygın görülen tuzaklar

Bu SSS'de önemli bir şeyin eksik olduğunu düşünüyorsanız, bir cevap yazın ve ek bir parça olarak buraya bağlayın.

Aşağıdaki metinde "dizi", sınıf şablonu değil "C dizisi" anlamına gelir std::array. C bildirici sözdizimi hakkında temel bilgiler varsayılmaktadır. Manuel kullanımın newve deleteaşağıda gösterildiği gibi, istisnalar karşısında son derece tehlikelidir, ancak bu başka bir SSS'nin konusudur .

(Not: Bu, Stack Overflow'ın C ++ SSS girişidir . Bu formda bir SSS sağlama fikrini eleştirmek istiyorsanız, tüm bunları başlatan metadaki yayınlama bunu yapmak için yer olacaktır. bu soru SSS fikrinin ilk başta başladığı C ++ sohbet odasında izlenir , bu nedenle cevabınızın bu fikri ortaya çıkaranlar tarafından okunması muhtemeldir.)


Eğer işaretçiler her zaman hedeflerinin ortasında bir yerde yerine başlangıcını işaret etseydi daha iyi olurdu ...
Deduplicator

Size daha fazla esneklik sağladığı için STL Vector uygulamasını kullanmalısınız.
Moiz Sajid

2
Birleşik durumu ile std::arrays, std::vectors ve gsl::spans - Ben açıkçası söylemek C ++ diziler nasıl kullanılacağı hakkında bir SSS beklenebilir "sadece, iyi, dikkate başlayabilirsiniz Artık değil bunları kullanarak."
einpoklum

Yanıtlar:


302

Tür düzeyindeki diziler

Bir dizi türü olarak adlandırılır T[n]burada Tbir eleman türü ve npozitif bir boyut , dizideki elementlerin sayısı. Dizi türü, öğe türünün ve boyutunun ürün türüdür. Bu bileşenlerden biri veya her ikisi de farklıysa, farklı bir tür elde edersiniz:

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

Boyutun türün bir parçası olduğunu, yani farklı boyuttaki dizi türlerinin birbiriyle kesinlikle ilgisi olmayan uyumsuz türler olduğunu unutmayın. sizeof(T[n])eşittir n * sizeof(T).

Dizi-işaretçi bozulması

Arasındaki tek "bağlantı" T[n]ve T[m]her iki tip örtülü olmasıdır dönüştürülmüş için T*ve bu dönüşüm sonucu dizinin ilk eleman bir işaretçidir. Yani, a'nın T*gerekli olduğu her yerde a sağlayabilirsiniz T[n]ve derleyici sessizce bu işaretçiyi sağlayacaktır:

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

Bu dönüşüm "dizi-işaretçi bozunumu" olarak bilinir ve büyük bir karışıklık kaynağıdır. Dizinin boyutu, artık ( T*) türünün bir parçası olmadığından bu işlemde kaybolur . Pro: Bir dizinin tür düzeyinde boyutunun unutulması, işaretçinin herhangi bir boyuttaki bir dizinin ilk öğesine işaret etmesini sağlar . Con: Bir dizinin ilk (veya başka bir) öğesine bir işaretçi verildiğinde, dizinin ne kadar büyük olduğunu veya işaretçinin dizinin sınırlarına göre tam olarak nerede olduğunu tespit etmenin bir yolu yoktur. İşaretçiler son derece aptalca .

Diziler işaretçi değil

Derleyici, bir dizinin yararlı olduğu kabul edildiğinde, yani bir işlem bir dizide başarısız olduğunda, ancak bir işaretçi üzerinde başarılı olduğunda, sessizce bir dizinin ilk öğesine bir işaretçi oluşturur. Diziden işaretçiye bu dönüşüm önemsizdir, çünkü elde edilen işaretçi değeri basitçe dizinin adresidir. İşaretçi olduğu Not olmayan dizide (ya da başka bir yerde bellekte) bir parçası olarak depolanır. Dizi işaretçi değildir.

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

Bir dizi etmez ki burada önemli bir bağlam olmayan zaman ilk elemana bir işaretçi bozunabilir olan &operatör kendisine tatbik edilir. Bu durumda, &operatör yalnızca ilk öğesine bir işaretçi değil , tüm diziye bir işaretçi verir. Bu durumda değerler (adresler) aynı olmasına rağmen , bir dizinin ilk öğesine bir işaretçi ve dizinin tamamına bir işaretçi tamamen farklı türlerdir:

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

Aşağıdaki ASCII sanatı bu ayrımı açıklamaktadır:

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

İlk öğenin işaretçisinin yalnızca tek bir tamsayıyı (küçük bir kutu olarak tasvir edilir) nasıl gösterirken, tüm dizinin işaretçisinin (büyük bir kutu olarak tasvir edilen) 8 tamsayı bir diziyi gösterdiğini unutmayın.

Aynı durum sınıflarda da ortaya çıkar ve belki daha açıktır. Bir nesneye işaretçi ve ilk veri üyesine işaretçi aynı değere (aynı adres) sahiptir, ancak bunlar tamamen farklı türlerdir.

C bildirim sözdizimine aşina değilseniz, türdeki parantez int(*)[8]önemlidir:

  • int(*)[8] , 8 tamsayıdan oluşan bir diziye işaretçi.
  • int*[8]8 işaretli bir dizidir, her öğe türü int*.

Elemanlara erişim

C ++, bir dizinin tek tek öğelerine erişmek için iki sözdizimsel varyasyon sağlar. İkisi de diğerinden üstün değildir ve her ikisini de tanımanız gerekir.

İşaretçi aritmetiği

pBir dizinin ilk öğesine bir işaretçi verildiğinde , ifade dizinin p+ii. Öğesine bir işaretçi verir. Daha sonra bu işaretçiyi kaydeden kişi tek tek öğelere erişebilir:

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

Bir diziyix belirtirse , bir dizi ve bir tamsayı eklemek anlamsız olduğu için (dizilerde artı işlemi yoktur), ancak bir işaretçi ve bir tamsayı eklemek mantıklı olacaktır:

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

(Kapalı olarak oluşturulmuş işaretçinin adı olmadığını unutmayın, bu yüzden onu x+0tanımlamak için yazdım .)

Öte yandan, eğer xbir belirtmektedir işaretçi birinci (veya diğer herhangi bir) bir dizi elemanına işaretçi için, dizi-to-pointer çürüme, gerekli değildir iönce ilave edilecektir vardır:

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

Gösterilen durumda, xbir işaretçi değişkeni (yanındaki küçük kutu tarafından görülebilir x), ancak bir işaretçi (veya türün başka herhangi bir ifadesi T*) döndüren bir işlevin sonucu da olabilir .

Endeksleme operatörü

Sözdizimi *(x+i)biraz beceriksiz olduğundan, C ++ alternatif sözdizimini sağlar x[i]:

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

Toplamanın değişmeli olması nedeniyle, aşağıdaki kod tamamen aynı şeyi yapar:

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

Dizin oluşturma operatörünün tanımı aşağıdaki ilginç denkliğe yol açar:

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

Ancak, &x[0]genellikle eşdeğer değildirx . Birincisi bir işaretçi, ikincisi bir dizi. Yalnızca içerik diziden işaretçiye bozunmayı tetiklediğinde xve &x[0]birbirinin yerine kullanılabilir. Örneğin:

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

İlk satırda, derleyici bir göstergeden işaretçiye bir atamayı algılar ve bu da başarılı bir şekilde başarılı olur. İkinci satırda, bir diziden işaretçiye bir atama algılar . Bu anlamsız olduğu için (ancak işaretçi -işaretçi ataması mantıklıdır), dizi-işaretçi bozulması her zamanki gibi devreye girer.

aralıklar

Tipte bir dizi T[n]sahip nendeksli elemanları 0için n-1; öğe yok n. Ve yine de, yarı açık aralıkları (başlangıcının dahil olduğu ve sonun özel olduğu yerlerde ) desteklemek için, C ++ bir işaretçi (var olmayan) n. Öğeye hesaplamaya izin verir, ancak bu işaretçiyi silmek için yasadışıdır:

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

Örneğin, bir diziyi sıralamak istiyorsanız, aşağıdakilerin her ikisi de eşit derecede iyi çalışır:

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

&x[n]Bu, eşdeğer olduğu için ikinci argüman olarak sağlamanın yasadışı olduğunu &*(x+n)ve alt ifadenin *(x+n)teknik olarak C ++ 'da tanımlanmamış davranışı (C99'da değil) çağırdığını unutmayın.

Ayrıca sadece xilk argüman olarak verebileceğinizi unutmayın . Bu benim zevkime göre biraz fazla aldatıcı ve aynı zamanda şablon argüman çıkarımını derleyici için biraz daha zorlaştırıyor, çünkü bu durumda ilk argüman bir dizi ama ikinci argüman bir işaretçi. (Yine, diziden işaretçiye bozunma devreye girer.)


Dizinin bir işaretçiye bozulmadığı durumlar burada referans olarak gösterilmiştir .
legends2k

@fredoverflow Erişim veya Aralıklar bölümünde, C dizilerinin döngüler için C ++ 11 aralık tabanlı çalıştığından bahsetmeye değer olabilir.
gnzlbg

135

Programcılar genellikle çok boyutlu dizileri işaretçi dizileriyle karıştırır.

Çok boyutlu diziler

Çoğu programcı adlandırılmış çok boyutlu dizilere aşinadır, ancak çoğu çok boyutlu dizinin anonim olarak da oluşturulabileceğinin farkında değildir. Çok boyutlu diziler genellikle "dizilerin dizileri" veya " gerçek çok boyutlu diziler" olarak adlandırılır.

Adlandırılmış çok boyutlu diziler

Adlandırılmış çok boyutlu diziler kullanıldığında, tüm boyutlar derleme zamanında bilinmelidir:

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

Adlandırılmış çok boyutlu bir dizi bellekte şöyle görünür:

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

Yukarıdaki gibi 2B ızgaraların yalnızca yararlı görselleştirmeler olduğunu unutmayın. C ++ açısından bakıldığında, bellek "düz" bir bayt dizisidir. Çok boyutlu bir dizinin elemanları satır-büyük sırasına göre saklanır. Yani, connect_four[0][6]ve connect_four[1][0]bellekte komşudur. Aslında connect_four[0][7]ve connect_four[1][0]aynı unsuru belirtin! Bu, çok boyutlu dizileri alıp büyük, tek boyutlu diziler olarak ele alabileceğiniz anlamına gelir:

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

Anonim çok boyutlu diziler

Anonim çok boyutlu dizilerle, ilk boyut dışındaki tüm boyutların derleme zamanında bilinmesi gerekir:

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

Anonim çok boyutlu bir dizi bellekte şöyle görünür:

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

Dizinin kendisinin hala bellekte tek bir blok olarak ayrıldığını unutmayın.

İşaretçi dizileri

Başka bir dolaylılık düzeyi ekleyerek sabit genişlik kısıtlamasının üstesinden gelebilirsiniz.

Adlandırılmış işaretçi dizileri

Farklı uzunluklardaki anonim dizilerle başlatılan beş işaretçi içeren bir dizi:

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

İşte bellekte nasıl göründüğü:

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

Artık her satır ayrı ayrı tahsis edildiğinden, 2B dizileri 1D dizileri olarak görüntülemek artık çalışmaz.

Anonim işaretçi dizileri

Farklı uzunluklarda anonim dizilerle başlatılan 5 (veya başka bir sayıda) işaretçi içeren anonim bir dizi:

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

İşte bellekte nasıl göründüğü:

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

Dönüşümler

Diziden işaretçiye bozunma doğal olarak dizilerin dizilerine ve işaretçilerin dizilerine kadar uzanır:

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;

Ancak, örtük dönüştürme yoktur T[h][w]için T**. Böyle bir örtük dönüştürme var olduysa, sonuç bir dizinin ilk elemana bir işaretçi olurdu hişaretçiler T(orijinal 2B dizideki bir satırın ilk elemana her işaret), ama bu işaretçi dizisi içinde hiçbir yerde yoktur bellek. Böyle bir dönüşüm istiyorsanız, gerekli işaretçi dizisini manuel olarak oluşturmalı ve doldurmalısınız:

int connect_four[6][7];

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

// ...

delete[] p;

Bunun orijinal çok boyutlu dizinin bir görünümünü oluşturduğunu unutmayın. Bunun yerine bir kopyaya ihtiyacınız varsa, fazladan diziler oluşturmanız ve verileri kendiniz kopyalamanız gerekir:

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;

Öneri olarak: Sen, işaret olmalıdır int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];hem de int (*p)[W] = new int[6][W];ve int (*p)[W] = new int[H][W];geçerli ifadeleri vardır Hve Wderleme sırasında bilinmektedir.
RobertS

88

Görev

Belirli bir nedenden ötürü, diziler birbirlerine atanamaz. std::copyBunun yerine kullanın :

#include <algorithm>

// ...

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

Bu, gerçek dizi atamasının sağlayabileceğinden daha esnektir, çünkü daha büyük dizilerin dilimlerini daha küçük dizilere kopyalamak mümkündür. std::copygenellikle ilkel tipler için maksimum performans vermek üzere uzmanlaşmıştır. Daha std::memcpyiyi performans göstermesi pek olası değildir . Emin değilseniz ölçün.

Doğrudan diziler atamak mümkün olmasa da, sen yapabilirsiniz yapılar ve sınıflar atamak ihtiva dizi üyelerini. Bunun nedeni, dizi üyelerinin derleyici tarafından varsayılan olarak sağlanan atama işleci tarafından üye olarak kopyalanmasıdır . Atama işlecini kendi yapı veya sınıf türleriniz için manuel olarak tanımlarsanız, dizi üyeleri için manuel kopyalamaya geri dönmeniz gerekir.

Parametre geçişi

Diziler değere göre iletilemez. İşaretçi veya başvuru ile iletebilirsiniz.

İşaretçi ile geç

Dizilerin kendileri değere göre iletilemediğinden, genellikle ilk öğelerine bir işaretçi değere göre iletilir. Buna genellikle "işaretçiden geç" denir. Dizinin boyutu bu işaretçi aracılığıyla alınamadığından, dizinin boyutunu (klasik C çözümü) gösteren ikinci bir parametre veya dizinin son öğesinden (C ++ yineleyici çözümü) sonra gelen ikinci bir işaretçi iletmeniz gerekir. :

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

Sözdizimsel bir alternatif olarak T p[], parametreleri şu şekilde de bildirebilirsiniz ve bu T* p , yalnızca parametre listeleri bağlamındakiyle aynı anlama gelir :

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

Sen yeniden olarak derleyici düşünebiliriz T p[]için T *p parametre listeleri yalnızca bağlamında . Bu özel kural, diziler ve işaretçiler hakkındaki tüm karışıklıktan kısmen sorumludur. Diğer tüm bağlamlarda, bir şeyi dizi veya işaretçi olarak bildirmek çok büyük bir fark yaratır.

Ne yazık ki, bir dizi parametresinde derleyici tarafından sessizce göz ardı edilen bir boyut da sağlayabilirsiniz. Yani, aşağıdaki üç imza derleyici hatalarıyla gösterildiği gibi tam olarak eşdeğerdir:

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

Referans ile geç

Diziler referans olarak da iletilebilir:

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

Bu durumda, dizi boyutu önemlidir. Yalnızca tam 8 öğeden oluşan dizileri kabul eden bir işlev yazmak çok işe yaramadığından, programcılar genellikle şablonlar gibi işlevler yazar:

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

Bu tür bir işlev şablonunu, tamsayıya bir işaretçi ile değil, yalnızca gerçek bir tamsayı dizisiyle çağırabileceğinizi unutmayın. Dizinin boyutu otomatik olarak çıkarılır ve her boyut niçin şablondan farklı bir işlev başlatılır. Hem öğe türünden hem de boyuttan soyut olan oldukça kullanışlı işlev şablonları da yazabilirsiniz .


2
Tho bile void foo(int a[3]) abir dizi değeri geçiyor gibi görünüyor bir not eklemek için buna değer olabilir, aiçinde foodeğiştirmek orijinal dizi değiştirecektir. Bu açık olmalıdır çünkü diziler kopyalanamaz, ancak bunu güçlendirmeye değebilir.
gnzlbg

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

int sum( int size_, int a[size_]);- (sanırım) C99'dan itibaren
Şef Gladiator

73

5. Dizileri kullanırken sık karşılaşılan tuzaklar.

5.1 Tuzak: Güvenli olmayan tipte bağlantıya güvenmek.

Tamam, size küresellerin (çeviri birimi dışında erişilebilen ad alanı kapsam değişkenleri) Evil ™ olduğu söylendi veya kendiniz öğrendiniz. Ama ne kadar gerçekten Evil ™ olduklarını biliyor muydunuz? [Main.cpp] ve [numbers.cpp] dosyalarından oluşan aşağıdaki programı göz önünde bulundurun:

// [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'de bu, hem MinGW g ++ 4.4.1 hem de Visual C ++ 10.0 ile iyi derler ve bağlantı kurar.

Türler eşleşmediğinden, programı çalıştırdığınızda çöküyor.

Windows 7 kilitlenme iletişim kutusu

Resmi açıklama: Programın Tanımsız Davranışı (UB) vardır ve çökmek yerine, sadece asılabilir veya belki de hiçbir şey yapamaz veya ABD, Rusya, Hindistan başkanlarına tehdit eden e-postalar gönderebilir, Çin ve İsviçre ve Burun Daemonlarının burnundan uçmasını sağla.

Uygulama içi açıklama: main.cppDizide, diziyle aynı adrese yerleştirilmiş bir işaretçi olarak değerlendirilir. 32 bit yürütülebilir dosya için bu int, dizideki ilk değerin bir işaretçi olarak değerlendirildiği anlamına gelir . Yani, içinde değişken içerdiği veya görünür, içerdiği . Bu, programın geleneksel olarak ayrılmış ve tuzağa neden olan adres alanının en altındaki belleğe erişmesine neden olur. Sonuç: bir çarpışma yaşarsınız.main.cppnumbers(int*)1

Derleyiciler bu hatayı teşhis etmeme haklarına tamamen sahiptir, çünkü C ++ 11 §3.5 / 10 bildirimler için uyumlu türlerin gerekliliği hakkında,

[N3290 §3.5 / 10]
Tür kimliğine ilişkin bu kuralın ihlali için bir tanılama gerekmez.

Aynı paragraf izin verilen varyasyonu detaylandırır:

… Bir dizi nesnesi için bildirimler, bağlı bir ana dizinin varlığına veya yokluğuna göre farklılık gösteren dizi türlerini belirtebilir (8.3.4).

İzin verilen bu varyasyon, bir çeviri biriminde dizi olarak ve başka bir çeviri biriminde işaretçi olarak ad bildirilmesini içermez.

5.2 Tuzak: Erken optimizasyon ( memset& arkadaşlar) yapmak.

Henüz yazılmadı

5.3 Tuzak: Eleman sayısını elde etmek için C deyimini kullanma.

Derin C deneyimiyle yazmak doğaldır…

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

Bir yana arraygereken ilk elemana işaretçi Bozunma sentezleme sizeof(a)/sizeof(a[0])aynı zamanda şu şekilde yazılabilir sizeof(a)/sizeof(*a). Aynı anlama gelir ve nasıl yazılırsa yazılsın , dizinin sayı öğelerini bulmak için C deyimidir .

Ana tuzak: C deyim tipik değildir. Örneğin, kod…

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

bir işaretçi geçirir N_ITEMSve bu nedenle büyük olasılıkla yanlış sonuç verir. Windows 7 32-bit yürütülebilir olarak derlenen üretir…

7 eleman, çağrı ekranı ...
1 eleman.

  1. Derleyici int const a[7]sadece yeniden yazar int const a[].
  2. Derleyici yeniden yazar int const a[]için int const* a.
  3. N_ITEMS bu nedenle bir işaretçi ile çağrılır.
  4. 32 bit yürütülebilir dosya için sizeof(array)(işaretçi boyutu) 4 olur.
  5. sizeof(*array)sizeof(int)32 bit yürütülebilir dosya için de 4 olan eşdeğerdir .

Çalışma zamanında bu hatayı tespit etmek için şunları yapabilirsiniz…

#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 öğe, display çağrısı ...
Onaylama başarısız oldu: ("N_ITEMS bağımsız değişken olarak gerçek bir dizi gerektiriyor", typeid (a)! = Typeid (& * a)), runtime_detect ion.cpp, line 16 dosyası

Bu uygulama Runtime'dan alışılmadık bir şekilde sonlandırmasını istedi.
Daha fazla bilgi için lütfen uygulamanın destek ekibiyle iletişime geçin.

Çalışma zamanı hata tespiti, hiçbir algılamadan daha iyidir, ancak biraz işlemci süresi ve belki de daha fazla programcı zamanı harcar. Derleme zamanında algılama ile daha iyi! Ve C ++ 98 ile yerel tür dizilerini desteklemekten mutluluk duyarsanız, bunu yapabilirsiniz:

#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 ++ ile ilk tam program yerine bu tanımı derleme, ben var…

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: 'void display (const int *)'
işlevinde: compile_time_detection.cpp: 14: hata: 'n_items (const int * &)' çağrısı için eşleşen işlev yok

M: \ sayım> _

Nasıl çalışır: dizi referans olarak geçirilir n_itemsve bu nedenle ilk öğeye işaretçi çürümez ve işlev yalnızca tür tarafından belirtilen öğelerin sayısını döndürebilir.

C ++ 11 ile bunu yerel tür diziler için de kullanabilirsiniz ve bir dizinin öğe sayısını bulmak için güvenli C ++ deyim türüdür .

5.4 C ++ 11 ve C ++ 14 tuzağı: Bir constexprdizi boyutu işlevi kullanma .

C ++ 11 ve sonraki sürümlerde bu doğaldır, ancak gördüğünüz gibi C ++ 03 işlevini değiştirmek için tehlikeli!

typedef ptrdiff_t   Size;

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

ile

using Size = ptrdiff_t;

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

burada önemli değişiklik, constexprbu fonksiyonun bir derleme zaman sabiti üretmesine izin veren kullanımıdır .

Örneğin, C ++ 03 işlevinin aksine, böyle bir derleme zaman sabiti, başka bir boyutla aynı boyuttaki bir diziyi bildirmek için kullanılabilir:

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

Ancak constexprsürümü kullanarak bu kodu düşünün :

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

Tuzak: Temmuz 2015 itibariyle, yukarıdaki MinGW-64 5.1.0 ile derlenir -pedantic-errorsve gcc.godbolt.org/ adresindeki çevrimiçi derleyicilerle test edilir , ayrıca clang 3.0 ve clang 3.2 ile yapılır, ancak clang 3.3, 3.4 ile değil. 1, 3.5.0, 3.5.1, 3.6 (rc1) veya 3.7 (deneysel). Ve Windows platformu için önemli, Visual C ++ 2015 ile derlenmez. Bunun nedeni, constexprifadelerde referansların kullanımı ile ilgili bir C ++ 11 / C ++ 14 ifadesidir:

C ++ 11 C ++ 14 $ 5.19 / 2 dokuz inci çizgi

Bir koşullu ekspresyonu e a, çekirdek sabit ifade değerlendirilmesi sürece esoyut makine (1.9), aşağıdaki işlemlerden birini değerlendirmek olur kuralları gereğince,:
        ⋮

  • bir kimlik-sentezleme referans önceki bir başlatma vardır ve her iki sürece referans tipte bir değişken ya da veri elemanına değinmektedir
    • sabit bir ifade ile başlatılır veya
    • ömrü e'nin değerlendirilmesinde başlayan bir nesnenin statik olmayan bir veri üyesidir;

Kişi her zaman daha ayrıntılı yazabilir

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

… Ancak bu Collectionham bir dizi olmadığında başarısız olur .

Diziler olmayan koleksiyonlarla başa çıkmak için bir n_itemsfonksiyonun aşırı yüklenebilirliğine ihtiyaç duyulur , fakat aynı zamanda derleme zamanı için dizi büyüklüğünün derleme zamanı gösterimi gerekir. C ++ 11 ve C ++ 14'te de iyi çalışan klasik C ++ 03 çözümü, işlevin sonucunu bir değer olarak değil, işlev sonuç türü aracılığıyla rapor etmesine izin vermektir . Örneğin şöyle:

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

Dönüş türü seçimi hakkında static_n_items: bu kod kullanılmaz, std::integral_constant çünkü std::integral_constantsonuç doğrudan constexprorijinal değeri yeniden ortaya koyar ve bir değer olarak temsil edilir . Bir Size_carriersınıf yerine , işlevin doğrudan bir diziye başvuru döndürmesine izin verilebilir. Ancak, herkes bu sözdizimine aşina değildir.

Adlandırma hakkında: constexpr-invalid-nedeniyle-başvuru sorunu için bu çözümün bir parçası , derleme zamanı seçimini sürekli açık yapmaktır.

İnşallah o-bir-orada-referans-dahil- constexprsorunu - sorunu C ++ 17 ile giderilecektir, ama o zamana kadar STATIC_N_ITEMSyukarıdaki gibi bir makro taşınabilirlik, örneğin clang ve Visual C ++ derleyicileri, tutma türü verir Emniyet.

İlgili: makrolar kapsamlara saygı göstermez, bu nedenle ad çakışmalarını önlemek için bir ad öneki kullanmak iyi bir fikir olabilir, örn MYLIB_STATIC_N_ITEMS.


1
+1 Büyük C kodlama testi: VC ++ 10.0 ve GCC 4.1.2'de 15 dakikayı düzeltmeye çalışıyorum Segmentation fault... Sonunda açıklamalarınızı okuduktan sonra buldum / anladım! Lütfen §5.2 bölümünüzü yazın :-) Şerefe
olibre

İyi. Bir nit - countOf için dönüş türü ptrdiff_t yerine size_t olmalıdır. Muhtemelen C ++ 11 / 14'te constexpr ve noexcept olması gerektiğini belirtmek gerekir.
Ricky65

@ Ricky65: C ++ 11 düşüncelerinden bahsettiğiniz için teşekkürler. Bu özellikler için destek, Visual C ++ için çok geç oldu. size_tBununla ilgili olarak , bunun modern platformlar için bildiğim hiçbir avantajı yoktur, ancak C ve C ++ 'ın örtülü tip dönüşüm kuralları nedeniyle bir takım sorunları vardır. Yani, ptrdiff_tsorunları önlemek için çok kasıtlı olarak kullanılır size_t. Ancak bir g ++ bu sürece dizi boyutu şablon parametresi ile eşleşen bir sorun olduğunu bilmelidir size_t(Ben bu derleyiciye özgü olmayan sorun size_tönemli, ama YMMV düşünmüyorum ).
Şerefe ve s. - Alf

@Alf. Standart Çalışma Taslağı'nda (N3936) 8.3.4 Okudum - Bir dizinin sınırı ... "std :: size_t türünün dönüştürülmüş sabit ifadesi ve değeri sıfırdan büyük olmalı".
Ricky65

@Ricky: Tutarsızlığa atıfta bulunuyorsanız, bu ifade mevcut C ++ 11 standardında mevcut değildir, bu nedenle bağlamı tahmin etmek zordur, ancak çelişki (dinamik olarak ayrılmış bir dizi C + başına 0 olabilir +11 §5.3.4 / 7) muhtemelen C ++ 14 ile sonuçlanmaz. Taslaklar sadece şudur: taslaklar. Bunun yerine "onun" un ne anlama geldiğini soruyorsanız, dönüştürülmüş olanı değil, orijinal ifadeyi ifade eder. Üçüncü taraftan bundan söz ederseniz, belki de böyle bir cümlenin size_t, dizilerin boyutlarını belirtmek için kullanılması gerektiği anlamına geldiğini düşünüyorsunuz , elbette hayır.
Şerefe ve s. - Alf

72

Dizi oluşturma ve başlatma

Diğer herhangi bir C ++ nesnesinde olduğu gibi, diziler doğrudan adlandırılmış değişkenlerde depolanabilir (daha sonra boyut derleme zamanı sabiti olmalıdır; C ++ VLA'ları desteklemez ) veya yığın üzerinde anonim olarak saklanabilir ve dolaylı olarak işaretçiler (ancak o zaman boyut çalışma zamanında hesaplanabilir).

Otomatik diziler

Otomatik diziler ("yığın üzerinde" yaşayan diziler), kontrol akışı statik olmayan bir yerel dizi değişkeninin tanımından her geçtiğinde oluşturulur:

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

Başlatma, artan sırada gerçekleştirilir. İlk değerlerin öğe türüne bağlı olduğuna dikkat edin T:

  • Eğer Ta, POD (gibi intyukarıdaki örnekte), bir başlatma gerçekleşir.
  • Aksi takdirde, varsayılan yapıcı Ttüm öğeleri başlatır.
  • Eğer Thiçbir erişilebilir varsayılan-yapıcı sağlar, program derleme yapmaz.

Alternatif olarak, başlangıç ​​değerleri, küme ayraçları ile çevrelenen virgülle ayrılmış bir liste olan dizi başlatıcıda açıkça belirtilebilir :

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

Bu durumda, dizi başlatıcıdaki öğelerin sayısı dizinin boyutuna eşit olduğundan, boyutu manuel olarak belirtmek gereksizdir. Derleyici tarafından otomatik olarak çıkarılabilir:

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

Boyutu belirlemek ve daha kısa bir dizi başlatıcı sağlamak da mümkündür:

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

Bu durumda, geri kalan öğeler sıfır olarak başlatılır . C ++ 'ın boş bir dizi başlatıcısına izin verdiğini (tüm elemanlar sıfır başlatılır), C89'un buna izin vermediğini (en az bir değer gerekli) unutmayın. Ayrıca dizi başlatıcıların yalnızca dizileri başlatmak için kullanılabileceğini unutmayın ; daha sonra ödevlerde kullanılamazlar.

Statik diziler

Statik diziler ("veri segmentinde" yaşayan diziler) static, ad alanı kapsamındaki anahtar kelime ve dizi değişkenleriyle tanımlanan yerel dizi değişkenleridir ("genel değişkenler"):

int global_static_array[8];

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

(Ad alanı kapsamındaki değişkenlerin dolaylı olarak statik olduğunu unutmayın. staticAnahtar kelimeyi tanımlarına eklemenin tamamen farklı, kullanımdan kaldırılmış bir anlamı vardır .)

Statik dizilerin otomatik dizilerden nasıl farklı davrandığı aşağıda açıklanmıştır:

  • Dizi başlatıcısı olmayan statik diziler, herhangi bir başka olası başlatmadan önce sıfır başlatılır.
  • Statik POD dizileri tam olarak bir kez başlatılır ve başlangıç ​​değerleri genellikle yürütülebilir dosyaya dönüştürülür, bu durumda çalışma zamanında başlatma maliyeti yoktur. Bununla birlikte, bu her zaman en fazla yer tasarrufu sağlayan çözüm değildir ve standart için gerekli değildir.
  • Statik POD olmayan diziler, kontrol akışı tanımlarından ilk kez geçtiğinde başlatılır . Lokal statik diziler durumunda, işlev asla çağrılmazsa bu asla gerçekleşmeyebilir.

(Yukarıdakilerin hiçbiri dizilere özgü değildir. Bu kurallar diğer statik nesneler için de aynı derecede geçerlidir.)

Dizi veri üyeleri

Dizi veri üyeleri, sahip oldukları nesne oluşturulduğunda oluşturulur. Ne yazık ki, C ++ 03 üye başlatıcısı listesinde dizileri başlatmak için hiçbir araç sağlamaz , bu nedenle başlatma atamalarla taklit edilmelidir:

class Foo
{
    int primes[8];

public:

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

Alternatif olarak, yapıcı gövdesinde otomatik bir dizi tanımlayabilir ve öğeleri aşağıdaki gibi kopyalayabilirsiniz:

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 ++ 0 x, diziler olabilir liste sayesinde başlatıcı elemanda başlatıldı düzgün başlatma :

class Foo
{
    int primes[8];

public:

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

Varsayılan yapıcısı olmayan öğe türleriyle çalışan tek çözüm budur.

Dinamik diziler

Dinamik dizilerin adı yoktur, bu nedenle bunlara erişmenin tek yolu işaretçilerdir. İsimleri olmadığı için bundan böyle “anonim diziler” olarak bahsedeceğim.

C'de, anonim diziler mallocve arkadaşlar aracılığıyla oluşturulur . C ++ 'da, anonim diziler, anonim bir dizinin new T[size]ilk öğesine bir işaretçi döndüren sözdizimi kullanılarak oluşturulur :

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

Aşağıdaki ASCII sanatı, boyut çalışma zamanında 8 olarak hesaplanırsa bellek düzenini gösterir:

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

Açıkçası, anonim diziler, ayrı olarak saklanması gereken ekstra işaretçi nedeniyle adlandırılmış dizilerden daha fazla bellek gerektirir. (Ücretsiz mağazada ek yük de vardır.)

Burada herhangi bir dizi-işaretçi çürümesi olmadığını unutmayın . Değerlendirilmesi rağmen new int[size], aslında bir oluşturmak etmez dizi tamsayılar, ifadenin sonucu new int[size]olan daha önce tek bir tamsayı için bir işaretçi (birinci eleman), olup tamsayı dizisi veya bilinmeyen boyutu tamsayı dizisi için bir işaretçi. Statik tip sistemi, dizi boyutlarının derleme zamanı sabitleri olmasını gerektirdiğinden bu mümkün olmaz. (Bu nedenle, resimde statik tür bilgisi ile anonim diziye açıklama eklemedim.)

Öğeler için varsayılan değerlerle ilgili olarak, anonim diziler otomatik dizilere benzer şekilde davranır. Normalde, anonim POD dizileri başlatılmaz, ancak değer başlatmayı tetikleyen özel bir sözdizimi vardır:

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

(Noktalı virgülden hemen önce sondaki parantez çiftini not edin.) Yine, C ++ 0x kuralları basitleştirir ve tekdüze başlatma sayesinde anonim diziler için başlangıç ​​değerlerinin belirlenmesine izin verir:

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

Anonim bir dizi kullanarak işiniz bittiğinde, onu sisteme geri bırakmanız gerekir:

delete[] p;

Her anonim diziyi bir kez serbest bırakmalı ve daha sonra bir daha asla dokunmamalısınız. Hiçbir zaman serbest bırakılmaması bir bellek sızıntısına (veya daha genel olarak, öğe türüne, bir kaynak sızıntısına bağlı olarak) neden olur ve birden çok kez serbest bırakmaya çalışmak tanımlanmamış davranışa neden olur. Diziyi serbest bırakmak yerine dizi olmayan formu delete(veya free) kullanmak delete[]da tanımsız bir davranıştır .


2
Ad staticalanı kapsamındaki kullanımın kullanımdan kaldırılması C ++ 11'de kaldırıldı.
efsane2k

newBen am operatörü olduğundan , kesinlikle allcated dizi başvuru ile dönebilir. Bunun bir anlamı yok ...
Deduplicator

@Deduplicator Hayır, çünkü tarihsel olarak newreferanslardan çok daha eskidir.
fredoverflow

@FredOverflow: Bu yüzden bir referans döndürememesinin bir nedeni var, bu yazılı açıklamadan tamamen farklı.
Tekilleştirici

2
@Deduplicator Bilinmeyen sınırların bir dizisine başvuru olduğunu düşünmüyorum. En azından g ++ derlemeyi reddediyorint a[10]; int (&r)[] = a;
fredoverflow 27:04
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.