C++ 2주차 캠프 강의 예습.1

정혜창·2024년 12월 29일
1

내일배움캠프

목록 보기
12/43

2-1 자원 관리하기

1.스택, 힙 메모리

일반 변수들은 대부분 스택 메모리 공간을 차지한다.
변수는 선언된 중괄호안에서 생존하게 된다. 괄호가 시작되면서 변수 선언을 하면 메모리에 할당되고, 괄호가 끝나는 순간 저절로 회수된다.

예제

#include <iostream>
using namespace std;

void func1() {
    int a = 10;  // 지역 변수 'a', stack 메모리에서 관리됨
    cout << "func1: a = " << a << endl;
}  // func1()이 종료되면 'a'는 소멸됨

int main() {
    func1();  // func1() 호출
    // 'a'는 func1() 호출 중에만 존재하고, 함수 종료 후 소멸됨
    return 0;
}

그러나 static 을 붙이면 변수는 사라지지 않는다. 초기화도 최초 한번 실행되고 더이상 초기화가 되지 않는다.

void func() {
	static int a = 10;
    a = a + 1;
    cout << "func1 : a = " << a << endl;
}

int main() {
	func()	// func1 : a = 11
    func()	// func1 : a = 12
    func()	// func1 : a = 13
    func()	// func1 : a = 14
    func()	// func1 : a = 15
    
    return 0;
}

내용 ++

static 키워드는 블록 내에서 선언된 지역 변수에도 사용할 수 있다. 지역 변수는 '자동 주기(auto duration)' 를 가지며, 정의되는 시점에서 생성되고 초기화되며, 정의된 블록이 끝나는 지점에서 소멸한다. static 키워드를 사용한 지역변수는 완전히 다른 의미가 있다. static 키워드를 사용한 지역 변수는 '자동주기(auto duration)' 에서 '정적주기(static duration)'로 바뀐다. 이것을 정적 변수라고도 부르는데, 생성된 스코프(생존 지역)가 종료한 이후에도 해당 값을 유지하는 변수다. 또한 정적 변수는 한 번만 초기화 되며 프로그램 수명 내내 지속된다.

참고 자료 : https://boycoding.tistory.com/169 소년코딩 (C++ 04.05 - static, 정적 변수)


참고로 전역변수와 정적변수는 값을 초기화했으면 Data 영역에 생성되고, 초기화하지 않았으면 BSS 영역에 생성되고 0이 들어간다.(OS 커널에 의해)

메모리에 관한 플러스 설명은 이 포스팅을 참조 : https://code4human.tistory.com/129

스택은 메모리 영역 자체가 크지 않다. 또한 스택에 저장된 메모리는 생존 영역을 벗어나면 자동으로 해지된다.
이를 해결할 수 있는 방법이 힙 메모리를 활용하는 것이다.

    1. 선언 시 new연산자를 사용한다. 해제 시 delete 연산자를 사용한다.
    1. 스택 처럼 자동으로 해지되지 않기 때문에 따로 사용자가 해제해줘야 한다.(리스크가 존재)
    1. 생존주기는 사용자가 선언한 순간부터 해지하기 전까지이다.

간단한 예제

  • 변수 하나의 영역에 대한 동적할당
    • int* ptr = new int(10); : int형 하나를 담을 수 있는 공간을 힙 메모리에 만들고 그 값에 10 넣겠다. 그리고 힙메모리가 할당된 위치(주소)를 ptr에 담아라. (*ptr = 10)
    • 힙메모리에 할당하게 되면 반드시 해제해줘야 한다. delete ptr;
  • 배열의 동적할당과 해제
    • int* arr = new int[5]; : 'int형 변수 다섯 개를 담을 공간(배열)을 힙 메모리에 만들고 그 힙메모리가 할당된 배열의 시작위치(주소)를 arr에 담아라.
    • arr[i] 배열 처럼 인덱스를 붙여서 할당된 배열에 접근 가능하다.
    • 마찬가지로 해제해줘야 한다. delete[] arr;

해제 하지 않을 경우의 예시

#include <iostream>
using namespace std;

