본문으로 건너뛰기

안동민 개발노트

본문 시작

함수의 정의와 호출

함수의 반환 타입·이름·매개변수·본문을 구성하고 원형 선언부터 호출과 반환까지의 실행 흐름을 익힙니다.

프로그램의 규모가 커지고 복잡해질수록 main 함수 하나에 모든 코드를 작성하는 것은 비효율적이고 비생산적입니다.

코드가 너무 길어져 이해하기 어렵고, 특정 기능을 여러 번 사용해야 할 때마다 코드를 복사/붙여넣기 해야 하는 문제가 발생합니다.

이러한 문제를 해결하고 코드를 구조화하며 재사용성을 높이는 데 필수적인 개념이 바로 함수(Function)입니다.

이 장에서는 함수가 무엇인지, 어떻게 정의하고 호출하는지, 왜 함수를 사용해야 하는지 다룹니다.

함수 원형 선언부터 호출과 반환까지의 순서를 한 번에 확인할 수 있도록 핵심 흐름을 도식화했습니다.


C++ 함수란 무엇인가?

함수는 특정 작업을 수행하는 코드 블록에 이름을 붙여놓은 것입니다.

함수는 필요할 때마다 이 이름을 호출하여 해당 작업을 반복적으로 수행할 수 있게 합니다.

함수의 주요 목적
  • 코드 재사용(Code Reusability): 한 번 정의한 함수는 프로그램의 여러 곳에서 필요할 때마다 호출하여 사용할 수 있습니다. 중복 코드를 줄이고 개발 시간을 단축합니다.
  • 모듈화(Modularity): 프로그램을 작은 기능 단위로 나누어 관리하기 쉽게 만듭니다. 각 함수는 독립적인 하나의 작업을 수행하므로, 프로그램 전체를 이해하기 전에 각 부분의 동작을 먼저 파악할 수 있습니다.
  • 가독성 향상(Improved Readability): 복잡한 작업을 함수로 분리하면 main 함수나 다른 함수의 코드가 훨씬 간결하고 읽기 쉬워집니다. 함수 이름만 보고도 어떤 작업을 하는지 유추할 수 있습니다.
  • 유지보수 용이성(Easier Maintenance): 특정 기능에 문제가 발생하면 해당 함수만 수정하면 되므로, 전체 코드를 뒤질 필요가 없습니다. 기능 개선 시에도 마찬가지입니다.

함수의 구성 요소

함수의 정의와 호출은 컴파일 시점의 선언·정의 계약실행 시점의 인자·매개변수·반환 흐름을 나누어 읽습니다.

이 장에서 다루는 일반적인 자유 함수 정의는 다음과 같은 기본적인 구성 요소를 가집니다.

함수의 기본 구조
반환타입 함수이름(매개변수타입1 매개변수이름1, 매개변수타입2 매개변수이름2, ...) {
    // 함수 몸체 (Function Body)
    // 함수가 수행할 작업이 정의되는 코드 블록
    // 필요 시 return 문으로 값을 반환
}
  • 반환 타입(Return Type): 함수가 작업을 수행한 후 호출자에게 돌려줄 값의 데이터 타입입니다. 만약 함수가 아무런 값도 반환하지 않는다면 void 키워드를 사용합니다.
  • 함수 이름(Function Name): 함수를 찾기 위한 식별자입니다. 다만 오버로드된 함수들은 같은 이름을 공유할 수 있으므로 이름만으로 항상 하나의 함수를 고유하게 가리키지는 않습니다. 컴파일러는 호출의 인자와 매개변수 타입 등을 이용해 후보 중 하나를 선택하며, 반환 타입만 다르게 해서는 오버로드할 수 없습니다.
  • 매개변수 목록(Parameter List): 함수 호출 때 인자로 초기화되는 매개변수들의 목록입니다. 각 매개변수에는 타입이 필요하고, 선언과 정의 모두에서 이름을 생략할 수 있습니다. 다만 정의의 본문에서 매개변수를 사용하려면 이름이 필요합니다. 매개변수가 없으면 괄호 () 안을 비웁니다.
  • 함수 몸체(Function Body): 중괄호 {} 안에 실제 함수가 수행할 작업의 코드가 작성됩니다.
  • return: return 키워드를 사용하여 함수가 반환 타입에 해당하는 값을 호출자에게 돌려줍니다. void 함수는 반환값 없이 본문 끝까지 실행하거나 return;으로 제어를 돌려줄 수 있습니다.

함수의 정의

함수를 정의한다는 것은 함수가 어떤 작업을 수행할지 코드로 작성하는 것을 의미합니다.

두 정수를 더하는 함수 정의
#include <iostream>

