[C++] 문자열

2024. 3. 26. 21:26Programming Language/C++

아래 링크 클릭 시 해당 본문으로 이동

문자열


문자열

문자열 = 주소값

문자열은 주소값을 반환하므로 포인터로 주소에 접근한 곳에 문자열이 존재한다.

• 문자열 리터럴은 작성한 코드 그 자체이고, 컴파일러에 의해 읽기 전용 메모리(ROM, Read Only Memory)에 저장된다.

• 문자열 리터럴은 불변(Immutable)인데, 이는 프로그램 안전성을 높이고, 동일한 리터럴을 재사용하여 컴파일러가 메모리 사용을 최적화할 수 있도록 한다.

• 문자열 리터럴은 프로그램 실행 도중에 실시간으로 데이터에 접근해서 수정할 수 없게 보호되고 있다.

• 문자열 리터럴(string literal)
큰따옴표(`""`)로 묶인 문자 시퀀스, 또는 와이드 문자의 경우 `L` 접두사가 붙고 큰따옴표(`""`)로 묶인 일련의 문자를 의미한다.
이런 리터럴은 상수 데이터로 포함된다.
• 시퀀스(sequence)
프로그래밍 및 문자열 리터럴에서 시퀀스는 차례로 배열된 순서가 지정된 일련의 문자를 의미한다.

+) 현재 문자열은 읽기 전용이지만 예전 컴파일러에서 문자열은 일반 포인터 타입이었다.
컴파일러가 문제를 잡아내지 못하다가 빌드까지 완료한 프로그램에서 런타임 에러가 발생해야 문제를 파악하는 것처럼 실수의 여지가 많았다.

배열 vs. const 포인터

// 1. 배열
wchar_t wChar[10] = L"abcdef";  // wChar은 지역 변수이므로 main() 함수 스택에 존재한다.

// 2. const 포인터
const wchar_t* pChar = L"abcdef";
// const 포인터로 wchar_t를 선언할 때 L"문자열" 형태가 가능한 이유
    // 문자열은 주소값을 반환하므로 포인터로 주소에 접근한 곳에 문자열이 존재하기 때문이다.
배열 const 포인터
문자열을 그대로 배열 쪽 메모리에 옮긴다. 포인터 변수가 문자열의 시작 주소를 직접적으로 가리킨다.
문자열 abcdef 뒤에 0까지 배열 Char에 옮긴다.

포인터 변수 pChar는 문자열 abcdef의 시작 주소를 가리키고 있다.



▷ 문자열의 특정 문자 변경 - 배열 vs. const 포인터

`b`를 `z`로 바꾼다는 면에서는 같지만, `wChar[1]`와 `pChar[1]`에 해당하는 `b`는 다르다.

// 배열 wChar 쪽 메모리에 있는 b를 z로 변경
wChar[1] = 'z';

// 문자열(주소값)에 있는 b를 z로 변경
pChar[1] = 'z';  // 오류 => pChar은 const 포인터이므로 값을 변경할 수 없다.


▶ 문자열 수정이 불가능한 경우

1) 문자열 리터럴

char* str = "Hello";
str[0] = 'h';  // 오류

 

2) const 포인터 사용

const char* cStr = "Hello";
cStr[0] = 'h';  // 오류

 

3) 와이드 문자 문자열 리터럴

const wchar_t* wStr = L"Hello";
wStr[0] = L'h';  // 오류 => 포인터로 문자열에 직접적으로 접근하여 값을 바꾸려고 했기 때문이다.

//wchar_t* wStr2 = L"Hello";  // 오류
// 컴파일러가 wchar_t*는 수정 가능한 데이터를 가리킨다고 생각하는데 문자열 리터럴(`L"Hello"`)은 불변이므로 오류를 발생시킨다.
// 따라서 `const wchar_t*`를 사용하는 게 더 안전하다.

 

강제 캐스팅으로 가능하게 만들 수는 있지만, 포인터로 문자열을 직접적으로 가리켜서 OS가 ROM 코드 쪽을 수정하려고 하면 예외가 발생하여 프로그램이 터져버린다.

//wchar_t* pChar = L"abcdef";  // 오류
wchar_t* pChar = (wchar_t*)L"abcdef";
pChar[0] = 'z';  // 런타임 에러

문자열은 코드 그 자체이므로 직접 접근해서 수정하는 것은 코드 영역을 건드리는 것이기 때문에 런타임 에러가 발생한다.

따라서 문자열에 대한 정보를 주소를 통해 얻으면 읽기 전용으로만 써야 한다.

 

4) `std::string`이 `const`로 선언되는 경우

const std::string cStr = L"Hello";
cStr[0] = L'h';  // 오류

 

5) 상수 배열

• `const`로 선언된 배열은 수정이 불가능하다.

const char cStr[] = L"Hello";
cStr[0] = L'h';  // 오류

문자열 수정하는 법

1) 로컬(지역) 배열 사용

