아무래도 배우는게 있으면 그때 그때 글을 써야할 거 같습니다.
좀 있다가 쓰려니까 글 쓰기가 어렵네요.
https://www.acmicpc.net/problem/1152
이번에 푼 문제는 1152번 단어의 개수 문제이다.
문제의 풀이
문제를 보고 예시를 보면 알겠지만,
우선 띄어쓰기 ' '의 갯수를 세고 거기에 +1 한 값이 단어의 개수가 된다.
앞에 띄어쓰기가 있다면 -1, 또한 뒤에도 띄어쓰기가 있다면 -1 하면 되겠다고 생각했다.
#include<stdio.h>
#include<string.h>
int main()
{
char string[1000000];
int i;
int cnt=0;
gets(string);
for(i=0; string[i] != '\0'; i++)
{
if(string[i] == ' ')
{
cnt++;
}
}
cnt++;
if(string[0] == ' ')
{
cnt--;
}
if(string[strlen(string)-1] == ' ')
{
cnt--;
}
printf("%d",cnt);
return 0;
}
그래서 처음 작성한 코드.
문자열을 입력받을 때 scanf를 사용하면 띄어쓰기에서 끝나기 때문에 gets나 fgets를 사용해야 한다고 생각했고, 처음에는 gets를 사용했다.
이렇게 해서 컴파일 에러. 무엇이 문제인지 찾아봤다.
저번 글을 작성하는 과정에서 버퍼에 대한 개념을 배웠다. 정확히는 기억나지 않지만, 다른 문제에서 scanf를 사용하고 뒤에 gets를 사용하는 것이 문제라는 것을 배웠다.
scanf, gets, fgets
컴퓨터에는 버퍼라는 개념이 있다. (어차피 나도 잘 모르지만) 간단히 말해 키보드로 들어와서 모니터로 나가는 것이 표준 입, 출력이다. 우리가 C언어에서 stdio로 사용하는 Standard Input Output은 그것을 관장하는 헤더파일인 것이다.
이러한 표준 입 출력에는 버퍼라는 개념이 있다. 우리가 동영상을 볼 때 '버퍼링'이라고 하는 것도 그 버퍼에서 나왔다.
https://ko.wikipedia.org/wiki/%EB%B2%84%ED%8D%BC_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)
임시로 저장되는 메모리 공간이라고 생각하면 되겠다.
문자열을 입력할 때, asdf(엔터)를 입력하면
scanf에서는 asdf(엔터)를 입력하면 엔터를 \0으로 바꾼다.
(띄어쓰기도 마찬가지다.)
asdf(\n) -> asdf(\0)
gets 함수는 asdf(엔터)에서 버퍼의 (\n)까지 가져오고 \0으로 바꾼다.
asdf(\n) -> asdf(\n) -> asdf(\0)
fgets함수도 \n까지 가지고 오고 그 후에 \0을 덧붙인다.
asdf(\n) -> asdf(\0)
무슨 뜻일까.. 코드를 가지고 테스트해봤다,
#include<stdio.h>
int main()
{
char s[100];
//scanf("%s",s); gets(s); fgets(s,10,stdin);
int i;
for(i=0; i<10; i++)
{
printf("%d ",s[i]);
}
printf("\n");
printf("%s",s);
return 0;
}
s에 총 3가지 입력함수를 사용해서 테스트 했다.
순서대로 scanf, gets, fgets이다. scanf와 gets는 같지만 fgets의 차이가 보이는가?
숫자를 10개 나타냈는데 asdf를 나타내는 102까지의 숫자 다음
fgets에 있는 10이라는 수는 \n을 뜻하는 아스키 코드이다.
다른 코드를 살펴보자
#include<stdio.h>
int main()
{
char s1[100];
char s2[100];
scanf("%s",s1);
// gets(s2); fgets(s2,10,stdin);
printf("%s",s1);
return 0;
}
입력으로 asdf(\n)을 했더니
gets, fgets 두 입력함수 모두 무시되고 asdf가 출력되었다.
scanf는 버퍼에 \n을 남기고 가기 때문에, 다음 입력함수가 사용되면 버퍼에 남아있는 \n을 읽고 바로 종료된다.
#include<stdio.h>
int main()
{
char s1[100];
char s2[100];
scanf("%s",s1);
fgets(s2,10,stdin);
printf("%s\n",s1);
printf("%d",s2[0]);
return 0;
}
다음 코드에서 fgets는 \n을 읽고 바로 종료되었음을 알 수 있다.
그러나 gets는 읽어오지 않았다.
이번엔 asdf(공백)(엔터)를 입력해보자 코드는 위 코드에 printf("%d",s2[1]);을 추가해서 다음 칸까지 관찰했다.
fgets는 띄어쓰기(32)와 \n(10) 모두 읽어온다.
gets는 띄어쓰기는 읽었으나, \n은 읽지 않았다.
scanf는 다음 문자열이 입력될 때까지, 그러니까 다음 입력함수도 실행되었다.
아 그런데 scanf("%c");로 문자를 입력받으면 \n이 들어간다.
이 밖에도 필요한 실험들은 본인이 해보면 되겠다.
지금까지 한 걸로 정리해보면,
scanf는 입력받으면 버퍼에 \n을 남긴다.
gets는 버퍼에 있는 \n을 보면 종료하지만 \n을 읽어오지는 않고
fgets는 버퍼에 있는 \n을 보면 종료하고 \n을 읽어온다.
버퍼에 \n이 있을 때 scanf의 %c를 사용하면 버퍼에 있는 \n이 입력되고 곧바로 종료된다.
위에서
scanf는 \n을 가져오지 않고 마지막에 \0을 붙인다.
gets 함수는 asdf(엔터)에서 버퍼의 (\n)까지 가져오고 \0으로 바꾼다.
fgets함수도 \n까지 가지고 오고 그 후에 \0을 덧붙인다. 라고 설명한 것을 다시 설명해보겠다.
여기서 가져온다는 것은 함수를 실행할 때 버퍼에서 가져온다는 뜻이다.
scanf 함수는 가져오지 않으니 이전에 실행한 scanf가 있어도 종료되지 않는다. (띄어쓰기에 대해서는 잘 모르겠다..)
%c는 무엇이든 버퍼에서 바로 가져오니까 종료되고
gets와 fgets는 버퍼에서 \n까지 가져오니까 둘 다 종료되지만 gets는 \0으로 바꾸기 때문에 0으로 출력되고 fgets는 가지고 오기 때문에 10이 출력된다.
그 뒤에 \0을 덧붙이기 때문에 다음 배열 칸은 0이 되어있다. (fgets로 asdf(엔터)할 때 0번, 1번 인덱스 뽑아보면 10, 0임)
너무 길어졌는데, 이런 문제를 해결하기 위해서 버퍼를 비우는 방법으로는
getchar(); 사용
fflush(stdin); 사용
tcflush(0,TCIFLUSH); 사용
... 등이 있다.
참고하시라.
정답
그래서 이런 문제를 해결하는 과정을 거치고
처음 코드는 컴파일 에러가 뜨는데
백준에서는 gets를 사용할 수 없었기 때문으로 기억한다.
#include<stdio.h>
#include<string.h>
int main()
{
char string[1000000];
int i;
int cnt=0;
fgets(string,sizeof(string),stdin);
string[strlen(string)-1] = 0;
for(i=0; string[i] != '\0'; i++)
{
if(string[i] == ' ')
{
cnt++;
}
}
cnt++;
if(string[0] == ' ')
{
cnt--;
}
if(string[strlen(string)-1] == ' ')
{
cnt--;
}
printf("%d",cnt);
return 0;
}
다음과 같이 고쳐서 문제를 해결했다.
strlen은 null까지를 읽어들여서 길이를 잰다.
문자열을 null까지 읽어들이고 ex) abc\0이면 3. c는 2번째 요소
마지막 요소를 가리키기 위해 strlen(string) -1 한다. fgets를 사용했으니 마지막 요소에는 \n이 들어가 있었을 테지만 0을 대입하면서 scanf를 사용하던 것처럼 사용하기 수월해진다.
왜냐하면 마지막 if문에 맨 끝 요소가 ' '인지를 검사하는데, \n을 0으로 대체하지 않았더라면
strlen을 통해서 if문에서 읽어들인 마지막 요소는 항상 \n이기 때문에 검사가 불가능하다.