C & C++/디딤돌 C언어

Part 16. 함수 개요 그리고 프로그램

언휴 2024. 1. 15. 14:17

Part 16. 함수 개요 그리고 프로그램

52. 함수 개요

함수 개요 - C언어

다양한 형태의 함수
다양한 형태의 함수

소프트웨어를 만들 때 가장 많은 비용이 들어가는 것이 유지 보수 비용이라는 연구 결과가 계속 나오고 있어요.
그리고 점점 유지 보수 비용이 차지하는 비율도 높아지고 있답니다.

프로그램을 제작할 때 유지 보수 비용을 줄이는 여러 가지 방법이 있는데 그 중에 하나가 재사용성을 높이는 것이죠.
여러 프로그램에서 공통으로 사용할 수 있는 것들을 라이브러리로 만들어서 필요할 때 추가하여 사용하는 거예요.
여러 개의 컴포넌트(Component, 부품)로 만들고 필요한 부품을 결합하여 프로그램을 만들어서 재사용성을 높이기도 해요.

그리고 프로그래밍할 때 재사용성을 높이는 기본은 함수를 만들어 사용하는 것이예요.
대부분의 프로그래밍 언어에서는 알고리즘이 같을 때 함수를 만들어 필요할 때 호출하여 사용하는 문법을 제공한답니다.

이번 장에서는 함수가 무엇인지 간단히 알아보고 간단한 알고리즘을 함수로 만드는 실습을 할거예요.
함수를 만드는 실습에서는 작성할 함수의 알고리즘을 파악하는 작업부터 시작하죠.
그리고 함수 이름을 정하고 결과에 영향을 주는 인자를 입력 매개변수 리스트로 정의하고 결과 형식을 약속해요.
그러고 나서 테스트 로직을 작성한 후에 함수 내부를 구현할 거예요.

컴퓨터 프로그램에서 함수는 특정 작업을 수행하는 코드의 집합이예요.
결국 컴퓨터 프로그램이 컴퓨터가 수행할 코드의 집합이므로 단위 함수들의 집합이라고 얘기할 수도 있겠죠.
물론 컴퓨터 프로그램은 수행할 코드 외에도 관리해야 할 데이터도 있어요.
따라서  “함수들의 집합이 컴퓨터 프로그램이다.”라고 말하는 것은 정확한 표현은 아니겠죠.
하지만 그만큼 함수가 차지하는 비중이 크다는 것을 말하는 거예요.

C언어 함수 문법에는 함수에서 해야 할 코드를 정의하는 함수 정의문과 작성한 함수를 호출하여 사용하는 함수 호출문, 함수 수행에 필요한 입력 매개변수 리스트와 리턴 형식을 컴파일러에게 알려주는 함수 선언문이 있어요.

◈ 함수 선언문, 정의문, 호출문

#include <stdio.h>
int Add(int a, int b); //함수 선언문
int main() //main 함수 정의문
{
    int re = 0;
    re = Add(2,3); //Add 함수 호출문
    printf("re:%d\n",re); //printf 함수 호출문
}
int Add(int a, int b)//Add 함수 정의문
{
    return a+b;
}

함수를 만들 때는 함수의 이름과 함수 수행에 필요한 입력 매개변수 리스트, 함수 수행 후에 호출한 곳으로 전달할 반환 형식과 함수에서 수행할 코드를 정의해요.

함수 이름은 호출하여 사용할 개발자가 이해하기 쉽게 함수에서 수행하는 기능이 무엇인지 알기 쉬운 동사로 정의하세요. 그리고 C 언어로 프로그램을 작성할 때는 같은 이름을 갖는 함수를 중복해서 정의할 수 없어요.

입력 매개변수 리스트는 함수를 수행하기 위해 호출하는 곳에서 전달하는 인자예요.
인자에 따라 결과에 영향을 주는 인수들이죠.
만약 두 수를 더하는 함수에서는 호출하는 곳에서 두 개의 수를 전달해야 계산할 수 있어요.
이 때 호출하는 곳에서 전달한 인자를 받기 위해 선언하는 것을 입력 매개변수 리스트라 말해요.
C언어에서 입력 매개변수 리스트는 0개에서 31까지 전달할 수 있어요.
반환 형식은 함수에서 수행할 결과를 호출한 곳에 전달할 값의 형식이예요.
함수에서 호출한 곳으로 반환할 수 있는 값은 최대 1개랍니다.

