Namespaces
Variants

Array declaration

From cppreference.net

배열은 특정 요소 타입 을 가진 객체들의 연속적으로 할당된 비어 있지 않은 시퀀스로 구성된 타입입니다. 이러한 객체들의 개수(배열 크기)는 배열 수명 동안 절대 변경되지 않습니다.

목차

구문

배열 선언의 선언 문법 에서, type-specifier 시퀀스는 element type (완전한 객체 타입이어야 함)을 지정하며, declarator 는 다음 형태를 가집니다:

[ static (선택 사항) 한정자  (선택 사항) 표현식  (선택 사항) ] 속성-명세-시퀀스  (선택 사항) (1)
[ 한정자  (선택 사항) static (선택 사항) 표현식  (선택 사항) ] 속성-명세-시퀀스  (선택 사항) (2)
[ 한정자  (선택 사항) * ] 속성-명세-시퀀스  (선택 사항) (3)
1,2) 일반 배열 선언자 구문
3) 지정되지 않은 크기의 VLA에 대한 선언자 (함수 프로토타입 범위에서만 나타날 수 있음) 여기서
expression - 쉼표 연산자 를 제외한 모든 표현식, 배열의 요소 수를 지정함
qualifiers - const , restrict , 또는 volatile 한정자의 모든 조합, 함수 매개변수 목록에서만 허용됨; 이는 이 배열 매개변수가 변환되는 포인터 타입을 한정함
attr-spec-seq - (C23) 속성 의 선택적 목록, 선언된 배열에 적용됨
float fa[11], *afp[17]; // fa는 11개의 float로 이루어진 배열입니다
                        // afp는 float에 대한 17개의 포인터 배열입니다

설명

배열 타입에는 여러 가지 변형이 있습니다: 알려진 상수 크기의 배열, 가변 길이 배열, 그리고 크기를 알 수 없는 배열이 있습니다.

상수로 알려진 크기의 배열

만약 expression 이 배열 선언자에서 정수 상수 표현식 이고 값이 0보다 큰 경우 그리고 요소 유형이 알려진 상수 크기를 가진 유형인 경우(즉, 요소가 VLA가 아님) (C99부터) , 해당 선언자는 상수로 알려진 크기의 배열을 선언합니다:

int n[10]; // 정수 상수는 상수 표현식입니다
char o[sizeof(double)]; // sizeof는 상수 표현식입니다
enum { MAX_SZ=100 };
int n[MAX_SZ]; // enum 상수는 상수 표현식입니다

상수로 알려진 크기의 배열은 배열 초기화 를 사용하여 초기값을 제공할 수 있습니다:

int a[5] = {1,2,3}; // int[5]을 선언하고 1,2,3,0,0으로 초기화
char str[] = "abc"; // char[4]을 선언하고 'a','b','c','\0'으로 초기화

함수 매개변수 목록에서, 배열 선언자 내에 추가적인 구문 요소들이 허용됩니다: static 키워드와 qualifiers 가 있으며, 이들은 크기 표현식 앞에 임의의 순서로 나타날 수 있습니다 (크기 표현식이 생략된 경우에도 나타날 수 있습니다).

함수 호출 에서 배열 매개변수가 static 키워드를 [ ] 사이에 사용하는 함수에 대해, 실제 매개변수의 값은 expression 으로 지정된 요소 수 이상을 가진 배열의 첫 번째 요소를 가리키는 유효한 포인터여야 합니다:

void fadd(double a[static 10], const double b[static 10])
{
    for (int i = 0; i < 10; i++)
    {
        if (a[i] < 0.0) return;
        a[i] += b[i];
    }
}
// fadd 호출은 컴파일 타임 경계 검사를 수행할 수 있으며
// 10개의 double을 미리 가져오는 등의 최적화도 허용합니다
int main(void)
{
    double a[10] = {0}, b[20] = {0};
    fadd(a, b); // 정상
    double x[5] = {0};
    fadd(x, b); // 정의되지 않은 동작: 배열 인수가 너무 작음
}

