[C/C++] 18. 배열

앞서 재귀 함수의 장단점과 팩토리얼 및 피보나치 수열 구현을 알아보았다. 이번 글에서는 동일한 자료형의 데이터를 묶어 관리하는 배열(Array)의 선언과 메모리 연속성, 0부터 시작하는 인덱스 규칙, 그리고 인덱스 범위 초과(Out of Bounds) 시 발생하는 메모리 오염 위험을 정리한다. 동일한 자료형의 변수가 여러 개 필요할 때, 개별 변수를 매번 선언()하는 비효율을 줄이기 위해 사용하는 문법이 배열이다. - 선언 문법: - 을 선언하면 4바이트 정수형 공간 10개(총 40바이트)가 연속으로 할당된다. - 0 초기화: 와 같이 중괄호를 비워두면 배열의 모든 요소를 으로 초기화할 수 있다. 배열의 개별 요소는 대괄호 안에 들어가는 인덱스(Index)를 통해 접근한다. - 배열의 인덱스는 항상 0부터 시작한다. - 크기가 $N$인 배열의 유효 인덱스 범위는 $0$부터 $N-1$까지다. - 크기가 10인 배열 의 첫 번째 요소는 - 마지막(10번째) 요소는 - 는 네 번째가 아닌 다섯 번째 요소를 의미한다. 배열의 가장 큰 물리적 특징은 메모리 공간에 빈틈없이 연속적으로 배치된다는 점이다. !array|700 - 인덱스 범위 초과: 크기가 10인 배열에서 이나 에 접근하는 것은, 배열에 할당된 40바이트 경계를 넘어 다른 변수가 사용 중인 메모리 영역을 강제로 침범하는 행위다. - 메모리 오염(Data Corruption): C/C++ 컴파일러는 실행 속도를 위해 배열 접근 시 경계 검사를 강제하지 않는 경우가 많다. 따라서 잘못된 인덱스 접근이 즉시 에러를 내지 않고, 인접한 다른 변수의 값을 멋대로 덮어씌워(오염시켜) 원인을 찾기 극도로 어려운 버그를 유발한다. - 빌드 모드별 차이: 디버그(Debug) 모드에서는 런타임 에러로 감지될 수 있지만, 릴리즈(Release) 모드에서는 오류 없이 실행되다가 프로그램이 돌연 비정상 종료(Crash)될 수 있어 각별한 주의가 필요하다. 배열은 내부적으로 시작 메모리 주소를 기반으로 오프셋(거리)을 계산하여 각 요소에 접근하는 방식으로 동작한다. 이러한 메모리 참조 방식은 C/C++의 핵심이자 난관인 포인터(Pointer) 개념과 직접적으로 연결된다. 배열의 연속 메모리 구조를 명확히 이해해 두어야 추후 포인터 연산과 주소 참조 문법을 흔들림 없이 학습할 수 있다. - 배열은 동일한 타입의 변수들을 메모리에 연속적으로 할당하여 관리하는 문법이다. - 인덱스는 0부터 시작하므로 크기 $N$인 배열의 마지막 인덱스는 $N-1$이다. - 인덱스 범위를 벗어난 접근은 인접 메모리를 오염시켜 치명적인 런타임 버그를 유발하므로 인덱스 계산 범위를 엄격하게 통제해야 한다.

2026년 8월 31일

[C/C++] 17. 재귀 함수(2)

