답변:
RAII의 간단한 (과도하게 사용 된) 예제는 File 클래스입니다. RAII가 없으면 코드는 다음과 같습니다.
File file("/path/to/file");
// Do stuff with file
file.close();
다시 말해, 파일이 끝나면 파일을 닫아야합니다. 여기에는 두 가지 단점이 있습니다. 첫째, File을 사용하는 곳마다 File :: close ()를 호출해야합니다. 두 번째 문제는 파일을 닫기 전에 예외가 발생하면 어떻게됩니까?
Java는 finally 절을 사용하여 두 번째 문제를 해결합니다.
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
또는 Java 7부터 try-with-resource 문 :
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++은 RAII를 사용하여 두 가지 문제를 모두 해결합니다. 즉, File의 소멸자에서 파일을 닫습니다. File 객체가 적시에 파괴되는 한 (어쨌든) 파일을 닫는 것은 우리를 위해 처리됩니다. 따라서 코드는 다음과 같습니다.
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
객체가 언제 소멸되는지 보장 할 수 없기 때문에 Java에서는 수행 할 수 없으므로 파일과 같은 리소스가 언제 해제되는지 보장 할 수 없습니다.
똑똑한 포인터-많은 시간 동안 스택에 객체를 만듭니다. 예를 들어 (그리고 다른 답변에서 예를 훔치는) :
void foo() {
std::string str;
// Do cool things to or using str
}
이것은 잘 작동하지만 str을 반환하려면 어떻게해야합니까? 우리는 이것을 쓸 수 있습니다 :
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
그래서, 무슨 일이야? 반환 유형은 std :: string이므로 값으로 반환한다는 의미입니다. 이것은 str을 복사하고 실제로 사본을 반환한다는 것을 의미합니다. 비용이 많이들 수 있으므로 복사 비용을 피할 수 있습니다. 따라서 참조 또는 포인터로 반환한다는 아이디어를 얻을 수 있습니다.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
불행히도이 코드는 작동하지 않습니다. str에 대한 포인터를 반환하지만 스택에 str이 생성되었으므로 foo ()를 종료하면 삭제됩니다. 다시 말해, 호출자가 포인터를 얻을 때까지는 쓸모가 없습니다 (그리고 사용하면 모든 종류의 펑키 오류가 발생할 수 있기 때문에 쓸모없는 것보다 나쁩니다)
그래서 해결책은 무엇입니까? 우리는 new를 사용하여 힙에 str을 만들 수 있습니다-foo ()가 완료되면 str은 파괴되지 않습니다.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
물론이 솔루션도 완벽하지 않습니다. 그 이유는 str을 생성했지만 삭제하지 않기 때문입니다. 아주 작은 프로그램에서는 문제가되지 않지만 일반적으로 삭제해야합니다. 우리는 호출자가 객체를 끝내면 객체를 삭제해야한다고 말할 수 있습니다. 단점은 호출자가 메모리를 관리해야하므로 복잡성이 증가하고 잘못되어 메모리 누수가 발생하여 더 이상 필요하지 않더라도 객체가 삭제되지 않는다는 것입니다.
스마트 포인터가 나오는 곳입니다. 다음 예제에서는 shared_ptr을 사용합니다. 실제로 사용하려는 것을 배우기 위해 다양한 유형의 스마트 포인터를 살펴볼 것을 제안합니다.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
이제 shared_ptr은 str에 대한 참조 수를 계산합니다. 예를 들어
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
이제 동일한 문자열에 대한 두 개의 참조가 있습니다. str에 대한 나머지 참조가 없으면 삭제됩니다. 따라서 더 이상 직접 삭제하는 것에 대해 걱정할 필요가 없습니다.
빠른 편집 : 일부 의견에서 지적 했듯이이 예제는 두 가지 이유로 완벽하지 않습니다. 첫째, 문자열 구현으로 인해 문자열을 복사하는 것이 저렴한 경향이 있습니다. 둘째, 명명 된 반환 값 최적화라는 이름으로 인해 컴파일러가 작업 속도를 높이기 위해 영리한 작업을 수행 할 수 있으므로 값으로 반환하는 것은 비용이 많이 들지 않을 수 있습니다.
File 클래스를 사용하여 다른 예제를 시도해 봅시다.
파일을 로그로 사용한다고 가정 해 봅시다. 이것은 파일을 추가 전용 모드로 열고 자 함을 의미합니다.
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
이제 파일을 몇 가지 다른 객체에 대한 로그로 설정해 보겠습니다.
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
불행히도이 예제는 끔찍하게 끝납니다.이 메소드가 종료 되 자마자 파일이 닫힙니다. 즉, foo와 bar는 이제 유효하지 않은 로그 파일을 가지고 있습니다. 힙에 파일을 생성하고 파일에 대한 포인터를 foo와 bar 모두에 전달할 수 있습니다.
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
그러나 누가 파일 삭제를 담당합니까? 파일을 삭제하지 않으면 메모리와 리소스 누수가 모두 발생합니다. foo 또는 bar가 파일을 먼저 완료할지 여부를 알 수 없으므로 파일 자체를 삭제할 것으로 기대할 수 없습니다. 예를 들어, bar가 파일을 완료하기 전에 foo가 파일을 삭제하면 bar에 유효하지 않은 포인터가 있습니다.
당신이 짐작했듯이, 우리는 똑똑한 포인터를 사용하여 우리를 도울 수 있습니다.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
이제 파일 삭제에 대해 걱정할 필요가 없습니다. foo와 bar가 모두 완료되고 더 이상 파일에 대한 참조가 없으면 (foo 및 bar가 손상되어) 파일이 자동으로 삭제됩니다.
RAII 단순하지만 멋진 개념의 이상한 이름입니다. SBRM ( Scope Bound Resource Management) 이라는 이름이 더 좋습니다. 아이디어는 종종 블록의 시작 부분에 리소스를 할당하고 블록이 종료 될 때 리소스를 해제해야한다는 것입니다. 블록을 나가는 것은 정상적인 흐름 제어, 점프, 심지어 예외에 의해 발생할 수 있습니다. 이 모든 경우를 다루기 위해 코드가 더 복잡하고 중복됩니다.
SBRM없이 그것을하는 예제 :
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
보시다시피, 우리는 pwned 할 수있는 방법이 많이 있습니다. 아이디어는 리소스 관리를 클래스로 캡슐화한다는 것입니다. 객체의 초기화는 자원을 획득한다 ( "자원 획득은 초기화이다"). 블록을 종료 할 때 (블록 범위) 리소스가 다시 해제됩니다.
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
리소스를 할당 / 할당 해제하기위한 목적이 아닌 자체 클래스가 있으면 좋습니다. 할당은 작업을 완료하기위한 추가 문제 일뿐입니다. 그러나 자원을 할당 / 할당 해제하려는 즉시 위의 내용은 적합하지 않습니다. 획득 한 모든 종류의 리소스에 대해 래핑 클래스를 작성해야합니다. 이를 용이하게하기 위해 스마트 포인터를 사용하면 해당 프로세스를 자동화 할 수 있습니다.
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
일반적으로 스마트 포인터는 delete
소유 한 리소스가 범위를 벗어날 때 호출 되는 새로운 / 삭제 주위의 얇은 래퍼 입니다. shared_ptr과 같은 일부 스마트 포인터를 사용하면 소위 삭제 도구를 말할 수 있습니다 delete
. 예를 들어, 올바른 삭제 자에 대해 shared_ptr을 알려주는 한, 창 핸들, 정규식 리소스 및 기타 임의 항목을 관리 할 수 있습니다.
목적에 따라 다른 스마트 포인터가 있습니다.
암호:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
auto_ptr과 달리, unique_ptr은 컨테이너에 넣을 수 있습니다. 컨테이너는 스트림 및 unique_ptr과 같이 복사 할 수없는 (그러나 이동 가능한) 유형을 보유 할 수 있기 때문입니다.
암호:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
암호:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
보시다시피, plot-source (function fx)는 공유되지만 각 항목에는 별도의 항목이 있으며 색상을 설정합니다. 코드가 스마트 포인터가 소유 한 자원을 참조해야하지만 자원을 소유 할 필요는 없을 때 사용되는 weak_ptr 클래스가 있습니다. 원시 포인터를 전달하는 대신 weak_ptr을 작성해야합니다. 더 이상 리소스를 소유 한 shared_ptr이 없더라도 weak_ptr 액세스 경로로 리소스에 액세스하려고하면 예외가 발생합니다.
unique_ptr
, 및 sort
도 마찬가지로 변경됩니다.
RAII는 변수가 생성자에서 필요한 모든 초기화와 소멸자에서 필요한 모든 정리를 처리 하도록하는 디자인 패러다임 입니다. 이렇게하면 모든 초기화 및 정리가 단일 단계로 줄어 듭니다.
C ++에는 RAII가 필요하지 않지만 RAII 메소드를 사용하면보다 강력한 코드가 생성된다는 것이 점점 더 인정되고 있습니다.
RAII가 C ++에서 유용한 이유는 C ++가 정상적인 코드 흐름 또는 예외에 의해 트리거 된 스택 해제를 통해 범위에 들어오고 나가는 변수의 생성 및 소멸을 본질적으로 관리하기 때문입니다. 그것은 C ++의 공짜입니다.
모든 초기화 및 정리를 이러한 메커니즘에 연결함으로써 C ++이이 작업을 처리하도록 보장합니다.
C ++에서 RAII에 대해 이야기하면 포인터를 정리할 때 특히 취약하기 때문에 일반적으로 스마트 포인터에 대해 논의하게됩니다. malloc 또는 new에서 얻은 힙 할당 메모리를 관리 할 때 포인터를 제거하기 전에 해당 메모리를 비우거나 삭제하는 것은 프로그래머의 책임입니다. 스마트 포인터는 RAII 철학을 사용하여 포인터 변수가 소멸 될 때마다 힙 할당 개체가 소멸되도록합니다.
스마트 포인터는 RAII의 변형입니다. RAII는 리소스 획득이 초기화 중임을 의미합니다. 스마트 포인터는 사용하기 전에 리소스 (메모리)를 얻은 다음 소멸자에서 자동으로 버립니다. 두 가지 일이 발생합니다.
예를 들어, 다른 예는 네트워크 소켓 RAII입니다. 이 경우 :
이제 알 수 있듯이 RAII는 사람들이 배치하는 데 도움이되므로 대부분의 경우 매우 유용한 도구입니다.
스마트 포인터의 C ++ 소스는 저의 응답을 포함하여 인터넷 주위에 수백만입니다.
Boost는 공유 메모리에 대한 Boost.Interprocess 의 것들을 포함하여 많은 것들을 가지고 있습니다 . 특히 같은 데이터 구조를 공유하는 5 개의 프로세스가있을 때와 같이 두통을 유발하는 상황에서 메모리 관리를 크게 간소화합니다. delete
메모리 누수 또는 실수로 두 번 해제되어 전체 힙을 손상시킬 수있는 포인터가 생기지 않도록 메모리 청크를 담당해야합니다 .
무효 foo () { std :: 문자열 바; // // 여기에 더 많은 코드 // }
무슨 일이 있어도 foo () 함수의 범위가 남아 있으면 bar가 올바르게 삭제됩니다.
내부적으로 std :: string 구현은 종종 참조 카운트 포인터를 사용합니다. 따라서 문자열의 복사본 중 하나가 변경된 경우에만 내부 문자열을 복사하면됩니다. 따라서 참조 횟수 스마트 포인터를 사용하면 필요할 때만 무언가를 복사 할 수 있습니다.
또한 내부 참조 카운팅을 통해 내부 문자열의 사본이 더 이상 필요하지 않을 때 메모리가 올바르게 삭제 될 수 있습니다.