int main() {
    int* ptr = new int(10); // 동적 할당
    cout << "동적 할당 값: " << *ptr << endl;

    // 여기서 ptr을 다시 선언하고 값 할당
    ptr = new int(20); // 새로운 메모리 할당 (이전 메모리 참조 손실)

    cout << "새로운 동적 할당 값: " << *ptr << endl;

    // 기존 메모리에 대한 참조가 끊어졌기 때문에 메모리 해제가 되지 않음 (메모리 누수 발생)
    
    delete ptr; // 현재 ptr이 가리키는 메모리 해제
    return 0;
}

이 예제에서 보면 새로운 메모리는 해제했으나 기존 메모리는 해제하지 않는다. 따라서 새로운 값을 할당하거나 더이상 쓰지 않으면 반드시 해제를 하는 습관을 들이는게 중요하다.

동적 배열의 예시

#include <iostream>
using namespace std;

void CreatDynamicArray() {
	int size;
    cout << "원하는 배열의 크기를 입력하세요 : ";
	cin >> size; // 런타임 중에 배열 크기 결정
    
    if(size > 0) {
    	int* arr = new int[size];   // 동적 배열 할당
    	for(int i = 0; i < size; i++){
    		arr[i] = 2*i;			// 동적 배열 원소 초기화
        	cout << arr[i] << " ";
        }
        delete[] arr; 				// 메모리 해제
    }
    else { 
    	cout << "invalid size" << endl;
    }
}

int main() {
	CreatDynamicArray();
    return 0;
}

2. Dangling Pointer

예를 들어 두 포인터(ptr1, ptr2)가 힙 메모리의 같은 곳을 참조하고 있다고 생각을 하자. 아마도 동적 변수나 동적 배열을 참조할 것이다. 따라서 ptr1이 이제 해당 메모리를 쓰는 경우가 없어서 해제를 하면delete ptr;힙 메모리에 있는 값은 날라간다. 그러나 ptr2 은 여전히 해당 메모리의 참조가 필요한데 해제가 되었을 수도 있다. 이럴 때 ptr2Dangling Pointer라고 한다.

실생활로 잠깐 예시를 들자면 a와 b가 사무실에서 같은 쓰레기통을 쓰고 있는데, a가 이제 쓰레기통이 필요없다고 버리면 b는 쓰레기통을 쓰고 싶어도 못쓰게 된다. 이럴때 b가 Dangling Pointer 이다.

간단한 예시

#include <iostream>
using namespace std;

void func5() {
    int* ptr = new int(40);  // 힙 메모리에 정수 40 할당
    int* ptr2 = ptr;

    cout << "ptr adress = " << ptr << endl;
    cout << "ptr2 adress = " << ptr2 << endl;
    cout << *ptr << endl;

    delete ptr;
    // delete ptr; 을 한번 더 적게 되면 실행시 문제가 생기게 된다. 이중 해지도 조심!

    cout << *ptr2 << endl;
}

int main() {
    func5();
    return 0;
}

3. 스마트 포인터

이렇게 힙에 저장되는 데이터들은 장점도 있지만 메모리관리가 필수이므로 위험성도 크다. C++에서 이런것을 보완하기 위해 나온 것이 스마트 포인터이다. 우선 스마트 포인터의 핵심 원리는 레퍼런스 카운터 이다. 직역하면 '참조 반격?' 이라고 생각할 수 있지만 숫자를 세는 count 뜻을 받아 '참조한 숫자' 라는 뜻이다.

위의 그림 처럼 참조한 곳이 두 군데면 레퍼런스 카운터는 2이다. 하나를 해지하더라도 참조한 값은 남아있게 되며, 모두 해제되어 레퍼런스 카운트가 0이되면 알아서 자동으로 힙영역의 메모리 공간을 해제한다.

3-1 unique_ptr :

  • 레퍼런스 카운터가 최대 1인 스마트 포인터. 소유권을(특정영역을 참조하는 것을 소유권이라고도 한다.)을 다른 포인터에게 양도하는 것은 가능하나, 동시에 두 개의 포인터가 특정영역을 소유할 수는 없다.
  • 최대 레퍼런스 카운터가 1이기 때문에 복사 or 대입이 되지 않는다. 이걸 시도하는 순간 컴파일 에러가 발생한다.
  • 이 포인터를 사용하기 위해서는 <memory>라는 헤더파일을 추가해야 한다.
  • 사용법 예제
