본문 바로가기

함수의 정의와 호출

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

지금까지의 프로그램은 모든 코드를 main 안에 적었습니다. 프로그램이 커지면 main이 길어져 흐름을 따라가기 어렵고, 같은 계산이 여러 곳에 필요할 때마다 코드를 복사해야 합니다. 복사한 코드에서 실수를 발견하면 복사한 곳을 모두 찾아 고쳐야 합니다.

함수(function)는 하나의 작업을 하는 코드에 이름을 붙인 단위입니다. 필요한 곳에서 이름으로 호출하면 그 작업을 실행하고, 결과가 있으면 호출한 곳으로 돌려줍니다. 이 글은 함수를 정의하고 호출하는 방법, 호출 한 번에서 매개변수와 반환값이 어떻게 오가는지, 선언과 정의의 순서를 다룹니다.

함수의 구성

함수 정의는 반환 타입, 이름, 매개변수 목록, 본문으로 이루어집니다. 아래 형식의 첫 줄은 함수를 쓰는 쪽이 알아야 할 정보로, 무엇을 넘기면 어떤 타입의 값을 돌려받는지를 알려 줍니다. 중괄호 안의 본문은 그 일을 실제로 어떻게 하는지를 적는 곳입니다.

함수 정의 형식
반환타입 함수이름(매개변수타입1 이름1, 매개변수타입2 이름2) {
    // 본문
    return 결과;
}

반환 타입은 함수가 호출한 곳에 돌려줄 값의 타입이며, 돌려줄 값이 없으면 void를 씁니다. 매개변수(parameter)는 호출할 때 넘겨받는 값을 담는 변수이고, 받을 값이 없으면 괄호를 비워 둡니다. 본문에서 return 식;을 실행하면 그 값을 결과로 함수를 끝내며, void 함수는 본문 끝에 도달하거나 return;을 만나면 끝납니다.

정의하고 호출하기

함수를 호출할 때는 이름 뒤 괄호 안에 매개변수에 넘길 값인 인자(argument)를 순서대로 적습니다. 아래 프로그램은 값을 돌려주는 addNumbers와 돌려주지 않는 printMessage를 정의하고, main에서 변수, 정수 리터럴, 문자열 리터럴을 인자로 넘겨 호출합니다. addNumbers의 결과는 변수에 담기도 하고 바로 출력하기도 하므로, 호출식 자체가 하나의 값이라는 점을 보세요.

define_and_call.cpp
#include <iostream>
#include <string>

int addNumbers(int num1, int num2) {
    int sum = num1 + num2;
    return sum;
}

void printMessage(std::string msg) {
    std::cout << "message: " << msg << '\n';
}

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

    int result = addNumbers(a, b);
    std::cout << "sum " << result << '\n';
    std::cout << addNumbers(5, 7) << '\n';

    printMessage("functions are reusable");
}
sum 30
12
message: functions are reusable

addNumbers(a, b)를 호출하면 매개변수 num1이 a의 값 10으로, num2가 b의 값 20으로 초기화된 뒤 본문이 실행됩니다. return sum;이 돌려준 30은 호출식 addNumbers(a, b)의 값이 되어 result를 초기화합니다. addNumbers(5, 7)처럼 리터럴을 넘길 수도 있고, 호출식의 값 12를 바로 출력에 쓸 수도 있습니다. printMessage는 void 함수여서 돌려주는 값이 없으므로, 호출식을 변수에 대입하거나 출력에 쓸 수 없습니다.

호출 한 번 따라가기

인자 a와 매개변수 num1은 서로 다른 변수입니다. 여기서 쓰는 값 매개변수는 호출할 때마다 새로 만들어져 인자의 값으로 초기화되고, 함수가 끝나면 사라집니다. 그래서 함수 안에서 매개변수를 바꿔도 호출한 쪽의 변수는 바뀌지 않습니다.

next_value.cpp
#include <iostream>

int nextValue(int value) {
    value += 1;
    return value;
}

int main() {
    int original = 4;
    int first = nextValue(original);
    int second = nextValue(first);
    std::cout << original << ' ' << first << ' ' << second << '\n';
}
4 5 6

첫 호출은 새 value를 4로 초기화해 5로 만든 뒤 돌려주고, original은 4 그대로입니다. 둘째 호출은 또 다른 value를 first의 값 5로 초기화해 6을 돌려줍니다. 호출한 쪽의 변수를 함수가 직접 바꾸게 하는 방법은 참조자에서 다룹니다.

