C ++ 이론(3)

Hi Beck·2023년 7월 3일

TIL-Language(C & C++)

목록 보기
29/34

#소멸자와 복사생성자

1.지난시간 복습

  • 동적할당 테크닉을 사용하지않으면 메모리낭비 및 프로그램 난이도(관련없는 빈공간을 다 계산하니깐)가 올라간다.
    -> 동적할당을 쓰면 변수만큼의 배열을 받아와서 쓸 수 있다.

  • 동적할당문법에 int 아니여도 다양한 자료형을 쓸 수 있다.즉 이말은 우리가 사용하고있는 클래스도 자료형이기 때문에 클래스로도 동적할당이 가능하다.

ex)
Myclass * ptr = new Myclass;
//클래스변수 하나일때

Myclass * ptr = new Myclass[num];
//클래스변수가 대량 즉 배열일때(이때 반드시 크기 넣어주기)

delete ptr;
//클래스변수 하나일때
delete[ ] ptr;
//클래스변수가 배열일때(이때 대괄호에는 크기 안넣어줘도됨)

  • (중요)생성자는 언제 쓰이냐?
    객체에서 필드와 메서드를 초기화할때 사용된다

이게 왜 초기화를 할 때 사용하냐?(생성자특징)

클래스변수를 선언할때 생성자가 자동으로 초기화된다.(x)
-> 용어를 똑바로 말하자

생성자는 클래스 객체를 생성할 때 자동으로 생성(정의)될 수도 있고 자동으로 불릴(호출) 수도 있다.(○)

클래스 내부에 생성자가 정의되어있지않은 경우는 컴파일러가 자동으로 생성자를 생성(정의)해준다.

2.본 수업 복습(소멸자)

소멸자 개념
객체가 소멸될 때 자동으로 실행되는 클래스의 멤버 함수다.
소멸자는 객체의 청소를 돕도록 설계되었다.

소멸자의 역할
이 객체가 안전하게 소멸할 수 있도록 객체의 필드나 메서드들을 정리해주는 역할을 한다.
(소멸자도 생성자처럼 자동으로 불린다.)

  • 객체는 사라질때 자기가 가지고있는것들을 비워내는 작업을 반드시 해야된다!

소멸자의 구조

  • 기본 생성자가 아래와 같이 생겼다면
    MYclass() {}
    기본 소멸자는 여기에 물결표시가 붙는다.(delete는 동적할당반납 문법이니 헷갈리지말자)
    ~MYclass() {}

  • 소멸자의 이름은 클래스 이름과 같아야한다.

  • 소멸자는 인수가 없다.

  • 소멸자는 반환값이 없다.

RAII
RAII은 객체의 수명과 리소스 관리와 연관있는 프로그래밍 디자인 기법이다. C++에서 RAII는 클래스의 생성자와 소멸자로 구현되었다.

= 리소스를 획득한다는건 초기화인데 이 말은 객체의 생성과소멸로 리소스 관리를 한다와 같다.(프로그래밍 스킬을 C++이 문법으로 만들어놓은 것) 자동이 아니다보니 프로그래머가 관리하는게 중요하다.

  • 리소스(메모리,파일,동적 할당 또는 데이터베이스 핸들 등)는 일반적으로 객체의 생성자에서 획득한다.
  • 그러면 그 리소스는 객체가 살아있는 동안 사용될 수 있다.
  • 객체가 소멸하면 자원이 소멸된다.

RAII의 장점은 리소스를 보유한 객체가 자동으로 정리됨에 따라 리소스 누수를 방지하는데 도움이된다.

생성자와의 차이

ㄱ. 갯수

소멸자는 생성자처럼 여러개 있을 수 없다.단 한 개만 존재하는이유는 소멸할때는 인자가 아예 필요없다.

ㄴ. 소멸되는 시점

