std::string 클래스
std::string의 값·길이·용량 계약과 연산자·멤버 함수·C API 경계를 익힙니다.
C 스타일 문자열은 접근 가능한 저장 범위, 끝 널 문자와 목적지 용량을 호출자가 직접 맞춰야 합니다. 일반적인 C++ 문자열 코드에서는 이 책임을 std::string 객체에 두고, 실제 C API를 호출하는 짧은 경계에서만 포인터 계약을 다루는 편이 명확합니다.
C++ · STRING OBJECT CONTRACT
std::string은 문자 시퀀스를 값으로 소유하고 길이와 저장 공간을 관리합니다. 현재 문자 수, 재할당 여유, 끝 널 문자는 서로 다른 계약입니다.
문자 시퀀스를 객체가 소유한다
복사하면 별도의 문자열 값을 얻습니다. C++11 이후 문자들은 연속 저장되지만 내부 표현과 작은 문자열 최적화는 구현 세부사항입니다.
현재 문자 개수를 센다
size()와 length()는 같은 값을 반환합니다. 끝 널 문자는 이 개수에 포함되지 않습니다.
재할당 전 저장 여유를 나타낸다
항상 현재 길이 이상이지만 길이와 같을 필요는 없습니다. 객체가 짧아져도 자동 축소를 보장하지 않습니다.
연속 범위 뒤에 끝 널 문자가 있다
std::string name = "Ada";
name.size() // 3
name.capacity() // 3 이상
name.data()[3] // '\0'
일반 문자 인덱스는 0부터 2까지입니다. 현행 draft에서 3은 끝 널 위치이며, 그 값을 char() 이외로 바꾸면 undefined behavior입니다.
길이 감소와 용량 감소는 별개다
name.reserve(80);
name.clear();
// size() == 0
// capacity()는 0일 필요 없음
name.shrink_to_fit(); // 비구속 요청
현행 draft와 C++20 이후 reserve(n)은 더 작은 용량으로 줄이지 않습니다. 현행 shrink_to_fit()은 용량을 늘리지 않는 비구속 요청이며, 실제 재할당하면 모든 원소 관찰자가 무효화됩니다.
객체 길이와 C 문자열 길이가 다를 수 있다
const char bytes[]{'A', '\0', 'B'};
std::string packet(bytes, 3);
packet.size() // 3
널 종료만 아는 C 함수는 첫 널에서 멈춥니다. 포인터와 길이를 함께 받는 API인지 확인합니다.
변경 연산은 기존 관찰자를 무효화할 수 있다
대입·추가·삽입·삭제·교체와 비상수 표준 라이브러리 호출 뒤에는 원소 포인터, 참조와 반복자의 유효성을 다시 판단합니다.
값의 소유권은 객체에 있고, 내부 주소의 수명은 별도 계약입니다. 현행 draft에서 shrink_to_fit()이 재할당하지 않으면 기존 관찰자는 그대로 유효합니다.
읽는 순서: 소유한 문자 값 → 현재 size() → 재할당 여유 capacity() → 끝 널과 관찰자 수명. 구현의 저장 전략은 추측하지 않습니다.
std::string이 관리하는 것
std::string은 개수가 변할 수 있는 char 시퀀스를 값으로 소유하는 표준 라이브러리 클래스입니다. 사용하려면 <string>을 포함합니다.
#include <string>
std::string empty;
std::string language = "C++";
std::string copy = language;객체를 이해할 때는 다음 세 값을 구분해야 합니다.
size()와length()는 현재 문자열에 들어 있는 문자 개수이며 서로 같은 값을 반환합니다.capacity()는 재할당 없이 담을 수 있도록 확보한 저장 용량입니다. 항상size() <= capacity()이지만 두 값이 같을 필요는 없습니다.data() + size()위치에는 값이char()인 끝 널 문자가 있습니다. 이 끝 표시는size()에 포함되지 않으며 다른 값으로 바꾸면 안 됩니다.
문자는 연속해서 저장되지만 객체 내부 표현, 성장 배수, 작은 문자열 최적화 여부는 구현 세부사항입니다. 이 세부사항에 의존하지 말고 공개 멤버 함수의 계약을 기준으로 작성합니다.
연속 저장과 data() + size()의 끝 널 보장은 C++11 이후 계약입니다. C++03 이하에서는 data()가 널 종료 버퍼를 반환한다고 가정할 수 없습니다. 이 절은 현행 draft의 저장 계약을 기준으로 하되, 쓰기 가능한 비상수 data()처럼 도입 시점이 다른 기능은 버전을 따로 표시합니다.
길이가 줄어도 용량이 자동으로 줄어드는 것은 아니다
erase(), clear(), 짧은 값의 대입은 size()를 줄이지만 capacity()까지 같은 크기로 축소한다고 보장하지 않습니다. 앞으로 필요한 크기를 알고 있다면 reserve()로 재할당 횟수를 줄일 수 있습니다. 현행 draft와 C++20 이후의 reserve(n)은 n > capacity()일 때만 재할당하며, 더 작은 n으로 호출해도 축소 요청이 되지 않습니다. C++17 이하의 wording은 더 작은 인자를 비구속 축소 요청으로 허용했으므로 그 규칙을 구버전 모드에 소급해서는 안 됩니다.
shrink_to_fit()은 C++11부터 제공되는, 용량을 size() 쪽으로 줄여 달라는 비구속 요청입니다. 현행 draft에서는 용량을 늘리지 않으며, 재할당이 일어나면 모든 원소 포인터·참조·반복자와 past-the-end 반복자가 무효화됩니다. 재할당이 없으면 그대로 유효합니다.
std::string text = "Hello";
text.reserve(80);
text.clear();
// text.size() == 0
// text.capacity()는 0일 필요가 없습니다.객체 선언과 초기화
std::string은 빈 값, 문자열 리터럴, 다른 문자열, 반복 문자, 부분 범위로 만들 수 있습니다.
std::string s1; // ""
std::string s2 = "Hello C++"; // 리터럴의 내용 복사
std::string s3 = s2; // 값 복사
std::string s4(5, 'A'); // "AAAAA"
std::string s5(s2, 6, 3); // "C++"널 종료 포인터 하나만 받는 생성자는 첫 널 문자까지 읽습니다. 포인터가 가리키는 데이터에 내장 널 문자가 있거나 길이를 별도로 알고 있다면 길이 overload를 사용합니다.
const char* ntbs = "Ada";
std::string name(ntbs); // 첫 '\0' 전까지 복사
const char bytes[]{'A', '\0', 'B'};
std::string packet(bytes, 3); // 내장 '\0'까지 포함해 3개 복사널 종료 overload에는 유효한 널 종료 시퀀스를, 길이 overload에는 읽을 수 있는 [ptr, ptr + count) 범위를 제공해야 합니다. 널 포인터를 문자열처럼 넘겨서는 안 됩니다.
값처럼 대입하고 연결하고 비교하기
대입은 왼쪽 객체의 값을 바꾸고, +=와 append()는 기존 객체 뒤에 붙입니다. +는 결과 문자열 값을 만듭니다.
std::string greeting = "Hello";
std::string target = "World";
std::string sentence = greeting + " " + target + "!";
greeting += " everyone";==와 !=은 포인터 주소가 아니라 문자열의 문자 시퀀스를 비교합니다. compare(other)는 왼쪽이 앞서면 음수, 같으면 0, 뒤면 양수를 반환하며 정확히 -1이나 1을 반환한다고 가정해서는 안 됩니다. 순서 비교는 char_traits가 정한 문자 단위의 lexicographical 비교이며, 자연어 locale의 사전 정렬이나 대소문자 무시 정렬이 아닙니다. 사용자 언어에 맞춘 정렬이 필요하면 별도의 locale·Unicode collation 정책이 필요합니다.
[]와 .at()의 경계
문자 데이터의 유효 인덱스는 0부터 size() - 1까지입니다.
std::string word = "Example";
word[0] = 'e'; // 인덱스가 유효함을 이미 증명한 경우
char c = word.at(2); // 범위를 검사하고 'a' 반환operator[]는 일반 문자 접근에서 범위를 검사하지 않습니다. 현행 draft의 precondition은 pos <= size()이고 pos == size()이면 data()[size()]의 끝 널 문자를 가리킵니다. 그 끝 널을 char() 이외의 값으로 바꾸면 undefined behavior이며, pos > size()는 허용되지 않습니다. 따라서 일반 문자 인덱스에는 계속 pos < size()를 사용합니다. .at(pos)는 pos >= size()이면 std::out_of_range를 던집니다. 외부 입력에서 온 인덱스라면 먼저 범위를 검증하거나 .at()을 사용합니다. front()와 back()은 문자열이 비어 있지 않을 때만 호출합니다.
연산과 멤버 함수 고르기
C++ · STRING OPERATION LEDGER
함수 이름보다 먼저 결과가 새 값인지, 원본 변경인지, 잠시 빌린 포인터인지 확인합니다. 인덱스·검색 실패·재할당·C API precondition을 연산과 함께 읽어야 합니다.
| 목적과 대표 표현 | 유지할 계약 |
|---|---|
| 대입
|
+는 결과 값을 만들고 =·+=는 왼쪽 객체를 바꿉니다. |
| 비교
|
주소가 아닌 문자 시퀀스를 비교합니다. compare()는 음수·0·양수만 해석하며, 순서는 문자 traits 기반 lexicographical 비교입니다. |
| 접근
|
일반 문자 위치는 pos < size()입니다. 현행 draft에서 pos == size()는 끝 널 위치이고, at()은 이 위치부터 out_of_range를 던집니다. |
| 검색
|
검색 실패는 npos입니다. 부분 문자열은 새 값을 반환하고 시작 위치 계약을 검사합니다. |
| 편집
|
객체를 바꾸며 기존 포인터·참조·반복자가 무효화될 수 있습니다. |
| C API
|
C++11 이후 c_str()와 data()는 같은 배열 시작을 가리킵니다. 내장 널·길이·쓰기 가능 여부를 API와 함께 명시합니다. |
새 값인가, 원본 변경인가
auto joined = left + right;
left += right;
첫 줄은 결과 값을 만들고, 둘째 줄은 left를 바꿉니다.
위치를 증명했는가
text[pos] // pos < size()
text.at(pos) // 검사 후 접근
find()의 결과는 npos인지 먼저 확인합니다.
내용이 같은가, 어느 쪽이 앞서는가
left == right
left.compare(right)
compare()의 정확한 수치가 아니라 음수·0·양수만 해석합니다.
C API가 무엇을 받는가
널 종료 읽기 전용인지, 포인터와 길이를 함께 받는지, 쓰기 버퍼인지 구분합니다. C++11 이후 c_str()와 data()는 같은 배열 시작을 가리키며, c_str()는 읽기 전용 경계입니다.
실패 sentinel과 인덱스를 먼저 확인한다
std::string text = "Learn C++ today.";
auto pos = text.find("C++");
if (pos != std::string::npos) {
auto topic = text.substr(pos, 3);
}
text.replace(10, 5, "safely");
결과: Learn C++ safely.
필요한 순간에 포인터를 얻는다
void legacy_read_only(const char*);
std::string message = "ready";
legacy_read_only(message.c_str());
문자열을 바꾸거나 비상수 표준 라이브러리 호출에 넘긴 뒤에는 기존 포인터를 재사용하지 않고 다시 얻습니다.
내장 널은 길이와 함께 보존한다
const char bytes[]{'A', '\0', 'B'};
std::string packet(bytes, 3);
널 종료 생성자와 C 함수는 첫 널에서 멈춥니다. 값 전체가 필요하면 읽을 수 있는 범위의 길이를 함께 전달합니다.
c_str()에 쓰지 않는다
반환형은 const char*입니다. 출력용 C API에는 별도의 writable 범위와 호출 후 실제 문자열 길이를 반영하는 계약이 필요합니다.
C++17 이후 비상수 data()로는 [0, size())의 문자를 쓸 수 있지만, 끝 널을 다른 값으로 바꾸거나 용량만 확보한 영역을 문자열 크기처럼 취급하면 안 됩니다.
선택 순서: 소유 값인지 → 조회·새 값·원본 변경인지 → 위치와 실패 결과가 유효한지 → 관찰자 무효화가 가능한지 → C API의 포인터·길이·쓰기 계약이 무엇인지.
많은 멤버 함수는 상태를 묻는 함수, 새 값을 반환하는 함수, 원본을 바꾸는 함수, C 경계를 여는 함수로 나누어 읽을 수 있습니다.
| 목적 | 대표 표현 | 결과와 경계 |
|---|---|---|
| 길이·빈 값 확인 | size(), length(), empty() | 원본을 바꾸지 않습니다. |
| 비교 | ==, compare() | 문자 시퀀스를 비교합니다. compare()의 음수·0·양수만 해석합니다. |
| 검색 | find(needle, pos) | 찾은 위치 또는 std::string::npos를 반환합니다. |
| 부분 문자열 | substr(pos, count) | 새 문자열을 반환합니다. pos > size()이면 예외이고 count는 남은 길이까지만 사용됩니다. |
| 뒤에 추가 | +=, append(), push_back() | 원본을 바꾸며 재할당될 수 있습니다. |
| 중간 편집 | insert(), erase(), replace() | 원본을 바꾸며 위치와 길이 계약을 확인해야 합니다. |
| 모든 문자 제거 | clear() | size()는 0이 되지만 용량 축소는 보장하지 않습니다. |
| 읽기 전용 C 포인터 | c_str() | 현재의 널 종료 내부 버퍼를 가리키는 const char*를 반환합니다. |
#include <iostream>
#include <string>
int main() {
std::string text = "Learn C++ today.";
const std::string::size_type pos = text.find("C++");
if (pos != std::string::npos) {
const std::string topic = text.substr(pos, 3);
std::cout << topic << '\n'; // C++
}
text.replace(10, 5, "safely");
std::cout << text << '\n'; // Learn C++ safely.
}find()의 반환값을 바로 문자 인덱스로 사용하지 말고 먼저 npos인지 확인합니다. 편집 함수 뒤에는 기존 포인터·참조·반복자의 유효성도 다시 판단합니다.
C 문자열과 만나는 경계
C 문자열에서 std::string으로
널 종료 입력은 std::string(raw)로 값 복사할 수 있습니다. 입력 길이가 별도로 주어지거나 내장 널을 보존해야 하면 std::string(raw, count)를 사용합니다. 생성된 객체는 원본 포인터와 독립적으로 자신의 값을 소유합니다.
std::string에서 읽기 전용 C API로
c_str()는 널 종료된 내부 버퍼의 const char*를 반환합니다. C++11 이후 c_str()와 data()는 같은 연속 배열의 시작을 가리킵니다. 포인터를 저장해 두기보다 필요한 호출 지점에서 얻어 즉시 넘깁니다.
void legacy_read_only(const char* text);
std::string message = "ready";
legacy_read_only(message.c_str());문자열의 원소를 가리키는 포인터·참조·반복자는 표준 라이브러리 함수에 비상수 std::string&로 넘기거나, 일부 원소 접근 함수를 제외한 비상수 멤버 함수를 호출하면 무효화될 수 있습니다. 따라서 값을 대입·추가·삽입·삭제·교체하거나 재할당 가능 연산을 한 뒤에는 c_str() 포인터를 다시 얻습니다. 객체가 소멸한 뒤에는 당연히 사용할 수 없습니다.
std::string은 내장 널 문자를 값의 일부로 가질 수 있습니다. 이런 문자열을 널 종료 규칙만 아는 C 함수에 넘기면 C 함수는 첫 널 문자까지만 봅니다. 길이를 함께 받는 API인지 확인해야 합니다.
쓰기용 C 버퍼가 필요한 API에 c_str()를 넘기거나 const를 제거해 쓰면 안 됩니다. C++17 이후 비상수 data()는 쓰기 가능한 포인터를 제공하지만 문자열의 현재 크기와 끝 널 계약을 지켜야 합니다. 출력 길이가 달라지는 C API라면 호출 전 확보할 범위와 호출 후 실제 size()를 어떻게 반영할지 별도의 버퍼 계약을 설계합니다.
사용 기준
새 C++ 코드에서 소유하는 가변 문자열의 기본값은 std::string입니다. 다만 객체가 모든 오류를 없애는 것은 아닙니다. 인덱스, 최대 길이, 할당 실패, 문자 인코딩, locale 정렬, 포인터 수명과 외부 API precondition은 여전히 코드가 정해야 합니다.
최종 선택 순서는 다음과 같습니다.
- 문자열 값을 소유해야 하는지 확인합니다.
- 현재 문자 수인
size()와 재할당 여유인capacity()를 구분합니다. - 조회·새 값 반환·원본 변경 중 어떤 연산인지 확인합니다.
- 인덱스와 검색 실패를 검증합니다.
- C API 경계에서만 포인터와 길이·널 종료·쓰기 가능 여부를 명시합니다.