함수의 이름과 입력 매개변수 리스트, 반환 형식을 합쳐서 함수 원형이라고 불러요.
함수 원형은 함수를 정의하는 곳과 사용하는 곳과의 약속이예요.
호출하는 곳에서는 함수 선언문이 필요하고 함수 원형에 맞게 호출해서 사용해요.
보통 함수 선언문을 헤더에 작성하여 사용하는 곳에서 헤더 파일만 포함하면 사용할 수 있게 만들어요.

함수 정의문은 반환 형식과 함수 이름을 명시하고 지시 연산자 ( ) 안에 입력 매개변수 리스트를 콤마로 구분하여 열거하죠.
그리고 블록을 지정하여 블록 내부에 수행할 코드를 작성해요.
함수는 함수의 종료 블록을 만나거나 return 문을 만나면 수행이 끝나요.
반환 형식이 있으면 return 뒤에 전달할 값의 표현식을 작성하여 전달하죠.

반환 형식함수 이름 (입력 매개 변수 리스트)
{
    [수행할 코드]
}

int Add(int a,int b)//함수 정의문
{
    return a+b;
}

함수 선언문은 함수 정의문에 작성한 함수 원형을 컴파일러에게 알려주는 구문이예요.
컴파일러는 함수 호출문을 만나면 정의한 함수의 원형에 맞게 사용하는지 확인해요.
만약 함수를 사용하는 호출문이 함수 정의문보다 앞에 있으면 컴파일러는 맞게 작성하였는지 확인할 수 없어요.

반환 형식   함수 이름 (입력 매개 변수 리스트);
int Add(int a,int b);//함수 선언문
함수 호출문은 만들어진 함수를 필요할 때 사용하는 구문이예요.
함수를 호출할 때는 함수 원형에 맞게 입력 인자를 전달해야겠죠.
호출하는 곳에서 반환 값을 사용하는 것은 선택 사항이예요.

int i = 3, j=4;
int re = 0;
re = Add(i,j);//함수 호출문
printf("Test 1: %d + %d = %d\n",i, j, re);
re = Add(2,6);//함수 호출문
printf("Test 2: %d + %d = %d\n",2, 6, re);

◈ 두 수를 더하는 함수 선언문, 정의문, 호출문

#include <stdio.h>
int Add(int a,int b);//함수 선언문
int main()
{
    int i = 3, j=4;
    int re = 0;
    re = Add(i,j);//함수 호출문
    printf("Test 1: %d + %d = %d\n",i, j, re);
    re = Add(2,6);//함수 호출문
    printf("Test 2: %d + %d = %d\n",2, 6, re);
    return 0;
}
int Add(int a,int b)//함수 정의문
{
    return a+b;
}

◈ 실행결과

Test 1: 3 + 4 = 7
Test 2: 2 + 6 = 8
 

그리고 C언어로 함수를 작성할 때 함수 내부에서 값을 기억할 필요가 있으면 변수를 선언할 수 있어요.

특정 범위 내의 정수 합계를 구하는 함수를 예로 들어 볼게요.
함수 이름은 CalculateSum이라 가정할게요.

특정 범위 내의 정수 합계를 구하려면 호출하는 곳에서 범위의 시작 값과 범위의 끝 값을 전달해야겠죠.
그리고 수행 결과로 합계를 반환해야겠죠.
따라서 함수의 원형은 다음과 같이 결정하면 적당해요.

int CalculateSum(int begin, int end);

합계를 기억하기 위한 변수와 반복문에서 사용할 카운터 변수가 필요해요.

int sum = 0; //합계
int lcnt = 0; //반복문의 루프 카운터

함수 내부에서는 시작 값에서 끝 값 사이의 합계를 계산하기 위해 반복문이 필요하겠죠.
반복문에서는 초기값을 시작 값으로 하고 끝 값보다 작거나 같을 때까지 1씩 증가하면서 합계에 더하세요.

for( lcnt = begin; lcnt <= end; lcnt++)
{
    sum += lcnt;
}

마지막으로 더한 합계를 반환하세요.

return sum;//결과 반환

◈ 특정 범위 내의 정수 합계를 구하는 함수

#include <stdio.h>
int CalculateSum(int begin, int end);//함수 선언문
 