// addNumbers 함수 정의
int addNumbers(int num1, int num2) { // 반환 타입: int, 함수 이름: addNumbers, 매개변수: int num1, int num2
    int sum = num1 + num2; // 함수 몸체
    return sum;            // 계산 결과 반환
}

int main() {
    // main 함수는 프로그램의 시작점이며, 다른 함수를 호출할 수 있습니다.
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

위 코드에서 addNumbers 함수는 두 개의 int형 매개변수(num1, num2)를 받아들여 이들을 더한 int형 결과를 반환하도록 정의되었습니다.


함수의 호출

함수를 호출한다는 것은 정의된 함수를 실행시키도록 지시하는 것을 의미합니다.

함수를 호출할 때는 함수 이름 뒤에 괄호 ()를 붙이고, 함수가 요구하는 인자(Arguments)를 괄호 안에 순서대로 전달합니다.

함수 호출 예시
#include <iostream>
#include <string>

// addNumbers 함수 정의 (위에 정의한 것과 동일)
int addNumbers(int num1, int num2) {
    int sum = num1 + num2;
    return sum;
}

// 메시지를 출력하고 아무것도 반환하지 않는 함수 정의
void printMessage(std::string msg) { // 반환 타입: void, 매개변수: std::string msg
    std::cout << "메시지: " << msg << std::endl;
    // void 함수이므로 반환값 없이 끝까지 실행하거나 return; 사용 가능
}

int main() {
    int a = 10;
    int b = 20;

    // addNumbers 함수 호출
    int result = addNumbers(a, b); // a와 b의 값을 num1과 num2로 전달
    std::cout << "두 수의 합: " << result << std::endl; // 출력: 두 수의 합: 30

    // 직접 상수 값을 전달하여 함수 호출
    std::cout << "5와 7의 합: " << addNumbers(5, 7) << std::endl; // 출력: 5와 7의 합: 12

    // printMessage 함수 호출
    printMessage("함수 호출은 재밌다!"); // 문자열 리터럴을 인자로 전달
    printMessage("Learning C++ is great!");

    return 0;
}
  • addNumbers(a, b): main 함수 내에서 addNumbers 함수를 호출합니다. 이 예제의 int 값 매개변수 num1, num2는 인자 식 a, b의 결과로 초기화됩니다. 함수가 반환하는 값(30)은 호출식의 결과가 되어 result를 초기화합니다.
  • printMessage("함수 호출은 재밌다!"): printMessage 함수를 호출합니다. "함수 호출은 재밌다!"라는 문자열 리터럴이 msg 매개변수로 전달됩니다. 이 함수는 void 타입이므로 반환하는 값이 없습니다.

함수를 호출하면 각 매개변수를 대응하는 인자로 초기화하고, 모든 매개변수 초기화가 끝난 뒤 함수 본문을 실행합니다. 값 매개변수는 인자에서 새 매개변수 객체를 초기화하므로 타입에 따라 변환·복사·이동이 관여할 수 있습니다. 참조 매개변수는 참조 호환성, 값 범주, const 조건이 허용하면 기존 대상에 직접 바인딩되고, 허용되는 경우에는 변환 결과로 생긴 임시 객체에 바인딩될 수도 있습니다. 따라서 모든 호출을 단순히 “매개변수 복사”라고 설명해서는 안 됩니다.

매개변수가 여러 개일 때 각 매개변수 초기화는 대응하는 인자의 값 계산과 부수효과까지 포함한 단위로 다른 매개변수 초기화와 불확정 순서(indeterminately sequenced)에 있습니다. 즉 서로 뒤섞이지는 않지만 어느 쪽이 먼저인지는 정해지지 않으므로, 왼쪽에서 오른쪽 순서를 가정하거나 서로 다른 인자에 순서 의존적인 부수효과를 넣어서는 안 됩니다. 비-void 함수에서 return expression;을 실행하면 피연산자가 선언된 반환 타입의 반환 참조 또는 결과 객체를 초기화하고, 호출식은 그 결과를 냅니다. 이 반환 초기화에는 타입과 식에 따라 변환·복사·이동이 관여하거나 복사가 생략될 수 있으므로 항상 복사된다고 단정하지 않습니다. void 함수는 값 없이 호출자에게 제어를 돌려줍니다.


함수 선언과 소스 순서

C++에서는 함수 호출 지점에서 사용할 선언이 먼저 보여야 합니다. 함수 정의를 호출문보다 앞에 두면 그 정의가 선언 역할도 하지만, 정의를 뒤에 둘 때는 먼저 함수 선언(Function Declaration)을 작성해야 합니다. 초보 교재에서는 본문 없는 이 선언을 함수 원형(Function Prototype)이라고도 부릅니다.

선언은 컴파일러가 함수 이름과 반환 타입, 매개변수 타입을 알고 호출을 검사하도록 합니다. 같은 이름의 오버로드가 여러 개라면 호출 지점에서 조회되는 선언들 가운데 인자에 맞는 후보를 이 시점에 선택합니다.

함수 원형의 형식
반환타입 함수이름(매개변수타입1, 매개변수타입2, ...); // 매개변수 이름은 생략 가능
  • 함수 원형은 함수 정의의 첫 줄과 같지만, 함수 몸체 {}가 없고 세미콜론(;)으로 끝납니다.
  • 매개변수의 이름은 생략할 수 있지만, 매개변수의 타입은 반드시 명시해야 합니다.
함수 원형을 사용한 예시
#include <iostream>

// 함수 원형 (함수 정의보다 먼저 선언)
int multiplyNumbers(int, int); // 선언에서는 매개변수 이름을 생략할 수 있음

// main 함수 (multiplyNumbers를 호출하기 전에 원형이 선언되어 있으므로 문제 없음)
int main() {
    int result = multiplyNumbers(10, 3); // multiplyNumbers 함수 호출
    std::cout << "두 수의 곱: " << result << std::endl; // 출력: 두 수의 곱: 30
    return 0;
}

// multiplyNumbers 함수 정의 (main 함수 뒤에 있어도 원형 덕분에 호출 가능)
int multiplyNumbers(int x, int y) {
    return x * y;
}

선언과 정의는 동일한 함수를 재선언하는 규칙에 맞아야 하며, 이 예제에서는 반환 타입과 매개변수 타입이 일치해야 합니다. 호출로 odr-use된 함수에는 프로그램 안에 정의가 필요합니다. 표준상 이 요구를 어긴 프로그램에는 진단이 필수는 아니며, 일반적인 별도 컴파일 환경에서는 해당 소스의 컴파일 뒤 링크 오류로 드러납니다.

대규모 프로젝트에서는 선언을 헤더 파일(.h 또는 .hpp)에 두고 정의를 소스 파일에 두는 방식이 흔합니다. 다음 다이어그램은 소스 순서에 관한 컴파일 계약과 실제 호출 때의 실행 순서를 한눈에 구분합니다.

함수 선언과 정의의 소스 순서 계약을 컴파일 구획으로, 대응 인자의 평가를 포함한 매개변수 초기화·본문·반환을 실행 구획으로 나눈 C++ 함수 흐름

Compile contract → runtime call

호출 지점에는 선언이 보여야 하고, 실행할 때는 인자 식의 결과로 매개변수를 초기화한 뒤 본문과 반환을 진행합니다. 소스에서 먼저 보이는 순서와 프로그램이 실행되는 순서를 섞지 않습니다.

컴파일 · source order

호출 전에 함수 계약이 보여야 한다

  1. 선언이 이름과 타입을 알린다

    본문 없는 선언이 먼저 보이거나, 함수 정의 자체가 호출보다 먼저 나와야 합니다. 선언의 매개변수 이름은 생략할 수 있습니다.

  2. 호출을 검사하고 오버로드를 고른다

    같은 이름의 함수가 여럿일 수 있습니다. 컴파일러는 호출 지점에서 조회되는 선언들 가운데 인자와 매개변수 타입을 바탕으로 후보를 선택하며, 반환 타입만으로는 오버로드를 구분하지 않습니다.

  3. 일치하는 정의가 본문을 제공한다

    호출로 odr-use된 함수에는 프로그램 안에 정의가 필요합니다. 표준상 진단이 필수는 아니며, 일반적인 별도 컴파일 환경에서는 링크 오류로 드러납니다.

실행 · call expression

인자에서 호출식 결과까지 진행한다

  1. 인자 식으로 매개변수 초기화

    각 매개변수 초기화는 대응 인자의 값 계산과 부수효과까지 포함합니다. 여러 초기화는 서로 뒤섞이지 않지만 어느 것이 먼저인지는 정해지지 않습니다. 값 매개변수에는 변환·복사·이동이 관여할 수 있고, 참조 매개변수는 참조 규칙이 허용하는 기존 대상이나 변환 결과의 임시 객체에 바인딩될 수 있습니다.

  2. 함수 본문 실행

    초기화된 매개변수 이름으로 함수가 맡은 계산과 작업을 수행합니다.

  3. 반환과 호출 지점 복귀

    return 피연산자는 선언된 반환 타입의 반환 참조 또는 결과 객체를 초기화하며, 호출식은 그 결과를 냅니다. 이때 변환·복사·이동이 관여하거나 복사가 생략될 수 있습니다. void 함수는 값 없이 제어만 돌려줍니다.

같은 코드를 컴파일 시점과 실행 시점으로 나누어 읽기

int multiply(int, int);          // 선언

int calculate() {
    return multiply(10, 3);      // 호출
}

int multiply(int left, int right) {
    return left * right;         // 정의
}

컴파일 시점에는 선언이 호출을 검사하게 하고 정의가 본문을 제공합니다. 실행 시점에는 103이 값 매개변수를 초기화하고, 반환된 30이 호출식의 결과가 됩니다.

읽는 순서: 선언 가시성 → 호출 검사와 오버로드 선택 → 정의 존재 여부를 먼저 확인하고, 실제 호출에서는 각 인자로 매개변수 초기화 → 본문 → 반환을 추적합니다.


함수로 분리할지 판단하기

함수는 프로그램을 설계하고 개발하는 데 있어 핵심적인 도구입니다.

함수로 분리할 코드는 반복 횟수 하나만 보지 말고, 이름 붙일 수 있는 책임인지, 입력과 출력의 경계가 분명한지, 독립적으로 변경·테스트할 가치가 있는지를 함께 판단합니다. 반대로 한 번만 쓰이는 짧고 명백한 표현을 억지로 추출해 호출부와 정의 사이를 계속 오가게 만들 필요는 없습니다.

코드의 책임과 이름, 반복과 변화, 입력·출력 경계, 테스트 가능성, 분리 비용을 기준으로 함수 추출 여부를 판단하는 표

Name → boundary → change

반복된다는 이유 하나만으로 추출하지 말고, 호출부에서 이름으로 드러낼 책임과 입력·출력 경계가 있는지 확인합니다. 분리는 읽기·변경·테스트 비용을 실제로 낮출 때 가치가 있습니다.

코드를 함수로 추출하기 전 묻는 다섯 가지 질문
판단 질문 함수로 분리할 신호 현재 위치에 둘 신호
책임과 이름 한 문장으로 설명되는 작업이며, 좋은 함수 이름이 호출부의 의도를 드러냅니다. 한 줄의 명백한 표현인데 이름이 구현을 되풀이할 뿐입니다.
반복과 변화 같은 규칙이 반복되거나 여러 호출자가 함께 따라야 할 변경점입니다. 한 번만 쓰이고 주변 문맥과 함께 볼 때 가장 쉽게 이해됩니다.
입력·출력 경계 필요한 값을 매개변수로 받고 결과나 효과를 짧게 설명할 수 있습니다. 숨은 전역 상태와 지나치게 많은 인자가 필요해 경계가 더 흐려집니다.
변경·테스트 독립적으로 수정하거나 검증할 가치가 있는 계산·검증·출력 책임입니다. 호출부와 항상 함께 바뀌는 사소한 세부사항이라 분리 이점이 작습니다.
읽기 비용 긴 본문에서 세부 구현을 감추면 전체 흐름이 더 선명해집니다. 정의로 이동해야만 이해할 수 있어 오히려 코드 탐색이 늘어납니다.
  1. 책임과 이름

    분리: 이름이 호출 의도를 드러냅니다. 유지: 이름이 한 줄 구현을 되풀이할 뿐입니다.

  2. 반복과 변화

    분리: 반복되거나 함께 바뀔 규칙입니다. 유지: 한 번 쓰이고 주변 문맥이 중요합니다.

  3. 입력·출력 경계

    분리: 매개변수와 결과가 분명합니다. 유지: 숨은 상태와 많은 인자가 필요합니다.

  4. 변경·테스트

    분리: 독립 수정·검증 가치가 있습니다. 유지: 호출부와 늘 함께 바뀌는 세부사항입니다.

  5. 읽기 비용

    분리: 긴 본문의 흐름이 선명해집니다. 유지: 정의를 오가는 탐색만 늘어납니다.

호출부의 의도

이름 있는 작은 작업으로 읽는다

const auto total = calculateTotal(items);
printResult(total);

계산과 출력의 책임이 분리되어 호출부는 프로그램의 큰 흐름을 보여줍니다.

추출 뒤 확인

경계가 실제로 더 단순해졌는가

  • 이름만 읽어도 작업의 목적을 알 수 있는가?
  • 매개변수와 반환값 또는 효과가 명시적인가?
  • 중복 제거·유지보수·협업·테스트 중 실제 이점이 있는가?

핵심: 함수 추출은 줄 수를 줄이는 기술이 아니라, 책임에 이름을 붙이고 호출자와 구현 사이의 계약을 선명하게 만드는 설계입니다.

  • 복잡성 관리: 거대한 문제를 작은 단위의 문제로 분해하여 해결할 수 있게 합니다.
  • 협업 용이: 여러 개발자가 각자의 함수를 독립적으로 개발할 수 있습니다.
  • 테스트 용이: 각 함수를 독립적으로 테스트하여 전체 프로그램의 안정성을 높일 수 있습니다.

따라서 C++ 프로그래밍에서 함수를 효과적으로 사용하는 것은 좋은 코드 품질과 효율적인 개발의 기초가 됩니다.