C++/객체지향 초급편

[되돌아 보며] C++의 꽃이라고 불리는 포인터(pointer, *), 레퍼런스(reference, &) 기본 개념을 되돌아보자.

쉽코딩 2023. 1. 30.

 

 

 

포인터, 레퍼런스 되돌아 보기

 

 

 

 

'되돌아보며'가 필요한 이유

객체지향 초급 편에 들어가기 앞서 C++의 꽃이라고 불리는 포인터, 레퍼런스 개념을 다시 한번 짚고 넘어가겠습니다.

자바(JAVA), 파이썬(Python), C#(CSharp) 과는 다르게 메모리 할당을 직접적(수동적)으로 Contorl 할 수 있다는 장점이 있습니다. → 물론, 잘했을 경우에만 해당됩니다. 잘못했을 경우 잘못된 메모리 할당으로 누수가 생길 수 있습니다. 

 

메모리 할당을 직접적으로 Control 할 수 있다면 좋은 것은 바로 'Process 속도 향상'입니다. 그래서 영상 처리와 같이 Just in Time Process의 경우 C#, Python, JAVA와 같은 언어를 사용하기보다는 공유 메모리 및 Thread 연산을 통해 연산 처리 속도를 높이는 작업이 주를 이룹니다. 이때 반드시 포인터, 레퍼런스 개념 이해가 필수적이며, 앞으로 객체지향 공부를 진행함에 있어 해당 기본 개념이 완벽하지 않으면 여러분은 언제나 '입문 편' 강의를 보고 또 보고 있을 것입니다. 

 

그래서 간단하게 한번 더 준비했습니다.

이제부터 시작하는 간단한 설명과 예시 하나 만으로 여러분은 포인터와 레퍼런스 개념을 읽힐 수 있으며, 한글자 한 글자를 꼼꼼히 읽고, 학습해야 됩니다.

 

포인터와 레퍼런스

'=' 각 두 영역에는 '동일한 차원'의 '값' 변수들이 와야된다.

      1) 예를 들면, 값 = 값 → int a = 10;  
      2) 예를 들면, 값 = 값 → int *a = ≺

 

*    → 포인터 변수를 지칭할 때 사용, 다른 변수의 메모리 주소를 가지고 있는 변수 → 메모리 위치에 접근하기 위해서는              디레퍼런스하는 과정이 필요.
 

&    → 참조할 때 사용, 이미 존재하는 '변수의 별칭(alias)'일 때 사용하는 변수, 내부적으로는 포인터와 같이 객체의 주소를 저장하도록 구현.

 

규칙 1. 변수를 있는 원형 그대로 쓰게 될 경우 주소값이 출력된다. 대표적인 게 (아래 코드) *num3과 array가 있다.
→ 무슨말이냐, 아래 코드를 보면 array 변수를 그냥 호출하면 주소값이 출력되고, 포인터 변수를 그냥 쓰면 주소값이 출력됩니다. (아래 코드의 출력 값을 통해 이해하면 Easy)

규칙 2. &는 별칭을 나타내는 표시이다. 그래서 변수명 그대로를 써야 메모리 값이 출력된다. (아래 코드) 해당 사례 1로  인해 &pr은 주소를 나타내는 주소값임에도 불구하고, 값이라고 하자. → 이해를 위해 제약한 부분. 

 

 

아래 예시의 주석과 함께 보면 이해하기 쉽다.


#include <iostream>
using namespace std;

int& RefRetFuncOne(int& ref)
{
	ref++;
	return ref;
}
	 
