KoreanFoodie's Study
C++ 기초 개념 10-4 : C++ 문자열의 모든 것 (string과 string_view) 본문
C++ 기초 개념 10-4 : C++ 문자열의 모든 것 (string과 string_view)
GoldGiver 2022. 4. 18. 17:07
모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!
std::string
std::string 이란 무엇일까?
사실 std::string 은 basic_string이라는 클래스 템플릿의 인스턴스화 버전이다. basic_string definition 은 다음과 같다 :
template <class CharT, class Traits = std::char_traits<CharT>,
class Allocator = std::allocator<CharT> >
class basic_string;
Traits 는 주어진 CharT 문자들에 대해 기본적인 문자열 연산들을 정의해 놓은 클래스를 의미한다. 여기서, Traits를 바꾸면 정렬 순서를 바꿀 수 있는 등 여러가지 조작을 할 수 있다!
다시 말해, basic_string 에 정의된 문자열 연산들은 사실 전부 Traits 의 기본적인 문자열 연산들을 가지고 정의되어 있다. 덕분에 문자열들을 어떻게 보관하는지에 대한 로직과 문자열들을 어떻게 연산하는지에 대한 로직을 분리시킬 수 있다. 전자는 basic_string 에서 해결하고, 후자는 Traits 에서 담당한다.
이렇게 로직을 분리한 이유는 basic_string 의 사용자에게 더 자유를 부여하기 위해서이다. 예를 들어, string 에서 문자열 비교시 대소 문자 구분을 하지 않는 버전을 만든다고 가정해 보자. 그렇다면 Traits 에서 문자열을 비교하는 부분만 살짝 바꿔주면 된다. Traits 에는 <string> 에 정의된 std::char_traits 클래스의 인스턴스화 버전을 전달한다.
숫자들의 순위가 알파벳보다 낮은 문자열 클래스를 만든 예시 코드를 보자.
#include <cctype>
#include <iostream>
#include <string>
// char_traits 의 모든 함수들은 static 함수 입니다.
struct my_char_traits : public std::char_traits<char> {
static int get_real_rank(char c) {
// 숫자면 순위를 엄청 떨어트린다.
if (isdigit(c)) {
return c + 256;
}
return c;
}
static bool lt(char c1, char c2) {
return get_real_rank(c1) < get_real_rank(c2);
}
static int compare(const char* s1, const char* s2, size_t n) {
while (n-- != 0) {
if (get_real_rank(*s1) < get_real_rank(*s2)) {
return -1;
}
if (get_real_rank(*s1) > get_real_rank(*s2)) {
return 1;
}
++s1;
++s2;
}
return 0;
}
};
int main() {
std::basic_string<char, my_char_traits> my_s1 = "1a";
std::basic_string<char, my_char_traits> my_s2 = "a1";
std::cout << "숫자의 우선순위가 더 낮은 문자열 : " << std::boolalpha
<< (my_s1 < my_s2) << std::endl;
std::string s1 = "1a";
std::string s2 = "a1";
std::cout << "일반 문자열 : " << std::boolalpha << (s1 < s2) << std::endl;
}
짧은 문자열 최적화 (SSO)
메모리 할당은 많은 시간을 잡아먹는다!
basic_string은 짧은 길이 문자열의 경우 따로 문자 데이터를 위한 메모리를 할당하는 대신, 그냥 객체 자체에 저장해버린다. 이것을 Short String Optimization (SSO) 라고 한다.
다음의 코드를 보자.
#include <iostream>
#include <string>
// 이와 같이 new 를 전역 함수로 정의하면 모든 new 연산자를 오버로딩 해버린다.
// (어떤 클래스의 멤버 함수로 정의하면 해당 클래스의 new 만 오버로딩됨)
void* operator new(std::size_t count) {
std::cout << count << " bytes 할당 " << std::endl;
return malloc(count);
}
int main() {
std::cout << "s1 생성 --- " << std::endl;
std::string s1 = "this is a pretty long sentence!!!";
std::cout << "s1 크기 : " << sizeof(s1) << std::endl;
std::cout << "s2 생성 --- " << std::endl;
std::string s2 = "short sentence";
std::cout << "s2 크기 : " << sizeof(s2) << std::endl;
}
위 코드의 결과값은 다음과 같다.
s1 생성 ---
34 bytes 할당
s1 크기 : 32
s2 생성 ---
s2 크기 : 32
즉, s1 의 경우 new 를 호출해 메모리를 할당했지만, s2 의 경우 32 바이트의 문자열 객체를 만들어 문자열을 바로 저장했다는 것을 알 수 있다!
리터럴 연산자(literal operator)
문자열 리터럴을 다음과 같이 정의해줄 수 있다.
auto str = "hello"s;
만약 s 를 붙이지 않으면, str 의 타입은 const char* 로 정의된다! 참고로 위의 리터럴 연산자를 사용하기 위해서는 std::literals 이라는 namespace 를 사용해 주어야 한다.
C++ 11 에 Raw String Literal 이라는 기능이 생겼다.
#include <iostream>
#include <string>
int main() {
std::string str = R"(asdfasdf
이 안에는
어떤 것들이 와도
// 이런것도 되고
#define hasldfjalskdfj
\n\n <--- Escape 안해도 됨
)";
std::cout << str;
}
만약 괄호를 넣고 싶다면 아래의 문법을 사용한다.
// Raw string 문법
R"/* delimiter */( /* 문자열 */ )/* delimiter */"
// delimiter 는 일치해야 한다! 아래 코드에서는 foo
#include <iostream>
#include <string>
int main() {
std::string str = R"foo(
)"; <-- 무시됨
)foo";
std::cout << str;
}
같은 느낌으로... R을 붙이는게 포인트! foot 대신에 어떤 단어가 와도 상관없다! 문법이 복잡하다고 느껴지면 그냥 "delimiter( 가 하나의 괄호라고 생각하면 편하다.
C++에서 한글 다루기
모든 문자들을 표현할 수 있도록 설계된 표준인 유니코드를 이용한다. 인코딩 방식은 다음과 같다 :
- UTF-8 : 문자를 최소 1부터 4바이트로 표현 (문자마다 길이가 다름)
- UTF-16 : 문자를 2 또는 4 바이트로 표현한다.
- UTF-32 : 문자를 4 바이트로 표현한다.
예시 코드를 보자.
// 1234567890 123 4 567
std::u32string u32_str = U"이건 UTF-32 문자열 입니다";
std::cout << u32_str.size() << std::endl;
// 결과값 : 17
UTF-32 는 각 글자를 각각 4바이트 씩 할당해서 저장하므로, 글자수인 17 이 나온다.
// 12 345678901 2 3456
std::string str = u8"이건 UTF-8 문자열 입니다";
std::cout << str.size() << std::endl;
// 결과값 : 32
UTF-8 의 경우 한글은 3바이트, 나머지는 1바이트로 계산해 3*8 + 1*8 = 32 가 나왔다. 이는 총 32 개의 char 가 필요하다는 뜻인데, std::string 은 인코딩에는 관심이 없고 그냥 단순하게 char 의 나열로 이루어져 있다고 생각한다. 따라서 str.size() 를 하면 문자열의 실제 길이가 아니라 char 이 몇 개가 있는지 알려준다.
따라서 str[1] 을 호출하면 "건" 이 나오는 것이 아니라, "이" 에서 UTF-8 인코딩의 두 번째 바이트 (UTF-8 인코딩에서 불가능한 값) 이 나오게 된다. 따라서 UTF-8 인코딩 문자열을 제대로 읽으려면 str[i] 를 0b1111110000 등의 바이트 범위와 비교하여 읽어야 한다.
// 1234567890 123 4 567
std::u16string u16_str = u"이건 UTF-16 문자열 입니다";
std::cout << u16_str.size() << std::endl;
// 결과값 : 17
UTF-16 인코딩은 최소 단위가 2바이트이다. 대부분의 문자들이 2바이트로 인코딩 된다! (알파벳, 한글, 한자 전부) 이모지나 상형문자는 4바이트로 인코딩된다.
UTF8-CPP 라는, C++ 에서 여러 방식으로 인코딩된 문자열을 쉽게 다룰 수 있게 도와주는 라이브러리도 있다!
string_view
만일 어떤 함수에다 문자열을 전달할 때, 읽기만 필요로 할 경우 보통 const std::string& 이나 const char* 로 받게 된다. 각각은 문제점이 있다. 예시 코드를 보자.
#include <iostream>
#include <string>
void* operator new(std::size_t count) {
std::cout << count << " bytes 할당 " << std::endl;
return malloc(count);
}
// 문자열에 "very" 라는 단어가 있으면 true 를 리턴함
bool contains_very(const std::string& str) {
return str.find("very") != std::string::npos;
}
int main() {
// 암묵적으로 std::string 객체가 불필요하게 생성된다.
std::cout << std::boolalpha << contains_very("c++ string is very easy to use")
<< std::endl;
std::cout << contains_very("c++ string is not easy to use") << std::endl;
}
위의 경우, contains_very 에서 const char* 을 인자로 넣어버려 std::string 객체가 생성되는 문제가 있다. 이를 해결하기 위해서는 const char* 을 인자로 받는 contains_very 함수를 하나 더 만들어 주어야 하는데...
string_view : 소유하지 않고 읽기만 한다!
위와 같은 문제는 string_view 가 나옴으로써 해결되었다!
// 문자열에 "very" 라는 단어가 있으면 true 를 리턴함
bool contains_very(std::string_view str) {
return str.find("very") != std::string_view::npos;
}
string_view 는 문자열을 참조해서 읽기만 하는 클래스로, string_view 가 현재 보고 있는 문자열이 소멸되면 Undefined behavior 가 발생한다.
하지만 소유하지 않는 특성 덕분에 string_view 객체 생성 시에 메모리할당이 불필요하다. 읽고 있는 문자열의 시작 주소값만 복사하면 되기 때문이다!
#include <iostream>
#include <string>
void* operator new(std::size_t count) {
std::cout << count << " bytes 할당 " << std::endl;
return malloc(count);
}
int main() {
std::cout << "string -----" << std::endl;
std::string s = "sometimes string is very slow";
std::cout << "--------------------" << std::endl;
std::cout << s.substr(0, 20) << std::endl << std::endl;
std::cout << "string_view -----" << std::endl;
std::string_view sv = s;
std::cout << "--------------------" << std::endl;
std::cout << sv.substr(0, 20) << std::endl;
}
위의 코드를 돌리면, s 는 메모리를 할당하지만, sv 는 메모리를 할당하지 않는다는 것을 확인할 수 있다.
다만 string_view 을 사용하기 위해서는, 참조하는 원본 string 이 삭제되지 않아야 한다.
#include <iostream>
#include <string>
std::string_view return_sv() {
std::string s = "this is a string";
std::string_view sv = s;
return sv;
}
int main() {
std::string_view sv = return_sv(); // <- sv 가 가리키는 s 는 이미 소멸됨!
// Undefined behavior!!!!
std::cout << sv << std::endl;
}
'Tutorials > C++ : Beginner' 카테고리의 다른 글
C++ 기초 개념 12-1 : 우측값, 이동 생성자와 우측값 레퍼런스 (0) | 2022.04.19 |
---|---|
C++ 기초 개념 11 : 예외처리 (0) | 2022.04.19 |
C++ 기초 개념 10-3 : STL 알고리즘 (STL algorithm) (0) | 2022.04.18 |
C++ 기초 개념 10-2 : 맵(map), 셋(set), unordered_map, unordered-set (0) | 2022.03.18 |
C++ 기초 개념 10-1 : 벡터(vector), 리스트(list), 덱(deque) (0) | 2022.03.18 |