Skip to content

8. 함수(Function)


1. 키워드

  • 함수(Function)
  • 반환 타입(Return Type)
  • 매개변수(Parameter)와 인수(Argument)
  • 함수 원형(Function Prototype)
  • 값에 의한 전달(Call by Value)과 참조에 의한 전달(Call by Reference)
  • 재귀 호출(Recursive Call)
  • 함수 포인터(Function Pointer)
  • typedefauto 키워드


2. 함수의 정의

  • 함수란 하나의 특별한 목적의 작업을 수행하기 위해 독립적으로 설계된 코드의 집합으로 정의할 수 있다.
  • C++ 프로그램에서 함수는 특정 작업을 캡슐화하는 데 유용하게 사용된다.


1) 함수를 사용하는 이유

  • 함수를 사용하는 가장 큰 이유는 바로 반복적인 프로그래밍을 피할 수 있기 때문이다.
  • 프로그램에서 특정 작업을 여러 번 반복해야 할 때는 해당 작업을 수행하는 함수를 작성하면 된다.
  • 그리고서 프로그램이 필요할 때마다 작성한 함수를 호출하면 해당 작업을 반복해서 수행할 수 있다.
  • 또한, 프로그램을 여러 개의 함수로 나누어 작성하면, 모듈화로 인해 전체적인 코드의 가독성이 좋아진다.
  • 그리고 프로그램에 문제가 발생하거나 기능의 변경이 필요할 때에도 손쉽게 유지보수를 할 수 있다.
  • 함수의 크기에 대해서 정확히 명시된 규칙은 없으나, 대략 하나의 기능을 하나의 함수로 만드는 것이 가장 좋다.


2) 함수의 선언

  • C++에서 함수를 선언하는 방법은 다음 그림과 같다.


001


  • 함수의 각 명칭의 내용은 다음과 같다.


1] 반환 타입

  • 함수가 모든 작업을 마치고 반환하는 데이터의 타입을 명시한다.

2] 함수 이름

  • 함수를 호출하기 위한 이름을 명시한다.

3] 매개변수 목록

  • 함수 호출 시에 전달되는 인수의 값을 저장할 변수들을 명시한다.

4] 함수 몸체

  • 함수의 고유 기능을 수행하는 명령문의 집합이다.


  • 함수 호출 시에는 여러 개의 인수를 전달할 수 있지만, 함수가 반환할 수 있는 값은 1개를 넘지 못한다.
  • 또한, 함수의 특성에 따라 인수나 반환값이 하나도 없는 함수도 존재할 수 있다.


  • C++에서는 반환값으로 배열을 제외한 모든 타입을 사용할 수 있다.
  • 하지만 구조체나 객체에 포함된 배열은 반환할 수 있다.


  • 다음의 코드는 인수로 전달 받은 두 수 중에서 더 작은 수를 반환하는 SmallNum() 함수를 정의하여 사용한다.


#include <iostream>
using namespace std;

int SmallNum(int num1, int num2) {
    if (num1 <= num2) {
        return num1;
    } else {
        return num2;
    }
}

int main(void) {
    int result;

    result = SmallNum(4, 6);
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    result = SmallNum(8, 6);
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    result = SmallNum(2, 8);
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    return 0;
}

// 두 수 중 더 작은 수는 4입니다.
// 두 수 중 더 작은 수는 6입니다.
// 두 수 중 더 작은 수는 2입니다.


3) 함수의 원형 선언

  • C++에서 함수를 정의할 때는 그 순서가 매우 중요하다.


  • 다음의 코드는 앞서 살펴본 코드에서 함수의 선언 순서만을 바꾼 것이다.


#include <iostream>
using namespace std;

int main(void) {
    int result;

    result = SmallNum(4, 6);  // 함수 SmallNum()을 아직 알 수 없음. 컴파일 오류 발생
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    result = SmallNum(8, 6);  // 함수 SmallNum()을 아직 알 수 없음. 컴파일 오류 발생
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    result = SmallNum(2, 8);  // 함수 SmallNum()을 아직 알 수 없음. 컴파일 오류 발생
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    return 0;
}

int SmallNum(int num1, int num2) {
    if (num1 <= num2) {
        return num1;
    } else {
        return num2;
    }
}


  • C++에서는 가장 먼저 main() 함수가 컴파일러에 의해 컴파일된다.
  • 위의 코드에서 컴파일러는 main() 함수에 등장하는 SmallNum() 함수를 아직 알지 못하기 때문에 컴파일 오류를 발생시킨다.
  • 따라서 컴파일러에 SmallNum() 함수는 나중에 정의되어 있다고 알려줘야 한다.
  • 그 역할을 하는 것이 바로 함수의 원형을 선언하는 것이다.
  • ANSI C에서는 기존 C와의 호환성을 유지하기 위해서 함수의 원형을 사용하지 않아도 되지만, C++에서는 반드시 함수의 원형을 사용해야만 한다.


  • 함수의 원형 선언은 다음과 같은 방식으로 선언된다.


