[C] - 함수

Chris Kim·2024년 9월 24일

프로그래밍언어

목록 보기
10/25

1. 함수 정의 및 호출 (Defining and Calling Function)

함수 정의(Function Definitions)

함수를 정의 한다는 것은 다음과 같은 형식을 가진다.

return-type function-name ( parameters )
{
declarations
statements
}

여기서 return-type이란 함수가 어떤 자료형의 값을 반환할 지를 말한다. 이와 관련된 규칙은 (1) 함수는 배열을 반환하지 않는다 단 자료형에 제한은 없다. (2) 자료형을 void로 정의하는 것은 함수가 어떤 값이라도 반환하지 않는다는 의미다. (3) 만약 C89에서 자료형을 생략한다면 기본적으로 int를 반환하지만, C99에서는 생략 자체가 불가능하다.

단 다음과 같이 자료형을 다른 줄에 입력해서 표현할 수 있다. 자료형이 unsigned long long int과 같이 이름이 매우 긴 경우 가독성 측면에서 매우 유용하다.

double
average(double a, double b)
{
return (a + b) / 2;
}

함수의;자료형, 이름 뒤에는 매개변수(parameter)가 온다. 각각의 매개변수는 자료형이 앞에 써져 있으며, ,로 각각의 매개변수가 구분된다. 만약 함수에 매개변수가 없다면 void가 쓰여져야만 한다. 여기서 매개변수의 자료형은 생략이 불가하다.
함수 실행부분에서는 선언과 구문이 모두 사용가능하다. 그러나 함수 정의 내부에서 선언된 변수는 오로지 함수 내부에서만 접근이 가능하다.

함수 호출(Function Calls)

함수 호출은 함수의 이름 그리고 ()로 감싸진 매개변수로 표현된다.

average(x, y)

만약 ()가 생략되면 함수는 호출되지 않는다. void 함수는 호출할 때마다, 뒤에 ;가 붙는다. 반면, 그 밖의 함수들은 반환값을 변수에 저장하거나, 판별, 혹은 다른 방식으로 이용될 수 있다. 만약 void 함수처럼 단독으로 사용한다면, 반환값은 그냥 버려질 것이다.
그냥 버린다는게 이상할 수도 있는데, 사실 몇몇 경우에는 쓸모가 있다. printf함수의 경우, 출력하는 문자의 갯수를 반환한다. 다음의 num_chars9라는 값을 가진다.

num_chars = printf("Hi, Mom!\n");

하지만 우리는 보통 출력값에 관심이 있지, 계산된 값을 버리는거에는 관심이 없다. 따라서 우리는 의도적으로 (void)라고 함수 앞에 캐스팅을 할 수있다.

2. 함수 선언(Function Declarations)

위에서 살펴봤듯, 함수를 정의하는 것은 언제나, 함수가 호출되기 이전에 위에서 이뤄진다. 사실 C언어에서는 함수가 굳이 앞에서 정의되지 않아도 된다. 뒤에서 정의된 함수를 컴파일러가 만나는 경우, 컴파일러는 이 함수가 어떤 타입인지, 어떤 매개변수를 얼마나 가지고 있는지 모른다. 하지만 컴파일러는 에러 메시지를 내보내기보다, 함수가 int를 반환한다고 가정한다. 이를 컴파일러가 묵시적 선언을 했다고 말한다.
컴파일러는 매개변수의 갯수도, 형도 모른다. 대신 기본 매개변수의 승격을 실행하면서 이게 맞기를 바랄 것이다. 그러나 함수정의를 만나서 그것이 다르다는 것을 알았을 때, 에러메시지가 나올 것이다.
이런 상황에서 쓰이는 것이 함수선언이다. 자세하게 정의하기 보다는 함수의 대략적인 형식을 미리 선언해주는 것이다.

return-type function-name (parameter);

말할 필요도 없다. 함수 정의와 같은 방식이다.(단 함수 내부의 선언과 구문은 생략되어있다.) 단 함수 정의와는 다르게 매개변수의 이름을 미리 정하지 않아도 된다. 자료형만 삽입해도 된다.

3. 입력값(Arguments)

사실 매개변수와 입력값은 같아보이지만 다르다. 매개변수는 함수 정의에서 사용되는 것이고, 입력값은 함수가 호출될 때 사용되는 것이다. 입력값의 값은 함수가 호출되어서 실행될때, 대응되는 매개변수에 복사되어 입력된다. 실질적으로 각 매개변수는 각각 대응하는 입력값으로 초기화되는 것이다.
입력값이 전달되는 과정은 장단점이 존재한다. 매개변수는 대응되는 입력값에 영향을 주지않고 바뀔 수 있다. 고로 매개변수는 함수 내에서 변수처럼 사용될 수 있다. 하지만 이는 반대로 함수 외부에서 주어진 입력값을 함수 내부에서 변경할 수 없다는 단점으로 작용한다.