보통 객체(변수)의 생명주기에 달려있어서 소멸될때인데(예로 들어 메인함수에서 객체 선언을 하는데 여기서 return 코드 밑줄부터 괄호까지는 사라지고 있는 단계다.이건 더이상 필드나 메서드에 접근할 수 없다는 것과 같다.그리고 괄호가 닫힐땐 완전히 끝난것.

근데 소멸자는 소멸된 직후에 불리는 애가 아니라, 소멸될 때 불리는 애다.

생성자는 변수들을 초기화하는거니깐 그전에 먼저 생성부터 시켜줘서 생성 직후에 생성되는거고 소멸자는 그럴 필요가 없어서 소멸 동시에 불리는 것임.

근데 정확히 말하자면 소멸자는 자기가 가지고 있는것들을 정리하는애인데 만약 자기가 정리할게 없다면 소멸자는 일을 못한다.그래서 소멸자는 객체가 삭제되기 직전에 불린다.

int main(){
Myclass a;

return 0
a;
}

이렇게 return이 끝나도 밑에 변수가 살아있을때 소멸자가 불린다.이때 a는 괄호를 나가고나서 끝났다할때 소멸자가 불리고 a가 사라지는것 즉,괄호를 벗어난 시점에 변수도 사라지고 소멸자도 불리는것이다. 그래서 시점이 소멸직전이라는것임.

생성자와 소멸자는 결국 내 멤버변수와 멤버함수를 정리하기위한 함수들이다.

  • 클래스 내부에서 생성된 포인터변수의 생명주기는 어떻게 될까?
    -> 클래스 내부 멤버변수들은 메인함수에서 생성되는 객체의 생명주기를 따라간다.
    왜냐면 클래스의 생명주기 즉 괄호안에서이지않을까 생각하겠지만 클래스의 생명주기는 객체가 생성되었을때 생성돼서 객체가 소멸될때까지 살아있다가 객체가 소멸될 때 같이 소멸됨

  • 소멸자가 존재하는 이유 (무엇을 정리하느냐)
    클래스 내부에서 동적할당 문법을 썼다고 가정하고 (int * ptr = new int) new는 힙공간에 메모리를 잡고 ptr은 주소값만 가지고있다.
    이름없는 int형 변수가 있고 그 주소값을 ptr이 지니고있고 이름없는 변수를 ptr이 가리킨다.근데 이 ptr이 속한 클래스가 객체따라 소멸돼서 ptr변수도 소멸됐다고 가정해보자.그러면 이름없는 int형 변수는 어떻게 되는가?

-> 반납을 하지않았기때문에 그 자리에 계속있다.(동적할당으로 빌려온 메모리는 반드시 반납해줘야되니깐) 이렇게 힙공간에 방치하면 메모리도 낭비고 이름은 없고 주소값이 있다보니 그 주소값을 가지고 있어서 유일하게 컨트롤이 가능했던 ptr이 사라지니 더이상 접근할 방법이 없다.(단,프로그램 종료시 운영체제가 정리해주긴한다.근데 프로그램은 하루쓰는게 아니다.)

생성자에서 초기화할 때 동적할당 즉 메모리를 힙영역에 빌렸으면 반드시 소멸자로 반납해야된다!(동적할당 안쓰고 일반함수로도 반납만 잘하면 상관없다.)

  • 주의할 점
    동적할당할때 기본생성자와 기본소멸자는 객체가 있는 이상 무조건 불린다.근데 일반함수는 정의는 해도 빌려놓고 불릴 수도 안불릴 수도 있다.그렇게되면 꼬이고 까먹는다.
    -> 안전빵으로 메모리리크(일반 프로그램에선 어마어마함)가 안일어나게끔 DELETE를 꼭 해주자.
    -> 클래스 내부에서 동적할당할때는 무조건 포인터변수가 쓰이는데 클래스의 멤버변수에서 포인터변수가 나오면 긴장하고 꼭 동적할당 받은 메모리를 가리키는 포인터변수가 아닌지 생각하고 꼭 반납까지 해야된다!!!

  • 포인터변수를 보면 체크해줘야할 세 가지
    ㄱ. null로 초기화가 되어있는지
    ㄴ. 동적할당받은 주소를 가리키는 포인터변수인지 반납은 되어있는지
    ㄷ. 복사생성자 정의문을 써서 딜리트 중복이 안일어나지않게했는지

  • 레퍼런스 변수와 CONST 변수는 원본 자체여서 애초에 동적할당과 상관없기에 생성 소멸과 관련없다.

  • NULLPTR은 NULL과 같다고보면된다.

  • 클래스 정의문에서 순차적으로 다 돌아가는게 아니라 그 안에 있는 멤버변수와 멤버함수 중에 메인함수에 호출된 애들만 생각하면되고 외의 순서에 해당하는 단순 변수선언문은 리턴값에 영향을 주지않는다.호출문의 인자가 들어가는 함수만 보면된다.

나의 실수 -> 긴장해서그런진 모르겠지만 선언문인데 호출문이나 값이 변경되는 코드로 보고그럼.... 변수선언문은 변수 선언이지 = 때문에 대입해야겠다는 이런 생각은 하지말자.

3.본 수업 복습(복사생성자) ---- 자동으로 생성되는 애 3번째

예로 들어 main 함수에 아래와 같이 객체 즉,클래스 변수가 존재하는데

MYdata a(10);
MYdata b(a);

  • a는 지역변수다.지역변수는 스택에 아래에서 위로 쌓인다.그렇다면 변수a의 메모리가 먼저쌓이고 그 다음에 b가 쌓인다.여기에서 아래있는 a를 뺄려면 b부터 빼야된다.
    그렇다는말은 소멸될 때 b가 먼저 소멸된다는말이다.==생성과 소멸은 역순

a가 인수 10을 넘기면서 객체 안의 임의의 멤버변수를 거치고 메인함수에 있는 함수를 통해 뭔가 유의미한 과정을 거친다.

반면 b의 형태는 어떻게 보고 b는 어떻게 생성됐냐?
b는 인자로 int형인 아닌 객체 a(클래스 변수)를 줬다.그렇다면 클래스에선 이 인자에 맞는 생성자가 불려야된다. -> 객체가 인자인 생성자 함수 -> 근데 만약에 클래스내 이런 생성자 함수가 없다면? -> 이럴때 자동으로 복사생성자라는게 생성된다.

복사생성자 == 객체를 가지고 내 객체를 생성하고 싶을때 사용되는 생성자

객체를 넣어서 객체를 초기화한다 == 넣은 객체에 있는것을 넣을 그릇 객체에 그대로 복사하고싶다와 같다.(==a안의것을 b에 그대로 복사해서 넣겠다.)

객체 a를 그대로 객체 b와 똑같이 셋팅하는걸로도 볼 수 있는데 현제 a는 MYdata에서 동적할당을 받아 포인터변수가 이름없는 변수의 주소값을 가리키고있고 그 주소값 안에는 인수로 받은 10이 있다.

객체 b에도 a가 가지고있는것들을 복사해주면 포인터변수가 들어가고 얘도 자동으로 동일한 주소값 안에 동일한 값인 10을 가리킬 것이다.

== 내가 객체를 생성하는데 다른 객체를 가지고 초기화하고싶을때 복사생성자가 불린다.
== 객체를 저 객체처럼 쓰고싶어서 저 객체랑 똑같이 초기화할때 복사생성자가 불린다.
== 내가 객체를 생성하는데 다른 객체를 가지고 생성하고싶을때 복사생성자가 불린다.
== MYdata b(a);의 코드일때 복사생성자가 불린다.
== MYdata b = a;의 코드일때 복사생성자가 불린다.
== 다른객체에 있던 값들로 객체 b를 채워준다.
== int형 생성자처럼 객체를 받고싶을때 불리는 생성자가 복사생성자

  • 복사생성자도 정의가 안되어있으면 컴파일러가 자동으로 생성해서 만들어준다.

복사생성자 특징

기본생성자는 아무 생성자없이 정의가 안되어있을시에만 생기고 한개라도 정의가 되어있다면 안생긴다.(인수가 다른게 있다면 그것때문에 안생김)
반면에 복사생성자는 정의가 안되어있으면 무조건 자동으로 생성시킨다.
(인수가 다른것들 다 자동으로 정의시켜줌)

기본생성자와 복사생성자가 없으면 자동으로 불려진다.결국은 용도,상황별로 코드에 따라 자동으로 생성자들이 불려지는것(사용된다x.자동으로 해주는거니깐)+생성자 중 형태에 맞는것 하나가 호출이된다.

출력문에 적용시
위의 예시에서 객체 a와 b 둘다 동적할당받은 메모리의 주소값을 가리키고 있다.그래서 메인함수에서 이 2개를 가지고 어떤 값을 출력하면 둘다 똑같이 가지고 있는 10이 출력된다.

복사생성자가 불려서 만들어진 객체의 소멸
b가 먼저 소멸되는데 얘가 소멸되기 직전에 소멸자가 불리는데 소멸자는 동적 할당한 메모리를 해체한다.즉,포인터변수가 가리키던애를 없애준다는것이다.그리고 그제서야 b가 소멸된다.이제 남은 a도 소멸되기 직전에 소멸자가 불리는데 코드 상 delete * ptr처럼 주소값에 있는 메모리를 또 해체할려는데 b가 소멸되면서 가리키는 애가 이미 사라져 이미 없는 공간에 delete를 하면 크래시가 일어나게된다.

크래시가 일어난 이유는
a 소멸자 한 번 b 소멸자 한 번 일대일 대응했기에 이건 이유가 아니고
동적할당 메모리를 두 번 삭제한게 문제인데 이건 a랑 b랑 똑같은 곳을 주소값이 같으니깐 가리키기 때문인것.주소값이 같은 이유는 복사생성자때문

-> 그렇다면 동적할당 받은 메모리가 다른 메모리라면 괜찮지않을까?
복사생성자의 원래 의도는 객체 하나 생성하고 동적할당 하나 생성하고 다른 객체 하나 생성하고 다른 동적할당 하나 생성하고이다.

기본 생성자는 이런 의도로 객체마다 동적할당을 선언하는데 클래스 멤버함수 내에서 복사생성자가 자동으로 생겨나서 new함수를 안하고 객체의 값복사해주면서 이런일이 생긴것

Q)소멸할때 두 객체가 똑같은 동적할당 메모리를 가리켜서 크래시가 일어나는데 이것을 해결하는 방법?

