220926 C++ #13

김혜진·2022년 9월 26일
0

C++

목록 보기
12/12

C++ #13

스마트 포인터

자동 파괴자 auto_ptr

  • C++에서는 객체 생성 시 사용했던 동적 메모리 또는 시스템 자원을 소멸 시 자동으로 소멸할 수 있는 매커니즘을 제공한다.
  • 범위를 벗어난 변수는 스택에서 제거되며, 객체의 파괴자가 호출되어 자신이 사용하던 자원을 알아서 정리한다.

auto_ptr의 필요성

동적 메모리 해제 시 문제점

  • 일반 파괴자는 스택으로 할당된 객체에 대해서는 소멸을 하지만, 동적으로 할당한 메모리에 대해서는 책임지지 않는 문제점이 있다.

  • 실수로 delete문을 빼먹는 경우

#include<iostream>

using namespace std;

void main()
{
	double* rate;

	rate = new double;
	*rate = 3.1415;
	cout << *rate << endl;
    // delete rate;
}

8바이트의 메모리 누수가 일어난다.

  • rate 변수는 동적으로 생성한 실수형 변수의 메모리를 저장하는 포인터 변수이다.

  • rate 변수의 동적 메모리를 해제하는 delete 수행을 주석처리한다.

  • rate 변수는 종료 시에 자동으로 메모리가 소멸되지만, 이 변수가 가리키는 주소 메모리는 자동으로 해제되지 않는다. 즉, 메모리 누수(Memory Leak)가 발생한다.

  • 동적으로 할당된 메모리는 이름이 없으므로 포인터를 잃어버리면 참조할 수 없어서 해제가 어렵게 된다.

  • 짧은 코드에서는 delete문을 빼먹는 실수는 하지 않을 것이다.

  • 정상적인 실행흐름이면 new/delete가 짝을 이루어 할당 해제가 수행되지만, 예외 조건에 의해 예외 처리 시에는 catch문 수행 후 delete는 수행하지 못하게 된다.

  • 지역 객체가 가리키는 메모리까지 해제되는 것은 아니기 때문에 메모리 누수(Memory Leak)가 발생한다.

  • 이러한 메모리 누수는 양이 많지 않아 당장은 별 문제가 되지 않지만, 오랫동안 실행되는 프로그램은 시스템 자원을 갉아먹기 때문에 나중에는 심각한 문제가 될 수 있다.

  • 이러한 문제를 해결하기 위해 만들어진 것이 바로 auto_ptr이다.

auto_ptr의 사용방법

auto_ptr의 형태

  • auto_ptr은 동적으로 할당된 메모리도 자동으로 해제하는 포인터의 레퍼 클래스이다.
  • auto_ptr 템플릿은 memory 헤더파일에 정의되어 있으므로 사용시 헤더를 선언한다.
  • auto_ptr 템플릿은 내부에 다음과 같이 정의되어 있다.
template<typename T> class auto_ptr
  • 포인터가 가리키는 대상체의 타입 T를 인수로 받아들이며 T* 형의 포인터를 관리한다.
  • 생성자로 전달한 포인터는 소멸자에서 delete로 해제하므로 포인터 뿐만 아니라 포인터가 가리키는 메모리도 자동으로 해제된다.
#include<iostream>
#include<memory>

using namespace std;

void main()
{
/*double* rate;

	rate = new double;
	*rate = 3.1415;
	cout << *rate << endl;
	delete rate;*/

	auto_ptr<double> rate(new double);
	*rate = 3.1415;
	cout << *rate << endl;
}
  • 새로운 double형 변수를 동적으로 할당하여 생성자를 전달했다.
  • rate의 소멸자에서는 delete를 자동으로 호출하므로 함수가 끝날 때 rate를 따로 해제할 필요가 없으며 해제되지도 않는다.
  • rate 객체 자체는 포인터가 아니기 때문에 delete rate 코드를 추가하면 컴파일 에러가 난다.

auto_ptr의 내부구조

smartPointer 클래스 분석

  • 생성자에서 T* p를 선언하는데 이 포인터는 생성자에서 초기화된다.
  • 객체 소멸 시 소멸자 내부에서는 포인터 p를 delete시킴으로써 p와 p가 가리키는 힙 메모리까지 모두 정리된다.
  • 오버로딩한 * 연산자를 객체로 호출하면 내부에서는 *p를 리턴한다.
  • 오버로딩한 -> 연산자를 객체로 호출하면 내부에서는 p를 리턴한다.
#include<iostream>
#include<memory>

using namespace std;

template <class T>
class smartPointer
{
private:
	T* p;
public:
	smartPointer(T* sp) 
	{
		p = sp;
	};
	~smartPointer() 
	{
		delete p;
	};

	T operator*() const
	{
		return *p;
	}
	
	T* operator->() const
	{
		return p;
	}
};

void main()
{
	smartPointer<string> pStr(new string("test"));
	cout << *pStr << endl; // pStr.operator*()
	cout << pStr->size() << endl;
}

출력결과
test
4


유용한 기법들

코딩스타일

중괄호 작성 스타일

  • C++, 자바, C# 등 컴파일러 기반의 대부분 언어들은 {}를 많이 사용한다.
  • {}의 사용에 따라 소스의 구조가 결정될 수도 있다.
  • GNU 스타일 : 블록을 통째로 안으로 들여쓴다. 구조가 제일 잘 보이는 스타일이기는 하지만 들여쓰기 정도가 심해서 수평으로 많은 코드를 작성하기 힘들다.
  • K&R 스타일 : C언어 C++ 창시자들이 이 스타일로 문서를 작성한다. 괄호가 블록 시작행의 끝에 있다는 점이 특이하다. 코드 라인을 절감할 수 있다는 장점이 있다.
  • BSD 스타일 : 여는 괄호가 별도의 소스 라인을 차지하므로 코드가 길어지는 단점이 있지만, 블록의 구조가 명시적으로 잘 보이며, 가독성이 좋은 장점이 있다.

