Home C언어 (13) 함수2
Post
Cancel

C언어 (13) 함수2

C언어 (13) 함수2

함수의 선언 방법을 알아보았고, 어떻게 사용하는지 좀 더 알아보자.
또 변수의 선언 위치에 따른 차이점과 종류를 알아보자.

변수의 종류와 범위

변수는 선언되는 위치나 종류에 따라 메모리 상에 존재하는 기간이 다르다.

  • 지역 변수
  • 전역 변수
  • 정적 변수
  • 외부 변수
  • 레지스터 변수

이렇게 5가지로 나눌 수 있고 각각 무슨 의미를 가지는지 알아보자.

지역 변수

지역 변수(Local Variable)은 함수, 조건문, 반복문의 중괄호 내부, 함수의 매개 변수등을 의미한다.
쉽게 중괄호로 감싸진 부분을 지역이라고 해보자.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
void out() {
    int ans = 20;
    printf("out 함수의 ans : %d\n", ans);
}
int main() {
    int ans = 10;
    out();
    printf("main 함수의 ans : %d\n", ans);
    return 0;
}

위의 코드에서는 out() 지역에 ans가 있고, main()지역에도 ans가 있다.
ans = 10이 된 후에 out()이 실행되어 ans=20으로 바뀐다. 하지만 main의 ans는 여전히 10이다.

1
2
out 함수의 ans : 20
main 함수의 ans : 10

실행시켜보면 2개의 ans는 이름은 같지만 다르다는 것을 알 수 있다. 다른 예제를 또 보자.
image

평범한 반복문이고, 탈출 후에 i를 출력하려고 하지만 i가 정의되어 있지 않다고 한다.
그 이유는 반복문의 조건문에서 int i를 해주었기에 i는 반복문 블럭 안에서만 존재한다.
이 블럭을 나오면 i의 공간은 사라진다.
이렇게 한 코드 블럭에서만 사용 가능한 변수를 지역 변수라고 부른다.

지역 변수의 메모리 생성 시점 : 중괄호 내에서 선언될 때
지역 변수의 메모리 생성 시점 : 중괄호를 탈출할 때

전역 변수

그러면 전역 변수(Global Variable)은 뭘까?
예상할 수 있겠지만 중괄호와 상관 없이 모든 곳에서 접근이 가능한 변수이다.
선언 위치는 중괄호 외부이고, 값의 초기화를 안해도 쓰레기 값이 안 들어간다는 특징이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int ans;
void out() {
    ans = 20;
    printf("out 함수의 ans : %d\n", ans);
}
int main() {
    printf("초기화 전의 ans : %d\n", ans);
    ans = 10;
    out();
    printf("main 함수의 ans : %d\n", ans);
    return 0;
}
1
2
3
초기화 전의 ans : 0
out 함수의 ans : 20
main 함수의 ans : 20

ans는 중괄호 밖, 가장 위에 전역 변수로 선언이 됐다.
우선 초기화를 하지 않고 ans를 출력해도 0이 나오는 것을 볼 수 있다.
ans = 10 후에 out()함수가 실행되어 ans=20이 된다.
out()의 ans와 main()의 ans는 동일하기에 결과는 둘 다 20이 출력된다.
이렇게 범위에 제한 받지 않고 사용할 수 있는 변수를 전역 변수라고 부른다.

사용할 때 주의할 점으로 전역 변수는 상수로 초기화해야 한다.

1
2
3
#include <stdio.h>
int ans = 3;
int ans2 = ans;

와 같은 초기화 방법은 에러가 발생한다.
또 전역 변수를 너무 많이 사용하면 모든 함수에서 사용하다 잘못된 접근이 발생하기 쉽다.
그렇기에 꼭 필요한 경우에만 사용해줘야 한다.

전역 변수의 메모리 생성 시점 : 프로그램이 시작할 때
전역 변수의 메모리 생성 시점 : 프로그래밍 종료될 때

정적 변수

전역 변수는 모든 영역에서 접근이 가능하고, 프로그램이 종료될 때까지 메모리가 남아있다.
단점으로는 프로그램에서 잘못된 접근으로 찾기 힘든 에러를 발생시킬 수 있다.
이런 단점을 부분적으로 보완한 변수가 정적 변수(Static Variable)이다.

정적 변수는 변수의 자료형 앞에 static 키워드를 넣어서 만든다.
전역 변수처럼 프로그램이 종료될 때까지 메모리가 남아있고, 자동으로 0으로 초기화된다.
또 상수로 반드시 초기화 해야 한다.
정적 변수만의 특징은 프로그램이 시작되고 초기화는 딱 한 번만 수행된다는 것이다.

1
static int num;
1
2
int a;
static int num = a; //오류 발생. 정적 변수도 상수로 초기화해야 한다.

