C-style 문자열
C++을 처음 배울 때, 대부분 아래와 같은 예문을 보게 됩니다.
#include <iostream>
int main(){
std::cout << "Hello, world!"; // C-style 문자열
return 0;
}
그리고, 이 예문에 사용된 "Hello, world!"가 C-style 문자열입니다.
이것을 좀 더 정확하게 정의하자면,
C-style 문자열은 문자열 타입을 가진 리터럴( literal )이다
라고 할 수 있습니다.
여기서, 문자열 타입이란 문자 배열( char array )을 말하고, 리터럴( literal )은 값을 직접 나타내는 프로그램 요소를 가리키는 단어입니다.
아래의 예제들을 통해, 이러한 리터럴의 개념을 알 수 있습니다.
#include <cmath>
int main(){
int variable = 10; // integer literal
const int answer = 42; // integer literal
double d = std::sin(108.87); // floating point literal passed to sin function
bool b = true; // boolean literal
char c = 'A'; // char literal
int* null_pointer = nullptr; // null pointer literal
}
첫 번째 줄의 10이 int 타입 리터럴이고, variable은 int 타입 변수입니다.
마찬가지로, 'A'가 char 타입 리터럴이고, c는 char 타입 변수입니다.
그리고, 마지막 줄의, 가리키는 객체가 없는 null 포인터라는 것을 의미하는 nullptr 리터럴도 있습니다.
이렇게, C++에서 말하는 리터럴은 타입이 정해져 있는 값이라고 할 수 있습니다.
그리고, 이러한 리터럴은, 위의 변수 variable과는 다르게, 메모리에 저장되지 않습니다.
● 그런데, 가끔 기존의 코드를 보면, 아래와 같은 문장을 만날 때가 있습니다.
#include <iostream>
#include <cstring>
int main(){
char str[] = "Apple3"; // C-style 문자열을 배열 변수에 저장
const char* pstr1 = str; // 배열 변수의 주소를 저장
const char* pstr2 = "Apple4"; // ??
int ret = strcmp(pstr1, pstr2);
// ...
return 0;
}
우선, 위의 첫 번째 문장은 합법입니다.
char 배열 타입의 문자열 리터럴 "Apple3"을 char 배열 타입의 변수 str에 저장하는 문장이니까요.
그리고, 첫 번째 포인터 pstr1을 초기화하는 문장도 자연스럽습니다.
컴파일러가 문자열 변수 str의 주소를 구하기 위해 암시적으로 & 연산사를 호출하고, 이 호출로 구해진 배열의 주소를 pstr1에 저장하고 있으니까요.
그런데, 두 번째 포인터 pstr2를 초기화하는 문장에서, 왜 컴파일러는 오류를 발생시키지 않았을까요?
다음과 같이, 리터럴 10으로부터 포인터 변수 pInt2를 초기화할 때는 오류가 발생하는데 말입니다.
int A = 10;
int* pInt1 = &A; // ok
int* pInt2 = &10; // int 타입 리터럴 10의 주소 대입. error !!
C-style 문자열의 두 가지 특징
● 첫 번째, C-style 문자열의 타입은 char 배열로, 이 배열은, 마지막 원소로, 암시적인 null 종료 문자( null terminator, '\0' )를 갖고 있습니다.
그리고, C와 C++ 에선 이 종료 문자를 통해, 문자열이 종료되었다는 것을 알 수 있습니다.
그래서, 다음의 문자열 "Hello, world!"의 크기는 13이 아니라, 14입니다.
#include <iostream>
#include <cstring>
using namespace std;
int main(){
//char str[13] = "Hello, world!"; // 배열 크기가 작으므로 오류 !!
char str[14] = "Hello, world!"; // null 종료 문자를 위해 공간이 필요
int len = strlen( str);
cout << "string length: " << len << endl;
return 0;
}
▼출력
string length: 13
위의 결과가 13으로 나온 이유는 strlen 함수가 '\0'을 제외한 문자의 개수를 반환하기 때문입니다.
하지만, 배열 크기를 13으로 하면, 컴파일러는 오류를 발생해서, 배열 크기로 14를 사용해야 한다고 알려줍니다.
● 두 번째, C-style 문자열은, 다른 종류의 리터럴( literal )과 달리, 프로그램의 시작부터 종료까지 존재하는 상수 객체입니다.
즉, 이 문자열은, 변수와 마찬가지로, 메모리 주소를 가진 객체라는 뜻이 됩니다.
( 좀 더 정확히는, 읽기 전용 메모리 블록에 저장됩니다. )
그래서, 다음의 pstr2를 초기화하는 문장이 문제를 발생하지 않은 것입니다.
( 아래서 얘기하겠지만, 이 과정에서 컴파일러는 char 배열 타입을 암시적으로 char* 타입으로 변환합니다. )
int main(){
char str[] = "Apple3";
char* pstr1 = str; // 객체의 주소를 저장
pstr1[5] = '2'; // Apple2로 변경 가능
char* pstr2 = "Apple4"; // 상수 객체의 주소를 저장
//pstr2[5] = '1'; // 상수 객체의 값을 바꿀 수 없음. error !!
}
하지만, pstr2를 char* 타입의 포인터로 선언해 컴파일이 되었다 하더라도, 상수 객체인 "Apple4" 문자열의 값을 변경할 수는 없습니다.
만약, pstr2 포인터를 통해서, 이 문자열을 수정하려고 하면 예외가 발생됩니다.
따라서, C-style 문자열을 포인터를 통해 사용하고 싶으면, const char* 타입으로 선언하는 것이 안전합니다.
( 컴파일러 옵션에 따라, 아예 const char* 타입의 포인터로만 사용해야 하는 경우도 있습니다. )
● 참고로, 다른 타입의 문자열 리터럴도 있습니다.
using namespace std::string_literals; // for string 리터럴
using namespace std::string_view_literals; // for string_view 리터럴
int main(){
std::string cstr = "Apple5"s; // std::string 타입의 문자열 리터럴
// C++ 17 이후, std::string_view 타입의 문자열 리터럴
std::string_view cstrView = "Apples6"sv;
}
위의 "Apple5"s는 std::string 타입의 리터럴입니다.
그리고, "Apple6"sv는 C++17에서 도입된 std::string_view 타입의 리터럴입니다.
( 이때, 접미어인 s와 sv에는 소문자를 사용해야 합니다. )
[C++] 문자열을 읽기 전용으로 참조하는 std::string_view
std::string_viewstd::string_view는 문자열을 사용하면서 발생하는, 무거운 복사 과정을 줄이고자 만든 클래스입니다. C++ 17 이상의 버전에서 사용할 수 있는, 이 클래스는 실제 원본 문자열을 읽기 전용
codingbonfire.tistory.com
이 두 리터럴( literal ) 객체는 임시적인 객체로, C-style 문자열과 달리, 사용된 문장을 벗어나면 바로 파괴됩니다.
C-style 문자열을 함수에 전달
아래는 함수에 전달한 C-style 문자열을 화면에 출력하는 코드입니다.
void print( char* pstr ){ // char* 타입의 매개 변수
std::cout << pstr << '\n';
}
int main(){
print( "Apple4" );
}
그런데, 위의 "Apple4"의 타입은 char 배열인데, 왜 이 C-style 문자열을 전달받는 함수 print는 char* 타입을 매개변수를 사용해야 하는 걸까요?
C++에서 char[2]와 char[3]은 서로 다른 타입입니다.
그렇기 때문에, 모든 크기의 char 배열 타입을 받아들일 수 있는 함수를 만들 수 없습니다.
그렇다고, "Apple4" 문자열만을 출력하기 위해, 그때그때 print( char pstr[7] ) 같은 함수를 작성할 수도 없는 노릇입니다.
그래서, C++은 이 문제를 해결하기 위해서, char 배열 타입을 char* 타입으로 변환하기로 결정했습니다.
이러한 변환을 배열의 붕괴( array decay )라고 합니다.
그래서, 다음과 같이 char 배열 타입의 매개 변수를 갖는 함수를 정의해도, 컴파일러는 이 매개변수의 타입을 char* 타입으로 인식합니다.
void print( char pstr[7] ){ // char pstr[]과 동일
std::cout << pstr << '\n';
}
위의 매개변수 char pstr[7]는 char pstr[]과 동일하고, 이때 배열의 크기를 나타내는 숫자 7은 무시되는 방식으로 처리됩니다.
그리고, char pstr[]도 컴파일러는 char* 타입으로 인식하기 때문에, 위는 함수는 결국 다음 함수와 동일합니다.
void print( char* pstr ){ // char* 타입 매개 변수
std::cout << pstr << '\n';
}
그런데, 이러한 배열의 붕괴는 배열의 크기 정보까지 제거하는 문제점을 만들었기 때문에, 이 문제를 해결하기 위해서 C-style 문자열의 끝에 null 종료 문자가 필요한 것입니다.
● 하지만, 위에서 얘기한 std::string_view 클래스를 사용하면, 이러한 C-style 문자열을 편리하게 사용할 수 있습니다.
#include <string_view>
void print( std::string_view sv){
size_t sz = sv.size(); // 문자열의 크기
std::cout << sv << " : " << sz << '\n';
}
이 클래스의 객체는, C-style 문자열을 함수에 전달할 때, 문자열이 복사되는 과정이 없을 뿐만 아니라, 문자열의 크기를 멤버 함수 size를 통해서 쉽게 구할 수 있습니다.
정리
- C-style 문자열은
const char 배열타입의 리터럴( literal )입니다. - C-style 문자열은, 다른 리터럴과 달리, 프로그램 시작부터 종료 시까지 유지되는 객체입니다.
- 함수에 전달 시, C-style 문자열은
const char*타입으로 붕괴( decay )됩니다. - 이러한 C-style 문자열을 다룰 때는
std::string_view클래스를 사용하는 것이 좋습니다.
이 글과 관련 있는 글들
null 포인터와 nullptr 리터럴( literal )
'C++' 카테고리의 다른 글
| [C++] 기존의 객체를 복사할 때 호출되는 복사 생성자( copy constructor ) (0) | 2025.03.30 |
|---|---|
| [C++] null 포인터와 nullptr 리터럴( literal ) (0) | 2025.03.26 |
| [C++] 초기화( initialization )의 세 가지 방법 (0) | 2025.03.23 |
| [C++] 변환 생성자와 암시적인 변환을 금지하는 explicit (0) | 2025.03.21 |
| [C++] C-style 타입 변환과 C++의 static_cast (0) | 2025.03.21 |