만약 qualifiers 가 존재한다면, 이들은 배열 매개변수 타입이 변환되는 포인터 타입을 한정합니다:

int f(const int a[20])
{
    // 이 함수에서 a는 const int* 타입을 가집니다 (const int에 대한 포인터)
}
int g(const int a[const 20])
{
    // 이 함수에서 a는 const int* const 타입을 가집니다 (const int에 대한 const 포인터)
}

이는 일반적으로 restrict 타입 한정자와 함께 사용됩니다:

void fadd(double a[static restrict 10],
          const double b[static restrict 10])
{
    for (int i = 0; i < 10; i++) // 루프는 언롤링되고 재정렬될 수 있음
    {
        if (a[i] < 0.0)
            break;
        a[i] += b[i];
    }
}

가변 길이 배열

만약 expression integer constant expression 이 아니라면, 선언자는 가변 크기 배열을 위한 것입니다.

제어 흐름이 선언을 지날 때마다, expression 이 평가되며(항상 0보다 큰 값으로 평가되어야 함), 배열이 할당됩니다(이에 상응하여 VLA의 lifetime 은 선언이 범위를 벗어날 때 종료됩니다). 각 VLA 인스턴스의 크기는 수명 동안 변경되지 않지만, 동일한 코드를 다시 지날 때는 다른 크기로 할당될 수 있습니다.

#include <stdio.h>
int main(void)
{
   int n = 1;
label:;
   int a[n]; // 10번 재할당되며, 매번 다른 크기를 가짐
   printf("The array has %zu elements\n", sizeof a / sizeof *a);
   if (n++ < 10)
       goto label; // VLA의 범위를 벗어나면 수명이 종료됨
}

크기가 * 인 경우, 이는 크기가 지정되지 않은 VLA에 대한 선언입니다. 이러한 선언은 함수 프로토타입 범위에서만 나타날 수 있으며, 완전한 타입의 배열을 선언합니다. 사실, 함수 프로토타입 범위의 모든 VLA 선언자는 expression * 로 대체된 것처럼 처리됩니다.

void foo(size_t x, int a[*]);
void foo(size_t x, int a[x])
{
    printf("%zu\n", sizeof a); // sizeof(int*)와 동일
}

가변 길이 배열과 그것들로부터 파생된 타입(이에 대한 포인터 등)은 일반적으로 "가변 수정 타입"(VM)으로 알려져 있습니다. 모든 가변 수정 타입의 객체는 블록 범위나 함수 프로토타입 범위에서만 선언될 수 있습니다.

extern int n;
int A[n];            // 오류: 파일 범위 VLA
extern int (*p2)[n]; // 오류: 파일 범위 VM
int B[100];          // OK: 상수 크기를 아는 파일 범위 배열
void fvla(int m, int C[m][m]); // OK: 프로토타입 범위 VLA

VLA는 자동 또는 할당된 저장 기간을 가져야 합니다. VLA에 대한 포인터는 정적 저장 기간을 가질 수 있지만, VLA 자체는 그럴 수 없습니다. 어떤 VM 유형도 링크를 가질 수 없습니다.

void fvla(int m, int C[m][m]) // OK: 블록 범위/자동 지속 시간 VLA 포인터
{
    typedef int VLA[m][m]; // OK: 블록 범위 VLA
    int D[m];              // OK: 블록 범위/자동 지속 시간 VLA
//  static int E[m]; // 오류: 정적 지속 시간 VLA
//  extern int F[m]; // 오류: 링크를 가지는 VLA
    int (*s)[m];     // OK: 블록 범위/자동 지속 시간 VM
    s = malloc(m * sizeof(int)); // OK: s는 할당된 저장 공간의 VLA를 가리킴
//  extern int (*r)[m]; // 오류: 링크를 가지는 VM
    static int (*q)[m] = &B; // OK: 블록 범위/정적 지속 시간 VM}
}