예제 코드로 정적 변수가 어떻게 움직이나 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void count() {
    static int x = 0;
    int y = 0;
    x++;
    y++;
    printf("x값 : %d, y값 : %d\n", x, y);
}
int main() {
    count();
    count();
    count();
    return 0;
}
1
2
3
x값 : 1, y값 : 1
x값 : 2, y값 : 1
x값 : 3, y값 : 1

main()은 count()를 3번 부르고, count()는 x, y를 1씩 증가시킨다.
이 때 x는 정적 변수라 맨 처음 count()에서 0으로 초기화한 뒤 다시 초기화되지 않는다.
그렇기에 count()가 또 호출되도 0이 되지 않고 1, 2, 3으로 증가한다.
y는 지역 변수이기에 0으로 초기화됨을 볼 수 있다.

그러면 정적 변수는 왜 사용하는 것일까?
어떤 함수의 내부나 조건문, 반복문 같은 지역에서 전역 변수의 기능을 사용하고 싶을 때 사용한다.
함수 안에서 누적 계산이나 호출 횟수를 세기 위해서 이전 호출의 상태를 기억하는 정적 변수가 필요하다.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
    while (1) {
        static int num = 0;
    }
    printf("%d", num);
    return 0;
}

위의 코드는 정적 변수가 while문 안에 선언되었기에 사용 가능한 범위는 while문 뿐이다.
그 범위를 벗어나 사용하려고 하면 오류가 발생한다. 이런 변수를 정적 지역 변수라고 한다.

정적 지역 변수의 메모리 생성 시점 : 중괄호 내에서 초기화될 때
정적 지역 변수의 메모리 생성 시점 : 프로그래밍 종료될 때

외부 변수

외부(Extern) 변수는 의미대로 현재 파일이 아닌 외부 파일에 선언된 변수를 참조한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<file.c>
#include <stdio.h>
int main() {
    extern int num1;
    printf("%d", num1);
    return 0;
}

<file2.c>
#include <stdio.h>
int num1=10;
void func() {
	printf("..");
}

이렇게 c 소스 코드가 2개가 있을 때 file.c에서 외부 변수를 통해 file2.c의 num1을 출력할 수 있다.
그러면 어떤 변수를 외부에서 참조하지 못하도록 막고 싶다면 어떤 방법이 있을까?
file2.c의 전역 변수를 static int num1 = 10로 바꿔주면 정적 전역 변수로 바꿀 수 있다.
이러면 외부 파일에서 참조할 수 없는 변수가 된다.

정리

구분지역 변수전역 변수정적 지역 변수정적 전역 변수
선언 위치중괄호 내부중괄호 외부중괄호 내부중괄호 외부
메모리 생성 시점중괄호 내부프로그램 시작 시중괄호 내부프로그램 시작 시
메모리 소멸 시점중괄호 탈출프로그램 종료 시프로그램 종료 시중괄호 탈출
사용 범위중괄호 내부프로그램 전체중괄호 내부선언된 소스 파일
초기화하지 않으면쓰레기 값0으로 자동 초기화0으로 자동 초기화0으로 자동 초기화

이렇게 자료형 뿐만 아니라 선언 위치에 따라 변수는 다양한 용도로 사용할 수 있다.

레지스터 변수

레지스터 변수는 조금 다른 변수이다.
레지스터란 CPU 내부의 임시 기억 장소로, 그곳에 변수를 할당해 값을 저장한다.
보통 우리가 선언하는 변수들은 CPU 외부의 RAM에 저장하기에 입출력 속도가 느리다.
레지스터는 CPU 내부에 있기에 메모리에 비해 처리 속도가 빠르다.

하지만 레지스터는 공간이 작기 때문에 많은 양의 레지스터 변수를 선언할 수는 없다.
그렇기에 메모리의 생성과 소멸이 빈번한 변수만 레지스터 변수로 사용하면 효과적일 수 있다.
대부분 컴파일러가 빈번한 변수에 대해 자동으로 레지스터 변수로 설정하기에 직접 지정하진 않아도 된다.
따라서 현대에는 코드로 레지스터 변수를 명시적으로 하나, 그냥 지역 변수로 하나 속도는 비슷하다.
이런게 있구나~라는 것만 알아두자.

변수의 저장 영역

image

C언어의 변수는 일부를 제외하고는 RAM (메모리)영역에 저장된다.
메모리 구조는 크게 코드, 스택, 데이터, 힙 영역으로 나눌 수 있다.
컴퓨터 구조, 운영체제를 공부하면 더 자세히 알 수 있다.

함수의 사용 방법

