함수 기초 C++ 자습서 32

함수 기초 C++

이번 포스팅 부터는 C++의 핵심 개념인 함수의 기초를 다루어 보겠습니다. 컴퓨터 프로그래밍에서 함수라는 용어는 수학의 그것과는 차이가 있습니다. 또 프로그래밍 언어의 분류를 절차적 언어, 객체지향 언어, 함수형 언어 등으로 분류하고 있는데 그런 개념을 알기 위해서도 함수가 중요합니다.

도대체 함수가 무엇인가? 이해하기 위해서는 실제로 함수에 대해서 사용해 보고 부딪치는 수 밖에 없습니다. 함수는 영어로 function 이라고 합니다. 사전적 의미는 기능이지요. 프로그램에서는 인수(argument)에 따라 처리해서 결과값을 반환하는 루틴(routine)을 의미합니다. 쉬운 예는 pow 함수가 있습니다. 아래의 코드는 cmath 라이브러리 함수인 pow 를 사용해서 2의 n 승을 출력합니다. 함수의 인수로 숫자를 주면 제곱의 결과값을 돌려줍니다.

우리가 사용하는 CPU에는 사칙연산 기능이 내장되어 있습니다. CPU에 내장된 기능을 ISA라 하여 정해진 기계어로 명령하면 됩니다. 그런데 그보다 복잡한 제곱이라던가 제곱근 이런 기능들은 ISA 명령어 한 두개로 실행이 되지 않기 때문에 함수로 만들어 놓는 것 입니다. C++에서 제공하는 기본 라이브러리 cmath 에서 일반적인 수학 함수를 제공합니다. 제곱이나 제곱근 같은 식은 많은 프로그램에서 사용할 수 있으니까요.

#include<iostream>
#include<cmath>

int main()
{

    using std::cout;
    using std::endl;

    for (int i = 1; i <= 10; i++)
    {
        cout << pow(2, i) << endl;
    }
    
    return 0;
}

[실행결과]
2
4
8
16
32
64
128
256
512
1024

많은 함수들이 이미 라이브러리로 구현되어 있어서 프로그래머가 모든 기능을 만들 필요가 없습니다. 서양의 프로그래머들은 특히 Don’t reinvent the wheel 이라는 격언을 좋아합니다. 수레바퀴가 이미 만들어져 있는데 또 만들지 말라는 말입니다. 이미 과거의 기술자들이 만들어 놓은 함수들은 오랜 시간 사용되면서 검증이 다 되어 있기 때문에 그 함수들을 잘 쓰면 됩니다.

물론 C++ 프로그래머는 남이 만든 함수를 사용하는 것도 중요하지만 스스로 함수를 만들 수 있어야 합니다.

함수 기초

C++에서 함수를 만들기 위해서 세가지 단계가 필요합니다. 함수 원형을 컴파일러에게 알려주고 함수를 구현해야 합니다. (함수 정의) 그리고 함수를 호출하는게 최종 단계입니다. C++의 기본 라이브러리 함수는 컴파일되어 있어서 사용할 수 있는데 원형이 저장된 헤더 파일을 포함 (include)만 하면 됩니다.

사용자가 직접 만드는 함수는 이 과정을 다 스스로 만들어야 합니다. 아래의 예제는 이 세 가지를 하나의 파일에서 구현하였습니다.

#include<iostream>

void printSomething();

using std::cout;
using std::endl;

int main()
{

    printSomething();

    return 0;
}

void printSomething()
{
    cout << "something" << endl;

}

void printSomething() 함수의 원형입니다. printSomething() 호출입니다. 맨 아래의 void printSomething()은 실제 코드가 구현되어 있습니다. 프로그램의 내용이야 별거 없지만 이 세가지 단계를 거쳐야 함수가 구현됩니다. 여기에서 함수 원형은 main 함수 보다 먼저 나와야 합니다. 순서상 호출을 먼저 하면 컴파일 에러가 됩니다. 왜냐하면 함수를 호출하면 우선 원형이 있는지 확인해야 하는데 컴파일러가 원형의 데이터를 보기도 전에 함수에서 호출해 버리니까 없다고 나옵니다. 원형은 구현과 연결되어서 코드를 찾아서 실행합니다.

C++의 라이브러리 파일에서는 헤더 파일에 원형이 있습니다. 실제 구현 파일은 컴파일이 완료된 상태이며 그렇기 때문에 사용자는 헤더 파일을 include 하는 것으로 라이브러리 함수를 사용할 수 있습니다.

함수의 정의

함수를 정의하는 것은 함수 코드를 구현하는 것 입니다. 함수의 형식은 다음과 같습니다.

반환타입 함수이름(매개변수)
{
    코드 블록
    return;
}

// void 형

void function(void)
{
    code;
    return;
}

// int 형

int function(int)
{
    code;
    return value;
}