• 작고 고정된 크기의 문자열 수정에 적합하다.

char str[] = "Hello";  // str은 수정 가능한 배열이다.
str[0] = 'h';  // 첫 번째 문자 수정
std::cout << str;

▽ 출력 결과

hello

 

2) 동적 메모리 사용

• 문자열 크기가 유동적이거나 컴파일 시 크기를 알 수 없을 때 사용한다.

char* str = new char[6];  // 동적 메모리 할당
// strcpy : 문자열 복사 함수
strcpy(str, "Hello");
str[0] = 'h';  // 첫 번째 문자 수정
std::cout << str;
delete[] str;  // 메모리 해제

▽ 출력 결과

hello

 

3) `std::string` 사용

• C++의 `std::string` 클래스를 사용하면 메모리 관리를 자동으로 처리하기 때문에 가장 권장하는 방법이다.

std::string str = "Hello";
str[0] = 'h';
std::cout << str;

▽ 출력 결과

hello

`L`을 붙이지 않으면 1byte로 표현한다?

이는 엄밀히 말하면 틀린 말이다.

`L`을 붙이지 않은 경우, 문자열은 시스템에서 사용하는 문자 인코딩에 따라 다르다.

일반적으로 ASCII, 멀티 바이트 캐릭터 셋(MBCS, Multi Byte Character Set), UTF-8 중에서 사용된다.

멀티 바이트 캐릭터 셋(MBCS, Multi Byte Character Set)은 문자에 따라서 가변 길이로 대응하기 때문에 1byte 문자와 2byte 문자를 섞어서 쓸 수 있다.

※ 멀티라고 해서 2byte로 착각하면 안 된다.
멀티(multi) = 동시에 여러 개

ASCII 문자(ex. "abc")는 각각 1byte로 표현되고, 한글 또는 다른 멀티 바이트 문자는 1byte 이상으로 표현될 수 있다.

보통 MBCS 인코딩에서는 2byte(또는 그 이상), UTF-8에서는 3byte로 표현된다.

`L`이 없다고 해서 모든 문자가 1byte로 표현되는 것은 아니며, 문자열 내의 ASCII 문자1byte로 표현되지만, 한글이나 다른 문자는 2byte 또는 그 이상의 바이트로 표현될 수 있다.

인코딩 - 다중 바이트 문자 vs. 와이드 문자

1) 다중 바이트 인코딩

• 문자열은 멀티 바이트 인코딩(ex. EUC-KR 또는 UTF-8) 형식으로 저장된다.

• 1바이트로 표현할 수 없는 한글 문자를 다루기 위해 2바이트 이상을 사용하는 방식이다.

• 한글 문자는 2바이트 이상으로 저장되고, 음수 바이트 값으로 저장된다.

char cArr[10] = "abc한글";

&quot;abc한글&quot; 메모리

`-57 '?'`, `-47 '?'` = 한

`-79 '?'`, `-37 '?'` = 글

이 음수 값들은 EUC-KR 인코딩에서 멀티 바이트 문자를 저장할 때 표현되는 값이다.

 

2) 와이드 문자 인코딩

• 문자열은 와이드 문자 형식(UTF-16)으로 저장되며, 모든 문자는 2바이트씩 저장된다.
• 한글 문자는 양수 정수 값으로 저장된다.

wchar_t wChar[10] = L"abc한글";

&quot;abc한글&quot; 메모리

`54620 '한'` = 한

`44544 '글'` = 글


멀티 바이트 문제점

`abc한글`에서 문자가 몇 개인지 체크할 때 어떤 문자이냐에 따라서 크기가 1byte일 수도 있고 2byte일 수도 있다.

(ex. 잘못해서 1byte씩 세는 경우 = 문자 7개 : `a`, `b`, `c` : 문자 3개  /  `한`, `글` : 문자 4개 )

따라서 문자가 1byte인지 2byte인지 체크하려면 그 기준이 애매하므로 비트값을 확인해야 한다.

// 한글이 음수로 표현된 값을 양수로 보기 위해 unsigned를 사용했다.
unsigned char cArr[10] = "abc한글";

char, unsigned char

2byte일 때 `한`은 약 5만, `글`은 약 4만짜리 글자였는데, 이를 끊어서 봤더니 `한`에는 199, 209가, `글`에는 177, 219가 들어있다.

 

209, 219

`한`에 있는 209와 `글`에 있는 219를 2진수로 표현해 보면 둘 다 `110`으로 시작하는데, 비트 앞에 `110`으로 시작하면 문자 길이가 2개짜리라는 의미이다.

 

이처럼 멀티 바이트에서 1byte 문자와 2byte 문자가 혼용되었을 때 비트를 일일이 체크하면서 몇 byte까지가 하나의 문자인지 확인해야 하는 번거로움이 있어서 다루기 쉽지 않다.

통일성 있게 무조건 2byte 단위라면 위의 과정이 거칠 필요가 없다.