본문 바로가기

Programming languages/C

6. C언어(함수 & 변수)

728x90
반응형

모듈)

모듈이란 독립되어 있는 프로그램의 일부분을 의미한다.

c에서의 모듈은 함수를 의미하는데 각 모듈들은 독자적으로 개발이 가능, 다른 모듈과 독립적으로 변경 가능, 유지보수가 용이, 모듈의 재사용이 가능하다는 것이다. 이렇게 모듈의 개념을 이용하는 프로그래밍을 모듈러 프로그래밍이라고 한다.

 

함수)

함수는 특정한 작업을 수행하는 독립적인 부분이다, 함수를 호출해서 우리는 함수를 사용할 수 있다.

함수는 입력에 따른 출력을 생성해야 한다.

함수를 사용하면 코드가 중복되는 것을 막을 수 있고, 코드의 재사용이 용이하다.

따라서 개발과정도 쉬워진다.

 

프로그램은 여러 개의 함수로 이루어져 있고 이중 가장 먼저 실행되는 함수는 main() 함수이다.

함수들은 호출을 통해서 연결된다.

 

함수는 사용자가 직접 만드는 사용자 정의 함수, 컴파일러에서 제공하는 라이브러리 함수로 구분된다.

또한 함수는 반환 값이 존재하는 반환형, 함수 헤더, 함수 몸체로 구분 가능하다.

함수는 이름과 반환형이 존재한다. 이때 반환형은 return 값의 자료형을 의미한다.

예제를 보면 매개변수에 따라 함수를 통해 연산한 뒤 조건에 따라 x or y를 return 하는데 이것의 자료형이 int라고 미리 컴파일러에게 알려주는 것이다. 만약에 반환 값이 존재하지 않는다면 함수를 void 형으로 선언하면 된다.

 

위의 예시는 main 함수 안에서 add라는 함수의 매개변수에 인수 2,3을 넣어서 호출한 것이고 이를 sum이라는 변수에 반환한다.

main 함수 뿐만 아니라 다른 함수 안에서 또 다른 함수를 호출하는 것도 가능하다.

함수 연산의 결과를 반환해줄 때 반환 값은 하나만 존재해야 한다. 인수는 여러 개가 가능하나 반환 값은 한 개다.

정수, 실수, 문자, 변수, 수식 등 다양한 값을 반환 값으로 가질 수 있다. 함수의 반환형에만 맞게 반환해주면 된다.

함수의 호출이 함수의 정의보다 먼저 발생할 경우 함수 원형이 필요하다.

또한 함수 원형을 선언하면 매개변수에 대한 정보를 알 수 있는데 만약에 매개변수에 옳지 않은 자료형이 들어오거나 인수의 개수가 많거나 적으면 실행 전에 에러를 감지할 수 있다. 이러한 이유 때문에 함수 원형 선언 시 변수명은 생략 가능하지만 자료형은 필히 적어준다. 함수는 계산을 끝냈을 때(함수 정의 반환) 반환 값을 레지스터나 메모리에 값을 임시 저장해 두는데 이 저장된 값을 호출한 함수가 그 위치에서 값을 가져옵니다. 이 과정에서 함수 원형에 int라는 반환 데이터형을 선언하였기 때문에 컴파일러는 이를 근거로 int형 데이터를 가져온다.

 

#include <stdio.h>

int is_prime(int); //함수 원형 선언

int main()
{
    int n = 0;
    printf("숫자를 입력하세요: ");
    scanf("%d",&n);

    int ans = is_prime(n); //소수 판별 함수 호출

    if(ans == 0) //반환값이 0이면 소수 아님
        printf("소수가 아닙니다.");
    
    else //반환값이 1이면 소수
        printf("소수입니다.");

    return 0;
}

int is_prime(int k)
{
    int ck = 1;
    
    for(int i = 2; i < k; i++) //2 ~ k-1로 나누었을때 떨어지는게 존재하면 소수 아님
    {
        if(k % i == 0) {
            ck = 0; //소수가 아닐 경우 ck를 0으로 바꿈
            break;
        }
    }
    if(ck == 1)
        return 1;
    else
        return 0;
}

위 함수는 배운 내용을 바탕으로 소수를 판별하는 함수를 작성하고 호출해본 것이다.

메인 함수 위에 is_prime 함수를 만들어 놓는다면 함수 원형을 선언하지 않고도 오류가 나지 않겠지만 보통 프로그램을 작성하다가 필요한 함수를 그때그때 만들 때가 많기 때문에 함수의 원형 선언을 잘 알아두는 게 좋을 것 같다.

