본문 바로가기
휴지통/C 프로그래밍

문자와 문자열 관련 함수

by 신재권 2021. 1. 26.

1. 스트림과 데이터의 이동

-무엇이 입력이고 무엇이 출력인가?

데이터의 입력과 출력은 프로그램의 흐름을 뜻하는 것이다. 그렇다면 무엇이 '입력'이고 무엇이 '출력'인가?

프로그램을 중심으로 프로그램 안으로 데이터가 흘러 들어오는 것이 입력이고, 프로그램 밖으로 데이터가 흘러 나가는 것이 출력이다.

가장 대표적인 입력장치로는 키보드가 있으며, 파일도 입력의 대상이 될 수 있다. 그리고 대표적인 출력장치로는 모니터가 있으며, 파일 역시 출력의 대상이 될 수 있다.

-데이터의 이동수단이 되는 스트림

서로 떨어져 있는 개체를 연결해주는 다리의 역할을 하는 매개체를 '스트림(Stream)'이라 한다.

실행중인 프로그램과 모니터를 연결해주는 '출력스트림'이라는 다리가 놓여있고, 실행중인 프로글매과 키보드를 연결해주는 '입력스트림'이라는 다리가 놓여있음을 알 수 있다.

printf함수와 scanf함수를 통해서 데이터를 입출력 할 수 있는 근본적인 이유는 바로 이 다리에 있다.

다리의 역할을 하는 스트림의 정체는 운영체제에서 제공하는 소프트웨어적인 가상의 다리이다. 다시 말해서, 운영체제는 외부장치와 프로그램과의 데이터 송수신의 도구가 되는 스트림을 제공하고 있다. 다시 말해서 운영체제는 외부장치와 프로그램과의 데이터 송수신의 도구가 되는 스트림을 제공하고 있다.

-스트림의 생성과 소멸

콘솔(일반적으로 키보드와 모니터를 의미) 입출력과 파일 입출력 사이에는 차이점이 하나 있다. 그것은 파일과의 연결을 위한 스트림의 생성은 우리가 직접 요구해야하지만, 콘솔과의 연결을 위한 스트림의 생성은 요구할 필요가 없다는 것이다. 다시말해서 콘솔입출력을 위한 스트림은 자동으로 생성이 된다.

콘솔 입출력을 위한 '입력스트림'과 '출력스트림'은 프로그램이 실행되면 자동으로 생성되고 ,프로그램이 종료되면 자동으로 소멸되는 스트림이다.

이 둘은 기본적으로 제공되는 표준스트림(Standard Stream)이다. 표준스트림에는 에러 스트림도 존재하며 이들 각각에는 다음과 같이 stdin, stdout, stderr라는 이름이 붙어있다.

stdin - 표준 입력 스트림 - 키보드 대상으로 입력

stdout- 표준 출력 스트림 - 모니터 대상으로 출력

stderr - 표준 에러 스트림 - 모니터 대상으로 출력

Stream(스트림)

스트림은 한방향으로 흐르는 데이터의 흐름을 뜻한다. 즉 스트림이란 단어에는 단 방향으로만 데이터의 전송이 이뤄진다는 뜻이 담겨있다. 실제로 입출력 스트림도 입력 스트림과 출력 스트림이 구분되어서 한 방향으로만 데이터의 흐름을 유지하고 있다.

2. 문자 단위 입출력 함수

문자 출력 함수 : putchar, fputc

모니터로 하나의 문자를 출력할때 일반적으로 사용하는 함수

int puchar(int c);

int fputc(int c, FILE *stream);

-> 함수 호출 성공시 쓰여진 문자정보가, 실패시 EOF반환

putchar 함수는 인자로 전달된 문자 정보를 stdout으로 표현되는 표준출력 스트림으로 전송하는 함수 이다.

따라서 인자로 전달된 문자를 모니터로 출력하는 함수라 할 수 있다. 그리고 문자를 전송한다는 측면에서 fputc함수도 putchar함수와 동일하다.

