C++ 포인터와 문자열 | C++ 자습서 13

C++ 포인터와 문자열

C++ 포인터와 문자열은 흥미로운 토픽입니다. C++ 은 C보다 문자열 처리에 개선이 되었지만 구조 자체는 C를 계승하고 있기 때문에 앞서서 C를 학습한 사람들은 더 수월하게 이해할 수 있을 것 입니다.

문자열의 기본은 char 형의 배열입니다. 아스키코드만 사용하면 byte로 충분하니까 char로 충분합니다. 한글이 표기 가능한 유니코드에서는 2바이트 이상을 사용하고 인코딩에 따라 바이트는 가변적으로 변합니다만 이 포스팅에서는 거기까지 상세히 다루지는 않을 겁니다.

포인터와 문자열의 관계를 이해하면 유니코드나 이런 것들은 자연스럽게 이해가 될 것 이니 char 형 문자열의 설명에 집중하겠습니다.

문자열의 주소

C++에서 이야기하는 주소는 언제나 메모리의 주소입니다. 운영체제가 관리하는 다양한 주소가 있지만 응용 프로그램 단계에서는 퉁쳐서 메모리 주소로 부릅니다. 스택이냐 힙이냐 정도를 구분하는 수준으로 장치 드라이버를 구현하는게 아닌 이상 물리 하드웨어적으로 깊게 들어가지는 않습니다.

운영체제는 뒤에서 메모리를 관리하기 위해 많은 일을 하고 있습니다. 그 중에는 가상 메모리도 포함됩니다. 그 부분을 더 알고 싶으면 운영체제론(Operating System)과 컴퓨터 구조 개론 수업을 추천합니다.

C++의 32비트 프로그래밍이라면 4바이트의 주소를 거의 보게 될겁니다. 0000 0000 8개가 있는데 16진수는 1개 자리가 4바이트를 나타냅니다. 2^4 = 16 이고 16진수의 표기법은 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F 입니다. 이제 주소값을 출력할 때의 의미를 알겠죠? 포맷지시자를 사용하면 16진수 임을 표기하기 위해서 0x 라는 접두어를 붙이기도 합니다.

어쨋든 16진수 시스템에 익숙해지는 것은 C++을 하는데 중요합니다. 보통 학교에서는 10진수를 기본으로 가르치기 때문에 많은 학생들이 처음에 익숙해지는데 시간이 걸립니다. 당연한 것이니 처음에 스피드가 느리다고 너무 걱정안해도 됩니다.

아래 예제 코드는 문자열의 주소를 출력하고 있습니다. 문자열이라고 싸잡아 이야기 했지만 그 형태가 char 배열 형태이거나 혹은 “string” 처럼 문자 리터럴(문자열 상수)일 수도 있고 그냥 포인터 일수도 있습니다 . string 객체는 char 형을 더욱 편하게 사용하기 위해 C++에서 만든 것 입니다. 데이터형의 본질은 char 형 입니다.

문자열은 char 배열을 하나씩 전달하는 시스템이 아닙니다. char text[20] 이면 20개 요소를 하나씩 코드에 전달하지 않습니다. 오히려 첫번째 주소 text (배열의 이름은 주소이다)만 넘겨줍니다. 그러면 컴파일러는 text가 char 배열이라는 것을 알기 때문에 cout 객체로 처리하면 text[0] 부터 문자를 꺼내기 시작합니다. char 타입인 1바이트씩 더하면서 값을 읽다가 마지막에 ‘/0’ 을 만나면 종료합니다.

따라서 문자열은 주소만 넘겨주면 컴파일러가 루프를 돌면서 문자열을 꺼내고 이를 출력스트림 output stream 으로 보냅니다. char 배열이나 char 포인터 string 객체, 문자 리터럴 등 종류에 관계없이 첫번째 주소만 넘겨주면 됩니다.

이것이 문자열을 이해하는데 가장 유용한 아이디어입니다.

#include <iostream>

using namespace std;

