Programming/C, C++

포인터의, 포인터에 의한, 포인터를 위한 포스트 였는데 문자열도 좀 낌 사실 뭐가 주인지 모르겠어요 우선 포스트를 쓰고 와봅시다

NONE_31D 2019. 10. 12. 00:01

이 모든 일의 시작은 단지 포인터와 주소 개념을 알려주려고 짠 코드의 실행 결과 때문이었습니다.

시작된 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
int main() {
    char a[] = "hello";
    char &ref = a[1];
    char * ptr = a;
    
    cout << &<< endl;
    cout << a << endl;
    cout << a[0<< endl;
    
    cout << &a[1<< endl;
    cout << &ref << endl;
    
    cout << &ptr << endl;
    cout << ptr << endl;
    cout << *ptr << endl;
}
cs

포인터를 공부했다면, 정말 그다지 어렵지 않은 개념을 포함한 코드입니다.

char 배열 형태로 문자열을 선언하고, a[1]을 참조하는 참조자도 선언하고 마지막으로는 a를 가리키는 포인터를 선언했습니다.

cout의 첫 세줄은 a 문자열의 주소, a 문자열 자체, a의 첫번째 글자를 출력하고

그 다음 두 줄은 a[1]의 주소값과 ref의 주소값을 출력해야 했습니다. ref는 참조자니 똑같은 a[1]의 참조자를 출력할 줄 알았죠.

마지막 세 줄은 ptr의 주소, ptr에 저장된 주소값, ptr이 가리키고 있는 값을 출력해야 했습니다.

 

출력 값은 다음과 같았습니다.

여기서 이해가 안되는 부분은 두가지였습니다.

1. 왜 ptr은 a의 주소값을 가리키지 않고 배열 자체를 출력하는 것인가?

2. 왜 &ref와 &a[1]은 a의 두번째 글자의 주소를 출력하지 않고 두번째부터 끝까지의 문자열을 출력해버리는 것인가..?

 

C에서 포인터 개념을 어느정도 떼고 왔다고 자신했는데 생각도 못한 출력물을 보고, 왜 이따구로 결과가 나오나 이유나 찾아보기로 합니다.

 

먼저, 제가 포인터에 대해 개념을 잘못 확립하고 있었나 의심을 하고 int형으로 다시 비슷한 코드를 짜봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
 
int main() {
    int n = 1;
    int &= n;
    int* p = &n;
 
    cout << n << endl;
    cout << &<< endl;
 
    cout << r << endl;
    cout << &<< endl;
 
    cout << *<< endl;
    cout << p << endl;
    cout << &<< endl;
}
cs

코드의 큰 틀은 바뀌지 않았습니다. int 형 n을 선언하고, n을 참조하는 r을 선언하고, n에 대한 포인터 변수를 선언했습니다.

출력문의 첫 단락은 n의 값과 n의 주소를 출력하고, 두번째 단락은 r의 값과 r의 주소를 출력,

마지막 단락은 p가 가리키고 있는 주소의 값, p에 저장된 주소값, p 자체의 주소를 출력했습니다.

 

실행 결과는 다음과 같습니다.

위에서 말한 것 같이 정상적으로 주소값들과 변수 값이 출력됩니다. 그렇다면 왜 char형에서는 다르게 나왔을까요? 두번째로는 c++이 너무 똑똑해서 그렇다는 가설을 갖고 C에서 비슷한 코드를 짜보았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
 
int main() {
 
    char a[] = "hello";    
    char * ptr = a;
 
    printf("%08x\n"&a);
    printf("%s\n", a);
    printf("%c\n", a[0]);
    printf("%08x\n"&a[0]);
 
    printf("%08x\n"&ptr);
    printf("%p\n", ptr);
    printf("%s\n", ptr);
    printf("%s\n", ptr+1);
    printf("%c\n"*ptr);
}
cs

C에서는 참조자 개념이 있지 않아 참조자는 제외하고 똑같이 char형 배열로 선언한 문자열, 그리고 이 배열에 대한 포인터를 선언했습니다.

 

출력문은 다음과 같이 선언했습니다. 

1. a의 주소값을 16진수로 표현하여 출력

2. a를 문자열로서 출력

3. a의 첫 글자를 char형으로 출력

4. a의 첫 글자의 주소를 16진수로 출력

 

5. ptr변수의 주소를 16진수로 출력

6. ptr변수의 값을 ptr형으로 출력 (똑같이 16진수 주소값으로 출력됩니다)

7. ptr변수의 값을 문자열형으로 출력

8. ptr변수에 저장된 값에다 1을 더한 값을 문자열형으로 출력

9. ptr변수가 가리키고 있는 값을 char형으로 출력

 

본론으로 들어가기 전, %p형은 주소값을 8자리 16진수로 출력합니다. %x로 주소값을 출력할 경우, 앞자리가 0일때 해당 부분이 생략되어 %08x로 8자리를 맞춰주었습니다.

 

이 코드를 작성하면서 C++에서 이상하게 출력이 된 부분에 대한 힌트를 얻기 시작했습니다. 출력형이 중요한건 아닐까? 하고 말이죠. 우선 C로 작성한 코드의 실행 결과를 살펴봅시다.

16진수로 출력한 부분에 대해서는 주소값이 정상적으로 나오고, 문자열형으로 출력한 부분에 대해서는 문자열 값이 나옵니다. 눈여겨볼 부분은 다음 두줄입니다. 

 

1. ptr을 문자열 형으로 출력했을 시 해당 문자열이 정상적으로 출력

2. ptr+1을 문자열 형으로 출력했을 시, 해당 문자열의 2번째 글자부터 문자열의 끝까지 출력

 

여기까지 코드를 짜고 실행을 해본 결과, 이 현상들은 컴파일러도, 언어의 문제가 아니라 함수 자체에서 주소값을 받아도 문자열을 실행할 수 있게끔 지원을 해주는 것이 아닌가라는 생각이 들었습니다. 그래서 printf의 레퍼런스를 찾아보았습니다.

 

printf 함수 구문

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int printf(
   const char *format [,
   argument]...
);
int _printf_l(
   const char *format,
   locale_t locale [,
   argument]...
);
int wprintf(
   const wchar_t *format [,
   argument]...
);
int _wprintf_l(
   const wchar_t *format,
   locale_t locale [,
   argument]...
);
cs

여기서 중요한 부분은 format입니다. printf는 value를 바로 넣어 출력하기보단 각 자료형 형태에 맞는 format을 지정해주고, 거기에 변수를 넣는 형식(구문에서는 argument 위치)으로 사용됩니다. 그래서 많은 format이 존재합니다. 당장 위의 C 코드만 살펴봐도 format을 어떻게 지정하느냐에 따라 주소 또는 문자열이 뜨는게 달라지는 것을 확인할 수 있습니다.

 

자세한 정보를 확인하기 위해, 형식 사양 구문 레퍼런스를 읽어봤습니다.

c 및 s를 사용하여 지정된 문자 및 문자열 인수는 printf 패밀리 함수에서는 char 및 char*로 해석되고 (후략)

s를 사용하면, char 포인터 형으로 자동 해석이 되는 것을 알 수 있었습니다. 아래 형식 필드 문자가 정리된 표를 추가로 확인해봅니다.

 

형식 필드 문자

s 문자열 printf 함수와 함께 사용될 때 단일 바이트 또는 멀티바이트 문자열을 지정하고, wprintf 함수와 함께 사용될 때는 와이드 문자열을 지정합니다. 첫 번째 null 문자 직전까지 또는 precision 값에 도달할 때까지 문자가 표시됩니다.

*precision = 정밀도 값 = 출력할 크기

 

%s 필드 문자를 사용할 경우, 지정한 출력 크기에 도달하거나 \0(문자열의 끝을 나타내는 null값)에 도달할 때 까지 문자를 표시하는 것을 알 수 있었습니다. ptr의 값은 문자열의 주소, 즉 문자열의 맨 앞글자의 주소를 갖고 있었으므로 char*로 해석되면서 문자열 전체가 출력되고, ptr+1은 문자열의 두번째 글자(char형 배열이므로 1바이트 뒤의 값은 두번째 글자)를 가리키면서 두번째 글자부터 \0, 즉 문자열의 끝까지 출력되게 됩니다. 

 

결국 C에서 주소값이 문자열로 출력되는 이유는 printf가 형식 필드 문자에 따라 해석하는 법이 달라지기 때문이었습니다. 

 

그럼 이제 C++에서 왜 문자열이 출력되는지 알아봅시다. 앞에서 C는 함수의 기능 때문이라는 것을 확인했기 때문에 C++도 cout의 기능 문제가 아닐까 싶어 cout 레퍼런스를 찾아보았습니다. 찾아본 결과. 실질적으로 표준 출력 버퍼를 통해 출력시키는 것은 cout이 아닌 << operator의 역할이라는 것을 알고, 그에 대한 레퍼런스를 찾아본 결과, 중요한 사실을 알았습니다.

std::ostream::operator<<
std::operator<< (ostream)

둘 다 cout << (변수 등) 구문에 사용되는 operator인데, 애초에 operator가 선언된 위치가 다릅니다. 하나는 ostream 내부의 멤버함수이고, 하나는 std자체의 멤버함수네요. 이 두 operator는 뒤에 위치한 값에 따라 오버로딩이 다르게 진행되는데, 인자값 구분은 다음과 같습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ostream member function
ostream& operator<< (bool val);
ostream& operator<< (short val);
ostream& operator<< (unsigned short val);
ostream& operator<< (int val);
ostream& operator<< (unsigned int val);
ostream& operator<< (long val);
ostream& operator<< (unsigned long val);
ostream& operator<< (float val);
ostream& operator<< (double val);
ostream& operator<< (long double val);
ostream& operator<< (void* val);
ostream& operator<< (streambuf* sb );
ostream& operator<< (ostream& (*pf)(ostream&));
ostream& operator<< (ios& (*pf)(ios&));
ostream& operator<< (ios_base& (*pf)(ios_base&));
 
// std member function
ostream& operator<< (ostream& os, char c);
ostream& operator<< (ostream& os, signed char c);
ostream& operator<< (ostream& os, unsigned char c);
ostream& operator<< (ostream& os, const char* s);
ostream& operator<< (ostream& os, const signed char* s);
ostream& operator<< (ostream& os, const unsigned char* s);
cs

숫자와 관련된 자료형을 인자로 받는 operator <<는 ostream의 멤버 함수고,

char과 같이 문자(문자열 포함)와 관련된 자료형을 인자로 받는 operator <<는 std의 멤버 함수네요.

 

해당 operator의 레퍼런스에 파라미터에 대한 설명이 있는데, 다음과 같이 명시했습니다.

s : Pointer to a null-terminated sequence of characters (a c-string).

null-terminated sequence 형태의 포인터. 명료하게 char * 형태를 의미하네요. 괄호 안에 친절히 c-string이라고 명시하기까지 했습니다. 그리고 이 operator는 다음과 같이 수행한다고 나와있습니다.

(2) character sequence
Inserts the C-string s into os.The terminating null character is not inserted into os.The length of the c-string is determined beforehand (as if calling strlen).

terminating null(문자열의 맨 마지막 \0)을 제외하고, 나머지 문자열을 os(Output Stream)에 넣는다고 합니다. 즉, 얘도 C의 printf("%s", arg) 와 같이 주소값을 char * 값으로 알아서 해석해버리고, \0이 나올 때 까지 string을 출력 스트림에 넣어버리네요. 여기서 잠시 코드를 다시 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
int main() {
    char a[] = "hello";
    char &ref = a[1];
    char * ptr = a;
    
    cout << &<< endl;
    cout << a << endl;
    cout << a[0<< endl;
    
    cout << &a[1<< endl;
    cout << &ref << endl;
    
    cout << &ptr << endl;
    cout << ptr << endl;
    cout << *ptr << endl;
}
cs

예상과 달리 출력된 부분은 다음과 같습니다

1. &a[1]

2. &ref

3. ptr

 

1, 2번은 배열의 2번째 글자의 주소값, 3번은 배열 자체의 주소값을 갖고 있었습니다.

1, 2번은 sub-string형태로, 3번은 c-string 그 자체로 인식되어 << operator가 알아서 string 형으로 출력을 해주던거였습니다.

 

 

여기까지 오면 역으로, cout을 이용해서 문자열의 중간에 위치한 글자의 주소는 어떻게 표현하는가? 라는 질문이 듭니다. 생각보다 방법은 간단했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;
 
int main() {
    char str[] = "hello";
    char * ptr = str;
 
    cout << str << endl;
    cout << ptr << endl;
    cout << &str << endl;
    cout << (int*)ptr << endl;
    cout << (int*)&str[1<< endl;
}
cs

실행 결과

int * 형으로 타입캐스팅 시켜 출력하는 방식입니다.

cout에서도 주소값으로 해석하여, 16진수로 출력되는 것을 확인할 수 있습니다. 

 

 

 

 

 

 

간만에 포인터 문제가 있어 개념 공부를 다시 해보게 되는 계기가 되었네요. 하면서 느끼는거지만 포인터 개념이란 결국 주소값 그 자체를 가지고 노는게 아닌가 싶은 기분입니다. 

 

- 참고링크는 접은글에