Search
Duplicate
⏭️

get_next_line

Holy Graph
1Circle
간략한 내용
File Descriptor 를 사용하여 파일 입력 구현, static 변수 활용
적정 기간
1 week
제작에 참여한 사람
진행 중인 사람
최종 편집일
May 27
통과한 사람
1 more property
Tester
Personal Design

Subjects

들어가기 앞서...

Jseo is IU
이번 프로젝트는 GNL~~ 겟 넥스트 라인!
이번에도 교재를 만들어 볼까요 ?ㅅ?
Welcome
씐나는 워료일!!! 워후!!!
어지러운 공부 진행하기 전에 새로운 마음가짐으로 달려보아요~~ 다들 난춘을 들어보세요! 여기를 눌러보아요!

1. 파일 디스크립터 (fd: File Descriptor)

1) 파일 디스크립터란?

파일을 대표하기 위해 시스템으로부터 할당 받은 음수가 아닌 0과 양수인 정수 값(Non-negative Integer)
프로세스에서 열린 파일의 목록을 관리하는 테이블의 인덱스
흔히 유닉스 시스템에 존재하는 모든 것은 파일이라고 한다. 일반적인 정규 파일(Regular File)에서부터 디렉토리(Directory), 소켓(Socket), 파이프(PIPE), 블록 디바이스, 캐릭터 디바이스 등등 모든 객체들은 파일로써 관리된다. 유닉스 시스템에서는 프로세스가 위와 같은 파일들을 접근할 때에 파일 디스크립터 (File Descriptor)라는 개념을 이용한다.
응용 프로세스가 파일을 열거나 생성 하게 되면 정수로 된 파일 디스크립터를 얻게 되는데 이 파일 디스크립터는 이후에 일어나는 모든 파일 동작 즉, 읽기 (read 함수), 쓰기 (write 함수), 파일 동작 제어 (fcntl 함수), 파일 닫기(close 함수)등의 동작에서 그 파일을 가리키는데 사용된다. 0, 1, 2는 프로그램이 메모리를 할당받아 실행될 때 기본적으로 할당되는 파일 디스크립터이다.

2) 기본적으로 할당되는 파일 디스크립터

0 : 표준 입력(Standard Input) / STDIN_FILENO
1 : 표준 출력(Standard Output) / STDOUT_FILENO
2 : 표준 에러(Standard Error) / STDERR_FILENO
(위 값들은 매크로로써 정의되어 있으며, unistd.h에서 확인할 수 있다.)
그러므로 우리가 생성하는 파일 디스크립터들은 3번부터 차례대로 할당받게 된다. 쉽게 생각하면, 파일 디스크립터는 파일을 다루기 위해서 해당 파일의 주소를 참조하여 접근하는 형태라고 생각하면 된다.
(일반적으로 파일 디스크립터 값은 0 ~ OPEN_MAX 까지 둘 수 있으며, OPEN_MAX라는 매크로 값은 코드가 실행되는 환경 (플랫폼)에 따라 다르다. 이는 limits.h에서 확인 가능하다. 만일 윈도우 환경이라면 stdio.hFOPEN_MAX에서 확인할 수 있다.)
OPEN_MAX 값을 확인하고 싶다면, 터미널에서 getconf OPEN_MAX라고 쳐보면 그 값을 얻을 수 있다. 혹은 터미널에서 sysconf(_SC_OPEN_MAX)를 통해서도 알 수 있다.
실제로는 OPEN_MAX 값만큼 파일이 열리진 않는다. 테스트를 해본 결과 8191 번 파일까지 열리고, 그 뒤부터는 열리지 않는것을 확인할 수 있다.
운영체제 마다 파일 디스크립터 에 대한 설정이 다르고, 리눅스 기준으로 시스템에 설정된 fd 제한과 유저에 따라 한 프로세스당 설정된 fd 제한이 각각 다른것을 보아 OPEN_MAX를 너무 맹신해서는 안 된다고 생각한다.
OPEN_MAX 테스트에 사용한 코드

3) 파일 디스크립터를 확인해보자