A) ( 꼭 밑에 보고오기)복사생성자가 자동으로 생성되니 포인터변수가 그대로 복사돼서 문제가 일어난건데 해결방법은 자동으로 정의되게하지말고 내가 직접 정의해야겠다. delete 두 번 안하도록 내가 직접 가져와서 하면된다.==b가 따로 동적할당한다는 소리다. 그러면 delete할 때 서로 다르게 가리키는 메모리를 지울 수 있음.

여기서 주의할건
어차피 복사생성자여서 복사는 어쩔 수 없이 해야되는데 그러면 각 복사를 하고 그 안에 있는 값만 옯기자는식이다.무슨말이냐면 두 객체가 값도 다른걸 가리키자가 아니라 같은 상자 안을 가리키고 있었으면 서로 다른 상자(서로 메모리의 주소값은 다름)를 가리키고 기존 상자 안에 있던 값만 복사하자는 소리다.

그래서 이렇게되게끔 우리가 직접 정의할 때는 다음과 같이 정의한다.(무조건 이렇게)
무조건 암기하기!

MYdata(const Mydata &rhs)
{
//메모리 할당
m_pnData = new int;

//포인터가 가리키는 위치에 값을 복사

  • m_pnData = * rhs.m_pnData
    }

클래스 내부에서 동적할당을 한다면 무조건 확인하고 에러가 나도 이걸 한번 확인해보자
프로그램을 정상종료를 안하는 경우가 많아 이런 경우는 에러가 발생하는지 몰라서 포인터변수가 존재하면 이런 동적할당 이슈에 신경쓰자.
첨부터 정의하면되지않냐하지만 동적할당을 받은 멤버변수를 단순 보관용으로 쓸 수 있다.
(주소값을 들고만있는)근데 이걸 함부로 해지했다가 또 중복돼서 크래시가 일어날 수 있다.포인터변수가 있다고해서 동적할당 할지 안할지 모르는건데 있으면 할 가능성이 높은것 그러면 이 이슈가 발생할 확률이 높다.써놔도 상관은 없다.(방지차)

