Study/C++

[C++]04.7 복합데이터형 - 포인터와 메모리 해제

SigmoidFunction 2023. 3. 31. 15:29
728x90

포인터는 값 자체가 아니라 값의 주소를 저장하는 변수다.

 

포인터를 이해하기 전에 일반적인 변수에 대해 명시적으로 그 주소를 알아내는 방법을 알아보자

 

주소 연산자(&)를 변수 앞에 붙이면 그 변수의 주소를 알아낼 수 있다.

home이 변수라면 &home은 그 변수의 주소이다!

// address.cpp -- & 연산자로 주소를 알아낸다.
#include <iostream>
int main()
{
    using namespace std;
    int donuts = 6;
    double cups = 4.5;

    cout << "donut's value = " << donuts;
    cout << ", donut's address = " << &donuts << endl;

    // cf) unsigned (&donuts) and unsighned (&cups) are somtimes required to be used.
    cout << "cups's value = " << cups;
    cout << ", cups's address = " << &cups << endl;
    return 0;
}

// donut's value = 6, donut's address = 0x61ff0c
// cups's value = 4.5, cups's address = 0x61ff00

16진수 표기가 메모리를 나타내는 가장 일반적인 방법이기에 주소값을 위와 같이 표기하게 된 것

 

 

포인터의 이름이 주소를 나타낸다. 

간접값(indirect value) 연산자 또는 간접 참조(dereferencing) 연산자라고 부르는 *를 포인터 이름 앞에 붙이면 그 주소에 저장되어 있는 값이 된다. 

 

예를 들어 manly가 포인터라고 했을 때, manly는 주소를 나타내고 *manly는 그 주소에 저장되어 있는 값을 나타낸다. *manly는 보통의 int형 변수와 동등하게 취급된다.

// pointer.cpp -- 처음으로 배우는 포인터 변수
#include <iostream>
int main()
{
    using namespace std;
    int updates = 6;        // declare int variable
    int *p_updates;         // declare a pointer to an int type
    p_updates = &updates;   // Assigning an address of type into to a pointer

    // 값을 두가지 방법으로 표현
    cout << "value: updates = " << updates;
    cout << ", *p_updates = " << *p_updates << endl;

    // 주소를 두가지 방법으로 표현
    cout << "address: &updates = " << &updates;
    cout << ", p_updates = " << p_updates << endl;

    // 포인터를 사용하여 값을 변경
    *p_updates = *p_updates + 1;
    cout << "The changed updates = " << updates << endl;
    return 0;
}


// value: updates = 6, *p_updates = 6
// address: &updates = 0x61ff08, p_updates = 0x61ff08
// The changed updates = 7

 

포인터의 선언과 초기화


포인터를 선언할 때는 그 포인터가 지시하는 데이터형이 무엇인지 서술해야한다. 위에서도 int *p_updates라고 했던것처럼! *연산자의 앞뒤에는 빈칸이 있어도 되고 없어도 되는데 C개발자들은 보통

int *ptr;

이런식으로 사용하고 C++ 개발자들은

int* ptr;

이런식으로 많이 사용한다는데 첫번째 선언은 int형이라는 것을 강조하고 두번째 선언은 int* 자체가 하나의 데이터형임을 강조한다고 한다.

 

다만, 한번에 여러개를 선언하려고 한다면 포인트 변수마다 *를 붙여야한다.

int* p1, p2;

이 경우에는 p1은 포인터 변수로 p2는 일반 int형 변수로 생성한다.

 

 

선언 구문을 사용하여 포인터를 초기화할 수 있다. 이런 경우 포인터에 의해 지시되는 값이 아니라 포인터가 초기화된다.

// init_ptr.cpp -- 포인터를 초기화한다.
#include <iostream>
int main()
{
    using namespace std;
    int higgens = 5;
    int * pt = &higgens;

    cout << "higgens's value = " << higgens
         << ", higgens's address = " << &higgens << endl;
    cout << "*pt's value = " << *pt
         << ", pt's value = " << pt << endl;

    return 0;
}

// higgens's value = 5, higgens's address = 0x61ff08
// *pt's value = 5, pt's value = 0x61ff08

여기서 *pt가 아니라 pt가 higgens의 주소로 초기화 되었음을 알 수 있다.

 

포인터의 위험

long * fellow;
*fellow = 543500;

fellow는 포인터다. 그런데 이 포인터는 어디를 지시하고 있는지는 알 수가 없다. 왜냐하면 fellow에 주소를 대입하는 단계를 빠트렸기 때문이다. 프로그램은 543500이 주소라고 생각할 것이다. fellow에 다른 값이 들어 있었더라도 문제없이 진행되기 때문에 fellow의 원래 값을 잃게 된다.

 

포인터를 사용할 때는 간접 참조 연산자(*)를 사용하기 전에 반드시 포인터를 적절한 주소로 초기화시켜 주어야 한다.

 

포인터와 수


포인터는 정수형이 아니다. 개념적으로 다른 데이터형이다.  그렇기 때문에 포인터에 정수를 직접 대입할 수 없다.

int * pt;
pt = 0xB8000000; // 데이터형 불일치

좌변은 int형을 지시하는 포인터이고 주소를 대입할 수 있지만 우변은 정수기 때문에 이를 주소로 사용하려면 주소형으로 변환해줘야한다.

int * pt;
pt = (int *) 0xB8000000; // 데이터형 일치

pt가 int형 값을 가지고 있다고 해서 int형이라고 생각하면 안된다.

 

 

new를 사용한 메모리 대입

