함수의 매개변수

매개변수(Parameter)란 함수가 수신하고, 수신 받은 값을 함수 내부에서 사용할 수 있게 해주는 변수입니다.

매개변수 선언은 함수를 정의하면서 소괄호() 안에 매개 변수를 선언할 수 있습니다.
이때 형식, 크기 및 매개 변수에 저장된 값의 식별자를 지정합니다.

예를 들면

int Sum(int a, int b)

Sum이라는 함수는 int 자료형을 가진 변수 2개를 매개변수로 받는것입니다.
Sum함수를 호출 할 때에는 항상 int 자료형을 가진 변수 2개를 넘겨주어야 합니다.

매개변수의 지역 범위는 함수가 호출될 때부터 함수가 끝날때까지 입니다.

매개변수를 인자라고도 부릅니다.
하지만 인수라는 단어를 사용하기도 하는데, 인수란 함수를 호출할 때 함수로 전달하는 값을 인수라고 합니다.

매개변수의 전달 방식

인수에 저장된 값을 매개변수에 전달할 때 3가지 방식 중 하나를 선택할 수 있습니다.

  • 값으로 전달(pass by value)
  • 참조로 전달(pass by reference)
  • 주소로 전달(pass by address)

값으로 전달

포인터를 사용하지 않고, 인수를 전달하는 경우 값으로 전달하게 됩니다.
이 경우 인수의 값은 함수 매개 변수의 값으로 복사됩니다.

값으로 전달을 사용하면 함수에서 매개변수의 값을 아무리 수정해도 원본 데이터가 손상, 변경, 제거가 되는 위험이 없습니다.
하지만 매번 값을 복사해야하기 때문에 함수가 여러번 호출 될 경우 큰 비용이 들어 성능이 저하될 수 있습니다.

#include <iostream>

int func(int x)
{
    x = 30;

    std::cout << x << std::endl; // 30
}

int main()
{
    int a = 4;

    func(4);

    std::cout << a << std::endl; // 4
}

이 경우 매개변수에 저장된 값인 x를 수정해도 인수a는 값에 영향을 받지 않습니다.

참조로 전달

참조자를 사용해 매개변수를 선언하면 인수를 전달하는 경우 참조로 전달하게 됩니다. 이 경우 인수로 전달받은 값을 복사하는 것이 아닌, 인수를 참조하게 됩니다.

일반적으로 함수 내부에서 함수 밖의 데이터를 조작하기 위해서 사용합니다.
이를 사용해 함수에서 여러 값을 반환할 수 있습니다.

참조로 전달을 사용하면 인수의 복사본이 만들어지지 않으므로 값으로 전달에 비해 비용이 적을 수 있습니다.
하지만 참조의 경우 자료형의 크기가 8byte이므로 인수의 자료형 크기가 작다면 인수에 비해 비용이 커질 수 있습니다.

참조로 전달을 하기 위해서는 매개변수를 선언할 때 자료형과 변수명 사이에 &기호를 사용한 참조로 선언하면 됩니다.

#include <iostream>

void func(int& x)
{
    x = 30;

    std::cout << x << std::endl; // 30
}

int main()
{
    int a = 4;

    func(a);

    std::cout << a << std::endl; // 30
}

값으로 전달했을 때와 다르게 a의 변수의 값이 변경됩니다.

매개변수를 값으로 전달하고싶지 않고, 참조를 통해 인수의 값을 변경해선 안된다면 값을 변경하지 않을 수 있는 방법이 있습니다.
const 참조를 사용하는 것입니다.

const 참조는 값의 변경은 불가능 하지만 원본 데이터의 접근및 수정은 가능합니다.
또한 변수가 참조를 통해 값이 변경되는 것을 허용하지 않는 참조이므로, 함수 호출을 한 사용자에게 인수를 변경하지 않는다는 것을 호출자에게 보장합니다.

#include <iostream>

void func(const int& x)
{
    x = 30; // compile error

    std::cout << x << std::endl;
}

const 참조 매개변수의 값을 변경하려 하면 위와 같이 컴파일 에러가 발생합니다.

const 참조 매개변수가 아닌 경우 const 인수를 전달할 수 없지만 const 참조 매개변수의 경우 non-const 및 const 인수를 전달 할 수 있습니다.

포인터와 배열을 참조로 전달해 값을 변경하도록 할 수 있습니다.

void func(int*& x);
void func(int (&arr)[]);
void func(vector<int>& arr);

C스타일 배열의 경우 매개 변수에서 배열 크기를 명시적으로 정의해야합니다.

주소로 전달

포인터를 사용해 매개변수를 선언하면 인수를 전달하는 경우 주소로 전달하게 됩니다. 이 경우 인수의 주소를 전달받아 주소의 복사본을 저장합니다.
이때 주소는 값으로 전달된 것이므로, 매개변수가 가리키는 주소를 변경해도 인수가 가리키는 주소가 변경되지 않습니다.
하지만 주소를 역참조해 값을 변경 할 수 있기 때문에 원본 데이터에 대한 접근 및 수정이 가능합니다.

