정신과 시간의 방
작성일
2023. 1. 7. 13:05
작성자
risehyun
  • 컴퓨터와 메모리
    변수의 본질은 메모리이며, 모든 메모리는 자신의 위치를 식별하기 위한 근거로 고유번호를 가지는데,
    이 번호를 메모리의 주소라고 한다. 이러한 메모리 주소는 보통 16진수로 표기한다.

    변수의 선언 및 정의는 메모리 확보를 의미하며 선언시 부여한 이름으로 확보한 메모리를 식별한다.


  • 변수를 이루는 세가지 요소
    1. 이름이 부여된 메모리
    2. 그 안에 담긴 정보
    3. 메모리의 주소


단항 연산자인 주소 번지 연산자(=주소 연산자, &)를 사용하면 선택한 메모리의 주소를 가져올 수 있다.
이를 %p 형식(주소 형식)으로 출력하면 16진수로 구성된 주소가 출력된다.

 

  • 포인터 변수의 선언과 정의
    포인터 변수는 메모리의 주소를 저장하기 위한 전용 변수이다.
    모든 바이트 단위 메모리에는 고유번호(주소)가 붙어있다. 또한 이는 위치정보라고 생각할 수 있다.

    프로그래머가 메모리를 직접 다루고 싶을 때는 그 주소를 컴파일러에게 물어봐야 한다.
    이때 주소 연산자(&)를 사용하면 컴파일러는 해당 메모리의 주소를 알려준다.

    '&nData'라는 연산은 "이름이 nData인 메모리의 실제 주소는?" 이라는 의미이다.


    주소 연산자와 반대되는 개념의 연산자로 '간접지정 연산자(*)'가 존재한다.
    여기서 지정이라는 말은 임의 대상 메모리에 대한 길이와 해석방법, 즉  자료형을 지정한다는 뜻이다.
    자료형이란 일정 길이(혹은 크기)의 메모리에 저장된 정보를 해석하는 방법이라고 할 수 있다.

    그렇기 때문에 만약 메모리를 int형으로 지정한다고 한다면,
    이것은 총 네 바이트의 메모리를 한 세트로 보고 그것을 int형 변수로 취급하겠다는 말이 된다.



  • 메모리의 직접지정과 간접지정

    어떤 메모리를 이렇게 int형으로 지정하는 방법은 '직접지정'과 '간접지정'이 있다.

    여기서 직접지정이란 특정 주소에서 네 바이트 메모리를 int형 변수로 보겠다고 확정하는 방법이다.
    즉 개념적으로 직접지정한 주소를 확정하여 식별하는 방식인 것이다.
    흔히 변수를 선언 및 정의하여 메모리를 사용하는 방식이 바로 이 직접지정 방식을 말한다.

    반면 간접지정은 변경될 수 있는 임의의 기준주소로 상대적인 위치(주소)를 식별하는 방법을 말한다.
    어떤 특정 기준을 근거로 상대인 메모리의 위치를 설명하는 방법으로, 우리가 어떤 건물을 지칭할 때
    건물 주소를 직접 말할 수도 있지만 "OO 편의점 옆 건물" 과 같이 특정 기준을 가지고 지칭 할 수 있는 것과 같다.

#include <stdio.h>

int main(void)
{
	// int 형식 변수 선언 및 정의
	int x = 10;

	// 변수 x를 가리키는 int 형식에 대한 포인터 변수 선언 및 정의
	int* pnData = &x;

	printf("x : %d\n", x);

	// pnData 포인터 변수가 가리키는 대상 메모리를 int형 변수로 간접 지정하고 20을 대입한다.
	// 현재 가리키는 대상 메모리는 변수 x의 메모리이므로 x의 값이 20이 된다.
	*pnData = 20;

	printf("x : %d\n", x);


	return 0;
}
&pnData : 식별자가 pnData인 대상에 대해 단항 주소 연산을 수행함.
-> 결과적으로 이름이 pnData인 메모리의 주소가 됨.

*pnData : 포인터 변수 pnData에 저장된 주소를 가진 메모리를 int형 변수로 취급함.

 

  • 포인터의 핵심 동작?

    1. 주소를 가리킬 수 있다.

    2. 그 주소를 받는 변수를 선언할 수 있다.

    3. 그 주소 변수를 이용해서 내가 알고 있는 주소로 가서 거기에 저장된 값을 직접 수정할 수 있다.
// 포인터 변수
// 주소를 저장하는 변수 -> (*)을 붙이지 않으면 일반 변수가 된다. (*)를 붙이면 포인터 변수가 된다.

// nullptr == 0, 해당 포인터가 아무것도 가리키지 않는다는 뜻으로 사용
int* pInt = nullptr;

// 위의 포인터 변수는 int 포인터를 가리키는 변수라는 뜻이다.

위의 내용을 전제로 아래의 내용이 성립한다.

 

// 포인트 변수 선언
int i = 100;
int* pInt = &i; // i라는 변수의 주소값을 가져온다. (&) -> 주소값을 의미

 

  • 주소란?
    메모리 공간 안에 데이터들이 0,1, 0, 1... 식으로 비트파이트 단위로 있는데, 이때 이 데이터들의 위치를 지칭한다.
    예시로, 어디 공간에 뭘 넣으라고 했을 때 명령을 수행하기 위해서는 그 공간의 위치를 알 수 있어야 한다.
    그 특정 메모리의 위치를 지칭하는 것이 주소다. 이러한 주소 개념을 C/C++ 문법에서 활용할 수 있다. 

 

