Post

C & C++ 기초 개념 정리(3) - Funtion

C & C++ 기초 개념 정리(3) - Funtion

Functions

함수(Function)란 프로그램에서 특정 작업을 수행하도록 설계된 코드 블록입니다. 함수는 재사용 가능하며, 코드의 가독성과 유지보수성을 높이는 데 도움을 줍니다. 함수는 입력값(parameter)을 받아 처리한 후 결과값(반환값)을 반환할 수 있습니다.

Predefined Functions

C++ 표준 라이브러리는 cmath, cstdlib, iostream 등 다양한 사전 정의된 함수들을 제공합니다.
해당 라이브러리들을 이용하기 위해선 코드 상단(헤더)에 #include <cmath>와 같이 라이브러리를 불러오는 작업이 필요합니다.

cmath

cmath 헤더는 수학 관련 함수들을 제공합니다. 예를 들어:

  • sqrt(x): x의 제곱근을 반환합니다.
  • pow(base, exp): base의 exp 제곱 값을 반환합니다.
  • sin(x), cos(x), tan(x): 삼각 함수 값을 계산합니다.
  • log(x): 자연 로그 값을 반환합니다.

cstdlib

cstdlib 헤더는 일반적인 유틸리티 함수들을 제공합니다. 예를 들어:

  • abs(x): x의 절대값을 반환합니다.(정수)
    • fabs(x): x의 실수 절대값을 반환합니다.
  • rand(): 난수를 생성합니다.
    • rand() % (max - min + 1) + min: 특정 범위 [min, max]의 정수를 생성합니다.
      • 예를 들어, rand() % 10 + 1은 1부터 10까지의 난수를 생성합니다.
      • (double)(RAND_MAX - rand()) / RAND_MAX
        이와 같이 0에서 1사이의 실수 난수 생성도 가능합니다.
    • srand(seed): 난수 생성기의 시드를 설정합니다.
      • seed 값에 따라 rand() 함수가 생성하는 난수의 순서가 결정됩니다.
      • 예를 들어, srand(time(0))를 사용하면 현재 시간을 기반으로 시드를 설정하여 매번 다른 난수를 생성할 수 있습니다.
      • 동일한 seed 값을 사용하면 항상 동일한 난수 시퀀스를 생성합니다.
  • atoi(str): 문자열을 정수로 변환합니다.
  • exit(status): 프로그램을 종료합니다.

iostream

iostream 헤더는 입출력 스트림을 처리하는 데 사용됩니다. 예를 들어:

  • std::cout: 표준 출력 스트림으로 데이터를 출력합니다.
  • std::cin: 표준 입력 스트림에서 데이터를 읽어옵니다.
  • std::endl: 출력 스트림에 줄바꿈을 추가합니다.
  • std::cerr: 표준 에러 스트림으로 데이터를 출력합니다.

Programmer-defined Functions

프로그래머가 정의한 함수는 사용자가 직접 작성하여 특정 작업을 수행하도록 설계된 함수입니다. 같은 계산을 반복할 때 유용하고, 가독성이 증가해 유지보수에도 도움이 됩니다.
프로그램의 중심이 되는 main함수와 함께 둘 수도 있고, 다른 파일에 정의하여 공유해서 사용할 수도 있습니다.
함수 사용에는 세가지 구성 요소가 있는데. 함수 선언(Function Declaration), 함수 정의(Function Definition), 함수 호출(Function Call)이 있습니다.

Function Declaration

함수 선언은 함수의 이름, 반환형, parameter 목록을 컴파일러에 알려주는 역할을 합니다. 함수 선언은 보통 프로그램의 시작 부분이나 헤더 파일에 작성됩니다.

1
<return_type> FnName(<formal-parameter-list>);

예시:

1
int multiply(int x, int y);
1
double divide(double numerator, double denominator);

함수를 선언만 하는 상황에선, parameter들의 이름 없이 자료형만 명시하는 것도 가능합니다.

