프로그래밍 지식/C++

포인터, 참조자(Pointer, Reference) C / C++

게임이 더 좋아 2022. 1. 27. 20:46
반응형
728x170

물론 참조변수에서 이미 알아봤지만 포인터와 함께 알아보겠다.

포인터와 참조자는 비슷한 맥락에서 쓰이지만 조금 다르다.
-> 포인터와 참조자의 다른점을 정확히 설명할 수 있다면 정말 아는 것.

여기서 짧게 설명하자면 

참조자는 한 번 참조되어 정의되면 다른 객체를 참조할 수 없어서

 항상 그 참조자와 처음 결합한 객체만 참조한다.

 


포인터에 대입하면 참조자가 아닌 다른 변수처럼 포인터 자체에 새로운 값이 대입된다.

즉, 다른 객체를 가리킬 수 있다.

 

[C || C++ 문법/함수] - C++문법/ 참조 변수, reference + 참조의 특성

알아보자

 


 

우선 먼저 포인터에 대해서 한마디 설명해보자.

 

포인터는 어떠한 변수를 가리키는 변수다.

?? 엥 변수를 가리키는 변수??

 

우선 변수는 말했듯이 변할 수 있는 데이터를 저장하는 메모리의 공간이라고 했다.

포인터는 변수의 공간이 어디있는지를(주소를)저장하는 메모리의 공간이다.

 

?? 엥 그럴거면 처음부터 변수로 만들지 굳이 주소를 따로 저장해야해?????

그냥 변수에 데이터 저장하고 그 변수를 쓰면 되는거지..

굳이 그 변수의 주소를 따로 또 저장해서 그 포인터를 써야하나..??

 

물론 우리가 짜는 프로그램에서는 그 변수에 대해 항상 알고있기 때문에 가능한 이야기다.

하지만 다른 프로그램에서 사용한다던가.. 다른 파일에서 참조한다든가 등의 이유로

우리는 변수의 이름만 알 수 있지 변수가 메모리 상에 어디있는지를 모른다.

즉, 파라미터로 int x를 줘봤자.

int x를 복사해서 연산하는 것이지 실제로 x에 접근하여 값을 가져온 것이 아니란 말이다.

 

다시 말하자면 우리는 진짜로 해당 변수에 대한 접근을 위해 포인터를 만들고

그 변수의 주소를 포인터에 담는 것이다.

 


 

그렇다면 참조자(참조 변수)는 무엇이냐???

해당 변수에 대한 다른 이름을 선언하는 것이다.

 

잠초자는 객체에 대해 별칭을 정하는 것으로 참조변수라고도 말한다.

참조변수는 &을 자료형과 같이 좌측에서 쓰여서 쉽게 구별가능하다.

즉, 주소 연산자로 우측에 쓰이지 않아서 구분이 쉽게 가능하다.

 

int ival = 1024;
int &refVal = ival; //refVal은 ival을 참조하는 변수가 되는 것이다.
int &refVal2; //참조자는 항상 참조할 변수와 같이 선언되지 단독으로 선언되지 않는다.(초기화와 선언이 같이 일어나야만 한다)



즉, refVal과 ival은 같다.

철수의 별명이 찰스라면 철수를 부르나 찰스를 부르나 철수가 대답할 것이다.

즉, 철수와 찰스는 이제 같다. 

찰스한테 무언가를 시키면 철수한테 무언가를 시킨 것과 같다.

int i = 1024, i2 = 2048; //둘 다 int로 선언
int &r = i, r2 = i2;이다. // r은 i의 참조변수, r2는 그냥  값이 i2인 int 변수
int i3 = 1024, &ri = i3; // i3는 int, ri는 i3의 참조변수



참조자의 타입과 그 참조자가 참조하는 객체의 타입은 항상 일치해야 한다.

그리고 참조자는 객체하고만 결합할 수 있다. (상수의 참조자는 생길 수 없다)

하지만 영희의 별명도 찰스가 된다?

하지만 이미 찰스는 철수의 참조변수이므로 쓸 수 없다.

한 번 참조변수로 초기화가 되었다면 다른 객체를 참조할 수는 없다.

** 참조자 자체는 객체가 아니기 때문이다.

 


 

포인터에 대해서 더 알아보자.

 

포인터는 대부분 변수 앞에 *을 붙임으로써 선언된다. 

물론 타입에다 int* ptr 과 int *ptr은 같다.

 

포인터는 다른 객체의 주소를 담고 있다.

그리고 **선언할 때에는 포인터 타입과 포인터가 가리키는 타입을 일치시켜줘야 한다.

 

포인터에 주소를 넣어야 하는데 그 주소는 & 연산자로 얻는다.

객체의 주소는 &(앰퍼샌드, address operator)로 얻는다.


포인터는 대입하거나 복사하기가 가능하고

포인터 하나가 Lifecycle 동안 여러 다른 객체를 가리킬 수 있다.

 

**포인터를 정의할 때는 초기화 하지 않아도 되는 점이 참조자와 다른 점이다.

**하지만 포인터를 구역 사용 범위에서 정의하면서 초기화 하지 않으면 여느 다른 변수와 같이 junk 값을 가진다.

 

포인터가 주소를 가리키지만 그 주소는 4가지 상태 중 하나로 존재한다.


1. 객체의 주소
2. 어느 객체의 마지막 요소 다음 위치
3. 어느 객체도 가리키지 않는 nullptr
4. 유효하지 않은 주소 값, 1,2,3을 제외한 값.

 


