Skip to content

9. C++ 함수(C++ Function)


1. 키워드

  • 참조자(Reference)
  • 디폴트 인수(Default Argument)
  • 함수 오버로딩(Function Overloading)과 함수 시그니처(Function Signature)
  • 인라인 함수(Inline Function)와 inline 키워드
  • 매크로 함수(Macro Function)


2. 참조자

  • C++에서는 특정 변수의 실제 이름 대신 사용할 수 있는 참조자라는 새로운 기능이 추가되었다.
  • 이러한 참조자는 크기가 큰 구조체와 같은 데이터를 함수의 인수로 전달해야 할 경우에 사용할 수 있다.
  • 또한, C++의 클래스를 설계할 때에도 자주 사용된다.


1) 참조자의 선언

  • C++에서 참조자는 다음과 같은 문법으로 선언한다.


int 변수이름;             // 변수의 선언
int &참조자이름 = 변수이름;  // 참조자 선언


  • 이때 &는 주소 연산자가 아닌 타입을 식별하기 위해 사용하는 식별자로 사용된 것이다.
  • 즉, int &int 타입 변수에 대한 참조를 의미한다.
  • 이렇게 선언된 참조자는 대상 변수와 같은 메모리 위치를 참조하게 된다.
  • 참조자를 선언할 때에는 다음과 같은 사항에 주의해야 한다.


1] 참조자의 타입은 대상이 되는 변수의 타입과 일치해야 한다.

2] 참조자는 선언과 동시에 초기화되어야 한다.

3] 참조자는 한 번 초기화되면, 참조하는 대상을 변경할 수 없다.


  • 다음의 코드에서 참조자를 이용해 증가 연산을 수행하면, 참조 변수뿐만 아니라 대상 변수도 같이 변경됨을 알 수 있다.


#include <iostream>
using namespace std;

int main(void) {
    int x = 10;  // 변수의 선언
    int &y = x;  // 참조자 선언

    cout << "x: " << x << ", y: " << y << endl;

    y++;  // 참조자를 이용한 증가 연산
    cout << "x: " << x << ", y: " << y << endl;

    cout << "x의 주소값: " << &x << ", y의 주소값: " << &y << endl;

    return 0;
}

// x: 10, y: 10
// x: 11, y: 11
// x의 주소값: 0x16b433478, y의 주소값: 0x16b433478


2) 함수의 인수로서 전달

  • C++에서 참조자는 주로 함수에 인수를 전달할 때 사용된다.
  • 함수가 참조자를 인수로 전달받으면, 참조자가 참조하고 있는 실제 변수의 값을 함수 내에서 조작할 수 있다.


  • 다음의 코드는 참조자를 사용하여 두 변수의 값을 서로 맞바꾸는 것이다.


#include <iostream>
using namespace std;

void Swap(int &, int &);

int main(void) {
    int num1 = 3, num2 = 7;

    cout << "변경 전 num1의 값은 " << num1
         << "이며, num2의 값은 " << num2 << "입니다." << endl;

    Swap(num1, num2);
    cout << "변경 후 num1의 값은 " << num1
         << "이며, num2의 값은 " << num2 << "입니다." << endl;

    return 0;
}

void Swap(int &x, int &y) {
    int temp;

    temp = x;
    x = y;
    y = temp;
}

// 변경 전 num1의 값은 3이며, num2의 값은 7입니다.
// 변경 후 num1의 값은 7이며, num2의 값은 3입니다.


  • 위와 같은 참조에 의한 전달은 참조자뿐만 아니라 포인터를 사용해도 똑같은 결과를 얻을 수 있다.
  • 포인터를 사용하는 방법과 참조자를 사용하는 방법 모두 결과는 같으며, 구문 형태 상의 차이점만이 존재한다.


  • 다음의 코드는 포인터를 사용하여 두 변수의 값을 서로 맞바꾸는 것이다.


#include <iostream>
using namespace std;

void Swap(int *, int *);

int main(void) {
    int num1 = 3, num2 = 7;
    int *ptr_num1 = &num1, *ptr_num2 = &num2;

    cout << "변경 전 num1의 값은 " << num1
         << "이며, num2의 값은 " << num2 << "입니다." << endl;

    Swap(ptr_num1, ptr_num2);
    cout << "변경 후 num1의 값은 " << num1
         << "이며, num2의 값은 " << num2 << "입니다." << endl;

    return 0;
}

