본문 바로가기
Script/C++

[C++] Chapter 02. 포인터#1 (기초, Reference)

by song.ift 2023. 5. 11.

포인터 기초 #1

포인터를 사용하기 전 사용한 방식

int number = 1;

  • number라는 이름의 4바이트 정수 타입의 바구니를 만든다.
  • number라는 변수 스택 메모리에 할당
  • number = 1라 함은, number 바구니에 1이라는 숫자를 넣으라는 의미.
  • 따라서 스택 메모리에 있는 특정 주소(number 바구니)에 우리가 원하는 값을 넣은 셈
  • number는 비유하자면 메모리에 이름을 붙인 것 (찰떡같이 알아들어서)
  • 나쁘지 않고 편리한데, 단점은 원본 수정이 불편하다.

포인터는 바구니는 바구니인데.. [주소를 저장하는 바구니]다.

참고) 포인터는 개발환경이 64비트 32비트냐에 따라 8바이트 4바이트 고정 크기로 설정된다.

 

int value 1 = *ptr

이걸 디스어셈블리로 보면

💡 00344A4F mov              eax, dword ptr    [ptr]
      00344A52 mov              ecx., dword ptr   [eax]
      00344A54 mov              dword ptr            [value], ecx
  1. ptr이라는 스택에 저장된 변수주소에서 값을 꺼낸 다음 eax 레지스터에 넣어준 다음,
  2. eax에 다시 들어가 한번 더 워프해서 그 내용물을 ecx에 꺼내준다.
  3. 최종적으로 ecx에 있는 값을 value라는 바구니에 전해준다. 총 두번 건너건너 한 셈

 

*ptr = 2

💡 00344A57 mov              eax, dword ptr    [ptr]
      00344A5A mov              dword ptr           [eax], 2

Ptr에 있는 값을 eax 레지스터에 넣어둔 다음에, 해당 주소로 이동해서 2를 넣어줬다.

 

지역 변수와 값 전달

포인터에 TYPE은 왜 붙여줄까?

*  =  포인터의 의미  =  주소를 저장하는 바구니  =  4 or 8바이트 고정 크기

근데 주소만 주면 뭐가 있는지 알 수가 없고, 그게 뭔지 유추할 수 있어야 한다. 사용을 할려면 무엇이 들어가있는지 알아야 한다는 것.

// 0b101010111     int *ptr;

어떤 데이터가 2진수로 저장되어있다고 가정되었을 때, 정수로 볼것이냐, 문자로 볼것이냐 어떤식으로 얘를 분석할것이냐에 따라 의미가 달라진다. 심지어 메모리 기준으로 1바이트 or 2바이트 or 4바이트로 볼것인지에 따라 숫자가 달라질 수도 있다.

실제로 메모리 창을 통해 보면 1바이트 정수, 2바이트 정수, 8바이트 정수에 따라 읽는 값이 달라질 수 있다. 주소 자체만으로는 불충분하고, 거기 가서 몇바이트를 꺼내서 사용해가지고 개를 정수인지 실수인지 분석하는거에 따라 데이터 인식이 달라진다.

void SetHP(int *hp)
{
    *hp = 100;
}
// [매개변수][RET][지역변수(hp=1)]  [매개변수(주소)][RET][지역변수]

int main()
{
    int hp = 1;
    SetHP(&hp);
}

 

포인터 연산

산술 연산자

int* pointer = &number;

number += 1; // 1증가했다

pointer += 1; // 4증가했다 (int니 4바이트)

  • 포인터에서 +나 -등 산술 연산으로 1을 더하거나 빠면,
  • 정말 ‘그 숫자’를 더하고 뺴라는 의미가 아니다.
  • 한번에 TYPE의 크기만큼을 이동하라!
  • 다음/이전 바구니로 이동하고 싶다 << [바구니 단위]의 이동으로
  • 즉, 1을 더하면 = 바구니 1개 이동시켜라

간접 연산자

struct Player
{
    int hp; // offset + 0
    int damage; // offset + 4
}

(*playerPtr).hp = 200;

playerPtr이라는 주소로 이동을 한 다음에 오프셋 +0에 해당하는 주소에 값을 넣어준거임

 

포인터 실습