반환타입 함수이름(매개변수목록타입);


  • 다음의 코드는 앞서 살펴본 코드에 함수의 원형 선언을 추가한 것이다.
  • 이처럼 함수의 원형은 main() 함수 앞에 미리 선언되어야 한다.


#include <iostream>
using namespace std;

int SmallNum(int, int);

int main(void) {
    int result;

    result = SmallNum(4, 6);
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    result = SmallNum(8, 6);
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    result = SmallNum(2, 8);
    cout << "두 수 중 더 작은 수는 " << result << "입니다." << endl;

    return 0;
}

int SmallNum(int num1, int num2) {
    if (num1 <= num2) {
        return num1;
    } else {
        return num2;
    }
}

// 두 수 중 더 작은 수는 4입니다.
// 두 수 중 더 작은 수는 6입니다.
// 두 수 중 더 작은 수는 2입니다.


3. 인수 전달 방법

  • 함수를 호출할 때에는 함수에 필요한 데이터를 인수로 전달해 줄 수 있다.
  • 이렇게 함수에 인수를 전달하는 방법에는 크게 다음과 같이 두 가지 방법이 있다.


1] 값에 의한 전달

2] 참조에 의한 전달


1) 값에 의한 전달

  • 값에 의한 전달 방법은 인수로 전달되는 변수가 가지고 있는 값을 함수 내에 매개변수에 복사하는 방식이다.
  • 이렇게 복사된 값으로 초기화된 매개변수는 인수로 전달된 변수와는 완전히 별개의 변수가 된다.
  • 따라서 함수 내에서 매개변수 조작은 인수로 전달되는 변수에 아무런 영향을 미치지 않는다.


#include <iostream>
using namespace std;

void Local(int);

int main(void) {
    int var = 20;
    cout << "변수 var의 초깃값은 " << var << "입니다." << endl;

    Local(var);
    cout << "Local() 함수 호출 후 변수 var의 값은 " << var << "입니다." << endl;

    return 0;
}

void Local(int num) {
    num += 10;
}

// 변수 var의 초깃값은 20입니다.
// Local() 함수 호출 후 변수 var의 값은 20입니다.


  • 위의 코드에서 Local() 함수의 매개변수 num은 인수로 변수 var의 값을 전달 받는다.
  • 따라서 함수 내에서 매개변수 num의 값을 아무리 변경하더라도 원래 인수로 전달된 변수 var의 값은 절대 변경되지 않는다.


2) 참조에 의한 전달

  • 참조에 의한 전달 방법은 인수로 전달된 변수의 값을 복사하는 것이 아닌, 원본 데이터를 직접 전달하는 것이다.
  • C에서는 이러한 작업을 포인터를 사용하여 인수로 전달된 변수의 주소값을 전달한다.
  • 하지만 C++에서는 이러한 작업을 참조자(Reference)를 사용하여 전달할 수 있다.


#include <iostream>
using namespace std;

void Local(int &);

int main(void) {
    int var = 20;
    cout << "변수 var의 초깃값은 " << var << "입니다." << endl;

    Local(var);
    cout << "Local() 함수 호출 후 변수 var의 값은 " << var << "입니다." << endl;

    return 0;
}

void Local(int &num) {
    num += 10;
}

// 변수 var의 초깃값은 20입니다.
// Local() 함수 호출 후 변수 var의 값은 30입니다.


  • 위의 코드에서 Local() 함수의 매개변수 num은 인수로 변수 var의 참조를 전달 받는다.
  • 따라서 함수 내에서 참조자 num의 값을 변경하면 원래 인수인 변수 var의 값도 같이 변경된다.


3) main() 함수의 인수 전달

  • main() 함수는 프로그램이 실행되면 제일 먼저 자동으로 호출되는 함수이다.
  • 이러한 main() 함수도 함수이기 때문에 인수를 전달 받을 수도 있고, 반환값을 가질 수도 있다.


  • main() 함수의 원형은 다음과 같다.


void(또는 int) main(int argc, char *argv[]);


  • main() 함수의 첫 번째 인수인 int 타입 변수 argc는 인수로 전달되는 문자열의 개수를 명시한다.
  • 두 번째 인수 char 타입 포인터의 포인터인 argv는 인수로 전달된 각각의 문자열이 포함되는 배열을 가리킨다.