가변 수정 타입은 구조체나 공용체의 멤버가 될 수 없습니다.

struct tag
{
    int z[n]; // 오류: VLA 구조체 멤버
    int (*y)[n]; // 오류: VM 구조체 멤버
};
(C99 이후)

컴파일러가 매크로 상수 __STDC_NO_VLA__ 를 정수 상수 1 로 정의하는 경우, VLA 및 VM 타입은 지원되지 않습니다.

(C11부터)
(C23까지)

컴파일러가 매크로 상수 __STDC_NO_VLA__ 를 정수 상수 1 로 정의하는 경우, 자동 저장 기간을 가진 VLA 객체는 지원되지 않습니다.

할당된 저장 기간을 가진 VM 타입과 VLA에 대한 지원은 필수입니다.

(C23부터)

크기를 알 수 없는 배열

만약 expression 이 배열 선언자에서 생략되면, 이는 크기를 알 수 없는 배열을 선언합니다. 함수 매개변수 목록(이러한 배열이 포인터로 변환되는 경우)과 initializer 가 사용 가능한 경우를 제외하고, 이러한 타입은 incomplete type 입니다 (크기가 지정되지 않은 VLA는 * 를 크기로 선언되며 완전한 타입임을 유의하세요) (C99부터) :

extern int x[]; // x의 타입은 "크기를 알 수 없는 int 배열"입니다
int a[] = {1,2,3}; // a의 타입은 "3개의 int 배열"입니다

struct 정의 내에서, 크기가 알려지지 않은 배열은 (다른 이름 있는 멤버가 최소 하나 이상 존재하는 경우) 마지막 멤버로 나타날 수 있으며, 이 경우 flexible array member 로 알려진 특별한 경우입니다. 자세한 내용은 struct 를 참조하십시오:

struct s { int n; double d[]; }; // s.d is a flexible array member
struct s *s1 = malloc(sizeof (struct s) + (sizeof (double) * 8)); // as if d was double d[8]


(C99부터)

한정자

배열 타입이 const , volatile , 또는 restrict (C99부터) 한정자로 선언되면( typedef 를 사용하여 가능함), 배열 타입 자체는 한정되지 않지만 그 요소 타입은 한정됩니다:

(C23 이전)

배열 타입과 그 요소 타입은 항상 동일하게 한정된 것으로 간주되지만, 배열 타입은 절대 _Atomic -한정된 것으로 간주되지 않습니다.

(C23부터)
typedef int A[2][3];
const A a = {{4, 5, 6}, {7, 8, 9}}; // const int 배열의 배열
int* pi = a[0]; // 오류: a[0]의 타입은 const int*
void* unqual_ptr = a; // C23까지는 OK; C23 이후부터는 오류
// 참고: clang은 C89-C17 모드에서도 C++/C23 규칙을 적용함

_Atomic 는 배열 타입에 적용할 수 없지만, 원자 타입의 배열은 허용됩니다.

typedef int A[2];
// _Atomic A a0 = {0};    // 오류
// _Atomic(A) a1 = {0};   // 오류
_Atomic int a2[2] = {0};  // OK
_Atomic(int) a3[2] = {0}; // OK
(C11부터)

할당

배열 타입의 객체는 수정 가능한 lvalue 가 아니며, 비록 그 주소를 취할 수는 있지만 대입 연산자의 좌변에 나타날 수 없습니다. 그러나 배열 멤버를 가진 구조체는 수정 가능한 lvalue이며 대입될 수 있습니다:

int a[3] = {1,2,3}, b[3] = {4,5,6};
int (*p)[3] = &a; // 정상, a의 주소를 취할 수 있음
// a = b;            // 오류, a는 배열임
struct { int c[3]; } s1, s2 = {3,4,5};
s1 = s2; // 정상: 배열 멤버를 포함하는 구조체는 대입 가능

배열에서 포인터로의 변환

배열 타입의 모든 lvalue 표현식 은 다음을 제외한 모든 컨텍스트에서 사용될 때

(C11부터)