단 fputc함수는 문자를 전송할 스트림을 지정할 수 있다. 즉 fputc함수를 이용하면 stdout뿐만 아니라, 파일을 대상으로도 데이터를 전송할 수 있다.

fputc함수에 대해서 부연설명 하자면 fputc함수의 두번째 매개변수 stream은 문자를 출력할 스트림의 지정에 사용된다. 이 인자에 표준 출력 스트림을 의미하는 stdout을 전달하면 putchar함수와 동일한 함수가 된다.

문자 입력 함수: getchar, fgetc

키보드로부터 하나의 문자를 입력 받을 때, 일반적으로 사용하는 두 함수

int getchar(void);

int fgetc(FILE *stream);

->파일의 끝에 도달하거나 함수호출 실패시 EOF반환

getchar함수는 stdin으로 표현되는 표준 입력 스트림으로부터 하나의 문자를 입력 받아서 반환하는 함수이다. 따라서 키보드르부터 하나의 문자를 입력 받는 함수라 할 수 있따. 그리고 fgetc 함수도 하나의 문자를 입력 받는 함수이다. 다만 getchar ㅎ마수와 달리 문자를 입력 받을 스트림을 지저할 수 있다.

위의 두 함수의 관계는 앞서 설명한 putchar, fputc함수의 관계와 동일하다.

모든 문자에는 엔터키 입력도 들어간다 아스키 코드 값 10인 \n으로 표현되는 문자이므로 입출력의 대상이 되는 것은 당연하다.

문자를 int형 변수에 저장하는 이유는 EOF로 인해 그렇다.

-문자 입출력에서의 EOF

EOF는 End Of File의 약자로서, 파일의 끝을 표현하기 위해서 정의해 놓은 상수이다. 따라서 파일을 대상으로 fgetc함수가 호출되면, 그리고 그 결과로 EOF가 반환되면, 이는 '파일의 끝에 도달해서 더 이상 읽을 내용이 없다'는 뜻이 된다.

그렇다면 키보드를 대상으로 하는 fgetc함수와 getchar함수는 언제 EOF를 반환할까?

-함수 호출의 실패.

-Windows에서 CTRL +Z키, Linux에서 CTRL + D키가 입력되는 경우

키보드에 입력에는 '파일의 끝'이라는 것이 존재할수 없다. 따라서 EOF의 반환시키를 따로 키의 입력으로 별도로 약속한 것이다.

getchar함수가 호출된다고 해서 하나의 문자만 입력하려고 노력하지 않아도 된다. 문자가 아닌 공백을 포함하는 문장을 입력해도 된다. 문장이 입력되면 문장을 구성하는 문자의 수 만큼 getchar함수가 호출되면서 모든 문자를 읽어 들이니 말이다.

-반환형이 int이고, int형 변수에 문자를 담는 이유?

int getchar(void);

int fgetc(FILE *stream);

반환되는 것은 1바이트 크기의 문자인데 반환형이 int이다. 이유가 뭔가?

그런데 위의 두함수가 반환하는 값중 하나인 EOF는 -1로 정의 된 상수이다. 따라서 반환형이 char형이라면, 그리고 char를 unsigned char로 처리하는 컴파일러에 의해서 컴파일이 되었다면, EOF는 반환의 과정에서 엉뚱한 양의 정수로 형 변환이 되어버리고 만다. 그래서 어떠한 상황에서도 -1을 인식할 수 있는 int형으로 반환형을 정의해 놓은 것이다. 물론 반환되는 값을 그대로 유지하기 위해서 우리도 int형 변수에 반환 값을 저장해야 한다.

문자 단위 입출력 함수의 존재 이유

단순히 하나의 문자를 입출력하는 것이 목적이면 속도가 빠른 이 함수를 이용하는 것이다.

3. 문자열 단위 입출력 함수

