C // 문자와 문자열관련함수
입력과 출력
프로그램 안으로 데이터가 흘러 들어오는 것이 입력
프로그램 밖으로 데이터가 흘러 나가는 것이 출력
입력장치 : 마우스, 키보드, 화상카메라, 파일 등등
출력장치 : 모니터, 프린터, 파일 등등
프로그램상에서 모니터로 문자열을 출력할 수 있는 이유!
프로그램과 모니터, 프로그램과 키보드는 기본적으로 연결되어 있는 개체가 아닌, 서로 떨어져 있는 개체이다. 프로그램 상에서 모니터와 키보드를 대상으로 데이터를 입출력 하기 위해서는 이들은 연결시켜 주는 다리가 필요한데 이를 스트림(stream)이라고 한다.
운영체제는 외부장치와 프로그램과의 데이터 송수신의 도구가 되는 스트림을 제공하고 있다.
파일입출력 시에는 파일과 연결을 위한 스트림을 직접 요구해야 하지만, 콘솔 입출력 시에는 콘솔 입출력을 위한 '입력 스트림'과 '출력 '스트림'은 프로그램이 실행하면 자동으로 생성, 프로그램 종료되면 자동으로 소멸되는 스트림이다. 이를 표준 스트림이라고 한다.
표준 스트림
- stdin 표준 입력 스트림 키보드 대상으로 입력
- stdout 표준 출력 스트림 모니터 대상으로 출력
- stderr 표준 에러 스트림 모니터 대상으로 출력
(표준 에러 스트림 : 모니터로 출력이 이뤄진다는 점에서 표준 출력 스트림과 차이가 없다. 하지만 이는 입출력 리다이렉션을 통해 모니터를 파일로 변경할 수 있다. 리눅스에서 사용)
문자단위 입출력 함수
문자 출력 함수 : putchar, fputc
모니터를 하나의 문자로 출력할 때 일반적으로 사용하는 두 함수
int putchar(int c);
int fputc(int c, FILE * stream);
-> 함수 호출 성공 시 쓰여진 문자정보가, 실패 시 EOF 반환
putchar함수는 인자로 전달된 문자정보를 stdout으로 표현되는 표준 출력 스트림으로 전송하는 함수, 인자로 전달된 문자를 모니터로 출력하는 함수
두 함수는 동일하지만 fputc함수는 문자를 전송할 스트림을 지정할 수 있다. 그 말은 fputc는 stdout가 아닌 파일을 대상으로도 데이터를 전송할 수 있다.
문자 입력 함수 : getchar, fgetc
키보드로부터 하나의 문자를 입력 받을 때 일반적으로 사용하는 두 함수
int getchar(void);
int fgetc(FILE * stream);
-> 파일의 끝에 도달하거나 함수 호출 실패 시 EOF 반환
getchar함수는 stdin으로 표현되는 표준 입력 스트림으로부터 하나의 문자를 입력 받아서 반환하는 함수
차이점은 fgetc 함수는 문자를 입력 받을 스트림을 지정할 수 있다.
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include<stdio.h> int main() { int ch1, ch2; ch1 = getchar(); ch2 = fgetc(stdin); putchar(ch1); fputc(ch2, stdout); return 0; } |
p
p
7, 8행 : 키보드로부터 각각 하나의 문자를 받고 있다.
10, 11행 : 모니터로 각각 하나의 문자를 출력하고 있다.
분명 두개의 문자를 입력받고, 두개의 문자를 출력했지만 하나의 문자가 입력되고 출력되었다. 눈에는 보이지 않지만 두개의 문자가 입력이 되고 출력이 되었다. 첫 번째 문자는 p, 두 번째 문자는 '엔터 키' 이다. 엔터 키도 아스키 코드 값이 10인 \n으로 표현되는 문자이기 때문에 입출력의 대상이 된다.
5행 : 문자를 int형 변수에 저장하고 있다. ( 맨 밑에서 설명 )
EOF ( End Of File )
파일의 끝을 표현하기 위해 정의해 놓은 상수 -1
파일을 대상으로 fgetc 함수가 호출되면, 그리고 그 결과로 EOF가 반환되면, 이는 파일의 끝에 도달해서 더 이상 읽을 내용이 없다는 뜻이다.
키보드의 입력에 파일의 끝이라는 것이 없기 때문에, Window에서 ctrl+Z, Linux에서 ctrl+D 키의 입력으로 별도로 약속을 해 놓은 것이다.
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include<stdio.h> int main() { int ch1; while(1) { ch1 = getchar(); if(ch1 == EOF) break; putchar(ch1); } return 0; } |
출력결과
Hi~
Hi~
my boyfriend
my boyfriend
sdf
sdf
^Z
getchar 함수는 하나의 문자만 받는다. 하지만 문자열을 입력하여도 그 문자열을 출력이 가능하다. 이는 문자 하나을 입력받고, 하나 출력하고를 계속 반복하기 때문이다. 한개한개 출력을 하기 때문에 문자열이 출력되는 것 처럼 보이게 되는 것이다.
반환형이 int형이고, int형 변수에 문자를 담는 이유
int getchar(void);
int fgetc(FILE * stream);
- char형은 예외적으로 signed char가 아닌 unsigned char로 표현하는 컴파일러가 존재한다.
- 파일의 끝에 도달했을 때 반환하는 EOF는 -1로 정의가 되어있다.
- char를 unsigned char로 표현하면 컴파일러는 EOF에 해당하는 -1을 반환하지 못한다.
- int는 모든 컴파일러가 signed int로 처리하기 때문에 -1 반환에 무리가 없다.
=> EOF는 -1을 반환하는데 char는 unsigned char로 처리하는 컴파일러가 예외적으로 존재해서 signed로만 처리하는 int로 처리하여 반환!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include<stdio.h> int main() { char *str = "Simple String"; printf("1. puts test \n"); puts(str); puts("So Simple String"); printf("2. puts test \n"); fputs(str,stdout); printf("\n"); fputs("So Simple String",stdout); printf("\n"); return 0; } |
1. puts test
Simple String
So Simple String
2. puts test
Simple String
So Simple String
puts 함수가 호출되면 문자열 출력 후 자동으로 개행이 이뤄지지만, fputs함수가 호출되면 문자열 출력 후 자동으로 개행이 이뤄지지 않는다.
문자열 입력 함수 : gets, fgets
char *gets(char * s);
char *fgets(char * s, int n, FILE * stream);
-> 파일의 끝에 도달하거나 함수 호출 실패 시 NULL 포인터 반환
int main()
{
char str[7]; // 7바이트 메모리 공간 할당
gets(str); // 입력 받은 문자열을 배열 str에 저장
. . . .
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include<stdio.h> int main() { char str[7]; int i; for(i=0;i<3;i++) { fgets(str, sizeof(str), stdin); printf("Read %d: %s \n",i+1, str); } return 0; } |
12345678901234567890
Read 1: 123456
Read 2: 789012
Read 3: 345678
문자열의 길이를 넘어서다 보니, fgets 함수는 7보다 하나 작은 6의 길이만큼 문자열을 읽는다. 따라서 프로그램 사용자는 한 번 입력하였지만, fgets 함수는 3회 모두 호출 되었다. 하지만 아직도 프로그램 사용자가 입력한 문자열을 모두 읽어 들이지 못한 상황이다. 이를 4회 또는 문자열의 길이를 늘렸다면 모두 읽어 들일 수 있다.
ex2)fgets()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include<stdio.h> int main() { char str[7]; int i; for(i=0;i<3;i++) { fgets(str, sizeof(str), stdin); printf("Read %d: %s \n",i+1, str); } return 0; } |
We
Read 1: We
Like
Read 2: Like
You
Read 3: You
11행의 개행은 하나가 삽입되어 있지만, 출력될 때마다 개행이 두 번 이루어진 이유는 fgets함수는 \n 를 만날 때까지 문자열을 읽어 들이는데, \n 을 제외시키거나 버리지 않고 문자열의 일부러 받아 들이므로 입력한 엔터 키의 정보 까지도 문자열의 일부로 저장이 되는 것이다.
또 한, fgets()함수는 \n을 만날 때까지 문자열을 읽어 들이기 때문에, 중간에 공백이 삽입되어도 가능하다!(scanf와는 다르게) 공백 문자도 문자열의 일부로 읽어들이기 때문!
표준 입출력과 버퍼
지금까지 사용해온 printf(), scanf(), fputc, fgetc 모두 표준 입출력 함수이다. 이러한 표준 입출력 함수를 통해서 데이터를 입출력 하는 경우, 해당 데이터들은 운영체제가 제공하는 '메모리버퍼'를 중간에 통과하게 된다. 메모리 버퍼는 데이터를 임시로 모아두는 메모리 공간이다
키보드를 통해 입력되는 데이터는, 일단 입력버퍼에 저장된 다음에 프로그램에 읽혀진다. 키보드로부터 입력된 데이터가 입력 스트림을 거쳐서 입력버퍼로 들어가는 시점은 엔터 키가 눌리는 시점이다.
버퍼링을 하는 이유는 데이터 전송의 효율성 때문이다. 버퍼링 없이 키보드가 눌릴 때마다 눌린 문자의 정보를 목적지로 바로 이동시키는 것보다 중간에 메모리 버퍼를 둬서 데이터를 한데 묶어서 이동시키는 것이 보다 효율적이고 빠르다.
문자열 출력함수가 반환되는 시점은 최종 목적지로 도달했을 때가 아니라, 출력버퍼에 모든 데이터를 넣었을 때!
따라서 함수가 반환되는 시점과 문자열 데이터가 모니터에 출력되는 시점이 다를 수 있다.
출력버퍼를 비우는 fflush 함수
int fflush(FILE * stream);
-> 함수호출 성공 시 0, 실패 시 EOF 반환
fflush(stdout); // 표준 출력버퍼를 비워라! > 모니터로 전송하라
버퍼에 저장된 내용이 비워지면서 데이터가 목적지로 이동한다.
그렇다면 입력버퍼는 어떻게 비우는가 ?
입력버퍼를 비워라! 라는 뜻은 입력버퍼에 있는 데이터를 소멸하라 라는 뜻으로 해석된다. 위의 fflush()를 이용하여 입력버퍼를 비우는 것은 크게 잘못된 일이다. C언어의 표준에서는 정의하고 있지 않기 때문이다. 컴파일러마다 되는 것도, 되지 않는 것도 있다.
그렇다면 입력버퍼 비우는 방법을 알아보자
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include<stdio.h> int main() { char perID[7]; char name[10]; fputs("주민번호 앞 6자리 입력: ", stdout); fgets(perID, sizeof(perID), stdin); fputs("이름 입력: ", stdout); fgets(name, sizeof(name), stdin); printf("주민번호 : %s \n", perID); printf("이름 : %s \n", name); return 0; } |
주민번호 앞 6자리 입력: 931027
이름 입력: 주민번호 : 931027
이름 :
이처럼 이름을 입력할 기회를 얻지 못하는 이유는 931027(enter) 이렇게 엔터를 포함한 7개의 문자가 입력되었기 때문이다.
9행의 fgets 함수의 인자로 7이 전달되었으니, 널 문자를 제외하고 최대 6문자를 읽어 드린다. 따라서 \n을 제외한 나머지 여섯 문자만 읽혀지고 \n은 입력버퍼에 남아 있게 된다. 그리고 이어서 12행의 fgets 함수가 호출된다. 그런데 fgets 함수는 \n을 만날 때까지 읽어 들이는 함수이니, 버퍼에 남아있는 \n만 읽어버리고 만다.
출력결과 2
주민번호 앞 6자리 입력: 931027-1234567
이름 입력: 주민번호 : 931027
이름 : -1234567
분명 주민번호 앞6자리만 입력하라고 했는데, -를 포함하여 14자리의 주민번호를 전부 입력하였다. 그래서 9행의 fgets 함수 호출을 통해서 여섯 개의 문자가 읽혀지고 12행의 fgets 함수호출을 통해서 나머지 문자들이 읽혀진 것이다. 이렇듯 명시한대로 행동하지 않는 프로그램 사용자를 고려한다면 주민번호 앞 6자리를 제외한 나머지 문자들을 입력버퍼에서 지워줘야 한다.
입력버퍼를 지우는 함수
void ClearLineFromReadBuffer(void)
{
while(getchar!='\n');
}
입력버퍼에 저장된 문자들은 읽어들이면서 지워진다. 읽어들인 문자를 저장하거나 하지는 않고 읽어드려 버린다.
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include<stdio.h> void ClearLineFromReadBuffer(void) { while(getchar()!='\n'); } int main() { char perID[7]; char name[10]; fputs("주민번호 앞 6자리 입력: ", stdout); fgets(perID, sizeof(perID), stdin); ClearLineFromReadBuffer(); fputs("이름 입력: ", stdout); fgets(name, sizeof(name), stdin); printf("주민번호 : %s \n", perID); printf("이름 : %s \n", name); return 0; } |
출력결과1
주민번호 앞 6자리 입력: 931027
이름 입력: ozil
주민번호 : 931027
이름 : ozil
입력버퍼에 남아있는 \n을 지워버리기 때문에 위와 같이 정상적으로 입출력이 이뤄진다.
출력결과2
주민번호 앞 6자리 입력: 931027-1234567
이름 입력: ozil
주민번호 : 931027
이름 : ozil
프로그램 사용자가 잘못 입력을 해도, 필요한 만큼만 읽어 들이고 나머지는 지워버리기 때문에 위에서 보는바와 정상 작동한다 .
문자열 관련 함수
문자열의 길이를 반환하는 함수 : strlen
문자열의 길이를 반환하되, 널 문자는 길이에 포함하지 않는다.
size_t strlen(const char * s);
int main()
{
char str[] = "1234567";
printf("%d", strlen(str)); // 문자열의 길이 7이 반환된다.
. . . .
}
fgets()함수호출을 통해 문자열을 입력 받고 싶은데, 같이 딸려서 들어오는 \n문자는 문자열에서 제외시키고 싶다.
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include<stdio.h> #include<string.h> void RemoveBSN(char str[]) { int len = strlen(str); str[len-1] = 0; } int main() { 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; } |
출력결과
문자열 입력: cheer up!
길이: 10, 내용: cheer up!
길이: 9, 내용: cheer up!
15행을 통한 출력에서는 개행이 2번 이뤄졌다. 하지만 RemoveBSN 함수호출 이후에 18행의 출력에서는 개행이 한 번 이뤄졌다. 이는 RemoveBSN 함수호출을 통해 \n 문자가 소멸되었기 때문이다.
문자열을 복사하는 함수 : 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에 복사
. . . .
}
문자열이 복사될 배열의 길이가 문자열의 길이보다 작지 않도록 주의해야 한다.
int main()
{
char str1[30] = "Simple String";
char str2[30];
strncpy(str2, str1, sizeof(str2));
. . . .
}
str1에 저장된 문자열을 str2에 복사하되, sizeof(str2)가 반환한 값에 해당하는 문자의 수 만큼만 복사를 진행하라!
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include<stdio.h> #include<string.h> int main() { char str1[20]="1234567890"; char str2[20]; char str3[5]; /** case1 **/ strcpy(str2, str1); puts(str2); /** case2 **/ strncpy(str3, str1, sizeof(str3)); puts(str3); /** case3 **/ strncpy(str3, str1, sizeof(str3)-1); str3[sizeof(str3)-1]=0; puts(str3); return 0; } |
출력결과
1234567890
1234????234567889
1234
case1 : 복사 후, 제대로 출력이 이루어졌다.
case2 15행 : 이 문장을 잘 보자. 복사하는 최대 문자수로 sizeof(str3)의 반환 값이 전달되었으니, 제대로 복사가 이루어지겠구나 라고 생각할 것이다. 배열의 길이가 5이니 총 5개의 문자가 복사가 잘 될 것이다. 하지만 이 5개의 문자 안에는 널 문자가 포함되지 않는다. 그래서 16행의 출력결과가 이상한 것이다. 널 문자가 존재해야 출력을 하다가 널 문자를 만나면 출력을 멈출텐데 널 문자가 없으니 엉뚱한 영역까지 출력을 하고 있는 것이다. 이는 case3을 통해 방법을 적어놓았다.
case3 : 배열의 마지막 문자를 0(널 문자)으로 넣어주므고 4개는 문자를 복사를 하고 마지막 5번째영역은 널 문자를 넣어주므로, 제대로 된 출력을 볼 수 있다.
문자열을 덧붙이는 함수 : strcat, strncat
덧붙여지 문자열의 주소 값 반환
char * strcat(char *dest, const char * stc);
char * strcat(char *dest, const char * stc, size_t n);
int main()
{
char str1[30] = "First~";
char str2[30] = "Second";
strcat(str1, str2); // str의 문자열 뒤에 str2를 복사
. . . .
}
str2의 문자열이 str1의 문자열 뒤에 덧 붙여지게 되는데, 형태는 다음과 같다.
널 문자 다음이 아닌 널 문자가 저장된 위치에서부터 덧붙임이 시작된다. 그래야 문자열의 끝에 하나의 널 문자만 존재하는 정상적인 문자열이 된다.
strncat(str1, str2, 8) // str2의 문자열중 최대 8개를 str1의 뒤에 덧붙여라
즉 str2의 길이가 8을 넘어선다면 8개의 문자까지만 str1에 덧붙이라는 의미인데, 이 8개의 문자에는 널 문자가 포함되지 않는다!
따라서 널 문자를 포함하여 실제로는 총 9개의 문자가 str1에 덧붙여진다.
(strncpy 함수와 달리 strncat 함수는 널 문자를 자동으로 삽입해준다.)
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include<stdio.h> #include<string.h> int main() { char str1[20]="First~"; char str2[20]="Second"; char str3[20]="Simple num: "; char str4[20]="1234567890"; /** case1 **/ strcat(str1, str2); puts(str1); /** case2 **/ strncat(str3, str4, 7); puts(str3); return 0; } |
출력결과
First~Second
Simple num: 1234567
13행 : str2에 저장된 문자열을 str1에 저장된 문자열의 뒤에 덧붙인다.
17행 : str4에 저장된 문자열을 str3에 저장된 문자열의 뒤에 덧붙이되, 7개의 문자만 덧붙이고 있다. 따라서 널 문자를 포함해서 8개의 문자가 덧붙여진다.
문자열을 비교하는 함수 : strcmp, strncmp
두 문자열의 내용이 같으면 0, 같지 않으면 0이 아닌 값 반환
int strcmp(const char * s1, const char * s2);
int strcmp(const char * s1, const char * s2, size_t n);
두 문자를 비교하여 다음의 결과를 반환한다. strncmp 함수는 세 번째 인자로 전달된 수의 크기만큼만 문자를 비교한다.
s1이 더 크면 0보다 큰 값 반환
s2가 더 크면 0보다 작은 값 반환
s1과 s2의 내용이 같으면 0 반환
문자열의 크고 작음은 아스키 코드 값을 기준으로 결정된다.
"ABCD"
"ABCC"
세 번째 문자까지는 동일하고 네 번째 문자를 비교하니 D의 아스키 코드 값이 C 의 아스키 코드 값보다 크다. 따라서 "ABCD"가 더 큰 문자열이 되어 양수가 출력된다.
printf("%d", strcmp("ABCD","ABCC"));
"ABCD"
"ABCDE"
네 번째 문자까지 동일하다. 마지막으로 다섯 번째 문자를 비교한다. "ABCD"의 다섯 번째 문자는 널 문자이고 "ABCDE"의 다섯 번째 문자는 E이기 때문에 음수가 출력
printf("%d", strcmp("ABCD","ABCDE"));
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include<stdio.h> #include<string.h> int main() { char str1[20]; char str2[20]; printf("문자열 입력 1: "); scanf("%s", str1); printf("문자열 입력 2: "); scanf("%s", str2); if(!strcmp(str1, str2)) { puts("두 문자열은 완벽히 같다."); } else { puts("두 문자열은 같지 않다."); if(!strncmp(str1, str2, 3)) { puts("그러나 앞 세 글자는 동일하다"); } } return 0; } |
문자열 입력 1: simon
문자열 입력 2: simple
두 문자열은 같지 않다.
그러나 앞 세 글자는 동일하다
14행 : str1과 str2가 동일하면 거짓을 의미하는 0 이 반환되기 때문에, ! 연산을 하여 거짓을 참으로 바꾸어야 한다.
22행 : 두 문자열이 일치하지 않을 때 실행된다. strncmp 함수의 세 번째 인자로 3이 전달되었으니, 앞의 세 문자가 동일한 경우에 한해서 if 문이 참이 되어 24행을 실행하게 된다.
이외의 변환함수 (헤더파일 stdlib.h)
int atoi(const char *str); // 문자열의 내용을 int형으로 변환
long atol(const char *str); // 문자열의 내용을 long형으로 변환
double atof(const char *str); // 문자열의 내용을 double형으로 변환
문자열로 표현된 정수나 실수의 값을 정수나 실수로 데이터로 변환해야 하는 경우가 있다. 문자열 "123"을 정수 123으로 변환하거나, 문자열 "7.11"을 실수 7.11로 변환해야 하는 경우 위의 함수를 이용하여 쉽게 해결이 가능하다.
ex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include<stdio.h> #include<stdlib.h> int main() { char str1[20]; printf("정수 입력: "); scanf("%s", str1); printf("%d \n", atoi(str1)); printf("실수 입력: "); scanf("%s", str1); printf("%lf \n", atof(str1)); return 0; } |
정수 입력: 123
123
실수 입력: 3.15
3.150000