앞서 재귀 함수의 동작 원리와 호출 스택(Call Stack), 그리고 필수적인 탈출 조건에 대해 알아보았다. 이번 글에서는 재귀 함수의 대표적인 활용 예시인 팩토리얼(Factorial)과 피보나치 수열(Fibonacci Sequence)을 반복문과 재귀 함수 두 가지 방식으로 직접 구현해보고, 각 방식의 구현 직관성과 성능 차이를 정리한다. 팩토리얼($n!$)의 수학적 정의인 $n! = n \times (n-1)!$을 코드로 그대로 옮기면 매우 직관적인 재귀 함수가 완성된다. - 동작 원리: 호출 시, 스택에는 , , 순으로 스택 프레임이 쌓인다. 마지막 이 탈출 조건을 만나 을 반환하면, 스택이 역으로 터지며(, , ) 최종 결과 가 계산된다. 피보나치 수열은 첫 두 숫자가 로 시작하며, 세 번째 숫자부터는 바로 앞 두 숫자의 합으로 구성되는 수열이다. (예: ) 반복문을 사용하여 $n$번째 피보나치 수를 구하려면, 이전 두 값을 저장할 변수(, )를 두고 루프를 돌며 이 값들을 계속해서 갱신해주어야 한다. - 특징: 변수 값을 갱신하는 논리가 직관적이지 않을 수 있어 구현 시 종이에 변수 값의 변화를 시각화해보는 것이 좋다. 하지만 연산 속도는 $O(n)$으로 매우 빠르다. 피보나치 수열의 정의인 $F(n) = F(n-1) + F(n-2)$ (단, $F(1)=F(2)=1$)을 코드로 옮기면 코드가 매우 간결하고 가독성이 뛰어나다. - 특징: 구현의 가독성은 극대화되지만, 함수가 두 갈래로 나뉘어 폭발적으로(기하급수적으로) 재귀 호출을 일으킨다. $n$이 커질수록 호출 횟수가 수십억 번을 넘어가 성능이 심각하게 저하된다. - 재귀 함수의 강력함: 팩토리얼이나 피보나치 수열처럼 수학적 정의 자체가 재귀적이거나, 트리(Tree)와 같은 계층적 자료구조를 다룰 때 재귀 함수는 코드를 매우 간결하고 직관적으로 만든다. - 성능적 한계: 피보나치 수열의 재귀 구현처럼 중복된 계산이 잦은 경우 함수 호출 오버헤드와 스택 증가로 인해 성능 이슈와 스택 오버플로우 위험이 크다. - 신중한 선택: 재귀는 편리한 도구이지만 모든 문제에 만능은 아니다. 가독성이 중요한 계층 구조에는 재귀를, 성능이 중요한 단순 반복 계산에는 반복문을 사용하는 등 상황에 맞춰 신중하게 선택해야 한다.

2026년 8월 31일

[C/C++] 16. 재귀 함수(1)

앞서 스택 메모리의 동작 방식과 함수 모듈화의 장점을 알아보았다. 이번 글에서는 디버깅 도구인 호출 스택(Call Stack)의 활용법과 함께, 함수가 자기 자신을 다시 호출하는 재귀 함수(Recursive Function)의 동작 원리 및 장단점을 정리한다. 복잡한 프로그램에서 문제(버그)를 추적할 때 눈으로만 코드를 읽는 것은 한계가 있다. Visual Studio 같은 IDE의 디버깅 도구 중 호출 스택(Call Stack)과 로컬(Locals) 창은 메모리의 현재 상태를 파악하는 강력한 무기다. - 호출 스택: 현재 메모리상에 어떤 함수들이 스택 구조(LIFO)로 쌓여 실행 중인지 실시간으로 보여준다. - 로컬 창: 호출 스택에서 특정 함수를 클릭하면, 해당 함수 스택에 할당된 지역 변수(Local Variable)들의 현재 값을 확인할 수 있다. - 에서 을 호출하면 호출 스택에 두 함수가 층층이 쌓여 있는 것을 볼 수 있으며, 동일한 이름의 변수라도 각 함수 스택마다 완전히 독립된 지역 변수로 관리됨을 눈으로 확인할 수 있다. 재귀 함수란 함수 내부에서 자기 자신을 다시 호출하는 함수를 말한다. - 일반적인 함수 호출이 다른 함수를 스택에 쌓는 것이라면, 재귀 호출은 완전히 동일한 복사본(새로운 스택 메모리)을 기존 스택 위에 계속해서 쌓아 올리는 것과 같다. - 따라서 안에서 을 호출하면, 서로 다른 스택 프레임을 가진 두 개의 함수가 메모리에 동시에 존재하게 된다. 재귀 함수를 설계할 때 가장 중요한 것은 탈출 조건(Base Case)을 반드시 명시해야 한다는 점이다. - 스택 오버플로우(Stack Overflow): 만약 탈출 조건이 없거나 잘못 설정되면 함수가 무한히 자신을 호출하게 된다. 결국 운영체제가 프로그램에 할당해 준 스택 메모리의 최대 한계치를 초과하게 되어, 프로그램이 비정상 종료(Crash)되는 치명적인 에러가 발생한다. - 탈출 조건을 만나면 가장 위쪽에 쌓인 함수부터 차례대로 을 수행하며 스택에서 제거(Pop)되고, 반환 값은 CPU 레지스터를 거쳐 이전 호출자에게 전달된다. 재귀 함수는 무조건 좋은 것도, 나쁜 것도 아니며 상황에 맞게 적절히 사용해야 한다. - 장점 (가독성과 직관성): 수학적 점화식이나 트리(Tree), 디렉토리 구조 같은 계층적 자료구조를 다룰 때 코드가 매우 간결해지고 직관적으로 변한다. 동일한 논리를 반복문으로 구현하려면 별도의 스택(Stack)을 만들어야 해서 코드가 복잡해지는 경우가 많다. - 단점 (성능 저하와 오버헤드): 함수가 호출되고 반환될 때마다 스택 프레임을 생성하고 매개변수를 복사하는 컨텍스트 스위칭 비용(Overhead)이 발생한다. 단순 반복문(for/while)에 비해 실행 속도가 느려질 수 있다. - 호출 스택(Call Stack)을 활용하면 현재 실행 중인 함수들의 흐름과 독립적인 지역 변수 상태를 시각적으로 추적할 수 있다. - 재귀 함수는 자기 자신을 호출하여 새로운 스택을 쌓는 구조이므로, 무한 루프를 막기 위한 탈출 조건이 필수적이다. - 재귀는 계층 구조 표현에 탁월하여 가독성을 높이지만, 잦은 함수 호출로 인한 성능 저하와 스택 오버플로우 위험을 동반한다.