라이브러리 함수)

라이브러리 함수는 컴파일러에서 제공하는 함수이다.

표준 입출력(stdio.h), 수학 연산(math.h), 문자열 처리(string.h), 시간 처리(time.h), 오류처리(errno.h), 데이터 검색과 정렬(stdlib.h) 등이 존재한다.

 

<난수 생성>

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX 45
int main()
{
    int i;
    srand((unsigned)time(NULL)); //시간 기반으로 난수 생성 시드 결정
    for(i = 0; i < 6; i++)
    {
        printf("%d ", 1+rand()%MAX);
    }

    return 0;
}

<기타 라이브러리 함수>

이처럼 라이브러리를 포함한 우리가 함수를 사용해야 하는 이유는 소스코드의 중복을 없애주고, 작성된 함수를 우리가 다른 프로그램에서 호출해서 사용이 가능하다. 또한 복잡한 문제를 단순한 부분으로 분해해서 사용할 수 있다.

 

변수)

변수의 속성은 이름, 타입, 크기,  값 + 범위, 생존시간, 연결 이 존재한다.

범위는 변수가 사용 가능한 범위, 가시성

생존 시간은 메모리에 존재하는 시간

연결은 다른 영역에 있는 변수와의 연결 상태를 의미한다.

 

변수는 전역 변수와 지역 변수가 있다.

전역 변수는 함수 외부에서 정의된다.

지역 변수는 함수 내부에서 정의된다.

 

#include <stdio.h>

int glob = 22; //전역변수
void check(int);

int main()
{
    int i = 10;
    int j = 5;

    printf("main에 존재하는 i: %d\n",i);
    printf("main에 존재하는 j(인수): %d\n",j);

    check(j); //check 함수 호출

    printf("main 안에서 전역변수 glob: %d\n",glob);

    int glob = 110;
    printf("main 안에 전역변수 glob를 지역변수로 덮어 씌움: %d\n",glob);
    printf("전역변수 지역변수화 시킨후 check 호출\n");
    check(j);


    return 0;
}

void check(int a)
{
    int i = 15;
    static int x = 0;
    printf("check 호출한 static x: %d\n",x);
    printf("check에 인수를 받은 수: %d\n",a);
    printf("check에 존재하는 i: %d\n",i);
    printf("check 안에서 전역 변수 glob: %d\n",glob);
    x++;
}

 

위처럼 지역변수는 함수 안에서만 의미가 있다.

i의 경우처럼 변수명이 같더라도 실행되는 함수가 다르기 때문에 각 함수마다 다른 값을 갖는다.

반면에 전역 변수는 두 함수에서 모두 동일한 값을 나타낸다.

전역 변수의 생존 기간은 프로그램의 시작부터 종료까지이다.

지역변수의 생존 기간은 함수의 시작부터 종료까지 이다.

지역변수의 생존기간이 함수의 종료와 같기 때문에 우리는 함수를 통해서 연산한 결과를 return을 통해서 main함수로 반환시키는 것이다.

모든 함수에서 사용되거나 공통적으로 사용하는 경우가 많은 case는 전역 변수로 설정하고 그렇지 않은 경우는 지역 변수로 설정한다.

또한 함수 안에서 전역 변수와 동일한 이름으로 지역변수를 선언하면 함수가 실행되는 동안은 지역변수로 사용이 된다.

폰 노이만 구조를 생각하면 위에서부터 1줄씩 차례대로 읽어내려오기에 위와 같은 현상이 발생한다.

다만 함수가 끝나면 지역 변수의 수명이 끝나기 때문에 전역 변수는 그 외에 다른 함수에서는 동일함이 보장된다.

지역변수와 전역 변수 얘기를 하면 메모리의 구조가 빠질 수 없다.

우리가 봐야 하는 건 data영역과 stack 영역이다.

 

data 영역에는 전역 변수와 정적(static) 변수(일반적으로 변수의 값이 변하지 않는 경우)가 저장된다.

이는 data 영역이 프로그램 시작과 함께 할당되면 프로그램 종료 시 소멸한다는 의미이다.

 

stack 영역에는 지역변수와 매개변수가 저장된다. stack 영역은 함수의 호출과 함께 할당되면 함수의 호출이 완료되면 소멸한다. 이처럼 스택 영역에 저장되는 함수의 호출 정보를 스택 프레임이라고 한다.

 