어떤 데이터형의 메모리를 원하는지 new연산자에게 알려주면 그에 알맞는 크기의 메모리 블록을 찾아내고 그 블록의 주소를 리턴한다. 이 주소를 포인터에 대입하면 된다.

int * pn = new int;

int형 데이터를 저장할 새로운 메모리가 필요하다고 하고 몇 바이트가 필요한지 파악하고 적당한 곳을 찾아 그 주소를 리턴한다.

이제 리턴되는 주소는 int형을 지시하는 포인터로 선언되어 있는 pn에 대입한다. pn은 주소고 *pn은 주소에 저장되는 값이다. 

 

영화관에서 2명의 좌석이 필요합니다 → H열 3,4번 → 지정해주면 그곳에 가서 착석하는 느낌

 

 

// use_new.cpp -- new 연산자 사용하기
#include <iostream>
int main()
{
    using namespace std;
    int nights = 1001;
    int * pt = new int;     // int형을 저장할 메모리를 대입
    *pt = 1001;             // 대입된 메모리에 값을 저장

    cout << "nights value = ";
    cout << nights << ": Memory location " << &nights << endl;
    cout << "int type ";
    cout << "value = " << *pt << ": Memory location = " << pt << endl;

    double * pd = new double;   // double형을 저장할 메모리를 대입
    *pd = 10000001.0;           // 그 메모리에 double형 값을 저장

    cout << "double type ";
    cout << "value = " << *pd << ": Memory location = " << pd << endl;
    cout << "Memory location of pointer pd: " << &pd << endl;
    cout << "Size of pt = " << sizeof(pt);
    cout << ": Size of *pt = " << sizeof(*pt) << endl;
    cout << "Size of pd = " << sizeof(pd);
    cout << ": Size of *pd = " << sizeof(*pd) << endl;
    return 0;
}

// nights value = 1001: Memory location 0x61ff08
// int type value = 1001: Memory location = 0x9718a8
// double type value = 1e+007: Memory location = 0x9718b8
// Memory location of pointer pd: 0x61ff04
// Size of pt = 4: Size of *pt = 4
// Size of pd = 4: Size of *pd = 8

 

 

delete를 사용한 메모리 해제

 

int * ps = new int; // new로 메모리 대입
...		// 메모리 사용
delete ps; 		// delete로 메모리 해제

ps 자체가 없어지는 것은 아니고 새로운 메모리를 지시하는데 사용할 수 있다. new를 사용한 후에는 반드시 delete를 사용하는 것이 좋은데. 대입은 되었지만 사용되지 않는 메무리 누수가 발생할 수 있기 때문이다. 

이미 해제한 메모리 블록을 다시 해제하려고 시도하면 안되고 보통의 변수를 선언하여 대입한 메모리는 delete로 해제할 수 없다.

 

int * ps = new int;	// O
delete ps;	// O
delete ps;	// X
int jugs = 5;	// O
int * pi = &jugs;	// O
delete pi;	// new로 대입한 메모리가 아니므로 X

 

 

new를 사용한 동적 배열의 생성

프로그램이 하나의 값만 요구하면 단순한 변수를 선언하는 것이 new와 포인터를 사용하는 것보다는 간단하지만 배열, 문자열, 구조체와 같은 커다란 데이터를 다룰 때는 new를 사용하는 것이 훨씬 효율적이다.

 

// new를 사용한 동적 배열의 생성
int * psome = new int [10];	// 10개의 int형 값을 저장할 블록을 대입

new연산자는 그 블록의 첫번째 원소의 주소를 리턴한다. 여기서 포인터 psome에 그 주소가 대입된다.

new를 사용하여 생성된 배열을 해제할 때에는 다음과 같이 delete의 또 다른 형식을 사용하여 컴파일러에게 그 배열을 해제한다는 사실을 알려야한다.

delete [] psome;	// 동적배열을 해제

대괄호를 사용하였기에 포인터가 지시하는 첫번째 원소가 해제되는 것이 아니라 배열 전체가 해제된다. 

 

int * pt = new int;
show * ps = new short [500];

delete [] pt;	// 결과 미정의, 사용불가
delete ps;		// 결고 미정의, 사용불가

 

new와 delete를 사용할 때의 규칙

  • new로 대입하지 않은 메모리는 delete로 해제하지 않는다
  • 같은 메모리 블록을 연달아 두 번 delete로 해제하지 않는다.
  • new [ ]로 메모리를 대입한 경우에는 delete [ ]로 해제한다.
  • new를 대괄호없이 사용했으면 delete도 대괄호 없이 사용한다.
  • 널 포인터에는 delete를 사용하는 것이 안전하다.(아무 일도 일어나지 않는다.)

 

// arraynew.cpp -- 배열을 위해 new 연산자 사용
#include <iostream>
int main()
{
    using namespace std;
    double * p3 = new double [3];   // double형 데이터 3개를 저장할 수 있는 공간을 대입한다.

    p3[0] = 0.2;
    p3[1] = 0.5;
    p3[2] = 0.8;
    cout << "p3[1] is " << p3[1] << ".\n";
    p3 = p3 + 1;        // 포인터를 증가시킨다.
    cout << "Now, p3[0] is " << p3[0] << " and ";
    cout << "p3[1] is " << p3[1] << ".\n";
    p3 = p3 - 1;        // 다시 시작 위치를 지시한다.
    delete [] p3;       // 배열 메모리를 해제한다.
    return 0;
}


// p3[1] is 0.5.
// Now, p3[0] is 0.5 and p3[1] is 0.8.

 

포인터 배열에서 더하고 빼는 것은 지시하는 손가락의 위치를 변경하는 것이라고 생각하면 된다.

728x90