2026년 8월 31일

[C/C++] 15. 함수(3)

앞서 함수 호출 원리와 스택 메모리의 동작 방식을 알아보았다. 이번 글에서는 반복문을 활용한 팩토리얼(Factorial) 알고리즘 구현을 통해 로직을 직접 짜보고, 중복 코드를 제거하기 위한 함수 모듈화(Modularization)의 필요성과 소프트웨어 설계 관점에서의 이점을 정리한다. 팩토리얼($n!$)은 $1$부터 특정 숫자 $n$까지의 모든 자연수를 곱하는 연산이다. (예: $4! = 1 \times 2 \times 3 \times 4 = 24$) 이를 코드로 구현할 때 핵심은 초기값 설정과 반복문의 유연성이다. - 초기값: 곱셈 연산의 누적 결과를 담는 변수는 반드시 로 초기화해야 한다. (으로 초기화하면 어떤 수를 곱해도 결과가 이 됨) - 반복문 활용: 반복 횟수(루프 인덱스)를 조작하여 매 단계마다 곱해질 숫자를 유연하게 생성한다. 만약 프로그램의 여러 위치에서 $4!$, $7!$, $10!$ 등 서로 다른 팩토리얼 계산이 반복적으로 필요하다면 어떻게 해야 할까? - 단순 복사-붙여넣기(Copy-Paste)의 문제점: - 동일한 반복문 코드가 여러 곳에 산재하여 전체 코드 길이가 불필요하게 늘어난다. - 계산 로직에 버그가 발생하거나 수정이 필요할 때 모든 복사본을 일일이 찾아 고쳐야 하므로 유지보수가 매우 어려워진다. - 협업 시 가독성을 심각하게 저하시킨다. 이러한 비효율을 해결하기 위해 독립적인 작업 단위를 별도의 함수로 분리(모듈화)해야 한다. 팩토리얼 계산 로직을 이라는 단일 함수로 캡슐화하면, 입력값(매개변수)에 따라 재사용 가능한 유연한 코드를 만들 수 있다. 프로그래밍에서 복잡한 시스템을 구축할 때 가장 중요한 것은 작은 단위부터 차근차근 쌓아 올리는 방식이다. - 가독성과 유지보수성 향상: 메인 로직 흐름이 단순해지며, 팩토리얼 계산 방식이 바뀌더라도 함수 내부만 수정하면 호출부 전체에 즉시 반영된다. - 소프트웨어 모듈화: 기능을 최소 단위의 함수로 나누어 두면, 이후 더 복잡한 알고리즘(예: 순열, 조합 등)을 작성할 때 이미 검증된 작은 함수들을 레고 블록처럼 조합하여 쉽게 구현할 수 있다. - 곱셈 누적 로직 구현 시 변수의 초기값은 반드시 로 설정해야 한다. - 반복되는 로직을 함수로 모듈화하면 코드의 재사용성을 극대화하고 유지보수 비용을 획기적으로 낮출 수 있다. - 견고한 프로그램을 만들기 위해서는 작은 단위의 기능을 수행하는 함수를 먼저 설계하고 조합해 나가는 개발 습관이 중요하다.