1
int multiply(int, int);

함수의 선언은 항상 그 사용보다 먼저여야합니다. 내용이 구현되어있지 않더라도 반드시 선언은 되어있어야 합니다.(따라서 main함수에서 어떤 함수를 사용할 때에는 반드시 main 상단에 선언이 되어있어야합니다.)

Function Definition

함수 정의는 함수의 실제 구현을 포함합니다. 함수가 수행할 구체적 작업을 코드로 작성합니다.

1
2
3
4
<return_type> FnName(<formal-parameter-list>) {
    // 함수의 작업을 수행하는 코드
    return <value>; // 반환값 (필요한 경우)
}

예시:

1
2
3
int multiply(int x, int y) {
    return x * y;
}
1
2
3
4
5
6
7
double divide(double numerator, double denominator) {
    if (denominator == 0) {
        std::cerr << "Error: Division by zero!" << std::endl;
        return 0; // 에러 처리
    }
    return numerator / denominator;
}

함수 정의는 함수 선언과 일치해야 하며, 함수의 반환형과 parameter 목록이 동일해야 합니다.

Function Declaration and Definition Together

함수 선언과 정의를 동시에 작성할 수도 있습니다. 이 경우 함수의 선언과 구현이 한 곳에 작성되므로 코드가 간결해지고, 작은 프로그램이나 간단한 함수에 적합합니다.

이 방식에선 함수는 호출되기 전에 정의되어 있어야 합니다.

1
2
3
4
5
6
7
8
9
10
#include <iostream>

int add(int a, int b) {
    return a + b;
}

int main() {
    std::cout << "Sum: " << add(3, 4) << std::endl; // 출력: Sum: 7
    return 0;
}

함수 선언과 정의를 분리하지 않으면, 헤더 파일을 사용하는 대규모 프로젝트에서는 코드 관리가 어려울 수 있으므로 상황에 따라 적절히 선택해야 합니다.

Function Call

함수 호출은 정의된 함수를 실행하는 작업입니다. 함수 호출 시 필요한 parameter를 전달합니다.

1
2
3
4
5
int result = multiply(3, 4); // multiply 함수 호출
std::cout << "Result: " << result << std::endl;

double quotient = divide(10.0, 2.0); // divide 함수 호출
std::cout << "Quotient: " << quotient << std::endl;

Parameter vs Argument

Parameter는 함수 선언 및 정의에서 사용되는 변수로, 함수가 입력값을 받을 준비를 하는 역할을 합니다. 반면, Argument는 함수 호출 시 실제로 전달되는 값입니다.

예시:

1
2
3
4
5
6
int add(int a, int b) { // a와 b는 parameter
    return a + b;
}

int result = add(5, 10); // 5와 10은 argument
std::cout << "Result: " << result << std::endl;

위 코드에서 ab는 함수 정의에서 사용된 parameter이고, 510은 함수 호출 시 전달된 argument입니다.

Default Arguments

Default arguments는 함수 parameter에 default argument을 제공하는 기능입니다. default argument이 제공된 parameter는 함수 호출 시 argument를 생략할 수 있습니다.

1
2
3
4
5
6
7
8
9
int add(int a, int b = 10) {
    return a + b;
}

int main() {
    std::cout << add(5) << std::endl; // 출력: 15 (b는 default argument 10 사용)
    std::cout << add(5, 20) << std::endl; // 출력: 25 (b는 20으로 대체)
    return 0;
}

default argument은 함수 선언에서 지정하는 것이 일반적이며, 함수 정의에서 지정할 필요는 없습니다. default argument이 지정된 parameter는 항상 default argument이 없는 parameter 뒤에 위치해야 합니다.

1
2
int multiply(int x, int y = 2, int z = 3); // 올바른 선언
int multiply(int x = 1, int y, int z); // 오류: default argument이 없는 parameter 뒤에 default argument이 올 수 없음

