C (7) - 변수 영역과 데이터 공유

변수 사용 영역

 

여태까지 단순히 자료형 변수명; 으로 선언했던 것이 사실은 지역 변수를 뜻하는 auto라는 예약어가 생략된 문장이었습니다. 이밖에도 전역 변수, 레지스터 변수 등 다양한 변수 종류가 있습니다.

 

지역 변수

 

지금까지 써왔던 변수들이 지역 변수(local variable)입니다. 지역 변수는 범위가 함수 내, 즉 일정 지역에서만 사용하는 변수입니다. 본래 auto 예약어와 함께 함수 안에 지역 변수를 선언합니다. auto 예약어는 생략할 수 있으며 이 경우 함수 안에 선언된 변수는 자동으로 지역 변수가 됩니다. 

지역 변수와 자동 변수(auto variable)은 같은 용어입니다.

 

지역 변수는 프로그램에서 가장 많이 사용되며 다음 2가지 특징을 갖습니다.

  1. 지역 변수는 사용 범위가 블록 내부로 제한되므로 다른 함수에서는 사용할 수 없습니다.
  2. 지역 변수는 이름이 같아도 선언된 함수가 다르면 각각 독립된 저장 공간을 갖습니다.

지역 변수를 사용했을 때의 장점으로는 함수 안에서만 사용하므로 함수가 반환되면 운영체제가 자동으로 할당된 메모리를 회수한다는 것과, 범위가 제한되어 디버깅에 용이하다는 것이 있습니다.

 

그런데 지역 변수가 할당된 저장 공간은 자동으로 초기화되지 않으므로 쓰레기 값이 사용되지 않도록 주의해야 합니다.

함수의 매개변수 또한 지역 변수와 동일한 특징을 갖습니다.

 

블럭 안의 지역 변수

지역 변수는 보통 함수 안에서 선언된 후 함수 끝까지 사용되지만, 특정 블록 안에 변수를 선언하면 사용 범위가 블록 내부로 제한됩니다.

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20;

    printf("교환 전 a, b : %d, %d\n", a, b);	// a: 10, b: 20
    {
        int temp;

        temp = a;
        a = b;
        b = temp;
    }
    printf("교환 후 a, b : %d, %d\n", a, b);	// a: 20, b: 10

    return 0;
}

위의 코드에서 temp는 내부 블럭 안에서 선언된 변수이기 때문에 블럭이 끝나면 더 이상 사용할 수 없습니다.

 

만약 사용 가능한 변수가 둘 이상이면 가장 가까운 블럭에 선언된 변수를 사용합니다.

 

전역 변수

 

함수 밖에 변수를 선언하면 전역 변수가 됩니다. 전역 변수(global variable)는 특정 함수의 블록에 포함되지 않으므로 사용 범위가 함수나 블럭으로 제한되지 않습니다. 전역 변수의 사용 범위는 프로그램 전체이므로 어떤 함수라도 안에서 직접 쓸 수 있습니다.

#include <stdio.h>

void assign10(void);
void assign20(void);

int a;

int main(void)
{
    printf("함수 호출 전 : %d\n", a);	// a: 0

    assign10();
    assign20();

    printf("함수 호출 후 : %d\n", a);	// a: 10

    return 0;
}

void assign10(void)
{
    a = 10;
}

void assign20(void)
{
    int a;
    
    a = 20;
}

전역 변수는 특별한 값으로 초기화하지 않아도 0으로 자동 초기화됩니다.

 

assign20() 함수에서처럼 전역 변수와 같은 이름의 지역 변수가 있을 경우 지역 변수를 먼저 사용합니다.

 

전역 변수는 모든 함수에서 자유롭게 접근할 수 있으므로 같은 변수를 여러 함수에서 쉽게 공유할 수 있습니다. 그러나 이런 특징은 장점보다 부작용이 더 크므로 사용을 꺼리는 이유가 됩니다. 전역 변수의 문제점을 정리해 보겠습니다.

  1. 전역 변수의 이름을 바꾸면 그 변수를 사용하는 모든 함수를 찾아 수정해야 합니다.
  2. 전역 변수의 값이 잘못된 경우 접근 가능한 모든 함수를 의심해야 합니다.
  3. 코드 블럭 내에 같은 이름의 지역 변수를 선언하면 거기서는 전역 변수를 사용할 수 없습니다.

결국 사용 범위가 명확하고 통제 가능한 지역 변수를 우선적으로 사용하고, 많은 함수에서 수시로 데이터를 공유하는 경우에만 제한적으로 전역 변수를 사용하는 것이 좋습니다.

 

정적 지역 변수

 

지역 변수를 선언할 때 static 예약어를 사용하면 정적 지연 변수(static variable)가 됩니다. 정적 지역 변수는 코드 블록 안에 선언하므로 일반 지역 변수와 같이 사용 범위가 블록 내부로 제한됩니다. 반면에 저장 공간이 메모리에 존재하는 기간이 다릅니다.

 

