이 문서는 C 언어에서 배열과 포인터의 기본 원리, 상호 관계, 그리고 실제 응용을 다룹니다. 이론 설명과 함께 동작 원리를 검증하는 코드 예제를 포함하며, 메모리 주소 기반 접근 방식의 중요성을 강조합니다.
배열의 본질과 활용
배열은 동일한 자료형의 요소들을 연속된 메모리 공간에 저장하는 구조입니다. 배열 이름은 그 시작 주소를 의미하며, 컴파일 타임에 크기가 고정됩니다.
단일 순회로 두 번째 최대값 찾기
중복 없는 정수 배열에서 최댓값 다음으로 큰 값을 한 번의 반복만으로 식별하는 알고리즘입니다. 초기값 설정과 조건 분기로 최적화됩니다.
#include <stdio.h>
int findSecondLargest(const int arr[], int size) {
if (size < 2) return -1; // 유효하지 않은 입력
int first = arr[0], second = arr[0];
for (int i = 1; i < size; ++i) {
if (arr[i] > first) {
second = first;
first = arr[i];
} else if (arr[i] > second && arr[i] != first) {
second = arr[i];
}
}
return (first == second) ? -1 : second;
}
int main() {
const int data[] = {102, 233, 435, 253, 12399, 45, 2353, 65, 434, 8877};
const int len = sizeof(data) / sizeof(data[0]);
int result = findSecondLargest(data, len);
printf("두 번째로 큰 값: %d\n", result);
return 0;
}
동적 크기 배열 입출력 함수
배열 크기를 매개변수로 받아 일반화된 I/O 루틴을 구현합니다. 함수 내부에서는 인덱스 기반 접근을 사용해 안전하게 데이터를 처리합니다.
#include <stdio.h>
void readArray(int *arr, int count) {
for (int idx = 0; idx < count; ++idx) {
scanf("%d", &arr[idx]);
}
}
void printArray(const int *arr, int count) {
for (int idx = 0; idx < count; ++idx) {
printf("요소[%d]: %d\n", idx, arr[idx]);
}
}
int main() {
int input[5], output[5] = {10, 20, 30, 40, 50};
printf("5개 정수 입력:\n");
readArray(input, 5);
printf("\n입력된 배열:\n");
printArray(input, 5);
printf("\n초기화된 배열:\n");
printArray(output, 5);
return 0;
}
포인터의 작동 원리
포인터는 메모리 주소를 저장하는 변수이며, 간접 참조를 통해 대상 값을 조작할 수 있습니다. 주소 연산자(&)와 역참조 연산자(*)가 핵심입니다.
세 가지 교환 전략 비교
값 전달, 주소 전달, 포인터 재할당을 통한 변수 교환 방식을 각각 구현하고 차이점을 분석합니다.
#include <stdio.h>
// 방법 1: 값 복사 → 원본 변경 불가
void swapByValue(int x, int y) {
int temp = x;
x = y;
y = temp;
printf("[값 복사] 함수 내: %d, %d\n", x, y);
}
// 방법 2: 주소 전달 → 원본 직접 수정
void swapByAddress(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
printf("[주소 전달] 함수 내: %d, %d\n", *x, *y);
}
// 방법 3: 포인터 변수 자체 재할당 → 원본 무관
void swapPointers(int **x, int **y) {
int *temp = *x;
*x = *y;
*y = temp;
printf("[포인터 교환] 함수 내: %d, %d\n", **x, **y);
}
int main() {
int a = 100, b = 200;
int *ptrA = &a, *ptrB = &b;
printf("교환 전: a=%d, b=%d\n", a, b);
swapByValue(a, b);
printf("값 복사 후: a=%d, b=%d\n", a, b);
swapByAddress(&a, &b);
printf("주소 전달 후: a=%d, b=%d\n", a, b);
swapPointers(&ptrA, &ptrB);
printf("포인터 교환 후: *ptrA=%d, *ptrB=%d\n", *ptrA, *ptrB);
return 0;
}
배열과 포인터의 메모리 관계
배열 이름은 첫 번째 요소의 주소를 가리키는 상수 포인터입니다. 따라서 a[i]는 *(a + i)와 완전히 동치이며, 포인터 산술 연산은 요소 크기에 따라 자동 조정됩니다.
문자열 처리의 주의점
문자열 리터럴은 읽기 전용 영역(.rodata)에 저장되므로, 이를 가리키는 포인터로 수정 시도하면 세그먼테이션 폴트가 발생합니다. 가변 문자열은 반드시 배열 형태로 선언해야 합니다.
#include <stdio.h>
int main() {
// 읽기 전용 문자열 → 수정 불가
const char *literal = "Hello";
// *(literal + 0) = 'h'; // 컴파일 경고 또는 런타임 오류
// 쓰기 가능한 문자열 배열
char mutable[] = "World";
mutable[0] = 'w'; // 허용됨
printf("수정된 문자열: %s\n", mutable);
// 배열 이름과 포인터 산술의 동등성 확인
int numbers[] = {10, 20, 30, 40};
printf("numbers[2] == *(numbers + 2): %s\n",
(numbers[2] == *(numbers + 2)) ? "참" : "거짓");
return 0;
}
실전 문제 해결 패턴
다음은 배열과 포인터 개념을 적용한 전형적인 알고리즘 문제들입니다.
최댓값 및 합계 계산 함수
일반화된 배열 처리 함수로, 포인터 기반 접근을 통해 유연성을 확보합니다.
#include <stdio.h>
#include <limits.h>
int findMax(const int *arr, int size) {
if (size <= 0) return INT_MIN;
int maxVal = *arr;
for (int i = 1; i < size; ++i) {
if (*(arr + i) > maxVal) maxVal = *(arr + i);
}
return maxVal;
}
int calculateSum(const int *arr, int size) {
int total = 0;
for (int i = 0; i < size; ++i) {
total += *(arr + i);
}
return total;
}
int main() {
int dataset[] = {15, 8, 22, 3, 47, 11};
const int length = sizeof(dataset) / sizeof(dataset[0]);
printf("최댓값: %d\n", findMax(dataset, length));
printf("합계: %d\n", calculateSum(dataset, length));
return 0;
}