10. Program Organization

하모씨·2021년 10월 25일
0

KNKsummary

목록 보기
9/23

1. Local Variables

함수의 body에 선언된 변수를 함수에 지역적(local)이라고 말한다. 아래의 함수에서 sum은 지역 변수(local variable)이다.

int sum_digits(int n)
{
    int sum = 0;    /* local variable */
    
    while (n > 0)
    {
        sum += n % 10;
        n /= 10;
    }
    
    return sum;
}

기본적으로, 지역 변수는 아래의 특성을 가진다.

  • automatic storage duration
    • 변수의 storage duration(or extent)은 프로그램 실행 중의 부분에서 변수가 존재하는 공간을 말한다.
      지역 변수에 대한 공간은 둘러싸는(enclosing) 함수가 호출되었을 때 "자동적으로" 할당되고, 함수가 반환되었을 때 할당 해제된다. 그래서 변수는 automatic storage duration을 가진다고 말한다. 지역 변수는 지역 변수를 둘러싸고(enclosing) 있는 함수가 반환되면 더 이상 값을 가지지 않는다. 함수가 다시 호출되었을 때, 그 변수가 예전의 값을 가지고 있을거라는 보장은 없다.
  • Block scope
    • 변수의 범위(scope)는 변수가 참조될 수 있는 프로그램의 텍스트 부분이다. 지역변수는 Block scope를 가지는데, 이는 변수가 선언된 지점부터 공간을 둘러싸는(enclosing) 함수의 끝부분까지만 인식할 수 있다. 지역 변수의 scope는 지역 변수가 속해있는 함수의 너머로 확장될 수 없기 때문에, 다른 함수에서 다른 목적으로 같은 이름을 사용할 수는 없다.

Section 18.2에서 이것들과 더 자세한 관련된 개념들을 볼 것이다.

C99는 함수의 처음 부분에 변수 선언이 올 필요가 없기 때문에, 지역 변수가 엄청나게 작은 scope를 가지는 것이 가능하다. 아래의 예시에서는 i가 선언되는 행 전까지 i의 scope가 시작하지 않는다. 또 i의 scope는 함수 body의 끝부분과 가까울 수 있다.

void f(void)
{
    ...
    int i;...-- scope of i
}

Static Local Variables

단어 static을 지역 변수의 선언에 넣는 것은 automatic storage duration 대신에 static storage duration을 가지도록 한다. static storage duration을 가진 변수는 영구적인 storage location을 가지게 되고, 그래서 프로그램의 실행 내내 값을 유지한다. 아래의 함수를 보자.

void f(void)
{
    static int i;    /* static local variable */
    ...
}

지역 변수 istatic으로 선언되었기 때문에, 프로그램의 실행 내내 동일한 메모리 위치를 차지하게 된다. f가 반환되었을 때, i는 스스로의 값을 잃지 않는다.
정적 지역 변수(static local variable)은 여전히 block scope를 가지는데, 그래서 다른 함수에서는 이를 인식할 수 없다. 간단히 말해서, 정적 변수는 다른 함수로부터 데이터를 숨기는 장소이지만, 같은 함수를 미래에 또 호출하면 값을 유지하고 있다.

Parameters

parameters는 automatic storage duration과 block scope와 같은 지역 변수의 특성과 동일한 특성을 가진다. 사실, parameter와 지역 변수의 실질적인 차이는 각각의 함수가 호출되었을 때, 각각의 parameter가 자동적으로 초기화된다는 점이다(대응되는 argument의 값을 대입하는 것으로).

2. External Variables

argument를 전달하는 것은 함수에 정보를 전달하는 하나의 방법이다. 함수는 외부 변수(external variable)를 통해서도 전달할 수 있다. external variable은 함수의 body 바깥에 선언된 변수이다.
external variable(또는 global variables로도 불림)의 특성은 지역 변수와 다른데, 아래의 특성을 가진다.

  • Static storage duration

    • external variable은 static storage duration을 가지는데, 이는 static으로 선언된 지역 변수와 비슷하다. external variable에 저장된 값은 무기한으로 거기에 유지된다.
  • File scope

    • external variable은 file scope를 가진다. 선언된 지점부터 공간을 둘러싸는 파일의 끝부분까지 인식이 가능하다. 결과적으로 external variable은 변수 선언의 이후에 있는 모든 함수에 의해 접근 가능하다(그리고 잠재적으로 수정이 가능하다).

Example: Using External Variables to Implement a Stack

