c++ string 클래스에 문자열을 저장할 때는 널문자가 없나요?

조회수 16724회
#include <iostream>
#include <string>
#include <cctype>

using namespace std;

void ChangeStr(string &s);

int main()
{
 string str;


 while(1)
 {
  cout<<"문자열을 입력하시오 (끝내려면 q): ";
  cin>>str;
  if(str[0] == 'q')
   break;
  ChangeStr(str);
  cout<<"바뀐 문자열 : "<<str<<endl;
  cin.get();
 }
 cout<<"종료 \n";

 return 0;
}

void ChangeStr(string &s)
{
 int i=0;
 while(i<s.length()) // s[i] != '\0'
 {
  s[i] = toupper(s[i]);
  i++;
 }
}

안녕하세요 지난번에 도움을 크게 받고 오늘도 도움을 받고자 질문드려봅니다. 문제를 풀다가 분명 맞는거 같은데 런타임 에러가나서 해결방법을 찾다가 결과적으로는 문제풀이에는 해결했지만 궁금한 요소가 남아있어 질문드립니다

위 소스는 string 객체에대한 참조를 매개변수로 취하고, string의 내용을 대문자로 변경하는 함수를 작성하라는 문제의 소스입니다. toupper 함수를 이용해서 말이죠.

질문할 부분은 ChangeStr 함수에서 while문의 조건식입니다.

현재 본문 소스에서 문자열의 길이를 알기위해서 s.length() 함수를 사용합니다 그런데 저건 수정된 소스이고,
제가 원래 작성했던 소스는 옆에 주석처리 되어있는 s[i] != '\0' 로 while문의 조건식을 채웠습니다.

매개변수에서 string &s가 참조형으로 선언되어있고 이건 인자로 전달받은 문자열의 메모리 공간을 참조하기 때문에 가능하다고 생각했습니다. while문으로 s의 문자열이
null 문자를 만날때까지 while 문을 진행하는거죠. while문의 조건식을 저런식으로 작성하고 컴파일해보니 컴파일은 되지만 문자열을 입력하고 함수가 호출되면
string subscript out of range 이라는 에러가 납니다.

무슨 에러인지 몰라 검색을 해보니 배열의 빈원소를 참조할 때 띄우는 메세지라는 것이라고 알게되었는데 제 수준에서는 이해가 잘 가지않습니다. string 객체 s는 제대로 문자열의 메모리공간을 참조하고 있어서 배열로 사용가능하다고 생각했습니다.

그런데 이 문제의 답안에는 s.length()를 사용해서 해결했더군요..문자열의 길이를 반환해서 그만큼 while문을 실행하는겁니다.그리고 왜 안되는지 찾는도중에 string 클래스는 널문자를 저장하지 않는다고 어디서 보았는데 정말 맞는말인가요? 그래서 s[i] 가 계속되면서 문자열의 끝을 몰라 저런 에러 메세지를 띄우는 건가요?

만약에 string 클래스가 널문자를 저장하지 않는다면 문자열의 끝을 컴파일러가 어떻게 알아차리나요?

답변해주시면 정말감사하겠습니다