입력값 변환

C언어는 입력값의 자료형이 매개변수와 일치할 것을 요구하지 않는다. 이와 관련된 규칙은, 컴파일러가 함수 호출 이전에 함수 선언 혹은 정의를 만났는지 여부에 따라 갈린다.
(1) 컴파일러가 함수 선언/정의를 이미 맞닥트린경우, 입력값은 묵시적으로 대응되는 매개변수의 자료형으로 변환된다.
(2) 컴파일러가 함수 선언/정의를 만나지 못한 경우, 컴파일러는 입력값의 승격에 대해 기본 설정을 가지고 있다. 첫 번째는 floatdouble로, 두 번째는 charshort 같은 대정수(integral)형은 int로 승격된다.
하지만 자동적으로 이뤄지는 입력값 변환에 의존하는 것은 매우 위험하다. 이는 정의되지 않은 행위로 이어질 수 있다.

배열 입력값

배열도 입력값으로 쓰일 수 있다. 함수의 매개변수가 1차원 배열인 경우 배열의 길이는 정의되지 않아도 된다. 물론 sizeof를 통해 배열의 길이를 함수정의에 집어넣을 수 있지 않냐고 의문을 제기할 수 있다. 그러나 니는 배열 매개변수에서 전혀 유효하지 않다. 마찬가지로 함수 선언에서 배열의 이름을 생략하는 것은 가능하다. 단 함수 선언/정의에서 []가 들어가지만, 함수 호출시 입력값에는 []가 들어가지 않는다는 점에 유의해야한다.
배열 입력값과 관련하여 가장 중요한 점은, 함수는 올바른 길이의 배열을 전달 받았는지 확인하지 않는다는 점이고, 우리는 이를 악용할 수 있다. 예를 들어 실제보다 배열의 길이가 더 짧다고 말하면 배열의 나머지 부분은 무시될 것이다. 참고로 만약 더 길다고 말하면 함수는 배열의 끝을 벗어날 것이고, 이는 정의되지 않은 행위다.
배열 입력값이 가지는 다른 중요한 부분은 배열의 원소는 함수 내부 구문에 의해서 변경될 수 있다는 점이다. 그 이유는 나중에 다룰 것이다.
만약 다차원 배열 입력값을 가진다면, 매개변수가 선언될 때, 1차원 길이만이 생략가능하다. 즉 2차원 부터는 우리가 정의해야한다.

VLA 입력값

C99는 배열 입력값과 관련하여 새로워진 부분이 있다. 첫 번째는 상수가 아닌 표현식을 통해 배열의 길이를 정의할 수 있게된 부분이고, 두 번째는 VLA가 매개변수가 될 수 있게 된 부분이다.

int get_sum_of_array(int size, int arr[size])
{
    …
}

sizearr보다 뒤에 있으면 안된다. 그 밖에도 *을 사용할 수 있다. 예시는 다음과 같다.

int get_sum_of_array(int size, int arr[*]);

함수 선언에서는 매개변수 이름을 생략할 수 있다. 이 경우 이런식으로 표현 가능하지만 후자는 관계성이 불분명하여 좋지 못하다.

int get_sum_of_array(int, int arr[*]);    
int get_sum_of_array(int, int arr[]);    

사실 VLA를 입력변수로 사용할 수 있게되면서, 다차원 배열을 입력하는 함수 작성이 매우 용이해졌다. 기존에는 1차원 이상의 배열에 대해서 고정된 길이만을 다루도록 정의해주어야 했기 때문이다.

int get_sum_of_two_dimensional_array(int row_size, int column_size, int arr[row_size][column_size]);
int get_sum_of_two_dimensional_array(int row_size, int column_size, int arr[*][*]);
int get_sum_of_two_dimensional_array(int row_size, int column_size, int arr[][column_size]);
int get_sum_of_two_dimensional_array(int row_size, int column_size, int arr[][*]);

배열 매개변수에서의 static 사용

static을 통해 배열의 최소크기를 컴파일러에게 전달 할 수 있다. 이는 실질적인 프로그램 실행에 영향을 주진 않지만, 처리속도를 향상시키는데 도움을 준다.

int get_sum_of_two_dimensional_array(int arr[static 3], int size)
{
    …
}

복합리터럴

배열을 입력변수로 사용하는 경우, 여태까지 배열은 입력변수로 사용되기 전에 선언되어야만 했다. 하지만 C99 부터는 복합리터럴(Compound Literal)을 통해 이런 수고로움을 덜 수 있게 되었다.

total = sum_array((int [i]){3, 0 ,3, 4, 1}, 5);

물론 {} 안쪽을 상수가 아닌 표현식으로 채워 넣어도 좋다. 읽기 전용으로만 사용하고 싶다면 (const int[]){5, 4} 같이 사용하면 된다.

4. return 구문

