본문 바로가기

C++

[TIL 8장] 포인터, 상수 포인터

오늘의 TIL 목차 (22.08. 03)

 

  • 포인터 ( feat. 포인터 연산자 ' * ', ADDRESS 연산자 ' & ' )
  • 상수 포인터

포인터


포인터

주소값을 저장하는 용도의 변수
자료형* 변수명;

[포인터 선언 방식]
int* p; // 주로 사용
int *p;
int * p;
[포인터 0 초기화] // 포인터는 0 대신 nullptr로 선언해주기로 약속
int*	p = nullptr; // 주로 사용
int*	p = NULL; 
int*	p = 0xffffffff; // 32비트 기반 프로그래밍에서 주소 값 중 최소는 0번지 최대는 0xffffffff번지이다.

 

 

포인터는 '대입(갖다)'의 개념이 아닌 '가리키다' 개념

■ 포인터 사용 이유 / 주의

- 포인터는 주소를 저장하는 변수기 때문에 &를 이용하여 주소값을 받아야함

- 포인터 사용 이유는 주소값을 이용해 메모리 공간에 접근해서 직접 값을 갖고오거나 변경하기 위해서

☞ 이름을 통한 접근은 매개변수가 '복사'로 이뤄지기 때문에 속도가 느려짐

☞ 이름을 통한 접근은 접근 권한이 중괄호 안에서만 있음 (벗어나면 사라짐)

☞ 이름을 통한 접근 사용 시 전역변수를 사용하면 문제가 해결되지만 추후 코드가 복잡해짐

■ 포인터는 간접 참조 ( =주소를 통한 접근 )

: 포인터라는 영역을 통해서 주소에 접근하여 값을 가져오거나 변경하므로 간접 참조이다. (직접 참조 X)

- VMS 주소는 0xn 형태, 16진수 정수와 형태가 동일하여 컴파일러가 구분을 못함

  ☞ 포인터라는 문법을 둘테니까 포인터 문법 뒤에 16진수 정수가 오면 그건 메모리 주소로 인식해라고 정해둠

 

- 컴퓨터는 1byte당 하나의 주소를 가짐

- int 는 4byte로 주소값 4개, 첫번째 주소가 대표주소로 포인터로 대표 주소 부르면 해당 메모리 공간 전체 불러옴

- 프로그래머는 VMS의 주소만 사용 가능, 포인터를 이용해 RAM 주소에 접근해서 사용

- 포인터는 변수적 속성을 가짐으로 코드 흐름에 따라 값 / 주소 변경 가능 ( 심볼릭 상수 const 가능 )

 

■ 포인터에 사용되는 연산자

& (ADDRESS 연산자 / 주소 추출 연산자)

: 할당된 메모리 공간의 첫 번째 주소를 추출하여 반환하는 연산자

 

  • &변수명 형태로 사용, 해당 변수명의 할당된 메모리 공간 중 시작 주소를 추출하여 반환
  • 포인터는 주소를 저장하는 함수이기 때문에 &변수명 으로 변수의 주소를 보내야 한다.
  • &iTemp 는 수정 불가능 ( ++(&iTemp); , &iTemp = ptr; , &iTemp += 3; 다 불가능 )

* (포인터 연산자) 

: 주소를 통해서 메모리 공간의 접근하는 연산자

 

  • *포인터변수명 형태로 사용, 해당 포인터가 가리키고 있는 (갖고있는) 시작 주소의 메모리 공간에 접근
  • int* ptr = &iTemp; 일 때, ptr은 iTemp의 주소 - *ptr은 iTemp의 값 (*ptr 값 변경 시 iTemp 값도 변경됨)
int iTemp = 0;
int* p = &iTemp;

cout << (*p) << endl; // *p는 iTemp 주소의 메모리 공간 즉, iTemp
*p = 40; // 포인터 p가 가리키는 메모리 공간에 40을 대입해라 (*가 붙었으므로)
cout << iTemp << endl; // *p가 가리키는 주소가 iTemp의 주소이므로 *p 값 변경은 iTemp 값 변경임