void Swap(int *ptr_x, int *ptr_y) {
    int temp;

    temp = *ptr_x;
    *ptr_x = *ptr_y;
    *ptr_y = temp;
}

// 변경 전 num1의 값은 3이며, num2의 값은 7입니다.
// 변경 후 num1의 값은 7이며, num2의 값은 3입니다.


  • C++에서 함수의 인수로 참조자를 사용하는 방법의 특징은 다음과 같다.


1] 함수 내에서 *(참조 연산자)를 사용하지 않으므로, 함수 내부의 코드가 깔끔하고 직관적이다.

2] 함수의 호출이 값에 의한 전달 방법과 같은 형태가 되어, 코드를 읽기가 쉽지 않다.


  • 따라서 간단한 함수에서는 굳이 참조에 의한 전달을 하지 말고 값에 의한 전달을 사용하는 것이 좋을 수 있다.
  • 또한, 참조 호출이 꼭 필요할 때에는 참조자보다는 포인터를 사용하는 것이 더욱 직관적일 수 있다.
  • 이런 이유로 C++에서 참조자는 크기가 큰 구조체나 클래스를 다룰 때에만 사용하는 것이 좋다.


3) 구조체의 참조

  • C++에서 참조자는 주로 구조체와 같은 사용자 정의 타입을 다룰 때 유용하게 사용된다.
  • 구조체를 참조하는 방법은 변수를 참조하는 방법과 같다.


#include <iostream>
#include <string>
using namespace std;

struct Book {
    string title;
    string author;
    int price;
};

void Display(const Book &);

int main(void) {
    Book web_book = {.title = "HTML과 CSS", .author = "홍길동", .price = 28000};

    Display(web_book);

    return 0;
}

void Display(const Book &bk) {
    cout << "책의 제목은 " << bk.title << "이고, "
         << "저자는 " << bk.author << "이며, "
         << "가격은 " << bk.price << "원입니다." << endl;
}

// 책의 제목은 HTML과 CSS이고, 저자는 홍길동이며, 가격은 28000원입니다.


  • 위의 코드처럼 함수 내부에서 구조체를 직접 변경할 필요가 없을 때는 const 키워드를 사용하여 원본 구조체에 대한 변경을 허용하지 않는 것이 좋다.


  • 다음의 코드는 포인터를 사용하여 구조체를 참조하는 것이다.


#include <iostream>
#include <string>
using namespace std;

struct Book {
    string title;
    string author;
    int price;
};

void Display(const Book *);

int main(void) {
    Book web_book = {.title = "HTML과 CSS", .author = "홍길동", .price = 28000};
    Book *ptr_web_book = &web_book;

    Display(ptr_web_book);

    return 0;
}

void Display(const Book *ptr_bk) {
    cout << "책의 제목은 " << ptr_bk->title << "이고, "
         << "저자는 " << ptr_bk->author << "이며, "
         << "가격은 " << ptr_bk->price << "원입니다." << endl;
}

// 책의 제목은 HTML과 CSS이고, 저자는 홍길동이며, 가격은 28000원입니다.


  • 함수 내에서 참조자를 사용하면 변수나 구조체 등을 직접적으로 다루는 것과 같은 느낌이고, 포인터를 사용하면 주소를 통해 간접적으로 다루는 것과 같은 느낌이다.


3. 디폴트 인수

  • C++에서 새롭게 정의된 디폴트 인수는 기본값이 미리 정의되어 있는 인수를 의미한다.
  • 함수를 호출할 때 인수를 전달하지 않으면, 함수는 자동으로 미리 정의되어 있는 디폴트 인수값을 사용하게 된다.
  • 물론 인수를 전달하여 함수를 호출하면, 디폴트 인수값이 아닌 전달된 인수를 가지고 함수를 호출하게 된다.


1) 디폴트 인수의 설정

  • C++에서 디폴트 인수를 설정할 때에는 다음과 같은 사항에 주의해야 한다.