반환은 화면에 출력하는 것이 아니라, 호출식에 결과를 넘겨주는 것입니다. nextValue 안에는 출력문이 없으므로, main이 돌려받은 값을 출력하지 않으면 계산한 숫자는 화면에 나타나지 않습니다. 결과를 계산하는 함수와 결과를 출력하는 함수를 나누어 두면, 계산 함수를 출력이 필요 없는 곳에서도 다시 쓸 수 있습니다.

더 깊이: 인자는 실제로 어디로 전달될까

매개변수가 호출마다 새로 만들어진다는 것은 언어의 규칙이고, 그 값을 기계어 수준에서 어떻게 넘길지는 플랫폼의 호출 규약(calling convention)이 정합니다. GCC는 Windows를 대상으로 할 때 Microsoft의 ABI를 기본으로 쓰므로, MSYS2의 GCC로 만든 x86-64 프로그램은 Microsoft가 문서로 정한 x64 호출 규약을 따릅니다. 이 규약에서는 왼쪽부터 네 번째 자리까지의 인자가 자리에 따라 정해진 레지스터로 넘어갑니다. 정수와 포인터는 RCX, RDX, R8, R9를, 실수는 XMM0부터 XMM3을 쓰고, 다섯째 인자부터는 스택에 놓입니다. 예를 들어 f(int a, double b)의 b는 둘째 자리이므로 XMM1로 갑니다. 그래서 addNumbers(a, b)의 두 인자도 메모리를 거치지 않고 레지스터로 전달될 수 있습니다.

호출하는 쪽은 인자가 네 개보다 적더라도, 레지스터 인자 네 개를 저장할 수 있는 32바이트의 그림자 공간(shadow store)을 스택에 마련해야 합니다. 64비트에 들어가는 정수나 포인터 결과는 RAX로, double 같은 실수 결과는 XMM0으로 돌아옵니다. 크기가 1, 2, 4, 8바이트가 아닌 구조체를 인자로 넘길 때는 호출하는 쪽이 만든 복사본의 주소를 넘기고, RAX에 담을 수 없는 구조체를 돌려받을 때는 호출하는 쪽이 결과를 담을 메모리를 마련해 그 주소를 숨은 첫 인자로 넘깁니다.

이 규칙은 운영체제마다 다르며, GCC 문서도 Windows가 아닌 시스템에서는 System V ABI가 기본이라고 적고 있습니다. 그래서 같은 C++ 소스라도 Windows와 Linux에서 인자를 담는 레지스터가 다르므로, 어셈블리를 읽거나 다른 언어와 함수를 주고받을 때는 대상 플랫폼의 규약을 확인합니다. 언어의 관점에서는 매개변수가 인자의 사본이라는 규칙만 지켜지면 되므로, 이 차이가 프로그램의 결과를 바꾸지는 않습니다.

인자를 계산하는 순서

인자가 여러 개이면 각 인자를 계산해 매개변수를 초기화한 뒤 본문을 실행합니다. 하지만 어느 인자를 먼저 계산하는지는 정해져 있지 않습니다. 연산자 우선순위에서 본 f() + g()처럼, addNumbers(f(), g())에서도 f()와 g() 가운데 무엇이 먼저 호출될지는 컴파일러가 정합니다.

두 인자의 계산이 서로 섞이지는 않지만 순서가 정해지지 않았으므로, 같은 변수를 바꾸거나 화면에 출력하는 함수를 두 인자에 함께 넣으면 결과가 컴파일러마다 달라질 수 있습니다. 순서가 중요하면 int x = f(); int y = g(); addNumbers(x, y);처럼 문장을 나누어 순서를 코드에 드러냅니다.

선언과 정의의 순서

C++에서는 함수를 호출하는 곳보다 앞에 그 함수의 선언이 보여야 합니다. 정의를 호출보다 앞에 두면 정의가 선언 역할도 하지만, 정의를 뒤에 두려면 먼저 본문 없이 세미콜론으로 끝나는 함수 선언(function declaration)을 적습니다. 이런 선언을 함수 원형(function prototype)이라고도 부릅니다.

declare_first.cpp
#include <iostream>

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

int main() {
    int result = multiply(10, 3);
    std::cout << "product " << result << '\n';
}

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

선언에는 반환 타입과 매개변수 타입이 있어야 하며, 매개변수 이름은 int multiply(int, int);처럼 생략해도 됩니다. 선언과 정의의 반환 타입과 매개변수 타입은 같아야 합니다. 아래 그림은 이 프로그램을 두 구획으로 나누어 보여 줍니다.