우선 파일 디스크립터를 확인하기 전에 필요한 함수를 살펴보자.
open(pathname, flag, mode) //return fd
C
복사
open 함수는 pathname이 가리키는 파일을 열고 열린 파일을 이후 호출에서 참조할 때 사용하는 파일 디스크립터를 리턴한다.(현재 가용한 숫자 중 가장 작은 값으로!)
open 함수의 더 자세한 활용은 밑에서 배우게 된다. 여기를 눌러보자!

파일 디스크립터 확인

file.c 파일과 test.txt파일을 생성한 후 프로세스가 파일을 열 때 얻는 파일 디스크립터를 출력해보자. test.txt파일은 아무내용이나 넣어도 상관 없다. 아래는 file.c 파일이다.
// file.c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> int main(void) { int fd; fd = open("test.txt", O_RDONLY); if (fd < 1) { printf("open() error"); exit(1); } printf("FD : %d\n", fd); close(fd); return (0); }
C
복사
실행 결과
해당 파일을 컴파일한 후 실행해보니 파일 디스크립터3부터 부여되는 것을 확인할 수 있다! 여기서 나오는 숫자 3이 어떤 의미일까? 아래의 그림을 보면 어떻게 정수값으로 된 파일 디스크립터를 통해 원하는 파일에 접근하는지 쉽게 이해할 수 있다.
파일 디스크립터의 값, 즉 fd는 프로세스가 유지하고 있는 FD Table의 인덱스이다. 위의 예제에서 우리는 3이라는 숫자가 나왔다. 그림에서 보듯이 우리는 이 숫자 3을 사용해서 FD Table의 3번째 인덱스로 접근하고, 해당 칸이 가리키는 File Table로 가서 원하는 행동을 할 수 있는 것이다.
FD Table의 각 칸들은 FD FlagFile Table Pointer를 가지고 있다.
File Table의 각 칸들은 modeinode Table PointerOffset을 가지고 있다.
inode Table은 소유자 그룹, 접근 모드(읽기, 쓰기, 실행 권한), 파일 형태, 고유 번호 (inode number, i-number) 등 해당 파일에 관한 정보를 가지고 있다.
inode란 무엇일까!? 파일을 기술하는 디스크 상의 데이터 구조로서 파일의 데이터 블록이 디스크 상의 어느 주소에 위치하고 있는가와 같은 파일에 대한 중요한 정보를 갖고 있다. 각각의 inode들은 고유 번호(inode number)를 가지고 있어서 파일을 식별할때 사용한다. 터미널에서 ls -i 옵션으로 inode number를 확인할 수 있다.
그렇다면, 이렇게 생생된 파일 디스크립터를 변경하는 것도 가능할까?

파일디스크립터 변경

파일 디스크립터를 변경시킬 수 있는지 확인하기 위해서 기존에 사용중인 파일 디스크립터 번호를 복제해볼 계획이다.
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> int main(void) { int fd; int fd2; fd = open("test.txt", O_RDONLY); fd2 = open("test.txt", O_RDONLY); if (fd < 1 || fd2 < 1) { printf("open() error"); exit(1); } printf("fd\t: %d\n", fd); printf("fd2\t: %d\n", fd2); printf("fd2 = dup(fd)\n"); fd2 = dup(fd); //dup() : 파일 디스크립터 복제 함수. printf("fd\t: %d\n", fd); printf("fd2\t: %d\n", fd2); close(fd); close(fd2); return (0); }
C
복사
실행 결과
기존 fd3을, fd24를 할당받았다. 이후 dup 함수를 활용해서 fd를 복사해서 fd2에 넣었더니 fd25가 되었다. 이것은 3이 이미 사용중인 파일 디스크럽터이고 4도 지금 현재 fd2에서 사용중이므로 5가 새로 할당된 것이다. 즉 dup 함수를 통해서 복제를 하면 새로운 파일 디스크립터를 생성한다고 생각하면 된다.
복제를 통해서 fd2 또한 fd로 통제하는 파일과 같은 녀석을 가리키게 되고 파일 디스크립터 값은 다르다.(각각을 통제해야하기 때문에 파일 디스크립터의 값은 다르다.)
출처

2. File Control을 위한 함수들

1) open 함수 그리고 create, close