1 답변

  • 기존 C 언어에서는 문자의 나열 과 마지막에 '\0' 을 넣어 문자열을 표현합니다.

    그에 반해 std::string문자의 나열길이로 문자열을 표현합니다. 따라서 마지막에 '\0' 을 넣지 않아도 문자열의 끝을 알 수 있습니다.

    예를들어 "hello"std::string 변수에 넣는다면, 문자열의 길이인 5와 문자들인 (h, e, l, l, o) 가 저장됩니다. 따라서 operator[] 를 통해 인덱스 5 에 접근하려 한다면 문자열의 범위를 넘어서기 때문에 std::out_of_range 예외를 발생합니다. 참고로 operator[] 에서 예외 발생은 표준 사항이 아니기 때문에 발생하지 않을 수도 있습니다.

    하지만 실제로 std::stringNULL('\0') 로 끝나는 문자열과의 호환성을 위해서 메모리 상에 '\0' 을 포함하여 문자열을 저장합니다. 다만 operator[] 를 통한 '\0' 에 대한 접근을 허용하지 않을 뿐입니다. 그리고 이 때 저장되는 '0'호환용일 뿐 문자열의 길이를 계산 한다거나 다른 용도로는 사용되지 않습니다.

    따라서 문자열의 끝을 확인하기 위해서 '\0' 검사를 하실 필요가 없습니다. 이미 문자열의 길이는 저장되어 length() 또는 size() 를 통해 가져올 수 있기 때문입니다.

    #include <iostream>
    #include <string>
    
    int main() {
        std::string v = "hello";
        for (std::size_t i = 0; i < v.size(); ++i)
            std::cout << v[i];
        return 0;
    }
    

    위에서 말씀 드렸듯이 std::string 은 호환을 위해서 c_str() 이란 멤버 함수를 갖고 있습니다. 이 함수는 char const* 주소를 반환하며 이 방식으로는 문자열의 길이를 알 수 없기 때문에 '\0' 검사를 통해 문자열의 끝을 확인해야합니다.

    #include <iostream>
    #include <string>
    
    int main() {
        std::string v = "hello";
        char const* p = v.c_str();
        for (; *p != '\0'; ++p)
            std::cout << *p;
        return 0;
    }
    

    정리하면 아래와 같습니다.

    std::string 은 문자열을 메모리에 저장할 때 실제론 '\0' 을 포함하지만 개념상 \0 에 대한 접근을 허용하지 않는다. 또한 문자열의 길이를 이미 갖고 있기 때문에 '\0' 탐색을 통해 길이를 계산하지 않아도 문자열의 끝을 알 수 있다.


    디버거가 보여주는 std::string'\0' 이 저장된 것을 보여주지 않을 수 있습니다. std::string 개념 상 구지 보여줄 필요가 없기 때문입니다.

    이를 직접 확인하기 위해서 아래와 같은 코드를 작성할 수 있습니다.

    #include <iostream>
    #include <string>
    
    int main() {
        std::string v = "hello";
    
        char const* str1 = v.c_str();
        char const* str2 = &v[0];
    
        std::cout << "address of 'str1': " << static_cast<void const*>(str1) << std::endl;
        std::cout << "address of 'str2': " << static_cast<void const*>(str2) << std::endl;
        std::cout << std::endl;
    
        for (std::size_t i = 0, size = v.size() + 1; i < size; ++i) {
            std::cout << "str1[" << i << "]: " << str1[i] << " (" << static_cast<int>(str1[i]) << ")" << std::endl;
        }
    
        for (std::size_t i = 0, size = v.size() + 1; i < size; ++i) {
            std::cout << "str2[" << i << "]: " << str2[i] << " (" << static_cast<int>(str2[i]) << ")" << std::endl;
        }
    
        return 0;
    }
    

    char const* str1 = v.c_str(); 를 통해 v 에 대한 NULL 로 끝나는 문자열을 가져옵니다. 이 문자열은 당연히 문자열 끝에 '\0' 이 존재 합니다.

    char const* str2 = &v[0];v 첫 번째 문자가 있는 주소를 가져옵니다. std::string 은 연속된 메모리 공간에 문자를 저장하기 때문에 str2 를 통해 뒤의 문자들도 읽어 올 수 있습니다.

    그리고 str1str2 가 동일한 주소라면, str2 도 문자열 끝에 '\0' 이 저장되어 있을 것 입니다.

    위 코드를 실행 해보면 아래와 같은 결과를 볼 수 있습니다.

    address of 'str1': 0x7fffe79ab658
    address of 'str2': 0x7fffe79ab658
    
    str1[0]: h (104)
    str1[1]: e (101)
    str1[2]: l (108)
    str1[3]: l (108)
    str1[4]: o (111)
    str1[5]:  (0)
    str2[0]: h (104)
    str2[1]: e (101)
    str2[2]: l (108)
    str2[3]: l (108)
    str2[4]: o (111)
    str2[5]:  (0)
    

    str1str2 는 동일한 주소라는 것을 알 수 있습니다. 각각의 문자열의 0~5 인덱스를 출력한 결과를 보면 인덱스 5'\0' 이 있는것을 확인할 수 있습니다.

    즉, c_str() 을 통해 가져온 주소는 결국 std::string 에서 직접 문자를 저장하고 있는 공간의 주소라는 것을 알 수 있습니다.

    이런 것들을 보아 std::string 은 갖고 있는 문자들의 나열 뒤에 '\0' 을 포함시키고 있는 것을 알 수 있습니다.

    • 답변 정말 감사합니다. 답변 써주신거보고 string 클래스에서는 문자열의 끝을 표현하기위해 null문자가 필요는 없지만 호환을 위해 실제로는 null문자를 가지고 계신다고 말씀해주신걸 보고 간단하게 실험을 해보았는데요, string 객체에 문자열을 저장하고 디버깅을 해보니, 문자열 끝에 딱히 null문자는 발견되지 않았습니다. 이것과 operator[]로 인덱스의 끝에 접근할시 out of range 에러를 띄운다고 말씀해주신것을 바탕으로 null 문자는 문자열의 끝에는 null문자가 저장되지 않는 것으로 예상이 가는데, 그렇다면 null문자는 단순 string 객체(또는 저장된 문자열? 정확히는 모르겠네요ㅜ)의 메모리 공간 어딘가 저희가 알 수없는 공간에 저장되어 있는것인가요?, 저희가 디버깅이라던지 눈으로는 확인은 할 수없나요?..혹은 제가 디버깅이 초보라 그런것일까요 codeslave 2018.5.12 19:08
    • 디버거는 그 부분을 숨겨서 보여주지 않습니다. 해당 내용에 대해서 답변 내용을 곧 갱신하겠습니다. 유동욱 2018.5.12 19:36
    • 감사합니다. 죄송하지만 한가지 정도 더 여쭈어볼게 있는데요 연장해주신 답변에서 str1 변수에 v.c_str() 함수호출로 string 객체 v에 저장되어있는 첫번째 문자의 주소(문자열의 주소)를 저장받고 있고, str2 변수는 &v[0] 의 값,즉 v[0]에 저장되어있는 문자의 주소값 ,문자열 Hello의 첫번째 문자주소(문자열의 주소)를 저장 받아 for문을 이용한 배열접근을 통해 null문자의 확인을 char const* 타입을 통해 보여주셨는데요. 이 두 부분같은 경우에서는 읽으면서 "Hello" 라는 문자열의 주소를 저장하고, 당연히 문자열의 끝에 null문자가 있을 것야 라고 생각을 했습니다. 왜냐하면 평소에 char const* 타입에 문자열을 저장할 때는 자동적으로 문자열의 끝에 null문자가 저장됨을 알고 배웠고,그러한 코드를 썼기때문입니다. 그런데도 const char* 타입의 예를 통해 들어주신거는 string 객체가에 문자열을 저장하고 있어도 문자열의 끝에는 '\0' 를 저장하는것은 const char* 타입 때와 똑같지만 답변 해주신 것처럼 string 객체의 operator[] 를 통한 \'0'문자의 접근 은 허용을 하지 않는 다는 것을 확인시켜주기 위한 것인가요? 두서없이 쓴거라 이해가 잘 가시지 않는 부분이 있다면 좀더 보충 설명해드리겠습니다ㅠ codeslave 2018.5.13 00:23
    • 질문의 요지를 이해하기 힘들어 그런데. 조금 간략하게 물어주실 수 있으신가요? 유동욱 2018.5.13 00:32
    • string 객체 v에 저장된 문자열을 const char* 타입의 포인터 변수 str1에 v.c_str() 함수 호출을 통해 v에 저장된 문자열의 주소를 반환해서 str1이 해당 문자열을 가르키게하고, 마찬가지로 str2에도 &v[0]를 str2 가 해당 문자열의 첫문자를 가르키게하는데요. str1과 str2는 const char* 형이므로 당연히 배열을 통해 null 문자에 접근이 가능하잖아요? 그런데 이것이 string 클래스 v와 무슨 관련이 있는것인지 잘 이해가 가질 않습니다. c_str()이나 &v[0]나 둘다 문자열의 첫문자 주소를 반환하는데, const char* 형 변수를 통해서 해당 문자열의 끝에 null 문자가 정상적으로 저장되어있음을 보여주시기 위한것인지요? 그래서 null문자가 string 객체 안에서도 문자열의 끝에 정상적으로 저장은 되어있지만 operator[] 을 통해서는 널문자에 접근하지 못하는 것뿐이다 라는 것을 설명하기 위한것인지요? codeslave 2018.5.13 01:05
    • 답변을 수정하려했는데 사이트 오류로 수정이 안되네요. `const char*` 는 `const char` 의 주소를 가리키기 위한 타입일 뿐, 해당 주소부터 1씩 증가 시키면서 따라가면 `'\0'` 이 나오는 것을 보장하지 않습니다. `const char*` 에 NULL 로 끝나는 문자열의 주소를 넣었기 때문에 `'\0'` 을 찾을 수 있는 겁니다. `std::string` 이 만약 `'\0'` 을 저장하고 있지 않았다면, `const char*` 으로 메모리를 탐색하여도 `'\0'` 이 나타나지 않습니다. `std::string` 은 `'\0'` 을 포함하지 않게 메모리에 문자열을 저장하고 있다가, `c_str()` 을 호출하면 새로운 메모리에 `'\0'` 을 포함한 문자열을 만들고 이 주소를 `c_str()` 을 통해 반환한다고 생각할 수 있습니다. 이 경우 `c_str()` 과 `&v[0]` 의 주소는 다르게 됩니다. `c_str()` 과 `&v[0]` 두 경우에 대해서 확인해본 이유는, `c_str()` 을 호출 하여도 새로운 메모리 할당이 아닌 동일한 주소이며 `std::string` 은 처음부터 메모리 `'\0'` 을 포함한 문자열을 메모리에 저장한다는 것을 보이기 위함 이었습니다. `operator[]` 는 구현에 따라 다르지만 입력 받은 인덱스가 0 이상 size() 미만 의 범위에 있는지를 확인합니다. 만약 해당 범위 밖에 있다면 예외를 발생시킵니다. `v[v.size()]` 의 경우 이 범위를 넘어서기 때문에 메모리에 `'\0'` 이 존재하더라도 예외가 발생하여 접근할 수 없습니다. 그에 비해 `const char*` 는 그러한 범위 검사를 하지 않기 때문에 `'\0'` 을 읽을 수 있습니다. 즉, `operator[]` 로는 확인할 수 없기 때문에 `const char*` 로 주소를 가져와 확인한 것입니다. 유동욱 2018.5.13 12:34
    • 아 제가 무작정 const char* 타입이면 null이 들어간다고 오해하게 썻네요..그 문자열 상수..큰따옴표로 저정하는(맞나요?ㅎㅎ) 일 경우에요! 드디어 뭔가 이해가 가기시작하는거 같습니다.. v가 만약 '\0'을 저장하고 있지 않다면 c_str() 와 &v[0]에서 c_str() 반환시 null 문자를 포함하는 문자열을 새로운 메모리에 만들고 그 메모리의 주소를 반환하고, 메모리가 새로 생성된 곳의 주소이기때문에 &v[0]와 다를것이지만, 출력결과 주소가 같으므로 처음부터 string은 '\0' 을 포함한 메모리에 문자열을 저장한다고 볼 수 있다..그리고 string 객체로 배열 접근 할 수 없는 이유는 string 클래스는 널문자를 가지고 있지만 operator[]가 v.size() 미만의 범위까지만 검사하므로, null문자가 저장되어있는 v[v.size()]에는 범위 밖이므로 접근할 수가 없다.. 이렇게 이해가 가는데 선생님이 설명해주신대로 올바르게 이해하고 있는 것처럼 보이시나요? codeslave 2018.5.14 03:15
    • 전후 관계가 약간 다르긴 한데 얼추 그렇습니다. 유동욱 2018.5.14 08:27
    • 많이 귀찮으셨을텐데 답변 하나하나 달아주셔서 감사합니다. 전후관계가 다르다는건 뭔지 잘모르겠지만 이정도로 이해하고 넘어가도록해야겠네요 정말 감사합니다! codeslave 2018.5.14 22:14

답변을 하려면 로그인이 필요합니다.

프로그래머스 커뮤니티는 개발자들을 위한 Q&A 서비스입니다. 로그인해야 답변을 작성하실 수 있습니다.

(ಠ_ಠ)
(ಠ‿ಠ)