순수 가상 함수
- 일반 가상함수는 무조건 구현해줘야 한다.
- 순수 가상함수는 구현이 없다.
- 순수 가상함수가 있는 클래스는 무조건 추상 클래스이다.
- 함수 선언 끝에 = 0 을 붙이게 되면 그 함수는 순수 가상 함수가 된다.
추상 클래스
- 추상 클래스는 객체화 할 수 없고 상속을 통해 파생 클래스에서 구현해야만 객체를 생성할 수 있다.
- 추상 클래스를 포인터 형식으로 만들고 자식클래스의 포인터를 담는 것은 가능하다.
- 추상 클래스를 상속받는 자식 클래스는 무조건 부모의 순수 가상함수를 무조건 구현해야된다.
class Base {
public:
virtual void myFunction() = 0; // 순수 가상 함수
};
사용 목적
- 인터페이스 설계 : 순수 가상 함수를 사용하면 인터페이스만 제공하고, 구현을 파생클래스에서 강제할 수 있다.
공통적인 인터페이스를 유지하면서 각 파생 클래스에서 다양한 구현을 유지할 수 있다.
- 다형성 : 추상 클래스와 순수 가상 함수를 통해 런타임 다형성을 구현할 수 있다. 객체가 어느 파생클래스에서 왔는지에 관계없이 동일한 방식으로 처리할 수 있다.
- 강제적인 함수 재정의: 파생 클래스에서 특정 함수를 반드시 구현하도록 강제할 수 있다. 순수 가상 함수를 구현하지 않으면 컴파일 오류가 발생하기 때문에 구현이 누락된 부분을 발견할 수 있다.
동적 할당
런타임 중에 필요한 경우에 따라 메모리의 크기나 개수를 유동적으로 할당하는 방법
메모리 공간은 힙(Heap) 메모리 영역에 할당되고 사용이 끝난 후에는 명시적으로 반드시 해제해줘야 한다.
프로그램이 실행 될때 필요한 만큼의 메모리를 할당할 수 있어 프로그램의 유연성과 효율성을 높인다.
c 스타일 동적할당
#include <malloc.h> 또는 <stdlib.h> 필요
malloc, free
#include <stdio.h>
#include <stdlib.h>
int main() {
int* ptr = (int*)malloc(sizeof(int)); // 정수형 변수 동적 할당
*ptr = 10; // 동적 할당된 메모리에 값 저장
free(ptr); // 메모리 해제
int* ptr = (int*)malloc(sizeof(int)*10); // 정수형 배열 동적 할당
for(int i = 0; i< 10 ;i++)
{
ptr[i] = 10;
}
free(ptr); // 메모리 해제
return 0;
}
C 스타일에서는 malloc을 통해 변수를 동적으로 할당할 수 있지만 include가 필요하다.
동적할당을 하게 되면 포인터 변수는 동적 할당한 주소를 가리키기만 하고 초기화는 따로 해야 한다.
사용이 끝난 후에는 free를 통해 반드시 해제시켜줘야 한다.
c++ 스타일 동적할당
#include <stdio.h>
#include <stdlib.h>
int main() {
int* ptr = new int(10); // int형 변수 하나를 동적 할당과 동시에 초기화
delete ptr; // ptr이 가리키는 메모리 해제
ptr = nullptr;
int* arr = new int[5]{0,}; // int형 배열 5개를 동적 할당과 동시에 0으로 초기화
for (int i = 0; i < 5; i++) {
arr[i] = i * 2; // 배열 초기화
}
delete[] arr; // 배열 메모리 해제
arr = nullptr;
return 0;
}
c++ style 동적할당은 new와 delete라는 operator를 통해 이루어지기 때문에 따로 include가 필요하지 않다.
동적할당을 하면서 동시에 초기화도 할 수 있다.
배열인 경우와 단일 값인 경우 할당 해제 방법이 다르다.
댕글링 포인터
이 동적 할당된 메모리에 대해 처음 할당한 포인터 말고 다른 포인터도 이 메모리 주소를 가리키고 있을 수 있다.
이 때 한 포인터에서 할당된 메모리를 해제해버리고 다른 포인터에서 이 메모리를 참조하려고 하면 오류가 생긴다.
이미 할당이 해제된 메모리를 여전히 가리키고 있는 포인터 변수를 댕글링 포인터라고 한다.
이러한 문제를 방지하기 위해 사용이 끝난 ptr은 반드시 nullptr로 설정해주거나 스마트 포인터를 사용 등의 방법이 있다.
힙 메모리
동적할당된 애들이 저장되는 공간 인데
스택이랑 반대방향으로 메모리가 사용된다.
힙도 마찬가지로 정해진 공간을 넘어서 사용하게 되면 힙 오버플로우가 발생하게된다.
데이터 메모리 영역 추가 설명
메모리의 데이터 영역은 좀더 세분화 된다.
.rodata(Read-Only Data)
const char* msg = "Hello, World!"; // .rodata에 저장
- 읽기 전용 데이터를 저장하는 영역.
- 주로 상수와 같은 변경되지 않는 데이터를 저장. 예) 문자열 리터럴이나 const 키워드로 선언된 변수들
- 읽기 전용이므로 프로그램 실행중에 값을 변경할 수 없고, 변경시 런타임 오류가 발생한다.
.data(Initialized Data)
int globalVar = 42; // .data에 저장
static int staticVar = 10; // .data에 저장
- 초기화된 전역 변수나 정적 변수가 저장되는 영역
- 전역 변수나 정적 변수가 초기화된 값을 가지고 있는 경우 저장
- 이 영역에 저장된 변수들은 이미 초기화가 된 상태
.bss(Block Started by Symbol)
int uninitializedGlobal; // .bss에 저장 (0으로 초기화됨)
static int uninitializedStatic; // .bss에 저장 (0으로 초기화됨)
- 초기화 되지 않은 전역 변수나 정적 변수가 저장되는 영역
- 전역 또는 정적 변수가 명시적인 초기값이 없는 경우 여기에 저장된다.
- 프로그램 실행 시 자동으로 0으로 초기화 된다.
- 실제로 실행파일 크기를 늘리지 않기 때문에 초기화되지 않은 변수를 많이 사용해도 프로그램 크기에 영향을 덜 준다.
동적 할당의 문제점
프로그래머에게 모든 권한을 주기 때문에 할당된 메모리가 제대로 해제안되는 경우 메모리 누수 발생
(스마트 포인터를 쓰는 것으로 어느정도 해결 가능한데 아직 안 씀)
해결 방법
_CrtSetDbgFlag()를 사용하면 누수가 발생하고 있는지를 확인할 수 있다.(#include <crtdbg.h>)
헤더중에 제일 아래에 둔다.
코드 실행이 종료되었을 때 절대절대 메모리 누수가 발생했으면 안된다.
헤더파일이랑 설정하는 법 무조건 외우자.
#include <crtdbg.h>
using namespace std;
int main()
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
int* ptr = new int[100];
return 0;
}
이 코드를 실행해보자.
내가 할당한 모든 메모리에 대해서 체크(_CRTDBG_ALLOC_MEM_DF)
메모리들 누수 있는지 체크(_CRTDBT_LEAK_CHECK_DF)
ptr에 int[100]크기의 배열을 동적으로 할당하고 이 메모리를 해제(delete)하지 않은상태로
종료하면 메모리 누수에 대한 내용이 출력된다.
로그에 메모리 누수가 발견되었다는 내용이다.
int[100] (4byte)*100 = 400byte
400바이트가 제대로 할당해제 되지 않았다고 알려주고 있다.
동적 할당된 배열을 할당 해제할 때는 꼭 delete[] 연산자를 사용해서 사용해야 한다.
safe delete
할당 해제할 때는 포인터가 가리키는 것이 nullptr이 아닌것을 확인하고 삭제해야 한다.
그리고 delete할 때 상속된 class인 경우 소멸자에 virtual이 꼭필요하다.
아래 코드를 보자.
class A
{
public:
A() { cout << "A 생성자 호출\n"; }
~A() { cout << "A 소멸자 호출\n"; }
private:
};
class B : public A
{
public:
B(){ cout << "B 생성자 호출\n"; }
~B() { cout << "B 소멸자 호출\n"; }
private:
};
int main()
{
A* a = new B();
delete a;
return 0;
}
B라는 객체를 생성해서 부모인 A의 포인터로 전달했는데
할당을 해제할 때 B의 소멸자가 호출되지 않았다.
만약에 B의 멤버 변수중 동적할당을 받는 변수가 있다면 B의 소멸자는 호출되지 않기 때문에
메모리 누수가 발생할 것이다.
A의 소멸자는 자신에게 할당된 객체가 B로 들어온 것인지 알 수 없다.
그래서 A의 소멸자가 호출되었을 때 A객체의 부분만 삭제한다.
따라서 소멸자에도 virtual을 붙여야 B의 소멸자를 호출할 수 있다.
class A
{
public:
A() { cout << "A 생성자 호출\n"; }
virtual ~A() { cout << "A 소멸자 호출\n"; }
private:
};
class B : public A
{
public:
B(){ cout << "B 생성자 호출\n"; }
~B() override { cout << "B 소멸자 호출\n"; }
private:
};
virtual을 붙이게 되면 B에 대한 소멸자까지 정상적으로 호출되는 것을 확인할 수 있다.
스마트 포인터를 쓰면 이러한 문제가 없을까 싶어서 해봤는데
unique_ptr에는 virtual 소멸자가 있어야 정상적으로 소멸자가 호출되고
shared_ptr에는 virtual과 관계없이 정상적으로 모두 할당 해제되는것을 볼 수 있었다.
이 부분에 대해서는 따로 찾아봐야 할 것 같다.
'개인 공부 및 프로젝트 > 국비과정' 카테고리의 다른 글
20241014 - 동적할당, 템플릿 (2) | 2024.10.25 |
---|---|
20241011 - default parameter, list initializer (0) | 2024.10.24 |
20241008 - 증감연산자, enum (0) | 2024.10.24 |
20241007 - 자료구조 (0) | 2024.10.24 |
20241004 - 소멸자, 함수포인터, virtual (0) | 2024.10.18 |