주소로 전달을 사용하면 인수의 원래 데이터에 대한 복사본이 만들어지지 않으므로 값으로 전달에 비해 비용이 적을 수 있습니다.
하지만 리터럴과 표현식은 주소가 없으므로 사용할 수 없습니다.

주소로 전달을 하기 위해서는 매개변수를 선언할 때 자료형과 변수명 사이에 *기호를 사용한 포인터로 선언하면 됩니다.

#include <iostream>

void func(int* x)
{
    *x = 30;

    std::cout << &x << std::endl; // x의 주소
    std::cout << *x << std::endl; // 30

    x = nullptr;

    std::cout << x << std::endl; // x가 가리키는 주소
}

int main()
{
    int a = 4;
    int* b = &a;

    func(b);

    std::cout << &b << std::endl; // b의 주소
    std::cout << *b << std::endl; // 30
    std::cout << b << std::endl;  // b가 가리키는 주소
}

xb가 가리키는 주소를 복사해 저장한것이므로 x가 가리키는 주소를 수정해도 b가 가리키는 주소가 변경되지 않습니다. bx가 가리키는 주소가 같아 x에서 역참조하고 값을 수정한다면 b에서 역참조한 값도 같이 변경됩니다.

배열의 경우 시작 주소이므로 길이가 얼만큼인지 추가로 매개변수가 필요합니다.
배열의 주소를 전달하는 경우는 다음과 같습니다.

void func(int* arr, int length);

디폴트 매개변수

디폴트 매개변수(default parameter)는 함수를 호출할 때 매개변수를 전달하지 않았을 경우 사용하게 될 기본값을 의미합니다.

디폴트 매개변수를 설정하면 함수 호출을 할 때 인수를 생략할 수 있다는 장점을 가집니다.
값을 전달하는 것은 컴파일러에 의해 처리됩니다.

함수를 선언 할 때 매개변수의 기본 값을 설정해주면 됩니다.

이때 주의해야할 사항들이 있습니다.

  • 디폴트 인수는 함수의 원형에만 지정할 수 있습니다.
  • 디폴트 인수는 가장 오른쪽부터 시작하여 순서대로만 지정할 수 있습니다.
  • 가운데 인수들만 별도로 디폴트 인수를 지정할 수는 없습니다.

디폴트 매개변수를 지정한 함수 원형의 선언 형태는 다음과 같습니다.

반환 자료형 함수 이름(자료형 매개변수명 = 기본 값, 자료형 매개변수명 = 기본 값);
int Sum(int a = 1, int b = 5);

디폴트 매개변수는 일반 매개변수와 같이 선언해야 할 경우 일반 매개변수 다음에 선언해야 하는데 형태는 다음과 같습니다.

int Sum(int a, int b = 5);  // 컴파일 성공
int Sum(int a = 1, int b);  // 컴파일 에러 발생

디폴트 매개변수를 가진 함수를 호출 할 때 함수의 매개변수 수보다 적은 매개변수를 전달하는 경우 가장 왼쪽의 매개변수부터 전달됩니다.

다음과 같은 형태로 호출할 수 있습니다.

int Sum(int a = 1, int b = 5);

Sum();          // a와 b는 기본 값이 전달 됩니다.
Sum(2);         // a는 2가 b는 기본 값이 전달 됩니다.
SUm(2, 3);      // a는 2가 b는 3이 전달 됩니다.

가변인자

가변인자(Variadic arguments / Variable argument)는 가변 인수 목록(Variable Argument Lists)이라고도 불립니다.

가변인자란 함수를 선언 할 때 인자의 개수를 정하지 않고 유동적으로 정해지지 않은 개수의 인자를 받을 수 있는 기능입니다.

이 가변인자를 사용하는 함수를 가변 함수(Variadic functions)라고합니다.

가변인자를 사용하는 것은 C 스타일과 C++ 스타일로 두가지가 있습니다.

C 스타일

C 스타일 가변인자 함수는 stdarg.h 헤더를 사용하며, 해당 헤더에 정의된 매크로를 사용해야합니다.

가변인자는 최소 1개 이상의 고정 인수가 있어야하고, 마지막 인자에 줄임표(...)가 있어야 합니다.

C 스타일의 가변 인자 리스트는 그렇게 안전하지 안전하지 않습니다.
매개변수의 개수를 알 수 없고, 함수를 호출한 측에서 첫 번쨰 인수에 정확한 개수를 입력하길 기대해야 합니다.
va_arg()매크로를 사용해 값을 해석하는데, 얼마든지 다른 타입으로 해석될 수 도 있으며, 해석한 타입이 정확한지 검증 할 수 없어 인수 타입을 알 수 없습니다.

이런 이유로 C 스타일의 가변 인자 리스트는 가능하면 사용하지 않는 것이 좋습니다.

C++ 스타일

