- C언어를 사용하던 시절에는 단순히 널로 끝나는 문자 배열로 스트링을 표현했다. 하지만 이렇게 하면 버퍼 오버플로를 비롯한 다양한 문제 때문에 보안 취약점이 드러날 수 있다.
- C++ 표준 라이브러리는 이러한 문제를 방지하기 위해 보다 안전하고 사용하기 쉬운 std::string 클래스를 제공한다.
2.1 동적 스트링 172
- 스트링을 주요 객체로 제공하는 프로그래밍 언어를 보면 대체로 스트링의 크기를 임의로 확장하거나, 서브스트링(부분 문자열)을 추출하거나 교체하는 것과 같은 고급 기능을 제공한다.
- 반면 C와 같은 언어는 스트링을 부가 기능처럼 취급한다. 그러므로 스트링을 언어의 정식 데이터 타입으로 제공하지 않고, 단순히 고정된 크기의 바이트 배열로 처리했다.
- C언어의 스트링 라이브러리는 기본적인 함수로만 구성되어 있고, 경계값 검사기능조차 없다. 반면 C++는 스트링을 핵심 데이터 타입으로 제공한다.
2.1.1 C 스타일 스트링
- C 언어는 스트링을 문자 배열로 표현했다.
- 스트링의 마지막에 널 문자를 붙여서 스트링이 끝났음을 표현했다. 널 문자에 대한 공식 기호는 NUL이다.
- C 스트링을 다룰 때 \0 문자를 담을 공간을 깜빡하고 할당하지 않는 실수를 저지르기 쉽다.
- C++에서 제공하는 스트링이 훨씬 뛰어나지만, C언어에서 스트링을 다루는 방법도 알아둘 필요가 있다. 아직도 C 스타일 스트링을 쓰는 C++ 프로그램이 많기 때문이다.
- C++는 C 언어에서 사용하던 스트링 연산에 대한 함수도 제공한다. 이러한 함수는 <cstring> 헤더 파일에 정의되어 있다. 이러한 함수는 대체로 메모리 할당 기능을 제공하지 않는다.
- strlen() 함수는 스트링을 저장하는 데 필요한 메모리 크기가 아니라 스트링 길이를 리턴한다. ( \0 포함X )
- 따라서 스트링을 저장하는 데 필요한 메모리를 제대로 할당하려면 문자 수에 1을 더한 크기로 지정해야 한다.
- C와 C++에서 제공하는 sizeof() 연산자는 데이터 타입이나 변수의 크기를 구하는 데 사용된다.
- C 스트링에 적용할 때는 sizeof()와 strlen()의 결과가 전혀 다르다. 따라서 스트링의 크기를 구할 때는 절대로 sizeof()를 사용하면 안 된다. sizeof()의 리턴값은 C스트링이 저장된 방식에 따라 다르기 때문이다.
- sizeof()는 \0을 포함하여 그 스트링에 대해 실제로 할당된 메모리 크기를 리턴한다.
- C스타일 스트링을 char*로 저장했다면 sizeof()는 포인터의 크기를 리턴한다.
- 마이크로소프트 비주얼 스튜디오에서 C스타일 스트링 함수를 사용하면 컴파일러에서 보안 관련 경고나 이 함수가 폐기되었다는 에러 메시지가 출력될 수 있다.
- 이러한 경고가 나오지 않게 하려면 strcpy_s()나 strcat_s()와 같은 다른 C 표준 라이브러리 함수를 사용하면 된다.
- 가장 좋은 방법은 C++ 표준 라이브러리에서 제공하는 std::string 클래스를 사용하는 것이다.
2.1.2 스트링 리터럴
- "hello" 처럼 변수에 담지 않고 곧바로 값으로 표현한 스트링을 스트링 리터럴이라 부른다.
- 스트링 리터럴은 내부적으로 메모리의 읽기 전용 영역에 저장된다.
- 그러므로 컴파일러는 같은 스트링 리터럴이 코드에 여러 번 나오면 이에 대한 레퍼런스를 재활용하는 방식으로 메모리를 절약한다. 다시 말해 코드에서 "hello"란 스트링 리터럴을 500번 넘게 작성해도 컴파일러는 hello에 대한 실제 메모리 공간은 딱 하나만 할당한다. 이를 리터럴 풀링이라 부른다.
- 스트링 리터럴을 변수에 대입할 수는 있지만 스트링 리터럴은 메모리의 읽기기 전용 영역뿐만 아니라 동일한 리터럴을 여러 곳에서 공유할 수 있기 때문에 변수에 저장하면 위험하다.
- const 없이 char* 타입 변수에 스트링 리터럴을 대입하더라도 그 값을 변경하지 않는 한 프로그램 실행에는 아무런 문제가 없다. 스트링 리터럴을 수정하는 동작에 대해서는 명확히 정의되어 잇지 않다. 따라서 프로그램이 갑자기 뻗어버릴 수도 있고, 실행은 되지만 겉으로 드러나지 않는 부작용이 발생할 수도 있고, 수정 작업을 그냥 무시할 수도 있고, 의도한 대로 작동할 수도 있다. 구체적인 동작은 컴파일러마다 다르다.
- 스트링 리터럴을 참조할 때는 const 문자에 대한 포인터를 사용하는 것이 훨씬 안전하다. 컴파일러는 읽기 전용 메모리에 쓰기 작업을 실행하는 것을 에러를 통해 걸러낼 수 있다.
- 문자 배열(char[])의 초깃값을 설정할 때도 스트링 리터럴을 사용한다. 이때 컴파일러는 주어진 스트링을 충분히 담을 정도로 큰 배열을 생성한 뒤 여기에 실제 스트링값을 복사한다. 컴파일러는 이렇게 만든 스트링 리터럴을 읽기 전용 메모리에 넣지 않으며 재활용하지도 않는다.
+ 로 스트링 리터럴
- 로 스트링 리터럴은 여러 줄에 걸쳐 작성하는 스트링 리터럴으로서, 그 안에 나오는 인용 부호를 이스케이프 시퀀스로 표현할 필요가 없고, \t나 \n 같은 이스케이프 시퀀스를 일반 텍스트로 취급한다.
- 로 스트링 리터럴에서는 이스케이프 시퀀스를 무시한다. 무시하고 문자 그대로 출력한다.
const char* str { "Hello "World"!" }; // 에러 발생
const char* str { "Hello \"World\"!" }; // 이스케이프 시퀀스 사용
const char* str { R"(Hello "World"!)" }; // 로 스트링 리터럴 사용, 이스케이프 시퀀스 X
// --------------------------------------------------------------------------------
const char* str { "Line 1\nLine 2" }; // 이스케이프 시퀀스 사용
const char* str { "Line 1
Line 2" }; // 로 스트링 리터럴 사용, 이스케이프 시퀀스 X
- 로 스트링 리터럴은 )로 끝나기 때문에 이 구문을 사용하는 스트링 안에 )를 넣을 수 없다.
- )문자를 추가하려면 확장 로 스트링 리터럴 구문으로 표현해야 한다.
R"d-char-sequence(r-char-sequence)d-char-sequence"
- r-char-sequence에 해당하는 부분이 실제 로 스트링이다.
- d-char-sequence라고 표현한 부분은 구분자 시퀀스로서, 반드시 로 스트링 리터럴의 시작과 끝에 똑같이 나와야 한다.
- 이 구분자 시퀀스는 최대 16개 문자를 가질 수 있다. 이때 구분자 시퀀스는 로스트링 리터럴 안에 나오지 않는 값으로 지정해야 한다.
- 로 스트링 리터럴을 사용하면 데이터베이스 쿼리 스트링이나 정규 표현식, 파일 경로 등을 쉽게 표현할 수 있다. 정규 표현식은 21장에서 자세히 설명한다.
2.1.3 C++ std::string 클래스
+ C스타일 스트링의 문제점
- 장점
- 간단하다. 내부적으로 기본 문자 타입과 배열 구조체로 처리한다.
- 가볍다. 제대로 사용하면 메모리를 꼭 필요한 만큼만 사용한다.
- 로우 레벨이다. 따라서 메모리의 실제 상태를 조작하거나 복사하기 쉽다.
- C 프로그래머에게 익숙하다. 새로 배울 필요가 없다.
- 단점
- 스트링 데이터 타입에 대한 고차원 기능을 구현하려면 상당한 노력이 필요하다.
- 찾기 힘든 메모리 버그가 발생하기 쉽다.
- C++의 객체지향적인 특성을 제대로 활용하지 못한다.
- 프로그래머가 내부 표현 방식을 이해해야 한다.
- C++의 string은 C 스타일의 스트링이 가진 장점은 그대로 남기고 단점은 없앴다.
+ string 클래스 사용법
- string은 실제로는 클래슺디만 마치 기본타입인 것처럼 사용한다. 그러므로 코드를 작성할 때 기본 타입처럼 취급하면 된다.
- C++ string의 연산자 오버로딩 덕분에 C스트링보다 훨씬 쓰기편하다.
+ 스트링 비교
- C 스트링은 ==연산자로 비교할 수 없다는 단점이 있다. 스트링의 내용이 아닌 포인터값을 비교하게 된다.
- C언어에서 스트링을 비교하려면 strcmp(a, b) 함수를 사용해야 한다.
- C++에서 제공하는 string에서는 ==, !=, <와 같은 연산자를 스트링에 적용할 수 있도록 오버로딩되어 있다. 물론 C처럼 각각의 문자를 []로 접근할 수도 있다.
- C++ string 클래스는 compare() 메서드도 제공한다. 이 메서드는 strcmp()와 동작 및 리턴 타입이 비슷하다.
- 리턴 값의 의미를 정확히 알아야 한다. 이 리턴값은 정숫값에 불과하므로 그 의미를 까먹기 쉽다.
- compare()는 값이 같은 경우에는 0을 리턴하고 다른 경우에는 0이 아닌 값을 리턴한다. 두 스트링이 같은지 확인하고 싶다면 compare()가 아닌 ==을 사용해야 한다.
- C++20부터는 3방향 비교 연산자를 사용하도록 개선되었다. string 클래스는 이 연산자를 정식으로 지원한다.
auto result { a <=> b };
if (is_lt(result)) { cout << "less" endl; }
if (is_gt(result)) { cout << "greater" endl; }
if (is_eq(result)) { cout << "equal" endl; }
+ 메모리 처리
- 연산자 오버로딩으로 string을 확장해도 메모리 관련 작업은 string 클래스가 알아서 처리해준다는 것을 알 수 있다.
- 따라서 메모리 오버런이 발생할 걱정을 할 필요가 없다.
- string 객체는 모두 스택 변수로 생성되기 때문이다.
- string 클래스를 사용하면 메모리를 할당하거나 크기를 조절할 일이 상당히 많긴 하지만 string 객체가 스코프를 벗어나자마자 여기에 할당된 메모리를 string 소멸자가 모두 정리한다.
+ C 스트링과 호환
- string 클래스에서 제공하는 c_str() 메서드를 사용하면 C언어에 대한 호환성을 보장할 수 있다.
- 이 메서드는 C 스트링을 표현하는 const 포인터를 리턴한다. 따라서 현재 string에 담긴 내용을 정확히 사용하려면 이 메서드를 호출한 직후에 리턴된 포인터를 활용하도록 코드를 작성하는 것이 좋다.
- 또한 함수 안에 생성된 스택 기반 string 객체에 대해서는 c_str()을 호출한 결과를 절대로 리턴값으로 전달하면 안 된다.
- string에서 제공하는 data() 메서드는 C++14까지만 해도 c_str()처럼 const char* 타입으로 값을 리턴했다.
- 그러나 C++17부터는 비 const 스트링에 대해 호출하면 char*를 리턴하도록 변경되었다.
+ 스트링 연산
- string 클래스는 다음과 같은 몇 가지 연산을 추가로 제공한다.
- substr (pos, len) : 인수로 지정한 시작 위치와 길이에 맞는 서브스트링을 리턴한다.
- find (str) : 인수로 지정한 서브스트링이 있는 지점을 리턴한다. 없다는 string::npos를 리턴한다.
- replace (pos, len, str) : 스트링에서 인수로 지정한 위치와 길이에 해당하는 부분을 str로 지정한 값으로 교체한다.
- starts_with(str)/ends_with(str) : 인수로 지정한 서브스트링으로 시작하거나 끝나면 true를 리턴한다.
- C++20부터 std::string은 constexpr 클래스이다. 즉 string은 컴파일 시간에 연산을 수행하는 데 사용할 수 있으며, constexpr 함수와 클래스 구현에 활용할 수 있다.
+ std::string 리터럴
- 스트링 리터럴은 주로 const char*로 처리한다. 표준 사용자 정의 리터럴 s를 사용하면 스트링 리터럴을 std::string으로 만들 수 있다.
auto string1 { "Hello World" }; // string1의 타입은 const char*다.
auto string2 { "Hello World"s }; // string2의 타입은 std::string이다.
- 표준 사용자 정의 리터럴 s를 사용하려면 std::literals::string_literals 네임스페이스를 추가한다. 그런데 string_literals와 literals 네임스페이스는 인라인 네임스페이스다.
- 기본적으로 인라인 네임스페이스에 선언된 것은 모두 자동으로 부모 네임스페이스에도 추가된다.
- 인라인 네임스페이스를 직접 정의하려면 inline 키워드를 사용한다.
+ std::vector와 스트링의 CTAD
- std::vector는 CTAD를 지원하므로 컴파일러는 초기자 리스트를 보고 vector의 타입을 자동으로 추론한다고 설명했다.
- 그런데 스트링 vector에 대해 CTAD를 적용할 때는 주의해야 한다.
vector names { "John", "Sam", "Joe" };
- 이 벡터의 타입을 vector<string>이 아닌 vector<const char*>로 추론한다. 이런 실수는 저지르기 쉬운데 코드가 정상적으로 작동하지 않거나 심하면 뻗어버린다.
vector names { "John"s, "Sam"s, "Joe"s };
- vector<string>으로 만들고 싶다면 앞 절에서 설명했듯이 std::string 리터럴로 지정해야 한다. 이때 각 스트링 리터럴 끝에 s를 붙인다.
2.1.4 숫자 변환
+ 하이레벨 숫자 변환
- std 네임스페이스는 <string>에 정의되어 있으며, 숫자를 string으로 변환하거나 string을 숫자로 쉽게 변환할 수 있는 다양한 헬퍼(편의) 함수를 제공한다.
- 숫자를 string으로 변환
string to_string(T val);
- 이런 함수는 string 객체를 새로 생성해서 리턴하며, 메모리 할당 작업도 처리해준다.
- string을 숫자로 변환
int stoi (const string& str, size_t *idx=0, int base=10);
long stol (const string& str, size_t *idx=0, int base=10);
unsigned stoul (const string& str, size_t *idx=0, int base=10);
long long stoll (const string& str, size_t *idx=0, int base=10);
unsigne long long stoull (const string& str, size_t *idx=0, int base=10);
float stof (const string& str, size_t *idx=0);
double stod (const string& str, size_t *idx=0);
long double stold (const string& str, size_t *idx=0);
- 여기 나온 변환 함수들은 제일 앞에 나온 공백 문자를 무시하고, 변환에 실패하면 invalid_Argument 익셉션을 던지고, 변환된 값이 리턴 타입의 범위를 벗어나면 out_of_range 익셉션을 던진다.
const string toParse { "123USD" };
size_t index { 0 };
int value { stoi(toParse, &index) };
cout << format("Parsed value: {}", value) << endl; // Parsed value: 123
cout << format("First non-parsed character: '{}'", toParse[index]) << endl; // First non-parsed character: 'U'
- stoi(), stol(), stoul(), stoll(), stoull()은 정숫값을 받으며, base란 매개변수를 통해 주어진 정숫값의 밑을 지정할 수 있다.
- 디폴트값은 10진수를 의미하는 10이며, 16을 지정하면 16진수가 된다. 밑을 0으로 지정하면 다음과 같은 규칙에 따라 주어진 숫자의 밑을 알아낸다.
- 숫자가 0x나 0x로 시작하면 16진수로 처리한다.
- 숫자가 0으로 시작하면 8진수로 처리한다.
- 나머지 경우에는 10진수로 처리한다.
+ 로우 레벨 숫자 변환
- 로우 레벨 숫자 변환에 대한 함수도 다양하게 제공되며 <charconv> 헤더에 정의되어 있다.
- 이 함수는 메모리 할당에 관련된 작업은 전혀 해주지 않고 std::string도 직접 다루지 않기 때문에 호출한 측에서 로 버퍼(원시 버퍼)를 할당하는 방식으로 사양해야 한다.
- 또한 고성능과 로케일 독립성에 최적화되었다. 그러므로 하이레벨 숫자 변환 함수에 비해 처리 속도가 엄청나게 빠르다.
- 또한 퍼펙트 라운드 트리핑 방식으로 설계되었다. 즉 숫잣값을 스트링 형태로 직렬화한 뒤, 그 결과로 나온 스트링을 다시 숫잣값으로 역직렬화하면 원래 값이 나온다.
- 퍼펙트 라운드 트리핑 방식(Perfect Round Tripping)은 주로 데이터 변환이나 직렬화/역직렬화 과정에서 원본 데이터가 손실 없이 그대로 유지되는 것을 의미하는 개념이다.
- 숫자 데이터와 사람이 읽기 좋은 포맷 사이의 변환 작업을 로케일에 독립적이고 퍼펙트 라운드 트리핑을 지원하면서
빠른 속도로 처리하고 싶다면 이러한 로우레벨 함수를 사용한다.
- 숫자를 스트링으로 변환
- 정수를 문자로 변환하려면 다음과 같은 함수를 사용한다.
to_chars_result to_chars(char* first, char* last, IntegerT value, int base = 10);
// IntegerT는 부호가 있는 정수나 부호 없는 정수 또는 char가 될 수 있다.
// 결과는 to_chars_result 타입으로 리턴되며 다음과 같이 정의되어 있다.
struct to_chars_result { char* ptr; errc ec; };
// 정상적으로 변환되었다면 ptr 멤버는 끝에서 두 번째 문자를 가리키고, 그렇지 않으면 last 값과 같다(이때 ec == errc::value_too_large다).
- 부동소수점 타입에 대한 변환 함수도 제공한다. FloatT 자리에 float, double, long double이 나올 수 있다. 구체적인 포맷은 chars_format 플래그를 조합해서 지정할 수 있다.
- 정밀도의 최댓값은 여섯 자리이다.
- 스트링을 숫자로 변환
- 반대 방향, 즉 문자 시퀀스를 숫잣값으로 변환하는 함수도 있다.
from_chars_result from_chars(const char* first, const char* last, IntegerT& value, int base = 10);
from_chars_result from_chars(const char* first, const char* last, FloatT& value, chars_format format = chars_format::general);
- 결과 타입의 ptr 멤버는 변환에 실패할 경우 첫 번재 문자에 대한 포인터가 되고, 제대로 변환될 때는 last와 같다.
- 변환된 문자가 하나도 없다면 ptr은 first와 같으며, 에러 코드는 errc::invalid_argument가 된다.
- 파싱된 값이 너무 커서 지정된 타입으로 표현할 수 없다면 에러 코드의 값은 errc::result_out_of_range가 된다.
- from_chars()는 앞에 나온 공백 문자를 무시하지 않는다.
2.1.5 std::string_view 클래스
- C++17 이전에는 읽기 전용 스트링을 받는 함수의 매개변수 타입을 쉽게 결정할 수 없었다.
- const char*로 지정하면 클라이언트에서 std::string을 사용할 때 c_str()이나 data()로 const char*를 구해야 한다. 이렇게하면 std::string의 객체지향 속성과 여기서 제공하는 뛰어난 헬퍼 메서드를 제대로 활용할 수 없다.
- const std::string&로 지정하면 항상 std::string만 사용해야 한다. 예를 들어 스트링 리터럴을 전달하면 컴파일러는 그 스트링 리터럴의 복사본이 담긴 string 객체를 생성해서 함수로 전달하기 때문에 오버헤드가 발생한다.
- C++17부터 추가된 std::string_view 클래스를 사용하면 이러한 고민을 해결할 수 있다.
- 이 클래스는 std::basic_string_view 클래스 템플릿의 인스턴스로서 <string_view> 헤더에 정의되어 있다.
- string_view는 실제로 const string& 대신 사용할 수 있으며 오버헤드도 없다. 다시 말해 스트링을 복사하지 않는다.
- string_view의 인터페이스는 c_str()이 없다는 점을 제외하면 std::string과 같다. data()는 똑같이 제공된다.
- string_view는 remove_prefix (size_t)와 remove_suffix(size_t)라는 메서드도 추가로 제공하는데, 지정한 오프셋만큼 스프링의 시작 포인터를 뒤로 미루거나 끝 포인터를 앞으로 당겨서 스트링을 축소하는 기능을 제공한다.
- string_view는 대부분 값으로 전달한다. 스트링에 대한 포인터와 길이만 갖고 있어서 복사하는 데 오버헤드가 적기 때문이다.
string_view extractExtension(string_view filename)
{
return filename.substr(filename.rfind('.'));
}
- extractExtension()을 호출하는 부분에서 복제 연산이 하나도 발생하지 않는다.
- extractExtension() 함수의 매개변수와 리턴 타입은 단지 포인터와 길이만 나타낸다. 그러므로 굉장히 효율적이다.
- string_view 생성자 중에서 로 버퍼와 길이를 매개변수로 받는 것도 있다. 이러한 생성자는 NUL로 끝나지 않는 스트링 버퍼로 string_view를 생성할 때 사용한다. 또한 NUL로 끝나는 스트링 버퍼를 사용할 때도 유용하다.
- 스트링의 길이를 이미 알고 있기 때문에 생성자에서 문자 수를 따로 셀 필요는 없다.
- 함수의 매개변수로 읽기 전용 스트링을 받을 때는 const string&나 const char* 대신 std::string_view를 사용한다.
- string_view를 사용하는 것만으로는 string이 생성되지는 않는다. string 생성자를 직접 호출하거나 string_view::data() 멤버로 생성해야 한다.
- 이와 같은 이유로 string과 string_view를 서로 결합할 수 없다.
- 스트링을 리턴하는 함수는 반드시 const string&나 string 타입으로 리턴해야 한다. string_view로 리턴하면 안 된다.
- string_view로 리턴하면 이것이 가리키던 본래 스트링을 다시 할당할 경우 string_view가 무효로 될 위험이 있기 때문이다.
- 클래스의 데이터 멤버를 const string&나 string_view로 지정하려면 이들이 가리키는 스트링이 객체의 수명 동안 살아 있도록 보장해야 한다. 따라서 std::string으로 저장하는 것이 안전하나다.
+ std::string_view와 임시 스트링
- string_view는 임시 스트링에 대한 뷰를 저장하는 용도로 사용하면 안 된다.
- string_view로 변수를 선언하면 초기자 표현식으로부터 임시 스트링이 생성되어 해당 포인터를 저장한다.
- 임시 스트링이 삭제되고 나면 string_view는 댕글링 포인터가 되어버린다.
+ std::string_view 리터럴
- 표준 사용자 정의 리터럴인 sv를 사용하면 스트링 리터럴을 std::string_view로 만들 수 있다.
- 표준 사용자 정의 리터럴인 sv를 사용하려면 해당 using 구문 중 하나를 적어줘야 한다.
auto sv { "My string_view"sv };
2.1.6 비표준 스트링
- C++로 프로젝트를 시작할 때 구성원이 사용할 스트링을 미리 결정하는 것은 굉장히 중요하며, 다음 사항은 반드시 명심해야 한다.
- C 스트링은 사용하지 않는다.
- MFC나 Qt 등에서 기본적으로 제공하는 스트링처럼 현재 사용하는 프레임워크에서 제공하는 스트링을 프로젝트의 표준 스트링으로 삼는다.
- std::string으로 스트링을 표현한다면 함수의 매개변수로 전달할 읽기 전용 스트링은 std::string_view로 지정한다. 스트링을 다른 방식으로 표현한다면 현재 프레임워크에서 제공하는 string_view와 유사한 기능을 활용한다.
2.2 스트링 포맷 지정 194
- C++20 이전에는 printf()와 같은 C함수를 사용하거나 C++I/O 스트림으로 스트링의 포맷을 지정했다.
- C 함수
- 안전하지 않고 커스텀 타입을 지원하도록 확장할 수 없기 때문에 권장하지 않는다.
- 포맷 스트링과 인수가 분리되어 있어서 읽기 쉽다. 따라서 다른 언어로 변환하기도 좋다.
- 예 : printf("x has value %d and y has value %d.\n", x, y);
- C++ I/O 스트림
- 타입에 안전하고 확정할 수 있어서 (C++20 이전 버전에서) 권장하는 방법이다.
- 스트링과 인수가 섞여 있어서 읽기 힘들다. 따라서 변환하기도 어렵다.
- 예 : cout << "x has value " << x << " and y has valye " << y << endl;
- C++20부터 <format>에 정의된 std::format()으로 스트링의 포맷을 지정할 수 있다. 이 함수는 C 함수의 장점과 C++ I/O 스트림의 장점을 모두 합친 것이다.
- format()의 첫 번째 인수는 포맷 지정 스트링이다. 그 뒤에 나오는 인수는 포맷 지정 스트링에 있는 빈 칸에 채워질 값이다.
-지금까지는 format()에서 빈 칸을 항상 빈 중괄호로 표기했다. 중괄호 안에 들어갈 스트링은 [index][:specifier] 형식으로 지정한다. 이때 모든 빈 칸에 index를 생략해도 되고, 모든 빈 칸에 대해 0부터 시작하는 인덱스 값을 두 번째 인수부터 필요한 만큼 지정할 수도 있다. 인덱스를 생략하면 format()의 두 번째 인수부터 나오는 값을 빈 칸에 순서대로 집어넣는다.
auto s1 { format("Read {} bytes from {}", n, "file1.txt") };
auto s1 { format("Read {0} bytes from {1}", n, "file1.txt") };
auto s1 { format("Read {0} bytes from {}", n, "file1.txt") }; // 혼용 사용 X
2.2.1 포맷 지정자
- 포맷 지정자는 값을 출력할 때 적용할 포맷을 설정한다.
- 포맷 지정자는 앞에 콜론(:)이 붙으며, 일반적으로 다음과 같은 형식으로 표기한다.
[[fill]align][sign][#][0][width][.precision][type]
+ width
- width 지정자는 주어진 값의 포맷을 적용할 필드의 최소 폭을 정한다. 이 값을 중괄호 집합으로 표기해도 되는데 이를 동적 폭이라 부른다.
+ [fill]align
- [fill]align은 채울 문자와 해당 필드에 값이 정렬되는 방식을 지정한다.
- < : 왼쪽 정렬 (정수나 부동소수점수가 아닌 값에 기본 적용)
- > : 오른쪽 정렬 (정수나 부동소수점수에 기본 적용)
- ^ : 가운데 정렬
- 출력될 때 [width]로 지정한 최소 폭에 맞게 채울 문자가 적용된다. [width]를 지정하지 않았다면 [fill]align이 적용되지 않는다.
int i { 42 };
cout << format("|{:7}|", i) << endl; // | 42|
cout << format("|{:<7}|", i) << endl; // |42 |
cout << format("|{:_>7}|", i) << endl;// |_____42|
cout << format("|{:_^7}|", i) << endl;// |__42___|
+ sign
- sign은 다음 중 하나로 지정한다.
- - : 음수에만 부호를 붙인다(디폴트).
- + : 음수와 양수 모두에 부호를 붙인다.
- space : 음수에는 마이너스 기호를 붙이고, 양수에는 빈 칸을 적용한다.
int i { 42 };
cout << format("|{:<5}|", i); // |42 |
cout << format("|{:<+5}|", i); // |+42 |
cout << format("|{:< 5}|", i); // | 42 |
cout << format("|{:< 5}|", -i); // |-42 |
+ #
- #은 얼티네이트 포매팅 규칙을 제공한다.
- 정수 타입과 16진수, 2진수, 8진수 숫자 포맷 지정에 대해 적용할 경우 0x, 0X, 0b, 0B, 0 등을 숫자 앞에 붙인다.
- 부동소수점 타입에 적용할 경우 뒤에 나오는 숫자가 없더라도 10진 구분자를 출력한다.
+ type
- type은 주어진 값을 반드시 따라야 할 타입을 지정한다. 다음과 같은 옵션이 있다.
+ precision
- precision은 부동소수점과 스트링 타입에만 적용된다.
- 부동소수섬 타입을 표기할 때는 점을 먼저 붙이고 그 뒤에 10진수 숫자를 적고, 스트링을 표기할 때는 점 뒤에 문자 개수를 적는다.
- 중괄호로 표기할 수 있으며 이를 동적 정밀도라고 한다.
+ 0
- 0은 숫잣값에 대해 적용할 때 [width]로 지정한 최소 폭에 맞게 0을 집어넣는다. 이때 0은 해당 숫잣값의 앞에 추가된다.
- 부호나 0x, 0X, 0b, 0B가 있다면 그 뒤에 나온다.
- 정렬 방식을 지정하지 않으면 무시한다.
2.2.2 포맷 지정자 에러
- 포맷 지정자는 주어진 규칙을 반드시 따라야 한다. 포맷 지정자에 에러가 있으면 std::format_error 익셉션을 던진다.
2.2.3 커스텀 타입 지원
- C++20 포맷 지정 라이브러리는 커스텀 타입에 대해 확장할 수 있다. 그러기 위해서는 std::formatter 클래스 템플릿을 특수화해야 한다.
- 이 템플릿은 parse()와 format()이라는 두 가지 메서드 템플릿을 제공한다.
- parse() 메서드는 문자 구간(context.begin(), context.end())으로 주어진 포맷 지정자를 파싱(구문 분석)한 뒤 그 결과를 formatter 클래스의 데이터 멤버에 저장하고, 파싱한 포맷 지정자 스트링 뒤에 나오는 문자를 가리키는 반복자를 리턴한다.
- format() 메서드는 parse()로 파싱한 포맷 지정자에 따라 첫 번째 인수로 주어진 값의 포맷을 지정하고, 그 결과를 context.out()에 쓴 다음, 출력 끝을 가리키는 반복자를 리턴한다.
- format_to() 함수는 사전에 할당된 버퍼를 첫 번째 인수로 받으며, 여기에 결과 스트링을 쓴다.
- 반면 format()은 스트링 객체를 새로 만들어서 리턴한다.
2.3 정리 203
- string 클래스
- string_view 클래스
- 기존 C 언어의 문자 배열에 비해 어떤 장점이 존재하는가?
- 숫자와 string을 쉽게 변환하는 여러가지 헬퍼 함수
- 로 스트링 리터럴 개념
- 스트링 포매팅 라이브러리
2.4 연습 문제 203
'Programming II > C++' 카테고리의 다른 글
[전문가를 위한 C++] CHAPTER4 전문가답게 C++프로그램 설계하기 (0) | 2025.04.02 |
---|---|
[전문가를 위한 C++] CHAPTER3 코딩 스타일 (0) | 2025.03.30 |
[전문가를 위한 C++] CHAPTER1 C++와 표준 라이브러리 초단기 속성 코스 (0) | 2025.03.30 |
전문가를 위한 C++ (0) | 2025.03.30 |
Effective C++ (0) | 2025.03.26 |