development

C ++에서 배열을 어떻게 사용합니까?

big-blog 2020. 2. 15. 23:07
반응형

C ++에서 배열을 어떻게 사용합니까?


C ++은 거의 모든 곳에서 사용되는 C의 배열을 상속했습니다. C ++는 사용하기 쉽고 오류가 덜 발생하는 추상화 ( std::vector<T>C ++ 98 std::array<T, n>부터 C ++ 11 부터 )를 제공하므로 C 에서처럼 배열이 필요하지 않습니다. 그러나 레거시를 읽을 때 코드로 작성하거나 C로 작성된 라이브러리와 상호 작용하려면 배열의 작동 방식을 확실하게 파악해야합니다.

이 FAQ는 다섯 부분으로 나뉩니다.

  1. 유형 수준 및 액세스 요소의 배열
  2. 배열 생성 및 초기화
  3. 할당 및 매개 변수 전달
  4. 다차원 배열과 포인터 배열
  5. 배열을 사용할 때의 일반적인 함정

이 FAQ에서 중요한 내용이 누락되었다고 생각되면 답변을 작성하여 여기에 추가 부분으로 연결하십시오.

다음 텍스트에서 "배열"은 클래스 템플릿이 아니라 "C 배열"을 의미 std::array합니다. C 선언자 구문에 대한 기본 지식이 있다고 가정합니다. 아래 에서 수동으로 사용 new하고 delete아래에 설명 된대로 예외 상황에서는 매우 위험하지만 이는 다른 FAQ 의 주제입니다 .

(참고 : 이것은 Stack Overflow의 C ++ FAQ에 대한 항목 입니다.이 양식으로 FAQ를 제공한다는 아이디어를 비판하려면이 모든 것을 시작한 메타에 게시 하면됩니다. 이 질문은 C ++ 대화방 에서 모니터링되며 여기서 FAQ 아이디어는 처음부터 시작되었으므로 아이디어를 얻은 사람들이 귀하의 답변을 읽을 가능성이 큽니다.)


타입 레벨의 배열

배열 유형으로 표시되는 T[n]T은 IS 요소 유형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).

배열에서 포인터로의 붕괴

간의 유일한 "연결" T[n]T[m]두 가지 유형을 암시 할 수있다 환산 으로 T*, 이러한 변환의 결과가 상기 어레이의 첫번째 엘리먼트에 대한 포인터이다. 즉, T*가 필요한 곳이면 어디서나 를 제공 할 수 T[n]있으며 컴파일러는 자동으로 해당 포인터를 제공합니다.

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

이 변환은 "배열 간 포인터 붕괴"라고하며 혼란의 주요 원인입니다. 이 프로세스에서 배열의 크기는 더 이상 유형 ( T*)의 일부가 아니므로 손실됩니다 . 장점 : 형식 수준에서 배열의 크기를 잊어 버리면 포인터가 모든 크기 의 배열의 첫 번째 요소를 가리킬 있습니다. 단점 : 배열의 첫 번째 (또는 다른) 요소에 대한 포인터가 주어지면 해당 배열의 크기 또는 배열의 경계를 기준으로 포인터가 가리키는 위치를 감지 할 방법이 없습니다. 포인터는 매우 바보 입니다.

배열은 포인터가 아니다

컴파일러는 유용한 것으로 간주 될 때마다, 즉 배열에서 작업이 실패하지만 포인터에서 성공할 때마다 배열의 첫 번째 요소에 대한 포인터를 자동으로 생성합니다. 결과 포인터 이 단순히 배열의 주소 이기 때문에 배열에서 포인터로의 변환은 쉽지 않습니다. 포인터는 배열 자체의 일부 (또는 메모리의 다른 위치)로 저장 되지 않습니다 . 배열은 포인터가 아닙니다.

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 선언자 구문에 익숙하지 않은 경우 유형의 괄호 int(*)[8]가 필수적입니다.

  • int(*)[8] 8 정수 배열의 포인터입니다.
  • int*[8]는 유형의 각 요소 인 8 개의 포인터 배열입니다 int*.

