본문 바로가기
프로그래밍/C언어

C언어 공부하기 - 스토리지클래스와 프로그램구조

by 헬맷쓰다 2020. 8. 19.
반응형

스토리지클래스

스토리지클래스(Storage Class)는 C언어에서 변수나 함수등의 생명주기와 변수가 어느 범위에서 사용될 수 있는지 그리고 메모리에 어떻게 저장이 되는지에 대한 것을 C언어의 지시어로 설정하는 것입니다.

뭐 이렇게 쓰고나니 초보자가 이해하기는 쉽지 않을 것 같군요. 다음과 같은 예를 들어볼께요.

#include <stdio.h>

void func1();

void main()
{
    int i = 10;    /* 자동변수 (또는 지역변수) */

    func1();
    printf("i = %d\n", i);
}

void func1()
{
    int i = 9;    /* 자동변수 (또는 지역변수) */
    
    printf("i = %d\n", i);
}
PS C:\CS> .\test.exe
i = 9
i = 10

main()함수와 func1()함수의 내부에 동일한 이름의 int i를 선언하고 각각 다른 값으로 초기화 해줬습니다. 이름은 같지만 각기 다른 함수에서 선언하였기 때문에 i 변수는 선언한 함수 내부에서만 사용을 해야하고 함수가 종료하면 변수는 사라져 버립니다. 지금까지 대부분의 예제에서 변수는 함수 안에서 선언한 후 사용을 했는데요, 이런 변수를 자동(auto)변수라고 하고 원래는 auto int i=... 이런식으로 써야하는데 보통 auto를 생략해서 사용합니다. 이 자동변수들은 프로그램의 스택(Stack)이라는 공간에 저장됩니다.

만약에 변수를 모든 함수에서 사용하고 싶다고 한다면, 아래와 같이 함수의 외부에 선언을 해서 사용을 하면 됩니다.

#include <stdio.h>

void func1();
int i;    /* 외부변수 (또는 전역변수)로 사용 */

void main()
{
    i = 9;
    func1();
    printf("i = %d\n", i);
}

void func1()
{
    i = 10;
    printf("i = %d\n", i);
}

C언어 프로그램은 main()함수에서 시작된다고 말씀드렸는데요,,, i에 9를 대입하고 func1()함수를 호출합니다. func1()함수에서는 i에 10을 대입하고 그 값을 출력하고 종료합니다. 다음 main()으로 돌아와서 i값을 출력을 하는데요 어떤 값이 출력될까요? 그렇죠! 10이 출력이 됩니다. int i를 외부변수(또는 전역변수)로 선언을 했으므로 어떤 함수에서 사용하여 값을 변경했다면 그 값이 다른 함수나 다른 블록에서 변경하거나 프로그램이 종료할 때까지 유지가 되고 프로그램의 메모리맵에서 정적데이터 영역에 저장되는 것이 자동변수와 차이점입니다. 

PS C:\CS> .\test.exe
i = 10
i = 10

실제 프로그램을 만들 때, 한 파일에 모든 코드를 다 때려 넣어서 몇만 몇십만 라인의 코딩을 하는 사람도 간혹 보긴 했지만 기능별 모듈을 나눠서 여러 파일로 코딩하는게 일반적입니다. 외부변수를 다른 파일들 간에도 사용을 하려면 어떻게 해야 할까요?

우선 위의 코드를 재활용해볼께요. 대부분의 코드는 동일하고 main()함수 바로 위에 void func2();가 있는데 test2.c에서 정의한 func2()함수를 사용하려고 선언했습니다. 그리고 main() 함수에 func2()함수를 호출해 줬습니다. 파일이름은 test1.c로 저장을 했습니다.

#include <stdio.h>

void func1();
int i;    /* 외부변수 (또는 전역변수)로 사용 */
void func2(); /* test2.c에서 정의한 함수 선언 */

void main()
{
    i = 9;
    func1();
    func2();    /* 외부에서 정의한 함수 호출 */
    printf("i = %d\n", i);
}

void func1()
{
    i = 10;
    printf("i = %d\n", i);
}

