juuuding

CH 21 문자와 문자열 관련 함수 본문

C언어/윤성우의 열혈 C

CH 21 문자와 문자열 관련 함수

jiuuu 2023. 3. 21. 23:48

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

 

1. '입력'과 '출력'은 무엇인가?

 프로그램 중심으로 프로그램 안으로 데이터가 흘러들어오는 것이 입력이고, 프로그램 밖으로 데이터가 흘러 나가는 것이 출력이다. 대표적인 입력 장치로는 키보드가 있고, 파일도 입력의 대상이 될 수 있다. 그리거 대표적인 출력 장치로는 모니터가 있고, 파일도 출력의 대상이 될 수 있다. 마우스, 프린터, 화상 카메라와 같은 장치들도 입출력 장치에 해당한다.

 

 

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

모니터와 키보드를 대상으로 데이터를 입출력 하기 위해서는 이들을 연결시켜주는 다리가 필요한데, 이 다리 역할을 하는 매개체를 '스트림(stream)'이라고 한다. 따라서 prinft 함수와 scanf 함수로 데이터를 입출력 할 수 있는 근본적인 이유는 스트림이다.

 

 

3. 스트림의 생성과 소멸

 콘솔(기보드, 모니터) 입출력과 파일 입출력 사이에는 하나의 차이점이 있다. 바로 파일과의 연결을 위한 스트림 생성은 우리가 직접 요구해야하지만, 콘솔과의 연결을 위한 스트림 생성은 요구할 필요가 없다. 다시 말해, 콘솔 입출력을 위한 '입력 스트림과' '출력 스트림'은 프로그램이 실행되면 자동으로 생성되고, 프로그램이 종료되면 자동으로 소멸되는 스트림이다. 즉, 이 둘은 기본적으로 제공되는 '표준 스트림(standard stream)'이다. 표준 스트림에는 '에러 스트림(stderr)'도 존재하는데 이는 '표준 출력 스트림(stdout)'과 차이가 없다. 하지만 이후 '입출력 리다이렉션(redirection)'이라는 기술을 익히고 나면 stderr의 출력 대상을 변경시킬 수 있어, stdout과 stderr의 차이를 구분할 수 있게 된다. 

 

 

 

 2. 문자 단위 입출력 함수

 

1. 문자 출력 함수: putchar, fputc

int putchar(int c);
int fputc(int c, FILE * stream);

int getchar(void);
int fgetc(FILE * stream);

 putchar는 인자로 전달된 문자 정보를 stdout으로 표현되는 표준 출력 스트림으로 전송하는 함수이다. 문자를 전송한다는 점에서 putchar와 fputc는 같다. 단, fputc는 문자를 전송할 스트림을 지정할 수 있다. 즉 fputc는 stdout 뿐만 아니라 파일을 대상으로도 데이터를 전송할 수 있다. 따라서 두번째 매개변수에 stdout을 전달하면 putchar와 동일한 함수가 된다.

 

 

2. 문자 입력 함수: getchar, fgetc

 getchar 함수는 표준 입력 스트림으로부터 하나의 문자를 입력 받아서 반환하는 함수이다. 즉, 키보드로부터 문자를 입력 받는 함수라 할 수 있다. 그리고 fgetc 함수도 하나의 문자를 입력 받는 함수이다. 다만 문자를 입력 받을 스트림을 지정할 수 있다. 위의 putchar와 fputc의 관계와 동일하다.

 

#define CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main(void) {

	int ch1, ch2;

	ch1 = getchar();	//문자 입력
	ch2 = fgetc(stdin);		//엔터 키 입력

	putchar(ch1);	//문자 출력
	fputc(ch2, stdout);	//엔터 키 출력
    
    /*
    p
    p
    */
}

 하나의 문자가 입력되고 출력된 것처럼 보이지만 엔터키를 포함해서 총 두개이다. 엔터 키도 아스키 코드 값이 10인 '\n'으로 표현되는 문자이다. 따라서 엔터 키도 입출력의 대상이 된다.

 위의 코드에서 "문자를 int형 변수에 저장하는 이유는 무엇일까?". getchar함수와 fgetc 함수의 반환형이 int이기 때문이다. getchar 함수와 fgetc 함수의 반환형이 int인 이유는 EOF를 먼저 배우고 알아보겠다. 

 

 

