[C++] 함수

최윤서·2024년 7월 1일

C++ functions

함수는 각각의 기능을 수행하기 위한 부분들로 코드를 나누어 구조화 하게 해준다.
C++에서, 함수는 이름을 가지고 있는 하나의 코드 그룹이다. 그리고 함수는 프로그램의 특정 부분에서 실행될 수 있다. 가장 흔한 구조는 아래와 같다.

type name ( parameter1, parameter2, ...) { statements }
  • type: 함수에서 리턴되는 값의 타입을 보여준다.
  • name: 함수의 이름이다.
  • parameters(필요한 경우): 함수를 호출하면서 호출 위치에서 함수로 값을 넘겨주기 위한 목적으로 사용된다. 각각의 파라미터는 식별자(타입)과 함께 따라온다. 각가의 파라미터는 ,(콤마)로 구분되며, 보통의 변수 선언과 비슷하게 생겼다. 그리고 파라미터들은 함수 안에서 거의 그 함수의 로컬변수인 것 처럼 사용된다.
  • statement: 함수의 몸통 부분으로, 함수가 실제 하는 일을 나타낸다.
int addition (int a, int b)
{
  int r;
  r=a+b;
  return r;
}

int main ()
{
  int z;
  z = addition (5,3);
  cout << "The result is " << z;
 }

이 프로그램은 두 가지 함수로 이루어져있다. C++에서는 함수가 몇 개 있든 main부터 실행하기 시작한다. main 함수는 자동으로 호출되는 유일한 함수이고, 다른 함수들은 main 안에서 호출되어야 실행될 수 있다.

위 메인함수는 z를 5와 3을 파라미터로 받은 리턴값 r을 변수 z에 저장한다.

함수의 리턴값은 함수가 호출 된 위치로 그 값과 프로그램의 실행 흐름을 넘겨준다. 리턴값은 함수의 앞에 쓰여있는 리턴 타입과 일치해야 한다.

메인 함수의 리턴값

메인함수는 int main ()으로, 리턴값이 정수로 정해져있지만 사실 보통은 retun statement가 없어도 return 0;을 만난다고 컴파일러가 생각하고 함수를 끝낸다. 메인에서 0이 반환되면 함수는 프로그램이 성공적으로 종료된 것으로 해석한다.

다른 함수는 비록 리턴값이 전혀 사용되지 않는다 해도 무조건 적절한 리턴값을 가지고 있어야 한다.

파라미터 (매개변수)

함수의 파라미터는 무조건 '값'으로 전달된다. 즉, 함수를 호출할 때 함수에 전달되는 것은 호출 시점의 인수의 '값'이며, 함수에 들어갈 때 매개변수로 표시된 변수에 복사된 '값'이다. 원본이 아니다.

int addition (int a, int b)
{
  int r;
  r=a+b;
  return r;
}

int main ()
{
  int x = 5;
  int y = 3;
  int z;
  z = addition (x, y);
  cout << "The result is " << z;
  cout << x << ' ' << y;
 }

이 함수의 경우 addition 함수에 들어가는 것은 x와 y가 가지고 있던 값의 복사본이다.
이 값은 (5와 3) 함수의 정의 부분에서 매개변수를 초기화하는 데 사용된다. (a에게 x, b에게 y를 전달)
즉, 변수 x와 y 그 자체는 함수에 전달되지 않고, 함수 호출 순간 x와 y가 가진 값의 복사본만 전달된다.

원본 참조 방법

만약 원본에도 영향을 주고 싶다면 어떻게 해야할까? 매개변수를 가져올 때 참조로 선언하면 된다.
C++에서는 매개변수 타입 뒤에 & 기호를 붙여서 참조 선언을 할 수 있다.

참조선언을 하게 되면 매개변수를 넘겨줄 때 복사본이 아니라 변수 그 자체를 넘겨주는 것이다. 원본과 매개변수는 열결되며, 함수 안의 로컬 변화도 실제 원본에 영향을 주게 된다.