4. 재귀 호출

  • 재귀 호출이란 함수 내부에서 함수가 자기 자신을 또다시 호출하는 것을 말한다.
  • 이러한 재귀 호출은 자기가 자신을 계속해서 호출하므로, 끝없이 반복될 것이다.
  • 따라서 함수 내에 재귀 호출을 중단하도록 조건이 변경될 명령문을 반드시 포함해야 한다.


1) 재귀 호출의 개념

  • 재귀 호출의 개념을 파악하기 위해서 우선 재귀 호출을 사용하지 않고 1부터 n까지의 합을 구하는 함수를 만들어보자.


#include <iostream>
using namespace std;

int Sum(int n) {
    int result = 0;

    for (int i = 0; i <= n; i++) {
        result += i;
    }

    return result;
}

int main(void) {
    int n = 10;
    int result;

    result = Sum(n);

    cout << "1부터 " << n << "까지의 합은 " << result << "입니다." << endl;

    return 0;
}

// 1부터 10까지의 합은 55입니다.


  • 위의 코드에서 Sum() 함수는 재귀 호출을 사용하지 않고 만든 함수이다.
  • 이러한 함수는 그냥 봐서는 그 목적을 바로 알 수 없으며, 코드를 해석해야 무슨 목적으로 만든 함수인지 알 수 있다.
  • 즉, 변수 iresult는 왜 정의되었으며, for 문은 왜 사용되었는지 바로 알 수가 없다.


  • 이제 재귀 호출을 사용하여 1부터 n까지의 합을 구하는 RecursiveSum() 함수를 만들어보자.
  • 우선 1부터 4까지의 합을 구하는 알고리즘을 생각해 보자.


1] 1부터 4까지의 합은 1부터 3까지의 합에 4를 더하면 된다.

2] 1부터 3까지의 합은 1부터 2까지의 합에 3를 더하면 된다.

3] 1부터 2까지의 합은 1부터 1까지의 합에 2를 더하면 된다.

4] 1부터 1까지의 합은 그냥 1이다.


  • 위의 알고리즘을 의사 코드(Pseudo Code)로 작성하면 다음과 같다.


시작
    1. n이 1이 아니면, n과 1부터 (n-1)까지의 합을 더한 값을 반환함.
    2. n이 1이면, 그냥 1을 반환함.


  • 위와 같이 논리적인 재귀 알고리즘을 구상하고, 의사 코드를 작성하면, 재귀 호출을 이용해 바로 코드로 옮길 수 있다.


#include <iostream>
using namespace std;

int RecursiveSum(int n) {
    if (n == 1) {  // n이 1이면, 그냥 1을 반환함
        return 1;
    }

    // n이 1이 아니면, n을 1부터 (n-1)까지의 합과 더한 값을 반환함
    return n + RecursiveSum(n - 1);
}

int main(void) {
    int n = 10;
    int result;

    result = RecursiveSum(n);

    cout << "1부터 " << n << "까지의 합은 " << result << "입니다." << endl;

    return 0;
}

// 1부터 10까지의 합은 55입니다.


  • 위의 코드에서 만약 if 문이 없다면, 이 메서드의 재귀 호출은 무한히 반복될 것이다.
  • 이처럼 재귀 호출이 무한히 반복되면, 해당 프로그램은 실행 직후 스택 오버플로우(Stack Overflow)에 의해 종료될 것이다.
  • 따라서 if 문처럼 재귀 호출을 중단하기 위한 조건문을 반드시 포함해야 한다.


스택 오버플로우(Stack Overflow)

  • 스택 오버플로우는 메모리 구조 중 스택 영역에서 해당 프로그램이 사용할 수 있는 메모리 공간 이상을 사용하려고 할 때 발생한다.


5. 함수 포인터

  • 프로그램에서 정의된 함수는 프로그램이 실행될 때 모두 메인 메모리에 올라간다.
  • 이때 함수의 이름은 메모리에 올라간 함수의 시작 주소를 가리키는 포인터 상수(Constant Pointer)가 된다.
  • 이렇게 함수의 시작 주소를 가리키는 포인터 상수를 함수 포인터라고 부른다.


포인터 상수(Constant Pointer)와 상수 포인터(Pointer to Constant)

  • 포인터 상수란 포인터 변수가 가리키고 있는 주소값을 변경할 수 없는 포인터를 의미한다.
  • 상수 포인터란 상수를 가리키는 포인터를 의미한다.


  • 함수 포인터의 포인터 타입은 함수의 반환값과 매개변수에 의해 결정된다.
  • 즉, 함수의 원형을 알아야만 해당 함수에 맞는 함수 포인터를 만들 수 있다.