만약 함수 declaration 단계에서 default argument를 지정한다면, definition 단계에서 기본값을 적을 필요도 없고, 적어서도 안됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int add(int a, int b = 10);

int main() {
    add(1,2);
    return 0;
}

// int add(int a, int b = 10){ <- compile error
//     return a + b;
// }

int add(int a, int b){ // correct definition
    return a + b;
}

함수간 호출 & 재귀

함수는 다른 함수에서 호출될 수 있으며, 이를 통해 코드의 재사용성을 높일 수 있습니다. 또한, 함수는 자기 자신을 호출할 수도 있는데, 이를 재귀(Recursion)라고 합니다.

함수 간 호출

한 함수에서 다른 함수를 호출하려면, 호출하려는 함수가 선언되어 있어야 합니다. 예를 들어:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int add(int a, int b) {
    return a + b;
}

int multiplyAndAdd(int x, int y, int z) {
    int product = x * y;
    return add(product, z); // add 함수 호출
}

int main() {
    int result = multiplyAndAdd(2, 3, 4);
    std::cout << "Result: " << result << std::endl; // 출력: 10
    return 0;
}

재귀 함수

재귀 함수는 자기 자신을 호출하는 함수입니다. 재귀를 사용할 때는 반드시 종료 조건(Base Case)을 명시해야 무한 루프를 방지할 수 있습니다.

예시: 팩토리얼 계산

1
2
3
4
5
6
7
8
9
10
11
12
int factorial(int n) {
    if (n <= 1) { // 종료 조건
        return 1;
    }
    return n * factorial(n - 1); // 재귀 호출
}

int main() {
    int num = 5;
    std::cout << "Factorial of " << num << " is " << factorial(num) << std::endl; // 출력: 120
    return 0;
}

재귀는 문제를 작은 단위로 나누어 해결하는 데 유용하지만, 호출 스택이 깊어질 경우 스택 오버플로(Stack Overflow)가 발생할 수 있으므로 주의가 필요합니다.

void 함수

void 함수는 반환값이 없는 함수입니다. 함수가 작업을 수행하지만 값을 반환하지 않을 때 사용됩니다. void 함수는 보통 출력, 상태 변경, 또는 다른 부수 효과를 수행하는 데 사용됩니다.

1
2
3
4
5
6
7
8
void printMessage() {
    std::cout << "Hello, World!" << std::endl;
}

int main() {
    printMessage(); // printMessage 함수 호출
    return 0;
}

위 예제에서 printMessage 함수는 단순히 메시지를 출력하며, 반환값이 없습니다.

void 함수는 parameter를 가질 수도 있고, parameter가 없을 수도 있습니다. parameter를 사용하는 경우:

1
2
3
4
5
6
7
8
void greetUser(std::string name) {
    std::cout << "Hello, " << name << "!" << std::endl;
}

int main() {
    greetUser("Alice"); // greetUser 함수 호출
    return 0;
}

위 코드에서 greetUser 함수는 사용자 이름을 parameter로 받아 출력합니다.

void함수는 반환값이 없으므로 어떤 값을 return한다면 컴파일 에러가 발생합니다. 하지만 return;과 같이 반환값이 없음을 명시한다면 오류가 나지 않습니다.

1
2
3
4
5
6
7
8
void func1(int n) {
    return ++n; // compile error
}

void func2(int m) {
    cout << m << endl;
    return; // OK
}

Preconditions and Postconditions

Preconditions는 함수가 호출되기 전에 만족해야 하는 조건을 나타냅니다. 함수가 올바르게 작동하려면 호출자가 이러한 조건을 충족해야 합니다. 예를 들어, 나눗셈 함수에서 분모가 0이 아니어야 한다는 조건이 있을 수 있습니다.

1
2
3
4
5
6
7
8
double divide(double numerator, double denominator) {
    // Precondition: denominator != 0
    if (denominator == 0) {
        std::cerr << "Error: Division by zero!" << std::endl;
        return 0; // 에러 처리
    }
    return numerator / denominator;
}