요소에 액세스

C ++는 배열의 개별 요소에 액세스하기위한 두 가지 구문 변형을 제공합니다. 둘 중 어느 쪽도 다른 쪽보다 우월하지 않으므로 두 가지 모두에 익숙해 져야합니다.

포인터 산술

p배열의 첫 번째 요소에 대한 포인터 주어지면 표현식 p+i배열 의 i 번째 요소에 대한 포인터를 생성합니다. 나중에 해당 포인터를 참조 해제하면 개별 요소에 액세스 할 수 있습니다.

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 번째 요소에 대한 포인터 계산을 허용하지만 해당 포인터를 역 참조하는 것은 불법입니다.

   +---+---+---+---+---+---+---+---+....
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)이며 하위 표현식은 *(x+n)기술적 으로 C ++에서 정의되지 않은 동작호출합니다 (C99는 아님).

또한 x첫 번째 주장으로 간단히 제공 할 수 있습니다. 그것은 내 취향에 비해 너무 간결하며 컴파일러의 경우 템플릿 인수 공제를 조금 더 어렵게 만듭니다.이 경우 첫 번째 인수는 배열이지만 두 번째 인수는 포인터이기 때문입니다. (다시 말해서 배열 간 포인터 붕괴가 시작됩니다.)


프로그래머는 종종 다차원 배열과 포인터 배열을 혼동합니다.

다차원 배열

대부분의 프로그래머는 명명 된 다차원 배열에 익숙하지만, 다차원 배열도 익명으로 만들 수 있다는 사실을 모르는 사람이 많습니다. 다차원 배열은 종종 "어레이 배열"또는 " 다차원 배열"이라고합니다.

명명 된 다차원 배열

명명 된 다차원 배열을 사용하는 경우 모든 차원을 컴파일 타임에 알아야합니다.

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]같은 요소를 나타냅니다! 즉, 다차원 배열을 가져 와서 큰 1 차원 배열로 취급 할 수 있습니다.

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: | | |
      +---+

배열 자체는 여전히 메모리에서 단일 블록으로 할당됩니다.

포인터 배열

다른 수준의 간접 지시를 도입하여 고정 너비 제한을 극복 할 수 있습니다.

명명 된 포인터 배열

다음은 길이가 다른 익명의 배열로 초기화 된 5 개의 포인터로 구성된 명명 된 배열입니다.

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(원래 2 차원 배열의 행의 첫 번째 요소에 대한 각 포인팅),하지만 포인터 배열은 어디서나 존재하지 않습니다 아직 메모리. 이러한 변환을 원하면 필요한 포인터 배열을 수동으로 작성하고 채워야합니다.

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;

할당

특별한 이유로 배열을 서로 지정할 수 없습니다. 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더 잘 수행 되지는 않습니다 . 의심스러운 경우 측정하십시오.

배열을 직접 할당 할 수는 없지만 배열 멤버 포함 하는 구조체와 클래스를 할당 수 있습니다 . 이는 배열 멤버가 컴파일러에서 기본값으로 제공되는 할당 연산자에 의해 멤버 단위로 복사 되기 때문 입니다. 자신의 구조체 또는 클래스 유형에 대해 할당 연산자를 수동으로 정의하는 경우 배열 멤버의 수동 복사로 폴백해야합니다.

매개 변수 전달

값으로 배열을 전달할 수 없습니다. 포인터 나 참조로 전달할 수 있습니다.

포인터로 전달

배열 자체는 값으로 전달 될 수 없으므로 일반적으로 첫 번째 요소에 대한 포인터가 값으로 전달됩니다. 이것을 종종 "포인터로 전달"이라고합니다. 배열의 크기는 해당 포인터를 통해 검색 할 수 없으므로 배열의 크기를 나타내는 두 번째 매개 변수 (클래식 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);
}

