구조체는 배열과 같이 여러 개의 데이터를 그룹으로 묶어 사용하는 자료형이다. 같은 자료형만을 그룹으로 사용할 수 있는 배열과는 달리, 구조체는 서로 다른 자료형을 그룹으로 묶을 수 있으므로 복잡한 자료 형태를 정의하는 데 유용하다. 이때, 구조체의 선언은 구조체이름, 자료형, 항목으로 구성되며, 항목별로 이름과 자료형을 선언해야 한다는 번거로움이 있다. 이는 배열과는 달리 각기 다른 자료형을 그룹으로 묶을 수 있기에 구별해주어야 하기 때문이다. 구조체 선언의 예시는 다음과 같다:
struct person {
char name[20]; // 글자수에 따른 메모리 할당
int age;
float height;
}; // 이곳에 바로 구조체 변수를 선언할 수 있다.
구조체 선언을 한 후 따로 struct person Lee; 라는 코드를 통해 구조체 변수를 선언 할 수 있으며, 구조체를 선언한 직후 변수의 이름을 기입하여 변수 선언을 하는 것도 가능하다. 구조체 변수 초기화는 중괄호를 통해 이루어진다. struct person Lee = { “Ann”, “22”, “170” }; 처럼 초기화하면 된다. 여기서 구조체와 포인터를 함께 사용할 경우나, 데이터 항목을 참조할 경우 중요하게 사용되는 구조체 연산자를 알아두어야 한다.
먼저, 점 연산자(.)는 데이터 항목을 개별적으로 지정할 때 사용된다. 간단히 '변수 이름.항목 이름'과 같은 형식으로 나타내면 된다. 예시로는 printf 함수에서 printf(“이름: %s\n”, Lee.name); 처럼 쓸 수 있다.
화살표 연산자(->)를 사용할 경우, 구조체의 주소를 저장한 포인터가 필수적이다. 따라서, 연산자를 사용하기 전 무조건 주소를 저장하는 코드를 추가해야만 연산자의 기능을 제대로 쓸 수 있다. 이 또한 예시로는 struct person *ptr = &Lee; printf("이름: %s\n", ptr->name); 로 쓰임을 설명할 수 있겠다. 포인터에 구조체 변수 ‘Lee’의 주소가 저장되므로, 화살표 연산자만을 사용해도 정상 출력되는 것이다.
그렇다면 재귀호출은 무엇일까? 재귀호출은 간단히 말해 자기 자신을 호출하여 순환이 수행되는 방식이라 볼 수 있다. 재귀호출에서 가장 중요한 개념은 베이스 케이스인데, 재귀호출을 실행할 때 종료 시점이 명확하지 않으면 무한으로 반복 호출되어 프로그램이 정상 작동하지 않기 때문이다. 베이스 케이스는 함수가 반복을 중지하는, 말 그대로 기초, 즉 바닥이 되는 케이스라 이해하면 된다. (마트료시카 인형을 떠올리면 보다 쉽게 이해할 수 있다!)
재귀함수는 항상 세 가지의 기본 틀을 이룬다. 함수 선언, 베이스 케이스, 자기 자신 호출이다. 재귀함수의 대표적인 예시는 팩토리얼, 피보나치 수열 등이 있고, 재귀호출이 사용된 예시 코드를 작성한다면 다음과 같다:
int sum(int n) {
if (n == 1) return 1; // 베이스 케이스
return n + sum(n - 1);
}
int main() {
int result = sum(5);
printf("합계: %d\n", result);
return 0;
}
이 예시는 1부터 5까지를 더하는 재귀함수고 앞서 말한 세 가지의 기본 틀을 이루고 있음을 알 수 있다. (혼자 재귀함수 코드를 작성하려면 굉장히 헷갈린다. 공부가 필요할 듯하다.) 재귀를 좀 더 수월하게 받아들이기 위해서 재귀를 반복문처럼 생각하는 것이 도움이 된다.
// for 문으로 1부터 5까지 출력하기
for (int i = 1; i <= 5; i++) {
printf("%d", i);
}
// 재귀 함수를 사용하기
void print_sum(int i) {
if (i > 5) return; //베이스 케이스 5까지 출력, void로 return만 작성
printf("%d", i);
print_sum(i + 1);
}
재귀함수에 익숙해지기 위해 간단한 코딩 테스트 문제를 하나 풀고 가겠다. 재귀를 써서 1부터 n까지의 합을 구하는 함수를 만들어보자.
#include <stdio.h>
int sum(int i, int n) {
if (i > n) return 0;
return i + sum (i + 1, n);
}
int main() {
int n;
printf("정수를 입력하시오.\n");
scanf("%d", &n);
int result = sum (1, n);
printf("합은: %d\n", result);
return 0;
}
재귀 로직을 살펴보면 이렇다. 값을 반환하는 재귀이므로 return을 사용한다. 출력과 같은 동작만 하는 재귀의 경우 return을 사용하지 않아도 된다. return i + sum(i + 1, n); 은 쉽게 말해 선수행 후수행을 분리한 식이라 볼 수 있다. 가장 먼저 i를 두고, 즉 지금 차례를 기록하고 그 후 i + 1부터 나머지를 재귀 호출하여 더하는 것. i는 지금 해야 할 일이고, i + 1은 그 이후에 할 일인 것이다. 여기서 내가 헷갈렸던 점은 이때 i 하나가 중첩되어 더해지는 것인가였는데, 이에 대한 답은 '아니다' 였다. 굳이 처음의 i와 나머지를 떼어놓는 이유는 각 재귀호출이 자기 자신의 차례를 직접 해결하고 다음 문제를 다음 호출에게 넘기기 위해서였다. 단지 호출할 때 스택처럼 쌓일뿐이다. 이를 나누지 않을 경우 재귀의 의미가 없어지고, 오히려 복잡해진다. 앞 함수의 sum에서 sum(1, 3)을한다 가정하면 후술할 구조로 반복된다.
sum(1, 4)
→ 1 + sum(2, 4)
→ 2 + sum(3, 4)
→ 3 + sum(4, 4)
이렇듯 문제를 작게 쪼개어 처리하는 것, 그것이 재귀의 핵심이다. 마지막 연습으로 n!(n 팩토리얼)을 구하는 코드를 작성해보겠다.
#include <stdio.h>
int factorial(int n) {
if (n == 0) return 1; //곱셈의 항등원은 1
return n * factorial(n - 1);
}
int main() {
int n;
printf("정수를 입력하시오.\n");
scanf("%d", &n);
int result = factorial(n);
printf("%d! = %d\n", n, result);
return 0;
}
이때 덧셈은 0, 곱셈은 1을 베이스 케이스로 반환한다는 것을 알아두면 좋다.
'Study > C' 카테고리의 다른 글
C 언어 자료구조 스터디 [5] <순차 선형 리스트의 삽입과 삭제 및 희소행렬> (0) | 2025.04.07 |
---|---|
C 언어 자료구조 스터디 [4] <동적 할당과 포인터> (0) | 2025.04.03 |
C 언어 자료구조 스터디 [3] <다차원 배열> (0) | 2025.04.01 |
C 언어 자료구조 스터디 [2] <산술연산자 / 관계연산자 / 비트연산자> (0) | 2025.03.28 |
C 언어 자료구조 스터디 [1] <2진수 정수와 실수 표현> (0) | 2025.03.20 |