(Part04. C언어의 깊은 이해) Chapter 21. 문자와 문자열 관련 함수
21-1 스트림과 데이터의 이동
이번 Chapter는 단순히 몇 개의 함수 정도가 아니라 데이터의 이동경로를 의미하는 '스트림', 그리고 이를 기반으로 하는 '데이터 입출력'에 대한 개념적 이해다.
데이터의 입력과 출력은 프로그램의 흐름을 뜻하는 것이다. 프로그램 안으로 데이터가 흘러 들어오는 것이 입력이고, 프로그램 밖으로 데이터가 흘러 나가는 것이 출력이다.
가장대표적인 입력장치로는 키보드가 있고, 파일도 입력의 대상이 될 수 있다. 그리고 가장 대표적인 출력장치는 모니터가 있으며, 파일 역시 출력의 대상이 될 수 있다.
스트림은 데이터의 이동수단이 된다.
프로그램상에서 모니터로 문자열을 출력할 수 있는 이유는 무엇일까?
pritf와 scanf의 함수로 데이터를 입출력 할수 있는 이유는 무엇일까?
우리가 구현한 프로그램과, 입출력 하는 모니터, 키보드 등은 기본적으로 연결되어 있는 개체가 아닌, 서로 떨어져 있는 개체이다. 따라서 프로그램상에서 이 사이의 관계를 연결시켜주는 다리가 필요하다.
이러한 다리의 역할을 하는 매개체가 '스트림(stream)' 이라 한다.
스트림은 '한 방향으로 흐르는 데이터의 흐름'을 뜻한다. 즉 스트림은 단방향 으로만 데이터의 전송이 이뤄진다는 뜻이다.
스트림은 운영체제에서 제공하는 소프트웨어적인(소프트웨어로 구현되어 있는) 가상의 다리이다.
운영체제는 외부장치와 프로그램과의 데이터 송수신의 도구가 되는 스트림을 제공하고 있다.
스트림의 생성과 소멸
콘솔(일반적으로 키보드와 모니터를 의미함) 입출력과 파일 입출력 사이에는 차이점이 하나 있다.
파일과의 연결을 위한 스트림의 생성은 우리가 직접 요구해야 하지만, 콘솔과의 연결을 위한 스트림은 자동으로 생성이 된다. (프로그램이 실행되면 '입력 스트림' 과 '출력 스트림' 이 자동으로 생성되고, 프로그램이 종료되면 자동으로 소멸된다. 이를 '표준 스트림(stdard stream)' 이라고 한다. 여기에는 '에러 스트림도 존재한다.)
● stdin 표준 입력 스트림 키보드 대상으로 입력
● stdout 표준 출력 스트림 모니터 대상으로 출력
● stderr 표준 에러 스트림 모니터 대상으로 출력
표준 에러 스트림과 표준 출력 스트림은 동일하다. 이후에 '입출력 리다이렉션(redirectiom)'(보통 리눅스나 유닉스를 접하면서 공부하게 된다.) 이라는 기술을 익히고 나면 표준 에러 스트림의 출력 대상을 변경시킬 수 있어서, 이 둘의 용도를 구분할 수 있게 될 것이다.
21-2 문자 단위 입출력 함수
문자 출력 함수: putchar, fputc
모니터로 하나의 문자를 출력할 때 일반적으로 사용하는 두 함수 이다.
#include(stdio.h>
int putchar(int c);
int fputc(int c, FILE* stream);
-> 함수호출 성공 시 쓰여진 문자정보가, 실패 시 EOF 반환
putchar 함수는 인자로 전달된 문자정보를 stdout으로 표현되는 표준 출력 스트림으로 전송하는 함수이다.
fputc 함수는 문자를 전송한다는 측면에선 동일하나 문자를 전송할 스트림을 지정할수 있다.
즉, stdout뿐만 아니라, 파일을 대상으로도 데이터를 전송할 수 있다.
이 함수의 두번째 매개변수 stream은 문자를 출력할 스트림의 지정에 사용된다. 이 인자에 stdout
을 전달하면 putchar 함수와 동일한 함수가 된다.
문자 입력 함수: getchar, fgetc
키보드로부터 하나의 문자를 입력 받을 때 일반적으로 사용하는 두 함수 이다.
#include <stdio.h>
int getchar(void);
int fgetc(FILE * stream);
-> 파일의 끝에 도달하거나 함수호출 실패 시 EOF 반환
이 둘의 관계는 위의 putchar과 fgetc와 동일하다
문자 입출력에서의 EOF
EOF는 End Of File의 약자로서, 파일의 끝을 표현하기 위해 정의해 놓은 상수이다.
fgetc와 getchar 에서는 두가지 경우에 EOF를 반환한다.
1. 함수호출의 실패
2. Windows에서 CTRL + Z키, Linux에서 CTRL+D 키가 입력되는 경우
반환형이 int이고, int형 변수에 문자를 담는 이유는?
EOF는 -1로 정의된 상수이다. 따라서 반환형이 char형 이라면 unsigned char로 처리하는 컴파일러가 있기 때문에 어떠한 상황이더라도 -1을 인식할수 있는 int형으로 정의해 둔것이다.
단순히 문자 하나를 입출력 하는 것이 목적이라면 printf 함수나 scanf 함수를 사용하는 것보다는 앞서 새로 소개한 함수들을 사용하는것이 낫다.
21-3 문자열 단위 입출력 함수
이번에 소개하는 문자열 입력 함수는 공벡을 포함하는 문자열도 입력 받을 수 있다.
문자열 출력 함수: puts, fputc
모니터로 하나의 문자열을 출력할 때 일반적으로 사용하는 두 함수는 다음과 같다.
#include <stdio.h>
int puts(const char * s);
int fputs(const char * s, FILE * stream);
-> 성공 시 음수가 아닌 값을, 실패 시 EOF 반환
puts 함수가 호출되면 문자열 출력 후 자동으로 개행이 이뤄지지만, fputs 함수가 호출되면 문자열 출력 후 자동으로 개형이 이뤄지지 않는다.
문자열 입력 함수:gets, fgets
두 개의 문자열 입력 함수는 다음과 같다.
#include <stdio.h>
char * gets(char * s);
char * fgets(char * s, int n, FILE * stream);
-> 파일의 끝에 도달하거나 함수호출 실패 시 NULL 포인터 반환
호출 방법
int main(void)
{
char str[7];
fgets(str, sizeof(str), stdin);
. . . .
}
fgets 함수는 \n을 만날 때까지 문자열을 읽어 들이고 읽어들인 \n 문자도 문자열의 일부로 받아드려서 출력하면 개행이 일어난다.
위의 문자열 입력 함수(gets, fgets)를 제외한 나머지 함수모두 반환형이 int 이나 gets, fgets만 char 형이다.
문자 입출력 함수는 매개변수도 int 형이고 문자열 입출력 함수는 매개변수가 char 형이다.
21-4 표준 입출력과 버퍼
'입출력 버퍼(buffer)' 역시 스트림의 개념과 더불어서 입출력을 이해하는데 매우 중요한 내용이다.
표준 입출력 기반의 버퍼
우리가 지금까지 공부해 온 입출력 함수들을 가리켜 '표준 입출력 함수'라 한다. ANSI C의 표준에서 정의된 함수이기 때문이다. 이것들 이용하여 데이터를 입출력 하는 경우, 해당 데이터들은 운영체제가 제공하는 '메모리 버퍼'를 중간에 통과 하게된다. (메모리버퍼란? 데이터를 임시로 모아두는(저장하는) 메모리 공간)
키보드로 입력을 할때 엔터 키를 기준으로 입력 스트림을 거쳐 입력버퍼(메모리버퍼)(임시저장소)에 들어간다.
키보드에서 입력된 데이터는 먼저 입력버퍼에 저장이 된다.
fgets, fgetc 의 입력과정(입력함수)
함수가 호출되고 입력이 된다.이 함수들이 실제로 데이터를 읽어드리는곳이 입력버퍼 이다. 입력버퍼에 데이터가 채워질때까지 커서가 깜박인다. 엔터를 치는 순간에 입력버퍼로 이동하게 된다. 그 후 문자가 들어온지 인식을 하고서 버퍼에서 데이터를 가져가면서 함수호출이 완료가 된다.
출력과정
이동시켜야될 양이 크다면 한번에 이동이 안될수도 있다. 콘솔상에선 하나의 문자열이 동시에 출력이 된다. 이 말은 프로그램 상에서 긴 문자열을 한번에 보내려고 하는데 여유치않아 버퍼에 한문자씩 꼭꼭 채워서 하나의 문자열을 완성해서 한번에 모니터로 출력하는 결과를 보아온거라고 생각해도 무리가 없다.
출력버퍼를 비운다는 것은 출력버퍼에 저장된 데이터를 목적지로 보내버린다는 뜻이다.
그 시점은 표준화되어 있지 않다.
1. 버퍼가 꽉찼을때 2. 주기적(일정시간에따라) 3. 라인버퍼(line단위로 문자가 입력되었을 때마다)
버퍼링을 하는 이유:
가장 큰 이유는 '데이터 전송의 효율성'과 관련이 있다. 외부장치와의 입출력은 시간이 걸리는 작업 이므로 누를때마다 목적지로 전송하는것보다 중간에 메모리 버퍼를 둬서 데이터를 한데 묶어서 이동시키는 것이 보다 효율적이고 빠르다.
실제로 입출력하는 과정은 내부에서 리소스 낭비가 심하다.
출력버퍼를 비우는 fflush 함수
출력버퍼가 비워진다는 것은 출력버퍼에 저장된 데이터가 버퍼를 떠나서 목적지로 이동됨을 뜻한다.
#include <stdio.h>
int fflush(FILE * stream);
-> 함수호출 성공 시 O, 실패 시 EOF 반환
fflush(stdout) // 표준 출력버퍼를 비워라!
이 함수는 파일을 대상으로도 호출이 가능하다. 파일을 대상으로 호출하면 데이터가 버퍼를 떠나서 파일에 기록이 된다.
입력버퍼를는 어떻게 비우나요?
'입력버퍼의 비워짐'은 '출력버퍼의 비워짐'과 개념적으로 차이가 있다.
'입력버퍼의 비워짐'은 데이터의 소멸을 의미한다. 입력버퍼를 비우는 함수는 따로 정의되어 있지않다.
void ClearLineFromReadBuffer(void)
{
while(getschar() != '\n');
}
입력버퍼에 저장된 문자들은 읽어 들이면 지워진다. 그 원리를 이용해서 지우는 것이다. 입력함수는 \n을 기준으로 입력을 받기 때문에 \n 설정을 했다.
21-5 입출력 이외의 문자열 관련 함수
헤더파일 string.h에 선언된 문자열 관련 함수들 중 사용 빈도수가 높은 몇몇 함수를 소개하고자 한다.
문자열의 길이를 반환하는 함수: strlen
인자로 전달된 문자열의 길이를 반환하는 함수로서 문자열과 관련해서 많이 사용되는 대표적인 함수이다.
#include <string.h>
size_t strlen(const char * s);
->전달된 문자열의 길이를 반환하되, 널 문자는 길이에 포함하지 않는다.
위 함수의 반환형 size_t는 일반적으로 다음과 같이 선언되어 있다.
typedef unsigned int size_t;
unsigned int의 선언을 size_t로 대신할 수 있다.
char str[] = "1234567";
printf("%u \n", strlen(str)); // 문자열의 길이 7이 출력된다.
참고로 strlen 함수의 반환형은 size_t이니, 이 함수의 반환 값을 insigned int형 변수에 저장하고 서식문자 %u로 출력하는 것이 정확하나 문자열이 아무리 길어도 문자열의 길이정보는 int형 변수에 저장이 가능하기 때문에, strlen 함수의 반환 값을 int형 변수에 저장하고 서식문자 %d로 출력하는 것도 가능할 뿐만 아니라 이것이 더 흔한 일이다.
문자열을 복사하는 함수들: strcpy, strncpy
이번에는 문자열의 복사에 사용되는 함수 둘을 소개하겠다.
#include <string.h>
char * strcpy(char * dest, const char * src);
char * strncpy(char * dest, const char * src, size_t n);
->복사된 문자열의 주소 값 반환
위의 strcpy 함수를 호출하는 형태는 다음과 같다.
int main(void)
{
char str1[30] = "Simple String"
char str2[30];
strcpy(str2, str1); // str1의 문자열을 str2에 복사, 3번쨰 인자로 sizeof(str2) 를 넣으면 strncpy 함수 호출형태다.)
. . . . . . .
}
"strncpy 형태로 호출을 하면 str1에 저장된 문자열을 str2에 복사하되, str1의 길이가 길다면, sizeof(str2)가 반환한 값에 해당하는 문자의 수 만큼만 복사를 진행해라!" 라는 뜻이다.
strncpy() 형태로 문자열을 복사할때 길이정보에 sizeof(str)을 넣으면 그 크기가 반환되지만 단순하게 널문자가 들어갈 공간은 생각지도 않고 연산이 되므로 sizeof(str) -1 형식으로 함수를 호출하고 그 후에 str[sizeof(str)-1] = 0; 형식으로 널값을 넣어줘야 한다.
문자열을 덧붙이는 함수들: strcat, strncat
이 두 함수는 문자열의 뒤에 다른 문자열을 복사하는 기능을 제공한다.
"야! str1에 저장된 문자열의 뒤에 str2에 저장된 문자열을 좀 복사해 주라" 라는 뜻으로 이해하면 된다.
#include <string.h>
char * strcat(char * dest, const char * src);
char * strncat(char * dest, const char * src, size_t n);
->덧붙여진 문자열의 주소 값 반환
int main(void)
{
char str1[30]="First~";
char str2[30]="Second";
strcat(str1, str2); // str1의 문자열 뒤에 str2를 복사
. . . . .
}
문자열이 덧붙혀질때 str1에 있는 널문자를 기준으로 덧붙여져서 정상적인 문자열 형태로 된다.
strncat(str1, str2, 8); //str2의 문자열중 최대 8개를 str1의 뒤에 덧붙여라! 다만 널 문자가 포함되지 않음.
따라서 널문자를 포함한 9개의 문자가 str1에 덧붙여진다.
문자열을 비교하는 함수들: strcmp, strncmp
배열의 이름을 조건문을 사용해서 비교하면 설령 문자열의 값이 같더라도 배열이름은 주소값을 나타내므로 비교를 할수 없다. 그래서 이런 함수들을 사용해야 한다.
#include <string.h>
int strcmp(const char * s1, const char * s2);
int strncmp(const char * s1, const char * s2), size_t n);
-> 두 문자열의 내용이 같으면 0, 같지 않으면 0이 아닌 값 반환
둘 모두 인자로 전달된 두 문자열의 내용을 비교하나 strncmp는 세 번째 인자로 전달된 수의 크기만큼 문자를 비교한다.
1. s1이 더 크면 0보다 큰 값 반환 s1이 사전편찬 순서상 뒤에 위치하면 0보다 큰 값 반환
2. s2가 더 크면 0보다 작은 값 반환 s2가 사전편찬 순서상 뒤에 위치하면 0보다 작은 값 반환
3. 내용이 모두 같으면 0 반환 내용이 모두 같으면 0 반환
크고 작음의 기준은 아스키 코드 값을 기준으로 결정한다. 첫 번째 문자부터 비교가 시작되고 널 문자도 비교대상에 포함된다. 같으면 0 같지않으면 0이 아닌 값 이정도로만 기억하고 있어도 되고 실제로도 이 정도의 수준에서 사용된다.
그 이외의 변환 함수들
이 함수들은 헤더파일 <stdlib.h>에 선언된 함수들이다.
int atoi(const char * str); 문자열의 내용을 int형으로 변환
log atol(const char * str); 문자열의 내용을 long형으로 변환
double atof(const char * str); 문자열의 내용을 double형으로 변환
문자열로 표현된 정수나 실수의 값을 해당 정수나 실수의 데이터로 변환해야 하는 경우가 간혹 있다.
1의 아스키코드 값은 1 이고 '1'의 아스키 코드값은 49 이다.
이 둘의 연산을 하려면 숫자, 문자의 구분이 필요하여 문자 1에는 '1'로 적어줘야하고 숫자 1은 그대로 1 입력하면 된다.