[C] 프로그램 파라미터를 처리하는 getopt() 사용법

C프로그램의 파라미터 파싱 및 처리

문자열 파싱은 스크래치 상태(아무것도 없는 빈 상태)로부터 구현하기가 만만치 않은데, 이를 도와주는 라이브러리 함수가 있으니 바로 unistd.h에 정의된 getopt() 함수이다.

getopt()

이 함수의 원형은 다음과 같다.

int getopt(int argc, char * const argv[], const char *optstring);

이 함수의 파라미터는 간단한다.

  1. argc, argv : main() 함수가 받은 파라미터를 그대로 전달한다.
  2. optstring : 파싱해야 할 파라미터를 쓴다. 옵션이 별도의 파라미터를 받는 경우 콜론을 함께 쓴다.

예를 들어 -h, -v, -f filename을 받는 세 가지 옵션이 있다고 하면 옵션스트링은 "hvf:"가 된다. 각각의 옵션을 파싱해내기 위해서는 getopt()함수가 0을 리턴할 때까지 계속해서 반복하면 된다.

그외 전역변수

이 함수와 관련된 전역 변수에는 다음과 같은 것들이 있다.

  • optarg : 옵션 뒤에 별도의 파라미터 값이 오는 경우, 이를 파싱한 결과 파라미터 값은 optarg에 문자열로 저장된다.
  • optind : 다음번 처리될 옵션의 인덱스이다. 만약 파싱한 옵션이후에 추가적인 파라미터를 받는다면 (예를 들어 입력 파일 이름 같이) 이 값을 활용할 수 있다. getopt()함수는 한 번 호출될 때마다 이 값을 업데이트한다.
  • opterr : 옵션에 문제가 있을 때, 이 값은 0이 아닌 값이되며, getopt()함수가 메시지를 표시하게 된다.
  • optopt : 알 수 없는 옵션을 만났을 때 해당 옵션이 여기에 들어간다. (이 때 getopt의 리턴값은 ‘?’가 된다.)

샘플 코드

아래 샘플 코드는 간략한 예를 보여준다. 이 코드에서는 a, b, c의 세 옵션을 인식하며 각각의 옵션이 주어지는 경우 해당 플래그 변수를 1로 정의하고 그 결과를 출력한다.

1:  #include <stdio.h>
2:  #include <unistd.h> // for getopt()
3:  
4:  int main(int argc, char * const * argv){
5:      int flag_a = 0, flag_b = 0, flag_c = 0;
6:      int c; // option
7:      while( (c = getopt(argc, argv, "abc")) != -1) {
8:          // -1 means getopt() parse all options
9:          switch(c) {
10:             case 'a':
11:                 flag_a=1;
12:                 break;
13:             case 'b':
14:                 flag_b=1;
15:                 break;
16:             case 'c':
17:                 flag_c=1;
18:                 break;
19:             case '?':
20:                 printf("Unknown flag : %c", optopt);
21:                 break;
22:         }
23:     }
24:     if(flag_a) {printf("flag a is ON \n");}
25:     if(flag_b) {printf("flag b is ON \n");}
26:     if(flag_c) {printf("flag c is ON \n");}
27:     return 0;
28: }

보통 while문으로 루프를 돌면서 옵션들을 하나씩 검사한다. getopt가 optstring으로 정의되지 않은 옵션문자를 만나면 opterr 값이 세팅되고, 동시에 getopt()함수가 해당 문자에 대해 메시지를 출력한다. 이 때 반환값은 '?'가 되고 이때 알 수 없는 옵션 문자는 optopt에 저장된다.

추가 파라미터를 필요로하는 옵션

gcc의 -o와 같이 파일 이름등의 추가 파라미터를 필요로하는 옵션은 다음과 같이 처리한다.

  1. optsting에서 옵션 문자 뒤에 콜론을 붙인다. "abcf:"와 같은 식으로 쓴다.
  2. getopt()함수는 -f를 만나면 자동으로 그 뒤에 붙은 문자열을 공백까지 탐색하여 이를 optarg에 복사한다.
  3. 처리시에는 리턴값이 ‘f’인 경우 optarg의 값을 그대로 액세스하면 된다.