다음으로 func2()함수를 정의한 test2.c 파일 내용입니다. 주목해야 할 점은 extern int i; 이 부분입니다. 의미는 test1.c에서 선언한 int i;를 사용하기 위해 외부변수를 사용한다는 선언입니다. func2()함수에서는 외부변수 i에 13을 대입하고 값을 출력해주네요.

#include <stdio.h>

extern int i;

void func2()
{
    i = 13;
    printf("i = %d\n", i);
}

실행결과는 예상대로 다음과 같습니다.

C:\CS>.\test.exe
i = 10
i = 13
i = 13

지금까지 내용을 정리해보면 자동변수와 외부변수가 있는데 자동변수는 함수나 모듈, 블럭내에서 선언하여 사용을 하고 모듈, 블럭을 빠져나가거나 함수가 종료되면 변수가 없어집니다. 외부변수는 함수의 외부에서 선언하며 프로그램이 종료할 때까지 살아있는 변수입니다. 프로그램 전체에서 공통으로 관리할 값이라면 외부변수를 사용하는게 편하고 좋은데 변수 이름을 지을 때 예제처럼 int i; 이런식으로 하는 것보다 외부변수인지 명시적으로 판단할 수 있도록 int g_val; 와 같이 규칙을 정하여 짓는게 좋습니다. 

 

정적변수 (Static variable)

자동(auto)변수는 함수나 모듈이 종료되면 자동 소멸되므로 메모리 절약에 좋기는 하지만 잦은 호출이 있다고 하면 변수의 생성 소멸을 반복하는게 비효율적일 수도 있습니다. 예를 들어 반지름을 입력받아 내부변수에 선언과 초기화를 한 원주율을 가지고 원의 둘레의 길이를 출력한다고 합시다. 원주율을 double pi = 3.141592;로 선언후 초기화를 했는데요. circle_l()함수를 호출할 때마다 원주율 변수는 생성과 소멸이 반복이 됩니다.

void circle_l (int r)
{
    double pi = 3.141592;
    printf ("원의 둘레 : %f\n", 2*d*pi);
}

이 함수를 수만 수억번 호출한다고 하면 변수의 생성소멸도 그만큼 발생하게 됩니다.

void circle_l (int r)
{
    static double pi = 3.141592;
    printf ("원의 둘레 : %f\n", 2*d*pi);
}

원주율 변수 앞에 static이라는 지시어를 붙이면 이 변수는 내부 정적변수이고 변수의 특징은 외부변수의 특징을 갖게 됩니다. 즉, 한번 선언하면 프로그램이 종료할 때까지 소멸하지 않게 되는 거죠.

외부정적변수도 외부변수선언시 static 지시어를 붙여서 사용이 가능합니다. 하지만 앞에서 다룬 여러 파일로 나눠진 경우 다른 파일에서 선언한 정적변수를 사용할 수는 없습니다. 즉, 자신을 선언한 파일에서만 사용할 수 있습니다. 만약 어떤 모듈을 만들때 자신이 선언한 외부변수를 다른 모듈에서 사용하지 못하게 하려면 외부정적변수로 선언을 하면 됩니다.

마지막으로 정적함수(static function)가 있습니다. 외부정적변수와 마찬가지로 정의한 파일에서만 사용하는 함수를 정적함수라고 합니다. 나만 사용하는 함수라고나 할까요? 이에 대한 설명은 다음에 기회가 되면 자세히 하겠습니다.

 

레지스터변수(Register variable)

레지스터? 네 맞습니다. CPU내에 있는 그 레지스터입니다. CPU안에 있는 여분의 레지스터를 이용하여 빠른 연산을 하겠다는 의도입니다.

이걸 사용하려면 변수 선언앞에 register라는 지시어를 붙이면 됩니다. 다음과 같이 반복을 수행하는 경우 사용할 수 있습니다.

{
    register int i;
    
    for (i = 0; i < 100000; i++) { /* 반복 연산 처리에 유용 */
        /* 뭔가 처리 */
    }
}