int main()
{
    int i = 1;
    int j = 100;
    int re = 0;
 
    re = CalculateSum(i,j); //함수 호출문
    printf("Sum(%d ~ %d) : %d\n", i, j, re);
 
    re = CalculateSum(5,100); //함수 호출문
    printf("Sum(%d ~ %d) : %d\n", 5, 100, re);
    return 0;
}
int CalculateSum(int begin, int end) //함수 정의문
{
    int sum = 0; //합계
    int lcnt = 0; //반복문의 루프 카운터
    for( lcnt = begin; lcnt <= end; lcnt++)
    {
        sum += lcnt;
    }
    return sum;//결과 반환
}

◈ 실행결과

Sum(1 ~ 100) : 5050
Sum(5 ~ 100) : 5040

◈ 기본 연습 
1. 두 개의 정수를 입력 매개변수로 전달받아 큰 수를 반환하는 함수를 작성하시오.
그리고 작성한 함수를 이용해서 2, 9 중에 큰 수를 출력하시오.

더보기
//두 개의 정수를 입력 매개변수로 전달받아 큰 수를 반환하는 함수 작성
//그리고 작성한 함수를 이용해서 2, 9 중에 큰 수를 출력
 
#include <stdio.h>
 
int FindBig(int a, int b);
int main(void)
{
    printf("%d\n", FindBig(2, 9));
    return 0;
}
int FindBig(int a, int b)
{
    if (a > b)
    {
        return a;
    }
    return b;
}

2. 하나의 정수를 입력 매개변수로 전달받아 홀수일 때는 0을 반환하고 짝수일 때는 1을 반환하는 함수를 작성하시오.
그리고 작성한 함수를 이용해서 1~10 사이에 몇 개의 짝수가 있는지 출력하시오.

더보기
//하나의 정수를 입력 매개변수로 전달받아 홀수일 때는 0을 반환하고 짝수일 때는 1을 반환하는 함수
//그리고 작성한 함수를 이용해서 1~10 사이에 몇 개의 짝수가 있는지 출력
 
#include <stdio.h>
 
int IsEven(int num);
int main(void)
{
    int i;
    int count = 0;
    for (i = 1; i <= 10; i++)
    {
        if (IsEven(i))
        {
            count++;
        }
    }
    printf("1~10 사이의 짝수 개수:%d\n", count);
    return 0;
}
int IsEven(int num)
{
    return num % 2==0;
}

3. 하나의 정수를 입력 매개변수로 전달받아 소수일 때는 1을 반환하고 그렇지 않을 때는 0을 반환하는 함수를 작성하시오.
여기서 소수란 1과 자기 자신만을 약수로 갖는 수를 말합니다.
그리고 작성한 함수를 이용해서 1~100 사이에 몇 개의 소수가 있는지 출력하시오.

더보기
//하나의 정수를 입력 매개변수로 전달받아 소수인지 판별하는 함수 작성
//작성한 함수를 이용해서 1~100 사이에 몇 개의 소수가 있는지 출력
 
#include <stdio.h>
int IsPrime(int n);
int main(void)
{
    int i;
    int count = 0;
    for (i = 1; i <= 100; i++)
    {
        if (IsPrime(i))
        {
            count++;
        }
    }
    printf("1~100 사이의 소수는 %d개\n", count);
    return 0;
}
int IsPrime(int n)
{
    int i;
    for (i = 2; i < n; i++)
    {
        if (n%i == 0)
        {
            return 0;
        }
    }
    return 1;
}

4. 두 개의 정수를 입력 매개변수로 전달받아 최소 공배수를 구하는 함수를 작성하시오.

더보기
//최소 공배수를 구하는 함수 작성
#include <stdio.h>
int FindLCM(int a, int b);
 
int main(void)
{
    int a, b;
    int lcm;
    
    printf("두 개의 정수 입력:");
    scanf_s("%d%d", &a, &b);
 
    lcm = FindLCM(a, b);
    if (lcm == -1)
    {
        printf("int 형식 표현 범위에서 최소 공배수를 구할 수 없습니다.\n");
    }
    else
    {
        printf("%d와 %d의 최소 공배수는 %d\n", a, b, lcm);
    }
 
    return 0;
}
 