이러한 처리 코드는 아래와 같다.

case 'f':
    memcpy(file_name, optarg, strlen(optarg));
    break;

또한 파라미터가 있다고 정의한 경우, 파라미터 값이 빠진채로 실행되었다면 이 때는 옵션이 인식할 수 없는 옵션으로 처리된다.

case '?':
    if(optopt == 'f') {
        printf("option -f requires FILENAME\n");
    }
    break;

optind

옵션문자들을 모두 처리한 이후에 프로그램 자체의 파라미터를 처리하기 위해서는 optindargv의 인덱스로 사용하여 파라미터를 전달 받을 수 있다.

그외 긴 이름

--output등의 긴 이름 옵션을 사용하기 위해서는 getoptlong함수를 사용한다.

[C] 동적으로 할당한 메모리 영역의 크기를 나중에 알 수 있게 하기

동적으로 할당받은 메모리의 크기를 구하기

sizeof 함수는 (정확히는 매크로 함수) 타입의 크기를 반환한다. 배열 이름은 배열의 시작번지를 나타내므로 포인터와 거의 동일하게 취급되고, 또 많은 서적에서 이 둘을 같은 것이라고 말하는데, 사실은 다르다. sizeof() 함수는 배열을 인자로 넣으면 배열의 크기를 알려준다. 따라서 배열의 요소의 개수는 sizeof(intArr) / sizeof(int)와 같은 식으로 구할 수 있다.

이와 반대로 동적으로 메모리를 할당받는 malloc 류의 함수는 포인터를 반환한다. 포인터는 unsigned long int형으로 시스템에 따라 4혹은 8바이트의 크기가 된다. 따라서 배열을 동적으로 할당하기 위해 메모리 할당 함수를 사용하면 sizeof(포인터이름)은 포인터의 크기를 반환하므로 배열의 요소의 개수를 알아낼 수가 없다.

사실 이미 메모리를 할당하는 시점에, 크기를 지정해 주었으므로 그 영역의 크기는 알고 있는 상황이지만 이 값을 변수에 담아서 계속 들고다니기란 조금 귀찮다. 여기서는 배열을 할당할 시에 그 크기를 나중에 알 수 있도록 하는 꼼수를 소개한다. [C] 동적으로 할당한 메모리 영역의 크기를 나중에 알 수 있게 하기 더보기

[c/c++] SMI를 SRT로 컨버팅하기

윈도버전

컴파일 시 큰 문제는 없는데, 대입하는 포인터의 형이 달라서 경고가 뜨는 경우가 있다는데, 내 경우에는 윈도에서 컴파일시 오류가 났다. char* 형에대해 메모리를 할당할 때 캐스팅을하지 않으면 void*로 취급하므로 오류가 나면서 컴파일이 안됐다. 수정~ [c/c++] SMI를 SRT로 컨버팅하기 더보기

[C/C++] 구조체 정보를 디스크에 읽고 쓰는 방법 (2/2)

지난 글에서는 구조체로 정의한 임의의 정보들을 디스크에 기록하는 방법을 예시를 통해 살펴보았다. 그런데 해당 글의 예제는 실제 기록하고 읽어들일 때 사용하는 함수(fread(), fwrite())의 사용법에 초점을 맞추었던 관계로 모든 정보가 구조체 내부에 들어있는 상황을 가정했다.

모든 정보가 구조체 속에 들어있는 상황은 구조체 멤버 중에 포인터가 없는 상황을 말한다. 모든 멤버의 정보는 구조체의 영역 내부에 존재하므로 구조체가 점유하고 있는 메모리 공간은 연속적이다. 따라서 이 경우 해당 구조체의 데이터를 메모리의 다른 어딘가로 복사할 수도 있고, 똑같은 방식으로 파일로 복사할 수도 있다. 바이트를 파일에 복사하는 것이 곧 디스크에 정보를 기록하는 것이었던 셈이다.

