C++

[강의] 10월 24일 수업정리

k-codestudy 2024. 10. 25. 04:35

class와 그게 관한 문제를 풀어보았다.

 

1.기본 생성자와 유니폼 초기화

 

 [ data.h ]  

#pragma once

class C_DATA
{
private:
	int m_nData;
public:
	C_DATA() = default;
	void print();
};

 

 [ data.cpp ]

#include <stdio.h>
#include "data.h"


void C_DATA::print()
{
    printf("%d\n", m_nData);
}

 

[ test header.cpp ] 

#include <iostream>
#include "data.h"

int main()
{
	C_DATA c1{};
	C_DATA c2;

	c1.print();
	c2.print();
}

 

1.1 차이점 

  • C_DATA c1{};
    • 유니폼 초기화 "{}"느 객체의 맴버 변수를 0으로 초기화를 한다. 따라서 c1 m_nData는 0으로 설정되어 printf()를 호출하면 0이 출력된다.
  • C_DATA c2;
    • 기본 초기화는 객체의 맴버 변수를 초기화하지 않는다. 이로 인해 c2의 m_nData는 초기화되지 않은 값을 가지게 되며, 이는 메모리상의 기존 데이터를 그대로 포함할 수 있다. 이 경우 - 858993460 같은 예상치 못한 값이 출력된다. 

이러한 차이점을 내는 이유는 C_DATA() = default; 가 선언되어있기 떄문이다.

이 코드는 기본 생성자를 명시적으로 사용하겠다는 의미이다. 기본 생성자는 맴버 변수를 초기화하지 않지만, 유니폼 초기화를 사용하면 변수들이 자동으로 0으로 초기화가 된다는 것이다.

 

1.2 기본 생성자를 명시적으로 선언해야 할까?

  • 유니폼 초기화
    • 유니폼 초기화({})는 기본 생성자와 함께 사용할 때, 기본적으로 변수를 0으로 설정하여 초기화되지 않은 변수가 남지 않도록 도와주는 역활이다. 이는 초기화되지 않은 변수로 인해 발생할 수 있는 오류를 예방할수 있다
  • 생성자가 필요한 이유
    • 생성자를 정의하게 되면, 컴파일러가 자동으로 제공하는 기본 생성자는 사라지는게 원칙이기에, 이 경우 명시적으로 생성자를 정의해야 객체를 올바르게 초기화할 수 있다.
      예를 들어, 복사 생성자를 처리하거나 특정 멤버 변수를 초기화해야 할 때 생성자를 사용하게 된다.
  • 유니폼 초기화의 장점
    • 변수 초기화 시 ()를 사용하면 함수 선언으로 해석될 수 있기 때문에, 이러한 모호성을 피하고 안정적으로 초기화를 수행하기 위해 {}를 사용하는 유니폼 초기화가 도입되었다. 이를 통해 초기화되지 않은 변수를 줄일 수 있습니다.

이렇게 기본생성자를 만들때는 명시적으로 기본 생성자의 기능을 사용하겠다와 유니폼 초기화를 붙히면 청소에 관련된 부분은 아무런 문제가 없을것이다.

 

2. Initializers

 [ data.h ]  

#pragma once

class C_DATA
{
private:
	int m_n1;
	int m_n2;
	int m_n3;
	int m_n4;
public:
	explicit C_DATA(int nData);
	void print();
};

 [ data.cpp ]

#include <stdio.h>
#include "data.h"

C_DATA::C_DATA(int nData) :
	m_n1{},
	m_n2{},
	m_n3{},
	m_n4{}
{
	m_n1 = m_n2 = m_n3 = m_n4 = nData;
}

void C_DATA::print()
{
	printf("%d, %d, %d, %d\n", m_n1, m_n2, m_n3, m_n4);
}

[ test header.cpp ] 

#include <iostream>
#include "data.h"

int main()
{
	C_DATA cData(10);

	cData.print();
}

위의 코드를 출력을 해보면 전부 10이라는 값이 들어가 있을것이다.

그렇다면 다른 식으로 초기화 부분을 수정해보자 

#include <stdio.h>
#include "data.h"

C_DATA::C_DATA(int nData) :
	m_n1{nData}, / m_n1{nData}, / m_n4{nData}
	m_n2{nData}, / m_n2{m_n1}, / m_n2{m_n4}
	m_n3{nData}, / m_n3{m_n1}, / m_n3{m_n4}
	m_n4{nData}  / m_n4{m_n1}  / m_n1{m_n4}
{

}

void C_DATA::print()
{
	printf("%d, %d, %d, %d\n", m_n1, m_n2, m_n3, m_n4);
}