printf함수와 scanf함수를 이용해서도 문자열의 입출력이 가능하다. 그러나 이번에 소개하는 문자열 입출력 함수는 그 성격이 제법 다르다. scanf함수는 공백이 포함된 형태의 문자열을 입력받는데 제한이 있지만, 이번에 소개하는 문자열 입력함수는 공백을 포함하는 문자열도 입력받을 수 있다.

문자열 출력 함수 : puts, fputs

모니터의 하나의 문자열을 출력할 때 일반적으로 사용하는 함수

int puts(const char *s);

int fputs(const char *s, FILE *stream);

->성공 시 음수가 아닌 값을, 실패시 EOF반환

puts함수는 출력의 대상이 stdout으로 결정되어 있찌만 fputs 함수는 두 번째 인자를 통해서 출력의 대상을 결정할 수 있다. 그리고 둘 다 첫번째 인자로 전달된는 주소 값의 문자열을 출력하지만, 출력의 형태에 있어 한가지 차이점이 있다.

puts함수가 호출되면 문자열 출력 후 자동으로 개행이 이뤄지지만, fputs함수가 호출되면 문자열 출력후 자동으로 개행이 이뤄지지 않는다.

문자열 입력 함수 : gets, fgets

char *gets(char *s) --삭제

char * fgets(char *s, int n, FILE*stream);

->파일의 끝에 도달하거나 함수 호출 실패시 NULL포인터 반환

gets 함수는 현재 비쥬얼 스튜디오 2019년 버전에서는 보안 문제로 삭제가 되었다.

char str[7];

gets(str); //입력 받은 문자열을 str에 저장

하지만 미리 마련해 놓은 배열을 넘어서는 길이의 문자열이 입력되면, 할당 받지 않은 메모리 공간을 침범하여 실행 중 오류가 발생하는 단점이 있어, 가급적이면 다음의 형태로 fgets함수를 호출하는 것이 좋다.

char str[7];

fgets(str, sizeof(str), stdin); stdin으로부터 문자열 입력받아서 std에 저장

stdin으로부터 문자열을 입력 받아서, 배열 str에 저장하되 sizoof(str)의 길이만큼 저장해라.

하지만 -1 길이의 문자열이 저장되는데 이는 널 문자의 저장을 위한 것이다. 아무리 공간이 부족해도 널 문자가 삽입되지 않으면 문자열이라 할 수 없기 때문에 문자열을 입력받으면 문자열의 끝에 자동으로 널 문자가 추가된다.

fgets함수는 \n을 만날 떄까지 문자열을 읽어들이는데 , \n을 제외 시키거나 버리지 않고 문자열의 일부로 받아들인다. 쉽게 말해서 입력한 엔터키의 정보까지도 문자열의 일부로 저장되는 것이다.

4. 표준 입출력과 버퍼

-표준 입출력 기반의 버퍼

우리가 지금까지 공부해 온 입출력 함수들을 가리켜 '표준 입출력 함수'라 한다. ANSI C의 표준에서 정의된 함수이기 때문이다. 이러한 표준 입출력 함수를 통해서 데이터를 입출력 하는 경우, 해당 데이터들은 운영체제가 제공하는 '메모리 버퍼'를 중간에 통과하게 된다.

여기서 말하는 메모리 버퍼는 데이터를 임시로 모아두는 (저장하는)메모리 공간이다.

키보드를 통해 입력되는 데이터는, 일단 입력버퍼에 저장된 다음에 프로그램에서 읽혀지는 것을 알수 있다. 즉 fgets함수가 읽어 들이는 문자열은 입력버퍼에 저장된 문자열이다. 그럼 키보드로부터 입력된 데이터가 입력 스트림을 거쳐서 입력버퍼로 들어가는 시점은 언제일까?

