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 |