하지만 실제 세계의 많은 예에서는 포인터가 자주 쓰인다. 문자열이나 크기가 정해지지 않은 배열, 그 외에 내부 구조를 알 수 없는 Opaque 타입같은 것들이 있다. (물론 Opaque 타입 같은 경우에는 내부 구조를 알 수 없으므로 저장도 할 수 없다.) 내부 속성이 포인터로 되어 있다는 것은 실제 값이 객체의 외부에 나와 있다는 것이므로, 실질적인 전체 객체의 데이터가 조각조각 흩어져 있는 것으로 이해할 수 있다.

하지만 파일에 데이터를 저장하는 것은 연속된 메모리 공간을 디스크로 복사하는 것이므로, 실제 데이터를 구성하는 모든 값들을 하나의 연속된 바이트 스트림으로 변환하는 것과 사실상 같은 일을 수행하게 된다.

  • 기본적인 원시 타입 값들은 각각 그 크기가 알려진 고정값이다. 이들은 해당 크기만큼의 바이트를 하나씩 저장하면 된다.
  • 포인터로 참조되는 값의 크기가 고정되어 있다면 마찬가지로 해당 타입의 크기만큼의 바이트를 저장하면 된다.
  • 값의 크기가 고정되어 있지 않은 데이터를 참조하는 포인터의 경우, 크기 (보통 이런 데이터의 크기는 size_t (unsigned long long)를 사용한다.) 값을 1개 기록하고 이어서 해당 크기 만큼의 값을 기록한다.

그리고 각각의 멤버들을 기록한 순서대로 하나씩 읽어서 원래의 데이터를 복원하면 된다. 그러면 지난번 예제와 유사하게 데이터를 저장하고, 파일로부터 읽어서 복원하는 연습을 해보자.

먼저 다음과 같은 Friend 타입을 정의한다. 이전 예제와 비슷한데, 이름을 저장하는 멤버가 char[] 배열이 아니라 char *로 포인터로 구성되어 있다. 따라서 이름을 나타내는 문자열의 길이는 결정되지 않았다.


typedef struct _friend {
  int age;
  float height;
  char * name;
} Friend;

이 타입을 파일에 저장하고 다시 읽어내는 기능을 구현하기 위해서는 2개가 아닌 4개의 함수를 작성할 것이다. 2개는 입출력에 해당하는 기능이며, 다른 2개는 좀 편하게 쓰려고..;;;

  1. Friend 객체에 name 속성을 할당해주는 함수
  2. 생성된 Friend 객체의 정보를 표시하는 함수 (저장/복원한 결과를 확인하기 위해)
  3. Friend를 파일에 기록하는 함수
  4. 파일에 기록된 내용을 바탕으로 Friend 데이터를 복구하는 함수
void FriendSetName(Friend *friend, const char * newName);
void FriendShow(Friend friend);
void FriendWriteIntoFile(Friend *friend, FILE * f);
void FriendReadFromFile(Friend *friend, FILE * f);

편의함수 작성

먼저 Friend 객체의 name 속성을 조작할 수 있는 함수를 작성해보자. 주어진 문자열이 필요한 순간까지 유지될 것이라는 보장이 없으므로, 따로 마련한 메모리에 복사하고 해당 포인터를 friend.name으로 연결한다. 구조체 내부의 값을 변경해야 하므로 포인터 형식으로 인자를 받아야 한다.

void FriendSetName(Friend *friend, const char * newName)
{
	// copy name into friend
	if(friend->name == newName) { return; }
	if(friend->name != NULL) { free(friend->name); }
	size_t l = strlen(newName);
	friend->name = (char *)malloc(l + 1);
	strcpy(friend->name, newName);
}

다음은 테스트용으로 Friend 타입 값의 내용을 출력하는 함수를 작성한다. (간단)

void FriendShow(Friend friend)
{
	printf("Friend at 0x%x : %s (%d, %.1f)\n", 
		&friend, friend.name, friend.age, friend.height);
}

기록하기