C++ 스타일 가변인자 함수는 C++11에서 도입된 가변 길이 템플릿(variadic template)과 파라미터 팩(parameter pack)을 사용할 수 있습니다.

가변 길이 템플릿을 사용하여 여러 개의 인자를 받아 화면에 출력하는 예시를 들어보겠습니다.
예시는 다음과 같습니다.

#include <iostream>

template <typename T>
void print(T arg) {
  std::cout << arg << std::endl;
}

template <typename T, typename... Types>
void print(T arg, Types... args) {
  std::cout << arg << ", ";
  print(args...);
}

int main() {
  print(1, 3.1, "abc");
  print(1, 2, 3, 4, 5, 6, 7);
}

템플릿의 typename 뒤에 온 줄임표(...)를 템플릿 파라미터 팩이라고 부릅니다.
함수에 인자로 줄임표(...)가 오는 것을 함수 파라미터 팩이라고 부릅니다.
이 파라미터 팩들은 0 개 이상의 함수 인자를 나타냅니다.

함수를 재귀적으로 호출하기 위해 템플릿 함수인 void print(T arg)를 가변 길이 템플릿 함수보다 앞에서 정의했습니다.
이렇게 재귀 함수 형태로 구성해야하기 때문에 반드시 재귀 종료 함수를 따로 만들어주어야 하는 단점이 있습니다.

위의 코드를 실행하면 print(1, 3.1, "abc")에서 어떤 print 함수를 호출할지 찾는데, 첫 번째 print 함수의 경우 인자를 1개만 받으므로 호출하지 않습니다.
그러므로 두 번째 가변 길이 템플릿인 print 함수가 호출됩니다.

print 함수의 코드가 실행되며 한번 더 print 함수를 호출하는데, 가변 길이 템플릿인 print 함수가 재귀적으로 한번 더 호출됩니다.

이때 코드는 다음과같은 모습이 될 것입니다.

void print(int arg, double arg2, const char* arg3) {
  std::cout << arg << ", ";
  print(arg2, arg3);
}

재귀적으로 호출된 함수에서는 다음과 같은 모습이 될 것입니다.

void print(double arg, const char* arg2) {
  std::cout << arg << ", ";
  print(arg2);
}

똑같이 print 함수의 코드가 실행되며 print 함수를 호출하는데, 이번에 어떤 함수를 호출할지 모호해보입니다.
파라미터 팩은 0개 이상의 인자들을 나타내므로 print 함수 두 가지 다 호출이 가능하기 때문입니다.

만약 가변 길이 템플릿 함수가 호출이 된다면 파라미터 팩은 아무것도 전달되지 않은 상태로 호출될 것입니다.
하지만 파라미터 팩이 없는 함수의 우선순위가 높아 첫 번째 print 함수가 호출됩니다.

Fold 형식

기존의 가변 길이 템플릿은 재귀 함수 형태로 구성해 재귀 종료 함수도 따로 구현해야하는 단점이 있었습니다.
이는 코드의 복잡도를 늘립니다.

C++17에서 도입된 Fold 형식을 사용한다면 훨씬 간단하게 표현할 수 있습니다.

예시는 다음과 같습니다.

#include <iostream>

template <typename... Ints>
int sum(Ints... nums) {
  return (... + nums);
}

int main() {
  std::cout << sum(1, 3, 2, 4, 7) << std::endl; // 17
}

(... + nums)return ((((1 + 3) + 2) + 4) + 7); 같이 컴파일러에서 해석됩니다.

위와 같은 형태를 단항 좌측 Fold(Unary left fold)라고 부릅니다.

C++17에서 지원하는 Fold 방식의 종류로 아래 표와 같이 총 4가지가 있습니다.

이름 Fold 방식 실제 전개 형태  
  (E op …) 단항 우측 Fold $(E_1\ op\ (…\ op\ (E_{N-1}\ op\ E_N)))$
  (… op E) 단항 좌측 Fold $(((E_1\ op\ E_2)\ op\ …)\ op\ E_N)$
  (E op … op I) 이항 우측 Fold $(E_1\ op\ (…\ op\ (E_{N-1}\ op\ (E_N\ op\ I))))$
  (I op … op E) 이항 좌측 Fold $((((I\ op\ E_1)\ op\ E_2)\ op\ …)\ op\ E_N)$

I는 초기값을 의미하며 파라미터 팩이 아닙니다.
op 자리에는 대부분의 이항 연산자들이 포함될 수 있습니다.

이항 Fold의 경우 예시는 다음과 같습니다.

#include <iostream>

template <typename Int, typename... Ints>
Int diff_from(Int start, Ints... nums) {
  return (start - ... - nums);
}

int main() {
  std::cout << diff_from(100, 1, 4, 2, 3, 10) << std::endl; // 80
}

return (start - ... - nums);같이 컴파일러에서 해석됩니다.

참조

https://modoocode.com/290

Date:     Updated:

카테고리:

태그:

Cpp 카테고리 내 다른 글 보러가기

댓글남기기