정수에 대한 포인터가 아니라 실제 정수 배열로만 함수 템플릿을 호출 할 수 있습니다. 배열의 크기는 자동으로 추론되며 모든 size n에 대해 템플릿에서 다른 함수가 인스턴스화됩니다. 요소 유형과 크기 모두에서 추상화되는 매우 유용한 함수 템플릿을 작성할 수도 있습니다 .


배열 생성 및 초기화

다른 종류의 C ++ 객체와 마찬가지로, 배열은 명명 된 변수에 직접 저장 될 수 있습니다 (그런 다음 크기는 컴파일 타임 상수 여야합니다. C ++는 VLA를 지원하지 않습니다 ). 또는 힙에 익명으로 저장되어 간접적으로 액세스 될 수 있습니다. 포인터 (런타임에서만 크기를 계산할 수 있음).

자동 배열

제어 흐름이 비 정적 로컬 배열 변수의 정의를 통과 할 때마다 자동 배열 ( "스택"에있는 배열)이 생성됩니다.

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

초기화는 오름차순으로 수행됩니다. 초기 값은 요소 유형에 따라 다릅니다 T.

  • 경우 TA는 POD (같은 int위의 예)에는 초기화가 일어나지 않는다.
  • 그렇지 않으면 기본 생성자가 T모든 요소 초기화합니다.
  • 경우 T에는 접근 기본적으로 생성자를 제공하지 않습니다, 프로그램은 컴파일되지 않습니다.

또는 초기 값은 괄호로 묶은 쉼표로 구분 된 목록으로 배열 초기화 프로그램 에서 명시 적으로 지정할 수 있습니다 .

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

이 경우 배열 이니셜 라이저의 요소 수는 배열의 크기와 같으므로 수동으로 크기를 지정하면 중복됩니다. 컴파일러가 자동으로 추론 할 수 있습니다.

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

크기를 지정하고 더 짧은 배열 이니셜 라이저를 제공 할 수도 있습니다.

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

이 경우 나머지 요소는 0으로 초기화 됩니다. C ++에서는 빈 배열 이니셜 라이저 (모든 요소가 0으로 초기화 됨)를 허용하지만 C89는 그렇지 않습니다 (적어도 하나의 값이 필요함). 또한 그 배열 초기화는 할 수 있습니다주의 초기화 배열; 나중에 과제에 사용할 수 없습니다.

정적 배열

정적 배열 ( "데이터 세그먼트"에있는 배열)은 static네임 스페이스 범위 키워드 및 배열 변수 ( "전역 변수")로 정의 된 로컬 배열 변수입니다 .

int global_static_array[8];

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

네임 스페이스 범위의 변수는 암시 적으로 정적입니다. static정의에 키워드를 추가하면 완전히 다른 사용되지 않는 의미가 있습니다.

정적 배열이 자동 배열과 다르게 동작하는 방법은 다음과 같습니다.

  • 배열 이니셜 라이저가없는 정적 배열은 추가로 초기화하기 전에 0으로 초기화됩니다.
  • 정적 POD 배열은 정확히 한 번 초기화 되고 초기 값은 일반적으로 실행 파일에 구워 지며, 이 경우 런타임시 초기화 비용이 없습니다. 이것이 항상 가장 공간 효율적인 솔루션 인 것은 아니며 표준에서 요구하지는 않습니다.
  • 정적 비 POD 어레이는 제어 흐름이 처음으로 정의를 통과 할 초기화 됩니다. 로컬 정적 배열의 경우 함수가 호출되지 않으면 발생하지 않을 수 있습니다.

위의 어느 것도 배열에만 해당되지 않습니다. 이러한 규칙은 다른 종류의 정적 객체에도 동일하게 적용됩니다.

배열 데이터 멤버

