배열 선언
배열 선언 (Array declaration)
같은 타입의 값 여러 개를 연속된 메모리에 나란히 두고 싶을 때 쓰는 게 배열이에요. 배열은 특정 요소 타입(element type)을 가진 객체들이 연속적으로 할당된 비어있지 않은 나열이고, 그 객체의 개수(배열 크기)는 배열의 수명 동안 변하지 않아요. 이 페이지에서 배열을 선언하는 문법과 여러 종류의 배열을 정리해 볼게요.
출처: cppreference
본문
배열은 특정 요소 타입을 가진 객체들이 연속적으로 할당된 비어있지 않은 나열로 구성된 타입이에요. 그 객체들의 개수(배열 크기)는 배열 수명 동안 절대 변하지 않아요.
문법
배열 선언의 선언 문법에서, 타입 지정자(type-specifier) 나열은 요소 타입(완전 객체 타입이어야 함)을 지정하고, 선언자(declarator)는 다음 형태를 가져요.
[ static(optional) qualifiers(optional) expression(optional) ] attr-spec-seq(optional) |
(1) | |
[ qualifiers(optional) static(optional) expression(optional) ] attr-spec-seq(optional) |
(2) | |
[ qualifiers(optional) * ] attr-spec-seq(optional) |
(3) |
1,2) 일반 배열 선언자 문법 3) 크기를 지정하지 않는 VLA용 선언자 (함수 프로토타입 스코프에서만 나타날 수 있음)
여기서
expression |
- | 쉼표 연산자를 제외한 아무 표현식. 배열의 요소 개수를 지정함 |
qualifiers |
- | const, restrict, volatile 한정자의 아무 조합. 함수 매개변수 목록에서만 허용되며, 이 배열 매개변수가 변환되는 포인터 타입을 한정함 |
attr-spec-seq |
- | (C23) 선언되는 배열에 적용되는 속성의 선택적 목록 |
float fa[11], *afp[17]; // fa is an array of 11 floats
// afp is an array of 17 pointers to floats
설명
배열 타입에는 여러 변형이 있어요. 상수로 알려진 크기의 배열(constant known size), 가변 길이 배열(VLA, variable-length array), 그리고 크기를 알 수 없는 배열(unknown size)이 그것이에요.
상수로 알려진 크기의 배열
배열 선언자의 expression이 0보다 큰 값을 가진 정수 상수 표현식이고 요소 타입이 알려진 상수 크기를 가진 타입(즉 요소가 VLA가 아님)이라면(since C99), 그 선언자는 상수로 알려진 크기의 배열을 선언해요.
int n[10]; // integer constants are constant expressions
char o[sizeof(double)]; // sizeof is a constant expression
enum { MAX_SZ=100 };
int n[MAX_SZ]; // enum constants are constant expressions
상수로 알려진 크기의 배열은 배열 초기화자를 써서 초기값을 줄 수 있어요.
int a[5] = {1,2,3}; // declares int[5] initialized to 1,2,3,0,0
char str[] = "abc"; // declares char[4] initialized to 'a','b','c','\0'
함수 매개변수 목록에서는 배열 선언자 안에 추가 문법 요소가 허용돼요. 키워드 static과 한정자는 크기 표현식 앞에 어떤 순서로든 나타날 수 있어요 (크기 표현식이 생략됐을 때도 나타날 수 있어요).
배열 매개변수가 [와 ] 사이에 키워드 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];
}
}
// a call to fadd may perform compile-time bounds checking
// and also permits optimizations such as prefetching 10 doubles
int main(void)
{
double a[10] = {0}, b[20] = {0};
fadd(a, b); // OK
double x[5] = {0};
fadd(x, b); // undefined behavior: array argument is too small
}
한정자가 있으면 그것은 배열 매개변수 타입이 변환되는 포인터 타입을 한정해요.
int f(const int a[20])
{
// in this function, a has type const int* (pointer to const int)
}
int g(const int a[const 20])
{
// in this function, a has type const int* const (const pointer to const int)
}
이건 restrict 타입 한정자와 함께 쓰는 경우가 흔해요.
void fadd(double a[static restrict 10],
const double b[static restrict 10])
{
for (int i = 0; i < 10; i++) // loop can be unrolled and reordered
{
if (a[i] < 0.0)
break;
a[i] += b[i];
}
}
가변 길이 배열 (Variable-length arrays)
expression이 정수 상수 표현식이 아니면, 그 선언자는 가변 크기의 배열용이에요.
제어 흐름이 선언을 지날 때마다 expression이 평가되고(항상 0보다 큰 값으로 평가돼야 함), 배열이 할당돼요 (그에 따라 VLA의 수명은 선언이 스코프를 벗어날 때 끝나요). 각 VLA 인스턴스의 크기는 수명 동안 변하지 않지만, 같은 코드를 다시 지나가면 다른 크기로 할당될 수 있어요.
#include <stdio.h>
int main(void)
{
int n = 1;
label:;
int a[n]; // re-allocated 10 times, each with a different size
printf("The array has %zu elements\n", sizeof a / sizeof *a);
if (n++ < 10)
goto label; // leaving the scope of a VLA ends its lifetime
}
크기가 *라면 그 선언은 크기를 지정하지 않는 VLA용이에요. 그런 선언은 함수 프로토타입 스코프에서만 나타날 수 있고, 완전 타입의 배열을 선언해요. 사실 함수 프로토타입 스코프의 모든 VLA 선언자는 expression이 *로 바뀐 것처럼 취급돼요.
void foo(size_t x, int a[*]);
void foo(size_t x, int a[x])
{
printf("%zu\n", sizeof a); // same as sizeof(int*)
}
가변 길이 배열과 거기서 파생된 타입(가리키는 포인터 등)은 흔히 "변형-수정 타입"(VM, variably-modified types)이라고 불러요. 변형-수정 타입의 어떤 객체든 블록 스코프나 함수 프로토타입 스코프에서만 선언할 수 있어요.
VLA는 자동 또는 할당된 저장 기간을 가져야 해요. VLA 자체는 아니고 VLA에 대한 포인터는 정적 저장 기간을 가질 수도 있어요. 어떤 VM 타입도 링키지를 가질 수 없어요.
변형-수정 타입은 struct나 union의 멤버가 될 수 없어요. (since C99)
extern int n;
int A[n]; // Error: file scope VLA
extern int (*p2)[n]; // Error: file scope VM
int B[100]; // OK: file-scope array of constant known size
void fvla(int m, int C[m][m]); // OK: prototype-scope VLA
void fvla(int m, int C[m][m]) // OK: block scope/auto duration pointer to VLA
{
typedef int VLA[m][m]; // OK: block scope VLA
int D[m]; // OK: block scope/auto duration VLA
// static int E[m]; // Error: static duration VLA
// extern int F[m]; // Error: VLA with linkage
int (*s)[m]; // OK: block scope/auto duration VM
s = malloc(m * sizeof(int)); // OK: s points to VLA in allocated storage
// extern int (*r)[m]; // Error: VM with linkage
static int (*q)[m] = &B; // OK: block scope/static duration VM
}
struct tag
{
int z[n]; // Error: VLA struct member
int (*y)[n]; // Error: VM struct member
};
컴파일러가 매크로 상수 __STDC_NO_VLA__를 정수 상수 1로 정의하면, VLA와 VM 타입은 지원되지 않아요. (since C11)(until C23)
컴파일러가 매크로 상수 __STDC_NO_VLA__를 정수 상수 1로 정의하면, 자동 저장 기간의 VLA 객체는 지원되지 않아요. VM 타입과 할당된 저장 기간의 VLA에 대한 지원은 필수예요. (since C23)
크기를 알 수 없는 배열
배열 선언자의 expression이 생략되면, 크기를 알 수 없는 배열을 선언해요. 함수 매개변수 목록(그런 배열은 포인터로 변환되는 곳)과 초기화자가 있는 경우를 제외하면, 그런 타입은 불완전 타입(incomplete type)이에요 (참고로 크기를 *로 선언한 크기 미지정 VLA는 완전 타입임)(since C99).
extern int x[]; // the type of x is "array of unknown bound of int"
int a[] = {1,2,3}; // the type of a is "array of 3 int"
struct 정의 안에서 크기를 알 수 없는 배열이 마지막 멤버로(이름 있는 다른 멤버가 적어도 하나 있을 때) 나타날 수 있는데, 그 경우 유연 배열 멤버(flexible array member)라고 불리는 특별한 경우예요. 자세한 내용은 struct 문서를 참고해요. (since C99)
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]
한정자
배열 타입이 const, volatile, restrict(since C99) 한정자로 선언되면(typedef를 통해 가능), 배열 타입 자체는 한정되지 않고 요소 타입이 한정돼요. (until C23)
배열 타입과 그 요소 타입은 항상 동일하게 한정된 것으로 간주되는데, 단 배열 타입은 _Atomic-한정으로 간주되지 않아요. (since C23)
typedef int A[2][3];
const A a = {{4, 5, 6}, {7, 8, 9}}; // array of array of const int
int* pi = a[0]; // Error: a[0] has type const int*
void* unqual_ptr = a; // OK until C23; error since C23
// Notes: clang applies the rule in C++/C23 even in C89-C17 modes
_Atomic은 배열 타입에 적용될 수 없지만, 원자 타입의 배열은 허용돼요. (since C11)
typedef int A[2];
// _Atomic A a0 = {0}; // Error
// _Atomic(A) a1 = {0}; // Error
_Atomic int a2[2] = {0}; // OK
_Atomic(int) a3[2] = {0}; // OK
대입 (Assignment)
배열 타입의 객체는 수정 가능한 lvalue(modifiable lvalue)가 아니에요. 주소를 취할 수는 있지만 대입 연산자의 왼쪽에는 나타날 수 없어요. 하지만 배열 멤버를 가진 struct는 수정 가능한 lvalue이고 대입할 수 있어요.
int a[3] = {1,2,3}, b[3] = {4,5,6};
int (*p)[3] = &a; // okay, address of a can be taken
// a = b; // error, a is an array
struct { int c[3]; } s1, s2 = {3,4,5};
s1 = s2; // okay: can assign structs holding array members
배열-포인터 변환 (Array to pointer conversion)
배열 타입의 어떤 lvalue 표현식이든, 다음 상황이 아닌 맥락에서 쓰이면 첫 요소를 가리키는 포인터로 암시적 변환이 일어나요. 그 결과는 lvalue가 아니에요.
- 주소 연산자(address-of)의 피연산자
sizeof의 피연산자typeof와typeof_unqual의 피연산자 (since C23)- 배열 초기화에 쓰이는 문자열 리터럴
_Alignof(since C11)(until C23)alignof(since C23)의 피연산자 (since C11)
배열이 register로 선언됐다면, 그런 변환을 시도하는 프로그램의 동작은 정의되지 않아요.
int a[3] = {1,2,3};
int* p = a;
printf("%zu\n", sizeof a); // prints size of array
printf("%zu\n", sizeof p); // prints size of a pointer
배열 타입이 함수 매개변수 목록에 쓰이면, 그에 대응하는 포인터 타입으로 변환돼요. int f(int a[2])와 int f(int* a)는 같은 함수를 선언해요. 함수의 실제 매개변수 타입이 포인터 타입이므로, 배열 인자로 함수를 호출하면 배열-포인터 변환이 일어나요. 인자 배열의 크기는 호출된 함수에서 알 수 없으므로 명시적으로 전달해야 해요.
#include <stdio.h>
void f(int a[], int sz) // actually declares void f(int* a, int sz)
{
for (int i = 0; i < sz; ++i)
printf("%d\n", a[i]);
}
void g(int (*a)[10]) // pointer to array parameter is not transformed
{
for (int i = 0; i < 10; ++i)
printf("%d\n", (*a)[i]);
}
int main(void)
{
int a[10] = {0};
f(a, 10); // converts a to int*, passes the pointer
g(&a); // passes a pointer to the array (no need to pass the size)
}
다차원 배열
배열의 요소 타입이 또 다른 배열이면 그 배열을 다차원(multidimensional)이라고 불러요.
// array of 2 arrays of 3 ints each
int a[2][3] = {{1,2,3}, // can be viewed as a 2x3 matrix
{4,5,6}}; // with row-major layout
배열-포인터 변환이 적용되면, 다차원 배열은 첫 요소, 즉 첫 행을 가리키는 포인터로 변환된다는 점에 주의하세요.
int a[2][3]; // 2x3 matrix
int (*p1)[3] = a; // pointer to the first 3-element row
int b[3][3][3]; // 3x3x3 cube
int (*p2)[3][3] = b; // pointer to the first 3x3 plane
VLA가 지원된다면 다차원 배열은 모든 차원에서 변형-수정될 수 있어요 (since C99).
int n = 10;
int a[n][2*n];
주의
길이 0짜리 배열 선언은 허용되지 않아요. 일부 컴파일러가 확장으로 제공하기는 하지만요 (보통 유연 배열 멤버의 C99 이전 구현으로).
VLA의 크기 표현식에 부작용이 있다면, 그 부작용이 sizeof 표현식의 일부이고 그 결과가 크기에 의존하지 않는 경우를 제외하고 항상 일어난다고 보장돼요.
int n = 5, m = 5;
size_t sz = sizeof(int (*[n++])[m++]); // n is incremented, m may or may not be incremented
더 알아보기
- 배열을 어떤 값으로 채울지 정하는 규칙은 배열 초기화 문서에서 볼 수 있어요.
- 배열 요소의 타입이 되는 다양한 산술 타입은 산술 타입 문서에서 다뤄요.
- cppreference의 배열 선언 원문을 참고할 수 있어요.