스터디/C방장

변수의 scope rule, lifetime / extern, static의 올바른 사용 방법

SW PLAN B 2021. 1. 7. 00:37
728x90

C언어의 정석, C언어의 재 정립

C언어 독학을 하시는 분들께는 처음부터 올바른 개념을 잡아드리고 C언어의 문법과 사용법, 현업에서 실제로 어떻게 쓰이는지 알고 싶은 분들께 제가 알고 있는 C에 관한 지식과 팁을 나눕니다.

잘 이해가지 않거나 아직 다루지 않은 개념에 대해서 카카오톡 오픈 채팅방에서 질문해주세요.

C방장 QR코드
C방장 오픈채팅방 QR코드

C방장 오픈 채팅방 입장하기


변수의 scope rule, lifetime / extern, static의 올바른 사용 방법

□ storage-class specifier (스토리지 클래스 지정자)

C언어에서 스토리지 클래스 지정자(storage-class specifier)는 변수의 수명(lifetime)과 범위(scope)를 결정합니다.

표준 C언어(C99)의 스토리지 클래스 지정자는 typedef, extern, static, auto, register로 다섯 가지가 있습니다. typedef 스토리지 클래스 지정자는 오직 편의성을 위해 존재하는 스토리지 클래스 지정자입니다. (typedef의 활용은 이번 포스팅에서 다루지 않습니다.)

□ scope

scope는 변수의 유효 범위를 의미합니다. 변수의 유효 범위는 변수가 선언된 위치스토리지 클래스 지정자에 따라 결정됩니다. 

변수의 유효 범위를 설명할 때 변수가 보인다(visiable) 혹은 숨겨졌다(hidden)는 표현을 쓰기도 합니다.

□ linkage

linkage는 스토리지 클래스 지정자에 의해 결정되는 속성입니다.

표준 C언어(C99)는 internal linkage, external linkage, none으로 세 가지가 있습니다. 

internal linkage는 해당 파일 내부에서만 적용되는 linkage 속성입니다. static 스토리지 클래스 지정자로 지정된 변수는 internal linkage 속성을 갖습니다.

external linkage는 파일 외부까지 연결 적용되는 linkage 속성입니다. extern 스토리지 클래스 지정자로 지정된 변수는 external linkage 속성을 갖습니다. 스토리지 클래스 지정자를 지정하지 않은 전역 변수는 external linkage를 갖습니다.

none은 스토리지 클래스 지정자를 사용하지 않은 지역변수의 linkage 속성입니다. 

□ initialization time (변수 생성 시기)

변수가 초기값을 갖거나 가시성을 갖게되는 시점을 의미합니다. 

□ lifetime (수명)

변수에 접근 가능한 시기를 의미합니다. 변수가 보이는(visable) 시점으로 표현하기도 합니다.

■ 변수의 속성

표 1. 변수의 속성

□ global variable (전역 변수) 공통

전역 변수는 함수 밖에 선언된 변수를 의미합니다.

초기값을 지정하지 않으면 0으로 초기화 되며 프로그램 전체에서 단 한 번만 초기화되어야 합니다. 프로그램이 시작될 때 한번 초기화됩니다.

오직 선언된 파일 내에서만 접근 가능합니다.

프로그램이 실행되는 동안 접근 가능합니다.

auto, register 스토리지 클래스 지정자는 전역 변수에 사용될 수 없습니다.

□ external global variable (외부 전역 변수)

extern 키워드를 명시적으로 사용하거나 스토리지 클래스 지정자를 생략한 전역 변수를 의미합니다.

외부 전역 변수의 scope는 file이고 external linkage 속성을 갖기 때문에 프로그램 전체에서 접근 가능합니다.


※ tip

전역변수를 해당 파일에서 사용하기 위해서는 반드시 해당 파일에서 선언되어야 합니다. 다른 파일에 extern으로 전역 변수가 선언, 정의되었다고 하더라도 다른 파일에서 사용하기 위해서는 해당 파일에서도 선언되어야 합니다.


□ static global variable (정적 전역 변수)

static 키워드를 통해 선언한 전역 변수를 의미합니다.

정적 전역변수는 오직 선언된 파일에서만 접근 가능합니다.

□ local variable (지역 변수) 공통

지역 변수는 함수 내에서 선언된 변수를 의미합니다.