int FindLCM(int a, int b)
{
    int lcm;
    if (a > b)//a가 b보다 크면
    {
        //두 수를 교환
        int temp = a;
        a = b;
        b = temp;
    }
    for (lcm = b; ; lcm += b)
    {
        if (lcm%a == 0)
        {
            return lcm;
        }
    }
    return -1;    
}

5. 두 개의 정수를 입력 매개변수로 전달받아 최대 공약수를 구하는 함수를 작성하시오.

더보기
//최대 공약수를 구하는 함수 작성
#include <stdio.h>
int FindGCD(int a, int b);
 
int main(void)
{
    int a, b;
    int lcm;
 
    printf("두 개의 정수 입력:");
    scanf_s("%d%d", &a, &b);
 
    lcm = FindGCD(a, b);
    if (lcm == -1)
    {
        printf("최대 공약수를 구할 수 없습니다.\n");
    }
    else
    {
        printf("%d와 %d의 최대 공약수는 %d\n", a, b, lcm);
    }
 
    return 0;
}
 
int FindGCD(int a, int b)
{
    int gcd;
    if (a > b)//a가 b보다 크면
    {
        //두 수를 교환
        int temp = a;
        a = b;
        b = temp;
    }
    for (gcd = a; ; gcd--)
    {
        if ((a%gcd==0)&&(b%gcd == 0))
        {
            return gcd;
        }
    }
    return -1;
}

53. 프로그램 생명 주기

 

프로그램 생명 주기, 프로세스 메모리 구조, 프로그램 동작 원리

이 부분은 프로그램 개발자들에게 공통적으로 필요한 기반 지식이예요.
직접적으로 프로그래밍 작성 능력과 관련있는 내용은 아니예요.
하지만 전산 엔지니어로써 기본적으로 알면 여러 분야에서 도움이 되는 내용이예요.
이 책에서는 깊은 내용까지 들어가지 않고 간단히 소개하기로 할게요.

프로그램 생명 주기
실행 상태의 프로그램을 프로세스라 불러요.
따라서 프로그램 생명 주기보다는 프로세스 생명 주기라는 말이 더 적당할거예요.
편의상 프로그램 생명 주기라 할게요.

프로그램은 수행해야 할 코드를 메모리에 로딩(loading)하는 것에서 출발하죠.
로딩은 하드 디스크나 CD 등의 저장 장치에 있는 데이터를 프로그램의 메모리에 옮기는 작업을 말해요.
또한 프로그램전체 영역에서 사용할 수 있는 전역 변수를 위한 메모리를 할당하고 초기화 작업을 수행해요.
그리고 프로그램 시작에 필요한 다른 초기 작업을 수행한 후에 개발자가 정의한 main 함수부터 시작한답니다.

콘솔 응용 프로그램은 시작하면서 표준 입력(stdin)과 표준 출력(stdout)을 여는 작업도 수행해요.
printf 함수를 호출하여 콘솔 화면에 데이터를 출력할 수 있는 이유도 시작하면서 표준 출력을 열어주기 때문이죠.
아시는 것처럼 개발자가 작성한 코드 중에서 제일 먼저 수행하는 코드가 main 함수예요.
이러한 이유로 main 함수를 진입점이라 부르죠.

프로그램은 기본적으로 하나의 코드를 수행하고 난 후에 바로 뒤에 있는 코드를 수행해요.
그런데 함수를 호출하면 호출한 함수를 정의한 코드를 수행하고 수행 후에는 호출한 코드 바로 뒤에 있는 코드를 수행한답니다.
이와 같이 동작이 가능한 이유는 함수를 호출하는 코드를 만나면 다음 수행 코드를 스택에 저장해 두었다가 피호출 함수가 끝나면 스택에 저장해 두었던 코드를 꺼내와서 그 위치부터 수행하기 때문이예요.
참고로 스택은은 LIFO(Last In First Out) 방식으로 자료를 보관하는 저장소예요.
스택은 LIFO 방식이어서  최근에 저장한 데이터를 먼저 꺼내줘요.

프로그램은 main 함수의 return 문이나 main 함수의 종료 블록 }을 만나면 끝나요.
물론 프로그램 중간에 종료하는 exit 함수를 호출하거나 비정상적인 상황이 발생하여 끝날 수도 있어요.

콘솔 응용 프로그램은 프로그램이 정상적으로 끝날 때 열려져 있는 모든 파일을 닫아주는 등의 후처리 작업을 수행해요.
자세한 사항은 Unix System Programming에 관한 책을 통해 학습해 보세요.

