10-4-나.main 함수의 인수

main도 일종의 함수이므로 인수를 가질 수 있고 리턴값도 가질 수 있다. main은 프로그램 실행 직후에 자동으로 호출된다는 점에 있어서 일반 함수와는 다른 특별한 면이 있으며 프로그램의 시작점이기 때문에 이름은 고정되어 있지만 함수의 원형은 고정적이지 않다. main 함수의 원형은 다소 복잡한데 다음과 같은 조합이 가능하다.

 

void(또는 int) main(int argc,char *argv[],char *env[]);

 

리턴값은 int형이거나 void형 중 하나를 선택할 수 있으며 세 개의 인수를 가지는데 인수는 뒤쪽부터 차례대로 생략 가능하다. 그래서 main 함수의 가능한 원형은 다음 여덟가지나 된다.

 

void main(void);

void main(int argc);

void main(int argc,char *argv[]);

void main(int argc,char *argv[],char *env[]);

int main(void);

int main(int argc);

int main(int argc,char *argv[]);

int main(int argc,char *argv[],char *env[]);

 

이 중 자주 사용되는 형식은 ⑤번과 ⑦번인데 이 책의 예제들은 주로 제일 간단한 ①번 형식을 사용하고 있다. main 함수의 가장 완벽한 원형은 ⑧번인데 이 원형에서 리턴값과 인수들의 의미에 대해 알아보자.

리턴값

리턴값은 없거나 있다면 정수형이어야 한다. C++ 표준 문서(43p)에는 정확하게 It shall have a return type of type int, but otherwise its type is implementation defined 이렇게 되어 있다. main 함수의 리턴값은 int형의 타입을 가지는 것이 좋지만 구현 방식에 따라 다른 타입을 가지는 것도 가능하다. 즉 C++ 스팩은 int를 강력히 권장하기는 하지만 강요하지는 않는다. 비주얼 C++은 int, void 모두 가능한데 과거부터 이렇게 써 왔기 때문에 호환성을 더 중요시한다는 입장이다. gcc는 표준의 권고대로 main 함수의 리턴 타입으로 int만 인정한다.

main 함수가 리턴하는 값을 탈출 코드(Exit Code)라고 하는데 프로그램이 실행을 마치고 운영체제로 복귀할 때 리턴되는 값이다. 함수가 작업 결과를 호출원으로 돌려주듯이 응용 프로그램도 작업결과를 리턴할 수 있다. main 함수가 프로그램 그 자체이므로 main 함수의 리턴값이 곧 프로그램의 리턴값이 된다.

탈출 코드는 보통 사용되지 않고 무시되는데 이 프로그램을 호출한 프로그램(보통 쉘)이 꼭 필요할 경우 탈출 코드를 사용하기도 한다. 예를 들어 도스의 배치 파일(*.bat) 내에서 응용 프로그램을 실행했을 때 이 프로그램의 실행 결과를 ERRORLEVEL이라는 환경 변수로 참조할 수 있다. 32비트 환경에서는 탈출 코드 외에도 응용 프로그램간의 통신을 위한 장치가 많이 준비되어 있어 요즘은 main의 리턴값을 잘 사용하지 않는다.

당연한 얘기겠지만 main 함수가 int형을 리턴할 때, 즉 원형을 int main()으로 했을 때는 main의 끝에 반드시 return문이 있어야 하며 그렇지 않을 경우 리턴값이 없다는 경고가 발생한다. 다른 함수들은 값을 리턴하지 않을 경우 에러로 처리되지만 main 함수만큼은 경고로 처리한다는 면에서 컴파일러가 main을 조금 특수하게 취급하는 것을 알 수 있다.

 

int main()

{

}

int func()

{

}

경고로 처리됨

에러로 처리됨

 

C++ 표준에는 main이 값을 리턴하지 않을 경우를 인정하고 있으므로 main은 설사 리턴 타입이 int이더라도 return문을 생략할 수 있다. 비주얼 C++ 7.0이나 gcc로 테스트해 보면 과연 그렇다는 것을 확인할 수 있을 것이다. 그러나 비주얼 C++ 6.0은 표준 이전의 컴파일러라 경고를 출력하며 그래서 이 귀찮은 경고를 보지 않기 위해 이 책의 모든 예제들은 비록 스팩의 권고와는 다르지만 void main() 형식을 사용한다. 5줄밖에 안되는 예제에 return 0;가 포함되면 예제 길이가 20% 늘어나므로 도저히 그럴 수 없는 것이다.