출력 결과:
0
40

 

■ 포인터의 크기

- 포인터의 크기는 x비트 기반 프로그래밍에 따라서 크기가 달라짐

- "32비트 기반 프로그래밍" 기준 포인터의 크기는 4byte

// 32비트 기반 프로그래밍 기준 포인터 주소의 크기
bool bCheck = true;
bool* ptr = &bCheck;
cout << sizeof(bool*) << "\t";
cout << sizeof(short*) << "\t";
cout << sizeof(int*) << "\t";
cout << sizeof(long long*) << endl << endl;

cout << sizeof(ptr) << "\t"; // 포인터 ptr의 주소 크기는 4byte
cout << sizeof(*ptr) << "\t"; // 포인터 ptr의 메모리 공간(=bCheck)은 1byte(자료형 bool)
cout << sizeof(&bDest) << endl; // bDest의 주소 크기는 당연히 4byte

출력결과:
4 	4 	4 	4
4 	1 	4

→ 포인터( bool*, int* ,,) 자료형 자체의 크기는 4byte (주소를 저장하는 공간)

→ sizeof(ptr)은 포인터 ptr이 가리키고 있는 주소의 자료형 크기(가리키는 주소가 int형이면 4byte, bool형이면 1byte)

 

■ 포인터 연산

int iTemp = 10;

int* ptr = &iTemp;

cout << ptr << endl; // 포인터 ptr(=&iTemp)은 주소값 출력

*ptr = 20; // *ptr(=iTemp)이므로 iTemp = 20과 같음

++(*ptr); // ptr의 메모리 공간 안에 있는 값 20 에 ++ 즉, 21
ptr++; // ptr의 주소를 이동(int형 타입 주소니까 4byte만큼 증가 이동)
ptr += 2; // ptr + 2 는 ptr 주소 2번 이동 (int형 타입 주소니까 4 * 2 = 8byte 이동)

// *ptr = 30; //이동된 메모리 공간은 변수명이 할당되지 않은 상태, 값을 넣긴하지만 논리적 오류

cout << ptr << endl; // iTemp 주소에서 총 3번 이동된 주소 출력

 

포인터 ptr과 *ptr의 차이

■ 포인터를 '언제' , '왜' 사용하는지에 대한 예시

- Swap 함수 ; 매개변수 포인터를 이용한 예시

void	pointer_Swap(int* _pA, int* _pB);
void	Swap(int _iA, int _iB);

void main(void)
{
    int iA = 10, iB = 20, iC = 100, iD = 200;
    pointer_Swap(&iA,&iB); //매개변수가 포인터이므로 주소값으로 넣어줘야함
    Swap(iC,iD);
    cout << iA << "\t" << iB << endl;
    cout << iC << "\t" << iD << endl;
}

void	pointer_Swap(int* _pA, int* _pB) //포인터를 사용한 경우
{
	int iC = 0;

	iC = *_pA;

	*_pA = *_pB;

	*_pB = iC;
}

void	Swap(int _iC, int _iD) // 포인터를 사용하지 않은 경우
{
	int iTemp = 0;

	iTemp = _iC;

	_iC = _iD;

	_iD = iTemp;
}

출력 결과 : 
20		10 // swap 됨
100 	200 // swap 안됨

☞ 원리

Pointer_Swap() 함수

1. main()함수 iA와 iB의 주소값을 매개변수로 받아옴.

2. main()함수 포인터(*)를 통해 iA와 iB의 메모리 공간에 접근하여 해당 주소 안의 값을 직접 교체

Swap() 함수

1. main()함수의 iC와 iD의 값을 복사하여 매개변수로 받아옴

2. 매개변수(Swap의 지역변수) iC와 iD의 값을 바꿔줌 - iC, iD는 복사본

3. Swap 함수가 종료되면서 Swap()함수의 iC와 iD는 소멸됨