위 결과창을 보면 static 변수로 선언된 x가 처음 호출에는 0을 출력하고 x++ 연산을 수행한 뒤에 두 번째 호출 때는 1이 되어 있는 것을 알 수 있다. static을 붙이면 지역변수가 정적 변수로 바뀌어서 data영역에 들어가기 때문에 함수가 종료된 후에도 소멸하지 않는 것이다.

 

register를 붙이면 cpu register에 변수를 저장하는 것도 가능하다.

 

#include <stdio.h>

int pass = 1234; //전역변수
int check(int); //함수원형

int main()
{
    int n;
    int ck = 0;
    int cnt = 0;

    while(1)
    {
        if(cnt ==4)
            break;

        printf("4자리 암호 입력: ");
        scanf("%d",&n);
        int key = check(n);

        if(key == 1)
        {
            printf("열렸습니다.");
            ck = 1;
            break;
        }
        else
            cnt = cnt + 1;
    }
    if(ck==0)
        printf("횟수 초과입니다");

    return 0;
}

int check(int a)
{
    if(a == pass)
        return 1;
    else
        return 0;
}

지역변수, 전역 변수, static 변수를 잘 섞어서 특정 횟수 이상 비밀번호를 틀리면 프로그램이 종료되는 경우이다.

 

연결)

연결이란 다른 범위에 속하는 변수들을 서로 연결하는 것이다.

외부 연결, 내부 연결, 무연결이 존재한다. 전역 변수만이 연결을 가질 수 있다.

 

저장 유형 정리)

순환)

순환이란 알고리즘이나 함수 수행 중 자기 자신을 다시 호출해서 문제를 해결하는 기법이다.

대표적인 문제로 피보나치 수열이 있다. 순환은 Recursion 혹은 재귀 호출이라고 부르기도 한다.

피보나치수열뿐만 아니라 DFS 등 다양한 알고리즘에도 많이 적용된다.

 

#include <stdio.h>

int fib(int n)
{
    if(n==1 || n ==2)
        return 1;
    else
        return fib(n-1) + fib(n-2); //재귀호출
}

int main()
{
    int x;
    printf("숫자를 입력하세요: ");
    scanf("%d",&x);
    int ans = fib(x);
    printf("%d번째 피보나치수는 %d 입니다.",x,ans);
    
    return 0;
}
#include <stdio.h>

int factorial(int n)
{
    if(n<=1)
        return 1;
    else
        return factorial(n-1) * n; //재귀호출
}

int main()
{
    int x;
    printf("숫자를 입력하세요: ");
    scanf("%d",&x);
    int ans = factorial(x);
    printf("팩토리얼%d의 값은 %d 입니다.",x,ans);

    return 0;
}

재귀 함수를 사용할 때는 무한 호출이 발생하지 않게 순환을 멈추는 부분이 필요하다.

하노이탑, 피보나치수열, 팩토리얼 3가지는 매우 유명한 재귀 호출 문제이기 때문에 외워두고 있는 것도 좋다.

직관으로 하노이탑의 알고리즘을 바로 생각해 내는 것은 매우 어렵기 때문이다.

나는 호출 순서를 이렇게 기억한다

입력받은 from, tmp, to를 1,2,3으로 한다면

hanoi(1,3,2)

hanoi(2,1,3)

이렇게 기억하면 편하다. 하나 씩 옮길 때마다 n은 1씩 빼주는 것이 자명하니깐.

 

#include <stdio.h>

void print_bin(int);

int main()
{
    int n;
    printf("이진수로 변환할 수를 입력하시오: ");
    scanf("%d",&n);
    print_bin(n);

    return 0;
}

void print_bin(int x)
{
    if(x>0)
    {
        print_bin(x/2);
        printf("%d",x%2);
    }
}

재귀 호출을 이용한 2진수 변환법이다. 일단 재귀 호출의 장점은 메모리를 적게 먹는다는 것이다.

유용한 스킬이니 알아두는 게 좋다.

728x90
반응형

'Programming languages > C' 카테고리의 다른 글

5. C언어(반복문)  (0) 2022.08.11
4. C언어(조건문)  (0) 2022.08.09
3. C언어(수식과 연산자)  (0) 2022.08.09
2. C언어(변수와 자료형)  (0) 2022.08.09
1. C언어(기본,함수,자료형)  (0) 2022.08.09