본문 바로가기
C++

C++ 함수기본

by godfeeling 2020. 7. 5.

함수란?

함수(function)란 하나의 특별한 목적의 작업을 수행하기 위해 독립적으로 설계된 코드의 집합으로 정의할 수 있습니다.

 

C++ 프로그램에서 함수는 특정 작업을 캡슐화하는 데 유용하게 사용됩니다.

 

함수를 사용하는 이유

함수를 사용하는 가장 큰 이유는 바로 반복적인 프로그래밍을 피할 수 있기 때문입니다.

 

프로그램에서 특정 작업을 여러 번 반복해야 할 때는 해당 작업을 수행하는 함수를 작성하면 됩니다.

 

그리고서 프로그램이 필요할 때마다 작성한 함수를 호출하면 해당 작업을 반복해서 수행할 수 있습니다.

 

 

 

또한, 프로그램을 여러 개의 함수로 나누어 작성하면, 모듈화로 인해 전체적인 코드의 가독성이 좋아집니다.

 

그리고 프로그램에 문제가 발생하거나 기능의 변경이 필요할 때에도 손쉽게 유지보수를 할 수 있습니다.

 

 

 

함수의 크기에 대해서 정확히 명시된 규칙은 없으나, 대략 하나의 기능을 하나의 함수로 만드는 것이 가장 좋습니다.

 

1. 반환 타입(return type) : 함수가 모든 작업을 마치고 반환하는 데이터의 타입을 명시합니다.

 

2. 함수 이름 : 함수를 호출하기 위한 이름을 명시합니다.

 

3. 매개변수 목록(parameters) : 함수 호출 시에 전달되는 인수의 값을 저장할 변수들을 명시합니다.

 

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;

 

}

 

함수의 원형 선언

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

 

 

 

다음 예제는 앞서 살펴본 예제에서 함수의 선언 순서만을 바꾼 예제입니다.

 

예제

#include <iostream>

 

using namespace std;

 

 

 

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;

 

}

 

}

 

 

C++에서는 가장 먼저 main() 함수가 컴파일러에 의해 컴파일됩니다.

 

 

 

위의 예제에서 컴파일러는 main() 함수에 등장하는 SmallNum() 함수를 아직 알지 못하기 때문에 컴파일 오류를 발생시킵니다.

 

따라서 컴파일러에 SmallNum() 함수는 나중에 정의되어 있다고 알려줘야 합니다.

 

그 역할을 하는 것이 바로 함수의 원형을 선언하는 것입니다.

 

 

 

ANSI C에서는 기존 C언어와의 호환성을 유지하기 위해서 함수의 원형을 사용하지 않아도 되지만, C++에서는 반드시 함수의 원형을 사용해야만 합니다.

 

 

 

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

 

문법

반환타입 SmallNum(매개변수목록);

 

 

 

다음 예제는 앞서 살펴본 예제에 함수의 원형 선언을 추가한 예제입니다.

 

이처럼 함수의 원형은 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;

 

}

 

}

 

 

인수 전달 방법

함수를 호출할 때에는 함수에 필요한 데이터를 인수(argument)로 전달해 줄 수 있습니다.

 

이렇게 함수에 인수를 전달하는 방법에는 크게 다음과 같이 두 가지 방법이 있습니다.

 

 

 

1. 값에 의한 전달(call by value)

 

2. 참조에 의한 전달(call by reference)

 

값에 의한 전달(call by value)

값에 의한 전달 방법은 인수로 전달되는 변수가 가지고 있는 값을 함수 내의 매개변수에 복사하는 방식입니다.

 

이렇게 복사된 값으로 초기화된 매개변수는 인수로 전달된 변수와는 완전히 별개의 변수가 됩니다.

 

따라서 함수 내에서의 매개변수 조작은 인수로 전달되는 변수에 아무런 영향을 미치지 않습니다.

 

 

 

예제

void Local(int);

 

 

 

int main(void)

 

{

 

int var = 10;

 

cout << "변수 var의 초깃값은 " << var << "입니다." << endl;

 

 

 

Local(var);

 

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

 

return 0;

 

}

 

 

 

void Local(int num)

 

{

 

num += 10;

 

}

 

 

위의 예제에서 Local() 함수의 매개변수 num는 인수로 변수 var의 값을 전달받습니다.

 

따라서 함수 내에서 매개변수 num의 값을 아무리 변경하더라도 원래 인수로 전달된 변수 var의 값은 절대 변경되지 않습니다.

 

참조에 의한 전달(call by reference)

참조에 의한 전달 방법은 인수로 전달된 변수의 값을 복사하는 것이 아닌, 원본 데이터를 직접 전달하는 것입니다.

 

C언어에서는 이러한 작업을 포인터를 사용하여 인수로 전달된 변수의 주소값을 전달합니다.

 

하지만 C++에서는 이러한 작업을 참조자(reference)를 사용하여 전달할 수 있습니다.

 

예제

void Local(int&);

 

 

 

int main(void)

 

{

 

...

 

Local(var);

 

...

 

}

 

 

 

void Local(int& num)

 

{

 

num += 10;

 

}

 

