c언어 개념 정리와 문제 풀이를 통해 알고리즘 수업을 준비하는 과정입니다. 그동안 수강했던 c언어 수업 자료와 윤성우의 열혈 C 프로그래밍의 내용을 포함해 저에게 기록할 가치가 있는 내용이 담겨 있습니다.
스트림과 데이터의 입출력
데이터의 입력과 출력은 프로그램의 흐름을 뜻한다.
프로그램을 중심으로 프로그램 안으로 데이터가 흘러 들어오는 것이 입력이고,
프로그램 밖으로 데이터가 흘러 나가는 것이 출력이다.
대표적인 입력장치로는 키보드가 있으며, 파일도 입력의 대상이 될 수 있다.
대표적은 출력장치로는 모니터가 있으며, 파일도 출력의 대상이 될 수 있다.
우리가 구현하는 프로그램과 모니터, 프로그램과 키보드는 연결되어 있지 않고 서로 떨어져 있는 개체이다.
따라서 프로그램상에서 모니터와 키보드를 대상으로 데이터를 입출력 하기 위해서는 이들을 연결시켜 주는 다리가 필요하고,
이러한 다리의 역할을 하는 매개체를 '스트림(stream)'이라 한다.
printf, scanf 함수를 통해서 데이터를 입출력 할 수 있는 근본적인 이유는 바로 이 다리에 있다.
스트림은 한 방향으로 흐르는 데이터의 흐름을 뜻한다. 단 방향으로만 데이터의 전송이 이뤄진다는 뜻이 담겨있다.
실제로 입출력 스트림도 입력 스트림과 출력 스트림이 구분되어서 한 방향으로만 데이터의 흐름을 유지하고 있다.
콘솔은 일반적으로 키보드와 모니터를 의미한다.
콘솔 입출력과 파일 입출력 사이에는 차이점이 하나 있다.
콘솔과의 연결을 위한 스트림의 생성은 요구할 필요가 없지만,
파일과의 연결을 위한 스트림의 생성은 직접 요구해야 한다는 점이다.
콘솔 입출력을 위한 스트림은 자동 생성된다. 콘솔 입출력을 위한 '입력 스트림'과 '출력 스트림'은 프로그램이 실행되면 자동으로 생성되고, 프로그램이 종료되면 자동으로 소멸되는 스트림이다. 즉, 이 둘은 기본적으로 제공되는 '표준 스트림'이다. 표준 스트림에는 3가지가 존재한다.
stdin 표준 입력 스트림 키보드 대상으로 입력
stdout 표준 출력 스트림 모니터 대상으로 출력
stderr 표준 에러 스트림 모니터 대상으로 출력
'표준 에러 스트림'은 모니터로 출력이 이뤄진다는 점에서 '표준 출력 스트림'과 동일하다.
보통 리눅스나 유닉스를 접하면서 '입출력 리다이렉션'이라는 기술을 익히고 나면, '표준 에러 스트림'의 출력 대상을 변경시킬 수 있어서 stdout과 stderr의 용도를 구분할 수 있게 된다.
문자 단위 입출력 함수
기본적인 입출력의 원리와 스트림에 대해 이해를 해야 다음 함수들이 의미가 있다.
문자 출력 함수: putchar, fputc
#include <stdio.h>
int putchar(int c);
int fputc(int c, FILE * stream);
putchar
인자로 전달된 문자정보를 stdout으로 표현되는 표준 출력 스트림으로 전송하는 함수이다.
인자로 전달된 문자를 모니터로 출력하는 함수라 할 수 있다.
fputc
문자를 전송한다는 측면에서는 putchar와 동일하다.
fputc 함수는 문자를 전송할 스트림을 지정할 수 있다.
stdout뿐만 아니라, 파일을 대상으로도 데이터를 전송할 수 있다.
두 번째 매개변수 stream은 문자를 출력할 스트림의 지정에 사용된다.
이 인자에 stdout을 전달하면, putchar 함수와 동일한 함수가 된다.
이 인자에 파일의 스트림 정보를 전달하면, 해당 파일로 문자가 전달된다. 해당 파일에 문자가 저장된다는 것이다.
위 두 함수 모두 오류가 발생해 정상적인 결과를 보장하지 못할 경우 EOF를 반환한다.
문자 입력 함수: getchar, fgetc
#include <stdio.h>
int getchar(void);
int fgetc(FILE * stream);
getchar
stdin으로 표현되는 표준 입력 스트림으로부터 하나의 문자를 입력 받아서 반환하는 함수이다.
키보드로부터 하나의 문자를 입력 받는 함수라 할 수 있다.
fgetc
하나의 문자를 입력 받는 함수인 것은 getchar와 동일하다.
fgetc는 문자를 입력 받을 스트림을 지정할 수 있다.
엔터 키
'엔터 키'는 아스키 코드 값이 10인 '\n'으로 표현되는 문자이다. 따라서 입출력의 대상이 되는 것은 당연하다.
문자 입력 함수의 반환형이 int?
문자 입력 시 getchar(), fgetc(stdin)의 반환형으로 int형 변수를 선언한다.
함수의 원형을 보면, getchar와 fgetc 함수의 반환형이 int이기 때문이다. 반환형이 int인 것은 EOF와 관련있다.
반환되는 것은 1바이트 크기의 문자인데, 반환형이 int이다. 이유가 무엇일까?
char를 unsigned char로 처리하는 컴파일러도 존재한다. 이것과 관련있다.
unsigned char, unsigned int란?
앞에 unsigned가 붙은 이유는 sign이 없어서 그렇다. 여기서 sign은 부호이다.
unsigned가 붙으면 양수와 0만 표현 가능하다.
getchar, fgetc가 반환하는 값 중 하나인 EOF는 -1로 정의된 상수이다.
반환형이 char형이라면, 그리고 char를 unsigned char로 처리하는 컴파일러에 의해 컴파일이 되었다면, EOF는 반환 과정에서 엉뚱한 양의 정수로 형 변환이 된다. 그래서 어떤 상황에서도 -1을 인식할 수 있는 int형으로 반환형을 정의한 것이다.
문자 입출력에서의 EOF
EOF는 end of file의 약자이다. 파일의 끝을 표현하기 위해서 정의해 놓은 상수이다.
파일을 대상으로 fgetc 함수가 호출되면, 그리고 그 결과로 EOF가 반환되면,
이는 '파일의 끝에 도달해서 더이상 읽을 내용이 없다'는 뜻이다.
키보드를 대상으로 fgetc 함수와 getchar 함수가 호출되면 언제 EOF를 반환할까?
함수호출의 실패, 윈도우에서 ctrl + z, 리눅스에서 ctrl + d 가 입력되는 경우 중 하나일 때 EOF를 반환한다.
키보드의 입력에 '파일의 끝'이 존재할 수 있나?
따라서 EOF의 반환시기를 ctrl + z, ctrl + d로 약속한 것이다.
문자 단위 입출력 함수의 존재 이유
printf, scanf 함수는 본래 서식지정을 통해서 새 입출력의 형태를 구성하는 함수이다.
이렇게 화려한 기능을 제공하는 함수는 대가가 요구된다. 사용하는 메모리 공간도 크고, 해야 할 연산의 양도 많아서 상대적으로 속도가 느리다. 별도의 서식지정도 해야 하므로 문장 구성도 번거롭다.
단순히 문자 하나를 입출력 하는 것이 목적이라면 printf, scanf 보다는 위에서 다룬 함수들을 사용하는 것이 낫다.
문자열 단위 입출력 함수
scanf 함수는 공백이 포함된 형태의 문자열을 입력 받는데 제한이 있었는데, 다음 문자열 입력 함수는 공백을 포함하는 문자열도 입력 받을 수 있다.
문자열 출력 함수: puts, fputs
#include <stdio.h>
int puts(const char * s);
int fputs(const char * s, FILE * stream);
puts
출력의 대상이 stdout으로 결정되어 있다.
첫 번째 인자로 전달되는 주소 값의 문자열을 출력한다. (줄바꿈 포함 o)
문자열 출력 후 자동으로 개행이 이뤄진다.
fputs
두 번째 인자를 통해서 출력의 대상을 결정할 수 있다.
첫 번째 인자로 전달되는 주소 값의 문자열을 출력한다. (줄바꿈 포함 x)
문자열 출력 후 자동으로 개행이 이뤄지지 않는다.
문자열 입력 함수: gets, fgets
#include <stdio.h>
char * gets(const char * s);
char * fgets(const char * s, int n, FILE * stream);
gets
int main(void)
{
char str[7]; // 7바이트의 메모리 공간 할당
gets(str); // 입력 받은 문자열을 배열 str에 저장
}
확실히 문장구성은 간단하다. 하지만 미리 마련해 놓은 배열을 넘는 길이의 문자열이 입력되면, 할당 받지 않은 메모리 공간을 침범해 실행 중 오류가 발생한다는 단점이 있다. 그래서 fgets 함수를 호출하는 것이 좋다.
fgets
int main(void)
{
char str[7]; // 7바이트의 메모리 공간 할당
fgets(str, sizeof(str), stdin); // stdin으로부터 문자열 입력 받아서 str에 저장
}
fgets 함수 호출의 의미는 다음과 같다.
stdin으로부터 문자열을 입력 받아서 배열 str에 저장하되, sizeof(str)의 길이만큼만 저장한다.
문자열을 입력 받으면 문자열의 끝에 자동으로 널 문자가 추가된다.
3번 반복하는 반복문 안에서 프로그램 사용자는 딱 한 번만 입력해도 fgets 함수는 3회 모두 호출 될 수 있다.
fgets 함수는 \n을 만날 때까지 문자열을 읽어 들이는데, \n을 버리지 않고 문자열의 일부로 받아들인다.
입력한 엔터 키까지 문자열의 일부로 저장된다.
gets, fgets에서 널 문자('\0')와 엔터('\n')
문자열은 마지막에 널 문자(NULL)을 넣어준다. 널 문자는 아스키 코드 값 0이다. '\0'으로 나타낸다.
gets는 엔터('\n')가 나올 때까지 읽는다.
sizeof(str) 이상이면 error다. 문자열이 아니게 된다.
fgets는 '\0'을 포함해서 size의 크기만큼 혹은 엔터('\n')가 나올 때까지 읽어 str에 넣는다.
후자의 경우는 '\0'과 '\n' 모두 포함하게 된다.
sizeof(str) 이상이면 (size-1)까지 읽고 그 다음에 '\0'을 넣는다.
scanf는 white character(공백, 탭, 엔터)가 나올 때까지 읽어서 str에 넣는다.
scanf는 공백이 나올 때까지 읽지만 gets, fgets는 엔터가 나올 때까지 읽기 때문에 한 줄을 하나의 문자열로 읽을 수 있다.
표준 입출력과 버퍼
입출력을 이해하는데 매우 중요한 내용 중 앞서 익힌 '스트림'의 개념과 '입출력 버퍼'이다.
표준 입출력 기반의 버퍼
지금까지 공부해 온 입출력 함수들을 '표준 입출력 함수'라 한다. printf, scanf, fputc, fgetc 모두 표준 입출력 함수이다.
표준 입출력 함수를 통해 데이터를 입출력 하는 경우, 해당 데이터들은 운영체제가 제공하는 '메모리 버퍼'를 중간에 통과한다.
'메모리 버퍼'는 데이터를 임시로 저장하는 메모리 공간이다.
키보드를 통해 입력되는 데이터는 일단 입력 버퍼에 저장된 다음에 (버퍼링 된 다음에) 프로그램에서 읽힌다.
즉 fgets 함수가 읽어 들이는 문자열은 입력버퍼에 저장된 문자열이다.
키보드로부터 입력된 데이터가 입력 스트림을 거쳐서 입력버퍼로 들어가는 시점은 엔터 키가 눌리는 시점이다.
키보드로 아무리 문자열을 입력해도 엔터 키가 눌리기 전에는 fgets 함수가 문자열을 읽어 들이지 못하는 것이다.
엔터 키가 눌리기 전에는 입력버퍼가 비워져 있으니 말이다.
버퍼링을 왜 할까?
데이터를 목적지로 바로 전송하지 않고 중간에 출력버퍼, 입력버퍼를 둬서 전송하고자 하는 데이터를 임시 저장을 하는 이유는 뭘까? 데이터 버퍼링의 가장 큰 이유는 '데이터 전송의 효율성'이다.
키보드, 모니터와 같이 외부 장치와의 입출력은 생각보다 시간이 걸린다. 따라서 버퍼링 없이 키보드가 눌릴 때마다 눌린 문자의 정보를 목적지로 바로 이동시키는 것보다 중간에 메모리 버퍼를 둬서 데이터를 한데 묶어 이동시키는 게 효율적이고 빠르다.
출력버퍼를 비우는 fflush 함수
출력버퍼가 비워진다는 것은 출력버퍼에 저장된 데이터가 버퍼를 떠나서 목적지로 이동됨을 뜻한다.
출력되면 비워진다.
근데 출력버퍼가 비워지는 시점은 시스템에 따라, 버퍼의 성격에 따라 달라서 fflush 함수를 알아 둘 필요가 있다.
#include <stdio.h>
int fflush(FILE * stream);
인자로 전달된 스트림의 버퍼를 비우는 기능을 제공한다.
fflush(stdout); // 표준 출력버퍼를 비워라.
입력버퍼 비우는 방법
입력버퍼가 비워진다는 것은 데이터의 소멸을 의미한다.
fgets 함수 호출로 인해서 엔터 키 포함 7문자가 입력되었다고 하자.
fgets 함수의 인자로 7이 전달되면, 널 문자를 제외하고 최대 6문자를 읽어들인다.
그럼, \n을 제외하고 여섯 문자만 읽혀지고 \n은 입력버퍼에 남아있다. 후에 fgets 함수가 다시 호출되면, fgets 함수는 \n을 만날 때까지 읽어 들이는 함수이니, 버퍼에 남아있는 \n만 읽어버리고 만다.
이럴 때는 입력버퍼에 남아있는 \n 문자 하나를 비워야 한다.
입력을 필요 이상으로 많이 했을 때는 나머지 문자들을 모두 비워야 한다.
while ( getchar() != '\n' );
\n이 읽혀질 때까지 입력버퍼에 저장된 문자들을 지운다.
입출력 이외의 문자열 관련 함수
헤더파일 string.h에 선언된 문자열 관련 함수들 중 일부이다.
- strlen: 문자열의 길이를 반환하는 함수
#include <stdio.h>
size_t strlen(const char *s);
이 함수의 반환형 size_t는 일반적으로 다음과 같이 선언되어 있다.
typedef unsigned int size_t;
unsigned int 선언을 size_t로 대신할 수 있다는 말이다.
strlen 함수의 반환형은 size_t이니, 이 함수의 반환 값을 unsigned int형 변수에 저장하고 서식문자 %u로 출력하는 것이 정확하다. 그러나 문자열이 아무리 길어도 문자열의 길이정보는 int형 변수에 저장이 가능하기 때문에 strlen 함수의 반환 값을 int형 변수에 저장하고 서식문자 %d로 출력하는 것도 가능할 뿐만 아니라 이것이 더 흔한 일이다.
strlen 함수의 반환 값을 int형 변수에 저장하고 %d로 출력한다.
fgets에서 엔터가 나올 때까지 문자열을 읽는데,
엔터('\n')와 널('\0') 모두 입력되었을 때 strlen을 사용해서 '\n' 자리에 '\0'을 넣어서 개행 안 되게 할 수도 있다.
- strcpy, strncpy: 문자열을 복사하는 함수들
#include <stdio.h>
char * strcpy ( char * dest, const char * src );
char * strcpy ( char * dest, const char * src, size_t n );
strcpy(str2, str1);
str2에 str1을 복사한다. 그리고 복사된 str2을 반환한다.
strncpy(str2, str1, sizeof(str2));
str2에 str1을 복사한다. str2의 길이가 길다면, sizeof(str2)가 반환한 값에 해당하는 문자의 수 만큼만 복사한다.
주의할 점은, strncpy 함수는 문자열을 단순하게 복사한다는 점이다.
따라서 문자열 마지막 인덱스에는 널 문자('\0')을 넣어줘야 한다.
- strcat, strncat: 문자열을 덧붙이는 함수들
#include <stdio.h>
char * strcat ( char * dest, const char * src );
char * strncat ( char * dest, const char * src, size_t n );
strcat(str1, str2);
str1에 str2를 덧붙인다. 그리고 str1을 반환한다.
strncat(str1, str2, n);
str1에 str2의 문자 n개를 덧붙인다.
strncat은 strncpy와 달리 문자열의 끝에 널 문자('\0')를 자동으로 붙여준다.
- strcmp, strncmp: 문자열을 비교하는 함수들
#include <stdio.h>
int strcmp ( const char * s1, const char * s2 );
int strcmp ( const char * s1, const char * s2, size_t n );
s1이 더 크면 0보다 큰 값 반환
s2가 더 크면 0보다 작은 값 반환
s1과 s2의 내용이 모두 같으면 0 반환
s1이 s2보다 작으면 -1, 같으면 0, 크면 1을 반환한다. 아스키 값으로 비교한다.
일반적으로 '0을 반환하면 동일한 문자열, 0이 아닌 값을 반환하면 동일하지 않은 문자열'로 사용한다.
- atoi: 문자열 형 변환 함수
#include <stdio.h>
int atoi ( const char * str );
문자열을 int형으로 변환해준다.
문자열을 long형으로 변환해주는 atol, 문자열을 double형으로 변환해주는 atof 함수가 있다.
'Algorithm > C' 카테고리의 다른 글
[c언어 개념] #13 파일 입출력 (0) | 2022.07.27 |
---|---|
[c언어 개념] #12 구조체, 구조체 배열, typedef, 공용체, 열거형 (0) | 2022.07.25 |
[c언어 개념] #10 함수 포인터, void 포인터 (0) | 2022.07.24 |
[c언어 개념] #9 포인터의 포인터, 이중 포인터, 배열 포인터, 포인터 배열 (0) | 2022.07.24 |
[c언어 개념] #8 2차원 배열 (0) | 2022.07.24 |