복사생성자의 정의문 (이 코드 그대로 자동 생성된다.)

MYclass(Const MYclass& rhs)
{
ptr = rhs.ptr; //b이 포인터 변수에 a의 포인터변수 주소값을 넣어라와 같음
}

인자로 MYclass 즉 래퍼런스 변수를 받는다.(const 상수형 뜻은 무시하기)
rhs == 래퍼런스
즉, 래퍼런스형 객체를 인자로 받는다.
-> 내 객체의 포인터 변수 ptr을 가져온 객체 안의 래퍼런스 변수 안에 있는 포인터 변수 ptr의 값으로 바꿔준다.(복사) 다른객체지만 클래스 자료형은 같기에 포인터변수는 가능

(구조체에서 배웠던 개념 : . 찍으면 구조체 혹은 클래스 안의 멤버변수에 접근할 수 있다.)

  • 기본생성자 안에 동적할당이 정의되어있을때, 복사생성자 안 포인터변수는 힙영역에서 빌린 메모리를 가리키는거 아닐까 생각하지만 복사생성자 정의문이 있는 시점에는 생성자는 객체에 하나만 불릴 수 있으니 기본생성자의 동적할당은 불리지않은것이다.
    그저 아무것도 들어있지않은 ptr에 새로 불려온 객체(레퍼런스 변수,a)의 포인터변수가 들고있는 주소값을 준것

  • 레퍼런스 변수를 인자로 받는건 알겠는데 레퍼런스 변수 선언문은 어딨나?
    -> Const MYclass& rhs는 함수의 인자고 이건 정의만 일단 되어있는데, 선언문은 이 인자의 출처에 있다.MYclass 함수가 불릴때 Const MYclass& rhs가 초기화됨