C++ 함수의 컴파일 계약과 호출 실행 순서

컴파일 계약 구획을 그대로 남기고 multiply(10,3)의 매개변수 10/3→본문 30→호출식 30이라는 런타임 구획을 단계로 추적할 수 있다.

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

Compile contract → runtime call

함수 선언은 호출을 검사하고, 실행은 매개변수 초기화부터 진행한다

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

컴파일 계약과 호출 시 실행은 서로 다른 구획각구획에3노드2방향이 있다. 컴파일쪽은 호출앞선 선언을 확인해 호출을검사하고 정의존재조건을확인하는 계약이며 실제프로그램실행을뜻하지않는다. 실행쪽은 multiply(10,3)의 left10/right3 초기화에서본문곱30을계산하고 호출식결과30으로복귀한다. 두 구획사이에화살표가없고모바일에서도별개다.컴파일 계약선언을 볼 수 있음multiply(int,int)호출 검사타입·선택 후보 확인정의가 필요본문을 제공실행 흐름매개변수 준비left=10 · right=3본문 계산left × right = 30결과로 복귀호출식의 값30두 구획 사이 실행 간선은 없음소스 위치 ≠ 호출 순서
컴파일 계약과 호출 시 실행은 서로 다른 구획각구획에3노드2방향이 있다. 컴파일쪽은 호출앞선 선언을 확인해 호출을검사하고 정의존재조건을확인하는 계약이며 실제프로그램실행을뜻하지않는다. 실행쪽은 multiply(10,3)의 left10/right3 초기화에서본문곱30을계산하고 호출식결과30으로복귀한다. 두 구획사이에화살표가없고모바일에서도별개다.컴파일 계약선언을 볼 수 있음multiply(int,int)호출 검사타입·선택 후보 확인정의가 필요본문을 제공실행 흐름매개변수 준비left=10 · right=3본문 계산left × right = 30결과로 복귀호출식의 값30두 구획 사이 실행 간선은 없음소스 위치 ≠ 호출 순서
컴파일 · 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; // 정의
}

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

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

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명각 구획에 3개 노드와 2개 방향이 있다. 컴파일 쪽은 호출에 앞선 선언을 확인해 호출을 검사하고 정의 존재 조건을 확인하는 계약이며 실제 프로그램 실행을 뜻하지 않는다. 실행 쪽은 multiply(10,3)의 left 10/right 3 초기화에서 본문의 곱 30을 계산하고 호출식 결과 30으로 복귀한다. 두 구획 사이에 화살표가 없고 모바일에서도 별개다.
노드·연결선 라벨호출식의 값 30
본문·주석각 매개변수 초기화는 대응 인자의 값 계산과 부수 효과까지 포함합니다. 여러 초기화는 서로 뒤섞이지 않지만 어느 것이 먼저인지는 정해지지 않습니다. 값 매개변수에는 변환·복사·이동이 관여할 수 있고, 참조 매개변수는 참조 규칙이 허용하는 기존 대상이나 변환 결과의 임시 객체에 바인딩될 수 있습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
함수 경계

multiply 호출 인수를 값 매개변수로 받는다.

  • 호출: multiply(10,3)
  • 매개변수: 10,3
  • 호출 경계에서 인수의 값이 매개변수 초기화에 쓰인다.
본문 계산

두 매개변수를 곱한다.

  • 계산: 10*3
  • 본문 결과: 30
  • 본문의 return이 계산값을 호출자 쪽으로 전달한다.
호출식 결과

호출자는 반환된 30을 사용한다.

  • 호출식 값: 30
  • 함수: 반환 완료
  • 매개변수·본문·반환값의 위치를 구분한다.

C++ 함수의 컴파일 계약과 호출 실행 순서

컴파일 계약 구획을 그대로 남기고 multiply(10,3)의 매개변수 10/3→본문 30→호출식 30이라는 런타임 구획을 단계로 추적할 수 있다.

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

Compile contract → runtime call

함수 선언은 호출을 검사하고, 실행은 매개변수 초기화부터 진행한다

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