들여쓰기

  • 블록 앞쪽에 탭이나 공백을 넣어 상위 블록보다 안쪽에 배치하여 종속관계를 표현한다.
  • 들여쓰기는 탭이나 공백을 사용한다.
  • 일반적으로 4가 가장 무난하다.

빈 줄 사용 여부

  • 빈 줄의 사용에 따라서 가독성의 차이가 생긴다.
  • 일반적으로 지역 변수 선언문과 본체 코드 사이를 빈 줄로 구분하는 것이 좋다.

명칭 작성법

  • 변수나 함수의 이름을 작성 시 언어나 스타일에 따라 조금씩 다르다.
  • 대상과 연관된 이름으로 작성해야 한다.
  • 이름이 너무 짧거나 길어서도 안된다. 3자 이상 10자 이내가 적당하다.
  • 길이가 긴 이름의 경우 단어와 단어 사이에 _(언더바)를 넣어서 이어주는 것도 좋다.
  • 어근만 대문자인 경우는 주로 함수의 이름을 정하는 경우 사용한다.
  • 변수의 이름 앞에 접두어 b, p, i 등을 붙여 타입을 구분해주는 경우도 있다.
    Ex) int iLoop, bool bStatus, char* pC

조건부 컴파일

조건부 컴파일 지시자

  • 지정한 조건의 진위 여부에 따라 코드의 일정 부분을 컴파일 할 것인지 결정한다.
  • 전처리문이므로 컴파일 전에 조건을 평가한다.
  • 한 벌의 코드를 조건에 따라 다르게 컴파일하여 상이한 실행 파일을 만들 수 있다.
#ifdef 매크로명
		코드
#endif
  • #ifdef 다음에 조건이 되는 매크로명을 써주고, #endif 사이에 조건부로 컴파일 할 코드를 작성한다.
#include<iostream>
#include<list>
#include<vector>

using namespace std;

#define LIST // 주석처리하면 vector가 실행
void main()
{
	int i;

#ifdef LIST
	list<int> lst;
	list<int>::iterator it;
#else
	vector<int>lst;
	vector<int>::iterator it;
#endif 
	for (i = 0; i < 5; i++)
		lst.push_back(i);
		
	for (it = lst.begin(); it != lst.end(); it++, i++)
		cout << i << "번째=" << *it << endl;
}

#pragma once

  • 소스 코드의 상단에서 심심치 않게 볼 수 있는 매크로이다.

  • #pragma 지시자는 컴파일러 지시자로써 플랫폼별로 다른 기능들에 대한 지시사항을 컴파일러에게 전달하기 위한 기능을 가지고 있다.

  • #pragma once를 헤더파일의 선두에 써두면 컴파일러는 딱 한 번만 헤더 파일을 포함한다.

  • 같은 헤더파일을 일부러 두 번 포함하지는 않겠지만 헤더 파일끼리 중첩하다보면 문제가 발생할 수 있다.

  • 조건부 컴파일 지시자로 한 번만 포함되도록 하는 것과 동일한 효과이다.


포인터와 참조자

  • 참조자 : 널 참조자(null reference)는 존재하지 않는다. 참조자는 어떠한 경우에서든지 메모리를 차지하고 있어야 한다.
  • 포인터 : 널 포인터(null pointer)가 존재한다.
  • 만약 객체를 참조하는 변수를 두고 싶은데, 그 객체가 널일 가능성이 있다면 참조자를 사용하지 말고, 포인터를 사용하라.
  • 다음과 같은 코드는 괘씸하고 못된 코드이다.
char *pc = 0;
char& rc = *pc;

포인터와 참조자를 구분하여 사용하자

  • 참조자는 반드시 초기화해야 한다.
string& rs; // 초기화 에러
string s("abc");
string& rs = s; // 초기화된 변수를 참조하므로 문제 없음
  • 참조자는 유효성 검사를 할 필요가 없다.
void prontDouble(const double& rd)
{
	cout << rd;
}
  • 포인터는 객체의 유효성을 반드시 검사해야 한다.(널포인터 검사)
void printDouble(const double *pd)
{
	if(pd)
    {
    	cout << *pd;
    }
}
  • 포인터를 써야 하는 경우
    가리킬 객체의 주소가 없을 때 (널포인터)
    하나의 변수를 가지고 여러 개의 객체를 바꾸어 참조해야 하는 경우

  • 참조자를 써야 하는 경우
    참조할 포인터가 반드시 존재할 것임을 알고 있을 때
    참조할 대상 객체를 바꿀 필요가 없을 때

메모리 릭(Memory Leak)

#include<iostream>
#include<crtdbg.h>

using namespace std;

void main()
{
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	int* nData = new int;
}

출력결과
Detected memory leaks!
Dumping objects ->
{74} normal block at 0x000001C9D8553650, 4 bytes long.
Data: < > CD CD CD CD
Object dump complete.
'[14400] AdvancedCplus.exe' 프로그램이 종료되었습니다(코드: 0 (0x0)).

74 : 74번째 메모리가 생성되는 부분에서 에러가 남.

	_CrtSetBreakAlloc(74);

문제가 되는 번호를 넣어 문제가 되는 곳을 파악할 수 있다.

profile
알고 쓰자!

0개의 댓글