깊은 복사, 얕은 복사 복습과, 2차원 배열 동적할당, 소멸자에 대한 수업을 들었다.
1. 깊은 복사, 얕은 복사 복습
1.1 포인터로 외부 데이터를 받아와 세팅
[header.h]
#pragma once
class C_DATA
{
private:
int* m_pData;
public:
C_DATA() = default;
void init(int* pData); // 외부에서 데이터를 받아와 셋팅
int getData();
};
[header.cpp]
#include "data.h"
void C_DATA::init(int* pData)
{
m_pData = pData;
}
int C_DATA::getData()
{
return *m_pData;
}
[main.cpp]
#include <iostream>
#include "data.h"
void print(C_DATA c);
int main() // int nData라는 외부 데이터를 빌려와서 사용중, 얕은 복사
{
int nData{};
C_DATA c1{};
C_DATA c2{};
c1.init(&nData);
c2.init(&nData);
nData = 100;
print(c1);
print(c2);
}
void print(C_DATA c)
{
printf("%d\n", c.getData());
}
- init 메서드를 이용하여 외부에서 전달받은 int 포인터를 m_pData에 설정을 하여 클래스 인스턴스가 외부 데이터에 접근할 수 있게 설정하고 이 방식이 얕은 복사를 사용한 것으로, 실제 데이터는 외부에 존재하며, 클래스는 그 데이터에 대한 포인터만 유지한다.
1.2 동적할당
[header.h]
#pragma once
class C_DATA
{
private:
int* m_pData;
public:
C_DATA() = default;
void init();
void release();
void setData(int nData);
int getData();
};
[header.cpp]
#include "data.h"
void C_DATA::init()
{
m_pData = new int{};
}
void C_DATA::release()
{
delete m_pData;
m_pData = nullptr; // 동적할당을 해제한 후 포인터에 널 포인터를 넣어주는게 국룰
}
void C_DATA::setData(int nData)
{
*m_pData = nData;
}
int C_DATA::getData()
{
return *m_pData;
}
[main.cpp]
#include <iostream>
#include "data.h"
void print(C_DATA c);
int main()
{
C_DATA c{};
c.init();
c.setData(10);
print(c);
c.release();
}
void print(C_DATA c)
{
printf("%d\n", c.getData());
}
- C_DATA 클래스는 외부 데이터나 동적메모리를 관리하며 외부 데이터를 포인터로 받아와 얕은 복사를 사용하며 동적할당을 통해 메모리를 관리하며, 사용 후에는 반드시 해제를 한다.
2. 2차원 배열 동적할당
2.1 설명
2차원 배열의 동적 할당은 일반적인 2차원 배열과 개념이 다르다.
기본적으로 배열은 한 줄로 연속된 메모리를 할당받고, 2차원 배열은 여러 개의 1차원 배열이 연결된 형태이다.
그러나 2차원 배열을 동적 할당할 경우, 각각의 행이 독립적으로 할당되기 때문에 끊어진 배열이라는 점이 차이점이다.
이는 각 행이 개별적인 1차원 배열을 가지게 되어 2차원 배열처럼 사용할 수 있을 뿐이다.
[ 1차원 배열 동적할당 ]
int *p = new int[3]{};
[ 2차원 배열 동적할당 ]
int (*p) [4]{};
p = new int [3][4]{};
- 하지만 배열의 크기가 상수로 정의되면 범용성이 떨어지기 때문에 다른 방식으로 사용이 된다.
2.2 사용법
#include <iostream>
int main()
{
int* p[3] = { nullptr,nullptr ,nullptr };
p[0] = new int[4] {};
p[1] = new int[4] {};
p[2] = new int[4] {};
for (int i = 0; i < 3; i++)
{
delete[] p[i];
}
}
- 위 코드는 기본적인 2차원 배열을 동적 할당하는 형태이므로, 동적 할당을 수행한 후에는 반드시 해제를 해줘야 하며, 해제는 할당한 순서의 역순으로 진행한다.
2.3 예시
#include <iostream>
int main()
{
int** p{};
p = new int* [3]{};
for (int i = 0; i < 3; i++)
{
p[i] = new int [4] {};
}
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", p[i][j]);
}
printf("\n");
}
for (int i = 0; i < 3; i++)
{
delete[] p[i];
}
delete[]p;
}
int** p {};
p = new int*[3]{};
- 포인터의 포인터를 사용하여 1차원 배열을 동적 할당하는 걸로 p는 int* 타입의 포인터를 3개 가리키는 포인터이다.
- 즉, p는 3개의 int* 포인터를 가지는 배열로 초기화되며, 이 배열은 각 행의 시작 주소를 저장하는 역할을 한다.
p [i] = new int [4];
- 반복문에서는 p의 각 요소에 대해 4개의 정수를 담을 수 있는 1차원 배열을 동적으로 할당한다
- 결과적으로 p는 3개의 행을 가지며, 각 행은 4개의 정수형 요소를 포함하는 구조가 된다.
delete [] p [i];
- 각 행에 대해 동적 할당된 메모리를 해제한다.
- 이 반복문은 p 배열에 저장된 각 포인터가 가리키는 1차원 배열을 메모리에서 제거한다.
delete [] p;
- 마지막으로 p 자신이 가리키는 1차원 배열(각 행의 포인터)을 해제한다.
- 이는 p가 더 이상 필요한 메모리를 사용하지 않도록 하기 위함이다.
3. 소멸자
3.1 정의
객체가 소멸될 때 자동으로 발생하는 메서드이다.
이는 객체의 생명주기가 끝날 때 발생하며, 주로 동적으로 할당된 메모리를 해제하기 위해 사용된다.
객체가 범위를 벗어나거나 명시적으로 삭제될 때 소멸자가 호출된다.
3.2 문법
소멸자는 생성자와 비슷한 구조를 가는데, 클래스 정의에서 ~ 기호를 붙여서 선언한다
class C_DATA
{
private:
public:
C_DATA(); // 생성자
~C_DATA(); // 소멸자
};
3.3 예시
[header.h]
#pragma once
class C_DATA
{
private:
int* m_pData;
public:
C_DATA();
~C_DATA();
void setData(int nData);
int getData();
};
[header.cpp]
#include "data.h"
C_DATA::C_DATA()
{
m_pData = new int{};
}
C_DATA::~C_DATA()
{
delete m_pData;
m_pData = nullptr;
}
void C_DATA::setData(int nData)
{
*m_pData = nData;
}
int C_DATA::getData()
{
return *m_pData;
}
[main.cpp]
#include <iostream>
#include "data.h"
int main()
{
C_DATA c{};
c.setData(100);
printf("%d\n", c.getData());
}
- 생성자에서 동적 메모리를 할당하고, 소멸자에서 메모리를 해제하는 구조이다.
3.4 주의할 점
#include <iostream>
#include "data.h"
void func(C_DATA c);
int main()
{
C_DATA c{};
c.setData(100);
printf("%d\n", c.getData());
func(c);
}
void func(C_DATA c)
{
}
이 코드에서 발생하는 문제는 소멸자가 두 번 호출된다는 것이다.
- func(c) 호출 시 c가 얕은 복사로 전달되어 m_pData라는 포인터가 두 개의 객체에 의해 공유가 된다.
- func의 스코프가 종료되면 func의 지역 변수 c에 대해 소멸자가 호출되는데 이때 m_pData가 해제된다.
- 이후 main 함수에서 c의 스코프가 종료되면서 또다시 소멸자가 호출이 되는데 이미 해제된 메모리에 접근하게 되어 프로그램이 비정사적으로 종료가 된다.
즉, 각 객체의 생명주기마다 소멸자는 하나만 호출되어야 하며, 얕은 복사를 피하거나 소유권을 명확히 해야 한다.
이를 통해 메모리 관리와 리소스 소유를 명확히 하여 예외 상황을 방지할 수 있다.
'C++' 카테고리의 다른 글
[강의] 11월 6일 수업정리 (0) | 2024.11.07 |
---|---|
[강의] 11월 5일 수업정리 (3) | 2024.11.05 |
[강의] 10월 31일 수업정리 (2) | 2024.11.01 |
[강의] 10월 30일 수업정리 (2) | 2024.10.31 |
[강의] 10월 29일 수업정리 (1) | 2024.10.29 |