3. 문자 입출력에서의 EOF

 EOF는 EndOfFile의 약자로서 파일의 끝을 표현하기 위해서 정의한 상수이다. 그렇다면 getchar와 fgetc는 언제 EOF를 반환할까? '함수호출의 실패'와 ''windows에서 ctrl+z, linux에서 ctrl+d가 입력되는 경우'이다.

 

 

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

 getchar와 fgetc 두 함수가 반환하는 값 중 하나인 EOF는 -1로 정의된 상수이다. 어떠한 상황에서도 -1을 인식할 수 있는 int형으로 반환형을 정의해두기 위해 int형을 반환형으로 설정한 것이다. 

 

 

* printf와 scanf는 본래 서식지정을 통해 새로운 입출력의 형태를 구성하는 함수이다. 이 함수들이 사용하는 메모리 공간도  크고, 해야할 연산도 많아 상대적으로 속도가 느리다. 따라서 단순히 문자 하나를 입출력 하는 것이 목표라면 앞서 본 함수를 사용하는 것이 낫다. 

 

 

 

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

 

* scanf 함수는 공백이 포함된 형태의 문자열을 입력 받는데 제한이 있다. 아래에 소개될 문자열 입력 함수는 공백을 포함하는 문자열도 입력 받을 수 있다.

 

 

1. 문자열 출력 함수: puts, fputs

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

  puts 함수는 출력의 대상이 stdout으로 결정되어 있지만, fputs 함수는 두번째 인자를 통해 출력의 대상을 결정할 수 있다. 그리고 이 두 함수는 출력의 형태에 있어서 한가지 차이점이 있다.

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

 

 

2. 문자열 입력 함수: gets, fgets

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

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

 gets로 입력을 받을 때 미리 마련해놓은 배열을 넘어서는 길이의 문자열이 입력되면, 할당 받지 않은 메모리 공간을 침범하여 실행 중 오류가 발생한다. 그래서 가급적 fgets 함수를 호출하는 것이 좋다. fgets 함수호출이 의미하는 것은 다음과 같다. "stdin으로부터 문자열을 입력 받아서 배열 str에 저장하되, sizeof(str)의 길이만큼만 저장해라". 이 함수를 사용하여 문자열을 입력 받으면 문자열의 끝에 자동으로 널 문자가 추가된다. 

 

#define CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main(void) {
	char str[5];
	int i;

	for (i = 0; i < 3; i++) {
		fgets(str, sizeof(str), stdin);
		printf("Read %d: %s \n", i + 1, str);
	}
	return 0;
}

 이것으로 키보드를 통한 입력이 총 3회 이뤄지도록 하고 결과 값을 출력하면, 문장이 출력될 때마다 개행이 두번 이루어진다. fgets 함수는 \n을 만날 때까지 문자열을 읽어 들이는데, \n을 제외시키거나 버리지 않고 문자열의 일부로 받아들인다. 즉 printf 함수 호출 시 문자열의 일부로 저장된 \n 한번, printf 호출문에 삽입된 \n에 의해 또 한번 개행이 이뤄지는 것이다.

 

 

 

 4. 표준 입출력과 버퍼

 

1. 표준 입출력 기반의 버퍼

 printf, scanf, fputc, fgetc 모두 표준 입출력 함수이다. 이러힌 표준 입출력 함수를 통해서 데이터를 입출력 하는 경우, 데이터들은 운영체제가 제공하는 '메모리 버퍼'를 중간에 통과하게 된다. 데이터가 입력 스트림을 거쳐 입력 버퍼로 들어가는 시점은 엔터 키가 눌리는 시점이다. 

 

 

2. 버퍼링을 하는 이유는 무엇인가?

 데이터 버퍼링의 가장 큰 이유는 '데이터 전송의 효율성'과 관련이 있다. 외부 장치와의 데이터 입출력은 시간이 걸리는 작업이기 때문에, 중간에 메모리 버퍼를 둬서 데이터를 한데 묶어 이동시키는 것이 보다 효율적이다.

 

 

3. 출력버퍼를 비우는 fflush 함수

 출력버퍼가 비워진다는 것은 출력버퍼에 저장된 데이터가 버퍼를 떠나서 목적지로 이동됨을 뜻한다. 

int fflush(FLUSH * stream);

fflush(stdout)이라하면 표준 출력 버퍼를 비워라는 뜻이 된다.

 

 

4. 입력버퍼는 어떻게 비워야 하나요? **

 입력버퍼의 비워짐은 데이터의 소멸을 의미한다. fgets는 \n을 만날 때까지 읽어 들이는 함수이다. 따라서 입력퍼버에 저장된 문자들을 지우기 위해서는 다음과 같은 함수가 필요하다.

