변수의 생존 조건

CHOI·2021년 7월 14일
0

C 언어

목록 보기
22/28
post-thumbnail

변수의 접근 범위

#include <stdio.h>

void function() {
  int a = 2;
}

int main() {
  int a = 3;
  function();

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

실행 결과

a = 3

분명히

void function() { int a = 2; }

함수에서 a 의 값을 2로 바꿔주었지만 실제로 a 를 출력해보면 2로 안바뀌고 3으로 나온다.

이러한 이유는 어떠한 함수에서 정의한 변수는 그 함수 내에서만 접근할 수 있기 때문이다.

이와 같이 해당 변수 내에서만 접근할 수 있는 변수를 지역 변수라고 한다. 즉, main 안에서의 afunction 안에서의 a 는 컴파일러가 보기에는 서로 다른 변수이다.

#include <stdio.h>

int main() {
  int a = 3;
  {
    int a = 4;
    printf("a = %d \n", a);
  }

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

참고로 중괄호 {} 는 하나의 지역으로 취급 된다. 해당 지역에서 정의된 변수는 바깥 지역에서 정의된 변수를 덮어 쓰는것 같이 되어 완전히 가리게 된다. 하지만 해당 지역 안에서만 그렇다.

따라서 안에서의 printf 에서는 4가 출력되고 밖에선 3이 출력된다. 이 두 a 는 아예 다른 변수로 취급된다. 또한 바깥 지역에서 안쪽 지역의 변수를 사용할 수 없다. 예를 들어

#include <stdio.h>

int main() {
  { int b = 4; }

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

이렇게 하면 b 를 정의하지 않았다고 하면서 오류가 발생한다. 왜냐하면 bprintf 가 살고있는 지역보다 안쪽 지역에서 정의되었기 때문에 바깥에서는 보이지 않기 때문이다.

전역 변수

그렇다면 어떠한 지역에도 속해 있지 않는 변수를 정의할 수 있을까? 물론 가능하다.

#include <stdio.h>

int global = 0;

int function() {
  global++;
  return 0;
}
int main() {
  global = 10;
  function();
  printf("%d \n", global);
  return 0;
}

실행 결과

11

이와 같이 어떠한 지역에도 속해있지 않은 변수를 전역 변수(global variable) 이라 한다. 전역 변수는 위의 지역 변수와 달리 어느 곳에서든 접근 할 수 있다.

지역 변수의 경우 함수가 종료될때 파괴 되었는데 전역 변수의 경우 프로그램이 시작 할 때 만들어졌다가 프로그램이 종료되면서 파괴 된다. 전역 변수는 지역 변수와 달리 메모리의 데이터 영역(Data segment)에 할당된다.

한 가지 재미있는 것은 모든 전역 변수들은 정의시 자동으로 0 으로 초기화 된다.

#include <stdio.h>

int global;
int function() {
  global++;
  return 0;
}
int main() {
  function();
  printf("%d \n", global);
  return 0;
}

실행 결과

1

만약 global 변수를 전역 변수로 하지 않고 지역 변수 같았더라면 변수를 초기화 하지 않았다고 경고를 했을 것이다. 그러나 전역 변수는 따로 초기화 하지 않는다면 디폴트로 0 으로 초기화 된다.

#include <stdio.h>

int how_many_called = 0;
int function() {
  how_many_called++;
  printf("called : %d \n", how_many_called);

  return 0;
}
int main() {
  function();
  function();
  function();
  function();
  return 0;
}

실행 결과

called : 1
called : 2
called : 3
called : 4

위 프로그램은 function 함수가 몇 번 호출 되었는지를 출력하는 프로그램이다. 만일 how_many_called 라는 변수를 함수의 지역 변수로 만들었다면 함수 종료 후 파괴되므로 정보를 보관할 수 없을 것이다.

#include <stdio.h>

int how_many_called = 0;
int how_many_called2 = 0;
int function() {
  how_many_called++;
  printf("function called : %d \n", how_many_called);

  return 0;
}
int function2() {
  how_many_called2++;
  printf("function 2 called : %d \n", how_many_called2);

  return 0;
}
int main() {
  function();
  function2();
  function();
  function2();
  function2();
  function2();
  function();
  function();
  function2();
  return 0;
}

실행 결과

function called : 1
function 2 called : 1
function called : 2
function 2 called : 2
function 2 called : 3
function 2 called : 4
function called : 3
function called : 4
function 2 called : 5

이번에는 또 다른 함수 function2 가 호출된 횟수를 구하기 위해서 전역 변수를 하나 더 추가하였다. 그렇다면 만약에 서로 다른 함수 10개가 호출된 횟수를 구하려면 아마도 10개의 전역 변수가 필요할 것이다.

이는 심각한 문제가 아닐 수 없다. 전역 변수는 모든 함수에서 접근이 가능하기 때문에 전역 변수를 사용할 때는 매우 주의를 기울여야 한다. 심지어 위 처럼 전역 변수를 수십개 선언하다보면 필연적으로 문제가 발생한다.

주의 사항

많은 수의 전역 변수를 선언하지 않는 것을 권장합니다.

변수의 생존 기간

앞서 우린 변수가 어떤 범위에서 접근이 가능한지에 대해서 알아보았다. 이번엔 정의한 변수가 얼마나 살아있는지에 대해서 알아보자.

일반적으로 변수는 자신이 정의된 지역을 빠져나갈 때 파괴된다. 이게 무슨 말이냐면 자신의 정의를 포함하고 있는 {} 를 벗어날 때 변수가 사라지게 된다.

예를 들어 코드르 보면

#include <stdio.h>

int* function() {
  int a = 2;
  return &a;
}

int main() {
  int* pa = function();
  printf("%d \n", *pa);
}

실행 결과

[1]    30588 segmentation fault (core dumped)  ./test

위 처럼 함수가 지역 변수의 주소값을 리턴한다고 경고하고 있다. 왜 문제인지 살펴보면

int* function() {
  int a = 2;
  return &a;
}

일단, a 라는 변수는 지역 변수이다. 따라서 a 를 정의한 function 함수를 벗어나면 a 는 소멸된다. 다시 말해서 a 를 사용할 수 없다는 것이다.

하지만 아래 처럼 a 의 주소값을 리턴하여 function 함수 외부에서 a 를 사용하려 한다면 어떨까?

int *pa = function();
printf("%d \n", *pa);

이 경우 pa 는 이미 파괴된 변수를 가리키고 있기 때문에 문제가 된다. 따라서 위 코드 실행시 오류가 발생한다.

그렇다면 지역을 빠져나가도 파괴되지 않는 변수는 없을까? 그게 바로 정적 변수(static variable) 이다.

정적 변수

#include <stdio.h>

int* function() {
  static int a = 2;
  return &a;
}

int main() {
  int* pa = function();
  printf("%d \n", *pa);
}

실행 결과

2
static int a = 2;

정적 변수를 사용하기 위해선 위와 같이 일반적인 변수 선언 앞에 static 을 붙여주면 된다. 그러면 해당 변수는 자신이 선언된 범위를 벗어나도 절대로 파괴되지 않는다.

int *pa = function();

따라서 a 가 정의된 지역을 벗어나도 사라지지 않기 때문에 2라는 값이 잘 출력된다.

여기서 한 가지 궁금한 점은 만약에 function 을 여러번 호출하면 a 가 여러번 2 로 호출되는거 아닌가? 할 수 있는데 a 는 딱 한 번만 초기화 된다. 다시 말해서 static int a = 2; 는 딱 한 번만 실행 된다는 말이다. 더 놀라운 점은 function 을 실행하지 않더라도 a 라는 정적 변수는 이미 정의되어 있는 상태라는 것이다.

따라서 이와 같이 정적 변수를 사용하면 아래 처럼 해당 함수가 얼마나 호출되었는지 알 수 있다.

#include <stdio.h>

int function() {
  static int how_many_called = 0;

  how_many_called++;
  printf("function called : %d \n", how_many_called);

  return 0;
}
int function2() {
  static int how_many_called = 0;

  how_many_called++;
  printf("function 2 called : %d \n", how_many_called);

  return 0;
}
int main() {
  function();
  function2();
  function();
  function2();
  function2();
  function2();
  function();
  function();
  function2();
  return 0;
}

실행 결과

function called : 1
function 2 called : 1
function called : 2
function 2 called : 2
function 2 called : 3
function 2 called : 4
function called : 3
function called : 4
function 2 called : 5

참고로 정적 변수도 전역 변수와 마찬가지로 데이터 영역에 저장되고 프로그램이 종료 될 때 파괴된다. 또한 정적 변수는 전역 변수처럼 특별한 값을 정의해주지 않으면 자동으로 0으로 초기화 된다.

데이터 세그먼트의 구조

주의 사항

아래 내용은 일반적인 운영체제에서 실행 파일이 메모리에 로드될 때 상황을 가정한 그림 입니다. C 언어 자체적으로는 스택 이나  영역을 따로 구분하지 않습니다. 하지만 대부분의 운영체제에서 프로그램을 실행한다면, 아래 그림 처럼 힙과 스택 영역을 구분해서 만들게됩니다.

프로그램이 실행되면 프로그램은 모두 RAM 에 적재 된다. 다시 말 해 프로그램의 모든 내용이 RAM 위로 올라 온다는 얘기이다. 여기에서 '프로그램의 모든 내용'이라 함은 프로그램의 코드와 프로그램의 데이터를 모두 의미한다. 이렇게 RAM 위로 올라오는 프로그램의 내용은 크게 나누어서 코드 세그먼트(Code Segment)데이터 세그먼트(Data Segment) 로 분류 할 수 있다.

중점적으로 살펴볼 부분은 데이터 세그먼트이다.


위와 같이 메모리에 배치 되어 있다는 것을 알 수 있다.

일단 먼저 주목할 부분은 읽기 전용(Read-Only) Data 부분이다. 이 부분은 이전에 상수와 리터럴에 대해서 다룰 때 등장 했었다. 이 부분은 데이터의 값이 절대로 변경될 수 없는 부분이다. 다시 말해서 궁극적으로 보호 받는 부분이다.

그 다음 위에 전역 변수와 정적 변수가 거쳐가는 데이터 영역이 있다. 그 위로 힙(Heap) 부분이 있는데 이에 대해서는 나중에 다루자. 힙 맨 위를 보면 스택(Stack)이 있다 스택은 지역 변수가 거쳐하는 곳이다. 스택의 특징은 지역 변수가 늘어나면 크기가 아래로 증가 했다가 변수가 파괴되면 다시 스택의 크기가 위로 줄어들게 된다. 스택의 크기가 늘어나는 방향은 메모리 주소가 낮아지는 방향(아래 방향)이라고 보면 된다.

#include <stdio.h>
int global = 3;
int main() {
  int i;
  char *str = "Hello, Baby";
  char arr[20] = "WHATTHEHECK";

  printf("global : %p \n", &global);
  printf("i :      %p \n", &i);
  printf("str :     %p \n", str);
  printf("arr :    %p \n", arr);
}

실행 결과

global : 0x100008018 
i :      0x16fdff3e0 
str :    0x100003f68 
arr :    0x16fdff3dc

(일단 주소값 비교의 편의를 위해서 띄어쓰기를 추가 하였다.)

그러면 실제로 주소값을 살펴봐서 메모리에 정말 그렇게 배치 되어 있는지 살펴보자. 필자의 결과와 지금 이 글을 보고 실제로 해본 당신의 결과는 다를 것이다. 왜냐하면 프로그램 실행 시 RAM 의 어느 부분에서 프로그램을 실행 할지는 아무도 모르기 때문이다.

가장 먼저 읽기 전용(Read Only ) 데이터인 str 을 보자. str 에는 "Hello, Baby" 라는 리터럴 주소값이 들어 있다. 따라서. str 의 값을 출력하면 Read Only 데이터의 위치를 대략적으로 알 수 있을 것이다. 여기선 0x100003f68 이다. 예상 했던 대로 출력된 주소 값 중에서 가장 작은 주소값이 나온다. 왜냐하면 Read Only 데이터는 데이터 세그먼트 맨 아래에 위치하기 때문이다.

두 번째로 전역 변수인 global 의 주소값을 보면 str 보다 약간 크지만 다른 주소값들에 비해 작다는 것을 알 수 있다. 그 이유는 global 이 전역 변수로 데이터 영역에 위치하기 때문이다. 그 다음으로 i 는 지역 변수로 stack 에 존재해 있다. stack 의 경우 지역 변수를 추가할 수록 메모리 주소가 작아지는 방향으로 추가 되기 때문에 나중에 정의된 arri 보다 작은 주소값을 가지고 있다.(그런데 이는 컴파일러 마음이기 때문에 i 의 주소값이 arr 보다 클 수 있다. 그러나 딱히 신경 안써도 된다.)

profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

0개의 댓글