external variables가 어떻게 사용될 수 있는지 설명하기 위해서, stack이라고 알려진 데이터 구조를 보자(stack은 추상적인 개념이며 C의 특징은 아니다. 다양한 프로그램 언어에서 구현될 수 있다). stack은 배열처럼 같은 자료형인 다수의 데이터를 저장할 수 있다. 그러나 stack에서의 작동은 한정적이다. stack top이라고 부르는 stack의 끝부분에 항목을 push(추가)하거나, stack의 동일한 끝부분에서 항목을 pop(제거)할 수 있다. stack의 top이 아닌 다른 항목을 수정하거나 검사하는 것은 금지되어있다.

C언어에서 stack을 구현하는 하나의 방법은 우리가 contents라고 부를 배열에 항목을 저장하는 것이다. top이라는 이름을 가진 분리된 정수형 변수가 stack top의 위치를 표시할 것이다. stack이 비어있을 때, top은 0의 값을 가진다. 항목을 stack에 push하기 위해서는, 우리는 contents에서 top에 의해 표시되는 위치에 간단하게 항목을 저장하면 되고, top을 증가시키면 된다. 항목을 pop하는 것은 top을 감소시키고, 그 후 감소된 top은 pop된 항목을 덮어쓰기 위해 contents의 index가 된다.

이 개요를 바탕으로, 프로그램의 조각이 아래의 있다(완성된 프로그램이 아니다). stack에 대한 contentstop 변수 선언하고, stack의 동작을 표현하는 함수의 집합을 제공한다. top 변수에 접근하기 위해 필요한 5개의 함수와, contents에 접근하기 위해 필요한 2개의 함수가 필요한데, 그래서 우리는 contentstop을 external로 만들 것이다.

#include <stdbool.h>    /* C99 only */

#define STACK_SIZE 100

/* external variables */
int contents[STACK_SIZE];
int top = 0;

void make_empty(void)
{
    top = 0;
}

bool is_empty(void)
{
   return top == 0;
}

bool is_full(void)
{
    return top == STACK_SIZE;
}

void push(int i)
{
    if (is_full())
        stack_overflow();
    else
        contents[top++] = i;
}

int pop(void)
{
    if (is_empty())
        stack_underflow();
    else
        return contents[--top];
}

Pros and Cons of External Variables

많은 함수들이 변수를 공유해야 할 때나 몇몇의 함수가 많은 변수를 공유할 때, external variable은 편리하다. 하지만, 대부분의 경우에서 변수를 공유하는 것보다는 오히려 parameter를 통해서 함수에게 전달하는 것이 더 좋다. 이유는 아래와 같다.

  • 만약 우리가 external variable을 프로그램 유지보수 동안에 수정한다면(자료형을 바꾸는 등), 이 변화가 함수에 어떻게 영향을 미치는지 알아보기 위해 같은 파일에 있는 모든 함수를 다 확인해보아야 한다.
  • 만약 external variable에 잘못된 값이 대입되었을 때, 잘못된 함수를 찾는 것이 어렵다. 이는 사람이 많은 파티에서 일어난 살인사건을 해결하는 것과 비슷하다. 용의자의 목록을 좁히는 쉬운 방법이 없다.
  • external variable에 의존하는 함수는 다른 프로그램에서 재사용하기가 어렵다. external variable에 의존하는 함수는 독립적이지 못하기 때문에 이를 재사용하기 위해선 필요한 external variable을 가져와야 할 것이다.

많은 프로그래머들이 external variable에 너무 많이 의존한다. 하나의 일반적인 남용을 예로 들면, 다른 함수에서 다른 목적을 가지는 변수에 동일한 external variable을 사용하는 것이다. 몇몇의 함수가 for구문을 제어하기 위해서 i라는 이름의 변수를 필요로 한다고 가정해보자. 각각의 함수에 사용할 i를 선언하지 않고, 어떤 프로그래머들은 프로그램의 맨 윗부분이 i를 선언한다. 변수를 모든 함수에 인식되게 만들기 위해서이다. 이러한 행동은 앞에서 말했던 이유때문에도 좋지 않지만, 오해의 소지 또한 만든다. 나중에 프로그램을 읽는 누군가가 변수의 사용이 관계되어 있다고 생각할 수 있지만 사실 그것이 아니기 때문이다.
external variable을 사용할 때에는, external variable이 의미있는 이름을 가지도록 해야한다(지역변수는 항상 의미있는 이름을 가질 필요가 없다. for 루프의 제어 변수로써 사용되는 i보다 더 좋은 이름을 생각하기 어렵기 때문이다). 만약 itemp와 같은 external variable을 사용하는 자신을 발견한다면, 이것들은 반드시 지역변수로써 사용되어야 한다는 것을 깨닫는 단서가 되어야 한다.