오직 선언된 block('{' 와 '}' 로 묶여있는 단위) 안에서 접근 가능합니다.

□ static local variable (정적 지역 변수)

static 키워드를 사용한 지역변수를 의미합니다.

정적 지역 변수의 초기값을 지정하지 않으면 0으로 초기화 되며 프로그램이 시작될 때 한번 초기화됩니다.


※ tip

정적 지역 변수는 static memory 영역에 생성되며 프로그램 실행되는 동안 그 값이 유지되는 것이 보장됩니다. 지역 변수가 block scope를 갖는 것은 엄밀히 말해 identifier(변수의 이름)로 접근하는 것을 의미합니다.

정적 지역 변수의 주소로 indirect access는 프로그램 실행되는 동안 가능합니다.


□ auto local variable (자동 지역 변수)

스토리지 클래스 지정자를 사용하지 않은 지역 변수는 자동 지역 변수로 생성됩니다.

자동 지역 변수가 선언된 block이 실행될 때 초기화 되며 초기값을 지정하지 않으면 쓰레기 값이 읽힙니다.

자동 지역 변수가 선언된 block이 실행되는 동안 접근 가능합니다.

□ register local variable (레지스터 지역 변수)

register 스토리지 클래스 지정자를 사용한 지역 변수를 의미합니다.

컴파일러에게 해당 변수를 레지스터에 할당하도록 힌트를 주지만 실제로 레지스터에 할당할지 안 할지는 컴파일러에 따라 다릅니다.

의미상 레지스터에 할당되는 변수이기 때문에 주소가 없고 '&'연산자를 사용할 수 없습니다. 또한 배열의 스토리지 클래스 지정자로 사용할 수 없습니다.


※ tip

visual studio에서 register 스토리지 클래스 지정자는 auto로 변환됩니다. 하지만 호환성을 위해 &연산자와 배열에 사용할 수 없는 문법은 적용됩니다.

https://docs.microsoft.com/ko-kr/cpp/c-language/register-storage-class-specifier?view=msvc-160


■ extern과 static의 올바른 사용법

한자리 수 더하기 빼기를 하는 프로그램을 예를 들어 설명드리겠습니다.

이 프로그램은 먼저 1, 2, c, q 중 하나를 입력받습니다. 1 또는 2를 입력받으면 추가로 두 수를 입력받아서 각각 더하기, 빼기의 결과를 출력합니다.

그리로 c를 입력받았을 때는 더하기 또는 빼기를 수행한 횟수를 출력하고 q를 입력받으면 종료하는 프로그램입니다.

프로그램의 소스코드는 main loop가 포함된 main_loop.c와 더하기, 빼기가 구현되어 있는 arithmetic.c로 이루어져 있습니다.

출력 예시

1:add 2:sub c:count q:quit
=> 1
1.add
input operand 1:1
input operand 2:2
1 + 2 = 3
1:add 2:sub c:count q:quit
=> 2
case 2
input operand 1:3
input operand 2:5
3 - 5 = -2
1:add 2:sub c:count q:quit
=> c
count:2
1:add 2:sub c:count q:quit
=> q
bye

◎ main_loop.c

#include <stdio.h>
#include "arithmetic.h"

static char input_single_char(void)
{
	char in, buffer;
	int check_buffer = 1;
	scanf_s("%c", &in, sizeof(char));
	while (1)
	{
		buffer = getchar();
		if (buffer == '\n' || buffer == EOF)
		{
			break;
		}
		else
		{
			check_buffer++;
		}
	}
	return in;
}

static void input_operand(char* op1, char* op2)
{
	printf("input operand 1:");
	*op1 = input_single_char();
	printf("input operand 2:");
	*op2 = input_single_char();
}

int main(void)
{
	char input;
	char operand1, operand2;
	int result;
	int flag_run_loop = 1;
	while (1 == flag_run_loop)
	{
		printf("1:add 2:sub c:count q:quit\n");
		printf("=> ");
		input = input_single_char();
		switch (input)
		{
			case '1':
				printf("1.add\n");
				input_operand(&operand1, &operand2);
				result = my_add(operand1, operand2);
				printf("%c + %c = %d\n", operand1, operand2, result);
				break;
			case '2':
				printf("case 2\n");
				input_operand(&operand1, &operand2);
				result = my_sub(operand1, operand2);
				printf("%c - %c = %d\n", operand1, operand2, result);
				break;
			case 'c':
				printf("count:%d\n", my_count());
				break;
			case 'q':
				printf("bye\n");
				flag_run_loop = 0;
				break;
			default:
				printf("wrong character:%c\n", input);
		}
	}
	return 0;
}

