C언어 (19) 문자열 이용하기
지난번에 문자열의 입출력 함수에 대해 자세히 알아봤다.
이제 문자열을 처리하는 함수와 포인터로 문자열을 사용해보자.
문자열 처리 함수
문자열을 프로그램에서 사용하다보면 문자열을 붙이거나, 길이를 구하거나, 비교하는 작업이 필요하다.
직접 함수로 작성해도 되지만 C에서는 이런 역할을 하는 여러 함수를 라이브러리로 제공한다.
1
#include <string.h>
를 맨 위에 추가하면 문자열 관련 함수들을 사용할 수 있다. 기능들을 하나씩 사용하면서 보자.
strlen
strlen()은 문자열의 길이를 알려주는 함수이다.
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <string.h>
int main() {
char s1[20] = "hello";
char s2[20] = "안녕하세요";
printf("s1의 길이 : %d\n", strlen(s1));
printf("s2의 길이 : %d\n", strlen(s2));
return 0;
}
1
2
s1의 길이 : 5
s2의 길이 : 10
인자로 주어진 문자열의 길이를 구해준다.
한글은 한글자에 2바이트의 크기를 가지기 때문에 5*2 = 10이 반환된다.
이 때 NULL 문자는 길이에 포함되지 않는다.
strcpy
strcpy()는 문자열을 복사하는 함수이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
int main() {
char s1[20] = "hello";
char s2[20];
char s3[20];
strcpy(s2, s1);
strncpy(s3, s1, 3);
printf("복사할 문자열 : %s\n", s1);
printf("복사된 문자열 : %s\n", s2);
printf("3개 복사된 문자열 : %s\n", s3);
return 0;
}
1
2
3
복사할 문자열 : hello
복사된 문자열 : hello
3개 복사된 문자열 : hel
첫 번째 인자의 메모리에 두 번째 인자의 문자열을 복사한다.
strncpy는 복사할 크기도 정할 수 있는 기능이 있다.
이 함수도 보안상 취약점이 있기 때문에 #define _CRT_SECURE_NO_WARNINGS를 해줘야 한다.
strcat
strcat()은 문자열을 합친다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
int main() {
char s1[20] = "hello";
char s2[20] = " World";
char s3[20] = " C programming";
printf("기존 문자열 : %s\n", s1);
strcat(s1, s2);
printf("s2가 결합된 문자열 : %s\n", s1);
strncat(s1, s3, 9);
printf("s3가 9글자 결합된 문자열 : %s\n", s1);
return 0;
}
1
2
3
기존 문자열 : hello
s2가 결합된 문자열 : hello World
s3가 9글자 결합된 문자열 : hello World C progra
첫번째 인자의 문자열에 두 번째 인자의 문자열을 합친다.
strncat은 합칠 문자열의 길이를 정할 수 있다.
strcmp
문자열 2개를 s1 == s2로 비교할 수 없다. 문자열도 배열이므로 주소를 비교하는 것과 같은 코드이다.
그래서 strcmp()는 문자열을 비교해주는 역할을 한다.
strcmp(s1, s2)는 s1이 s2보다 작으면 -1, 같으면 0, 크면 1을 반환한다.
strncmp()는 비교할 문자열의 개수를 정할 수 있다.
이때 문자열이 작거나 크다는 기준은 문자열의 앞에서부터 ASCII 값들을 비교하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>
int main() {
char s1[20] = "abc";
char s2[20] = "abcdef";
char s3[20] = "zxc";
printf("첫번째와 세번째 비교 : %d\n", strcmp(s1, s3));
printf("첫번째와 두번째 3글자만 비교 : %d\n", strncmp(s1, s2, 3));
printf("세번째와 두번째 비교 : %d\n", strcmp(s3, s2));
return 0;
}
1
2
3
첫번째와 세번째 비교 : -1
첫번째와 두번째 3글자만 비교 : 0
세번째와 두번째 비교 : 1
s1의 a가 s3의 z보다 작으므로 첫번째는 -1, s1과 s2의 앞에서 3글자는 abc로 같으니 0이 반환된다.
s3의 z가 s2의 a보다 크므로 세번째는 1이 반환된다.
문자열 <-> 수치 변환
프로그램을 만들다보면 “123”이라는 문자열을 123이라는 정수 값으로 바꿔 사용해야 할 때가 있다.
어떻게 쉽게 바꿀 수 있을까?
우선 sscanf와 sprintf 함수를 이용할 수 있다.
scanf와 printf는 각각 키보드로 입력을 받고, 모니터로 출력을 하는 함수였다.
sscanf는 메모리에서 데이터를 입력을 받고, sprintf는 메모리에 데이터를 출력을 하는 함수이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
int main() {
char s1[20] = "123";
char s2[20] = "100 200";
char s3[20];
int num, num1, num2;
sscanf(s1, "%d", &num);
printf("%d\n", num + 20);
sscanf(s2, "%d %d", &num1, &num2);
printf("%d\n", num1 + num2);
sprintf(s3, "%d %d", num1 + num2, num1 - num2);
printf("%s\n", s3);
return 0;
}
1
2
3
143
300
300 -100
코드를 한 줄 씩 보면 sscanf(s1, “%d”, &num); 에서 s1 배열에서 입력을 받아 num에 넣는다.
그러니까 s1의 값이 정수형으로 바뀌어 num에 들어간다.
두 번째 sscanf(s2, “%d %d”, &num1, &num2);는 나아가 s2 배열에서 입력을 받는다.
num1에는 100, num2에는 200이 들어가니 num1+num2 = 300이라는 결과가 나온다.
세 번째 sprintf(s3, “%d %d”, num1 + num2, num1 - num2);는 각각의 연산을 s3 배열에 출력한다.
그러면 계산한 정수값의 결과가 문자열 형태로 바뀌어 s3 배열에 들어가는 것이다.
이렇게 문자열을 정수로 바꿔 이용할 수 있다.
다른 방법도 있는데,
atoi()는 문자열을 int형 데이터로 전환하고, atof()는 문자열을 double형 데이터로 전환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
int main() {
char s1[20] = "123";
char s2[20] = "3.141592";
int num;
double d;
num = atoi(s1);
d = atof(s2);
printf("%d %f", num, d);
return 0;
}
이렇게 내장 함수를 통해서 간단하게 문자열을 정수로 바꿀 수도 있다.
이외에도 많은 C언어 표준 함수들이 있으니 필요에 따라 검색하며 유용하게 사용하자.
문자열의 배열
문자열도 1차원 배열이기 때문에 2차원 배열로 만들면 문자열의 배열이 만들어진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main() {
char s[5][20] = {
"Hello",
"World",
"enjoy",
"Programming",
"visual studio"
};
for (int i = 0; i < 5; i++) {
printf("%s\n", s[i]);
}
return 0;
}
이렇게 s[i]를 통해 문자열의 배열에 접근해 사용할 수 있다.
문자열과 포인터
지금까지 문자열을 저장할 때 문자형 배열로 저장했다.
그런데 문자형 포인터를 선언하고 여기에 문자열의 주소를 저장할 수 있다.
1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
char* cp = "Hello world!";
cp[6] = 'g'; // 불가능
printf("%s", cp);
return 0;
}
1
Hello world!
이렇게 cp에 저장하면 출력도 잘 된다. 그런데 코드를 뜯어보면 이게 무슨 말일까?
char* cp는 문자형 주소를 저장하는 포인터 변수이다. 여기에 문자열이 저장되고 있다.
그러면 문자열이 주소로 저장된다는 것일까? <- 이것이 맞다.
1
2
3
4
5
#include <stdio.h>
int main() {
printf("%p", "Hello world!");
return 0;
}
신기하게도 이 코드를 실행시키면 어떤 주소가 출력된다.
C언어에서는 큰따옴표로 묶인 것을 문자열 리터럴로 다뤄 메모리의 한 부분에 이런 값들을 저장한다.
즉 “Hello world!”는 문자열 리터럴이니까 메모리에 저장이 되었고, 주소를 가지고 있다.
또한 리터럴이 저장되는 곳은 읽기만 가능한 곳이라, 문자열의 내용을 변경할 수 없다.
1
2
char str[] = "hello";
str[1] = 'H' // 가능
이렇게 배열에 저장하는 것은 한 글자씩 배열에 저장하는 것이라 리터럴과는 다른 곳에 저장된다.
그렇기에 수정이 가능한 것이다.
마지막으로 문자열에서 앞뒤 공백을 없애는 trim 기능을 하는 함수를 보고 문자열을 끝내자.
이 코드를 처음 보고 이해했을 때 엄청 충격적이고 코드가 아름답다는 생각을 했었다.
포인터와 반복문을 이용했는데 참 신기하다. 너무 강렬한 기억으로 아직까지도 남아있다.
이런게 코딩의 매력이 아닌가 싶어..서 블로그에 남기고 싶다.
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
31
32
#include <stdio.h>
void trim(char* str) {
// 앞 공백 제거
while (*str == ' ') {
str++;
}
// 뒤 공백 제거
char* end = str;
while (*end) {
end++;
}
end--;
while (end > str && *end == ' ') {
end--;
}
// 문자열 끝에 널 종료 문자 넣기
*(end + 1) = '\0';
}
int main() {
char str[] = " Hello, World! ";
printf("Before trim: '%s'\n", str);
trim(str); // trim 함수 호출
printf("After trim: '%s'\n", str);
return 0;
}
문자열 str[0]부터 포인터가 시작해 앞 공백이 없어질 때까지 포인터를 증가시킨다.
그 다음 end라는 포인터는 str부터 시작해 문자열의 끝까지 증가한다.
c언어에서는 값이 있으면 참으로 간주하기에 while(*end)라는 코드로 이 기능을 할 수 있다.
마지막으로 끝에서부터 공백이 아닐때까지 end 포인터를 감소시키고, 끝에 NULL문자를 넣는다.
str의 시작 부분은 공백이 아닌 곳까지 증가했고, 뒤에서 공백이 시작되는 부분에 NULL문자가 들어갔다.
그렇기에 str을 출력하면 앞 뒤 공백이 전부 사라진 Hello, World!만이 출력된다.
와.. 코드가 참 예쁜 것 같다.
C언어에서 문자열은 다루기 왜 이렇게 복잡할까.. 그래도 귀찮은거지 어렵지는 않다.
이제 파일 입출력, 메모리 할당, 컴파일 잡다한 이야기만 끝내면 C언어는 끝이 난다!