지역(local)에서 사용되어야 하는 변수를 external로 만드는 것은 오히려 짜증나는 버그를 만들 수 있다. 아래의 예제를 보자. 10 10으로 정렬된 별(``)을 출력한다.

int i;

void print_one_row(void)
{
    for (i = 1, i <= 10; i++)
        printf("*");
}

void print_all_rows(void)
{
    for (i = 1; i <= 10; i++)
    {
        print_one_row();
        printf("\n");
    }
}

10줄을 출력하는 대신에, print_all_rows는 딱 한 줄만 출력할 것이다. print_one_row가 처음 호출된 이후에 반환할 때에는. i는 11의 값을 가진다. print_all_rows안의 for 구문은 i를 증가시키고 10보다 작거나 같은지 검사할 것이다. 당연히 조건에 맞지 않으므로 루프는 종료되고 함수가 반환된다.

3. Blocks

Section 5.2에서 우리는 아래의 형태의 복합 구문(compound statements)을 본 적이 있다.

{ statements }

C언어는 선언을 포함하는 복합 구문또한 허용한다.

{ declaration statements }

이러한 복합 구문을 설명하기 위해 블록(block)이라는 용어를 사용할 것이다. 아래의 block 예제를 보자.

if (i > j)
{
    /* swap values of i and j */
    int temp = i;
    i = j;
    j = temp;
}

기본적으로 블록에서 선언된 변수의 storage duration은 automatic이다. 변수에 대한 storage는 block에 들어갔을 때 할당되고, block에서 빠져나올때 할당을 취소(deallocate)하게 된다. 변수는 block scope를 가지고 있는데, 바깥의 block에서는 참조될 수 없다. block에 속한 변수는 static을 부여받는 것으로 static storage duration을 가지게 할 수도 있다.
함수의 body는 block이다. 우리가 일시적인 사용을 위한 변수를 필요로 할 때 함수 body 내부에서 block은 유용하다. 위의 예제에서, 우리는 ij를 교환(swap)할 수 있도록 일시적인 변수를 필요로 했다. block 내부의 일시적인 변수는 두 가지 장점을 가진다.
1. 함수 body의 시작부분을 잠깐만 사용될 변수들이 어지럽히는 것을 피할 수 있다.
2. 이름 충돌(conflict)을 감소시킨다. 위의 에제에서 이름 temp는 동일한 함수 내에서 다른 목적을 가지고 사용될 수 있다. temp 변수는 변수가 선언된 block의 지역에 제한적이다.
C99는 함수 내부 어디에서나 변수를 선언할 수 있는 것처럼, block 내부의 어디에서나 변수가 선언되는 것을 허용한다.

4. Scope

C 프로그램에서, 동일한 식별자(identifier)는 여러 개의 다른 의미를 가질 수 있다. C언어의 scope 규칙은 프로그래머가(또는 컴파일러가) 프로그램 내의 특정한 지점에서의 어떤 의미와 관련이 있는지 결정할 수 있도록 한다.
가장 중요한 scope 규칙은, block 내부에서 이미 인식가능한(file scope나 둘러싸고 있는 block에서 이미 선언되어서) 식별자의 이름을 선언했을 때, 새로운 선언이 일시적으로 그 전에 선언된 것을 "숨기고" 식별자가 새로운 의미를 가지도록 한다는 것이다.
아래의 예시에서, i는 4개의 다른 의미를 지닌다.

int i;            /* Declaration 1 */

void f(int i)     /* Declaration 2 */
{
    i = 1;  // used by declaration 2
}

void g(void)
{
    int i = 2;    /* Declaration 3 */
    
    if (i > 0)    // used by declaration 3
    {
        int i;    /* Declaration 4 */
        
        i = 3;    // used by declaration 4
    }
    
    i = 4;        // used by declaration 3
}

void h(void)
{
    i = 5;        // used by declaration 1
}
  • Declaration 1에서, i는 static storage duration과 file scope를 가진 변수이다.
  • Declaration 2에서, i는 block scope를 가진 parameter이다.
  • Declaration 3에서, i는 block scope를 가진 automatic 변수이다.
  • Declaration 4에서, i는 block scope를 가진 automatic 변수이다.