int main(void)
{
	// 변수명이란? 변수에 명시한 고유한 식별자를 변수명이라고하고, 변수로 참조할 수 있는 데이터를 변수값이라 한다.
	// '=' 각 두 영역에는 '동일한 차원'의 '값' 변수들이 와야된다.
	// 1) 예를 들면, 값 = 값		→ int a = 10;  
	// 2) 예를 들면, 값 = 값		→ int *a = &pr;
	// * → 포인터 변수를 지칭할 때 사용, 다른 변수의 메모리 주소를 가지고 있는 변수 → 메모리 위치에 접근하기 위해서는 디레퍼런스하는 과정이 필요.
	// & → 참조할 때 사용, 이미 존재하는 '변수의 별칭(alias)'일 때 사용하는 변수, 내부적으로는 포인터와 같이 객체의 주소를 저장하도록 구현.
 
	int num1 = 1;
	int& num2 = RefRetFuncOne(num1);

	num1++;
	num2++;

	cout << "num1 : " << num1 << endl;
	cout << "num2 : " << num2 << endl;

	int &pr			= num1;		// → 해당 사례 1 
	int *num3		= &pr;		//*num3 → 값, &pr → 주소값(=그런데 값으로 보자) 
	int array[3]	= {1,2,3}; 
	
	// 규칙 1. 변수를 있는 원형 그대로 쓰게 될 경우 주소값이 출력된다. 대표적인게 *num3과 array가 있다.
	// 규칙 2. &는 별칭을 나타내는 표시이다. 그래서 변수명 그대로를 써야 메모리 값이 출력된다. 해당 사례 1로 인해 &pr은 주소를 나타내는 주소값임에도 불구하고, 값이라고 하자. 

	cout << "*num3 memory value : "		<< *num3	<< endl;		// output : 4 → 메모리 값
	cout << "num3 memory address : "	<< num3		<< endl;		// output : 00000074062FF654 → 주소값
	cout << "pr memory value : "		<< pr		<< endl;		// output : 4 → 메모리 값
	cout << "&pr memory address : "		<< &pr		<< endl;		// output : 00000074062FF654 → 주소값
	cout << "array memory address : "	<< array	<< endl;		// output : 00000014473DF688 → 주소값
	cout << "array[1] memory value : "	<< array[1] << endl;		// output : 2 → 메모리 값

}

 

 

결과는 아래의 스크린숏과 같습니다.

 

출력 결과

 

 

포인터를 쓰는 이유

대표적으로 간단한 이유 중 한 가지 예시는 memcpy() 때문이다. 메모리를 복사하기 위해 memcpy()를 사용합니다. 그런데 memcpy()의 규칙은 아래와 같이

 

첫 번째 인자와 두 번째 인자에 주소값이 와야 되기 때문에 포인터가 와야 되됩니다.

세 번째 인자에는 복사할 데이터 값의 길이(크기)가 와야 됩니다.

 

그러면, 큰 프로젝트의 경우 복사할 데이터의 크기를 모두 알고 있어야 되는데 데이터 복사량이 많을 경우 이를 기억하기에는 막일이며, 기억하기 어렵습니다.

 

그래서 이러한  memcpy()가 아닌 포인터를 통해 쉽게 동적 할당을 하기 위해 사용됩니다.   

 

void* memcpy (void* dest, const void* source, size_t num)

첫번째 인자 void* dest
= 복사 받을 메모리를 가리키는 포인터

두번째 인자 const void* source
= 복사할 메모리를 가리키고 있는 포인터

세번째 인자 size_t num
= 복사할 데이터(값)의 길이(바이트 단위)

 

 

memcpy()의 대표 예시입니다.

#include<string.h>
#include<stdio.h>

int main(void)
{
    int src[] = { 1,2,3 };
    int dest[3];

    // 메모리 복사
    memcpy(dest, src, sizeof(int) * 3); // dest와 src는 모두 주소값이다. cout해보면 배열의 명칭만 쓸 경우 주소값이 출력된다.
    // memcpy(dest, src, sizeof(src)); sizeof(src)도 가능

    // 복사한 배열
    for (int i = 0; i < 3; ++i)
    {
        printf("%d ", src[i]);
    }

    printf("\n");

    // 복사된 배열
    for (int i = 0; i < 3; ++i)
    {
        printf("%d ", dest[i]);
    }

    return 0;

    
}
    // output   1 2 3
    //          1 2 3

댓글