Interactive한 키보드 입력은 대개 여러 입력이 모아져서 프로그램에 한 줄씩 전달되게 됩니다. 이런 까닭은 운영 체제가 입력된 줄을 편집할 (backspace/delete 등) 수 있는 일관된 방법을 (편집 기능을 일일히 코딩하지 않더라도) 제공하기 위해서 입니다. 즉 사용자가 RETURN 키를 눌렀을 때에 비로소, 한 줄이 프로그램에 전달됩니다. 프로그램이 한 문자씩 읽어들이게 짜여져 있다고 하더라도 한 문자를 읽어들이는 함수(예를 들면 getchar())가 호출될 때, 프로그램이 중단되고, 사용자에게 한 줄을 입력받은 다음, 각각의 문자가 빠른 속도로 그 함수에게 전달됩니다.
질문한 내용처럼 한 문자가 읽혀지는 즉시, 프로그램에 전달되게 하려면. 한 줄씩 전달되게 하는 그 logic을 멈추게 해야 합니다. 어떤 시스템 (예를 들면 MS-DOS, VMS의 어떤 모드)에서는 프로그램이 OS 수준의 입력 함수를 불러서 해결하기도 하며, 어떤 시스템 (예를 들면 Unix, VMS의 어떤 모드)에서는 입력을 처리하는 (대개 “터미널 드라이버”라고 부르는) OS의 부분에다 줄 단위 처리 기능을 끄라고 알려야 합니다. 그리고 일반적인 입력 함수(예를 들면 read(), getchar(), 등)를 써서 한 문자씩 읽습니다. 또 어떤 시스템에서는 (특히 오래된 배치 프로세싱19.1 시스템) 입력 처리가 주변 장치에 의해 처리되며, 줄 단위 입력만 처리할 수 있어, 다른 방식을 쓸 수 없는 경우도 있습니다.
그러므로, 문자 단위로 처리하고 싶거나 (키보드 에코 기능을 끄고 싶다면), 여러분이 쓰고 있는 시스템에 의존적인 특수한 방법을 써야 합니다. comp.lang.c는 C 언어가 정의하고 있는 기능에 대한 것을 다루는 곳이므로 이 질문을 다루기에는 적당하지 않습니다. 시스템에 의존적인 다른 뉴스 그룹, 예를 들면 아래 뉴스 그룹에 묻는 것이 좋습니다:
그러나 이러한 질문은 자주 이 곳에 게시되므로, 일반적인 경우에 처리할 수 있는 방법을 간단히 소개합니다.
어떤 버전의 curses에서는 cbreak(), noecho(), getch()와 같은, 질문의 목적에 맞는 함수를 제공합니다. 간단히, 짧은 암호를 입력할 수 있는 루틴이 필요하다면 getpass()를 쓰면 됩니다.
UNIX에서는 ioctl() 함수를 써서 터미널 드라이버 모드를
제어할 수 있습니다. (“classic” 버전에서는 CBREAK나 RAW를
쓰고, System V나 POSIX 시스템에서는 ICANON, c_cc[VMIN]
,
그리고 c_cc[VTIME]
을 쓰고,
대부분 시스템에서는 ECHO를 쓸 수 있습니다.
또 system()과 stty 명령도 쓸 수 있습니다. (좀 더 자세한
설명을 원한다면, “classic” 버전에서는 <sgtty.h>
와
tty(4)를, System V에서는 <termio.h>
와 termio(4)를,
POSIX 시스템에서는 <termios.h>
와 termios(4)를 찾아보시기
바랍니다.)
MS-DOS에서는 getch()나 getche()를 쓰거나, BIOS 인터럽트에 해당하는 함수를 쓰면 됩니다.
VMS에서는 스크린 관리(Screen Management) 루틴(SMG$)이나 curses, 또는 IO$_READVBLK 함수를 통해, 저수준 $QIO를 (또는 IO$M_NOECHO) 쓰면 됩니다. (VMS 터미널 드라이버에서는 한 번에 한 글자씩19.2 읽거나 “pass through” 모드를 쓸 수 있습니다.)
다른 운영체제에서 하는 방법은 직접 찾아보시기 바랍니다.
(덧붙이면, 단순히 setbuf()나 setvbuf()를 써서 stdin이 버퍼링되지 않게 하는 것은 한 번에 한 글자씩 받아 들이는 것과 별 상관이 없습니다.)
이식성이 뛰어난 프로그램을 만들고자 할 때 가장 좋은 방법은 이러한 함수들을 정의하고 (1) 터미널 드라이버나 입력 시스템을 “한 번에 한 글자씩” 읽도록 설정하고, (2) 문자를 입력받고, (3) 프로그램을 끝낼 때 원래의 터미널 모드로 복원시키는 것입니다. (이론상, 이런 종류의 함수들은 언젠가 C 표준에 포함될 것입니다.) 이 FAQ 목록의 확장판(질문 20.40 참고)에서는 여러 시스템에서 이러한 함수를 만드는 법에 대한 예제가 나와 있습니다.
... TODO ... 덧붙여 질문 19.2도 참고하시기 바랍니다.
즉, ioctl()은 권장되지 않는 함수입니다. 자세한 것은 [Lewine] 를 참고하기 바랍니다.
c_cc[VTIME]
, kbhit(),
rdchk(), 그리고 open()이나 fcntl()에서 O_NDELAY
옵션이 이러한 작업을 해 줄 수 있습니다.
덧붙여 질문
19.1도 참고하시기 바랍니다.
\r
'을 출력하는
것입니다. 이 문자를 라인 피드(line feed) 문자 없이 혼자 쓰면
현재 줄을 다시 덮어 쓸 수 있습니다.
또는 백스페이스(backspace) 문자인 `\b
'을 쓰면 커서를
한 칸 왼쪽으로 옮겨 한 글자를 덮어 쓸 수 있게 됩니다.
화면을 지우는 방법 중 가장 이식성이 높은 방법은 폼 피드(form-feed)
문자 (`\f
')를 출력하는 것입니다. 이 문자를 출력하면
대다수 화면이 지워지게 됩니다. (조금 지저분하지만) 더 이식성이
높은 방법은 충분히 많은 newline 문자를 출력해서 한 화면 분의
여러 줄들을 넘겨 버리는 것입니다.
마지막 수단으로, system()을 써서 (질문
19.27 참고)
화면을 지우는 명령을 실행할 수도 있습니다.
\033
')
처리되며, 이 입력을 분석해 내는 것은 상당히 까다롭습니다.
(keypad()를 부를 경우 curses가 이 작업을 대신 처리해 줍니다.)
MS-DOS에서는 만약 문자 값이 0인 것(문자 `0'이 아님!)이 들어온다는 것은, 다음 문자가 특별한 키 입력 값이라는 것을 나타내는 신호입니다. 이 것을 스캔 코드(scan code)라고 하는데, 여기에 관한 것은 여러 DOS programming guide를 찾아보시면 됩니다. (몇 개만 말해보면; up, left, right, down 화살표 키는 각각 72, 75, 77, 80이며, function key들은 59에서 68 사이의 값을 가집니다.)
fprintf(ofd, "\033[J");을 쓰면 escape sequence,
ESC [ J
를 보낼 수 있습니다.
http://www.gnu.org/software/plotutils/plotutils.html
MS-DOS에서 작업한다면 VESA나 BGI 표준에 따르는 라이브러리를 쓰기를 원할 것입니다.
플로터에 그리는 작업은 대개 특정 escape sequence로 이루어집니다; 플로터 제조자는 대개 C 언어로 된 라이브러리 패키지를 제공하므로, 이 것을 쓰던지 net을 뒤져보기 바랍니다.
윈도우 시스템(매킨토시나 X Window System, 또는 Microsoft Windows)을 쓰고 있다면, 윈도우 기능을 쓰고 싶어할지도 모릅니다; 이 경우 관련된 뉴스 그룹이나 FAQ 목록을 먼저 참고하기 바랍니다.
이런 목적으로 쓸 수 있는 함수는 stat(), access(), fopen()이 있습니다. (fopen()을 쓴다면 파일을 읽기 모드로 열고 바로 닫으면 됩니다. fopen()이 실패한다고 해서 무조건 파일이 존재하지 않는다는 것은 아닙니다.) 물론 이 함수들 중에서는 fopen()이 가장 이식성이 뛰어납니다. UNIX의 set-UID 기능이 있다면 access() 함수는 주의깊게 써야 합니다.
단순히 파일이 성공적으로 열렸다고 가정하는 것보다 항상 리턴 값을 검사해서 실패했는지 조사하는 것이 바람직합니다.
UNIX에서는 stat() 함수가 정확한 값을 알려 줄 수 있습니다. 대부분의 다른 시스템에서는 UNIX와 비슷한 stat() 함수를 제공하고, 비슷한 값(정확하지 않을 수도 있음)을 알려줍니다. fseek()를 써서 파일 위치를 맨 뒤로 옮긴 다음, ftell()을 써서 값을 얻어내거나 fstat()을 쓸 수도 있지만 이 두가지 방법은 같은 단점을 가집니다. 일단 fstat()은 이식성이 뛰어나지 않으며, stat()과 같은 정보를 알려줍니다. ftell()은 바이너리 파일이 아닐 경우 (즉 텍스트 파일), 정확한 바이트 갯수를 알려준다는 보장이 없습니다. 어떤 시스템은 filesize()나 filelength()와 같은 함수를 제공하지만, 역시 이식성이 뛰어나지 않습니다.
파일을 읽기 전에 그 파일의 크기를 아는 것이 꼭 필요한 지 먼저 생각해 보기 바랍니다. 왜냐하면 C 언어에서 파일 크기를 알아내는 가장 정확한 방법은 파일을 열어서 읽어보는 것이기 때문입니다. 파일을 읽어가며 파일 크기를 계산하는 것도 한가지 해결책이 될 수 있습니다.
F_FREESP
옵션을 써서 파일 크기를
줄일 수 있습니다. MS-DOS에서는 가끔 write(fd, "", 0)
을 써서
해결할 수 있지만 이들은 모두 이식성이 뛰어나지 않습니다.
마찬가지로 파일의 앞 부분을 잘라내는 것도 이식성이 뛰어난 방법은
존재하지 않습니다.
덧붙여 질문
19.14도 참고하시기 바랍니다.
fopen("c:\newdir\file.dat", "r")
\n
과 \f
가 쓰였으므로
-- 아마 존재하지 않을 것입니다. 따라서 생각한 것처럼 파일이
열리지 않습니다.
문자 상수나 문자열에서 백슬래시, \
는 이스케이프 문자로
해석되어, 뒤따르는 문자에게 특별한 의미를 주는 데에 쓰인다는 것을
기억하기 바랍니다. 백슬래시가 파일 이름19.5에 쓰이기 위해서는
백슬래시를 두번 써서 다음과 같이 만들어야 합니다:
fopen("c:\\newdir\\file.dat", "r")
MS-DOS에서는 다른 방법을 쓸 수 있습니다. 백슬래시 대신에 슬래시를 써도 디렉토리를 구분하는 문자로 인식되기 때문에 다음과 같이 하면 됩니다:
fopen("c:/newdir/file.dat", "r")
(그러나, 전처리기 directive인 #include
에서 쓰는
파일 이름은 문자열이 아니라는 것을 명심해야 합니다.
따라서 거기에는 백슬래시를 그냥 한번만 써도 됩니다.)
가능한 상대 경로를 쓰기 바라며, 상대 경로가 복잡해질 경우라면, 대부분의 컴파일러는 헤더 파일이 위치한 경로를 지정해 줄 수 있는 옵션을 제공하므로, 그 기능을 쓰시기 바랍니다 (대개 컴파일러는 -I 옵션을 이 목적으로 씁니다.)
두 가지 이유에서 동시에 열 수 있는 파일 갯수에 제한이 있습니다: 하나는 운영 체제에서 쓸 수 있는 저수준 “파일 descriptor”나 “파일 핸들”의 갯수에 제한이 있거나, 표준 입출력(stdio) 라이브러리에서 쓸 수 있는 FILE 구조체의 갯수에 제한이 있기 때문입니다. 이 두 수치가 모두 충분해야 파일을 열 수 있습니다. MS-DOS 시스템에서는 CONFIG.SYS 파일을 수정해서 운영체제가 다룰 수 있는 파일의 갯수를 고칠 수 있습니다. 어떤 컴파일러는 어떤 명령을 (또는 몇몇의 소스 파일을) 써서 stdio FILE 구조체의 갯수를 늘릴 수 있습니다.
64K는 (현재에도) 상당히 큰 블럭입니다. 여러분의 컴퓨터에 얼마나 많은 메모리가 비어있느냐와는 상관없이, 연속된 64K의 블럭을 할당하는 것은 쉽지 않습니다. (C 표준은 한 오브젝트가 32K보다 클 경우, 아무것도 보장하지 않습니다. [C9X]에서는 64K가 그 제한입니다.) 이 경우, 연속적인 공간이 아니어도 상관없는 그러한 방식으로 쓰는 것이 좋습니다. 예를 들어 동적으로 할당하는 다차원 배열19.6의 경우, 포인터를 가리키는 포인터19.7를 쓸 수 있고, 또 linked list나 포인터의 배열19.8을 쓸 수도 있습니다.
만약 PC 호환 (8086 기반의) 시스템이라면, 그리고 64K 또는 640K 메모리 제한에 걸린다면 “huge” 메모리 모델을 쓰거나 확장 메모리(expanded memory)나 연장 메모리(extended memory)를 쓰는 것도 생각해보기 바랍니다. 또 malloc 계열의 변종인 halloc()이나 farmalloc()을 쓰는 방법도 있고, 32-bit “flat” 컴파일러(예를 들면 djgpp, 질문 18.3 참고)를 쓰는 방법도 있습니다. 또는 DOS extender를 쓰거나 다른 운영체제를 쓰는 것도 생각할 수 있습니다.
unsigned int *magicloc = (unsigned int *)0x12345678;
그러면 `*magicloc'이 여러분이 원하는 위치를 나타내게 됩니다.
(MS-DOS에서는 MK_FP()
와 같은, 세그먼트와 offset을
나타내는 편리한 방법을 찾을 수 있을 것입니다.)
The function system passes its string argument to the operating system's command processor for execution in some implementation-defined way. In UNIX systems, the command processor is the shell. The value returned by system is implementation-defined but is usually the completion status of the command.해석하면 다음과 같습니다:
system()이 전달받는 인자는 운영 체제의 명령 처리기에 전달되어 시스템에 의존적인 방식으로 실행됩니다. UNIX 시스템에서는 명령 처리기는 shell입니다. system()의 리턴 값은 시스템에 의존적이나, 대개는 실행한 명령의 끝냄 코드가 됩니다.
popen() 함수를 쓸 수 없다면 system() 함수를 써서 출력이 파일로 저장되게 한 다음, 그 파일을 읽는 방법을 쓸 수도 있습니다.
Unix를 쓰고 있고, popen() 함수로 충분치 않다면, pipe(), dup(), fork(), exec() 함수를 써서 할 수 있습니다.
(한 가지 기억해 두어야 하는 것은 freopen() 함수는 원하는 대로 동작하지 않을 수도 있다는 것입니다.)
argv[0]
이 절대 경로를 나타내거나, 일부분을 나타낼 수도
있지만, 아무것도 나타내지 않을 수도 있습니다.
이 경우 명령 처리기19.9의
검색 경로 알고리즘을 흉내내서 실행 파일의 절대 경로를
얻어낼 수 있습니다.
그러나 이것이 완벽한 해결책이라고 말할 수는 없습니다.
Unix에서 process는 자신의 environment만을 (대부분 setenv(), putenv() 함수를 써서) 고칠 수 있고, 변경된 환경 변수들은 그대로 자식(child) 프로세스에게 전달되지만, 부모(parent) 프로세스에는 전달되지 않습니다. MS-DOS에서는 마스터 환경 변수19.10를 고칠 수 있지만, 이 방법은 매우 복잡합니다. 자세한 것은 MS-DOS FAQ 목록을 참고하기 바랍니다.)
ld -A
명령을 써서
링크를 할 수 있습니다. 대부분의 SunOS와 System V의 버전들은
-ldl
라이브러리를 써서 오브젝트 파일이 동적으로
로딩되게 할 수 있습니다. VMS에서는 LIB$FIND_IMAGE_SYMBOL
을 써서 이 기능을 만들 수 있고, GNU에서는 이 목적으로
“dld” 패키지를 제공합니다. 덧붙여 질문
15.13도 참고하시기 바랍니다.
물론, clock() 함수 만이 ANSI 표준입니다.
clock() 함수를 두 번 호출하고 그 리턴 값을 비교해서
걸린 시간을 비교할 수 있으며, 그 시간의 단위는 CLOCKS_PER_SEG
가
1보다 클 경우, 초 단위보다 더 세밀할 수 있습니다.
그러나 clock()은 현재 프로세스에서만 시간차를 계산할 수 있기
때문에, 멀티태스킹 시스템에서는 진짜 시간과는 약간 다를 수 있습니다.
만약 여러분이 delay 함수를 만들기를 원하고, 시간을 알려주는 함수를 쓸 수 있다면, CPU-intensive busy-waiting을 써서 만들 수 있을 것입니다. 그러나 이는 싱글 태스크 머신에서 혼자 쓸 경우에만 쓸 수 있습니다. 멀티태스킹 운영 체제에서는 여러분의 프로세스가 어떤 기간동안 잠들어 있을 수 있다(예를 들어 sleep(), select(), pause()를 alarm()과 setitimer()와 함께 써서)는 것을 명심해야 합니다.
시간을 지연시키는 간단한 방법은 다음과 같이 아무것도 하지 않는 루프를 사용하는 것입니다:
long int i; for(i = 0; i < 1000000; i++) ;
그러나, 이러한 코드를 쓰는 것은 될 수 있으면 피하기 바랍니다. 일단 몇 번 반복할 것인가를 잘 선택해야 하며, 더 빠른 프로세서에서는 이 값이 더 늘어날 것입니다. 엎친데 덮친 격으로, 좋은 컴파일러는 위와 같이 아무 것도 하지 않는 코드는 최적화 과정에서 빼 버릴 수 있습니다.
#include <signal.h> signal(SIGINT, SIG_IGN);
위는 인터럽트 시그널을 무시하는 것이고, 아래처럼 인터럽트 시그널이 발생했을 때 특정 함수인 func()가 불려지게 만들 수도 있습니다:
extern void func(int); signal(SIGINT, func);
Unix와 같은 멀티 태스킹 시스템에서는 다음과 같은 방법을 쓰는 것이 더 좋습니다:
extern void func(int); if (signal(SIGINT, SIG_IGN) != SIG_IGN) signal(SIGINT, func);
위에서 조건문과 추가적으로 부른 signal()은 foreground에서 실행된 프로그램에서 발생한 키보드 인터럽트가 background에서 동작하는 프로그램에게 잘못 전달되는 것을 막아 줍니다. (이런 형태의 코드는 signal()을 부르는 방식에 어긋나지 않습니다.)
어떤 시스템에서는 키보드 인터럽트가 터미널 입력 시스템에 따라 영향을 받기도 합니다; 질문 19.1을 참고하기 바랍니다. 어떤 시스템에서는 키보드 인터럽트를 검사하는 것은 프로그램이 입력을 읽고 있을 때에만 적용되며, 그럴 경우, 인터럽트 처리는 어떤 입력 루틴이 쓰이느냐에 따라 달라집니다. MS-DOS 시스템에서는 setcbrk()이나 ctrlbrk() 함수를 쓰기도 합니다.
<math.h>
안의 함수에 의해) 났을 때,
불려지게 할 수 있습니다. 또는 signal() 함수를 써서
(질문
19.38 참고) SIGFPE를 잡아낼 수도 있습니다.
덧붙여 질문
14.9도 참고하시기 바랍니다.
http://kipper.york.ac.uk/~vic/sock-faq/
물론 프로그램의 많은 부분을 ANSI 호환으로 작성하는 것은 매우 바람직하며 또 가능합니다. 즉 시스템에 의존적인 부분들만 몇몇의 파일에 따로 모아 두면, 나중에 그 부분만 새로 작성하면 프로그램이 돌아갈 수 있도록 만들면 좋습니다.
Seong-Kook Shin