// 포인트 변수 선언
int i = 100;
int* pInt = &i; // i라는 변수의 주소값을 가져온다. (&) -> 주소값을 의미
                // 1. 주소를 가리킬 수 있다.(&)
                // 2. 그 주소를 저장하는 변수를 만들 수 있다 (*)


// 포인터 값 할당
(*pInt) = 100; // i라는 변수에 100이 할당된다.
               // 3. 주소 변수를 이용해서 내가 알고 있는 주소로 가서 직접 내용을 수정한다.

 

  • 포인터가 주소를 저장하는 방식은 정수 방식일까 실수 방식일까?
    포인터형 변수가 내부의 데이터를 처리할 때 어떤 방식을 사용하는지 알기 위해서는 먼저 주소의 단위를 알아야 한다.

// 주소의 단위
100;

105;

 

     100과 105 사이에 있는 5개 칸이 1개당 얼마의 메모리를 가질까? 그 주소 단위를 알아야 한다. 이때 주소의 단위는 바이트(Byte)이다. 이 1byte는 다시 8개의 bit로 쪼갤 수 있다.

 

     - 그렇다면 더 세부적인 비트 단위까지 주소를 부여할 수 있을까?

     아니다. 주소 단위 자체가 바이트이기 때문이다.

 

    - 그럼 주소를 표현하는 방식은 무엇인가?

    실수가 아닌 정수 타입이다. 따라서 100번과 105번 사이에 100.5번지 같은 실수는 존재할 수 없다. 이와 같은 이유 때문에 더더욱 비트단위로 주소를 보유할 수 없다. 만약 아래처럼 100번과 102번 주소가 있다고 가정하면 이 둘 사이에는 2byte의 여유 공간이 존재한다.

 

     - 왜 포인터 변수를 선언할 때 자료형을 적어줘야할까?
       포인터 변수가 가지고 있는 정보는 주소밖에 없기 때문에 그 주소를 찾아갔을 때 int가 있는지 char가 있는지 알 수 없다. 따라서 포인터 변수는 선언할 때 어떤 자료형의 포인터인지 알려주어야한다. 해당 주소에 가서 어디에 접근해야하는지 알려주기 위함이다. (자료형이 int라면 4Byte에 접근)


       즉, 포인터 앞에 선언되는 자료형의 의미는 자기가 가리킬 데이터의 타입을 정해놓는 것이다. 그렇기 때문에 접근 시에 이 타입의 사이즈에 딱 맞춰서 접근할 수 있는 것이다. 

 

       하지만 여기서는 단순히 사이즈만 문제가 되는 것이 아니다. int변수는 정수형 표현방식이다. 그것까지도 포함되어 있다. 실수와 정수는 비트 단위에서 표현하는 방식 자체가 다르기 때문에 이를 구분하는 것은 매우 중요하다. 정수 표현 방식은 비트 수를 이진법 기준으로 하지만 실수는 가수, 지수 등으로 부동소수점을 사용하여 완전히 다르게 수를 표현한다.

예를 들어 다음과 같은 변수가 있다고 가정하자.

 

float f = 3.f;

위의 변수는 정수 3이 아닌 실수 3을 나타낸다. 만약 이것을 동일한 4byte 사이즈를 가진 int 포인터 변수에서 접근하도록 하면 어떻게 될까?

 

int i = 100;
float f = 3.f;

int* pInt = &f; // int 포인터에 실수 타입 변수 주소에 접근하도록 하면 어떻게 될까? 오류가 뜬다.

// 주소로 접근
(*pInt) = 100;

바로 컴파일러에서 오류를 표시한다. 서로 타입이 맞지 않기 때문에 문법차원에서 방어를 해주는 것이다.

하지만 아래 코드처럼 강제 캐스팅을 이용하면 억지로 f 주소값을 int 포인터 변수에 넣을 수 있다.

 

int* pInt = (int*)&f;

이 경우 오류는 뜨지 않는다. 결과적으로 실수값의 주소가 int 포인터 변수에 들어오게 된다.

이 변수는 애초에 int 주소 변수만 받겠다고 선언한 상태지만 강제 캐스팅로 인해 실체가 float가 된 것이다.

 

int i = 100;
float f = 3.f;

int* pInt = (int*)&f;

// 주소로 접근
i = *pInt;

이때 디버깅을 해보면 i에 엄청 큰 숫자가 들어가있음을 알 수 있다. int포인터는 주소를 받아서 갔을 때 그것을 int로 간주하겠다는 의미를 가지고 있는데 그곳에 int가 아닌 float 주소를 넣었기 때문이다.

앞서 말했듯, int인 정수 표현방식과 float의 실수 표현 방식은 다르다. float에는 실수표현방식으로 실수 3을 표현하기 위한 비트가 들어가있는데 그것을 int 정수 형식으로 해석해버리니 잘못된 숫자가 들어가게 되는 것이다.

 

정리하면 다음과 같다.

<포인터 변수 자료형 선언>
자료형 + * EX. int*
(*)이 붙었으므로 이것은 포인터 변수가 된다.
이때 앞에 붙는 자료형의 의미는 바로 해당 포인터 변수에게 전달된 주소를 해석하는 단위이다

'C,C++ > [강의] 어소트락 C++' 카테고리의 다른 글

포인터 문제 풀이  (0) 2023.01.14
포인터 배열  (0) 2023.01.09
운영체제  (0) 2023.01.06
정적 변수/외부 변수  (0) 2023.01.04
분할 구현  (0) 2023.01.01