void Func(int, int);


  • 위의 함수 원형에 대한 적절한 함수 포인터는 다음과 같다.


void (*ptr_func)(int, int);


  • 연산자의 우선순위 때문에 반드시 *ptr_func 부분을 ()로 둘러싸야 정상적으로 동작할 것이다.


  • 함수 포인터는 함수를 또 다른 함수의 인수로 전달할 때 유용하게 사용된다.


#include <iostream>
using namespace std;

double Add(double, double);
double Sub(double, double);
double Mul(double, double);
double Div(double, double);
double Calculator(double, double, double (*func)(double, double));

int main(void) {
    double (*calc)(double, double) = NULL;  // 함수 포인터 선언
    double num1 = 3, num2 = 4, result = 0;
    char oper = '*';

    switch (oper) {
        case '+':
            calc = Add;
            break;
        case '-':
            calc = Sub;
            break;
        case '*':
            calc = Mul;
            break;
        case '/':
            calc = Div;
            break;
        default:
            cout << "사칙연산(+, -, *, /)만을 지원합니다." << endl;
            break;
    }

    result = Calculator(num1, num2, calc);
    cout << "사칙 연산의 결과는 " << result << "입니다." << endl;

    return 0;
}

double Add(double num1, double num2) { return num1 + num2; }
double Sub(double num1, double num2) { return num1 - num2; }
double Mul(double num1, double num2) { return num1 * num2; }
double Div(double num1, double num2) { return num1 / num2; }
double Calculator(double num1, double num2, double (*func)(double, double)) {
    return func(num1, num2);
}

// 사칙 연산의 결과는 12입니다.


  • 위의 코드는 함수 포인터를 사용하여 사용자의 입력에 따라 사칙연산 함수 중 하나를 선택한다.
  • 이렇게 선택된 함수를 함수 포인터를 사용하여 Calculator() 함수에 인수로 전달하고 있다.


널 포인터(Null Pointer)

  • 포인터를 초기화할 때 0이나 NULL을 대입하여 초기화한 포인터를 널 포인터라고 한다.
  • 널 포인터는 아무것도 가리키지 않는 포인터라는 의미이다.


1) 함수 포인터의 표기법

  • 앞선 코드에서 사용한 함수 포인터는 다음과 같다.


double (*calc)(double, double) = NULL;


  • 이처럼 함수 포인터의 가장 큰 단점은 바로 그 표기법이 복잡한 데 있다.
  • C++에서는 이러한 복잡한 표기법을 단순화하는 방법으로 다음의 두 가지 키워드를 제공한다.


1] typedef 키워드

2] auto 키워드


  • typedef 키워드를 이용하면 복잡한 함수 포인터 타입에 새로운 이름을 붙일 수 있다.


typedef double (*CalcFunc)(double, double); // 함수 포인터에 CalcFunc이라는 새로운 이름을 붙임
CalcFunc ptr_func = calc;


  • C++11부터 제공하는 auto 키워드를 이용하면 복잡한 함수 포인터 타입으로 자동 타입 변환할 수 있다.


auto ptr_func = calc;


  • 다음의 코드는 앞선 코드에 typedef 키워드를 사용하여 간략화한 것이다.


#include <iostream>
using namespace std;

typedef double (*Arith)(double, double);  // typedef 키워드를 이용한 새로운 이름 선언
double Add(double, double);
double Sub(double, double);
double Mul(double, double);
double Div(double, double);
double Calculator(double, double, Arith);

int main(void) {
    Arith calc = NULL;  // 함수 포인터 선언
    double num1 = 3, num2 = 4, result = 0;
    char oper = '*';

    switch (oper) {
        case '+':
            calc = Add;
            break;
        case '-':
            calc = Sub;
            break;
        case '*':
            calc = Mul;
            break;
        case '/':
            calc = Div;
            break;
        default:
            cout << "사칙연산(+, -, *, /)만을 지원합니다." << endl;
            break;
    }

    result = Calculator(num1, num2, calc);
    cout << "사칙 연산의 결과는 " << result << "입니다." << endl;

    return 0;
}

double Add(double num1, double num2) { return num1 + num2; }
double Sub(double num1, double num2) { return num1 - num2; }
double Mul(double num1, double num2) { return num1 * num2; }
double Div(double num1, double num2) { return num1 / num2; }
double Calculator(double num1, double num2, Arith func) {
    return func(num1, num2);
}

// 사칙 연산의 결과는 12입니다.

References