이는 엔터키가 눌리는 시점이다. 그래서 키보드로 아무리 문자열을 입력해도 엔터 키가 눌리기 전에는 fgets 함수가 문자열을 읽어 들이지 못하는 것이다. 엔터 키가 눌리기 전에는 입력 버퍼가 비워져 있기 때문이다.

-버퍼링(Buffering)을 하는 이유

데이터를 목적지로 바로 전송하지 않고 중간에 출력버퍼와 입력버퍼를 둬서 전송하고자 하는 데이터를 임시 저장하는 이유는 무엇일까? 이러한 데이터 버퍼링의 가장 큰 이유는 '데이터 전송의 효율성'과 관련이 있다. 키보드나 모니터와 같은 외부장치와의 데이터 입출력은 생각보다 시간이 거릴는 작업이다. 따라서 버퍼링 없이 키보드가 눌릴 때 마다 눌린 문자의 정보를 바로 이동시키는 것보다 중간에 메모리 버퍼를 둬서 데이터를 한데 묶어서 이동시키는 것이 보다 효율적이고 빠르다.

-출력 버퍼를 비우는 fflush함수

출력 버퍼가 비워진다는 것은 출력 버퍼에 저장된 데이터가 버퍼를 떠나서 목적지로 이동됨을 뜻한다. 그런데 출력버퍼가 비워지는 시점은 시스템에 따라 그리고 버퍼에 성격에 따라 달라진다.예를 들어서 버퍼가 꽉찼을때 비워지는 버퍼도 있고, 하나의 문장이 완전히 입력되었을 때마다 비워지는 버퍼도 있다. 이렇듯 버퍼가 지워지는 시점은 동일하지 않기 때문에 다음 함수를 알아둘 필요가 있다.

int fflush(FILE *stream);

->함수 호출 성공시 0, 실패시 EOF반환

위 함수는 인자로 전달된 스트림의 버퍼를 비우는 기능을 제공한다. 따라서 다음과 같이 함수를 호출한다.

fflush(stdout); //표준 출력 버퍼를 비워라

어떠한 시스템의 어떠한 표준 출력버퍼라 할지라도 버퍼에 저장된 내용이 비워지면서 데이터가 목적지로 이동한다.

참고로 위의 함수는 파일을 대상으로도 호출이 가능하다. 인자로 파일의 스트림 정보가 전달되면, 해당 버퍼에 저장되어 있던 데이터들이 버퍼를 떠나서 파일에 기록이 된다. 그런데 콘솔입출력을 하는 상황이라면 stdout대상으로 위의 함수를 호출할 일은 사실상 많지 않다.

-입력버퍼는 어떻게 비워야 하는가?

'입력 버퍼의 비워짐'은 '출력버퍼의 비워짐'과 개념적으로 차이가 있다. '출력버퍼의 비워짐'이 저장된 데이터가 목적지로 전송됨을 의미한다면 '입력버퍼의 비워짐'은 데이터의 소멸을 의미하기 때문이다. 가끔은 입력버퍼에 남아있는 불필요한 데이터의 소멸을 위해서 입력버퍼를 비워야 하는 경우가 종종 있다.

fflush 함수는 출력버퍼를 대상으로 호출하는 함수이다.

void ClearLineFromReadBuffer(void){

while(getchar() != '\n');

}

위 함수를 정의해 입력버퍼에 남아있는 널 문자를 지울 수 있다.

입력 버퍼에 저장된 문자들은 읽어 들이면 지워진다. 그래서 \n을 만날 때까지 문자를 읽어들이는 함수를 정의하였다. 물론 읽어 들인 문자를 저장하거나 하지는 않는다. 버리는것이 목적이니 말이다.

즉 입력버퍼를 통째로 비우는 함수가 아닌 , \n이 읽혀질 때까지 입력버퍼에 저장된 문자등를 지우는 함수이다.

5. 입출력 이외의 문자열 관련 함수

string.h에 선언된 문자열 관련 함수들 중 사용 빈도수가 높은 몇몇 함수를 소개한다.

