[C++] const와 포인터

2024. 3. 14. 08:13Programming Language/C++

아래 링크 클릭 시 해당 본문으로 이동

const

const와 포인터

const와 포인터를 쓰는 이유


const

• `const` : 변동되지 않는 상수값

상수

 값이 고정되어 변하지 않는 수

int main()
{
    20;
    20 = 30;  // 오류 => 상수를 상수를 넣을 수 없다.
  
    return 0;
}

l-value, r-value

 l-value : 바뀔 수 있는 값 (변수)

 r-value : 바뀔 수 없는 값 (상수)

상수는 변수처럼 값을 변경할 수 없다.

상수화

 값이 바뀔 수 없는 상태

변수 앞에 붙으면 해당 변수는 상수화가 된다.

int main()
{
    const int cInt = 100;
    cInt = 50;  // 오류
  
    return 0;
}

`const` 키워드를 붙여서 `cInt`를 상수로 만들었다.

`20`이 `20`인 것처럼 `cInt`는 그냥 `100`이다.

상수가 된 `cInt`에 값을 넣으려고 했기 때문에 오류가 발생한다.

오류 (cInt)

상수화된 변수의 값을 바꿀 수는 있다.

상수화된 변숫값을 절대 바꿀 수 없는 것은 아니다.

변수를 상수로 만들어도 주소로 접근하면 변경이 가능하다.

바로 접근해서 값을 변경하면 오류가 발생하므로 상수화된 변수를 해당 자료형 포인터로 캐스팅하면 된다.

#include <stdio.h>

int main()
{
	// int형 변수 cInt에 100을 넣고, 그 주소를 int 포인터 변수 pInt에 넣기 위해 (int*)로 캐스팅한다.
	const int cInt = 100;
    int* pInt = 0;
    
    pInt = (int*)&cInt;  // int*로 캐스팅
    *pInt = 50;  // cInt값을 50으로 변경
    
    printf("cInt 출력 : %d\n", cInt);

	return 0;
}

 

캐스팅하지 않으면 오류가 발생한다.

pInt = &cInt;  // 오류 => int*로 캐스팅 해줘야 한다.

오류


주소로 접근하여 값을 `50`으로 변경했고 로컬 창에도 `50`으로 변경되어 있는데, 콘솔 창에는 값이 바뀌지 않은 채로 출력됐다.

로컬 창(cInt는 50)
콘솔 창(cInt는 100)

바꾼 `cInt`값이 출력에 반영되지 않은 이유

결론부터 말하자면 컴파일러가 최적화 옵션을 사용했기 때문이다.

컴파일러는 const 키워드가 붙은 `cInt`를 변경할 수 없는 값으로 처리하여 레지스터에 임시로 저장해 놓은 `100`을 전달한다.

• 레지스터 메모리 : CPU에서 빠르게 접근하기 위해 사용하는 메모리

다시 말해, `pInt`로 수정한 값은 출력에 영향을 주지 않는다.

컴파일러 입장 : "값이 바뀌지 않는 상수인데 억지로 값을 바꾸려 했기 때문에 값이 변경되지 않는 등의 문제는 내 책임이 아니다."

 

`volatile` 키워드를 붙이면 상수의 값을 변경할 수는 있다.

volatile

• 휘발성이라는 뜻으로, 레지스터 최적화를 쓰지 않게 한다.

즉, 연산할 때 레지스터 메모리를 이용하지 못하게 하고 무조건 지칭한 변수의 값을 꼭 확인하게 한다.

#include <stdio.h>

int main()
{
	volatile const int cInt = 100;  // const 키워드가 붙었지만 volatile로 인해 값을 변경할 수 있다.
    int* pInt = 0;
    
    pInt = (int*)&cInt;
    *pInt = 50;  // cInt값을 50으로 변경
    
    printf("cInt 출력 : %d\n", cInt);  // cInt 출력 : 50

	return 0;
}

콘솔 창(cInt는 50)

`const`를 이용하여 상수로 선언해 놓고 포인터로 강제로 값을 변경하는 것 자체가 문제이다.

위 예시처럼 값이 바뀔지 안 바뀔지도 모르는 이런 코드는 지양해야 한다.


포인터로 변경할 수 있는 2가지

1. 포인터가 가리키고 있는 대상(변수)을 변경할 수 있다. (포인터 변수 자체가 바뀐다.)

• 다른 변수를 가리킨다.

• `nullptr`로 아무것도 가리키지 않게 한다.

pInt = nullptr;

▷ 예시

int main()
{
	int a = 0;
    int b = 5;
    
    // 1. 포인터가 가리키고 있는 대상(변수) 변경
    int* pInt = &a;
    // 포인터 변수가 가리키는 대상을 a에서 b로 변경
    pInt = &b;  // pInt는 b의 주소를 가리킨다.

	return 0;
}

 

2. 포인터가 가리키고 있는 주소값(변숫값)을 변경할 수 있다.

▷ 예시