CPU내의 레지스터의 갯수는 한정되어 있으므로 레지스터 지시어를 사용하여 변수를 여러개 선언하더라도 전부 레지스터 변수로 잡히지는 않습니다. 게다가 요즘 컴파일러 성능이 좋아져서 레지스터 변수를 사용하지 않아도 알아서 코드 최적화를 하기 때문에 현재는 개발자가 특별한 경우가 아니면 사용하지 않습니다.

 

C 프리프로세서 (C Preprocessor)

전처리기라고 해석되는 프리프로세서는 #include 를 이용해서 컴파일시 어떤 파일을 불러올 수도 있고 #define 을 이용하여 상수나 문자열을 정의하거나 조건부 컴파일을 할 수 있습니다.

아시다시피 #include를 이용해서 파일을 삽입하는 예는 모든 소스에서 표준 입출력을 위한 #include <stdio.h>를 포함하는 것을 봤습니다. #include 다음에 <>사이에 들어있는 stdio.h파일은 컴파일러가 알고 있는 include디렉토리에 있습니다. GNU 개발도구를 윈도우 PC의 C:\MinGW에 설치했다면 include 디렉토리는 C:\MinGW\include로 gcc 컴파일러가 알고 있습니다. 개발자가 직접 작성한 파일을 포함하려면 <>가 아니라 ""를 사용할 수도 있습니다. 만약 현재 작성중인 파일이 test.c라고 하고 같은 디렉토리에 test.h를 포함하려고 하면 test.c에 #include "test.h" 이렇게 포함시켜 주어야 합니다.

다음은 #define문을 볼텐데요. 먼저 두개의 정수를 비교해서 큰 값을 돌려주는 함수를 작성해 보겠습니다.

int max (int a, int b)
{
    if (a >= b)
        return a;
    else
        return b;
}

그런데 두개의 실수를 비교해서 큰 값을 돌려주는 함수를 작성한다면 같은 로직을 사용한 데이터 형만 다른 함수를 또 만들어야겠네요.

float max (float a, float b)
{
    if (a >= b)
        return a;
    else
        return b;
}

이를 #define문을 사용하여 다음과 같이 만들 수 있습니다.

#define max(a, b) ((a) >= (b) ? (a) : (b))

(조건) ? ((case1) : (case2))는 삼항연산자로 조건에 맞으면 첫번째 case1을 그렇지 않으면 case2를 반환하는 기능을 합니다. 위의 의미는 함수를 정의한 것이 아니라 max(a, b)를 그 뒤에 있는 문장으로 치환하라는 뜻입니다. 그래서 매크로 치환이라고도 합니다.

매크로치환을 함수 뿐 아니라 상수를 쓸 수도 있습니다.

#define NULL 0x00    /* 널문자 */
#define PI 3.141592  /* 원주율 정의 */
#define HELLO "Hello, World!" /* 문자열 */
#define ERROR -1

#include와 #define 문을 왜 프리프로세서 라고 하나면 컴파일러가 컴파일을 하기 전에 #include로 필요한 파일을 다 포함하고 #define문으로 치환된 문장을 복원하는 전처리를 하기 때문이랍니다.

마지막으로 #define 계열을 이용한 조건부 컴파일에 대해서 간략하게 알아보고 마무리할께요.

#ifndef TEST
#define TEST
/* 뭔가 처리 */
#endif

첫 줄에 #ifndef TEST는 TEST라고 정의되어 있지 않으면 다음줄 #define TEST에서 정의를 하고 #endif까지 처리하라는 의미입니다. 테스트를 위한 코드를 작성해서 테스트 버전 컴파일의 경우 포함했다가 배포버전에서는 뺴고 컴파일하는 등 조건에 따른 컴파일을 할 때 유용합니다.

그 외 여러가지 전처리 지시어가 있습니다만 자주 쓰는건 예를 든 몇가지 입니다. 특히 매크로 함수는 유용하게 쓸 수 있으니 간단한 함수들을 작성해보는 연습을 해보시길 바랍니다.

좀 길어졌는데요. 여기까지 왔다면 C언어 문법의 절반은 지나왔다고 생각합니다. 다음시간에는 C언어 입문의 최대의 난관인 배열과 포인터에 대해서 깊게 알아보도록 합시다. 안녕~

반응형

댓글