-> 메인함수에서 MYclass b(a); 이렇게 선언할 때 a라는 인자가 Const MYclass& rhs에 들어가면서 초기화된다.즉,레퍼런스 변수는 곧 a다.

Q)생성자는 각 상황마다 불리는데 복사생성자가 생기면 이미 같은 클래스 내부의 멤버변수들과 멤버함수들이 같은걸로 초기화되었으니깐 기본 생성자는 안생기는건지?

A)복사생성자가 실행될때 애초에 기본 생성자가 아예 상관이 없다.
-> 생성자는 여러개 정의될 수 있지만 상황에 따라 호출되는건 하나다.

-> 또한 동적할당받을때도 생성자별로 내부에서 받는데( 생성자라면 무엇이든 동적할당 받을 수 있음 ) 동적할당을 어느 생성자에 했든 그 동적할당받은 생성자가 호출되어야지만 할당이 되는것이지 호출되지도않았는데 임의의 생성자 안에 정의되어있다고 신경 쓸 필요가x.
복사생성자를 쓸 때 안에 동적할당 문법이 있으면 즉 복사해서 객체를 생성하고자할때는 그 안에서 동적할당하겠다라는 뜻과 같다.

결론 : 복사해서 생성하고싶을때, 객체를 인자로 줬을때 복사생성자가 불리고 없어도 자동으로 생성된다.

profile
Emotional realizer

0개의 댓글