문자열의 길이를 반환하는 함수 : strlen

size_t strlen (const char *s);

->전달된 문자열의 길이를 반환하되, 널 문자는 길이에 포함하지 않는다.

size_t는 일반적으로 다음과 같이 선언되어 있다.

typedef unsigned int size_t;

size_t len;

unsigned int len;

두 선언은 동일하다

char str[]= "1234567"; printf("%u \n", strlen(str)); //문자열 길이 7이 출력

strlen 함수의 반환형은 size_t이니, 이 함수의 반환 값을 unsigned int 형 변수에 저장하고 서식문저 %u로 출력하는 것이 정확하다.

그러나 문자열이 아무리 길어도 문자열의 길이정보는 int형 변수에 저장이 가능하기 때문에, strlen 함수의 반환 값을 int형 변수에 저장하고 서식문자 %d로 출력하는 것도 가능할 뿐만 아니라 이것이 더 흔한 일이다. 그래서 이후부터는 strlen함수의 반환값을 int형 변수에 저장하고 %d로 출력한다.

\n문자를 문자열에서 제외시키는 방법

void Remove(char str[]){ int len= strlen(str); str[len-1]= 0; }

-문자열을 복사하는 함수: strcpy, strncpy

char *strcpy(char * dest, const char *src);

char *strcpy(char *dest, const char *src, size_t n);

-> 복사된 문자열의 주소 값 반환