1] 디폴트 인수는 함수의 원형에만 지정할 수 있다.

2] 디폴트 인수는 가장 오른쪽부터 시작하여 순서대로만 지정할 수 있다.

3] 가운데 인수들만 별도로 디폴트 인수를 지정할 수는 없다.


1. void Display(int x, int y, char ch, int z = 4);        // 가능함
2. void Display(int x, int y, char ch = 'a', int z = 4);  // 가능함
3. void Display(int x, int y = 2, char ch, int z = 4);    // 오류
4. void Display(int x = 1, int y = 2, char ch, int z);    // 오류


2) 디폴트 인수가 설정된 함수의 호출

  • 함수로 전달된 인수는 왼쪽부터 순서대로 매개변수 목록에 대입된다.
  • 따라서 디폴트 인수가 설정된 함수를 호출할 때에는 인수의 전달을 건너뛸 수 없다.


void Display(int x, int y, char ch = 'a', int z = 4);  // 함수의 원형

1. Display(1, 2);          // 가능함 -> display(1, 2, 'a', 4)와 같음
2. Display(3, 4, 'b');     // 가능함 -> display(3, 4, 'b', 4)와 같음
3. Display(5, 6, 'c', 9);  // 가능함 -> display(5, 6, 'c', 8)와 같음
4. Display(7, 8, , 9);     // 오류: 인수 전달은 건너뛸 수 없음
#include <iostream>
using namespace std;

double Multi(double, double n = 2);

int main(void) {
    cout << Multi(3) << endl;     // Multi(3, 2)와 같음 : 3 * 3
    cout << Multi(3, 3) << endl;  // 3 * 3 * 3
    cout << Multi(3, 4) << endl;  // 3 * 3 * 3 * 3

    return 0;
}

double Multi(double x, double n) {
    double result = x;

    for (int i = 1; i < n; i++) {
        result *= x;
    }

    return result;
}

// 9
// 27
// 81


4. 함수 오버로딩

  • 디폴트 인수가 인수의 개수를 달리하여 같은 함수를 호출하는 것이라면, 함수 오버로딩은 같은 이름의 함수를 중복하여 정의하는 것을 의미한다.
  • C++에서 새롭게 추가된 함수 오버로딩은 여러 함수를 하나의 이름으로 연결해 준다.
  • 즉, 함수 오버로딩이란 같은 일을 처리하는 함수를 매개변수의 형식을 조금씩 달리하여, 하나의 이름으로 작성할 수 있게 해주는 것이다.
  • 이와 같은 함수 오버로딩은 객체 지향 프로그래밍의 특징 중 바로 다형성의 구현이다.


1) 함수 시그니처

  • 함수 오버로딩의 핵심은 바로 함수 시그니처에 있다.
  • 함수 시그니처란 함수의 원형에 명시되는 매개변수 리스트를 가리킨다.
  • 만약 두 함수의 매개변수의 개수와 그 타입이 모두 같다면, 이 두 함수의 시그니처는 같다고 할 수 있다.
  • 즉, 함수의 오버로딩은 서로 다른 시그니처를 가지는 여러 함수를 같은 이름으로 정의하는 것이라고 할 수 있다.


2) 함수 오버로딩의 예제

  • C++ 컴파일러는 사용자가 오버로딩된 함수를 호출하면, 그것과 같은 시그니처를 가지는 함수의 함수의 원형을 찾아 호출해 준다.


  • 다음의 코드는 함수의 오버로딩을 이용하여 정의한 Display() 함수의 원형이다.


1. void Display(const char* str, int n);              // 문자열 str을 n번 출력함
2. void Display(const char* str1, const char* str2);  // 문자열 str1과 str2를 연달아 출력함
3. void Display(int x, int y);                        // x * y를 출력함
4. void Display(double x, double y);                  // x / y를 출력함


  • 이제 사용자가 Display() 함수를 호출하면, C++ 컴파일러는 자동으로 같은 시그니처를 가지는 함수의 원형을 찾아 호출한다.