컴파일 계약과 호출 시 실행은 서로 다른 구획각구획에3노드2방향이 있다. 컴파일쪽은 호출앞선 선언을 확인해 호출을검사하고 정의존재조건을확인하는 계약이며 실제프로그램실행을뜻하지않는다. 실행쪽은 multiply(10,3)의 left10/right3 초기화에서본문곱30을계산하고 호출식결과30으로복귀한다. 두 구획사이에화살표가없고모바일에서도별개다.컴파일 계약선언을 볼 수 있음multiply(int,int)호출 검사타입·선택 후보 확인정의가 필요본문을 제공실행 흐름매개변수 준비left=10 · right=3본문 계산left × right = 30결과로 복귀호출식의 값30두 구획 사이 실행 간선은 없음소스 위치 ≠ 호출 순서
컴파일 계약과 호출 시 실행은 서로 다른 구획각구획에3노드2방향이 있다. 컴파일쪽은 호출앞선 선언을 확인해 호출을검사하고 정의존재조건을확인하는 계약이며 실제프로그램실행을뜻하지않는다. 실행쪽은 multiply(10,3)의 left10/right3 초기화에서본문곱30을계산하고 호출식결과30으로복귀한다. 두 구획사이에화살표가없고모바일에서도별개다.컴파일 계약선언을 볼 수 있음multiply(int,int)호출 검사타입·선택 후보 확인정의가 필요본문을 제공실행 흐름매개변수 준비left=10 · right=3본문 계산left × right = 30결과로 복귀호출식의 값30두 구획 사이 실행 간선은 없음소스 위치 ≠ 호출 순서
컴파일 · 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; // 정의
}

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

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

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명각 구획에 3개 노드와 2개 방향이 있다. 컴파일 쪽은 호출에 앞선 선언을 확인해 호출을 검사하고 정의 존재 조건을 확인하는 계약이며 실제 프로그램 실행을 뜻하지 않는다. 실행 쪽은 multiply(10,3)의 left 10/right 3 초기화에서 본문의 곱 30을 계산하고 호출식 결과 30으로 복귀한다. 두 구획 사이에 화살표가 없고 모바일에서도 별개다.
노드·연결선 라벨호출식의 값 30
본문·주석각 매개변수 초기화는 대응 인자의 값 계산과 부수 효과까지 포함합니다. 여러 초기화는 서로 뒤섞이지 않지만 어느 것이 먼저인지는 정해지지 않습니다. 값 매개변수에는 변환·복사·이동이 관여할 수 있고, 참조 매개변수는 참조 규칙이 허용하는 기존 대상이나 변환 결과의 임시 객체에 바인딩될 수 있습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
함수 경계

multiply 호출 인수를 값 매개변수로 받는다.

  • 호출: multiply(10,3)
  • 매개변수: 10,3
  • 호출 경계에서 인수의 값이 매개변수 초기화에 쓰인다.
본문 계산

두 매개변수를 곱한다.

  • 계산: 10*3
  • 본문 결과: 30
  • 본문의 return이 계산값을 호출자 쪽으로 전달한다.
호출식 결과

호출자는 반환된 30을 사용한다.

  • 호출식 값: 30
  • 함수: 반환 완료
  • 매개변수·본문·반환값의 위치를 구분한다.

컴파일 구획에서 컴파일러는 호출보다 앞의 선언을 보고 multiply(10, 3)의 인자 개수와 타입이 맞는지 검사하며, 정의는 프로그램 어딘가에 있어야 합니다. 실행 구획에서는 left가 10, right가 3으로 초기화되고, 본문이 곱 30을 계산해 호출식의 값으로 돌아갑니다. 소스에서 정의가 main보다 뒤에 있다는 것과, 실행할 때 main이 먼저 시작해 multiply를 부른다는 것은 서로 다른 이야기입니다.

선언 없이 뒤에 정의된 함수를 호출하면 컴파일러가 그 이름을 알지 못해 컴파일 오류가 납니다. 반대로 선언은 있지만 정의가 어디에도 없으면, 컴파일과 실행 과정의 twice 예처럼 컴파일은 성공하고 링크 단계에서 정의를 찾지 못해 실패합니다. 큰 프로젝트에서는 선언을 헤더 파일에, 정의를 소스 파일에 두어 여러 파일이 같은 선언을 함께 씁니다.

반환값이 있는 함수는 모든 경로에서 반환합니다

반환 타입이 void가 아닌 함수는 끝나는 모든 경로에서 return 식;을 실행해야 합니다. 값을 돌려주지 않고 본문 끝에 도달하면 정의되지 않은 동작이며, 예외는 return이 없으면 0을 돌려주는 main뿐입니다.

sign.cpp
int sign(int x) {
    if (x > 0) {
        return 1;
    }
    if (x < 0) {
        return -1;
    }
    return 0;
}