return은 함수의 결과값을 반환한다는 뜻이다. return 뒤에는 표현식이 들어갈 수 있다. 따라서 다음과 같은 예시도 가능하다.

return n >= 0 ? n : 0;

만약 함수의 자료형과 다른 값을 return 하게 되더라도 함수의 자료형에 맞게 변환된다. void 함수는 값을 반환하지 않지만, return 단독으로 사용되는 경우 함수를 종료하게 된다. 물론 이 뒤에 무언가 붙는다면 컴파일 타임 에러가 발생하므로 주의하자. 반대로 non-void 함수의 경우 return이 실행되지 않는경우, "control reaches end of non-void function"라는 에러메시지가 발생하며, 이는 정의되지 않은 행위다.

5. 프로그램 종료

일반적으로 main 함수는 정수형을 반환한다. 하지만 C89에서는 함수 앞에 int를 생략한 경우가 있는데, C99에서는 허용하지 않으므로 주의해야한다.
일반적으로 프로그램이 종료되는 경우에는 main 함수가 테스트 될 수 있는 상태코드가 반환된다. 물론 이를 어떻게 사용하는지는 아무런 제한이 없다. 그러나 이러한 관습을 지킨다면, 타인이 프로그램을 테스트 하는 것이 용이하다.

exit 함수

exit함수는 main 함수에서 프로그램을 종료시킬 수 있는 한 방법이다. 다음은 종료에 사용되는 예시다.

exit(0);
exit(EXIT_SUCCESS);
exit(EXIT_FILURES);

EXIT_SUCCESSEXIT_FAILURE는 <stdlib.h>에서 정의된 매크로로, 각 0, 1을 뜻한다.

6. 재귀함수

일반적으로 재귀함수는 다음과 같이 쓰인다.

int fact(int n)
{
if (n <= 1) return 1; else
return n * fact(n - 1);
}

이런 재귀함수에서 주의해야할 점은, 종료 조건을 설정하는 것이다. 그렇지 않으면 무한히 재귀함수 자신을 호출하고, 프로그램은 종료되지 않을 것이다.

퀵소트-정렬 알고리즘(Quicksort Algorithm)

재귀함수의 실사용 예시를 보이기 위해 퀵소트-정렬 알고리즘을 살펴보자. 퀵소트 정렬 알고리즘은 다음과 같은 순서로 이뤄진다.

1. 배열 내에서 임의의 원소  _e_ 를 선택한다. 이것은 파티션 원소(partitioning element)다. 나머지 원소들 중, 1부터 i-1은 e보다 작거나 같은 원소들이고 i+1부터 n은 e보다 크거나 같은 원소들이다. 즉 _e_ 를 기점으로 두 개의 집합이 만들어진다.
2. _e_ 보다 작거나 같은 원소들로 이뤄진 집합에 대해 위의 작업을 반복적으로 수행한다.
3. _e_ 보다 크거나 같은 원소들로 이뤄진 집합에 대해 위의 작업을 반복적으로 수행한다.

이 알고리즘의 핵심은 한번 임의의 원소 e 를 기점으로 크고 작은 집합 두 개를 구분함과 동시에 e 의 위치는 확정된다는 사실이다. 즉 e 앞/뒤 집합 내의 정렬은 확보가 되지 않지만, e가 배열 전체에서 가지는 자신의 위치는 보장된다. 이 작업을 쪼개진 집합에도 반복적으로 적용하면 모든 원소를 파티션 원소로 한 번씩은 선택하게 되고, 모든 원소에 대해 정렬을 행할 수 있다.
다음은 퀵소트-정렬 알고리즘의 예시 코드다. 이 코드가 가장 효율적인 코드는 아닌 것에 유의하라.

/* Sorts an array of integers using Quicksort algorithm */ #include <stdio.h>

#define N 10

void quicksort(int a[], int low, int high); 
int split(int a[], int low, int high);

int main(void)
{
int a[N], i;
printf("Enter %d numbers to be sorted: ", N) ;

for (i = 0; i < N; i++) scanf("%d", &a[i]);
	quicksort(a, 0, N - 1) ;

printf("In sorted order: ") ; 
for (i = 0; i < N; i++) 
	printf("%d ", a[i]); 
printf("\n");

return 0;
}

void quicksort (int a[], int low, int high)
{
	int middle;
	if (low >= high) return; 
    middle = split(a, low, high); 
    quicksort(a, low, middle - 1); 
    quicksort(a, middle + 1 , high);
}

int split (int a[], int low, int high)
{
	int part_element = a [low]; 
    for (; ; ) {
		while (low < high && part_element <= a[high]) 
        	high- - ;
		if (low >= high) break; 
        a[low++] = a[high];

		while (low < high && a[low] <= part_element)
			loW+ +;
		if (low >= high) break; 
        a [high--] = a [low];
        }
        
		a [high] = part_element; 
        return high;
}
profile
회계+IT=???

0개의 댓글