💡 [매개변수][RET][지역변수(temp(c,c,c), player(b,b,b))] [매개변수(&temp)][RET][지역변수(ret(100, 10, 2)]

Player = CreatePlayer();

포인터를 안쓰고 지역변수 리턴을 통해 값을 바꾸게 되는 위 방법은 사실 어셈블리로 통역해보면

StatInfo temp = CreatePlayer();
Player = temp;

중간 단계를 거쳐서 복사 두번이 일어난 셈. 이유는 정확히 모르고 컴파일러가 그렇게 만들어준거니까 최선의 방법으로 생각했나보다.

[매개변수][RET][지역변수(monster(40,8,1))] [매개변수(&monster)][RET][지역변수]

CreateMonster(&monster);

Player = monster;

구조체 복사가 한줄이라 별거아닌 것처럼 보이지만 어셈블리로 보면

Player.hp = monster.hp;

Player.attack = monstyer.attack

한줄씩 복사하는 거랑 다를게 없다. 빠르지가 않다는 것.

어마어마한 크기일수록 크니 복사하는것도 조심해서 사용해야한다.

 

참조 기초

int& reference = number;

참조는 로우레벨(어셈블리) 관점에서 실제 작동 방식은 int*와 똑같음

C++ 관점에서는 number라는 바구니에 또 다른 이름을 부여한 것.

*참조의 주소

&reference라고 메모리에서 검색하면 포인터와 달리 &number 주소로 가질 것이다.

이는 참조라는 것은 C++ 단에서 있는 존재이지 사실 어셈블리 레벨로 가면 참조라는 개념은 딱히 존재하지 않는다.

따라서 우리가 C++ 코드에서 reference를 사용하게 되면, 컴파일 후에는 마치 원래 변수를 사용한 것 마냥 치환해버려도 된다는 의미이다. (위에서 number 주소로 가는 이유도 그런 것)

반면 함수에서 reference를 함수의 인자로 받는 형태로 만들고, 실제 함수 호출하는 시점에 breakpoint 걸고 멈춰서 살펴보면, 포인터처럼 주소값이 함수 인자로 들어가 있는 것을 확인할 수 있다.

💡 결론 : 참조는 '진퉁' 그 자체를 건드린다는 개념을 위배하지 않는 한 컴파일러가 문법에 맞게 알아서 만들어준다. 따라서 일반적으로 말하면, 별도의 메모리를 할당하진 않는다.

StatInfo구조체가 1000바이트 짜리 대형 구조체라면?

  • (값 전달) StatInfo로 넘기면 1000바이트가 복사
  • (주소 전달) StatInfo*는 8바이트
  • (참조 전달) StatInfo&는 8바이트

참조를 어셈블리로 동작하는 걸 보면 포인터와 방식이 완전 일치한다.

즉 객체를 넘기는 것처럼 보이는 외관과는 다르게, 실제로는 주소를 넘기고 있는 것이다.

*참조형의 크기

참조형이 '내부적으로' 주소를 받는 것이지만 C++ 관점에서는 '원본 객체의 별칭' 정도로 봐야 하고 주소와 관련된 내용은 숨겨져 있다.

따라서 sizeof를 하면 주소가 아닌 해당 객체 자체의 sizeof를 해주기 때문에 저런 결과가 나온 것이다.

별도의 객체를 임시 생성하는 것은 절대 절대 아니다.

 

포인터 vs 참조

💡 성능 : 똑같음
      편의성 : 참조 승

편의성 관련

편의성이 좋다는게 꼭 장점만은 아니다.

포인터는 주소를 넘기니 확실하게 원본을 넘긴다는 힌트를 줄 수 있는데, 참조는 자연스럽게 모르고 지나칠 수도 있음

ex) 마음대로 고친다면

       = const를 사용해서 마음대로 고치는 부분 개선 가능. 대부분 set로 붙여씀

참고로 포인터도 const를 사용해서 개선 가능.

* 앞이냐 뒤냐에 따라 의미가 달라지긴 함

초기화 여부

참조 타입은 바구니의 2번째 이름

  • 참조하는 대상이 없으면 안됌

반면 포인터는 그냥 어떠한 주소라는 의미

  • 대상이 실존하지 않을 수도 있음. nullptr 가능하다는 말
 💡 그래서 결론은?
       팀바팀. 정해진 답은 없다.
       ex) 구글에서 만든 오픈소스를 보면 거의 무조건 포인터 사용
       ex) 언리얼 엔진에선 reference도 애용

 

선호 스타일

없는 경우도 고려해야 한다면 pointer (null 체크 필수)

바뀌지 않고 읽는 용도(readonly)만 사용하면 const ref&

그 외 일반적으로 ref (명시적으로 호출할 때 OUT을 붙인다)

외부에서 값 변경을 할 경우가 있다면 pointer. (하지만 거의 은닉화)

단, 다른 사람이 pointer로 만들어놨다면 계속해서 pointer 사용. 섞어서 쓰지 않는다.

댓글