1. Display("C++", 3);               // 1번 Display() 함수 호출 -> "C++C++C++"
2. Display("C++", " Programming");  // 2번 Display() 함수 호출 -> "C++ Programming"
3. Display(3, 4);                   // 3번 Display() 함수 호출 -> 12
4. Display(4.2, 2.1);               // 4번 Display() 함수 호출 -> 2
5. Display(4.2, 3);                 // 3번과 4번 모두 호출 가능 -> 컴파일 오류가 발생함


  • 문제는 Display(4.2, 3);과 같은 함수의 호출로 첫 번째 인수로는 double 타입의 인수를, 두 번째 인수로는 int 타입의 인수를 전달한다.
  • 이와 같은 함수 호출은 void Display(int x, int y);void Display(double x, double y);과 같은 시그니처를 가지는 Display() 함수를 모두 호출할 수 있으므로, 모호한 호출이 된다.
  • C++에서는 오버로딩한 함수의 이러한 모호한 호출을 허용하지 않으며, 이럴 때에는 오류를 발생시킨다.


  • 다음의 코드는 함수 오버로딩을 이용한 것이다.


#include <iostream>
using namespace std;

void Shift(int, int);
void Shift(int, int, int);
void Shift(int, int, int, int);

int main(void) {
    Shift(1, 2);
    Shift(1, 2, 3);
    Shift(1, 2, 3, 4);

    return 0;
}

void Shift(int w, int x) {
    int temp;

    temp = w;
    w = x;
    x = temp;

    cout << w << ", " << x << endl;
}

void Shift(int w, int x, int y) {
    int temp;

    temp = w;
    w = x;
    x = y;
    y = temp;

    cout << w << ", " << x << ", " << y << endl;
}

void Shift(int w, int x, int y, int z) {
    int temp;

    temp = w;
    w = x;
    x = y;
    y = z;
    z = temp;

    cout << w << ", " << x << ", " << y << ", " << z << endl;
}

// 2, 1
// 2, 3, 1
// 2, 3, 4, 1


  • 위의 코드처럼 함수의 오버로딩은 매개변수의 타입뿐만 아니라 매개변수의 개수를 달리해도 작성할 수 있다.


5. 인라인 함수

1) C++에서의 함수 호출 과정

  • 함수가 호출되면 우선 스택에, 함수로 전달할 매개변수와 함께 호출이 끝난 뒤 돌아갈 반환 주소값을 저장하게 된다.
  • 그리고서 프로그램의 제어가 함수의 위치로 넘어와 함수 내에 선언된 지역 변수도 스택에 저장한다.
  • 그때부터 함수의 모든 코드를 실행하게 되고, 실행이 전부 끝나면 반환값을 넘겨 준다.
  • 그 후 프로그램의 제어는 스택에 저장된 돌아갈 반환 주소값으로 이동하여, 스택에 저장된 함수 호출 정보를 제거한다.
  • 이와 같은 일련의 함수 호출 과정이 함수마다 일어나게 된다.


2) 인라인 함수

  • 위와 같이 C++에서 함수의 호출은 꽤 복잡한 과정을 거치므로, 약간의 시간이 걸리게 된다.
  • 이때 함수를 실행하는 시간이 오래 걸린다면, 함수를 호출하는 데 걸리는 시간은 전혀 문제가 되지 않는다.
  • 하지만 함수의 실행 시간이 매우 짧다면, 함수 호출에 걸리는 시간도 부담이 될 수 있다.


  • C++에서는 이러한 경우에 사용할 수 있는 인라인 함수라는 것을 제공한다.
  • 인라인 함수는 호출될 때 일반적인 함수의 호출 과정을 거치지 않고, 함수의 모든 코드를 호출된 자리에 바로 삽입하는 방식의 함수이다.
  • 이 방식은 함수를 호출하는 데 걸리는 시간은 절약되나, 함수 호출 과정으로 생기는 여러 이점을 포기하게 된다.
  • 따라서 코드가 매우 적은 함수만을 인라인 함수로 선언하는 것이 좋다.


3) 인라인 함수의 선언

  • C++에서 인라인 함수는 다음과 같이 선언한다.


inline 함수의원형
또는
inline 함수의정의


  • inline 키워드는 함수의 원형이나 함수의 정의 어느 한 쪽에만 표기해도 되며, 양쪽 다 표기해도 상관없다.