Linux, Unix 계열의 시스템에서 Process(프로세스)가 File(파일)을 열 때 open 함수 혹은 openat 함수를 사용할 수 있다.
#include <fcntl.h> // open 함수가 있는 헤더파일이다. int open(const char *pathname, int flag); int open(const char *pathname, int flag, mode_t mode); int openat(int dirfd, const char *pathname, int flag); int openat(int dirfd, const char *pathname, int flag, mode_t mode);
C
복사
매개 변수로 File Path(절대경로 or 상대경로), flag, mode 등을 받고 File Descriptor(fd)값을 반환한다. 이때 에러가 나면 -1을 반환한다.
즉, open 관련 함수는 path에 명시된 파일을 flag에서 설정한 모드로 열어서 파일 디스크립터를 반환하는 함수이다.

openat 함수와 open 함수의 차이

open은 해당 경로의 파일을 flag 옵션을 적용하여 FD Table의 인덱스인 FD를 리턴한다면, openatopen과 동일한 작업을 수행하지만 dirfd값을 추가로 받아 경로를 이용하는 방식에 open과 차이가 있다.
openat 함수가 도입된 데에는 다음과 같은 문제를 해결하기 위함이다.
멀티 쓰레드 환경에서 상대 경로를 다루기 쉽게 해준다. 같은 프로세스에 있는 쓰레드들은 CWD(Current Working Directory)를 공유한다.
TOCTTOU(time-of-check-to-time-of-use) 문제를 해결하기 위해서 사용된다.
openat 함수는? 관건은 path절대 경로인지 상대 경로인지에 따라 달라진다. 1. 경로로 주어진 path인자가 절대 경로라면 dirfd는 무시된다. 2. 경로로 주어진 path인자가 상대 경로라면, FD Tabledirfd 인덱스에 해당하는 항목을 찾아 나온 디렉토리를 기준으로 path를 붙여 찾아간다. 3. 경로로 주어진 path인자가 상대 경로라면, dirfd 값이 현재 작업 디렉토리를 의미하는 (Current Working Directory) AT_FDCWD로 되어 있다면 현재 디렉토리를 기준으로 path를 붙여 찾아간다. 이 때, dirfdAT_FDCWD라는 특수 값이 존재하며 이 때는 open과 동일하게 동작한다. (open 역시 CWD 기준으로 동작함) (openat의 매개 변수 dirfd의 이름에서도 유추할 수 있지만, 현재 FD Table에 기록되어 있는 디렉토리를 이용하기 위함이므로 해당 디렉토리fd를 이용한다. 매개 변수로 사용하는 dirfd 값을 얻기 위해 디렉토리 구조체를 인자로 사용하는 int dirfd(DIR *dirp) 함수를 주로 이용한다.)
Search
oflag
기준
Necessary
파일을 쓰기 전용으로 연다. (Write Only)
Necessary
파일을 쓰기와 읽기용으로 연다. (Read & Write)
Necessary
파일을 실행 전용으로 연다. (Execute Only)
Necessary
디렉토리 파일을 탐색 전용으로 연다. (Search Only)
Optional
파일의 끝부분 (EOF)에 write하도록 설정한다.
Optional
FD_CLOEXEC 플래그를 설정한 채 파일을 연다. (exec류의 함수를 수행하고 나면 fd가 닫긴다.)
Optional
파일이 없으면 생성한다. 이 플래그를 명시하면, open 함수에 Permission 정보를 추가로 더 받아야 한다. 파일이 존재하면 연다.
Optional
path에 해당하는 파일이 디렉토리가 아니면 에러를 발생한다.
Optional
O_CREAT 플래그와 같이 사용한다 파일이 이미 존재하면 에러를 발생한다.
Optional
path에 해당하는 파일이 터미널 장치인 경우, 해당 장치를 현재 프로세스의 컨트롤링 터미널로 할당하지 않는다.
Optional
path에 해당하는 파일이 심볼릭 링크면 에러를 발생한다.
Optional
FIFO, Block Device, Charactoer Device에 대해 논 블록킹 방식으로 read 함수와 write 함수를 수행하도록 기본 설정을 세팅한다.
Optional
path에 해당하는 파일에 write 함수를 사용할 경우 실제 물리적인 I/O가 끝날 때까지 기다리도록 설정한다
Optional
파일이 이미 존재하고 write-only, read-write모드로 열 수 있는 경우, 파일 사이즈를 0으로 초기화시킨다
Optional
write 함수 수행시 파일의 데이터 부분에 실제 물리적인 I/O가 끝나기를 기다린다. 파일의 설정이나 Attribute부분에 대한 업데이트는 기다리지 않는다.
Optional
read 함수 수행시 커널에 해당 파일의 offset에 대한 write 함수의 pending이 있으면 그 write 함수의 수행이 끝나기를 기다린다.