int main()
{
    cout << endl << "[---------- char array ----------]" << endl;

    char myText[20] = "Greetings!";

    cout << myText        << "is my Text" << endl;
    cout << "- address: " << &myText << endl;

    cout << "- just string: "    << "Greetings!" << endl;
    cout << "- address: "        << &"Greetings!" << endl;

    cout << "\n[---------- string class ----------]" << endl;

    string myString = "Hello";

    cout << "- just string: " << myString << endl;
    cout << "- address: "     << &myString << endl;

    cout << "- just string: " << "Hello" << endl;
    cout << "- address: "     << &"Hello" << endl;
    
    return 0;
}
[---------- char array ----------]
Greetings!is my Text
- address: 00BAFCE0
- just string: Greetings!
- address: 007CDDF8

[---------- string class ----------]
- just string: Hello
- address: 00BAFCBC
- just string: Hello
- address: 007CDE6C

위에서 보면 & 주소연산자를 써서 확인하고 있습니다. myText 는 주소를 가지고 있습니다. 그런데 &myText는 본인의 주소입니다. myString 도 주소를 가지고 있습니다. &myString 은 자신의 주소이고요. “Hello” 도 마찬가지로 주소입니다. 우리눈에는 따옴표로 보이는데 컴파일러가 보면 Hello 라는 문자열 리터럴이 저장된 주소입니다. 역시 &”Hello”도 자신의 주소입니다.

여기서 알 수 있는 것은 포인터를 만들어야 주소가 생기는게 아니라 원래 있는 주소를 사용하는 방식이라는 것 입니다. 배열은 그 특성상 처음부터 포인터와 같습니다. 왜냐하면 자신의 주소를 확인할 수 있고 값이 저장된 곳을 역참조도 할 수 있기 때문입니다.

* [ ] 이런 기호들이 혼란스럽게 하기 때문에 조금 혼란스러울 뿐입니다.

이는 C++ 뿐 아니라 모든 프로그래밍 언어들의 컴파일러들이 다루는 방식입니다. 어떤 프로그래밍 언어라도 변수를 사용하려면 메모리의 주소에 접근해야 합니다. C++은 그냥 투명하게 운영체제 수준에서 접근하는 것이고 자바같이 가상머신(JVM)을 사용하는 현대 언어에서는 자신들 만의 메모리 매핑법을 가지고 있습니다. 메모리를 건들지 못하게 하려면 자바처럼 해시코드를 이용합니다.

문자열 포인터

문자열 포인터에 대하여 상세히 분석해 보겠습니다. 자기것으로 만들기 위해서는 코드를 읽는 것으로는 충분하지 않으니 자신만의 코드로 바뀌보는 것이 좋습니다.

아래 코드는 char 베열 부터 char 포인터까지 하나하나 주소를 파악하는 예제입니다.

배열의 이름은 배열 첫번째 요소의 주소입니다. 여기서는 nameC 입니다. sizeof 연산자를 사용하면 사이즈에 차이가 나는데 이는 배열 전체의 주소냐, 배열값의 주소냐, 배열 주소의 주소냐에 따라 차이가 납니다.

아주 보다보면 빡칠 수도 있는 내용입니다. 제일 좋은 방법은 스스로 코드를 순서대로 작성해보는 것 입니다.

#include <iostream>

using namespace std;

int main()
{
    cout << "\n[---------- char array and const char ----------]" << endl;

    char nameC[20] = "Mike";
    const char* nameCC = "John";
    char* ps;

    ps = nameC; // assign address of array

    // 주소를 전달해야 cout 이 문자열을 읽는다. '/0' 까지

    cout << "\n[---------- char array and pointer ----------]" << endl;
    
    cout << "nameC char: " << &nameC[0] << endl; 
    cout << "nameC char: " << nameC << endl; 
    cout << "ps string : " << ps << endl; 
    cout << "ps string : " << (ps+0) << endl; 

    cout << "\n[---------- size of char array ----------]" << endl;
    // 배열은 &를 사용해서 주소를 가져올 수 있다.
    // 배열 전체 주소는 nameC 라 헷갈리지 않는다.
    // 아래 sizeof 연산자의 차이를 살펴보면 알수 있다.
    cout << "address char: " << &nameC << endl;
    
    cout << "sizeof nameC    : " << sizeof nameC     << endl;
    cout << "sizeof nameC[0] : " << sizeof nameC[0]  << endl;
    cout << "sizeof &nameC   : " << sizeof &nameC    << endl;
    cout << "sizeof &nameC[0]: " << sizeof &nameC[0] << endl;

    cout << "\n[---------- char array pointer address ----------]" << endl;
    // 주소를 출력하려면 포인터를 형변환 한다. 
    // 주소만 가져오려면 형태는 상관이 없다. (char라면 void나 int로 바꿈)
    cout << "address ps  : " << (void*)ps << endl;
    cout << "address ps  : " << (int*)ps << endl;
    cout << "address ps  : " << (float*)ps << endl;

    ps = (char*) nameCC;

    cout << "\n[---------- const char pointer ----------]" << endl;
    cout << "const char *: " << nameCC << endl;
    cout << "const char * address : " << (void*)nameCC << endl;
    
    cout << "ps string : " << ps << endl; 
    cout << "address ps  : " << (void*)ps << endl;

    return 0;

}
[---------- char array and const char ----------]

