8. 함수(Function)
1. 키워드
- 함수(Function)
- 반환 타입(Return Type)
- 매개변수(Parameter)와 인수(Argument)
- 함수 원형(Function Prototype)
- 값에 의한 전달(Call by Value)과 참조에 의한 전달(Call by Reference)
- 재귀 호출(Recursive Call)
- 함수 포인터(Function Pointer)
typedef
와auto
키워드
2. 함수의 정의
- 함수란 하나의 특별한 목적의 작업을 수행하기 위해 독립적으로 설계된 코드의 집합으로 정의할 수 있다.
- C++ 프로그램에서 함수는 특정 작업을 캡슐화하는 데 유용하게 사용된다.
1) 함수를 사용하는 이유
- 함수를 사용하는 가장 큰 이유는 바로 반복적인 프로그래밍을 피할 수 있기 때문이다.
- 프로그램에서 특정 작업을 여러 번 반복해야 할 때는 해당 작업을 수행하는 함수를 작성하면 된다.
- 그리고서 프로그램이 필요할 때마다 작성한 함수를 호출하면 해당 작업을 반복해서 수행할 수 있다.
- 또한, 프로그램을 여러 개의 함수로 나누어 작성하면, 모듈화로 인해 전체적인 코드의 가독성이 좋아진다.
- 그리고 프로그램에 문제가 발생하거나 기능의 변경이 필요할 때에도 손쉽게 유지보수를 할 수 있다.
- 함수의 크기에 대해서 정확히 명시된 규칙은 없으나, 대략 하나의 기능을 하나의 함수로 만드는 것이 가장 좋다.
2) 함수의 선언
- C++에서 함수를 선언하는 방법은 다음 그림과 같다.
- 함수의 각 명칭의 내용은 다음과 같다.
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()
함수의 원형은 다음과 같다.
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()
함수는 재귀 호출을 사용하지 않고 만든 함수이다. - 이러한 함수는 그냥 봐서는 그 목적을 바로 알 수 없으며, 코드를 해석해야 무슨 목적으로 만든 함수인지 알 수 있다.
- 즉, 변수
i
와result
는 왜 정의되었으며,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)로 작성하면 다음과 같다.
- 위와 같이 논리적인 재귀 알고리즘을 구상하고, 의사 코드를 작성하면, 재귀 호출을 이용해 바로 코드로 옮길 수 있다.
#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)
- 포인터 상수란 포인터 변수가 가리키고 있는 주소값을 변경할 수 없는 포인터를 의미한다.
- 상수 포인터란 상수를 가리키는 포인터를 의미한다.
- 함수 포인터의 포인터 타입은 함수의 반환값과 매개변수에 의해 결정된다.
- 즉, 함수의 원형을 알아야만 해당 함수에 맞는 함수 포인터를 만들 수 있다.
- 위의 함수 원형에 대한 적절한 함수 포인터는 다음과 같다.
- 연산자의 우선순위 때문에 반드시
*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) 함수 포인터의 표기법
- 앞선 코드에서 사용한 함수 포인터는 다음과 같다.
- 이처럼 함수 포인터의 가장 큰 단점은 바로 그 표기법이 복잡한 데 있다.
- C++에서는 이러한 복잡한 표기법을 단순화하는 방법으로 다음의 두 가지 키워드를 제공한다.
1] typedef
키워드
2] auto
키워드
typedef
키워드를 이용하면 복잡한 함수 포인터 타입에 새로운 이름을 붙일 수 있다.
typedef double (*CalcFunc)(double, double); // 함수 포인터에 CalcFunc이라는 새로운 이름을 붙임
CalcFunc ptr_func = calc;
- C++11부터 제공하는
auto
키워드를 이용하면 복잡한 함수 포인터 타입으로 자동 타입 변환할 수 있다.
- 다음의 코드는 앞선 코드에
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입니다.