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 )