첫 번째 요소에 대한 포인터로 암시적 변환 을 수행합니다. 결과는 lvalue가 아닙니다.

배열이 register 로 선언된 경우, 이러한 변환을 시도하는 프로그램의 동작은 정의되지 않습니다.

int a[3] = {1,2,3};
int* p = a;
printf("%zu\n", sizeof a); // 배열의 크기를 출력
printf("%zu\n", sizeof p); // 포인터의 크기를 출력

배열 타입이 함수 매개변수 목록에서 사용될 때, 해당 포인터 타입으로 변환됩니다: int f ( int a [ 2 ] ) int f ( int * a ) 는 동일한 함수를 선언합니다. 함수의 실제 매개변수 타입이 포인터 타입이므로, 배열 인자를 사용한 함수 호출은 배열-포인터 변환을 수행합니다; 인자 배열의 크기는 호출된 함수에서 사용할 수 없으며 명시적으로 전달되어야 합니다:

#include <stdio.h>
void f(int a[], int sz) // 실제로는 void f(int* a, int sz)를 선언함
{
    for (int i = 0; i < sz; ++i)
        printf("%d\n", a[i]);
}
void g(int (*a)[10]) // 배열에 대한 포인터 매개변수는 변환되지 않음
{
    for (int i = 0; i < 10; ++i)
        printf("%d\n", (*a)[i]);
}
int main(void)
{
    int a[10] = {0};
    f(a, 10); // a를 int*로 변환하고 포인터를 전달함
    g(&a);    // 배열에 대한 포인터를 전달함 (크기를 전달할 필요 없음)
}

다차원 배열

배열의 원소 타입이 다른 배열일 때, 그 배열을 다차원 배열이라고 합니다:

// 2개의 배열로 이루어진 배열, 각 배열은 3개의 int를 가짐
int a[2][3] = {{1,2,3},  // 2x3 행렬로 볼 수 있음
               {4,5,6}}; // 행 우선 배치 방식

배열-포인터 변환이 적용될 때, 다차원 배열은 첫 번째 요소에 대한 포인터로 변환된다는 점에 유의하십시오. 예를 들어, 첫 번째 행에 대한 포인터로 변환됩니다:

int a[2][3]; // 2x3 행렬
int (*p1)[3] = a; // 첫 번째 3-요소 행에 대한 포인터
int b[3][3][3]; // 3x3x3 큐브
int (*p2)[3][3] = b; // 첫 번째 3x3 평면에 대한 포인터

다차원 배열은 모든 차원에서 가변적으로 수정될 수 있습니다 (VLA가 지원되는 경우) (C11부터) :

int n = 10;
int a[n][2*n];
(C99부터)

참고 사항

길이가 0인 배열 선언은 허용되지 않으며, 일부 컴파일러가 확장 기능으로 제공하기도 합니다(일반적으로 C99 이전의 flexible array members 구현으로).

VLA의 크기 expression 에 부작용이 있는 경우, sizeof 표현식의 결과가 그것에 의존하지 않는 경우를 제외하고 부작용이 발생하는 것이 보장됩니다:

int n = 5, m = 5;
size_t sz = sizeof(int (*[n++])[m++]); // n은 증가하지만, m은 증가할 수도 있고 증가하지 않을 수도 있음

참고문헌

  • C23 표준 (ISO/IEC 9899:2024):
  • 6.7.6.2 배열 선언자 (p: TBD)
  • C17 표준 (ISO/IEC 9899:2018):
  • 6.7.6.2 배열 선언자 (p: 94-96)
  • C11 표준 (ISO/IEC 9899:2011):
  • 6.7.6.2 배열 선언자 (p: 130-132)
  • C99 표준 (ISO/IEC 9899:1999):
  • 6.7.5.2 배열 선언자 (p: 116-118)
  • C89/C90 표준 (ISO/IEC 9899:1990):
  • 3.5.4.2 배열 선언자

참고 항목

C++ 문서 참조: 배열 선언