주소(address) :
C++에서의 주소란, 메모리 위치를 직접 가리킬 때 사용되는 메모리에서의 '주소'를 의미한다. 컴퓨터 안에서 사용되는 주소이기 때문에 16진수를 사용하여 0x1000, 0x1004... 이런 식으로 표현하는 경우가 많다.
인간과 컴퓨터를 비교해 보면, cpu를 두뇌로 메모리를 노트로 생각해 볼 수 있는데, cpu(머리속)에 있는 변수와 객체 등등의 개념을 사용하려면 메모리(노트)에 올려놓고 써야만 한다. 이때 노트에 쓴 것들의 위치가 존재하듯이 메모리를 할당해야만 한다. 그래서 그 메모리마다 고유의 주소를 갖게 되는 것이다.
1. 주소 변산자 & - 변수의 주소(메모리에서의 위치) 확인하기
변수값이 메모리의 어느 부분에 저장되어 있는지 그 주소를 확인할 때에는 주소 변산자(address operator), &를 사용한다.
주소 변산자:
&변수명
예시1)
2. 포인터
1) 포인터의 원리 이해하기
- 포인터에 주소를 저장할 수 있다.
포인터(pointer):
주소를 저장하는 특수한 변수.
주소를 담을 수 있는 자료형이이다.
포인터는 일반적인 변수와 같은 방식으로 사용한다.
포인터는 사용하기 전에 'pA'처럼 이름을 정해서 선언한다.
단, 포인터를 저장하는 변수에는 반드시 *라는 기호를 하나 붙여야 한다는 조건이 있다.
포인터 선언:
형명* 포인터명
예시) int* pA;
이 문장은 int형 변수의 주소를 저장할 수 있는 포인터 pA를 선언한 것이다.
포인터에는 원칙적으로 지정된 형의 주소값만 저장할 수 있다.
즉, pA는 int형 변수의 주소만 저장할 수 있다.
포인터 pA에 무언가 주소를 저장해 보자.
예시2)
pA = &a;
변수 a의 주소를 pA에 저장한다.
즉, 이 대입 작업을 통해 포인터 pA에 변수 a의 주소를 저장하기에 성공했다.
이와 같은 대입을 통해 변수 a와 포인터 pA사이에 무언가 '관계'가 만들어 졌다고 볼 수 있을 것이다.
이러한 관계를 일컬어, pA가 변수a를 가리킨다고 부르기도 한다.
pA(의 값)가 변수 a(의 위치)를 가리키게 되었다라고 이해해 보자.
2) 포인터를 통해 변수의 값 확인하기 (간접참조연산자 *)
-간접참조연산자 *를 사용하면 포인터가 가리키는 변수의 값을 알아낼 수 있다.
포인터 변수에 주소값이 저장되면, 그 포인터를 통해 원래 변수의 값을 역추적하기가 가능해진다.
간접참조연산자(indirection operator) *연산자는 포인터 앞에 붙여, 포인터를 통해 변수의 값을 추적할 때 사용한다.
간접참조연산자:
*포인터 명;
예시3)
이 코드의 경우 가장 먼저 포인터 pA에 주소를 대입한다. 즉, 포인터 pA가 변수 a를 가리키도록 만든 것.
그 후에 pA에 *연산자를 사용하면 변수 a의 값을 알아낼 수 있다. 즉,
*pA <----------> a
같다
<정리>
a |
변수 a |
&a |
변수 a의 주소 |
pA |
변수 a의 주소를 저장한 포인터 |
*pA |
변수 a의 주소를 저장한 포인터가 가리키는 변수(의 값) -----------------> 변수a |
주의사항:
int *pA.;
int* pA;
이 문장들은 같은 의미이다. 다만,
int* pA, pB;
라는 선언은 int* pA; int pB;라는 변수들을 선언한 것과 같다.
pA와 pB모두 int* 형으로 선언하고 싶다면, 콤마를 사용하는 대신 여러 행에 나누어 선언하거나,
int *pA, *pB; 라고 선언해야 한다.
3) 포인터에 다른 주소 대입하기
변수 a의 주소를 저장하는 포인터 pA가 다른 변수 b의 주소를 저장하도록 만들 수 있다.
예시4)
즉, 포인터에 저장된 주소값은 바꿀 수 있다.
다만, 포인터에는 원칙적으로 지정된 형의 주소값만 저장할 수 있으니 주의!
주의사항:
(1) 포인터는 가급적이면 초기화를 수행하도록 작성하는 것이 좋다.
(2) 포인터에는 반드시 주소를 대입시킨 후에 사용한다. 그렇지 않다면 에러가 날 것이다!
4) 포인터를 사용하여 변수의 값 변경시키기
포인터를 사용하면 포인터가 가리키는 변수 자체의 값도 변경시킬 수 있다.
예시5)
이 코드는 변수 a의 값을 실행중에 바꾸고 있다.
포인터 pA가 a를 가리킬 때, *pA와 a가 같아진다는 성질을 활용한 것이다. 즉,
*pA <----------> a
같다!!
따라서,
*pA=50; <----------> a=50;
동일함!
-간접참조연산자 *를 사용하면 포인터가 가리키는 변수의 값을 알아낼 수 있다-
**포인터 그림으로 이해하기**
3. 인수와 포인터
1) 작동하지 않는 함수
예시6)
위의 코드는 num를 5 증가시킬 목적으로 작성하였으나, 이상하게도 작동하지 않는다.
마찬가지로, 밑의 코드는 x와 y를 교환할 목적으로 작성된 swap함수이지만,
함수를 호출하여 실행시켜 보면, 예상했던 결과가 나오지 않는다!
이유는, 함수에 실인수를 넘기면 실인수의 '값'만 함수에 전달되기 때문이다.
위의 경우 swap() 함수는 가인수 x와 y의 교환만 담당하는 것이지 실인수 num1과 num2와는 아무런 관계가 없다는 것이다. 이러한 인수 전달법을 값 전달(pass by value)라고 부르기도 한다.
자세히 보면, 함수 안에서 가인수 x와 y의 값을 교환하고 있지만, 이는 어디까지나 변수 Num1과 Num2의 값을 '복사'한 5와 10을 교환한 것이 불과하다. 즉, swap()함수 안에서 값을 교환하더라도 호출자의 변수인 Num1과 Num2에는 영향을 미칠 수가 없는 것이다.
쉽게 말해, 실인수를 사람으로 함수를 어떤 집으로 보면, 사람이 집에 잠시 자기 자신의 값을 복제해서 함수라는 집으로 전달한다고 생각하면 된다! 실인수라는 사람이 자기 자신의 성질을 함수라는 집에 복사해서 보냈을 뿐인데, 갑자기 실인수라는 사람의 성질이 바뀔 수는 없는 노릇이니까!
-가인수만 swap될 뿐이지 실인수는 swap되는 것이 아니므로, swap()함수는 작동하지 않는다.-
포인터를 잘 활용하면, 함수 호출 시 지정된 인수 값을 변경할 수 있다. 이를 위해서 swap() 함수의 가인수를 포인터로 정의해야 한다.
이 함수가 하는 일은 방금 전의 swap() 함수와 같지만,
가인수가 포인터이므로 호출할 때 인수로 변수의 주소를 넘겨야 한다. 결과는?
호출할 때, 변수 Num1과 Num2의 주소 (&Num1, &Num2)를 넘겼다. 그 결과 Num1과 Num2의 값이 교환된 것을 볼 수 있다.
이처럼 호출 변수의 값을 함수 안에서 변경하고 싶다면, 가인수를 포인터로 선언하고 함수를 호출할 때 주소값을 넘기면 된다. 그러면 가인수는 넘겨받은 포인터로 초기화된다. 그 결과 Num1 Num2의 주소가 포인터 pX, pY에 저장된다.
가인수 <------------------------ 실인수
pX <------------------------ Num1의 주소
pY <------------------------ Num2의 주소
*pX <------------------------ Num1(실인수)
*pY <------------------------ Num2(실인수)
같다!
*pX, *pY는 Num1 Num2와 같은 것을 가리킨다
- 원칙적으로 함수 안에서 실인수를 변경할 수 없다. 포인터를 사용하면 실인수의 값을 변경할 수 있다. -
4. 인수와 레퍼런스(reference)
- 함수 안에서 실인수를 변경하는 경우 레퍼런스를 사용한다. -
1) 레퍼런스의 원리 이해하기.
Reference : 참조, 발췌란 의미이다. 오리지널이 될 수 없다!
레퍼런스:
형명& 레퍼런스명 = 변수;
int a;
int& rA = a;
이 코드에서 'rA'가 레퍼런스이다. rA를 변수a로 초기화한 것이다.
예시7)
변수A와 레퍼런스 rA의 값은 같다.
레퍼런스 rA에 값을 대입하면 변수 a의 값도 바뀝니다.
레퍼런스 rA의 주소는 변수 a의 주소와 같다.
즉,
rA <------------> a
동일함
포인터로 비유해 보면,
rA = 50; <---------------> a= 50;
동일함
주의사항:
레퍼런스는 반드시 레퍼런스 대상이 되는 변수로 초기화해 두어야 한다.
int& rA;
rA = a;
이처럼 선언과 대입을 분리할 수 없다!
2) 인수에 레퍼런스 사용하기.
swap함수를 포인터 대신 레퍼런스를 사용해서 구현해 보자.
예시8)
레퍼런스를 인수로 받는 swap() 함수를 사용하면 변수 Num1과 Num2를 그대로 실인수로 넘길 수 있다.
x(가인수 쪽) <---------------> Num1(실인수 쪽)
같다
y(가인수 쪽) <---------------> Num2(실인수 쪽)
같다
즉, 가인수를 교환하면 실인수도 교환된다.
레퍼런스는 포인터와 유사한 기능을 갖고 있지만 레퍼런스와 포인트는 엄연히 다른 개념이므로 포인터에 레퍼런스를 대입할 수는 없다.
만약 실인수의 값을 바꾸고 싶지 않을 경우에는,
const 제한자를 사용하면 된다.
void func1(const int* pX);
void func2(const int& X);
(함수 안의 실인수의 값을 변경하지 않을 것이라는 사실을 명시)
const를 사용하면 함수 안에서 인수를 변경하는 코드가 에러가 된다.
5. Call by reference // Call by value // Call by address
1) Call by reference 참조에 의한 호출
int b;
int& a = b;
int& a = 3;(X) 메모리할당(X)
참조는 크기를 지닐 수 없다. (위 swap() 함수에서 실인수 값이 바뀌지 않는 이유)
// int& x = input 메모리할당x
ex)
void Sum5(int& x)
{
x += 5;
cout << x << endl;
}
int main()
{
int num = 3;
Sum5(num);
}
//Sum5(6); (X) - 6이란 상수가 메모리 안에 있다고 하기 어렵다.
2) Call by value 값에 의한 호출
함수 밖과 안의 구분이 잘 되는 것이 장점이다.
// int x =input; 메모리할당0
ex)
void Sum6(int x)
{
x += 6;
cout << x << endl;
}
int main()
{
Sum6(num);
Sum6(6);
}
둘 다 가능!
3) Call by address 주소에 의한 호출
int a;
int *x 와 &a가 같다면,
*x = 3 일때
*x 는 a와 같다!
//int* x = input 메모리할당
바로 위에 함수를 응용해서 예시를 들어보면,
ex)
void Sum5(int* x)
{
//x += 5;
//cout << x << endl; (x)
*x += 5;
cout << *x << endl;
}
<마지막 정리>
- 자료형으로 쓰이는 포인터 (4byte)
type * : 해당 자료형의 주소를 담는 자료형
L(주소값담는변수) = R(주소값) - 자료형으로 쓰이는 레퍼런스 (x)
type & : 참조 자료형
L(참조자료형 변수) = R(값)
- 연산자로 쓰이는 포인터
*variable : 해당 주소가 가르키는 값
R 의 주소가 가르키는 값 - 연산자로 쓰이는 레퍼런스
&variable : 해당 변수의 주소값
R 의 주소
'C++ 코딩 > C++ 기초개념' 카테고리의 다른 글
<C++ 기초> 식과 연산자, 연산자의 종류와 우선순위 (0) | 2021.02.23 |
---|---|
<C++ 기초> 동적배열, sti vector, 멤버함수 (0) | 2021.02.18 |
<C++ 기초> typedef, 열거형 enum (0) | 2021.02.16 |
<C++ 기초> 문자열 조작하기 -char[], string (0) | 2021.02.15 |
<C++ 기초> 메모리 동적 확보, 배열의 동적인 확보 (0) | 2021.02.14 |
<C++ 기초> 변수와 스코프(지역변수와 전역변수) 02.14 (0) | 2021.02.14 |
<C++ 기초> 메모리 구조 힙, 스택, 데이터, 코드 (0) | 2021.02.09 |
<C++ 기초> 배열에 대한 이해, 문자열과 배열, char 02.08 (0) | 2021.02.08 |