다음은 Friend 객체를 파일에 저장하는 실제 예시이다. 각각의 멤버를 순서대로 기록한다. 문자열의 길이 정보는 복원시에 필요하므로 따로 구해서 같이 기록해주어야 한다. 그외의 방법은 이전에 소개한 fwrite() 함수만 잘 사용하면 된다. 참고로 문자열의 길이를 구하는 strlen()함수는 종료문자(NULL)를 포함하지 않으므로 +1 한 크기만큼을 함께 저장해준다.

void FriendWriteIntoFile(Friend *friend, FILE *f)
{
	// Wrtie
	size_t l_name = strlen(friend->name) + 1;
	fwrite(&(friend->age), sizeof(int), 1, f);
	fwrite(&(friend->height), sizeof(float), 1, f);
	fwrite(&l_name, sizeof(size_t), 1, f);
	fwrite(friend->name, sizeof(char), l_name, f);
}

읽어오기

읽어오는 함수 부분은 조금까다롭다. 우선 가장 중요한 것은 기록된 순서와 똑같은 순서대로 값을 가져와야 한다는 것이다. 두 번째로 주의해야 할 것은 Friend 내부에는 문자열을 보관할 공간이 없다는 점이다. 따라서 읽어오는 시점에 문자열을 위한 메모리 공간은 반드시 따로 할당하여 생성해야 한다.

void FriendReadFromFile(Friend *friend, FILE *f)
{
	// Read
	size_t l_name;
	fread(&(friend->age), sizeof(int), 1, f);
	fread(&(friend->height), sizeof(float), 1, f);
	fread(&l_name, sizeof(size_t), 1, f);
	// Allocate space for name string
	friend->name = (char*)malloc(l_name);
	fread(friend->name, sizeof(char), l_name, f);
}

예제 작성

이제 이상의 내용들을 활용해서 데이터를 생성하고, 파일에 저장한 후 다시 다른 객체로 내용을 로드하는 과정을 따라보자.

  1. fr1을 생성하고 초기화한다.
  2. fr1의 내용을 출력한다.
  3. fr1을 파일에 저장한다.
  4. fr2를 생성한다.
  5. 3.에서 저장한 파일에서부터 fr2를 초기화한다.
  6. fr2의 내용을 출력한다.

즉 파일을 매개로 객체를 복사하는 동작인 셈이다.

int main() {
	Friend fr1, fr2;
	fr1.age = 20;
	fr1.height = 179.4;
	fr1.name = NULL;
	FriendSetName(&fr1, "A friend in need is a friend indeed.");
	FriendShow(fr1);
	
	printf("Save fr1 into a file.\n");
	FILE * f = fopen("a.dat", "wb");
	FriendWriteIntoFile(&fr1, f);
	fclose(f);
	printf("Saved.\n");
	
	
	printf("Read fr2 from file\n");
	fr2.name = NULL;
	f = fopen("a.dat", "rb");
	FriendReadFromFile(&fr2, f);
	fclose(f);
	
	FriendShow(fr2);
	printf("Completed.\n");

	// release
	free(fr1.name);
	free(fr2.name);
	return 0
};

다음은 출력결과. 두 객체의 메모리 상의 위치가 다른 것으로 서로 다른 객체인 것을 알 수 있고, 그 값이 동일하게 복구된 것을 확인할 수 있다.

Friend at 0x64fdc8 : A friend in need is a friend indeed. (20, 179.4)
Save fr1 into a file.
Saved.
Read fr2 from file
Friend at 0x64fdb0 : A friend in need is a friend indeed. (20, 179.4)
Completed

[C/C++] 구조체 정보를 디스크에 읽고 쓰는 방법 (1/2)

텍스트가 아닌 애플리케이션의 고유한 데이터를 파일에 기록하는 방법에 대해서 알아보자. 애플리케이션 내에서 정의한 어떤 정보는 다양한 형태와 구조를 가질 수 있기 때문에, 평문 텍스트를 저장하는 것과는 다르다. 이 글에서는 간단한 데이터 타입을 구조체를 통해 정의해보고, 이렇게 만들어진 데이터를 어떻게 디스크에 저장하고 또 다시 불러올 수 있는지에 대해서 알아볼 것이다.

[C/C++] 구조체 정보를 디스크에 읽고 쓰는 방법 (1/2) 더보기