정적 지역 변수는 선언된 함수가 반환되더라도 그 저장 공간을 계속 유지합니다. 따라서 하나의 함수가 여러 번 호출되는 경우 같은 변수를 공유하는 것이 가능합니다. 

#include <stdio.h>

void auto_func(void);
void static_func(void);

int main(void)
{
    int i;

    printf("auto variable... \n");
    for (i = 0; i < 3; i++)
    {
    	auto_func();
    }

    printf("static variable... \n");
    for (i = 0; i < 3; i++)
    {
    	static_func();
    }

    return 0;
}

void auto_func(void)
{
    auto int a = 0;

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

void static_func(void)
{
    static int a;

    a++;
    printf("%d\n", a);
}
auto variable...
1
1
1
static varibale...
1
2
3

auto_func 함수에서의 변수 a는 함수가 호출될 때마다 메모리에 새롭게 할당되고 그때마다 0으로 초기화됩니다. 그 값을 1 증가시켜 출력하므로 함수의 호출 횟수와 관계없이 a는 항상 1이 출력됩니다.

 

반면 static_func 함수에서는 static 예약어를 사용하여 지역 변수를 선언했기 때문에 프로그램이 실행될 때 메모리에 할당되며 프로그램이 끝날 때까지 존재합니다. 그래서 1씩 증가시킨 값이 누적되어 출력됩니다.

 

레지스터 변수

 

레지스터 변수(register variable)는 블록 혹은 함수 내에 변수를 선언할 때 register 예약어를 사용합니다. 레지스터 변수의 가장 큰 특징은 저장 공간이 할당되는 위치에 있다는 것입니다. 이름과 같이 레지스터 변수는 CPU 안에 있는 저장 공간인 레지스터를 사용합니다.

 

레지스터는 CPU 안에 있어 데이터 처리 속도가 가장 빠른 저장 공간입니다. 따라서 반복문에 쓰는 변수와 같이 사용 횟수가 많은 경우 레지스터에 할당하면 실행 시간을 줄일 수 있습니다.

#include <stdio.h>

int main(void)
{
    register int i;
    auto int sum = 0;

    for {i = 1; i <= 10000; i++)
    {
        sum += i;
    }

    printf("%d\n", sum);

    return 0;
}

8행의 반복문이 실행되면 i를 비교하고 증가시키는 작업을 계속 반복합니다. 또한 10행에도 i를 사용하므로 i를 사용하는 횟수는 반복 횟수보다 3배 이상 많습니다. 이렇게 자주 사용하는 변수를 레지스터 변수로 선언하면 변수의 데이터 처리 속도가 램 메모리에 있늘 때보다 빨라 프로그램 실행 시간을 줄일 수 있습니다. 메인 메모리에 있는 일반 변수의 값은 레지스터로 옮겨진 후 연산 장치에 사용됩니다.

 

레지스터 변수 사용 시 주의사항은 다음과 같습니다.

  1. 전역 변수는 CPU 자원을 잠깐 빌리는 레지스터 변수로 선언할 수 없습니다.
  2. 레지스터 변수는 주소 연산자를 사용하여 주소를 구할 수 없습니다.
  3. 레지스터 변수로 선언하더라도 실제 레지스터 사용 여부는 컴파일러가 결정합니다.

 

함수의 데이터 공유 방법

 

프로그램은 데이터와 명령으로 이뤄집니다. 컴퓨터가 주로 하는 일은 데이터를 명령으로 처리하는 일인데, 이때 특정 기능의 명령을 묶어서 함수로 만들어 사용합니다. 지금부터는 그 함수들끼리 데이터를 어떻게 공유하는지 알아보겠습니다.

 

값을 복사해서 전달하는 방법

 

값을 호출할 때 필요한 데이터를 전달하는 가장 일반적인 방법은 값을 복사해서 전달하는 것입니다. 이 방법은 함수를 호출할 때 변수뿐 아니라 상수나 수식도 사용할 수 있으며 지금까지 여러 차례 사용했던 방법입니다. 이렇게 하면 호출된 함수가 반환된 이후에도 호출한 함수에 있는 변수의 값은 변하지 않습니다.

#include <stdio.h>

void add_ten(int a);

int main(void)
{
    int a = 10;

    add_ten(a);
    printf("a : %d\n", a);	// a: 10

    return 0;
}

void add_ten(int a)
{
    a = a + 10;
}

9행에서 변수 a를 인수로 주고 함수를 호출합니다. 이 경우 a 값이 복사되어 함수에 전달되며 15행에서는 매개변수 a를 위한 저장 공간이 별도로 할당되어 인수로 넘어오는 값을 저장합니다.

 

위 코드에서 변수 a의 출력 결과가 처음과 같은 10으로 유지되는 이유는, main 함수에서 선언한 a를 함수에 전달할 때 변수 자체가 아닌 값만 복사되어 전달되기 때문입니다. 따라서 함수 내부에서 수행한 연산은 main 함수의 a에는 영향을 주지 않습니다.

 

만약 증가된 값을 출력하고 싶다면 add_ten 함수에서 증가된 값을 반환하는 문장도 있어야 합니다.

 

주소를 전달하는 방법

 

함수를 호출할 때 처리할 변수의 주소를 넘기는 방법도 있습니다. 이 경우 포인터 연산으로 값을 처리해야 하므로 값을 전달하는 방법보다 불편합니다. 하지만 호출된 함수에서 호출한 함수에 있는 변수의 값을 직접 바꿀 수 있습니다.

#include <stdio.h>

void add_ten(int a);

int main(void)
{
    int a = 10;

    add_ten(&a);
    printf("a : %d\n", a);	// a: 20

    return 0;
}

void add_ten(int *pa)
{
    *pa = *pa + 10;
}

9행에서 변수 a의 주소가 함수의 인자로 전달되며, 15행에서는 포인터를 매개변수로 선언해 받습니다. 따라서 17행에서 매개변수 pa에 간접 참조 연산자를 사용하면 main 함수에 있는 a를 사용할 수 있으며 그 값을 바꾸는 것도 가능해집니다.

 

값을 복사해서 전달하는 방식  vs  주소를 전달하는 방식

 

이 두 방식은 각기 장단점이 있습니다.

 

먼저 값을 복사해서 전달하는 방식은 원본 데이터를 보존할 수 있으므로 안정성을 담보해야 하는 상황에 유용합니다. 다만, 원본의 데이터 수정이 목적인 경우에는 사용법이 제한됩니다.

 

반대로 주소를 전달하는 방식은 원본 데이터를 바꿀 때 유용하지만, 사용법이 복잡하며 절대 바꾸면 안되는 데이터를 다룰 때 사용하면 문제가 생길 수 있습니다. 따라서 꼭 필요한 경우가 아니면 값을 복사해서 전달하는 방식을 기본으로 사용합니다.

 

👉  call by value  /  call by reference

 

call by value는 함수를 호출할 때 변수의 값을 복사해 인수로 주는 방식을 말하며, call by reference는 호출 함수의 변수를 피호출 함수에서 매개변수의 이름으로 직접 사용하는 방식을 말합니다.

 

그런데 C 언어에서는 call by reference를 구현하는 문법이 없습니다. 주소를 전달하는 방식이 call by reference처럼 동작하는 것 같지만, 엄밀히 말하면 이 방법도 주소 값을 주고 받아 간접 참조 연산을 통해 사용하는 방식이기 때문에 형식상 결국 call by value 입니다.

(Java 또한 모든 함수 호출이 call by value 방식입니다.)

 

참고로 C++에서는 참조(&) 문법으로 call by reference를 지원합니다.

// C++
void func(int &x) 
{
    x = 20;
}

 

주소를 반환하는 함수

 

반환값이 있는 함수는 호출한 함수로 값을 복사해서 반환합니다. 함수 안에서 사용한 지역 변수는 함수가 반환되면 저장 공간이 사라지므로 그 값을 복사해 반환해야 호출한 함수에서 사용할 수 있습니다. 

 

그러나 함수가 반환된 후에도 변수의 저장 공간이 계속 유지된다면 주소를 반환해 호출하는 함수에서 쓸 수도 있습니다. 즉, 정적 지역 변수와 전역 변수는 주소를 반환할 수 있습니다.

#include <stdio.h>

int *sum(int a, int b);

int main(void)
{
    int *resp;

    resp = sum(10, 20);
    printf("합 : %d\n", *resp);

    return 0;
}

int *sum(int a, int b) 
{
    static int res;

    res = a + b;

    return &res;
}

15행의 sum 함수는 매개변수에 받은 두 정수를 더한 후에 직접 반환하지 않고 res 변수에 저장하고 res의 주소를 반환합니다. 이 경우 호출한 함수에서는 그 값을 포인터에 저장하고 간접 참조 연산을 수행해 언제든지 두 정수의 합을 사용할 수 있습니다.

 

그러나 이처럼 주소를 반환하는 함수를 만들 때는 2가지 주의할 점이 있습니다.

  1. 반환값의 자료형은 반환값을 저장할 포인터의 자료형과 같아야 합니다.
  2. 지역 변수의 주소를 반환해서는 안됩니다.

'Lang > C' 카테고리의 다른 글

C (9) - 메모리 동적 할당  (2) 2025.08.28
C (8) - 다차원 배열, 포인터 배열, 응용 포인터  (2) 2025.08.25
C (6) - 문자와 문자열  (2) 2025.08.18
C (5) - 배열과 포인터 ②  (2) 2025.08.15
C (4) - 배열과 포인터 ①  (3) 2025.08.11