문자열 배열 재대입과 복사 — strncpy·snprintf·sscanf 비교
NOTE
char str[] = "test";선언 후str = "ppp";처럼 재대입이 되는지, 그리고 버퍼로 문자열을 복사할 때strncpy·snprintf·sscanf가 서로 같은 의미인지를 정리한다.
0. 배경
두 가지 질문을 합쳐서 정리한다.
char str[] = "test";선언 후str = "ppp";로 값을 바꿀 수 있나?char buffer[10];에str을 복사할 때strncpy/snprintf/sscanf셋이 같은 동작인가?
1. 배열 이름은 재대입할 수 없다
char str[] = "test"; /* str은 5바이트 배열('t','e','s','t','\0') — "초기화" 시점에 복사됨 */
str = "ppp"; /* 컴파일 에러: assignment to expression with array type */char str[] = "test";는 초기화(initialization)다. 컴파일러가str이라는 배열 메모리를 만들고 그 자리에 문자들을 복사해 넣는다.str = "ppp";는 대입(assignment)인데, 배열 이름은 “수정 가능한 lvalue”가 아니다 — 배열은 애초에 “어딘가를 가리키는” 게 아니라 그 메모리 자리 자체가 데이터이므로, 배열 전체를 다른 곳으로 재대입하는 문법 자체가 없다.- 값을 바꾸려면 배열 안의 내용을 덮어써야 한다.
strcpy(str, "ppp"); /* OK — str이 "ppp\0"(4바이트)를 담을 만큼 큼(5바이트) */
char *str = "test";였다면 얘기가 다르다포인터로 선언했다면
str = "ppp";는 문법적으로 된다. 다만 이건str이 가리키는 대상을 문자열 리터럴"test"에서"ppp"로 바꿔치기하는 것뿐이지, 원래"test"리터럴의 내용을 고치는 게 아니다. 문자열 리터럴은 읽기 전용 메모리에 있어서str[0] = 'x';처럼 내용을 직접 고치려 하면 런타임에 크래시(미정의 동작)가 난다.char str[](배열)과char *str(포인터)는 선언문 모양은 비슷해도 메모리 모델이 완전히 다르다는 점이 여기서 갈린다.
2. buffer로 복사하기: strncpy vs snprintf vs sscanf
2.1 strncpy — 파라미터 순서에 주의
char *strncpy(char *dest, const char *src, size_t n);char str[] = "test";
char buffer[10];
strncpy(buffer, str, sizeof(buffer) - 1); /* 올바른 순서: dest, src, n */
buffer[sizeof(buffer) - 1] = '\0'; /* strncpy는 n을 다 썼을 때 널 종료를 보장 안 함 — 직접 채워주는 게 관용구 */질문에 적힌 strncpy(buffer, sizeof(buffer), str)는 인자 순서가 실제 시그니처와 어긋난다.
- 2번째 자리는
src(문자열 포인터), 3번째 자리는n(개수)이어야 하는데, 정수(sizeof(buffer))와 포인터(str)의 자리가 뒤바뀌어 있다. 이 상태면 컴파일러가 “포인터-정수형 불일치” 경고를 내고, 실행 시 정수를 주소로 취급해 엉뚱한 메모리를 읽어 크래시 나거나 미정의 동작이 된다. n은 “복사할 최대 바이트 수”일 뿐 버퍼 크기를 자동으로 맞춰주지 않는다.src가n바이트 이상이면 널 종료 문자가 안 붙는다 — 그래서sizeof(buffer) - 1만큼만 복사하고 마지막 칸을 직접'\0'으로 채우는 패턴이 필요하다.
2.2 snprintf — 널 종료를 항상 보장, 실무에서 더 선호
snprintf(buffer, sizeof(buffer), "%s", str);- [C] snprintf와 sscanf — 문자열 조립과 파싱의 차이에서 정리했듯,
snprintf는size를 넘으면 잘라내되(size가 0이 아닌 한) 항상 널 종료를 보장한다. str이buffer보다 짧거나 같으면strncpy(buffer, str, sizeof(buffer)-1); buffer[sizeof(buffer)-1]='\0';와 결과가 동일하다. 다만snprintf가 널 종료를 자동으로 챙겨줘서 실수할 여지가 적다 — 단순 복사 용도면snprintf가 더 안전한 선택.
2.3 sscanf — “복사”가 아니라 “파싱”이라 의미가 다르다
sscanf(str, "%s", buffer); /* 폭 제한 없음 — 위험 */
sscanf(str, "%9s", buffer); /* buffer[10]이면 최대 9자 + 널 = 10, 안전 */%s는 공백 문자(스페이스·탭·개행) 전까지만 읽는다.str에 공백이 없으면("test") 결과가 전체 복사처럼 보이지만, 이건 우연히 같아 보이는 것뿐 의미가 같은 게 아니다.str이"hello world"였다면buffer엔"hello"만 들어가고 나머지는 버려진다.- 필드 폭(
%9s)을 안 주면buffer크기와 무관하게 얼마든지 길게 써버릴 수 있어 오버플로우 위험이 있다. - 결론: 순수하게 문자열 전체를 복사할 목적이면
sscanf는 맞는 도구가 아니다.sscanf는 “포맷에 맞춰 구조가 있는 문자열을 분해”하는 용도지, 통짜 복사용이 아니다.
3. 요약
| 방법 | 적합한 용도 | 널 종료 | 공백 처리 |
|---|---|---|---|
strncpy(dest, src, n) | 단순 복사, 직접 크기 제어 | n을 다 썼으면 보장 안 됨 → 직접 처리 필요 | 공백 포함 그대로 복사 |
snprintf(dest, size, "%s", src) | 단순 복사(권장) | 항상 보장 | 공백 포함 그대로 복사 |
sscanf(src, "%s", dest) | 공백으로 구분된 토큰 하나만 추출 | 필드 폭 안 주면 보장 안 됨(오버플로우 위험) | 첫 공백에서 끊김 |