Postconditions는 함수가 실행된 후 보장해야 하는 조건을 나타냅니다. 함수가 반환하는 값이나 상태가 특정 조건을 충족해야 함을 의미합니다.

1
2
3
4
5
int add(int a, int b) {
    int result = a + b;
    // Postcondition: result == a + b
    return result;
}

Preconditions와 Postconditions는 함수의 신뢰성을 높이고, 코드의 의도를 명확히 하며, 디버깅을 용이하게 합니다. 이러한 조건은 주석으로 명시하거나, 필요에 따라 assert를 사용하여 런타임에 확인할 수도 있습니다.

1
2
3
4
5
6
7
8
#include <cassert>

int subtract(int a, int b) {
    int result = a - b;
    // Postcondition: result + b == a
    assert(result + b == a);
    return result;
}

main 함수

main 함수는 C++ 프로그램의 시작점이자 종료점입니다. 모든 C++ 프로그램은 반드시 하나의 main 함수를 포함해야 합니다. 프로그램 실행 시 main 함수가 호출되며, 함수가 종료되면 프로그램도 종료됩니다.

main 함수의 기본 구조는 다음과 같습니다:

1
2
3
4
int main() {
    // 프로그램의 실행 코드
    return 0; // 프로그램 종료 상태 반환
}

return 0;은 프로그램이 성공적으로 종료되었음을 나타냅니다. 반환값은 운영 체제에 전달되며, 0 이외의 값을 반환하면 오류가 발생했음을 나타낼 수 있습니다.

main 함수의 parameter

main 함수는 parameter를 가질 수 있습니다. 이를 통해 command line argument를 받을 수 있습니다.

1
2
3
4
5
int main(int argc, char* argv[]) {
    // argc: command line argument의 개수
    // argv: command line argument의 배열
    return 0;
}
  • argc는 command line argument의 개수를 나타냅니다.
  • argv는 command line argument의 문자열 배열입니다. argv[0]은 프로그램의 이름을 포함합니다.

예시:

1
2
3
4
5
6
7
8
9
#include <iostream>

int main(int argc, char* argv[]) {
    std::cout << "Number of arguments: " << argc << std::endl;
    for (int i = 0; i < argc; ++i) {
        std::cout << "Argument " << i << ": " << argv[i] << std::endl;
    }
    return 0;
}

위 코드는 command line argument를 출력합니다. 예를 들어, 프로그램을 ./program arg1 arg2로 실행하면 다음과 같은 출력이 나타납니다:

1
2
3
4
Number of arguments: 3
Argument 0: ./program
Argument 1: arg1
Argument 2: arg2

main 함수는 프로그램의 진입점으로, 다른 함수들을 호출하여 프로그램의 논리를 구현합니다. 프로그램의 구조와 흐름을 명확히 하기 위해 main 함수는 간결하게 유지하는 것이 좋습니다.

Scope Rules

Local Scope

변수는 선언된 블록 내에서만 접근할 수 있습니다. 이를 지역 스코프(Local Scope)라고 합니다. 함수 내부에서 선언된 변수는 해당 함수 내에서만 유효합니다.

1
2
3
4
void example() {
    int x = 10; // x는 example 함수 내에서만 유효
    std::cout << x << std::endl;
}

Global Scope

블록 외부에서 선언된 변수는 전역 변수(Global Variable)로, 프로그램 전체에서 접근할 수 있습니다. 하지만 전역 변수 사용은 프로그램의 복잡성을 증가시킬 수 있으므로 주의가 필요합니다.

1
2
3
4
5
int globalVar = 5; // 전역 변수

void printGlobal() {
    std::cout << globalVar << std::endl;
}

만약 어떤 값을 상수로 정해놓고자 한다면 해당 값을 Global로 사용하는 것은 좋은 전략입니다.