저번 글에서는 간단하게 숫자를 더하는 함수나 세는 함수만 이용해봤다.
함수를 부품이라고 생각하면, 그것을 조립해서 하나의 프로그램을 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
void goToDesk(); 
void lookAtBoard(); 
void pickUpPen(); 
void writeOnBoard(); 

// 1. 책상으로 가시오
void goToDesk() {
    printf("책상으로 가시오\n");
    lookAtBoard();  // 칠판을 보시오를 호출
}

// 2. 칠판을 보시오
void lookAtBoard() {
    printf("칠판을 보시오\n");
    pickUpPen();  // 펜을 들어라를 호출
}

// 3. 펜을 들어라
void pickUpPen() {
    printf("펜을 들어라\n");
    writeOnBoard();  // 글을 써라를 호출
}

// 4. 글을 써라
void writeOnBoard() {
    printf("글을 써라\n");
}

int main() {
    // 첫 번째 함수 호출, 나머지는 각 함수에서 다른 함수를 호출
    goToDesk();

    return 0;
}

이렇게 main()에서는 하나의 함수만 불렀지만 각각의 함수들이 서로를 호출한다.
이런 식으로도 함수를 자주 사용한다.

재귀 함수

함수의 장점에서 마지막에 있던 재귀 함수(Recursive Function)이다.
이름에서 알 수 있듯이 함수내에서 자기 자신을 호출하는 함수를 재귀 함수라고 한다.
이것을 잘 이용하면 복잡한 알고리즘 문제를 해결할 수 있지만, 자기 자신을 호출하기에 시간과
메모리 공간의 효율이 저하될 수 있다. 따라서 사용할 때 주의해야 한다.

간단하게 자기 자신을 계속 호출하는 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
void self(); 
int main() {
    self();

    return 0;
}
void self() {
    printf("안녕\n");
    self();
}

이렇게 자기 자신을 계속 호출하다보면 메모리가 부족해지고, 그 때 프로그램이 끝난다.
함수는 호출될 때마다 지역 변수, return 주소 등이 메모리 영역의 스택에 쌓인다.
위의 코드는 self()를 무한히 호출하기에 언젠가 스택이 가득찰 때까지 저장하고 결국 넘쳐 오류가 발생한다.

그렇기에 반복문처럼 종료 조건을 만들어줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
void self(); 
int main() {
    int a = 1;
    self(a);

    return 0;
}
void self(int i) {
    printf("안녕 %d회\n", i);
    if (i > 5)return;
    self(i + 1);
}

이렇게 하면 self는 int형 매개변수를 입력으로 받는 함수가 되었다.
매개변수 i는 맨 처음 1이고, 다음은 2.. 이렇게 늘어나다 i>5가 되면 return으로 함수를 종료한다.

그런데 이건 반복문으로도 똑같이 할 수 있는 것이 아닌가? 라는 생각이 든다.
팩토리얼과 피보나치수를 구하는 가장 대표적인 재귀 코드를 보며 어떻게 사용하는지 보자.

1
2
3
4
5
6
7
8
9
10
11
<팩토리얼>
#include <stdio.h>
int fact(int n);
int main() {
    printf("%d", fact(5));
    return 0;
}
int fact(int n) {
    if (n == 1)return 1;
    return n * fact(n - 1);
}

5!을 구하는 코드이다. 순서대로 보면 5 * fact(4)를 반환하고, fact(4)는 4*fact(3)을 반환한다.
이것이 이어지면 5 * 4 * 3 * 2 * fact(1)이 되고, fact(1)은 1을 반환하므로 결과는 5!와 같다.

1
2
3
4
5
6
7
8
9
10
11
<피보나치>
#include <stdio.h>
int fib(int n);
int main() {
    printf("%d", fib(5));
    return 0;
}
int fib(int n) {
    if (n == 1 || n==2)return 1;
    return fib(n - 2) + fib(n - 1);
}

이것은 1, 1, 2, 3, 5, 8, 13… 인 피보나치 수열의 5번째 값을 구하는 코드이다.
그림으로 표현하면 밑처럼 함수를 호출하고, 5라는 값을 반환할 것이다.

image

그림에서도 알 수 있다시피 재귀함수의 문제는 반복 횟수가 커질수록 반복 횟수도 많아진다.
위의 코드에서 fib(50)정도의 수를 넣고 코드를 실행해보면 반복 횟수가 엄청 많아 결과가 안 나올것이다.
이제 이런것을 해결하기 위한 방법도 있는데, 그건 알고리즘을 공부하면서 더 알아보면 좋을 것 같다.


이렇게 길었던 함수도 다 끝냈다.
내용은 엄청 많지 않았는데 예시 코드로 보는게 좋을 것 같아서 이것저것 넣다보니 길어졌다.
이제 함수와 포인터의 연관까지 보면 C언어의 기본은 다 끝낸 것 같다.

This post is written by PRO.