creat 함수

새로운 파일 생성은 creat 함수를 이용할 수 있다.
#include <fcntl.h> int creat(const char *path, mode_t mode);
C
복사
open 함수와 마찬가지로 성공하면 File Descriptor(fd)를, 실패하면 -1을 반환한다.
creat 함수는 open 함수로도 구현할 수 있다.
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); // mode는 생성될 파일의 퍼미션 정보!
C
복사
creat 함수의 최대 단점은 write 모드로만 열린다는 것이다. 다시 읽기 위해서는 creat 함수로 파일을 만든 후, close g함수로 닫고 O_RDONLY로 읽는 과정이 필요한 것이다. 따라서 아래의 코드처럼 사용하는 것이 더 좋은 선택일 수 있다.
open(path, O_RDWR | O_CREAT | O_TRUNC, mode);
C
복사

close 함수

open 함수로 연 파일은 close 함수로 닫을 수 있다.
#include <unistd.h> int close(int fd);
C
복사
정상적으로 종료되면 0을, 실패하면 -1을 반환한다.
파일을 닫으면, 프로세스가 파일에 설정했던 Record Lock(레코드 잠금)도 자동으로 잠금 해제된다. 또 한 프로세스가 종료되면 프로세스가 열어놨던 파일들은 close 함수로 닫기게 된다.
Record Lock(레코드 잠금)이란? 멀티 쓰레드 프로그램에서, 여러 쓰레드가 하나의 파일에 동시에 접근할 경우 파일 잠금이 필요할 수 있다. 한번에 하나의 쓰레드만 파일에 읽기및 쓰기를 해야 하는 경우가 있을 수 있기 때문이다. 레코드 잠금에 대한 자세한 내용은 여기를 참고하면 된다. 요약하자면 잠금이라는 행동의 주체는 운영체제이다. 잠금에 대한 기록이 운영체제에서 관리, 기록을 한다고 보면 된다. (Record Lock 자체는 fcntl.h의 fcntl 함수를 통해 이뤄지며, 이 때 flock이라고 하는 잠금을 위한 구조체를 이용한다.)
fcntl은 file control의 줄임말이다! 추가적인 정보는 여기를 참고하면 된다.

2) read 함수

파일을 열고 난 후 데이터를 읽어올 때에는 read 함수를 사용한다.
#include <unistd.h> ssize_t read(int fd, void *buff, size_t nbytes);
C
복사
size_t란? size를 나타내기 위한 type으로 보통의 32 bit machine에서는 32 bit, 즉 unsigned int로 되어있다. 가장 유명한 sizeof라는 연산자가 반환하는 값을 담기 위한 type으로 보면 되는데 이 역시 크기를 의미하므로 많은 I/O 함수에서 사용된다. ssize_tsigned size type으로 보통의 32 bit machine에서는 간단히 말해 int다. I/O 함수의 반환값으로 많이 사용되는데 그 이유는 해당 IO 함수의 실패를 알려주기 위해서이다. 따라서 수행도중 오류가 발생했을 경우 -1을 반환하면서 해당 I/O 함수의 실패를 알려줄 수 있다. 추가적인 내용은 여기서 확인할 수 있다.
read함수는 파일로 부터 읽은 데이터를 저장할 공간인 buff를 가리키는 포인터읽을 데이터의 크기를 받아서 정상적으로 종료되면 읽어온 바이트 수를, 실패하면 -1을, 읽을 데이터가 없으면(파일의 끝에서 시도) 0을 반환한다.
아래는 사용 예제이다.
//text.txt abcdefg
C
복사
//read.c #include <unistd.h> #include <fcntl.h> #include <stdio.h> int main(void) { int fd; char buf[100]; fd = open("./text.txt", O_RDONLY); if (fd == -1) printf("file open error"); else { read(fd, buf, sizeof(buf)); printf("%s", buf); close(fd); } return(0); }
C
복사