int main(){ char str1[30] = "Simple String"; char str2[30] ; strcpy(str2, str1); //str1의 문자열을 str2에 복사 }

위의 코드가 실행되면 str2,에는 str1이 저장하고 있는 문자열이 복사된다. 물론 문자열이 복사될 배열의 길이보다 문자열의 길이보다 작지 않도록 주의해야 한다.

int main(){ char str1[30] ="Simple String"; char str2[30]; strncpy(str2, str1, sizeof(str2)); }

위 코드의 strncpy의 함수 호출문이 의미하는 바는 다음과 같다.

str1에 저장된 문자열을 str2에 복사하되, str1의 길이가 매우 길다면 sizeof(str2)가 반환한 값에 해당하는 문자의 수 만큼 복사를 진행해라!

strncpy 함수는 복사될 배열의 길이를 넘어서지 않는 범위 내에서 복사를 진행하고자 하는 경우에 유용하다.

복사하는 최대 문자의 수로 sizeof(_) 반환값이 전달되어도 할당된 배열을 넘어서서 복사가 될수 있다.

만약 5가 반환되었으면 5개의 문자 안에 널 문자가 포함되지 않는다는 문제가 있따.

strncpy는 문자열을 단순하게 복사한다. 5개의 문자를 복사하려고 하면 앞에서부터 딱 5개의 문자만 복사한다. 마지막 문자가 널 문자인지 아닌지는 상관 하지 않는다.

하지만 문자열은 반드시 널문자가 존재해야되는데 위 상태로 출력을 하면 엉뚱한 영역의 값이 출력되는 것이다.

그러므로 sizeof(_)-1 값을 해줘서 널문자가 들어갈 공간을 만들어 줘야된다.

-문자열을 덧붙이는 함수: strcat, strncat

char *strcat(char *dest, const char*src);

char *strncat(char *dest, const char *src, size_t n);

int main(){ char str1[30]= "First~"; char str2[30] = "Second"; strcat(str1, str2); // str1의 문자열 뒤에 str2를 복사 }

위 형태로 strcat함수가 호출되면 str2의 문자열이 str1의 문자열 뒤에 덧붙여지는데, 덧 붙여지는 형태는 다음과 같다.

덧 붙임이 시작되는 위치는 널문자 다음이 아닌, 널 문자가 저장된 위치부터이다. 이럿듯 널 문자가 저장된 위치에서부터 복사가 진행되어야 덧붙임 이후에도 문자열의 끝에 하나의 널문자만 존재하는 정상적인 문자열이 존재하게 된다.

strncat(str1, str2, 8);

이 문장이 의미하는 바는 다음과 같다.

str2의 문자열중 최대 8개를 str1의 뒤에 덧붙여라!

즉 str2의 길이가 8을 넘어선다면 8개의 문자까지만 str1에 덧붙이라는 의미인데 이 8개의 문자에는 널 문자가 포함되지 않는 사실에 주목해야한다. 따라서 널 문자를 포함하여 실제로는 총 9개의 문자가 str1에 덧붙여진다. 이렇듯 strncpy 함수와 달리 strncat함수는 문자열의 끝에 널문자를 자동으로 삽입해준다.

문자열을 비교하는 함수 : strcmp, strncmp

int strcmp(const char *s1, const char *s2);

int strncmp(const char *s1, const char *s2, size_t n);

->두 문자열의 내용이 같으면 0, 같지 않으면 0이 아닌 값을 반환한다.

위의 두함수 모두 인자로 전달된 두 문자열의 내용을 비교하여 다음의 결과를 반환한다. 단 strncmp 함수는 세번째 인자로 전달된 수의 크기만큼 문자를 비교한다. 즉 strncmp함수를 호출하면 앞에서 부터 시작에서 중간부분까지 부분적으로만 문자열을 비교할 수있다.

s1이 더 크면 0보다 큰값 반환

s2이 더크면 0보다 작은 값 반환

s1과 s2의 내용이 모두 같으면 0반환

여기서 말하는 문자열의 크고 작음은 아스키 코드 값을 기준으로 결정된다.

ABCD

ABCC

첫번째 문자부터 비교가 시작된다. 그런데 세번째 문자까지는 동일하다, 따라서 네번째 문자를 비교하게된다. 그런데 D의 아스키코드 값이 C의 아스키코드 값보다 크다 . 따라서 ABCD가 더 큰문자열이 되어 양수가 출력된다.

참고로 양수가 반환되어야 한다는 사실만 표준으로 정의되어 있을 뿐, 그 값이 구체적으로 얼마인지는 정의되어 있지 않다.

ABCD

ABCDE

첫번째 문자부터 비교가 시작되어 네번째 문자까지 동일하다. 그래서 마지막으로 다섯번째 문자를 비교하는데 첫번째 인자의 5번째 문자는 NULL값 (널 문자도 비교의 대상에 포함된다)이고 , 두번째 인자의 다섯번째 문자는 E이므로 , E의 아스키 코드값이 널 문자보다 크므로 음수가 반환된다.

일반적으로 strcmp함수를 호출할 때에는 다음 두 가지 사실에만 근거하여 코드를 작성한다.

0이 반환되면 동일한 문자열, 0이 아닌 값이 반환되면 동일하지 않은 문자열

언제 음수가 반환되고, 언제 양수가 반환되는지는 그리 중요하게 인식되지 않는다.

위의 함수는 사전편찬 순서를 비교하는 것과 같다

s1이 사전편찬 순서상 뒤에 위치하면 0보다 큰값 반환

s2가 사전편찬 순서상 뒤에 위치하면 0보다 작은값 반환

s1과 s2의 내용이 모두 같으면 0반환

strcmp 함수의 문자열 비교 방식에 대해서 설명했는데, 단순하게 두 문자열이 같으면 0, 같지않으면 0이 아닌 값이 반환된다고 기억하고 있어도 충분하다.

-그 외의 반환 함수들

<stdlib.h>에 선언된 함수들이다

int atoi(const char *str); //문자열의 내용을 int로 변환

long atol(const char *str); //문자열의 내용을 long으로 변환

double atof(const char *str);//문자열의 내용을 double로 변환

문자열로 표현된 정수나 실수의 값을 해당 정수나 실수의 데이터로 변환해야 하는 경우가 있기때문에, 위 함수로 쉽게 할수 변환할 수 있다.