argc

운영체제가 이 프로그램을 실행했을 때 전달되는 인수의 개수이다. 함수를 호출할 때 인수를 전달하듯이 운영체제가 프로그램을 호출할 때도 인수를 전달할 수 있다. 만약 파일끼리 복사하는 boksa.exe라는 프로그램을 작성했다고 하자. 이 프로그램은 복사할 원본 파일과 복사 대상 파일의 이름을 다음과 같이 인수로 전달받을 것이다.

 

boksa file1.txt file2.txt

 

이때 실행 파일의 이름인 boksa뒤의 file1.txt와 file2.txt가 프로그램의 인수이다. 이 인수들은 운영체제가 프로그램으로 전달하는 작업거리, 즉 어떤 일을 하라는 정보들이다. main 함수가 프로그램의 시작점이기 때문에 프로그램의 인수가 main 함수로 전달된다.

argc 인수는 main 함수로 전달된 인수의 개수이다. 첫 번째 인수는 실행 파일명으로 고정되어 있으므로 argc는 항상 1보다 크다. boksa a b 식으로 호출할 경우 argc는 3이 된다. 이 값은 인수를 필요로 하는 프로그램에서 인수가 제대로 입력되었는지를 검사할 때 사용한다. 인수가 없으면 실행할 수 없는 경우 이 값을 조사해서 인수가 제대로 전달되었는지 확인한다.

만약 필요한 인수가 없다거나 또는 남는다면 에러 메시지를 출력하고 프로그램을 끝내거나 아니면 디폴트를 취해야 한다. 예를 들어 boksa.exe는 복사 원본과 복사 대상 파일이 반드시 전달되어야 하므로 argc가 3 미만일 경우 "복사할 파일을 입력하세요"라는 에러 메시지를 출력하면 될 것이다.

argv

프로그램으로 전달된 실제 인수값이며 이 값을 읽으면 운영체제로부터 어떤 인수가 전달되었는지 알 수 있다. 운영체제가 프로그램을 실행할 때는 항상 문자열 형태의 쉘 명령을 입력하기 때문에 인수의 타입은 항상 문자열일 수밖에 없다. file1.txt라고 하든 1234라고 하든 명령행에서는 정수니 실수니 하는 것들이 없으므로 입력된 액면 그대로 문자열 인수가 전달된다. 만약 전달된 인수가 정수라면 일단 문자열로 받아서 atoi 등의 변환 함수로 정수로 바꿔 사용해야 한다.

이런 문자열 인수가 한꺼번에 여러 개 전달될 수 있다. boksa file1.txt file2.txt의 경우 boksa, file1.txt, file2.txt 세 개의 문자열이 인수로 전달된다. 그래서 argv는 문자형 포인터를 가리키는 포인터여야 한다. 간단히 말하자면 argv는 문자형 이중 포인터이고 좀 더 현실적으로 얘기하자면 문자열 배열이다. 원형에는 char *argv[]로 되어 있는데 이 표현은 char **argv와 같다. main 함수의 인수를 함수 부분에서 설명하지 못하고 여기까지 미룬 이유가 바로 이 함수가 이중 포인터를 사용하기 때문이다.

argv[0]는 항상 프로그램의 이름이 전달되는데 통상 완전 경로라고 보면 된다. 정확하게 표현하자면 쉘이 이 프로그램을 실행할 때 입력한 실행 파일명인데 명령행에서 실행 파일명만 입력했으면 경로는 포함되지 않을 수도 있다. c:\prg 디렉토리에 있는 boksa.exe를 실행했다면 argv[0]는 "c:\prg\boksa.exe"이다. 이 값은 우리가 원하는 인수, 즉 프로그램의 작업거리라고는 할 수 없어 잘 사용되지 않는다. 프로그램으로 전달된 첫 번째 인수는 argv[1]이며 두 번째 인수 이후부터 argv[2], argv[3] 식으로 전달된다. boksa file1.txt file2.txt 명령으로 boksa명령을 실행했다면 이때 전달되는 argv 배열은 다음과 같다.

env