더군다나 4번의 경우 유효하지 않은 포인터 값을 복사하거나 접근하는 것은 오류가 된다.
또한 2,3번의 경우에서도 포인터가 가리키고 있는 것은 객체를 가리키는 것이 아니므로 접근해서는 안된다.


포인터로 객체를 가리키고 있으면 역참조 연산자(*, deference operator)를 사용해서 객체에 접근이 가능하다

더보기

예를 들어 *p = i //이 문장은 p가 가리키고 있는 객체에 i를 대입하는 것이다.

 

그래서 초기화를 할 때 대개 nullptr로 하곤 한다.

nullptr, 널 포인터는 어느 객체도 가리키지 않으므로

포인터를 사용하기 전에 null체크를 통해 접근이 가능한지 확인한 후 이용한다.

 

널 포인터를 만드는 방법이 몇가지 있다.

 

int *p1 = nullptr;  //이 문장은 int *p1 = 0과 같다. -> 일반적으로 많이 쓰고 이렇게 쓰는 것이 좋다.
int *p2 = 0;
//다만 아래 문장은 #include <cstdlib>을 해야 가능하다.
//전처리기 변수에서 NULL을 0으로 정의하기 때문이다.
int *p3 = NULL;  //이 문장 또한 int *p3 = 0과 같다.

 

즉, 포인터에 0을 대입하는 것은 바로 널 포인터를 만들겠다는 것이다.

다만 초기화 시가 아니라

변수의 값이 0일 때 

해당 변수를 포인터에 대입하는 것은 금지되어있다.

 

int z = 0;
pi = z; //변수는 포인터에 대입할 수 없기 때문이다. -> 오류

 

그리고 초기화 시키는 가장 중요한 이유가 있다.

포인터는 무조건 초기화시키고 사용해야 한다. 

-> 즉, 해당 포인터를 초기화 시키지 않았다면.. 유효한지 판단불가능이다.
-> 왜냐하면 미초기화 포인터라도 값을 가지게 되는데 해당 값은 대부분 그 포인터가 위치한 메모리의 비트 내용을 주소로 사용하므로 해당 주소의 객체에 접근한다.
즉, 해당 객체에 대한 접근이 유효한 것인지 아닌지 모른채로 접근하게 되는 것이다.
-> 틀려도 접근, 맞으면 우연히 맞음 -> 오류 발생함.

 

그렇기 때문에 포인터가 가리키는 객체가 바로 정해지지 않았다면 nullptr로 초기화 시켜야한다.

 

int i = 42;
int *pi = 0; //nullptr
int *pi2 = &i; //i의 주소로 초기화
int *pi3; //초기화 안함
pi3 = pi2; //pi3는 pi2가 가리키는 객체인 i를 가리키게 됨
pi2 = 0; //pi2는 이제 아무 객체도 가리키지 않음.

 

항상 바뀔 수도 있으므로

대입할 때 포인터가 바뀌는지, 포인터로 가리키는 객체가 바뀌는지 알기 어렵다.

따만 대입을 할 경우에는 왼쪽 피연산자가 어떤 식으로든 바뀐다는 점이다.

pi = &ival; // pi 값이 바뀌어서 이제는 ival을 가리킨다.

*pi = 0; // pi가 가리키는 ival의 값이 0으로 대입되는 것이지 pi가 가리키는 객체가 바뀐 것은 아니다.



포인터 값이 유효하다면 조건에도 쓸 수 있다.

하지만 포인터가 0이라면 false라고 판단한다.


예를 들어보자

int ival = 1024;
int *pi = 0;
int *pi2 = &ival;
if(pi) //0, nullptr이므로 false가 됨
...
if(pi2) //0이 아니므로 true가 됨.
...



또한 포인터 간의 == 연산이나 != 연산도 가능하며 true, false를 반환한다.

 






특별한 포인터가 있는데

void* 포인터이다.

모든 객체에 대한 주소를 가리킬 수 있는 특별한 포인터다.
다른 포인터처럼 주소를 담지만 이 친구는 가리키는 객체의 타입은 모른다.

double obj = 3.14, *pd = &obj; //double 변수 obj 선언 정의, double 포인터 pd 선언, 정의

void *pv = &obj;//obj가 double 이지만 void 포인터는 모든 객체를 가리킬 수 있어서 가능

pv = pd; //pv는 pd가 가리키는 주소도 가리킬 수 있다.



하지만 void* 포인터가 하는 일을 제한적이다.


다른 포인터와 비교하거나 함수에 전달하거나 반환받으면서 다른 void* 포인터에 대입할 수 있지만
void*를 사용해서 이 타입으로 가리키는 객체에 대한 연산은 불가능하다.

-> 당연히 가리키는 객체의 타입이 정해지지 않았기 때문이다.

그래서 일반적으로 void* 포인터가 쓰이는 경우는

그 포인터를 사용하여 메모리에 저장되어 있는 객체에 접근할 때보다 메모리를 메모리로 다루기 위해 쓴다.

메모리의 주소만 저장한다고 보면 된다.

 




참조자와 포인터를 섞어쓰면 아주 어렵게 되는데 예를 통해 쉽게 알아보자

 

int i = 10;
int *p; //포인터 선언

int *&r = p; //p의 타입이 포인터이기 때문에, 참조자 r은 p(포인터)를 가리키게 된다.
r = &i; // r과 p는 같다고 보면 되므로, r이 i의 주소를 가리킨다는 것은 p가 i를 가리킨다는 것과 같다.
*r = 0; // r의 값 연산자(*)이 붙었으므로 p가 가리키는 i의 값을 0으로 대입한다는 말과 같다.



반응형
그리드형