자신의 객체가 생성 될 때 배열 데이터 멤버가 생성됩니다. 불행히도 C ++ 03은 멤버 이니셜 라이저 목록 에서 배열을 초기화 할 수단을 제공하지 않으므로 할당을 통해 초기화를 위조해야합니다.

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에서는 균일 한 초기화 덕분에 멤버 이니셜 라이저 목록에서 배열 초기화 할 수 있습니다 .

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[]것도 정의되지 않은 동작 입니다.


5. 배열을 사용할 때의 일반적인 함정.

5.1 함정 : 신뢰할 수없는 유형 안전 연결.

전역 (번역 단위 외부에서 액세스 할 수있는 네임 스페이스 범위 변수)이 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 (Undefined Behavior)가 있으므로 충돌하지 않고 그냥 중단하거나 아무 것도하지 않거나 미국, 러시아, 인도, 중국과 스위스, 코에서 코피를 날리십시오.

실습 설명 : 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하므로 대부분 잘못된 결과를 생성합니다. Windows 7에서 32 비트 실행 파일로 컴파일되어…

7 개의 요소, 디스플레이 호출 ...
1 개의 요소.

  1. 컴파일러 int const a[7]는 just로 다시 씁니다 int const a[].
  2. 컴파일러는 재 작성 int const a[]int const* a.
  3. N_ITEMS 따라서 포인터로 호출됩니다.
  4. 32 비트 실행 파일 sizeof(array)(포인터 크기)의 경우 4입니다.
  5. sizeof(*array)동등 sizeof(int)32- 비트 실행을위한도 4이다.

런타임시이 오류를 감지하려면 다음을 수행하십시오.

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

이 응용 프로그램은 런타임을 비정상적인 방식으로 종료하도록 요청했습니다.
자세한 내용은 응용 프로그램 지원 팀에 문의하십시오.

런타임 오류 감지는 감지하지 않는 것보다 낫지 만 프로세서 시간이 약간 소요되고 프로그래머 시간이 훨씬 더 많이 소요됩니다. 컴파일 타임에 감지 기능이 향상되었습니다! 그리고 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 : error : '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 );
}

함정 : 2015 년 7 월 현재 위의 내용은 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로 컴파일되지 않습니다. 그 이유는 constexpr식에서 참조 사용에 대한 C ++ 11 / C ++ 14 문입니다 .

C ++ 11 C ++ 14 $ 5.19 / 2 아홉 번째 대시

조건 표현은 e A는 핵심 상수 표현 의 평가하지 않는 e추상 기계 (1.9), 다음 식 중 하나를 평가하는 것의 규칙에 따라, :
        ⋮

  • 참조에 선행 초기화가없고 다음 중 하나가 아닌 경우 참조 유형의 변수 또는 데이터 멤버를 참조 하는 id- 표현식
    • 상수 표현식으로 초기화되거나
    • e의 평가 내에서 수명이 시작된 객체의 비 정적 데이터 멤버입니다.

항상 더 장황하게 쓸 수 있습니다

// 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 ++ 11 및 C ++ 14에서도 잘 작동하는 클래식 C ++ 03 솔루션은 함수가 결과를 값이 아니라 함수 결과 유형을 통해보고하도록하는 것 입니다. 예를 들면 다음과 같습니다.

// 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-to-to-to-reference 문제에 대한 이 솔루션의 일부 는 컴파일 타임을 일정하게 선택하는 것입니다.

바라건대 oops-the-the-a-a-a-a-a-a-a-a-in-your- constexpr문제는 C ++ 17로 수정 될 것이지만, 그때까지 STATIC_N_ITEMS위와 같은 매크로는 유형을 유지하는 clang 및 Visual C ++ 컴파일러와 같은 이식성을 제공합니다. 안전.

관련 : 매크로는 범위를 고려하지 않으므로 이름 충돌을 피하려면 이름 접두사를 사용하는 것이 좋습니다 (예 :) MYLIB_STATIC_N_ITEMS.

참고 URL : https://stackoverflow.com/questions/4810664/how-do-i-use-arrays-in-c



반응형