그렇다면 읽어오려는 파일의 크기가 버퍼보다 큰 경우엔 어떻게 해야 할까?

이전엔 버퍼 크기가 100, 파일의 크기는 겨우 7이었기 때문에 별 문제가 되지 않았다. 하지만 만약 버퍼의 크기가 너무 작다면 어떻게 해야 할까? 아래의 코드는 버퍼 사이즈가 1이고 파일의 크기는 7인 경우이다.
위의 코드에서 수정 된 부분은 아래 else구문 부분이다. 자세히 살펴보면 읽어온 데이터의 수가 양수인 경우에 반복문을 돌면서 버퍼의 크기만큼 읽고 출력을 해준다. 이후 버퍼를 0으로 초기화 한 후 반복문을 반복한다.
//read_smallbuf.c #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #define BUFFER_SIZE 1 int main(void) { char buf[BUFFER_SIZE + 1]; int fd; int temp_read_size = 0; fd = open("./text.txt", O_RDONLY); if (fd == -1) printf("file open error"); else { while ((temp_read_size = read(fd, buf, BUFFER_SIZE)) > 0) { printf("%s", buf); memset(buf, 0, BUFFER_SIZE); } close(fd); } return(0); }
C
복사

읽어온 파일을 프로세스 내에서 사용가능한 변수에 저장해보자

위의 예제는 바로 문자열을 출력했기 때문에 버퍼의 크기가 작은 문제를 비교적 간단하게 해결할 수 있었다. 하지만 파일로 부터 읽어온 데이터를 단지 출력을 위해서만 쓰는 경우는 드물다. 조금씩 버퍼로 읽어온 파일을 프로그램 내에서 사용할 수 있도록 저장해보자.
//read_save.c #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #define BUFFER_SIZE 1 int main(void) { char buf[BUFFER_SIZE + 1]; char *save; int fd; int temp_read_size = 0; fd = open("./text.txt", O_RDONLY); if (fd == -1) printf("file open error"); else { while ((temp_read_size = read(fd, buf, BUFFER_SIZE)) > 0) { buf[temp_read_size] = '\0'; if(save == NULL) save = strdup(buf); else strcat(save, buf); } printf("%s", save); close(fd); } return(0); }
C
복사
위의 코드를 보면 save라는 char * 형 변수에 파일로부터 읽어온 데이터를 모두 저장하는 것을 확인할 수 있다. strdup 함수와 strcat 함수를 활용해서 buf에서 읽어온 데이터를 save에 저장하는 방식이다.
여기서의 strcat은 공간 부족 시 자동으로 재할당하여 concatenating을 진행해준다.

개행이 있는 파일의 데이터를 개행이 나오기 전까지만 출력해보자

우리가 파일을 읽어올 때, 실행할 때마다 개행을 만나기 전까지만 출력을 하고 싶은 경우가 있다. 이럴때 보통 라이브러리에서 제공하는 fgets 함수를 활용해서 코드를 구현하곤 한다. 아래 코드를 이해해보자.
FILE *fp = fopen("./text.txt", "r"); while (fgets(buf, BUFFER_SIZE, fp) != NULL) { printf("%s", buf); }
C
복사
지금은 위의 코드를 완벽하게 이해할 필요는 없다. 간단하게 설명하면 fgets함수가 개행을 만나거나 BUFFER_SIZE만큼 파일을 읽으면 해당 위치로 포인터를 이동시키고 읽기를 끝내게 되는 것이다.
하지만 우리의 read함수는 개행따위 신경쓰지않고 무조건 버퍼 사이즈 만큼 읽게 된다. 그럼 open함수로 개행까지만 읽고 싶을 때에는 어떻게 구현해야 하는 것일까? 직접 고민해보자.
GNL 과제 코드를 구현하기 전에 read를 활용한 코드를 많이 테스트해보길 바란다.