함수에는 매개변수와 반환값이 있습니다. 반환값은 리턴값(return value)라고도 합니다. 프로그램이 함수를 실행시키는 것을 호출(call)이라고 합니다. 호출된 함수에 전달하는 수를 인수(argument) 라고 하며 인수를 받은 함수는 값을 복사한 매개변수(parameter)를 처리하여 호출한 위치에 리턴값을 돌려줍니다. 이것이 함수의 작동원리 입니다.

함수에는 인수를 전달할 수도 있고 안할 수도 있습니다. 또 리턴값이 있을 수도 있고 없을 수도 있습니다. 파이썬 같은 동적 언어는 이를 암시적으로 컴파일러가 처리하지만 C++ 에서는 모두 프로그래머가 정확하게 설정해야 합니다. 이런 것만 봐도 C++은 다른 고수준 언어보다 프로그래머가 직접 챙겨야 하는 부분이 많습니다. 언어의 난이도는 다 다르기 때문에 이런 특성을 잘 이해하는 것도 프로그래밍 학습에 중요한 부분입니다.

리턴값이 없는 경우: 함수의 이름 앞에 void 타입을 사용합니다. void 는 비어있다, 없다의 뜻으로 리턴값이 없음을 의미합니다.

void function()

매개변수가 없는 경우: 함수 이름 다음에( ) 안에는 인수를 전달합니다. 외부에서 주는 값이 인수(argument) 받는 값이 매개변수(parameter) 입니다. 매개변수가 없다면 인수를 전달할 필요가 없습니다. 이런 경우 명시적으로 (void) 이렇게 작성하기도 합니다만 ( ) 처럼 비워 놔도 컴파일러는 매개변수 없이 처리합니다.

리턴값과 매개변수가 중요한 이유는 함수의 핵심 원리이기 때문입니다. 매개변수를 입력(INPUT)하여 리턴값을 출력(OUTPUT) 개념으로 보면 함수(function)는 블랙박스 입니다. 이 함수가 있기 때문에 라이브러리나 모듈 형식의 프로그래밍이 가능해집니다. 복잡한 기능도 함수로 구현하면 원리를 몰라도 누구나 쉽게 사용할 수 있습니다. 현대의 응용 프로그래밍에서는 앞서간 이들이 오픈소스로 구현한 함수를 가져다 쓰는 능력이 중요합니다. 객체지향 프로그래밍에서의 메소드도 원초적으로 보면 C/C++ 의 함수에 몇가지 규칙을 더한 것으로 내부 동작은 함수와 유사합니다.

C++에서 함수를 만들 때는 입력과 출력 둘 다 정의해야 합니다. 그럼 예제로 보겠습니다.

리턴값이 없는 경우 예제

리턴값이 없는 것은 딱히 리턴을 할 필요가 없어서 입니다. 예를 들어서 화면에 메시지를 출력하는 함수라면 딱히 필요가 없습니다. 아래의 예제는 함수의 매개변수 만큼 문자열을 반복해서 출력합니다.

이 함수의 목적이 출력하는 일이기 때문에 return 이 필요 없습니다. 물론 함수 호출이 끝나고 복귀할 때는 당연히 다시 돌아가는(return) 동작이 일어납니다. 또 return 문을 명시적으로 사용해서 프로그램의 제어를 만들 수 있습니다. 함수의 실행 도중에 return 문을 만나면 반환값 자체는 없더라도 자신을 호출한 main 함수에게로 돌아갑니다. 루프의 break 문 같은 것이지요.

#include<iostream>

void sayHelloNtimes(int n);

using std::cout;
using std::endl;

int main()
{

    sayHelloNtimes(3);

    return 0;
}

void sayHelloNtimes(int n)
{
    for (int i = 0; i < n; i++)
    {
        cout << "Hello World! of C++" << endl;
    }
}

[실행결과]
Hello World! of C++
Hello World! of C++
Hello World! of C++

리턴값이 있는 경우 예제

리턴값이 있는 경우 호출한 쪽에서 받아서 사용하면 됩니다. 받을 때의 규칙은 타입이 같아야 합니다. 아니면 형변환을 해줍니다. 함수는 실행도중에 return 문을 만나면 리턴값을 보냅니다. 즉 처음 만나는 return 문에서 종료합니다. if 등 제어문을 사용해서 적절한 위치에 return 을 배열하면 매개변수에 따라 return 문을 여러개 사용할 수 있습니다. 다만 하나의 함수에 너무 많은 return 문을 사용하는 것은 가독성에 좋지 않습니다.

#include<iostream>

int myMax(int a, int b);

using std::cout;
using std::endl;

int main()
{

    int var = myMax(7,3);

    cout << var << endl;

    return 0;
}

int myMax(int a, int b)
{
    if (a > b)
        return a;
    else
        return b;
}

함수의 원형

함수의 원형은 prototype 이라고 합니다. 함수를 선언 한다고도 합니다. 선언과 정의는 비슷해 보이지만 다릅니다.

