1. 자원 관리하기
1. 스택 메모리
메모리 영역이 크지는 않지만, 생존 영역을 벗어나면 자동으로 해제시켜준다.
스택 메모리의 생존 주기는 { } 안이다. 밖으로 나가면 해제된다.
static 키워드
한번 선언되면, 프로그램 종료될 때 소멸된다.
함수 안에서 선언한 후 함수가 끝나더라도, 값이 사라지지 않는다.
아래 예시의 경우 11, 12, 13, ... , 20 까지 출력된다.
#include <iostream>
using namespace std;
void func()
{
static int a = 10;
a++;
cout << "a : " << a << "\n";
}
int main()
{
int n = 10;
while(n--)
func();
return 0;
}
2. 힙 메모리
프로그램 실행 시 동적으로 할당하는 공간은 힙 메모리를 활용하게 된다.
new와 delete 연산자를 이용
스택 메모리 영역처럼 자동으로 해제되지 않기 때문에, 코드를 짤 때 조심해야 한다.
변수 하나의 동적 할당과 해제
void func()
{
int* ptr = new int(10);
delete ptr;
}
배열의 동적 할당과 해제
아래 예시는 사용자의 입력을 받아서 배열의 크기를 할당해주는 예시이다.
실제로 이런식으로 사용하는 것은 위험할 수 있다.
void func()
{
int size;
cin >> size;
if (size > 0)
{
int* arr = new int[size];
for (int i = 0; i < size; ++i)
{
arr[i] = i * 2;
}
}
}
3. Dangling Pointer
가리키고 있던 메모리 공간이 해제 되어 버려서, 포인터가 해지된 공간을 계속해서 가리키고 있는 상황을 말한다.
해지된 곳을 가리키는 것은 Segment Fault (잘못된 메모리 참조) 를 발생시킬 수 있다.
dangling pointer를 발생시킬 수 있는 두가지 나쁜 예시
메모리 공간을 공유하는 두 개의 포인터 중 한 가지만 해제하는 경우(func1)
같은 공간의 메모리 해제를 두 번하는 경우(func2)
func2의 경우 컴파일 이전에 warning을 준다. (초기화 되지 않은 메모리를 사용한다)
디버거로 실행한 경우 아래의 오류가 뜬다.
void func1()
{
int* ptr = new int(40);
int* ptr2 = ptr;
cout << "ptr adress = " << ptr << endl;
cout << "ptr2 adress = " << ptr2 << endl;
cout << *ptr << endl;
delete ptr;
cout << *ptr2 << endl; //40이 아닌 이상한 값이 출력된다.
}
void func2()
{
int* ptr = new int(30);
cout << "Value: " << *ptr << endl;
delete ptr;
delete ptr; //이 경우 컴파일러에서도 warning 을 주고, 컴파일 후 비정상적으로 종료된다.
}
4. Smart Pointer
위에서 언급한 힙 메모리에서 관리해야 하는 데이터들은 메모리 해제가 굉장히 중요하기 때문에 코드를 잘 관리하여 메모리 할당과 해제를 잘 해줘야 한다는 어려움이 있다.
이러한 어려움을 해결해 주는 것이 reference counter를 이용한 smart pointer이다.
smart pointer는 자신을 참조하고 있는 포인터의 갯수가 0이 되는 순간 자동적으로 메모리를 해제해주는 방식으로 동작한다.
#include <memory> 를 해서 사용할 수 있다.
종류
unique_ptr
reference counter가 최대 1인 smart pointer이다.
그렇기 때문에 복사 혹은 대입이 불가능하다.
move를 통해서 소유권을 이전시켜주는 방식은 가능하다.
shared_ptr
reference counter가 N개가 될 수 있는 smart pointer이다.
현재 reference counter를 볼 수 있는 use_count()와
현재 포인터를 초기화하는 reset() 함수가 존재한다.
unique_ptr 예시
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
public:
MyClass(int val) :value(val)
{
cout << "MyClass 생성: " << value << "\n";
}
~MyClass()
{
cout << "MyClass 소멸" << value << "\n";
}
const void Display()const
{
cout << "value: " << value << "\n";
}
private:
int value;
};
int main()
{
unique_ptr<MyClass> myObject = make_unique<MyClass>(10);
cout << "--------\n";
myObject->Display();
unique_ptr<MyClass> newObject = std::move(myObject);
if (myObject == nullptr)
{
cout << "empty object\n";
}
newObject->Display();
cout << "--------\n";
return 0;
}
실행 결과
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
public:
MyClass(int val) :value(val)
{
cout << "MyClass 생성: " << value << "\n";
}
~MyClass()
{
cout << "MyClass 소멸" << value << "\n";
}
const void Display()const
{
cout << "value: " << value << "\n";
}
private:
int value;
};
int main()
{
shared_ptr<MyClass> myObject = make_shared<MyClass>(10);
cout << "--------\n";
shared_ptr<MyClass> newObject1 = myObject;
shared_ptr<MyClass> newObject2 = newObject1;
cout << "ref count : " << myObject.use_count() << "\n";
newObject1->Display();
newObject2->Display();
newObject2.reset();
cout << "ref count : " << myObject.use_count() << "\n";
cout << "--------\n";
return 0;
}
실행 결과
5. 얕은 복사와 깊은 복사
얕은 복사
포인터 연산에서 실제 값이 복사되는 것이 아니라 위치가 공유되는 것이다.
포인터 연산에서 대입연산자 주의하자
아래 예시처럼 대입하는 경우 위치가 공유되는 것이다!
위치가 공유된다는 것은 B라는 int 타입의 포인터가 만들어진 후 A 포인터가 가리키는 주소값을 가리키게 만들었다 라는 의미이다.
아래의 예시에서 주석처리한 곳에 ...된 곳을 보면 A와 B의 주소값이 같은 것을 볼 수 있다.
즉 A와 B는 같은 곳을 가리킨다는 의미이다.
#include <iostream>
using namespace std;
int main()
{
int* A = new int(30);
int* B = A;
cout << *A << " " << A << "\n"; //30, ...50
cout << *B << " " << B << "\n"; //30, ...50
delete A;
cout << *B << endl; //dangling pointer
return 0;
}
깊은 복사
dangling pointer 방지하기
주석처리한 곳에 ... 된 곳이 주소값을 말하는데, A와 B의 주소값이 다른 것을 볼 수 있다.
즉, 30이라는 값을 가리키는 새로운 메모리 공간을 할당한 것
#include <iostream>
using namespace std;
int main()
{
int* A = new int(30);
int* B = new int(*A);
cout << *A << " " << A << "\n"; // 30, ...10
cout << *B << " " << B << "\n"; // 30, ...90
delete A;
cout << *B << endl; // 30
delete B;
return 0;
}
6. 숙제
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Logger
{
public:
Logger()
:logCount(0)
{}
~Logger()
{
cout << "Logger instance destroyed.\n";
}
void LogInfo(string message);
void LogWarning(string message);
void LogError(string message);
void ShowTotalLogs();
private:
int logCount;
};
void Logger::LogInfo(string message)
{
logCount++;
cout << "[INFO]: " << message << "\n";
}
void Logger::LogWarning(string message)
{
logCount++;
cout << "[WARNING]: " << message << "\n";
}
void Logger::LogError(string message)
{
logCount++;
cout << "[ERROR]: " << message << "\n";
}
void Logger::ShowTotalLogs()
{
cout << "Total logs recorded: " << logCount << "\n";
}
int main()
{
unique_ptr<Logger> logger = make_unique<Logger>();
logger->LogInfo("System is starting");
logger->LogWarning("Low disk space");
logger->LogError("Unable to connect to the server");
logger->ShowTotalLogs();
return 0;
}
2. 템플릿
1. 함수 오버로딩
이름이 같은데, 매개변수가 다른 함수
매개변수의 타입이 다르거나
매개변수의 갯수가 다르거나
함수 오버로딩의 순서
정확한 타입 일치
암묵적인 타입 변환
오버로딩된 함수 매개변수 타입이 (int, int) 와 (float, float) 인 경우
우리가 해당 함수에 int float 타입 변수를 넣었다면, 두 번째로 넣은 변수인 float를 int로 형변환 하여 (int, int) 함수를 호출한다.
2. 템플릿
숙제
파라메터가 boolean 일 때 컴파일 에러 발생시켜야 하는데, 이대로 구현한 결과 에러가 발생하지 않아
bool 타입 템플릿 특수화와 = delete; 를 통해 에러를 발생시키도록 하였다.
#include <iostream>
using namespace std;
template <typename T>
T Add(T a, T b)
{
return a + b;
}
//컴파일 에러 발생시키기 위한 처리
template<>
bool Add(bool a, bool b) = delete;
int main() {
// 정수 더하기
cout << "3 + 5 = " << Add(3, 5) << endl;
// 실수 더하기
cout << "2.5 + 4.3 = " << Add(2.5, 4.3) << endl;
// 문자열 합치기
cout << "\"Hello, \" + \"World!\" = " << Add(string("Hello, "), string("World!")) << endl;
// 아래 코드는 컴파일 에러가 발생해야 함
//cout << Add(true, false) << endl;
return 0;
}