[---------- char array and pointer ----------]
nameC char: Mike
nameC char: Mike
ps string : Mike
ps string : Mike

[---------- size of char array ----------]
address char: 012FFEBC
sizeof nameC    : 20
sizeof nameC[0] : 1
sizeof &nameC   : 4
sizeof &nameC[0]: 4

[---------- char array pointer address ----------]
address ps  : 012FFEBC
address ps  : 012FFEBC
address ps  : 012FFEBC

[---------- const char pointer ----------]
const char *: John
const char * address : 004D9B78
ps string : John
address ps  : 004D9B78

결과를 보면 어쨋든 C++에서는 하나의 문자열에 접근하기 위한 방법을 무수히 만들 수 있습니다. 이는 포인터가 주소를 저장할 수 있는 변수라는 이유에서 출발합니다. 리스트의 대표적 자료형인 링크드 리스트는 다음 블록의 주소를 앞의 블록 포인터에 연결시킴으로써 끝도 없는 리스트를 만들어 낼 수 있습니다.

예를 들어 최근에 가장 핫한 블록체인 기술은 여러가지 네트워크 동기화 기술을 필요로 하지만 실제 블록은 링크드 리스트와 SHA256라는 해시 함수로 이루어져 있습니다. 비트코인에 대한 관심이 그렇게나 높은데도 링크드 리스트의 기반 기술이 포인터라는 것은 일반인들에게 거의 알려지지 않았습니다.

당연히 보통 사람들이 알 필요는 없을 겁니다. 그러니 C++을 학습하는 우리들은 일반인들에게 강의를 할 수 있을 정도로 완벽하게 이해할 수 있다면 좋을 겁니다.

이 C++ 자습서 시리즈에서는 계속해서 포인터에 대한 주제를 다룰 것 입니다.

요약

C++ 을 깊게 팔수록 그 안에서 뭔가 고구마 줄기같이 끓임없이 줄줄이 나옵니다. 어떤 때는 미궁으로 들어가는 느낌이 들기도 하는데요. C++을 배워서 뭔가 만들어 내기까지는 다른 언어들보다 약간의 인내심이 더 필요합니다. 결국 C++을 마스터 하는 것은 본인의 열정에 달려있습니다.

이 자습서는 인터넷에 나와있는 수많은 문서들 처럼 지나가는 사람들에게 약간의 영감을 주기 위해서 만들었습니다.

다른 언어는 모르겠는데 C++은 별로 왕도가 없기 때문에 역시 영문 기술 문서를 많이 읽어 보고 실습을 열심히 하는 것이 좋은 방법이라고 생각합니다. C++ 자체가 어떤 특정 프로그래밍에 특화된게 아니라 범용 언어로(general language) 하나의 거대한 문서 시스템이니까요.

참고문서

To count Vowels in a string using Pointer in C++ Program (tutorialspoint.com)

Simple Program for Print String Using Pointer in C++ – C++ Programming Concepts (thiyagaraaj.com)

C++ Pointers (w3schools.com)

C++ Array, String, Pointer and Reference (magodo.github.io)

참고영상

How Strings Work in C++ (and how to use them) – YouTube

C Programming Tutorial – 45 – Strings and Pointers – YouTube

Character arrays and pointers – part 1 – YouTube

Leave a Comment