int main()
{
	int a = 0;
    int b = 5;
    
    int* pInt = &a;
    
    // 2. 포인터가 가리키고 있는 주소값(변숫값) 변경
    *pInt = 1;  // a값을 1로 변경한다.

	return 0;
}

const와 포인터

 const의 위치에 따라 상수화가 되는 대상이 달라진다.

※ Tip
1. 영어에서 형용사가 명사를 수식하는 것처럼 `const`가 누굴 수식하는지 생각하면 이해하기 쉬울 것이다.
  (ex. cute cat : 귀여운 고양이)
2. 나는 이해하기 쉽게 "상수화"를 "잠금 상태"라고 생각했다.
  const 포인터 포인터 const const 포인터 const
상수화되는 대상 포인터 변수가 가리키는 값 포인터 변수 자체 포인터 변수가 가리키는 값
&
포인터 변수 자체
변경 가능 가리키는 대상(변수, 주소) 변경 원래 포인터 변수가 가리키는 값 변경 없음.
변경 불가능 포인터 변수가 가리키는 값 변경 가리키는 대상(변수, 주소) 변경 포인터 변수가 가리키는 값 변경
&
가리키는 대상(변수, 주소) 변경

1. const 포인터 (`const 포인터 포인터변수`)

• 포인터 변수가 가리키는 이 상수화된다.

Tip1) `const`가 포인터를 수식하므로 포인터가 가리키고 있는 값이 상수화된다.

Tip2) 포인터 변수가 가리키는 값이 잠겼기 때문에 변수의 주소도 잠겨있다.

▶ 변경 가능

포인터 변수가 원래 가리키고 있던 주소에서 다른 주소를 가리키게 할 수 있다.

즉, 포인터 변수에 다른 변수의 주소를 넣을 수 있다.

▶ 변경 불가능

포인터가 가리키는 값은 변경할 수 없다.

 

▷ 예시

int main()
{
	int a = 0;
    int b = 0;
	int* pInt = &a;
    
	const int* pConstInt = &a;
    *pConstInt;  // 접근은 가능
    *pConstInt = 50;  // 변수의 값 변경 : 오류
    
    pConstInt = &b;  // b의 주소를 저장한다.
    
    return 0;
}

 

`const`가 `int*`를 수식하므로 포인터가 가리키고 있는 값이 상수화된다.

const int* pConstInt = &a;

const 포인터

 

rvalue이기 때문에 값을 변경할 수 없다.

*pConstInt = 50;  // 변숫값 변경 : 오류

▷ 그림으로 이해하기

포인터 const
포인터 const

`const 포인터`에서 주의할 점

• `const 포인터` : 포인터 변수의 기능을 상수화하여 주소를 변경할 수 없게 제한했지만 변수를 상수화시킨 것이 아니므로 변숫값은 수정할 수 있다.

const 포인터로 포인터 변수의 기능을 상수화하여 포인터 변수가 주소를 변경할 수 없게 제한했을 뿐, 변수가 상수화된 것이 아니다.

문법적으로는 포인터 변수가 상수화되었지만 변수의 공간은 존재하기 때문에 변숫값은 수정할 수 있다.

문법적으로는 상수화가 되었지만 변수 공간은 존재하고, 포인터의 기능이 상수화된 것이다.

이처럼 상수화가 된 값을 강제로 바꿀 수 있기 때문에 아예 값이 바뀌지 않을 것이라고 생각하는 것은 지양하기

▷ 예시

int main()
{
	int a = 0;
    const int* pInt = &a;
    
    *pInt = 100;  // 오류
    a = 100;  // 가능
    
	return 0;
}

아래 코드에서 `a = 100;`이 가능한 이유도 이 때문이다.

const 포인터로 포인터 변수 `pInt`의 기능을 제한한 것뿐, `a`가 상수가 된 것이 아니므로 `a`값을 변경할 수 있다.

2. 포인터 const (`포인터 const 포인터변수`)

포인터 변수 자체가 상수화된다.

• 선언뿐만 아니라 초기화도 해줘야 된다.

Tip1) `const`가 `pConstInt`를 수식하므로 가리키고 있는 값이 아니라 포인터 변수가 상수화된다.

Tip2) 포인터 변수가 잠겨있다.

▶ 변경 가능

원래 가리키고 있던 값은 수정할 수 있다.

▶ 변경 불가능

포인터가 가리키고 있던 주소에서 다른 주소를 가리키게 할 수 없다.

즉, 다른 주소로 변경할 수 없다.

 

▷ 예시

int main()
{
	int* const pIntConst = &a;
	*pIntConst = 100;  // 값 변경 가능
	pIntConst = &b;  // 오류 => 가리키고 있는 변수 변경 불가능
    
    return 0;
}

 

`const`가 `pConstInt`를 수식하므로 포인터 변수 자체가 상수화된다.

int* const pConstInt = &a;

포인터 const

`pIntConst`가 가리키고 있는 `a`의 값을 변경할 수 있지만, 가리키고 있던 변수에서 다른 변수를 가리키게 할 수는 없다.

▷ 그림으로 이해하기