이렇게 초기화를 진행하게 된다면 어떤식으로 초기화가 되는지 결과를 출력해보자

 

  • 첫 번째 방식 (m_n2{m_n1})과 두 번째 방식 (m_n2{nData})에서는 m_n1이 초기화된 후 m_n2, m_n3, m_n4가 차례대로 초기화가 되어 모든 값이 10으로 설정된다.
  • 하지만 마지막의 방식 (m_n2{m_n4})에서는 문제가 발생합니다
    이는 m_n2가 초기화될 때, m_n4는 아직 초기화되지 않은 상태일 수 있기 때문이다. C++에서는 멤버 변수의 초기화 순서가 클래스 정의에서의 선언 순서를 따릅니다. 따라서, m_n1, m_n2, m_n3가 초기화될 때 m_n4는 아직 nData의 값을 받지 않은 상태이기에 그 결과 나머지 m_n의 값들은 예상치 못한 값을 가질 수 있다.
  • 그렇기에 이니셜라이즈드 즉 생성하면서 맴버를 꽂는 행위는 위험한 행위라는 것이다.

 

2. 1 안전한 초기화 방법: 늦은 초기화

 

이러한 문제를 피하려면, 늦은 초기화를 사용할 수 있다. 이는 초기화와 값을 설정하는 과정을 분리하는 방법.

 

 [ data.h ]  

#pragma once

class C_DATA
{
private:
	int m_n1;
	int m_n2;
	int m_n3;
	int m_n4;
public:
	C_DATA() = default;
	void init(int nData);
	void print();
};

 [ data.cpp ]

#include <stdio.h>
#include "data.h"

void C_DATA::init(int nData)
{
	m_n1 = nData;
	m_n2 = nData;
	m_n3 = nData;
	m_n4 = nData;
}

void C_DATA::print()
{
	printf("%d, %d, %d, %d\n", m_n1, m_n2, m_n3, m_n4);
}

[ test header.cpp ] 

#include <iostream>
#include "data.h"

int main()
{
	C_DATA cData{};

	cData.init(10);

	cData.print();
}

이런식으로 init를 사용하여 청소와 준비를 분리시키는 것이다. 이것이 늦은초기화이다.

 

늦은 초기화의 장점

  • 초기화 순서 문제 해결: 멤버 변수들이 모두 초기화된 후 init() 함수를 호출하므로, 초기화 순서로 인한 오류를 방지할 수 있다.
  • 코드의 가독성 향상: 멤버 변수의 초기화와 값을 설정하는 로직이 분리되어, 더 명확한 코드를 작성할 수 있다.
  • 유연한 초기화: 초기화 과정과 데이터를 설정하는 과정이 분리되어, 복잡한 객체 초기화나 다양한 설정이 필요한 경우에도 유연하게 대응할 수 있다.

 

 

 

3. [문제] 두개의 수를 관리, 더한 결과를 사용자에게 전달한다.

[ data.h ]  

#pragma once

class C_ADD
{
private:
	int m_nData1;
	int m_nData2;
	int m_nResult;
public:
	C_ADD() = default;
	void init();
	void setData1(int nData);
	void setData2(int nData);
	void addData();
	int addResult();
};

 [ data.cpp ]

#include <stdio.h>
#include "data.h"


void C_ADD::init()
{

}

void C_ADD::setData1(int nData)
{
	m_nData1 = nData;
}

void C_ADD::setData2(int nData)
{
	m_nData2 = nData;
}

void C_ADD::addData()
{
	m_nResult = m_nData1 + m_nData2;
}

int C_ADD::addResult()
{
	return m_nResult;
}

[ test header.cpp ] 

#include "data.h"

int main()
{
	C_ADD cData{};

	cData.init();

	cData.setData1(10);
	cData.setData2(30);

	cData.addData();

	printf("%d\n", cData.addResult());
}

 

주의할 점 

  • get시리즈는 내용물을 얻어가는 형태이기에 안에 작업을 시키면 안됨
  • 쓰여있는 메소드에는 쓰여있는 일만 시키는 형태를 지키면서 코드를 작성하기
  • 내가 어느 기점을 기준으로 코드를 나눌것인지 잘 생각하며 코드를 작성할것

'C++' 카테고리의 다른 글

[강의] 10월 29일 수업정리  (1) 2024.10.29
[강의] 10월 25일 수업정리  (0) 2024.10.27
[강의] 10월 23일 수업정리  (2) 2024.10.24
[강의] 10월 22일 수업정리  (1) 2024.10.23
[강의] 10월 18일 수업정리  (1) 2024.10.20