54. 프로세스 메모리 구조

이번에는 간단하게 프로그램 개발자들이 알아야하는 프로세스(실행 중인 프로그램) 메모리 구조를 알아볼게요.

프로세스의 주요 메모리는 크게 Text, Data, BSS, Stack, Heap으로 구분할 수 있어요.

프로그램의 코드는 Text 영역에 잡히고 전역 변수는 Data 영역에 잡히죠.
이 두 개의 영역의 메모리 크기는 컴파일 시점에 결정해서 프로세스 동작 중에는 크기가 변하지 않아요.
그런데 함수 호출에 의한 지역 변수나 호출 처리에 필요한 메모리는 런 타임(프로세스 동작 시)에 잡히죠.
피 호출 함수가 끝나면 호출한 함수로 복귀하고 자신의 메모리를 해제하기 위해 Stack에 잡혀요.
이 외에 앞으로 배울 동적 메모리 할당 함수 호출하면 Heap 메모리에 잡히죠.

Text 메모리에는 프로그램에서 수행할 코드가 로딩되는 메모리 영역으로 변하지 않는 부분이예요.
Data 메모리와 BSS 메모리는 전역 변수(함수 외부에 선언한 변수로 프로그램 전체 영역에서 사용 가능)와 정적 변수(static 키워드를 명시하여 선언한 변수)를 위한 영역이죠.
Data 메모리는 초기값을 설정한 전역 변수와 정적 변수를 위한 영역이예요.
BSS 메모리는 초기값을 설정하지 않은 전역 변수와 정적 변수를 위한 영역으로 0으로 자동 초기화해 줘요.

Stack 메모리는 지역 변수와 프로그램 수행 흐름을 위한 데이터를 임시로 저장하는 영역이예요.
Heap 메모리는 프로그램 동작 중에 동적으로 메모리를 할당하여 사용할 수 있는 영역이예요.
동적 메모리 할당에 관한 사항은 뒤에서 배울 내용이죠.

따라서 Text, Data, BSS 영역은 프로그램 로딩할 때 크기를 결정하며 수행 중간에 크기 변화는 없어요.
하지만 Stack과 Heap 영역은 프로그램 수행 중에 메모리 크기가 변해요.

이보다 더 자세한 내용은 Unix System Programming 관한 책을 통해 학습하세요.

프로세스 메모리 구조
프로세스 메모리 구조

55. 프로그램 동작 원리

CPU 내부와 프로그램 동작 원리
CPU 내부와 프로그램 동작 원리

이번에는 CPU 내에서 어떻게 프로그램 코드가 동작하는지 간단히 알아볼게요.

프로그램을 실행하면 운영체제(O/S)는 프로그램 이미지를 메모리에 로딩하여 프로세스를 만들죠.
프로세스는 생성하면서 초기 작업을 수행하고 난 후에 개발자가 작성한 진입점 코드를 수행한답니다.

컴퓨터 CPU에는 수행할 코드 주소를 기억하는 PC(프로그램 카운터) 레지스터(CPU 내부의 데이터 기억 장치)가 있어요.
CPU에서는 PC 레지스터에 있는 코드를 디코딩하여 수행할 명령어와 데이터를 분리하는 작업을 선행하죠.
그리고 ALU에서 실제 작업을 처리해요.
ALU(Arithmetic Logic Unit, 산술 논리 처리 장치)는 산술 논리를 처리하는 장치예요.
그리고 ALU에서 처리한 결과를 범용 레지스터(데이터나 메모리 주소 등을 기억할 수 있는 레지스터)에 출력해요.
이러한 작업이 끝나면 PC 레지스터의 코드 주소를 다음 주소로 증가시켜 같은 작업을 반복 수행한답니다.

따라서 프로그램에 작성한 코드는 대부분의 구문은 작성한 순서로 수행해요.
하지만 선택문이나 함수 호출문은 수행할 코드 주소를 기억하는 PC 레지스터의 값을 직접 변경하는 작업을 수행해요.
이렇게 PC 레지스터 값을 직접 변경하는 구문을 분기문이라 불러요.
특히 함수 이름은 함수 정의문에 작성한 코드의 시작 메모리 주소를 의미하여 배열 이름처럼 포인터 상수로 취급한답니다.