3) write 함수

파일을 열고 난 후 데이터를 쓸 때에는 write 함수를 사용한다.
#include <unistd.h> ssize_t write(int fd, const void *buff, size_t nbytes);
C
복사
정상적으로 종료되면 기록한 바이트 수를, 실패하면 -1을 반환한다.
write 함수에 대한 자세한 지식은 여기서 알아보자.
출처

3. 프로그램과 프로세스에 대하여

1) Program (프로그램)이란?

일반적으로 프로그램이라 함은 컴퓨터에서 실행되는 명령어 모음이 들어 있는 덩어리라고 볼 수 있다. 이 때의 덩어리는 CodeData로 나뉜다.
프로그램 자체로써는 아무런 의미가 없고, 그것을 더블 클릭 등으로 실행했을 때 의미를 갖게 된다. 프로그램을 실행하면 프로그램 안에 있는 명령어를 한 줄씩 수행하면서 프로그램이 뭔가 실행하는 등의 상태를 갖게 된다. 이렇게 프로그램이 상태(State)를 갖고 있는 것을 프로세스라고 한다.

2) Process (프로세스)란?

프로세스는 운영체제에 의해서 관리 되며, 프로세스 별로 사용 가능한 메모리 영역이 존재한다.(기본적으로 서로 다른 프로세스는 각 프로세스가 사용하는 메모리 공간을 읽고 쓰는 것이 불가능하다) 프로그램이 프로세스로써 돌아가게 되면 프로그램의 CodeData는 프로세스 메모리로 불러들여 진다. 이 때 프로세스는 Code Segment, Data Segment, Heap, Stack (Stack은 함수의 호출 기록 및 지역 변수들의 기록이 담겨 있다.)의 구조로 메모리 레이아웃을 가진다. (프로그램은 HDD, SSD와 같은 곳에 존재하고, 프로세스는 RAM에 존재한다.)
일반적인 Stack의 크기는 84KB 정도이다. 이 크기는 시스템에 따라 그 다를 수 있는데, 이 때문에 아마 GNL을 진행하면서 지역 변수로 문자 배열을 선언 시 문제를 겪을 수 있다. (지역 변수로 선언하면 Stack에 할당되므로) 대다수의 카뎃들이 지역 변수로 100만짜리 char 배열을 선언했을 때는 문제가 없엇겠지만, 1000만부터는 할당 공간이 부족하여 잘못된 주소 접근으로 Segmentation Fault를 많이 겪게 된다. 따라서 이를 해결하기 위해선 시스템 설정으로 Stack의 크기를 강제로 늘려주거나, 메모리 이용을 Stack을 피하면 된다. 이는 곧 정적 변수 혹은 전역 변수로 선언하여 Data Segment에 위치시키면 된다는 것이다. 혹은 동적할당을 통해 Heap에 위치시키는 것도 하나의 방법이 될 수 있다.

3) Thread (쓰레드)란?

여기서 쓰레드에 대한 이야기를 아주 살짝하고 가면, 프로세스를 처리하는 기본 단위는 쓰레드이다. 이 때, 프로세스 내에는 여러 쓰레드가 있을 수 있으며, 한 프로세스 안에 속한 여러 쓰레드들은 프로세스 안의 Heap, Code Segment, Data Segment를 공유하며 Stack만 별도로 가진다. (이 말은 각 쓰레드에서 실행되는 함수와 지역 변수들이 쓰레드 마다 별도로 관리된다는 의미이다.) 프로세스 내에는 하나의 쓰레드만 둔채로 실행이 될 수도 있는데, 이를 싱글 쓰레드 프로그램이라고 한다. 또한 이렇게 싱글 쓰레드로 작동하도록 프로그램을 설계하고 구현하는 것을 싱글 쓰레드 모델이라고 한다. 싱글 쓰레드 프로그램멀티 쓰레드 프로그램은 기본적으로 프로세스를 처리하는 메인 쓰레드가 존재한다.
프로세스가 갖는 전반적인 메모리 레이아웃은 아래의 글을 통해 살펴보자. 이를 통해 static 변수가 어느 메모리 레이아웃에 존재하는지 알 수 있다.
메모리 레이아웃에 대한 이해가 되었다면, static이라는 키워드에 대해서 알아볼 필요가 있다.