const 포인터

3. const 포인터 const (`const 포인터 const 포인터변수`)

• 포인터가 가리키는 값과 포인터 변수 자체가 상수화된다.

Tip1) `const`가 `int*`와 `pConstInt`를 수식하므로 가리키고 있는 값과 포인터 변수가 상수화된다.

Tip2) 포인터가 가리키고 있는 값과 포인터 변수 자체가 잠겨있다.

▶ 변경 불가능

가리키는 값과 주소를 변경할 수 없다.

처음 가리킨 주소만 가리킨다.

 

▷ 예시

int main()
{
	const int* const pConstIntConst = &a;
    
    return 0;
}

 

`const`가 `int*`와 `pConstInt`를 수식하므로 값과 주소를 변경할 수 없다.

const int* const pConstInt = &a;

const 포인터 const

▷ 그림으로 이해하기

const 포인터 const


아래 코드처럼 쓰는 경우는 거의 없지만 무엇이 상수화된 것일까?

int const* p = &a;

 

const가 누굴 수식하는지 자세히 보면 `*`를 수식하고 있다.

const 포인터

따라서 위 코드는 `const 포인터`에 해당하므로 가리키는 값이 상수화된다.

const가 포인터인 `*`을 기준으로 `*`보다 앞에 있는지 뒤에 있는지를 보면 된다.


`const`와 포인터를 쓰는 이유

▷ 예시

// 매개 변수 a
// 함수를 호출하면 매개변수는 해당 함수 스택에 포함된다.
void Output(int a)
{
	// 코드 내용 생략
}

int main()
{
	int a = 0;
    
    a = 100;
    // Output() 함수를 호출하고 a값인 100을 전달한다.
    Output(a);  // 인자 a

	return 0;
}
• 인자 : 함수를 호출했을 때 전달되는 값
• 매개 변수 (파라미터, parameter) : 전달된 인자를 받는 변수

▷ `Output()`가 종료되지 전까지의 메모리의 상태

`main()`와 `Output()`에 동일한 변수와 그 값이 저장되어 있다.

메모리 상황

새 함수 스택을 만들어서 값을 복사하고 함수가 끝나면 없애는 과정이 반복된다.

이때 int처럼 기본 자료형이 아니라 구조체 자료형이거나, 데이터가 너무 많거나 크다면 프로그램 속도가 느려지는 등의 성능 면에서 문제가 발생한다.

 

데이터 복사량이 많아지면 비효율적이므로 포인터를 사용하여 필요할 때마다 주소에 접근하여 값을 사용하는 방법을 이용한다.

그럼 함수와 스택을 또 만들 필요 없이 원래 있던 스택을 유지한 채로 주소에 접근하여 값을 사용할 수 있다.

void Output(int * pI)
{

}

int main()
{
	int a = 0;
    
    a = 100;
    Output(&a);

	return 0;
}

 

▶ 주소를 준 입장에서 생각해 보기

데이터 복사량이 많아서 주소를 줬고, 값은 변경할 생각이 없다.

하지만 원치 않는 동작으로 `main()`의 `a`값이 변경되어 원했던 것과 다른 결과가 나올 수도 있다.

이런 상황을 방지하기 위해 포인터 변수에 `const`를 붙여서 값이 바뀌지 않게끔 한다.

void Output(const int* pI)
{

}

int main()
{
	int a = 0;
    
    a = 100;
    Output(&a);

	return 0;
}

 

`pI`에 접근은 할 수 있지만 `const 포인터` 형태이기 때문에 값은 수정할 수 없다.

void Output(const int* pI)
{
	int i = *pI;  // pI에 접근
    *pI = 100;  // 오류
    *pI = 200;  // 오류
}

int main()
{
	int a = 0;
    
    a = 100;
    Output(&a);

	return 0;
}

 

※ Tip!

함수 선언에 대한 내용 보는 법

`Output(` 부분에서 괄호의 오른쪽에 커서를 놓고 `Ctrl + Shift + Space`를 누르면 함수 선언과 관련된 목록을 볼 수 있다.

함수 선언 목록

만약 팀으로 코드를 작성한다면 다른 팀원이 `const 포인터`로 선언했다는 사실과 그 사람의 의도를 알 수 있다.


int 포인터를 선언한 후 인자로 받은 `pI`를 int*로 강제 캐스팅하면 `a`값을 수정할 수 있다.

void Output(const int* pI)
{
	int i = *pI;  // pI에 접근
    //*pI = 100;  // 오류
    //*pI = 200;  // 오류
    
    int* pInt = (int*)pI;  // 인자로 받은 pI를 int*로 강제 캐스팅
    *pInt = 200;  // a값 200으로 변경
}

int main()
{
	int a = 0;
    
    a = 100;
    Output(&a);

	return 0;
}

 

로컬 창 (a는 200)

`const 포인터`로 선언하면 절대 못 바꾸는 것은 아니지만 `const`로 상수화 시켜놓고 값을 강제로 바꾸는 것은 지양해야 한다.