학생 10명의 점수를 다루려고 변수 10개를 따로 선언하면, 합계를 구할 때 10번 더해야 하고 인원이 바뀌면 코드를 전부 고쳐야 한다. 배열은 같은 종류의 데이터 여러 개를 하나의 이름으로 묶어, 이런 번거로움을 없애준다.
1. 배열이란?
같은 자료형의 데이터 여러 개를 하나의 변수 이름으로 묶어 관리하는 자료구조다. 각 데이터는 0부터 시작하는 인덱스(index)로 구별하며, 크기는 고정되고 메모리의 연속된 공간에 저장된다.

int scores[10]; // int형 10개짜리 배열
scores[0]부터 scores[9]까지 10개의 요소가 만들어진다. 인덱스가 0부터 시작하므로, 크기가 10인 배열의 마지막 인덱스는 9다. 이걸 놓쳐 scores[10]에 접근하면 배열 범위를 벗어나는 위험한 코드가 된다. 처음엔 scores[10]이 열 번째 요소인 줄 알고 접근했다가, 인덱스가 0부터라는 걸 다시 새겼다.
초기화

int scores[5] = {10, 20, 30, 40, 50};
int scores[5] = {10, 20, 30}; // 나머지 scores[3], scores[4]는 0
초기값을 중괄호로 묶어 대입한다. 초기값이 요소 수보다 적으면 앞쪽만 채우고 나머지는 모두 0으로 초기화된다. 이를 이용해 int a[100] = {0};으로 배열 전체를 0으로 초기화하는 관용구도 자주 쓴다.
a = b; 로 배열을 통째로 복사하거나 if (a == b) 로 내용을 비교할 수 없다. 배열 이름은 사실 "첫 요소의 주소"라서, == 는 내용이 아니라 주소를 비교해버린다. 복사·비교는 반드시 반복문으로 원소를 하나씩 처리해야 한다.
for (int i = 0; i < SIZE; i++)
b[i] = a[i]; // 원소를 일일이 복사
배열을 a = b로 복사하려다 안 돼서 의아했는데, "배열 이름이 곧 첫 요소의 주소"라는 한 줄로 모든 게 설명됐다. 이 사실이 뒤의 포인터로 이어진다는 예감이 들었다.
2. 배열과 함수
배열을 함수에 넘길 때는 일반 변수와 동작이 다르다. 이 차이가 배열의 가장 중요한 성질이다.
- 일반 변수: 값을 복사해 전달한다. 함수 안에서 바꿔도 원본은 그대로다.
- 배열: 첫 요소의 주소가 전달된다. 함수 안에서 원소를 바꾸면 원본도 함께 바뀐다.
// 일반 변수: 값 복사 → 원본 안 바뀜 (swap 실패)
void swapFail(int a, int b) {
int t = a; a = b; b = t;
}
// 배열: 주소 전달 → 원본이 바뀜 (swap 성공)
void swapOk(int arr[]) {
int t = arr[0]; arr[0] = arr[1]; arr[1] = t;
}
배열 이름이 곧 주소이기 때문에 생기는 현상이다. 큰 배열을 통째로 복사하지 않고 주소만 넘기니 효율적이지만, "함수가 원본을 건드릴 수 있다"는 점은 늘 의식해야 한다. 일반 변수 swap은 실패하고 배열 swap은 성공하는 걸 나란히 보니, 값 전달과 주소 전달의 차이가 확실히 박혔다.
3. 2차원 배열
행과 열로 이뤄진 표 형태의 데이터는 2차원 배열로 다룬다.
![[ch07_2차원배열정의.png]]
int s[3][5]; // 3행 5열
초기화는 행 단위로 중괄호를 중첩한다.
int s[3][5] = {
{ 0, 1, 2, 3, 4}, // 0행
{10, 11, 12, 13, 14}, // 1행
{20, 21, 22, 23, 24} // 2행
};
2차원 배열을 초기화할 때 행 개수는 생략할 수 있다(int s[][5] = {...}). 열 크기는 반드시 적어야 컴파일러가 한 행의 길이를 알 수 있기 때문이다.
성적표, 좌석 배치, 게임 보드처럼 격자 형태의 데이터를 다룰 때 2차원 배열과 이중 for문이 짝을 이룬다.
4. 수업 코드로 보는 최솟값·최댓값 찾기
arr_min_max_prg.c는 배열에 들어 있는 정수 중 최솟값과 최댓값을 찾고, 정렬까지 연결한 예제였다. 핵심은 첫 번째 원소를 후보로 잡고, 나머지를 차례대로 보며 후보를 갱신하는 것이다.
#define SIZE 10
int iArray[SIZE] = {4, 6, 3, 8, 2, 5, 3, 7, 9, 1};
int iIndex;
int iMin;
int iMax;
iMin = iArray[0];
iMax = iArray[0];
for (iIndex = 0; iIndex < SIZE; iIndex++)
{
if (iArray[iIndex] < iMin)
{
iMin = iArray[iIndex];
}
if (iArray[iIndex] > iMax)
{
iMax = iArray[iIndex];
}
}
printf("최솟값: %d\n", iMin);
printf("최댓값: %d\n", iMax);
| iIndex | 검사값 | iMin 변화 | iMax 변화 |
|---|---|---|---|
| 0 | 4 | 4 | 4 |
| 1 | 6 | 유지 | 6 |
| 2 | 3 | 3 | 유지 |
| 3 | 8 | 유지 | 8 |
| 4 | 2 | 2 | 유지 |
| 8 | 9 | 유지 | 9 |
| 9 | 1 | 1 | 유지 |
선택 정렬로 두 번째 최솟값 찾기
수업 코드에서는 배열을 정렬한 뒤 iArray[1]을 두 번째 최솟값으로 출력했다. 정렬 흐름은 "현재 위치부터 끝까지 최솟값 위치를 찾고, 현재 위치와 교환"이다.
int iLoop;
int iSearchIndex;
int iMinIndex;
int iTemp;
for (iLoop = 0; iLoop < SIZE; iLoop++)
{
iMinIndex = iLoop;
for (iSearchIndex = iLoop; iSearchIndex < SIZE; iSearchIndex++)
{
if (iArray[iSearchIndex] < iArray[iMinIndex])
{
iMinIndex = iSearchIndex;
}
}
iTemp = iArray[iLoop];
iArray[iLoop] = iArray[iMinIndex];
iArray[iMinIndex] = iTemp;
}
printf("두 번째 최솟값: %d\n", iArray[1]);
정렬 전 배열이 {4, 6, 3, 8, 2, 5, 3, 7, 9, 1}이면
정렬 후는 {1, 2, 3, 3, 4, 5, 6, 7, 8, 9}가 되고,
두 번째 최솟값은 2다.
오늘 느낀 점
- 배열의 거의 모든 성질(통째 복사 불가, 함수에서 원본 변경, arr[i]의 동작)이 "배열 이름 = 첫 요소의 주소"라는 한 사실에서 나온다는 게 핵심이었다.
- 인덱스 0부터,
a=b불가, 배열 swap 성공처럼 직접 부딪혀 본 것들이 오래 남았다. 특히 swap을 값/주소로 나란히 비교한 게 도움이 컸다. - 배열이 포인터와 깊이 맞닿아 있다는 예감이 강하게 들었다. 여기를 잘 잡아두면 다음 포인터가 수월할 것 같다.
한 걸음 더
- C는 배열 인덱스가 범위를 벗어나도 검사해주지 않는다.
int a[5]에서a[10]에 값을 써도 컴파일·실행이 되지만, 엉뚱한 메모리를 건드려 다른 변수를 망가뜨리거나 프로그램이 죽는다. 이 "경계 검사 없음"이 C의 빠른 속도의 이유이자 보안 취약점(버퍼 오버플로)의 원천이기도 하다. - 배열 이름이 주소라는 사실은 다음 글에서 배울 포인터와 직결된다.
arr[i]는 사실 내부적으로*(arr + i)로 해석되는데, 배열과 포인터가 깊이 맞닿아 있음을 보여준다. 배열을 잘 이해해두면 포인터가 한결 쉬워진다. - 배열 크기는 컴파일 시점에 정해지는 고정값이라, 실행 중에 늘리거나 줄일 수 없다. "입력 개수를 모를 때"처럼 크기를 유연하게 다루려면 동적 할당(malloc)이 필요한데, 이건 포인터를 배운 뒤에 다룬다.
'language > c' 카테고리의 다른 글
| [C언어] 06. 함수 (Function) (0) | 2026.08.10 |
|---|---|
| [C언어] 05. 반복문 (0) | 2026.08.03 |
| [C언어] 04. 조건문 (0) | 2026.07.27 |
| [C언어] 03. 수식과 연산자 (0) | 2026.07.20 |
| [C언어] 02. 기본 변수와 입력 (0) | 2026.07.10 |