→ 복사해서 받아온 매개변수를 Swap 함수 안에서 복사본으로 변수들을 서로 교체하다가 함수 끝나면서 소멸 )

→ 만약 return을 한다면 임시 저장 장치에 복사해놓고 Swap() 함수의 변수들은 소멸, main()으로 돌아가 Swap(iC, iD); 라인 부분만 반환값을 돌려주고 해당 라인을 벗어나면 반환값도 소멸 즉, main() 자체의 iC와 iD 값(원본)은 그대로

4. main()으로 돌아오면 cout << iC << iD; 는 기존의 iC, iD 값 그대로 100 200 이 출력됨 

 

상수 포인터


[ 상수 포인터 ]

: const의 위치에 따라 값 또는 주소를 상수화 시키는 포인터

 

int* const ptr = &iDest; // 상수 포인터 : 오로지 하나의 주소값만 가질 수 있는 상수화가 이뤄짐

const int*	ptr = &iDest;	// *ptr 가리키는 값이 오로지 30으로 고정 (읽기 전용만 사용 가능)
const int* const ptr = &iDest; // 포인터 ptr의 값과 주소 둘 다 고정

 

■ 예시

int		iDest = 30;

// 상수 포인터는 주소 변경 불가능
int* const icPtr = &iDest; // 상수 포인터 : 오로지 하나의 주소값만 가질 수 있는 상수화가 이뤄짐
const int*	ciPtr = &iDest;	// *ptr 가리키는 값이 오로지 30으로 고정, 주소 변경은 가능하지만 변경된 주소의 값은 30이 아닌 쓰레기값 출력됨
const int* const ccPtr = &iDest; // 주소와 값 둘 다 %iDest, 30 으로 고정

cout << icPtr << endl; // 상수 포인터 주소 출력
cout << ++(*icPtr) << endl; // 상수 포인터 값 증가 -> 30 + 1 = 31 출력
// cout << ++icPtr << endl; 상수 포인터는 주소 변경 자체가 안됨

cout << ciPtr << endl; // 값 고정 포인터 주소 출력
cout << ++ciPtr << endl; // 값 고정 포인터 주소 증가 (4byte 이동)
cout << *ciPtr << endl; // 값 고정 포인터(주소 이동됨) 값 출력 -> 고정 값 30이 아닌 쓰레기값 출력

값이 고정된 상수 포인터 ( const int* ptr )에 대한 자세한 설명

int iA = 10;
const int* ptr = &iA; // 값 고정 상수 포인터는 포인터를 통한 읽기 모드만 가능
// *ptr = 30; // 값이 고정된 상수 포인터는 포인터를 통한 값 변경 불가능
iA = 30; // 포인터를 거치지 않은 직접 값 변경은 가능
cout << *ptr << endl; // 포인터를 통하지 않고 값 변경을 했으므로 값 변경된 30으로 출력

++ptr; // 값이 고정된 포인터의 주소를 이동할 경우
cout << *ptr << endl; //  iA의 다음에 들어있는 쓰레기값 출력됨
// 포인터 ptr은 값 고정 상수 포인터이기에 이동된 변수에 있는 쓰레기값을 해당 주소에 고정시킴

→ 값을 고정한 상수 포인터는 포인터로 접근한 경우, 해당 주소는 해당 값만 읽을 수 있다는 것을 의미. 포인터를 통해서 값을 변경할 수 없고 ( *ptr = 30 ; ), 다른 접근 방식으로는 값 변경 가능 ( iA = 30; ) 

→ 값 고정 상수 포인터 주소가 이동한 경우, 이동한 주소의 값을 읽어들이고 그 값도 포인터를 통해서는 값 고정 시켜버림. ( ex. ++ptr; 은 쓰레기값이 들어있는데 그 쓰레기값도 고정시켜서 해당 주소에서는 포인터 접근으로는 읽기밖에 안됨, 변경 불가능 )