C++
[강의] 12월 17일 수업정리
k-codestudy
2024. 12. 17. 17:56
오늘은 싱글톤, 포인터 함수 기본에 대한 수업을 들었다.
1. 싱글톤
1.1 싱글톤 기본 원형
싱글톤이란 특정 클래스의 인스턴스를 단 하나만 생성되도록 보장하는 디자인 패턴이다.
[디자인 패턴, 간단하게 말해서 클래스 결합 예제로 클래스를 이런 식으로 만들게 되면 그 방식대로 사용가능하다는 이야기]
- 여러 번 호출되더라도 새로운 인스턴스를 생성하지 않고, 최초 호출 시 생성된 인스턴스를 재활용합니다.
예시를 봐보자
#pragma once
class C_DATA
{
private:
int m_nData;
private:
C_DATA() = default; // 생성자를 private으로 선언하여 외부에서 호출하지 못하도록 막음
public:
void setData(int nData);
int getData();
static C_DATA* getInstance(); // 싱글톤 인스턴스 반환 정적 함수
};
- 생성자를 private에 위치하여 외부에서 객체를 직접 생성하지 못하도록 막는다.
- 정적 메서드로 접근한다. ( getInstance 메서드는 클래스의 유일한 인스턴스에 접근할 수 있도록 한다 )
- 정적 메서드를 사용하지 않으면 인스턴스 없이 호출할 수 없기 때문에, 반드시 static으로 선언해야 한다.
#include "data.h"
void C_DATA::setData(int nData)
{
m_nData = nData;
}
int C_DATA::getData()
{
return m_nData;
}
C_DATA* C_DATA::getInstance()
{
static C_DATA cData{}; // 소속은 C_DATA이고, cData는 사라지는놈이기에 static으로 고정
return &cData; // cData의 주소값을 넘겨준다
}
- static 지역 변수 :
- cData는 함수 내에서 선언되지만 static으로 선언되었기에 프로그램이 종료될 때까지 메모리가 유지된다
- 최초 호출 시 단 한 번만 초기화된다. - 포인터 반환 :
- 함수는 cData의 주소값을 반환하여 외부에서 싱글톤 인스턴스를 사용할 수 있도록 한다.
#include <iostream>
#include "data.h"
int main()
{
C_DATA::getInstance()->setData(100);
printf("%d\n", C_DATA::getInstance()->getData());
}
- C_DATA::getInstance()를 호출하면 싱글톤 인스턴스가 반환된다.
- 반환된 포인터를 통해 -> 연산자를 사용하여 멤버 함수에 접근한다.
1.2 C++ 방식 + JAVA, C# 방식 [ //부분이 JAVA, C# ]
#pragma once
class C_DATA
{
private:
static C_DATA* m_pData;
int m_nData;
private:
C_DATA() = default;
public:
void setData(int nData);
int getData();
static C_DATA* getInstance();
static void createInstance();
static void releseInstance();
};
1.2.1 Singleton 패턴 구현
- 클래스의 객체 생성을 제한하고, 단일 인스턴스만 존재하도록 제어.
- 생성자를 private으로 지정하여 외부에서의 직접적인 객체 생성을 차단.
- m_pData는 static 멤버로, 모든 클래스의 객체가 공유하는 단일 메모리 영역을 사용.
1.2.2 createInstance와 releaseInstance 메서드 분리
- createInstance: Singleton 인스턴스를 생성. 늦은 초기화를 지원하며, 객체 생성을 명시적으로 제어 가능.
- releaseInstance: 생성된 Singleton 인스턴스를 삭제. 메모리 해제를 명확히 관리하여 C++에서 발생 가능한 메모리 누수를 방지.
1.2.3 getInstance 메서드
- 이미 생성된 Singleton 인스턴스를 반환.
- 객체 생성 시점을 제어하고 싶을 때 createInstance를 사용하며, getInstance는 단순 참조 역할만 수행.
#include "data.h"
C_DATA* C_DATA::m_pData = nullptr; // C_DATA * 소속이고 C_DATA:: 네임스페이스 해준 것
void C_DATA::setData(int nData)
{
m_nData = nData;
}
int C_DATA::getData()
{
return m_nData;
}
C_DATA* C_DATA::getInstance()
{
//if (!m_pData) // 내가 원하는 타이밍에 메모리를 호출하겠다.
// m_pData = new C_DATA{};
return m_pData;
}
void C_DATA::createInstance()
{
if (!m_pData)
m_pData = new C_DATA{};
}
void C_DATA::releseInstance()
{
if (m_pData)
{
delete m_pData;
m_pData = nullptr;
}
}
- C_DATA* m_pData는 C_DATA 클래스의 static 멤버로 선언된 포인터이다.
이를 C_DATA::m_pData로 명시적으로 초기화하여 네임스페이스를 명확히 지정하고, 초기값으로 nullptr을 할당.
1.2.4 createInstance와 releaseInstance를 분리했는가?
- 메모리 관리 문제
- C++에서는 메모리 관리가 직접적이기에, 객체의 생성과 해제 시점을 명확하게 정의해야 한다.
- getInstance에서 객체를 자동 생성하는 방식은 생성 시점을 불분명하게 만들고, 메모리 해제 누락 가능성이 생길 수 있다. 그렇기에 createInstance와 releaseInstance를 분리해 사용자가 명시적으로 객체의 생성과 해제를 관리하도록 설계한 것
- 명시적인 제어
- createInstance를 통해 필요한 시점에 객체를 생성할 수 있고, releaseInstance를 통해 필요가 없어졌을 때 객체를 명확히 해제할 수 있다.
- 늦은 초기화(Lazy Initialization)도 가능하며, 메모리 누수를 방지한다.
1.2.5 단일 스레드에서의 초기화 타이밍
오늘 강의하면서 나왔던 이야기인데 단일 스레드 환경에서는 정적 변수의 메모리 할당이 프로그램 시작 시점에 이루어지는데 getInstace()가 호출되기 전에 메모리가 준비되지 않는 경우는 아직까지 없기에 메모리 사용 시점에 문제는 존재하지 않는다 라는 의견이었다.
물론 멀티 스레드의 경우엔 그런 경우가 있다.
즉, 멀티 스레드의 경우 그런 경우가 있지만 단일 스레드의 호출이 프로그램 시작 시점보다 빠르게 되는 거 아니면 없다는 의견이다.
#include <iostream>
#include "data.h"
int main()
{
//C_DATA::getInstance()->setData(100);
//printf("%d\n", C_DATA::getInstance()->getData());
C_DATA::createInstance(); // 이러면 늦은 초기화도 가능하징
C_DATA::getInstance()->setData(100);
printf("%d\n", C_DATA::getInstance()->getData());
C_DATA::releseInstance();
}
2. 포인터 함수
#include <iostream>
int add(int n1, int n2);
int mul(int n1, int n2);
int main()
{
int (*pFunc)(int, int) {};
pFunc = add;
printf("%d\n", pFunc(10, 20));
pFunc = mul;
printf("%d\n", pFunc(10, 20));
//int (*arFunc[2])(int, int) {};
//int nType{};
//nType = 0;
//printf("%d\n", arFunc[nType](10, 20));
}
int add(int n1, int n2)
{
return n1 + n2;
}
int mul(int n1, int n2)
{
return n1 * n2;
}
2.1 함수 포인터란?
- 함수의 주소를 가리키는 포인터
- 특정 시그니처(반환값, 매개변수)가 동일한 함수들을 하나의 포인터에 담아 호출 가능
2.2 용도
- 실행 중 원하는 함수 선택 및 호출이 가능
- 다수의 함수 포인터 배열로 기능을 묶어서 관리 가능
2.3 핵심 특징
- 모양이 동일해야 사용 가능하다.
- C++에서는 상속과 다형성으로 대체 가능해 잘 사용되지 않음