void ClearLineFromReadBuffer(void){
	while(getchar()!='\n');
}

이것은 \n이 읽혀질 때까지 입력버퍼에 저장된 문자들을 지우는 함수이다. 

 

 

 

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

 

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

# include <string.h>
size_t strlen(const char *s);

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

 우선 typedef unsigned int size_t;는 "unsigned int 선언을 size_t로 대신할 수 있다." 정도로 기억하고 있자.

"fgets 함수호출을 통해서 문자열을 입력 받고 싶은데, 같이 딸려서 들어오는 \n 문자는 문자열에서 제외시키고 싶다."라는 요구사항에 대한 해결책은 다음 코드와 같다.

#define CRT_SECURE_NO_WARNINGS
#include <stdio.h>

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

int main(void) {
	char str[100];
	printf("문자열 입력: ");
	fgets(str, sizeof(str), stdin);
	printf("길이: %d, 내용: %s \n", strlen(str), str);

	RemoveBSN(str);
	printf("길이: %d, 내용: %s \n", strlen(str), str);

	return 0;
}

결과 값을 확인해보면 RemoveBSN 함수호출을 통해 \n 문자가 소멸되었다는 것을 알 수 있다.

 

 

2. 문자열을 복사하는 함수들: strcpy, strncpy

# include <string.h>
char * strcpy(char * dest, const char *src);
char *strncpy(char * dest, const char *src, size_t n);

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

 *strncpy의 부연 설명을 하자면, "src에 저장된 문자열을 dest에 복사하되 src의 길이가 길다면 dest의 크기만큼 복사를 진행하라" 이다. 이렇듯 strncpy는 복사될 배열의 길이를 넘어서지 않는 범위 내에서 복사를 진행하고자 하는 경우에 유용하다. 하지만 strncpy 함수는 단순히 문자열을 복사하고 마지막 문자가 널 문자인지 아닌지는 상관하지 않는다. 그렇기 때문에 만약 복사된 문자열의 마지막 문자가 널 문자가 아니라면 출력 값이 이상해진다. 따라서 strncpy 함수의 세번째 인자로 배열의 실제 길이보다 하나 작은 값을 전달해서 널 문자가 삽입될 공간을 남겨두고 복사를 진행해야한다. 예를 들어 strncpy(str3, str1, sizeof(str3)-1); str3[sizeof(str3)-1]=0; 처럼 말이다. 

 

 

3. 문자열을 덧붙이는 함수들: strcat, strncat

# include <string.h>
char * strcat(char * dest, const char *src);
char *strncat(char * dest, const char *src, size_t n);

//덧붙여진 문자열의 주소 값 반환

 여기서 덧붙임이 시작되는 위치는 널 문자가 저장된 위치에서부터이다. 그래야 덧붙임 이후에도 문자열의 끝에 하나의 널 문자만 존재하는 정상적인 문자열이 된다. strncat(str1, str2, 8);이 의미하는 바는 "str2 문자열 중 최대 8개를 str1의 뒤에 덧붙여라"는 것이다.이 8개의 문자에는 널 문자가 포함되지 않고, 실제로는 널 문자를 포함하여 9개의 문자가 str1에 덧붙여진다. 이렇듯 strncpy와 다르게 strncat 함수는 문자열의 끝에 널 문자를 자동으로 삽입해준다.

 

 

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

 if(str1==str2)라는 문장으로 문자열 내용을 비교할 수 없다. 따라서 문자열 내용을 비교하고자 한다면 다음과 같은 함수를 호출해야한다. 

# include <string.h>
char * strcmp(const char * s1, const char *s2);
char *strncmp(const char * s1, const char *s2, size_t n);

//두 문자열의 내용이 같으면 0, 같지 않으면 0이 아닌 값 반환

여기서 strncmp 함수는 세 번째 인자로 전달된 수의 크기만큼만 문자를 비교한다. 즉 strncmp 함수를 호출하면 앞에서부터 중간부분까지 부분적으로 문자열을 비교할 수 있다.

 

 

5. 그 이외의 변환 함수들

- int atoi (const char * str);  -> 문자열의 내용을 int형으로 반환

- int atol (const char * str);  -> 문자열의 내용을 long형으로 반환

- int atof (const char * str);  -> 문자열의 내용을 double형으로 반환