4. static 변수

1) static 변수란?

static 변수는 Global 변수 (전역 변수), Local 변수 (지역 변수) 어느 것으로 이용이 가능하다.
전역이든 지역이든 static 변수는 Data Segment에 위치한다.

2) 외부 정적 변수

전역으로 선언된 static 변수는 외부 정적 변수라고도 불리며, 별도의 초기화 구문이 없어도 0으로 초기화된다.
Data SegmentBSS 영역에 위치하여 0으로 초기화된다. 초기화 구문 존재 시에는 Data SegmentData 영역에 위치한다.

3) 내부 정적 변수

특정 함수나 클래스 내부에 선언된 지역 변수내부 정적 변수라고도 불리며, 외부 정적 변수와 마찬가지로 별도의 초기화 구문이 없어도 0으로 초기화 된다. 또한 내부 정적 변수의 경우에도 프로세스의 메모리가 할당되는 프로그램의 시작 시점에 이뤄지기 때문에 함수 실행 등으로 인한 선언문에서의 실행으로는 초기화가 이뤄지지 않고 무시된다.
초기화 시점이 프로그램의 시작이라서 함수 실행 시 초기화 구문에서 초기화가 안 된다고 했는데, 이렇게 되어도 문제가 없는 이유는 static 변수는 함수 혹은 클래스에 대해서 내부 정적 변수로 이용되는 경우에 각 함수 별 혹은 클래스 별로 공유되는 일종의 공유 변수로 이용되기 때문이다. 아래 코드를 보자.
#include <stdio.h> void increase_num(void) { static int num = 4; printf("%d\n", num); ++num; } int main(void) { increase_num(); increase_num(); increase_num(); return (0); }
C
복사
위의 경우 static int num이라는 내부 정적 변수의 초기화는 프로그램의 시작에 이뤄지며 초기 값은 4가 된다. 이 때 static int numincrease_num이라는 함수의 지역 변수처럼 보여 Stack에 위치할 것 같지만, 실제로는 (이 경우에는 초기화 구문이 존재하므로 BSS 영역이 아닌) Data 영역에 위치하고 있다. 위에서 언급했던 초기화 구문이 동작하지 않는다는 얘기는 increase_num 함수 내의 초기화 구문인 static int num = 4가 매 함수 실행마다 이뤄지지 않는다는 말이다. 또한 내부 정적 변수는 특정 함수 혹은 클래스 간 공유되어 사용된다고 했기 때문에 위 main 함수의 실행 결과는 4, 5, 6가 된다.

4) 주의할 점

다른 소스 파일에 존재하는 전역 static 변수 (외부 정적 변수)는 참조 할 수 없다.

즉, extern이 불가능 하고 이를 시도하면 컴파일 오류가 발생한다.
increase_num.c
#include <stdio.h> static int num = 4; void increase_num(void) { printf("%d\n", num); ++num; }
C
복사
main.c
#include <stdio.h> extern int num; // 컴파일 에러 void increase_num(void); int main(void) { increase_num(); increase_num(); increase_num(); printf("%d\n", num); return (0); }
C
복사
위와 같이 extern을 받게 되면 아래 사진과 같은 오류를 볼 수 있다.

static 변수는 매개 변수로 사용할 수 없다.

매개 변수에 static을 붙이더라도 매개 변수는 static으로써의 역할을 수행할 수 없다. 즉, 값이 유지 되지 않는다. 아래와 같이 매개 변수로 사용하려고 하면 오류가 나는 것을 볼 수 있다.