6. C/C++의 문자열 리터럴과 문자 배열
1. 문자열 리터럴
- 문자열 리터럴의 경우
const char *
로 표현되며, 읽기 전용 메모리에 저장되어 포인터를 통해 읽어올 수 있다.
#include <stdio.h>
#include <string.h>
int main()
{
const char *s1 = "Hello";
char *s2 = "Hello";
printf("char 타입의 크기: %lu\n\n", sizeof(char));
printf("s1의 크기: %lu\n", sizeof(s1));
printf("s1의 길이: %lu\n", strlen(s1));
printf("s1이 가리키는 메모리 주소: %p\n\n", s1);
printf("s2의 크기: %lu\n", sizeof(s2));
printf("s2의 길이: %lu\n", strlen(s2));
printf("s2이 가리키는 메모리 주소: %p\n\n", s2);
return 0;
}
// char 타입의 크기: 1
//
// s1의 크기: 8
// s1의 길이: 5
// s1이 가리키는 메모리 주소: 0x100003ee8
//
// s2의 크기: 8
// s2의 길이: 5
// s2이 가리키는 메모리 주소: 0x100003ee8
- 이때
s1
과 s2
는 const
키워드의 유무 차이만 있을 뿐 여전히 수정이 불가능한 값이다.
s1
과 s2
는 포인터이기 때문에 sizeof
의 결과가 문자열 리터럴의 길이가 아닌 8바이트로 나오는 것을 확인할 수 있다.
- 대부분의 컴파일러는 문자열 리터럴을 Release 빌드에서 같은 주소 값을 가지도록 최적화된다.
2. 문자 배열
- C/C++에서는 문자열 리터럴과 문자로 이루어진 배열(문자 배열)은 다르게 동작한다.
#include <stdio.h>
#include <string.h>
int main()
{
char *s1 = "Hello";
char s2[] = "Hello";
char s3[] = {'H', 'e', 'l', 'l', 'o', '\0'};
char s4[] = {'H', 'e', 'l', 'l', 'o'};
printf("s1의 크기: %lu\n", sizeof(s1));
printf("s1의 길이: %lu\n", strlen(s1));
printf("s1이 가리키는 메모리 주소: %p\n\n", s1);
printf("s2의 크기: %lu\n", sizeof(s2));
printf("s2의 길이: %lu\n", strlen(s2));
printf("s2이 가리키는 메모리 주소: %p\n\n", s2);
printf("s3의 크기: %lu\n", sizeof(s3));
printf("s3의 길이: %lu\n", strlen(s3));
printf("s3이 가리키는 메모리 주소: %p\n\n", s3);
printf("s4의 크기: %lu\n", sizeof(s4));
printf("s4의 길이: %lu\n", strlen(s4));
printf("s4이 가리키는 메모리 주소: %p\n\n", s4);
return 0;
}
// s1의 크기: 8
// s1의 길이: 5
// s1이 가리키는 메모리 주소: 0x100003e68
//
// s2의 크기: 6
// s2의 길이: 5
// s2이 가리키는 메모리 주소: 0x3041272ba
//
// s3의 크기: 6
// s3의 길이: 5
// s3이 가리키는 메모리 주소: 0x3041272b4
//
// s4의 크기: 5
// s4의 길이: 10
// s4이 가리키는 메모리 주소: 0x3041272af
s1
은 읽기 전용 메모리에 저장된 문자열 리터럴의 시작 위치를 가리키는 포인터이다.
- 포인터이기 때문에
sizeof
의 결과가 문자열 리터럴의 길이가 아닌 8바이트로 나오는 것을 확인할 수 있다.
s2
, s3
, s4
는 스택에 저장된 배열이다.
- 포인터와 다르게 배열은
sizeof
의 결과가 배열의 크기(배열 요소의 개수)로 나온다.
- 여기서
s2
의 "Hello"
는 문자열 리터럴처럼 보이지만 사실 컴파일러에 의해 {'H', 'e', 'l', 'l', 'o', '\0'}
형태로 치환된다.
- 즉,
s2
와 s3
는 같은 구문이다.
- 그리고 문자 배열을 사용할 때 주의할 점은
'\0'
이 마지막 요소가 되어야 한다는 것이다.
s4
를 보면 '\0'
이 마지막 요소가 아니기 때문에 strlen
이 올바른 문자열 길이를 출력하지 못한다는 것을 확인할 수 있다.
3. 문자열 리터럴과 동적 할당된 문자 배열
- 동적으로 메모리를 할당한 후 포인터로 동적 할당된 메모리 주소를 가리키도록 해보자.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char *s1 = "Hello";
char *s2 = malloc(sizeof(char) * 6);
strcpy(s2, "Hello");
printf("s1의 크기: %lu\n", sizeof(s1));
printf("s1의 길이: %lu\n", strlen(s1));
printf("s1이 가리키는 메모리 주소: %p\n\n", s1);
printf("s2의 크기: %lu\n", sizeof(s2));
printf("s2의 길이: %lu\n", strlen(s2));
printf("s2이 가리키는 메모리 주소: %p\n\n", s2);
free(s2);
return 0;
}
// s1의 크기: 8
// s1의 길이: 5
// s1이 가리키는 메모리 주소: 0x100003f0e
//
// s2의 크기: 8
// s2의 길이: 5
// s2이 가리키는 메모리 주소: 0x1090040e0
- 문자열 리터럴과 동적 할당된 문자 배열은 모두 첫 번째 문자의 주소를 가리키는 포인터로 표현된다.
s1
과 s2
는 모두 포인터이기 때문에 sizeof
의 결과가 문자열 리터럴의 길이가 아닌 8바이트로 나오는 것을 확인할 수 있다.
#include <stdio.h>
int main()
{
char *s1 = "Hello";
s1[4] = '!';
printf("s1: %s", s1);
return 0;
}
s1
은 문자열 리터럴 "Hello"
의 메모리 주소를 가리키고 있다.
- 이때
s1
의 마지막 문자를 '!'
로 변경하려고 하면 에러가 발생한다.
- 문자열 리터럴은 읽기 전용 메모리 저장되어 있기 때문에 접근하여 수정할 수 없다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char *s2 = malloc(sizeof(char) * 6);
strcpy(s2, "Hello");
s2[4] = '!';
printf("s2: %s\n", s2);
return 0;
}
// s2: Hell!
s2
은 동적 할당된 메모리 주소를 가리키고 있다.
- 이때
strcpy
로 "Hello"
를 저장한 후 마지막 문자를 '!'
로 변경하니 문제가 없다.
- 동적 할당된 문자 배열은 힙에 저장되어 있기 때문에 접근하여 수정할 수 있다.
#include <stdio.h>
#include <string.h>
int main()
{
char *s1 = "Hello";
printf("s1의 크기: %lu\n", sizeof(s1));
printf("s1의 길이: %lu\n", strlen(s1));
printf("s1이 가리키는 메모리 주소: %p\n\n", s1);
s1 = "Bye";
printf("s1의 크기: %lu\n", sizeof(s1));
printf("s1의 길이: %lu\n", strlen(s1));
printf("s1이 가리키는 메모리 주소: %p\n\n", s1);
return 0;
}
// s1의 크기: 8
// s1의 길이: 5
// s1이 가리키는 메모리 주소: 0x100003f5c
//
// s1의 크기: 8
// s1의 길이: 3
// s1이 가리키는 메모리 주소: 0x100003fb2
- 물론 위의 코드처럼
s1
이 가리키는 문자열 리터럴의 메모리 주소를 다른 문자열 리터럴의 메모리 주소로 변경하는 것은 가능하다.
References