본문 바로가기
카테고리 없음

C 언어 기본] 함수

by redcubes 2024. 3. 17.

목차

A. 작성과 사용

B. 유형

C. 정리

A. 작성 사용

- main, printf, scanf 같은 것. printf, scanf처럼특정 기능을 미리 약속해서 프로그램에 바로 쓸 수 있게 구현된 것을 표준 라이브러리 함수 라고 함.(stdio.h) 

____1.  정의

-  함수원형( 반환형 + 함수명 + (매개변수(=인수들)) )+{수행내용} = 함수 정의

- 함수를 다른 함수 안에서 정의할 수는 없음.

____2. 호출 반환

- 인수의 값은 매개변수에 복사되어 사용됨.

- return: 함수 호출할 때 컴파일러가 반환값 자장할 공간 미리 준비, 컴파일러 별도 확보, 식별자 없음=휘발=대입 필요.

____3. 선언

-함수원형+; 매개변수 생략하고 타입만 쓸 수도 있음.
그런데 왜 하나? 정의가 있는데?

C언어에서 함수 선언은 여러 이유로 중요합니다. 가장 큰 이유는 컴파일러에게 함수의 존재와 그 형태를 알려줌으로써, 함수를 사용하기 전에 이미 그 함수에 대한 정보를 제공하기 위해서입니다. 구체적으로 함수 선언이 필요한 이유는 다음과 같습니다:

  1. 타입 체킹: 함수 선언은 함수의 반환 타입과 매개변수의 타입 및 개수 정보를 포함합니다. 이를 통해 컴파일러는 함수 호출 시 올바른 타입과 개수의 인자가 전달되었는지 확인할 수 있습니다. 타입이 맞지 않거나 인자의 개수가 다르면 컴파일러는 오류를 발생시켜 프로그래머에게 알립니다.
  2. 함수의 선행 선언: C언어는 위에서 아래로 코드를 순차적으로 컴파일합니다. 함수를 선언하기 전에 호출하려고 하면 컴파일러는 해당 함수를 인식할 수 없어 오류를 발생시킵니다. 함수 선언을 통해 컴파일러에게 함수의 존재를 미리 알려주면, 함수의 정의 위치와 관계없이 어디서든지 해당 함수를 사용할 수 있습니다.
  3. 모듈화 및 재사용: 함수 선언을 통해 다른 파일이나 모듈에서도 해당 함수를 쉽게 재사용할 수 있습니다. 예를 들어, 함수 선언을 헤더 파일(.h)에 넣고 이 헤더 파일을 여러 소스 파일에서 포함(include)시키면, 여러 소스 파일에서 같은 함수를 사용할 수 있습니다.
  4. 가독성 및 유지보수: 함수 선언은 함수의 인터페이스만을 제공하기 때문에, 프로그램의 다른 부분을 작성하거나 읽을 때 함수의 구현 세부 사항에 신경 쓰지 않아도 됩니다. 이는 코드의 가독성을 높이고 유지보수를 용이하게 합니다.

요약하자면, 함수 선언은 타입 체킹, 컴파일 오류 예방, 코드의 모듈화 및 재사용성 향상, 가독성 및 유지보수의 용이성을 위해 필수적입니다.

B. 유형

-

____1. 매개변수 없음(void)

____2. void 리턴없음

return;

____3. void 둘 다 없음(viod)

____4. 재귀 & 반복문과의 차이

- recursive call function

#include <stdio.h>

void fruit(void);			// 함수 선언

int main(void)
{
	fruit();				// 함수 호출

	return 0;
}

void fruit(void)			// 재귀호출 함수 정의
{
	printf("apple\n");
	fruit();				// 자신을 다시 호출
}

스택을 이용. 스택 오버플로. 재귀에는 탈출을 위한 기본 조건식 필수.

- 반복문과의 차이. 직전 호출한 곳으로 돌아감.

C. 정리

-

____1. 요약

-

____2 . 확인문제

-

____3. 궁금증

모든 재귀문을 반복문으로 바꿀 수 있나?

모든 재귀 함수는 이론적으로 반복문으로 변환될 수 있습니다. 컴퓨터 과학에서는 재귀와 반복이 동등한 계산 능력을 가지고 있다는 것을 이해하는 것이 중요합니다. 이는 모든 재귀적 알고리즘을 반복적 알고리즘으로 바꿀 수 있으며, 그 반대도 가능함을 의미합니다. 이 변환 과정은 때로는 간단하고 직관적일 수 있지만, 복잡한 재귀적 로직을 가진 경우에는 더 복잡한 반복 구조를 필요로 할 수 있습니다.