i는 5번 사용되었고, C언어의 scope 규칙이 i의 의미를 각각 다르게 결정할 수 있도록 하였다.

  • i = 1 대입은 Declaration 2의 parameter를 참조하는데, 이는 Declaration 2가 Declaration 1을 숨겼기 때문이다.
  • i > 0 검사는 Declaration 3의 변수를 참조하는데, 이는 Declaration 3이 Declaration 1을 숨겼고, Declaration 2는 scope 바깥이기 때문이다.
  • i = 3 대입은 Declaration 4의 변수를 참조하는데, 이는 Declaration 4가 Declaration 3을 숨겼기 때문이다.
  • i = 4 대입은 Declaration 3의 변수를 참조하는데, 이는 Declaration 4는 scope 바깥이기 때문이다.
  • i = 5 대입은 Declaration 1의 변수를 참조한다.

5. Organizing a C Program

하나의 파일에 프로그램을 만들 것인데, Chapter 15에서는 어떻게 여러 개의 파일로 쪼개진 프로그램을 만드는지 알아볼 것이다.
지금까지 프로그램들은 아래의 것들을 포함하였었다.

Preprocessing directives such as #include and #define
Type definitions
Declarations of external variables
Function prototypes
Function definitions

C언어는 위의 항목들의 순서에 대한 몇몇가지 규칙을 부여했다. preprocessing directive는 이것이 행에 나타나기 전까지는 어떠한 효과도 내지 않는다. type name은 정의되기 전까지 사용할 수 없다. 변수는 선언되기 전까지 사용될 수 없다. C언어가 함수에 대해 까다롭지는 않지만, 모든 함수는 호출되기 전에 정의하거나 선언하는 것을 강력하게 추천한다.(C99는 이를 요구사항으로 만들었다)
이런 규칙들에 따르기 위한 프로그램을 구성하기 위한 여러가지 방법이 있다. 그 중 가능한 순서를 아래에 나열했다.

#include directives
#define directives
Type definitions
Declarations of external variables
Prototypes for functions other than main
Definition of main
Definitions of other functions

#include directive를 넣는 것은 이치에 맞는데, 프로그램 내부의 다양한 장소에 필요한 정보를 가져다주기 때문이다.
#define directive는 프로그램 전반에 걸쳐 일반적으로 사용될 매크로(macro)를 생성한다.
external variable 선언의 위에 type definition을 넣는 것은 논리적인데, 이 변수들의 선언은 정의된 type name을 참조할 수 있기 때문이다.
external variable을 선언하는 것은 그 이후의 모든 함수들이 external variable이 사용 가능하도록 한다.
main을 제외하고 모든 함수를 선언하는 것은 컴파일러가 함수의 prototype을 발견하기 전에 함수가 호출되었을 때 발생하는 문제들을 피할 수 있게 해준다. 이러한 행동은 함수 정의를 어떤 순서든지 정렬할 수 있게 해주는데, 알파벳순으로 정렬한다던지, 아니면 관련된 함수끼리 그룹화시켜 정리하는 등을 가능하게 해준다. 예를 들어 다른 함수보다 먼저 main을 정의하면 보는 사람이 프로그램의 시작 지점을 쉽게 찾을 수 있다.
함수에게 주어진 이름과, 함수의 의도를 설명하고, 각각의 parameter의 의미를 설명하고, 함수의 반환 값을 설명하거나, 함수가 가진 side effect(external variable을 수정하는 등)의 종류나 목록을 가진 박스 형태의 주석을 각각의 함수 정의 앞에 쓰는 것이 좋다.

책에는 Poker 예제가 실려있는데, 책의 poker.c를 참고하여 위의 내용과 코드를 하나씩 맞춰가면서 확인해보면 도움이 될 것입니다.



Others


static storage duration을 가진 지역 변수는 재귀함수에 어떠한 영향을 미치는가?

함수가 재귀적으로 호출되었을 때, 복사본들은 각각의 호출마다 automatic variables로 만들어진다. 이것은 static variable에는 일어나지 않는다. 대신, 모든 함수의 호출은 같은 static variable을 공유한다.


아래의 예시에서는, ji와 같은 값으로 초기화되었는데 이름이 i인 2개의 변수가 있다. 이 코드는 규칙에 맞는가? 만약 맞다면 j의 값은 1인가 2인가?

int i = 1;

void f(void)
{
    int j = i;
    int i = 2;
    ...
}

이 코드는 아주 규칙에 맞다. 지역 변수의 scope는 그 지역 변수가 선언될 때 까지는 시작하지 않는다. 그러므로 j의 선언은 external variable인 i를 참조하게 되며, j의 초기화 값은 1이 될것이다.

profile
저장용

0개의 댓글