C 의 문자열은 결국 '\0' 으로 끝나는 char 배열이다. str 함수들은 전부 이 '\0' 을 믿고 움직인다.
C 의 문자열이란?
C 에는 string 타입이 따로 없다. 문자를 char 배열에 한 칸씩 넣고, 끝에 '\0' (널 문자)을 붙여서 "여기까지가 문자열"이라고 표시한다.
char s[] = "hello"; // 실제로는 'h','e','l','l','o','\0' → 6바이트
그래서 문자열을 담을 공간은 항상 글자 수 + 1 이 필요하다. 이 + 1 을 까먹는 게 문자열 버그의 대부분이다.
str 함수들은 크기를 따로 받지 않고 '\0' 이 나올 때까지 계속 읽는다. '\0' 이 없으면 배열 밖까지 계속 읽어버린다.
strlen
size_t strlen(const char *s);
'\0' 전까지의 글자 수를 돌려준다. '\0' 은 세지 않는다.
char s[32] = "hello";
printf("%zu\n", strlen(s)); // 5
printf("%zu\n", sizeof(s)); // 32
sizeof 와 헷갈리기 쉬운데 둘은 보는 게 다르다.
| 보는 것 | 결과 | |
| strlen(s) | 실제 들어있는 글자 수 | 5 |
| sizeof(s) | 배열 전체 크기 | 32 |
| sizeof(p) (p 가 char *) | 포인터 크기 | 8 |
sizeof 는 연산자라서 컴파일 시점에 타입만 보고 크기를 낸다. 문자열 내용을 읽지 않는다. strlen 은 함수라서 실행 중에 '\0' 을 찾을 때까지 한 칸씩 센다.
strcmp
int strcmp(const char *a, const char *b);
같으면 0, a 가 사전순으로 앞이면 음수, 뒤면 양수를 돌려준다.
if (strcmp(a, b) == 0) {
printf("같다\n");
}
== 로 비교하면 안 되는 이유?
a == b 는 문자열 내용이 아니라 주소를 비교한다. 내용이 같아도 다른 곳에 저장돼 있으면 false 가 나온다. 컴파일은 되기 때문에 더 찾기 어렵다.
앞 n 글자만 비교하고 싶으면 strncmp 를 쓴다.
strcpy / strncpy
char *strcpy(char *dst, const char *src);
char *strncpy(char *dst, const char *src, size_t n);
strcpy 는 src 를 '\0' 까지 dst 에 복사한다. dst 가 충분히 큰지는 전혀 확인하지 않는다. 모자라면 남의 메모리를 덮어쓴다 (버퍼 오버플로우).
그래서 크기를 제한하는 strncpy 가 있다. 그런데 이것도 함정이 있다. src 가 n 보다 길면 '\0' 을 붙이지 않는다. 관련 내용은 여기 정리되어 있다.
TIL - C Debugging Lab : Use After Free
문제풀이를 위해 공부한 개념Use After Free말 그대로 free() 한 메모리를 다시 사용하는 것이다.free(p) 가 하는 일 : 이 영역 이제 안 쓴다고 할당자(allocator) 에게 반납한다.free(p) 가 하지 않는 일 : 메
skylarcoding.tistory.com
char dst[5];
strncpy(dst, "hello world", sizeof(dst));
dst[sizeof(dst) - 1] = '\0'; // 직접 막아줘야 한다
이름만 보면 안전한 버전 같은데 뒤처리를 직접 해줘야 한다.
strcat / strncat
char *strcat(char *dst, const char *src);
char *strncat(char *dst, const char *src, size_t n);
dst 끝('\0' 자리)부터 src 를 이어 붙인다. dst 는 이미 '\0' 으로 끝나는 문자열이어야 한다. 초기화 안 한 배열에 strcat 을 하면 어디가 끝인지 모르기 때문에 쓰레기 값 뒤에 붙는다.
char buf[32] = "hello";
strcat(buf, ", world"); // "hello, world"
strncat 은 strncpy 와 다르게 항상 '\0' 을 붙여준다. 대신 n 은 "dst 전체 크기"가 아니라 "src 에서 최대 몇 글자를 붙일지"이다. 그래서 남은 공간은 직접 계산해야 한다.
strncat(buf, src, sizeof(buf) - strlen(buf) - 1);
strdup
char *strdup(const char *s);
문자열 복사본을 새로 만들어서 돌려준다. 내부에서 malloc 을 하기 때문에 free 를 꼭 해줘야 한다. malloc 과 free 는 한 쌍이라는 규칙이 여기에도 그대로 적용된다.
char *copy = strdup("hello");
if (copy == NULL) {
return 1;
}
free(copy);
copy = NULL;
원래 POSIX 함수이고 C23 부터 표준에 들어왔다. 직접 만들면 결국 이거다.
char *copy = malloc(strlen(s) + 1); // + 1 은 '\0' 자리
strcpy(copy, s);
strchr / strrchr / strstr
char *strchr(const char *s, int c); // 문자 c 가 처음 나오는 위치
char *strrchr(const char *s, int c); // 문자 c 가 마지막으로 나오는 위치
char *strstr(const char *hay, const char *needle); // 부분 문자열 위치
셋 다 찾으면 그 위치의 포인터를, 못 찾으면 NULL 을 돌려준다. 인덱스가 필요하면 포인터끼리 빼면 된다.
char *p = strchr(buf, 'w');
if (p != NULL) {
printf("%ld\n", (long)(p - buf)); // 인덱스
}
포인터 산술이라서 가능한 거다. 같은 배열 안의 포인터끼리 빼면 칸 수가 나온다.
포인터에 +1 하면 왜 다음 칸으로 가는지는 배열과 포인터 글에 정리해뒀다.
C 에서의 char[], String 배열 타입
C 에서 char 은 문자 타입이 아니라 크기가 1바이트인 정수 타입이다. 다른 언어와 달리 C 는 문자라는 개념을 언어 차원에서 거의 갖고 있지 않다. 그냥 숫자를 담아두고, 출력할 때 ASCII 표를 보고
skylarcoding.tistory.com
strtok
char *strtok(char *s, const char *delim);
구분자로 문자열을 잘라준다. 첫 호출에는 문자열을, 그 다음부터는 NULL 을 넣는다.
char line[] = "red,green,blue";
for (char *tok = strtok(line, ","); tok != NULL; tok = strtok(NULL, ",")) {
printf("%s\n", tok);
}
두 번째부터 왜 NULL 을 넣는지?
strtok 이 내부에 "어디까지 잘랐는지"를 기억하고 있어서이다. NULL 을 넣으면 "아까 하던 데서 이어서"라는 뜻이다.
그래서 주의할 점이 있다.
- 원본을 수정한다. 구분자 자리에 '\0' 을 박는 방식이다.
- 그래서
char *s = "a,b";같은 문자열 리터럴에 쓰면 안 된다. 리터럴은 수정 불가라 UB 이다.char s[] = "a,b";처럼 배열로 받아야 한다. - 내부 상태가 하나라서 strtok 안에서 또 strtok 을 쓰면 꼬인다.
mem 함수들
str 함수는 '\0' 을 기준으로, mem 함수는 바이트 수를 기준으로 움직인다. 그래서 문자열이 아닌 int 배열이나 구조체에도 쓸 수 있다.
void *memcpy(void *dst, const void *src, size_t n);
void *memmove(void *dst, const void *src, size_t n);
void *memset(void *s, int c, size_t n);
int memcmp(const void *a, const void *b, size_t n);
memcpy 와 memmove 차이?
둘 다 n 바이트를 복사한다. 차이는 두 영역이 겹칠 때이다. memcpy 는 겹치면 UB 이고, memmove 는 겹쳐도 안전하게 처리해준다. 배열 안에서 원소를 한 칸씩 밀 때처럼 같은 배열 안에서 옮기면 memmove 를 써야 한다.
memset 은 바이트 단위
int arr[5];
memset(arr, 0, sizeof(arr)); // 전부 0 (OK)
memset(arr, 1, sizeof(arr)); // 전부 1 이 아니라 16843009
memset 은 바이트 하나하나를 c 로 채운다. int 는 4바이트니까 0x01010101 이 되어서 16843009 가 나온다. 그래서 int 배열에는 0 초기화 용도로만 쓰는 게 안전하다. 0 으로 초기화된 동적 배열이 필요하면 calloc 을 쓰면 된다.
snprintf
<stdio.h> 에 있는 함수지만 문자열 만들 때 제일 많이 쓴다.
int snprintf(char *buf, size_t size, const char *fmt, ...);
printf 처럼 포맷을 쓰는데 결과를 buf 에 저장한다. size 를 넘지 않고 항상 '\0' 을 붙여준다. strcpy + strcat 조합보다 안전해서 복사나 이어 붙이기 대신 쓰기도 한다.
char out[16];
snprintf(out, sizeof(out), "%s-%d", "id", 42); // "id-42"
흔한 실수들
- '\0' 자리(+ 1)를 안 챙김
- == 로 문자열 비교
- strncpy 후 '\0' 을 안 넣음
- 초기화 안 한 배열에 strcat
- strdup 결과를 free 안 함
- 문자열 리터럴에 strtok 이나 수정
- 포인터에 sizeof 를 써서 길이를 구함
- memset 으로 int 배열을 0 이 아닌 값으로 채움
전체 흐름
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char buf[32];
strcpy(buf, "hello"); // 1. 복사
strcat(buf, ", world"); // 2. 이어 붙이기
printf("%s (%zu)\n", buf, strlen(buf)); // 3. 길이
if (strcmp(buf, "hello, world") == 0) { // 4. 비교
printf("같다\n");
}
char *p = strstr(buf, "world"); // 5. 검색
if (p != NULL) {
printf("위치: %ld\n", (long)(p - buf));
}
char *copy = strdup(buf); // 6. 동적 복사
if (copy == NULL) {
return 1;
}
copy[0] = 'H';
printf("%s / %s\n", copy, buf);
free(copy); // 7. 반납
copy = NULL;
char line[] = "red,green,blue"; // 8. 자르기 (배열로 받기)
for (char *tok = strtok(line, ","); tok != NULL; tok = strtok(NULL, ",")) {
printf("%s\n", tok);
}
return 0;
}
- 문자열 공간을 글자 수 + 1 로 잡았는가
- 비교는 strcmp 로 했는가
- 복사·이어 붙이기 전에 dst 크기가 충분한지 확인했는가
- strncpy 뒤에 '\0' 을 직접 넣었는가
- strdup 한 줄마다 대응하는 free 가 있는가
- 수정하는 함수(strtok 등)에 리터럴을 넣지 않았는가