C++은 함수의 원형을 사용자와 실제 코드 간의 인터페이스로 두고 있습니다. main 함수 위에 항상 포함하는 #include <헤더파일> 은 라이브러리 함수의 원형이 들어있는 헤더 파일을 가져옵니다. 이 과정을 완전히 이해하려면 C++의 문법도 좋지만 GCC(GNU Complier Collection)의 메뉴얼로 실습하면 좀 더 도움이 됩니다.

여기서는 함수의 원형의 규칙에 대해서 알아보겠습니다. 아래의 예제에서 위에서 처음 나오는 int myMax(int, int) 와 void helloNtimes(int a)가 함수의 원형입니다. 컴파일러가 이 문장을 만나면 이 함수는 어떤 매개변수 타입을 받는지 또 반환값은 무엇인지 확인합니다. 이렇게 함으로써 얻는 장점은 파일이 여러개인 프로그램을 효율적으로 처리할 수 있게 됩니다. 프로그램의 크기가 커지면 여러개의 파일로 나눠지는데 컴파일러는 인터페이스인 원형을 가지고 있다가 최종 실행 파일을 만드는 링크 과정에서 원형과 정의를 연결 하면 됩니다.

함수의 정의를 뺀 후에 gcc 컴파일러로 -c 옵션을 걸고 컴파일을 해보면 함수의 원형만 있고 구현부인 정의가 없어도 컴파일이 가능합니다. 하지만 링킹을 시도하면 undefined reference라는 메시지와 함께 실패합니다.

함수 원형의 또 하나의 특징은 원형에서는 int myMax(int, int) 처럼 매개변수의 타입만 넣어도 됩니다. 원형에서는 매개변수를 당장 생성하는게 아니라 매개변수의 사이즈를 체크하는 것 까지 필요합니다. 컴파일러는 어떤 타입의 매개변수를 몇개 받는지, 리턴값의 타입이 무엇인지를 체크하여 메모리의 크기를 계산할 수 있습니다. 그래서 원리적으로는 생략 가능합니다.

함수 원형은 호출 지점보다 먼저 나와야 컴파일 오류가 안납니다. 원형에 대한 내용이 많아지면 헤더 파일로 분리하고 함수 정의 파일을 별도로 컴파일하면 됩니다. 이 포스팅에서 컴파일 방법을 다루지는 않겠지만 제일 확실히 이해하는 방법은 GCC 로 직접 빌드 과정을 진행해 보는 것 입니다.

아래 예제에서 함수의 원형을 선언할 때는 세미콜론 ; 을 끝에 붙이고 정의를 할 때는 { } 괄호로 구분하는 것에 주의합니다.

#include<iostream>

int myMax(int, int);
void helloNtimes(int a);

using std::cout;
using std::endl;

int main()
{

    int var = myMax(7,3);

    cout << var << endl;

    helloNtimes(5);

    return 0;
}

int myMax(int a, int b)
{
    if (a > b)
        return a;
    else
        return b;
}

void helloNtimes(int a)
{
    for (int i = 0; i < a; i++)
    {
        cout << "hello world c++!" << endl;
    }
}

함수의 원형에 나와있는 정보들로 컴파일러는 함수 호출 시에 매개변수와 리턴값이 제대로 사용되는지 검증이 가능합니다. 매개변수나 반환값이 원형과 매치가 안되는 경우 컴파일 오류가 납니다.

함수 호출과 매개변수

함수의 호출 시에는 매개변수를 전달해야 합니다. 매개 변수의 전달 방식은 두 종류가 있습니다. Call by Value 와 Call by Reference 입니다. 이 부분은 포인터를 학습한 후에 이해가 가기 때문에 해당 포스팅으로 미뤄두겠습니다. 다만 전달하는 변수를 인수(argument) 라 하고 받는 변수를 매개변수(parameter) 라고 합니다. 국내의 오래된 서적에는 매개변수로 통일하는 경우가 있는데 두 개는 다른 것 입니다. 메모리에서 위치가 다르고 변수의 수명(scope)도 다릅니다. 호출할 때 필요한 매개변수를 어떤 방식으로 넘겨 줄 것인가? – 함수 호출에서는 중요한 내용이 됩니다.

반환값은 똑같은 타입으로 받아야 합니다. int myMax 함수의 반환값을 char나 double 로 받는다면 제대로 값을 받을 수 없습니다. 이것은 형변환(type casting) 문제와도 관련이 있는데 동적 타이핑 언어를 사용하다가 다시 C++을 사용하려면 이런 부분들이 번거롭게 느껴집니다. 그 만큼 세심하고 기계에 가까운 언어기 때문에 C++은 성능이 좋고 신뢰도가 높다고 말하는 것 이겠지요.

요약

함수의 기초에 대하여 대략적으로 알아봤습니다. 기초는 큰 그림을 잡기 위한 설명 정도 이고 다음 포스팅에서 함수의 개별 주제를 상세히 다뤄 보겠습니다.

참고문서

함수(C++) | Microsoft Docs

Leave a Comment