#include <iostream>
#include <memory> // unique_ptr 사용
using namespace std;

int main() {
    // unique_ptr 생성
    unique_ptr<int> ptr1 = make_unique<int>(10); /* unique_ptr/ make_unique 
    												하나의 짝으로 보면된다. 레퍼런스 카운터1 */
        // unique_ptr이 관리하는 값 출력
    cout << "ptr1의 값: " << *ptr1 << endl;

    // unique_ptr은 복사가 불가능
    // unique_ptr<int> ptr2 = ptr1; // 컴파일 에러 발생!

    // 범위를 벗어나면 메모리 자동 해제
    return 0;
}
  • move를 이용한 unique_ptr 소유권 이전
    unique_ptr<int> ptr2 = move(ptr1) : ptr1의 소유권이 ptr2로 이동
  • if (!ptr1) {~} : ptr1에 아무것도 담고 있지 않으면 실행문 실행~
  • 실전 예제
#include <iostream>
#include <memory>
using namespace std;

class MyClass {
public:
    MyClass(int val) : value(val) {
        cout << "MyClass 생성: " << value << endl;
    }
    ~MyClass() {
        cout << "MyClass 소멸: " << value << endl;
    }
    void display() const {
        cout << "값: " << value << endl;
    }
private:
    int value;
};

int main() {
    // unique_ptr로 MyClass 객체 관리
    unique_ptr<MyClass> myObject = make_unique<MyClass>(42);

    // MyClass 멤버 함수 호출
    myObject->display();

    // 소유권 이동
    unique_ptr<MyClass> newOwner = move(myObject);

    if (!myObject) {
        cout << "myObject는 이제 비어 있습니다." << endl;
    }
    newOwner->display();

    // 범위를 벗어나면 newOwner가 관리하는 메모리 자동 해제
    return 0;
}

3-2 shared_ptr

  • shared_ptr은 레퍼런스 카운터가 N개가 될수 있는 스마트 포인터이다.
  • 레퍼런스 카운터 갯수를 볼 수 있는use_count() 와 현재 포인터를 초기화 할 수 있는 reset()을 제공한다.
    shared_ptr, make_shared 공간을 만들 때 하나의 짝이라고 보면된다. unique_ptr, make_unique와 같음
  • 사용 예제
#include <iostream>
#include <memory>
using namespace std;

class MyClass {
public:
    MyClass(int val) : value(val) {
        cout << "MyClass 생성: " << value << endl; // 출력: MyClass 생성: 42
    }
    ~MyClass() {
        cout << "MyClass 소멸: " << value << endl; // 출력: MyClass 소멸: 42
    }
    void display() const {
        cout << "값: " << value << endl; // 출력: 값: 42
    }
private:
    int value;
};

int main() {
    // shared_ptr로 MyClass 객체 관리
    shared_ptr<MyClass> obj1 = make_shared<MyClass>(42);

    // 참조 공유
    shared_ptr<MyClass> obj2 = obj1;

    cout << "obj1과 obj2의 참조 카운트: " << obj1.use_count() << endl; // 출력: 2

    obj2->display(); // 출력: 값: 42

    // obj2를 해제해도 obj1이 객체를 유지
    obj2.reset();
    cout << "obj2 해제 후 obj1의 참조 카운트: " << obj1.use_count() << endl; // 출력: 1

    return 0;
}

4. 얕은 복사와 깊은 복사

4-1 얕은 복사

얕은 복사(Shallow Copy)는 대입 연산자를 활용해서 두개의 포인터가 같은 위치를 공유하는 것을 말한다. (dangling pointer가 발생할 수 있다.)

int* A = new int(30);
int* B = A; // 이런 경우가 얕은 복사 : 실제 값이 복사가 되는게 아닌 위치를 공유

4-2 깊은 복사

깊은 복사(Depp Copy)란 독립된 메모리 영역을 할당해서 위치가 아닌 가리키는 내용(값)이 복사되는 것을 말한다. 독립된 영역

int* A = new int(30);
int* B = new int(*A); // 이런 경우가 깊은 복사 : new를 "'힙(heap)'메모리에 공간을 할당하겠다." 라고 생각하면 된다.

profile
Unreal 1기

0개의 댓글

관련 채용 정보