운영체제의 환경 변수를 알려 준다. 환경 변수는 운영체제마다 다르게 정의하는데 DOS의 경우 Path, Prompt 등이 있고 윈도우즈의 경우 컴퓨터 이름, 시스템 디렉토리 등의 정보들이 있다. 응용 프로그램에게 자신이 실행되는 환경을 알 수 있도록 해 준다는 의도로 전달되는 인수이나 이 인수가 아니더라도 환경 변수를 조사할 수 있는 다른 방법이 있기 때문에 실질적으로 사용되지 않는다. 무시해 버리자.

 

다음 예제는 main 함수로 전달되는 인수를 화면으로 출력하기만 한다. 실용성은 없지만 명령행에서 입력한 인수가 어떻게 프로그램으로 전달되는지를 볼 수 있을 것이다.

 

: MainArg

#include <Turboc.h>

 

void main(int argc, char *argv[],char *env[])

{

     int i;

 

     printf("전달된 인수의 개수 = %d\n",argc);

     for (i=0;i<argc;i++) {

          printf("%d번째 인수 = %s\n",i,argv[i]);

     }

}

 

통합 개발 환경에서도 인수를 지정할 수 있지만 불편하므로 명령 프롬프트창을 열고 직접 프로그램을 실행해 보자.

 

C:\CExam\MainArg\Debug>MainArg dog baby

전달된 인수의 개수 = 3

0번째 인수 = C:\CExam\MainArg\Debug\MainArg.exe

1번째 인수 = dog

2번째 인수 = baby

 

전달된 인수의 개수를 보여주고 모든 인수를 출력하였다. 다음 예제는 좀 더 실용적인데 명령행에서 계산식을 입력받아 간단한 정수 사칙 연산을 수행한다.

 

: CalcArg

#include <Turboc.h>

 

void main(int argc, char *argv[])

{

     int a,b,c;

     char op;

 

     if (argc != 4) {

          printf("계산식을 정확하게 입력해 주십시오.");

          exit(0);

     }

 

     a=atoi(argv[1]);

     b=atoi(argv[3]);

     op=argv[2][0];

 

     switch (op) {

     case '+':

          c=a+b;

          break;

     case '-':

          c=a-b;

          break;

     case '*':

          c=a*b;

          break;

     case '/':

          c=a/b;

          break;

     default:

          printf("사칙 연산만 할 수 있습니다.\n");

          exit(0);

     }

 

     printf("계산 결과 = %d",c);

}

 

CalcArg 실행 파일명과 간단한 이항 연산식을 인수로 전달하면 이 식을 계산해 출력한다. 인수간의 구분을 위해서는 피연산자와 연산자 사이를 반드시 공백으로 띄워야 한다. 실행 결과는 다음과 같다.

 

C:\CExam\CalcArg\Debug>CalcArg 234 + 567

계산 결과 = 801

C:\CExam\CalcArg\Debug>CalcArg 578 * 12

계산 결과 = 6936

 

인수가 4개가 아니면 간단한 사용법을 출력한 후 프로그램을 종료하도록 했다. 이 프로그램은 계산의 재료를 명령행으로부터 전달받는데 계산할 식을 제대로 주지 않으면 정상적인 동작을 할 수 없다. 만약 argc를 점검하지 않으면 엉뚱한 인수로 계산을 수행하게 되므로 결과를 전혀 예측할 수 없을 것이다.

일단 인수가 4개 다 전달되었으면 argv[1]과 argv[3]에 전달된 피연산자를 구해 a, b에 저장한다. 명령행에서 전달된 인수는 모두 문자열이므로 정수로 바꾸기 위해 atoi 함수를 사용했다. 연산자는 중간의 argv[2]로 전달되는데 모두 한 문자로 구성되어 있으므로 argv[2]의 첫 문자만 살펴보면 된다. 만약 연산자가 +, -, *, / 중의 하나가 아니면 에러 메시지를 출력한다.

인수 4개가 제대로 전달되었고 사칙 연산중의 하나이면 입력된 식대로 연산하여 그 결과를 출력하고 프로그램을 종료한다. 연산자와 피연산자 사이에 공백이 반드시 있어야 하고 사칙 연산밖에 못하지만 main 함수의 인수를 어떻게 전달받고 사용하는지를 살펴보기에는 부족하지 않을 것이다.