1
2
3
4
5
6
7
8
9
10
11
12
const double PI = 3.14159; // 전역 상수

double calculateCircleArea(double radius) {
    // PI는 전역 상수로 모든 함수에서 접근 가능
    return PI * radius * radius;
}

int main() {
    double radius = 5.0;
    std::cout << "Area of circle with radius " << radius << " is " << calculateCircleArea(radius) << std::endl;
    return 0;
}

상수(constant)는 한번 초기화 된 뒤엔 프로그램 수명 중 다시는 변경할 수 없는 값입니다. 일반 변수 선언과 같이 초기화하고 const를 앞에 붙여줄 수 있습니다. 상수는 변수와 달리 반드시 초기화가 필요합니다.

Block Scope

중괄호 {}로 둘러싸인 블록 내에서 선언된 변수는 해당 블록 내에서만 유효합니다.

1
2
3
4
5
6
7
8
int main() {
    {
        int y = 20; // y는 이 블록 내에서만 유효
        std::cout << y << std::endl;
    }
    // std::cout << y; // 오류: y는 블록 밖에서 접근 불가
    return 0;
}

Parameters

함수 parameter에 값을 전달할 때, 목적에 따라 두가지의 접근법을 사용할 수 있습니다.

Call by Value

Call by value는 함수에 argument를 전달할 때, argument의 복사본을 함수에 전달하는 방식입니다. 함수 내부에서 parameter 값을 변경해도 원래 argument에는 영향을 미치지 않습니다. 기본 전달 방식입니다.

1
2
3
4
5
6
7
8
9
10
void modifyValue(int x) {
    x = 10; // x의 값만 변경, 원래 argument는 변경되지 않음
}

int main() {
    int num = 5;
    modifyValue(num);
    std::cout << "num: " << num << std::endl; // 출력: num: 5
    return 0;
}
  • Pitfall
    1
    2
    3
    
    void func(int x) {
      int x;
    }
    

    call by value의 argument는 자동으로 선언되기 때문에, 함수 내에서 선언되면 컴파일 에러로 이어집니다.

Call by Reference

Call by reference는 함수에 argument를 전달할 때, argument의 reference를 함수에 전달하는 방식입니다. 함수 내부에서 parameter 값을 변경하면 원래 argument도 변경됩니다.

1
2
3
4
5
6
7
8
9
10
void modifyValue(int& x) {
    x = 10; // x의 reference를 통해 원래 argument도 변경됨
}

int main() {
    int num = 5;
    modifyValue(num);
    std::cout << "num: " << num << std::endl; // 출력: num: 10
    return 0;
}

Const Reference

참조를 사용하여 값을 전달하되, 함수 내부에서 값을 변경하지 못하도록 할 수도 있습니다. 이를 위해 const 키워드를 사용합니다.

1
2
3
4
5
6
7
8
9
10
void printValue(const int& x) {
    std::cout << "Value: " << x << std::endl;
    // x = 10; // 오류: const 참조는 값을 변경할 수 없음
}

int main() {
    int num = 5;
    printValue(num);
    return 0;
}

Call by value를 이용하지 않고 이와 같은 전략을 사용하는 이유는, call by value는 복사를 이용하기 때문입니다. argumet의 값이 매우 크다면 Const Reference를 활용해 메모리를 아끼는 전략을 사용할 수 있습니다.

Mixed Parameter List

함수 parameter를 전달할 때, Call by Value와 Call by Reference를 혼합하여 사용할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
void processValues(int x, int& y) {
    x = x * 2; 
    y = y * 2; 
}

int main() {
    int a = 5, b = 10;
    processValues(a, b);
    std::cout << "a: " << a << ", b: " << b << std::endl; // 출력: a: 5, b: 20
    return 0;
}

해당 포스트는 서울대학교 전기정보공학부 정교민 교수님의 프로그래밍방법론 25-1학기 강의를 정리한 내용입니다.

This post is licensed under CC BY 4.0 by the author.