개요
c/c++의 스코프와 raii 디자인 패턴에 대해 설명하고, raii 디자인 패턴을 따르는 c++11 표준 구현체인 스마트포인터에 대해서 설명합니다. 스마트 포인터는 스코프와 raii 디자인 패턴을 이용해 자원의 해제를 자동으로 수행합니다.
스코프
스코프(Scope)는 C++에서 변수와 함수의 유효 범위를 정의하는데 사용되는 개념입니다. 스코프를 이해하는 것은 변수의 생존 기간과 접근 가능성을 이해하는 데 도움이 됩니다. 스코프는 아래와 같은 종류가 있습니다.
블록 스코프 (Block Scope)
- 블록 내에서 선언된 변수는 해당 블록의 스코프에서만 유효합니다.
if
,for
같은 선택문, 반복문,try-catch
의catch
: 해당 문의 시작부터 끝까지{ }
로 감싼 코드(복합문-compound statements):{ }
내부- catch에서 사용된 compound statements 는 예외적로 스코프가 아님 -> 일단 레퍼런스엔 이렇게 되어있는데 왜그런지는 더 봐야겠어요…
#include <iostream>
int main() {
// 외부 블록 변수
int externalVar = 20;
{
// 블록 스코프 내에서만 유효한 변수
int blockVar = 30;
std::cout << "Block Variable: " << blockVar << std::endl;
}
// blockVar에 접근 불가 (블록 외부에서는 유효하지 않음)
// std::cout << blockVar << std::endl;
int array[10] = {0,}
for (int i=0; i<10; i++) // for 문 시작
{// compound statements 시작
// i는 compound statements의 블록 스코프의 외부 스코프(for 문 블록 스코프)
// 에서 정의 되었으므로 접근 가능
array[i] = i;
}// compound statements 끝 이면서 for문 끝
return 0;
}
{ }
가 쓰인 모든곳은 catch에서 사용된 경우를 빼면 예외 없이 블록 스코프입니다. 가령 if(){ ... }
블록에서 … 위치에 오는 코드에서 변수를 선언하면 모두 }
를 지나 스코프를 벗어나면 사라집니다.
그런데 for (int i=0; i<10; i++){ ... }
에서의 i는 정확히 어떤 스코프에 속하게 될까요? for문 자체가 스코프가 됩니다. for 문은 for ( ... ) statements
형태로 되어있고, 이 자체로 스코프가 생성됩니다. 그리고 for 문 내부에 있는 { ... }
의 스코프가 또 생깁니다. 따라서 { ... }
내부에서는 i에 접근하면 외부 스코프의 변수를 사용하는게 됩니다.
이하 작성중
지역 스코프 (Local Scope)
- 함수 내에서 선언된 변수와 함수는 해당 함수의 지역 스코프에서만 접근 가능합니다.
- 매개변수 및 함수 내부에서 선언된 변수는 해당 블록 내에서만 유효합니다.
cppCopy code
#include <iostream>
void localFunction(int paramVar) {
// 지역 변수
int localVar = 5;
// localVar은 localFunction의 지역 스코프에서만 유효
std::cout << "Local Variable: " << localVar << std::endl;
}
int main() {
// main 함수의 내부에서는= paramVar과 localVar에 접근 불가 (localFunction의 스코프에서 선언된 변수)
// std::cout << localVar << std::endl;
localFunction();
return 0;
}
전역 스코프 (Global Scope)
전역 스코프는 다른 스코프에 속해있지 않은 영역을 말합니다. 일반적으로 “전역 변수를 선언한다” 라고 말할때 사용되는 그 영역입니다.
- 전역 스코프에서 정의된 변수나 함수는 프로그램 전체에서 접근 가능합니다.
- 전역 변수는 파일 전체에서 사용 가능하며, 다른 파일에서
extern
키워드를 사용하여 접근할 수 있습니다.
cppCopy code
#include <iostream>// 전역 변수
int globalVariable = 10;
// 전역 함수
void globalFunction() {
std::cout << "Global Function" << std::endl;
}
int main() {
// 전역 변수 접근
std::cout << "Global Variable: " << globalVariable << std::endl;
// 전역 함수 호출
globalFunction();
return 0;
}
이렇게 스코프는 변수와 함수의 유효 범위를 제한하고 코드의 가독성 및 유지보수성을 향상시키는데 도움을 줍니다. 함수와 블록을 통해 스코프를 잘 활용하면 변수의 적절한 범위를 유지할 수 있습니다.
스코프와 객체의 메모리 관리는 밀접한 관련이 있습니다. 스코프 내에서 객체가 선언되고 사용되는 경우, 객체의 생존 기간과 메모리 해제가 스코프 규칙에 따라 이루어집니다. 아래는 스코프와 객체 메모리 관리의 관점에서 설명한 예시입니다.
1. 전역 스코프에서 객체 선언 및 메모리 관리:
cppCopy code
#include <iostream>// 전역 스코프에서 객체 선언
class GlobalObject {
public:
GlobalObject() {
std::cout << "Global Object Created" << std::endl;
}
~GlobalObject() {
std::cout << "Global Object Destroyed" << std::endl;
}
};
// 전역 변수로 선언된 객체
GlobalObject globalObj;
int main() {
std::cout << "Inside Main Function" << std::endl;
// main 함수 내에서 전역 객체에 접근
return 0;
}
GlobalObject
는 전역 스코프에서 선언되었으며, 프로그램이 시작할 때 생성되고 종료할 때 파괴됩니다.- 객체의 생성 및 소멸 메시지가 프로그램 실행 전후에 출력됩니다.
2. 지역 스코프에서 객체 선언 및 메모리 관리:
cppCopy code
#include <iostream>void localFunction() {
// 지역 스코프에서 객체 선언
class LocalObject {
public:
LocalObject() {
std::cout << "Local Object Created" << std::endl;
}
~LocalObject() {
std::cout << "Local Object Destroyed" << std::endl;
}
};
// 지역 변수로 선언된 객체
LocalObject localObject;
}
int main() {
std::cout << "Inside Main Function" << std::endl;
localFunction();
// main 함수 내에서는 localFunction의 지역 객체에 접근 불가
return 0;
}
LocalObject
는localFunction
의 지역 스코프에서 선언되었으며, 함수가 호출될 때 생성되고 함수 실행이 끝날 때 파괴됩니다.localObject
의 생존 기간은localFunction
의 실행 범위에 제한되어 있습니다.
3. 블록 스코프에서 객체 선언 및 메모리 관리:
cppCopy code
#include <iostream>int main() {
std::cout << "Inside Main Function" << std::endl;
{
// 블록 스코프에서 객체 선언
class BlockObject {
public:
BlockObject() {
std::cout << "Block Object Created" << std::endl;
}
~BlockObject() {
std::cout << "Block Object Destroyed" << std::endl;
}
};
// 블록 내에서 선언된 객체
BlockObject blockObject;
}
// 블록 외부에서는 blockObject에 접근 불가
return 0;
}
BlockObject
는 블록 스코프에서 선언되었으며, 블록이 실행될 때 생성되고 블록이 끝날 때 파괴됩니다.blockObject
의 생존 기간은 블록의 실행 범위에 제한되어 있습니다.
스코프가 객체의 메모리 관리와 어떻게 연관되는지 이해함으로써, 변수나 객체의 생존 기간을 효과적으로 관리하고 메모리 누수를 방지할 수 있습니다. C++에서는 객체의 생성자와 소멸자를 활용하여 객체의 생명주기를 제어하므로, 스코프에 따른 메모리 관리에 유의해야 합니다.
포인터의 문제점
메모리 누수
포인터를 수동으로 할당하고 해제 하는 것을 잊어버리면, 메모리 누수가 발생
void memoryLeakExample() {
int* ptr = new int; // 메모리 동적 할당
// 포인터를 해제하지 않음
// delete ptr; 를 추가하지 않으면 메모리 누수가 발생
}
댕글링 포인터
댕글링 포인터는 이미 해제된 메모리를 참조하는 포인터로, 예측할 수 없는 동작을 유발할 수 있음 - segmentation fault, 보안위협 등
int* danglingPointerExample() {
int* ptr = new int;
delete ptr;
return ptr; // 댕글링 포인터 반환
}
다중 소유 문제
여러 포인터가 같은 메모리를 가리키고 있을 때, 메모리를 해제하는 순서에 따라 다양한 문제가 발생할 수 있습니다. - 이중 free 등
void multipleOwnershipIssue() {
int* ptr1 = new int;
int* ptr2 = ptr1; // ptr2가 ptr1을 가리킴
delete ptr1;
// 이중 free
delete ptr2;
}
RAII 디자인 패턴
RAII 디자인 패턴은 자원(메모리)의 획득과 해제를 객체의 생성과 소멸에 결합하는 디자인 패턴입니다. c++에서는 객체의 생성자와 소멸자를 정의 할 수 있으며, 객체가 스코프를 벗어나면 소멸자가 실행되는 점을 이용해서 구현하게 됩니다.
RAII 디자인 패턴을 따르는 스마트 포인터
스마트 포인터는 c++ 에서 RAII 디자인 패턴의 구현제라고 볼수 있습니다.
다음과 같은 종류가 있습니다.
shared_ptr
- 참조 횟수 기반shared_ptr
는 참조 횟수 기반 스마트 포인터입니다. 각각 인스턴스별로 참조 횟수를 카운팅 하며, 인스턴스를 참조하는 객체가 사라지면 참조 횟수가 감소합니다. 0이 되면 해당 인스턴스의 메모리를 해제합니다.
unique_ptr
- 소유권 기반unique_ptr
는 소유권 기반 스마트 포인터 입니다. 특정 객체에 유일한 소유권을 지정하고, 해당 객체를 벗어나 다른 객체에 “=” 연산자 등으로 복사 하는 행위를 금지 합니다. 이를 통해 해당 객체 외에는 delete를 수행하지 못하게 만듭니다. 스코프를 벗어나면 자동으로 메모리를 해제합니다.