이렇게 되면 a와 b는 x와 y를 부르는 새로운 별명이라고 생각하면 된다.

int addition (int &a, int &b)
{
  int r;
  a = 10;
  b = 8;
  r=a+b;
  return r;
}

int main ()
{
  int x = 5;
  int y = 3;
  int z;
  z = addition (x, y);
  cout << "The result is " << z;
  cout << x << ' ' << y;
 }

따라서 addition 함수는 매개변수를 참조로 선언해 변수가 참조로 전달되었기 때문에 원본 x, y에는 영향을 주지 않지만 함수 호출 시 초기화 한 해당 변수의 값에 반영된다. 출력값은 18, 10, 8이다.

효율적으로 참조 사용하기와 const 참조

값으로 매개변수를 취하는 함수에서는 사본이 만들어져 전달된다. 이 떄 매개변수가 정수가 아니라 복사하는 데 비용이 많이 드는 문자열이라면 어떻게 될까?

string concatenate (string a, string b)
{
  return a+b;
}

이 함수에서는 문자열 a, b를 모두 복사해야 하는데 문자열의 길이가 얼마인지 확언할 수 없고, 그렇기 때문에 대량의 데이터를 복사하면서 오버헤드가 발생할 가능성이 있다.

string concatenate (string& a, string& b)
{
  return a+b;
}

이와 같이 참조에 의한 인수로 변경하면 사본이 필요하지 않아서 복사할 필요가 없다. 저 문자열을 가리키는 포인터만 전송하면 되기 때문에 비용도 많이 들지 않는다.

그런데 보통 참조로 변수를 전달하는 이유는 원본 변수를 수정할 목적이 있기 때문이다. 그렇기 때문에 참조 매개변수로 보낸 문자열이 함수에 의해 수정되지 않도록 보장해야 한다.

string concatenate (const string& a, const string& b)
{
  return a+b;
}

이렇게 하면 전달된 매개변수를 이 함수에서 상수로 지정하여 a와 b를 모두 수정할 수 없게 된다. 그러나 접근은 a와 b에 모두 참조로 하기 때문에 효율성은 좋아진다. 실제로 복사본을 만들 필요 없이 참조만 하면서 원본이 수정되지 않게 할 수 있는 것이다.

const참조는 이렇게 큰 데이터를 다룰 때는 효율적이지만 정수같이 간단한 것을 다룰 땐 오히려 비효율적일 수도 있기 때문에 잘 생각하고 사용해야 한다.

Inline functions

함수를 호출하는 것은 보통 특정한 오버헤드를 필요로 한다.
그래서 매우 짧거나 간단한 함수들은 함수를 따로 선언하고 호출하는 것 보다 그냥 필요한 위치에 그 코드를 삽입하는 게 나을 수도 있다.

inline string concatenate (const string& a, const string& b)
{
  return a+b;
}

함수 선언 전에 'inline'이라는 specifier를 붙여서 컴파일러가 호출해서 사용하는 일반 함수보다 in-place 확장을 하는 함수를 사용하고 싶어한다는 걸 알 수 있게 해준다. 이것은 함수 자체의 행동을 바꾼다기보다는 컴파일러에게 함수가 호출되는 각 지점에 함수가 삽입되어야 한다는 점을 알려주는 것이다.

즉, 함수 호출로 함수 위치를 알아내서 함수로 갔다가 다시 돌아오는 번거로운 과정 없이 함수 호출이 함수 자체의 내용 복사본으로 대체되어 오버헤드가 제거된다.

인라인 함수를 알고 있으면 좋지만 최신 컴파일러는 함수를 적절하게 인라인 하기 때문에 inline 키워드를 꼭 사용하지는 않아도 된다고 한다.

Default values in parameters

C++에서 함수는 상황에 따라 적합한 파라미터를 가질 수 있다.
여러개의 파라미터, 즉 매개변수가 필요한 함수의 경우 필요한 파라미터 n개가 있을 때, n개 미만을 가지고도 작동이 가능하다는 뜻이다.