#include <iostream>
using namespace std;

inline int Sub(int, int);
void Print(int);

int main(void) {
    int num1 = 5, num2 = 3;
    int result;

    result = Sub(num1, num2);
    Print(result);

    return 0;
}

int Sub(int x, int y) { return x - y; }
inline void Print(int x) {
    cout << "계산 결과는 " << x << "입니다." << endl;
}

// 계산 결과는 2입니다.


  • 위의 코드에서 Sub() 함수와 Print() 함수는 인라인 함수로 정의되어 호출된다.
  • 보통 인라인 함수는 크기가 작으므로, 함수의 원형이 나오는 자리에 함수의 본체까지 함께 정의하는 경우가 많다.


#include <iostream>
using namespace std;

inline int Sub(int x, int y) { return x - y; }
inline void Print(int x) {
    cout << "계산 결과는 " << x << "입니다." << endl;
}

int main(void) {
    int num1 = 5, num2 = 3;
    int result;

    result = Sub(num1, num2);
    Print(result);

    return 0;
}

// 계산 결과는 2입니다.


  • 위의 코드는 실제로는 다음과 같이 인라인 코드로 삽입되어 실행된다.


#include <iostream>
using namespace std;

int main(void) {
    int num1 = 5, num2 = 3;
    int result;

    {
        int x = num1, y = num2;
        result = x - y;
    }

    {
        int x = result;
        cout << "계산 결과는 " << x << "입니다." << endl;
    }

    return 0;
}

// 계산 결과는 2입니다.


  • 참고로 인라인 함수에서는 재귀 호출이 허용되지 않는다.


4) 매크로 함수와 인라인 함수

  • C에서는 C++의 인라인 함수와 비슷한 기능의 매크로 함수를 사용한다.
  • #define 선행처리 지시문에 인수로 함수의 정의를 전달함으로써, 함수처럼 동작하는 매크로를 만들 수 있다.
  • 이러한 매크로를 함수 같은 매크로(Function-like Macro) 또는 매크로 함수라고 한다.


  • 하지만 매크로 함수는 일반 함수와는 달리 단순 치환만을 해주므로, 일반 함수와 똑같은 방식으로 동작하지 않는다.
  • 이러한 매크로 함수를 일반 함수처럼 사용하기 위해서는 모든 인수를 ()로 감싸야만 한다.


#include <iostream>
using namespace std;

#define SQR(X) X * X

int main(void) {
    int x = 5;

    cout << SQR(10) << endl;
    cout << SQR(x) << endl;
    cout << SQR(x + 3) << endl;

    return 0;
}

// 100
// 25
// 23


  • 위의 코드에서 매크로 함수 SQR(x + 3)의 결과는 예상과 많이 다르다.
  • 그 이유는 매크로 함수의 단순 치환 과정에 있으며, 제대로 된 결과를 얻기 위해서는 매크로 함수의 정의 부분을 다음과 같이 수정해야 한다.


#include <iostream>
using namespace std;

#define SQR(X) ((X) * (X))

int main(void) {
    int x = 5;

    cout << SQR(10) << endl;
    cout << SQR(x) << endl;
    cout << SQR(x + 3) << endl;

    return 0;
}

// 100
// 25
// 64


  • 하지만 C++의 인라인 함수는 단순 치환이 아닌 함수의 모든 코드를 호출된 자리에 인라인 코드로 삽입해 주는 것이다.
  • 따라서 일반 함수처럼 값이나 수식을 인수로 전달할 수 있으며, 매개변수 타입에 맞춘 자동 타입 변환도 지원한다.


  • 다음의 코드는 앞선 매크로 함수의 코드를 인라인 함수로 작성한 것이다.


#include <iostream>
using namespace std;

inline int Sqr(int x) { return x * x; }

int main(void) {
    int x = 5;

    cout << "계산 결과는 " << Sqr(10) << "입니다." << endl;
    cout << "계산 결과는 " << Sqr(x) << "입니다." << endl;
    cout << "계산 결과는 " << Sqr(x + 3) << "입니다." << endl;

    return 0;
}

// 계산 결과는 100입니다.
// 계산 결과는 25입니다.
// 계산 결과는 64입니다.

References