마지막 return 0;이 없으면 x가 0일 때 어떤 return도 만나지 않고 본문이 끝납니다. GCC는 C++에서 기본으로 켜진 -Wreturn-type으로 값을 돌려주지 않는 경로를 경고하므로, 이 경고는 실수의 신호로 보고 반드시 고칩니다. void 함수에서는 return;을 조건문 안에 두어, 더 할 일이 없을 때 일찍 끝낼 수 있습니다.

함수로 나눌지 판단하기

함수로 나누면 이름이 생기고, 그 이름이 호출하는 곳에서 코드의 의도를 설명합니다. 다만 반복된다는 이유 하나만으로 나누기보다, 아래 그림의 질문처럼 한 문장으로 설명되는 작업인지, 필요한 값을 매개변수로 받고 결과를 짧게 돌려줄 수 있는지, 따로 고치거나 확인할 가치가 있는지를 함께 봅니다.

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

Name → boundary → change

함수는 이름 있는 책임과 입력·출력 경계가 있을 때 분리한다

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

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

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

  2. 반복과 변화

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

  3. 입력·출력 경계

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

  4. 변경·테스트

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

  5. 읽기 비용

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

호출부의 의도

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

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

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

추출 뒤 확인

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

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

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

한 번만 쓰이는 짧고 명백한 식을 억지로 함수로 빼면, 읽는 사람이 호출부와 정의 사이를 오가야 해서 오히려 읽기 어려워집니다. 반대로 긴 본문의 한 단계를 이름 있는 함수로 빼면, main에는 단계의 이름만 남아 전체 흐름이 잘 보입니다. 함수가 지나치게 많은 매개변수를 받아야 한다면, 나누는 위치가 알맞은지 다시 생각해 봅니다.

정리

  • 함수는 반환 타입, 이름, 매개변수, 본문으로 정의하며, 돌려줄 값이 없으면 반환 타입을 void로 둡니다.
  • 호출하면 각 매개변수가 대응하는 인자로 초기화된 뒤 본문이 실행되고, return의 값이 호출식의 값이 됩니다.
  • 값 매개변수는 호출마다 새로 만들어지므로, 함수 안에서 바꿔도 호출한 쪽의 변수는 그대로입니다.
  • 여러 인자를 계산하는 순서는 정해져 있지 않으므로, 순서에 따라 결과가 바뀌는 인자를 함께 넘기지 않습니다.
  • 호출보다 앞에 선언이 보여야 하며, 선언만 있고 정의가 없으면 링크 단계에서 실패합니다.
  • void가 아닌 함수가 값을 돌려주지 않고 끝나면 정의되지 않은 동작이므로, -Wreturn-type 경고를 반드시 고칩니다.

연습 문제

1. 인자 하나 바꾸기

next_value.cpp에서 둘째 호출의 인자만 first에서 original로 바꾸면 출력은 무엇인가요? 마지막 출력문을 지우면 두 호출이 계산한 숫자는 화면에 나타나나요?

해설 보기

둘째 호출의 value도 original의 값 4에서 시작하므로 second는 5이고, 출력은 4 5 5입니다. 출력문을 지우면 아무 숫자도 나타나지 않습니다. 두 return은 여전히 계산 결과를 돌려주지만, 그 값을 화면에 내보내는 것은 출력문의 일입니다.

2. 빠진 반환

sign.cpp에서 마지막 return 0;을 지우면 sign(5), sign(-3), sign(0)은 각각 어떻게 되나요?

해설 보기

sign(5)는 1, sign(-3)은 -1을 정상적으로 돌려줍니다. sign(0)은 두 조건이 모두 거짓이어서 return 없이 본문 끝에 도달하므로 정의되지 않은 동작이며, 어떤 값을 돌려준다고 말할 수 없습니다. GCC의 -Wreturn-type 경고가 이 경로를 알려 줍니다.

3. 어느 단계의 실패인가

declare_first.cpp에서 (가) 맨 위의 선언을 지운 경우와 (나) 선언은 두고 맨 아래의 정의를 지운 경우, 각각 어느 단계에서 실패하나요?

해설 보기

(가)는 main의 호출 지점에서 multiply라는 이름의 선언이 보이지 않으므로 컴파일 단계에서 오류가 납니다. (나)는 선언 덕분에 컴파일은 성공하지만, 링커가 multiply의 정의를 찾지 못해 링크 단계에서 실패합니다. 두 경우 모두 실행 파일이 만들어지지 않습니다.

다음 글 함수 오버로딩에서는 같은 이름의 함수를 매개변수만 다르게 여러 개 정의하고, 컴파일러가 호출에 맞는 함수를 고르는 방법을 다룹니다.