switch-case 문에서 my_add(), my_sub(), my_count() 함수가 호출됩니다. 그런데 이 함수들은 arithmetic.c에 정의되어 있기 때문에 arithmetic.h를 main_loop.c에 포함시켜야 합니다.

input_single_char()는 한 자리 문자를 입력받는 함수이고 input_operand()는 연산에 쓰일 두 피연산자(operand)를 입력받는 함수입니다. 두 함수는 main_loop.c 에서만 쓰일 예정이므로 static으로 선언하였습니다.


※ tip

한 자리 문자를 입력받는 것 처럼 반복 사용되는 기능들은 별도의 함수를 만들고 모듈화하여 관리하는 것이 좋습니다.

2021/01/03 - [스터디/C방장] - 협업을 위한 코드, 읽기 쉬운 코드 작성하는 법. Top Down, 모듈화 (feat. 소수 구하기)


arithmetic.h

#pragma once
extern int my_count(void);
extern int my_add(char op1, char op2);
extern int my_sub(char op1, char op2);

다른 파일에서 참조되어야 하는 함수들은 헤더 파일에 extern으로 선언되어 있습니다.

◎ arithmetic.c

#include "arithmetic.h"

static int count;

int my_count(void)
{
	return count;
}

static inc_count(void)
{
	count++;
}

int my_add(char op1, char op2)
{
	inc_count();
	int conv_op1 = op1 - 48;
	int conv_op2 = op2 - 48;
	return conv_op1 + conv_op2;
}

int my_sub(char op1, char op2)
{
	inc_count();
	int conv_op1 = op1 - 48;
	int conv_op2 = op2 - 48;
	return conv_op1 - conv_op2;
}

전역 변수 count가 static으로 선언된 것에 주의해주세요. 이렇게 전역 변수를 사용할 경우에는 예외 없이 static 변수를 사용할 것을 추천드립니다. 그리고 외부에서 이 변수에 접근이 필요할 경우 my_count() 함수처럼 함수 호출을 통해 접근하는 것을 추천드립니다.

이런 방식은 객체 지향 언어에서 캡슐화(encapsulation)와 비슷한 개념입니다. 어떠한 전역 변수에 대한 접근은 그 파일에서만 이루어지고 다른 파일에서는 (extern으로 선언된 함수를 통해) 허락된 동작만 가능하게 하는 것입니다.

지금처럼 단순한 경우는 문제가 생겨도 디버깅이 쉽지만 프로그램이 복잡해질수록 파일이 많아질수록 디버깅이 어려워집니다. 코드를 타이핑하는 당장에는 조금 번거롭고 귀찮을 수 있지만 이렇게 구현하는 것이 큰 단위의 프로그램을 만들고 향후 필연적으로 생길 문제를 해결하는데 도움이 됩니다. 처음 배우는 단계에서부터 이런 습관을 들이는 것을 강력하게 추천드립니다.

inc_count()처럼 파일 내부에서만 쓰는 함수들은 명시적으로 static을 붙여주는 것을 추천드립니다.


※ tip

만약 어쩔 수 없이 extern으로 전역 변수를 선언하고 싶다면?

1. 헤더에 extern으로 선언하고 다른 하나의 파일에 정의하고 참조하고 싶은 파일에서 헤더 파일을 include 하여 포함하는 방법.

2. 하나의 파일에 전역 변수를 정의하고 참조하고 싶은 파일에 extern으로 선언하여 사용하는 방법.

이렇게 두 가지를 쓸 수 있지만 추천드리지 않습니다.


도움이 되셨다면 즐겨찾기 등록하고 또 오세요


 

C언어의 정석, C언어 재 정립 글 목록보기

함께 읽으면 좋은 글

2020/12/27 - [스터디/C방장] - 정수형 type 이야기 [C언어 파헤치기 1]

2020/12/27 - [스터디/C방장] - 문자형/character type 이야기 [C언어 파헤치기 2]

2021/01/03 - [스터디/C방장] - 협업을 위한 코드, 읽기 쉬운 코드 작성하는 법. Top Down, 모듈화 (feat. 소수 구하기)


 

반응형