2026년 8월 31일

[C/C++] 14. 함수(2)

앞서 와 를 통한 표준 입출력 및 메모리 주소 전달 방식을 알아보았다. 이번 글에서는 함수가 호출되고 반환되는 전체적인 실행 흐름, 후입선출(LIFO) 원리를 따르는 스택(Stack) 자료구조, 그리고 함수 실행 시 스택 메모리 영역의 할당과 해제 원리를 정리한다. C/C++ 프로그램은 가장 먼저 함수에서 실행을 시작하며, 함수가 종료될 때 프로그램 전체가 종료된다. - 함수의 중첩 호출: 함수 내부에서 다른 함수를 호출하고, 그 함수가 또 다른 함수를 연쇄적으로 호출할 수 있다. - 제어 흐름의 전환: 의 단계를 거친다. 즉, 호출된 하위 함수가 완전히 종료되어야만 이를 호출했던 상위 함수가 다음 코드를 이어갈 수 있다. 함수의 호출 및 종료 메커니즘은 자료구조의 스택(Stack) 원리와 정확히 일치한다. !stack - 스택(Stack, LIFO / FILO): 접시를 차곡차곡 쌓아 올리는 것처럼, 가장 나중에 들어간 데이터가 가장 먼저 나오는 후입선출(Last In First Out) 구조다. - 큐(Queue, FIFO): 줄을 서는 것처럼 먼저 들어간 데이터가 먼저 나가는 선입선출(First In First Out) 구조로, 스택과 대비된다. - 함수 호출의 적용: 가장 먼저 실행된 함수는 스택의 가장 밑에 쌓이고, 그 위로 호출되는 함수들이 차례로 쌓인다. 나중에 호출된 함수가 작업을 마치고 스택에서 빠져나가야 비로소 이전 함수로 제어권이 돌아온다. 함수가 실행될 때 사용하는 메모리 공간을 스택 영역이라고 부른다. 1. 호출 시 메모리 할당: 함수가 호출되면 컴파일 시점에 계산된 크기만큼 스택 프레임(Stack Frame)이 할당되며, 매개변수와 내부 지역 변수들이 이 공간에 생성된다. 2. 종료 시 메모리 해제: 함수가 을 만나 종료되는 즉시, 해당 함수가 점유하던 스택 메모리는 통째로 반환(해제)된다. 3. 변수 스코프의 독립성: 서로 다른 함수에서 같은 이름의 변수()를 선언해도 충돌하지 않는 이유는, 각 함수마다 독립된 스택 메모리 영역에 변수가 존재하기 때문이다. 프로그래밍을 학습할 때 흔히 작성한 '코드' 자체를 메모리 공간과 혼동하기 쉽다. 하지만 둘은 명확히 구분된다. - 코드(Code): CPU가 어떤 작업을 수행해야 하는지 적어둔 명령어의 집합(설계도/작업 지시서)이다. - 메모리(Memory): 명령어가 실행되면서 실제로 변수가 만들어지고 데이터가 기록되는 실제 작업 공간(토지/공간)이다. 함수가 종료된다고 해서 작성된 '코드'가 사라지는 것이 아니라, 코드가 실행되면서 스택 영역에 잡혔던 '메모리'가 해제되는 것이다. - 함수 호출과 반환은 가장 나중에 호출된 함수가 먼저 종료되는 스택(후입선출, LIFO) 방식을 따른다. - 함수 내부의 지역 변수와 매개변수는 함수 호출 시 스택 메모리에 할당되고, 함수 종료 시 즉시 해제된다. - 코드는 실행할 명령어 집합이며, 실제 데이터가 생성되고 소멸하는 메모리 공간과 분리해서 이해해야 한다.

2026년 8월 31일

Gyulog