위의 예제에서 Local() 함수의 매개변수 num는 인수로 변수 var의 참조를 전달받습니다.

 

따라서 함수 내에서 참조자 num의 값을 변경하면 원래 인수인 변수 var의 값도 같이 변경됩니다.

 

main() 함수의 인수 전달

main() 함수는 프로그램이 실행되면 제일 먼저 자동으로 호출되는 함수입니다.

 

이러한 main() 함수도 함수이기 때문에 인수를 전달받을 수도 있고, 반환값을 가질 수도 있습니다.

 

 

 

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

 

원형

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

 

 

 

main() 함수의 첫 번째 인수인 int형 변수 argc는 인수로 전달되는 문자열의 개수를 명시합니다.

 

두 번째 인수 char형 포인터의 포인터인 argv는 인수로 전달된 각각의 문자열이 포함되는 배열을 가리킵니다.

 

재귀 호출(recursive call)

재귀 호출(recursive call)이란 함수 내부에서 함수가 자기 자신을 또다시 호출하는 것을 말합니다.

 

이러한 재귀 호출은 자기가 자신을 계속해서 호출하므로, 끝없이 반복될 것입니다.

 

따라서 함수 내에 재귀 호출을 중단하도록 조건이 변경될 명령문을 반드시 포함해야 합니다.

 

 

 

프로그래밍을 처음 접하는 사람들은 이러한 재귀 호출이 왜 필요한가에 대해 이해하기 힘들 수도 있습니다.

 

하지만 재귀 호출은 알고리즘이나 자료 구조론에서는 매우 중요한 개념 중 하나입니다.

 

또한, 재귀 호출을 사용하면 복잡한 문제도 매우 간단하게 논리적으로 접근하여 표현할 수 있습니다.

 

재귀 호출의 개념

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

 

예제

int Sum(int n)

 

{

 

int result = 0;

 

for (int i = 1; i <= n; i++)

 

{

 

result += i;

 

}

 

return result;

 

}

 

 

 

위의 예제에서 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. n1이 아니면, n1부터 (n-1)까지의 합을 더한 값을 반환함.

 

2. n1이면, 그냥 1을 반환함.

 

 

의사 코드(pseudo code)란 특정 프로그래밍 언어의 문법에 맞춰 작성된 것이 아닌, 일반적인 언어로 알고리즘을 표현한 코드를 의미합니다.

 

 

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

 

예제

int RecursiveSum(int n)

 

{

 

if (n == 1) // n1이면, 그냥 1을 반환함.

 

{

 

return 1;

 

}

 

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

 

}

 

위의 예제에서 만약 if 문이 없다면, 이 메소드의 재귀 호출은 무한히 반복될 것입니다.

 

이처럼 재귀 호출이 무한히 반복되면, 해당 프로그램은 실행 직후 스택 오버플로우(stack overflow)에 의해 종료될 것입니다.

 

따라서 if 문처럼 재귀 호출을 중단하기 위한 조건문을 반드시 포함해야 합니다.

 

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

 

 

이처럼 재귀 호출은 다양한 알고리즘을 표현한 의사 코드를 그대로 코드로 옮길 수 있게 해주므로, 직관적인 프로그래밍을 하는 데 많은 도움을 줍니다.

 

함수 포인터(function pointer)

프로그램에서 정의된 함수는 프로그램이 실행될 때 모두 메인 메모리에 올라갑니다.

 

이때 함수의 이름은 메모리에 올라간 함수의 시작 주소를 가리키는 포인터 상수(constant pointer)가 됩니다.

 

이렇게 함수의 시작 주소를 가리키는 포인터 상수를 함수 포인터(function 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 << "사칙연산(+, -, *, /)만을 지원합니다."; }

 

break;

 

}

 

 

 

result = Calculator(num1, num2, calc);

 

cout << "사칙 연산의 결과는 " << result << "입니다.";

 

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); }

 

위의 예제는 함수 포인터를 사용하여 사용자의 입력에 따라 4개의 사칙연산 함수 중 하나를 선택합니다.

 

이렇게 선택된 함수를 함수 포인터를 사용하여 Calculator() 함수에 인수로 전달하고 있습니다.

 

포인터를 초기화할 때 0이나 NULL을 대입하여 초기화한 포인터를 널 포인터(null pointer)라고 합니다.

 

널 포인터는 아무것도 가리키지 않는 포인터라는 의미입니다.

 

함수 포인터의 표기법

앞선 예제에서 사용한 함수 포인터는 다음과 같습니다.

 

예제

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 Calculator(double , double, Arith);

 

 

 

int main(void)

 

{

 

Arith calc = NULL; // 함수 포인터 선언

 

double num1 = 4, num2 = 5, result = 0;

 

char oper = '+';

 

...

 

}

 

...

 

double Calculator(double num1, double num2, Arith func) { return func(num1, num2); }

'C++' 카테고리의 다른 글

C++ 범위  (0) 2020.07.05
C++ 함수  (0) 2020.07.05
C++ 구조체  (0) 2020.07.05
C++ 문자열  (0) 2020.07.05
C++ 배열 & 포인터  (0) 2020.07.05

댓글