이러한 경우, 함수는 파라미터에 대한 기본 값을 가지고 있어서 파라미터 값이 호출 시에 넘어오지 않은 경우 그 값을 가지고 실행할 수 있다.

#include <iostream>
using namespace std;

int divide (int a, int b=2)
{
  int r;
  r=a/b;
  return (r);
}

int main ()
{
  cout << divide (12) << '\n';
  cout << divide (20,4) << '\n';
  return 0;
}

위의 경우 6과 5가 출력된다.

divide (12)

는 함수 divide에 12만을 파라미터로 넘겨주기 때문에, 이 경우 divide의 파라미터 b의 기본값인 2가 적용되어 6이 출력되게 된다.

divide (20,4)

그러나 두 번째 호출에서 20과 4를 넘겨주어 divide에 필요한 두 가지 파라미터를 모두 전달했을 때는, 20과 4를 활용하여 결과값은 5가 된다.

함수 선언하기와 디폴트 매개변수 (Declaring functions)

C++에서, 특정 표현들은 선언하기 전에는 사용할 수 없다. 예를들어 정수 변수 x를 선언하기도 전에 x를 사용할 수는 없다. 함수도 마찬가지이다.

함수가 선언되기 전에는 사용할 수 없다. 즉, 함수는 그것을 호출할 수 있는 함수보다 먼저 (코드 줄 상으로) 선언되어야 한다는 것이다. 만약 main함수가 그 안에서 호출되는 함수보다 더 위에 있는 경우, 함수가 선언되기도 전에 호출하는 것이므로 함수를 찾을 수 없다.

그러나 함수의 프로토타입은 함수를 완벽하게 정의하기 전에도 선언될 수 있다. 함수의 파라미터와 이름 등 syntax는 그대로 쓰되, 몸통 블록 대신 ;를 남기는 것이다.

int protofunction (int first, int second);
int protofunction (int, int);

위와 같이 타입만 파라미터 칸에 남겨두어도 되지만, 이름을 포함하는 것이 함수의 가독성에는 더 도움이 된다.

#include <iostream>
using namespace std;

void odd (int x);
void even (int x);

int main()
{
  int i;
  do {
    cout << "Please, enter number (0 to exit): ";
    cin >> i;
    odd (i);
  } while (i!=0);
  return 0;
}

void odd (int x)
{
  if ((x%2)!=0) cout << "It is odd.\n";
  else even (x);
}

void even (int x)
{
  if ((x%2)==0) cout << "It is even.\n";
  else odd (x);
}

이 예시에서 맨 윗줄의 포로토타입 함수는 호출에 필요한 부분을 이미 갖추고 있다. 파라미터의 타입, 이름, 리턴타입까지 모두 있다. 프로토타입 함수가 제 자리에 있으면 그 함수가 완벽하게 정의되기 전에도 호출될 수 있다. 위처럼 main 이후에 함수가 몸통까지 완전히 정의되어도 괜찮다는 뜻이다.

<조건>

  • 매개변수의 개수가 같을 것
  • 매개변수의 자료형이 같을 것
  • 반환 형태가 같을 것

재귀함수 - Recursivity

재귀함수는 함수가 스스로 호출되어야 하는 함수이다. 대표적으로 팩토리얼 계산이 있다.
함수가 리턴 되는 과정에서 자기자신을 호출하기 때문에 함수의 처음으로 돌아가서 계산한다.

함수를 실행하는 과정에서 괄호 안에서 계속 함수를 펼치면서 실행하고, 마지막에서 더이상 재귀 값으로 반환하지 않으면 호출된 위치로 돌아가면서 값을 완성한다.

// factorial calculator
#include <iostream>
using namespace std;

long factorial (long a)
{
  if (a > 1)
   return (a * factorial (a-1));
  else
   return 1;
}

int main ()
{
  long number = 9;
  cout << number << "! = " << factorial (number);
  return 0;
}

재귀함수는 스택 오버플로우가 발생되지 않도록 조건을 잘 거는 것이 중요하다.

profile
일 잘 하고싶은 기개디자이너

0개의 댓글