재귀를 반복으로 변환하는 방법

  1. 스택을 사용한 변환: 재귀 함수가 자신을 호출하는 과정에서 생성되는 호출 스택을 모방하기 위해 명시적인 스택 데이터 구조를 사용할 수 있습니다. 이 방법은 특히 다중 재귀 호출을 가진 함수에서 유용합니다.
  2. 반복문 사용: 간단한 재귀 함수는 for 또는 while 반복문을 사용하여 직접적으로 변환될 수 있습니다. 이때, 재귀 호출의 기저 조건은 반복문의 종료 조건으로, 재귀적 단계는 반복문의 본문으로 변환됩니다.

예시

재귀 함수 예시: 팩토리얼 계산

int factorial(int n) {
    if (n <= 1) return 1; // 기저 조건
    else return n * factorial(n - 1); // 재귀 호출
}

반복문으로 변환된 예시: 팩토리얼 계산

int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

재귀적 접근 방식이 자연스럽게 표현되는 알고리즘(예: 트리 순회, 그래프 알고리즘, 분할 정복 알고리즘 등)도 있습니다. 이 경우, 재귀 함수를 반복문으로 변환하는 것이 가능하긴 하지만, 코드의 가독성이나 간결성이 저하될 수 있습니다. 결국, 재귀와 반복 중 어느 것을 사용할지 결정하는 것은 특정 문제에 대한 해결 방법의 명확성, 이해의 용이성, 그리고 성능 요구 사항을 고려하여 결정되어야 합니다.

성능 차이가 없나?

재귀 함수와 반복문 사이의 성능 차이는 존재할 수 있으며, 이는 여러 요소에 따라 달라질 수 있습니다. 각 접근 방식의 성능을 결정하는 주요 요소는 다음과 같습니다:

  1. 메모리 사용: 재귀 함수는 호출될 때마다 호출 스택에 새로운 레이어를 추가합니다. 이로 인해, 깊은 재귀 호출이 발생하는 경우 상당한 양의 메모리를 사용할 수 있으며, 최악의 경우 스택 오버플로를 일으킬 수 있습니다. 반면, 반복문은 추가적인 스택 프레임을 생성하지 않으므로 메모리 사용량이 상대적으로 적습니다.
  2. 호출 오버헤드: 재귀 함수는 자기 자신을 호출할 때마다 함수 호출에 대한 오버헤드가 발생합니다. 이 오버헤드에는 매개변수 전달, 실행 컨텍스트 스위치 등이 포함됩니다. 반복문은 이러한 추가 오버헤드 없이 연속적인 실행이 가능하므로, 경우에 따라 더 효율적일 수 있습니다.
  3. 최적화 가능성: 컴파일러 최적화 기능은 반복문을 더 잘 최적화할 수 있는 경우가 많습니다. 예를 들어, '꼬리 호출 최적화(tail call optimization)'는 일부 재귀 함수에서 작동할 수 있지만, 모든 컴파일러와 모든 상황에서 적용 가능한 것은 아닙니다. 반복문은 일반적으로 더 예측 가능한 성능 특성을 가집니다.
  4. 알고리즘의 복잡성: 특정 알고리즘은 재귀적으로 표현했을 때 더 자연스럽고 이해하기 쉬울 수 있습니다. 예를 들어, 퀵 정렬이나 병합 정렬 같은 분할 정복 알고리즘은 재귀적 구현이 더 직관적입니다. 이 경우, 재귀의 사용이 성능에 미치는 영향은 알고리즘의 효율성과 코드의 간결성을 고려할 때 정당화될 수 있습니다.

결국 성능 차이는 구현된 알고리즘의 특성, 사용된 프로그래밍 언어, 컴파일러의 최적화 능력 등에 따라 달라집니다. 일반적으로 간단한 반복 작업은 반복문을 사용하는 것이 더 효율적이며, 복잡한 알고리즘에서는 재귀 함수가 코드의 가독성과 관리를 용이하게 할 수 있습니다. 성능이 중요한 상황에서는 두 접근 방식을 모두 고려하고, 가능하다면 벤치마크를 통해 더 효율적인 방식을 결정하는 것이 좋습니다.