C++/어려운 C++

[C++] 초기화

econo-my 2025. 4. 15. 19:37

Uniform Initialization

균일 초기화 : 객체의 형태에 상관 없이 중괄호 {} 를 사용해서 초기화 하는 것

struct Point
{
	int x, y;
};

class Rational
{

public:
	Rational(int n, int d) : numerator(n), denominator(d) {}

private:
	int numerator, denominator;
};

int main()
{
	int n = { 0 };
	int m[2] = { 1, 2 };
	Point p = { 1,2 };
	Rational r = { 1,2 };

/*  같은 표현
	int n{ 0 };
	int m[2]{ 1, 2 };
	Point p{ 1,2 };
	Rational r{ 1,2 }; 
*/

	//암시적 타입 변환들을 허용하지 않는다.
	//정확히 데이터 손실이 있는 변환을 허용하지 않는다.
	int n2 = 3.4; //컴파일 가능
	int n3 = { 3.4 }; //error
	double n4 = { 3 }; //컴파일 가능
}

 

Direct(직접 초기화) vs Copy(복사 초기화)

  • 직접 초기화 : 초기화 시에 =을 사용하지 않는 것
  • 복사 초기화 : 초기화 시 = 을 사용하는 것
int main()
{
	int n1 = 0; // copy initialization
	int n2(0);	// direct initialization

	int n3 = { 0 };
	int n4{ 0 };
}

 

 

  • 둘의 차이점
class Point
{
	int x, y;
public:
	// explicit : 변환 생성자로 사용될수 없다.
	//		      또하나의 의미, copy initialization 될수 없다.
	explicit Point()    	     : x(0), y(0) {}
	explicit Point(int a)        : x(a), y(0) {}
	explicit Point(int a, int b) : x(a), y(b) {}
};

int main()
{
	Point p1(5);	// ok.
	//Point p2 = 5;	// ok. 하지만 생성자 explicit 라면 error.

	Point p3(1, 1);		// ok
	//Point p4 = (1, 1);	// error.

	Point p5{ 1,1 };	// ok
	//Point p6 = { 1,1 };	// ok. 하지만 생성자 explicit 라면 error.

	Point p7;
	Point p8{};		// direct
	//Point p9 = {};  // copy. error
}

 

Default(기본 초기화) vs Value(기본값 초기화)

  • 기본 설명
  •  
  • 사용자 정의 타입일 경우 주의 사항
#include <iostream>
using namespace std;

class Point
{
public:
	int x;
	int y;

    Point() {}
	//Point() = default;
};
int main()
{
	Point p1;   // default initialization
	Point p2{}; // value   initialization

	cout << p1.x << endl; // 쓰레기 값
	cout << p2.x << endl; // 쓰레기 값, 값 초기화를 의도 하였을 경우 Point() = default; 초기화를 사용 해야함
}

 

  • 디폴트 초기화 관련 규칙 더 알아보기
#include <iostream>
using namespace std;

int main()
{
	int n1;		// default. 쓰레기값
	int n2{};	// value.   0
	int n3();   // 함수선언.

	int* p1 = new int;	// default. 쓰레기값.
	int* p2 = new int();// value.   0
	int* p3 = new int{};// value.   0

	cout << *p1 << endl; // 쓰레기 값
	cout << *p2 << endl; // 0
	cout << *p3 << endl; // 0
}

 

 

 

#include <iostream>
#include <cstdio>

struct Data
{
	int age; //나이
	char name[100]; //이름
	int balance; //지갑 속의 용돈
};

struct Test
{
	Test(int a) {}
};

void Print(Data user)
{
	std::cout << user.age << ", " << user.name << ", 잔액 : " << user.balance << std::endl;
}

int main()
{
	// 1. 대입 연산자를 사용하여 리터럴을 대입하는 방식으로 변수의 생성과 초기화는 구조체나 클래스 타입의 경우 허용하지 않는다.
	// 컴파일 시 int 타입에서 구조체로 타입을 변환시킬 수 없다는 에러가 발생한다.
	// Data user01 = 21;
	// 이것은 내 생각 -> 위의 구절은 인자가 하나인 생성자를 염두에 두지 않고 선생님께서 쓰신 말같다.
	Test test = 1;

	// 2. 구조체 내 변수의 순서에 맞추어 리터럴을 함수의 인수로 사용하여 변수를 초기화한다면,
	// 다음과 같은 에러가 발생한다. 이 문제의 해결 방안은 구조체의 생성자를 만들어 제공하면 된다.
	// Data user02(33, "이순신", 200000);

	// 3. 그러나 아래와 같이 중괄호를 사용한다면, 초기화가 혀용된다.
	// 입력된 인수는 순서적으로 멤버 변수에 할당된다.
	Data user03{ 53, "강감찬", 20000 }; // -std=c+11 이후부터 사용이 가능하다.
	Print(user03);

	// 4. 무명 변수를 만들어 무명 변수의 값을 변수에 대입시켜 초기화한다. 앞에서 보았던 2. 와 동일한 
	// 에러가 발생한다. 이 문제 역시 생성자를 만들어주면 해결된다.
	// Data user04 = Data(45, "이성계", 304040);

	// 5. 아래처럼 함수 호출 연산자와 함께 변수를 생성한다면, 컴파일 에러가 발생한다.
	// 무명변수는 허용되지만 안정성의 이유로 아래와 같이 직접 변수를 생성하는 방식은 ISO에서 금지하는 방식이다.
	// Data user05();

	// 6. 아래와 같이 Data 타입의 무명 변수를 생성하고 변수에 대입한다면, 변수가 생성되는
	// 동시에 모든 멤버 변수는 타입에 따라 초기화된다.
	Data user06 = Data();
	Print(user06);

	// 7. 구조체와 클래스는 다음과 같이 나열된 값으로 변수에 순서대로 할당하여 초기화시킬 수 있다.
	// 이 방식은 C 언어에서 유래된 방식이다.
	Data user07 = { 21, "홍길동", 10000 };
	Print(user07);

	// 8. 초기화없이 구조체의 변수를 선언한다면 멤버 변수에 알 수 없는 값이 들어간다.
	Data user08;
	Print(user08);

	// new 라는 키워드를 사용하여 메모리 저장소를 생성하여 변수에 할당한다.
	// 이 경우 역시 멤버 변수에 알 수 없는 값이 들어간다.
	Data* user_08 = new Data;
	Print(*user_08);

	// 9. new 지정자와 함께 아래처럼 변수를 생성하면 모두 자동으로 초기화된다.
	Data* user09 = new Data();
	Print(*user09);
}

 

C++ 17 책의 초기화 부분을 읽어보며 궁금한 점이 몇 개 생겼다.

new 연산자를 이용해 변수를 초기화할 때  ()  를 붙였을 때와 안 붙였을 때 차이가 뭘까

 

컴파일러가 만들어주는 기본 생성자가 그 이유인 것 같은데 정확히는 모르겠으니 테스트 해보자.

#include <iostream>

struct Data
{
	int a;
	std::string b;
	char c;
};

class Data2
{
	int a;
	std::string b;
	char c;

public:
	int d;
	std::string e;
	char f;
};

int main()
{
	Data data{ 0, "a", 'c' };
	//Data data2(1, "a", 'c');
	Data* data3 = new Data;
	Data* data4 = new Data();
	//Data* data5 = new Data(1, "a", 'c');
	Data* data6 = new Data{ 6, "3", 'c' };
	//Data* data7 = new Data{ "a", 3, 'c'};

	Data2* data8 = new Data2;
	Data2* data9 = new Data2();
	//Data2* data10 = new Data2{ 6, "3", 'c' };


	std::cout << "Data 0번 : " << data.a << std::endl;
	std::cout << "Data 3번 : " << data3->a << std::endl;
	std::cout << "Data 4번 : " << data4->a << std::endl;
	std::cout << "Data 6번 : " << data6->a << std::endl;
}

// 출력 결과
Data 0번 : 0
Data 3번 : -842150451
Data 4번 : 0
Data 6번 : 6

data3, data4 를 봐보자

 

확실히 출력 결과를 보면 data3 객체의 멤버 변수에는 쓰레기 값이 들어가있는 것으로 보인다.

그럼 생성자를 만들어서 로그를 찍어보자.

 

#include <iostream>
#define LOG(x) std::cout << x << std::endl;

struct Data
{
	Data() { LOG("기본 생성자"); }

	int a;
	std::string b;
	char c;
};

class Data2
{
	int a;
	std::string b;
	char c;
	 
public:
	int d;
	std::string e;
	char f;
};

int main()
{
	//Data data{ 0, "a", 'c' };
	//Data data2(1, "a", 'c');
	Data* data3 = new Data;
	Data* data4 = new Data();
	//Data* data5 = new Data(1, "a", 'c');
	//Data* data6 = new Data{ 6, "3", 'c' };
	//Data* data7 = new Data{ "a", 3, 'c'};

	Data2* data8 = new Data2;
	Data2* data9 = new Data2();
	//Data2* data10 = new Data2{ 6, "3", 'c' };


	//std::cout << "Data 0번 : " << data.a << std::endl;
	std::cout << "Data 3번 : " << data3->a << std::endl;
	std::cout << "Data 4번 : " << data4->a << std::endl;
	//std::cout << "Data 6번 : " << data6->a << std::endl;
}

// 출력 결과
기본 생성자
기본 생성자
Data 3번 : -842150451
Data 4번 : -842150451

Data 구조체에 기본 생성자를 만드니 유니폼 초기화로 초기화한 Data 들에서 오류가 나 주석 처리했다.

-> data3 과 data4 의 차이를 살펴보고 이것도 알아보자.

 

출력 결과를 보니까 둘 다 쓰레기 값이 들어가 있다. 

 

결론적으로

1. 컴파일러가 기본으로 만들어주는 생성자는 모든 멤버 변수를 기본값으로 초기화 해준다.

2. 초기화할 때 () 를 붙이는 것은 기본 생성자를 호출 해준다.

 

인터넷을 뒤져보며 안건데 C++ 은 POD란 개념이 있다.

복잡하지 않은 사용자 정의형 데이터를 진짜 '데이터' 그 자체로 보는 것이다.

자세한 사항은 https://haedallog.tistory.com/78 이 분이 잘 써주셨다.

 

 

 

이제 위에서 유니폼 초기화가 왜 오류가 났는지 살펴보자.

결론은 brace-initialization(중괄호 초기화)와 사용자 정의 생성자(user-defined constructor)가 충돌하기 때문이다.

 

유니폼 초기화 == aggregate initialization 

aggregate initialization은 aggregate 타입에만 허용된다.

 

구조체 또는 클래스가 아래 조건을 만족하면 aggregate:

  1. 모든 멤버가 public
  2. 사용자 정의 생성자 없음
  3. 가상 함수 없음
  4. 상속 없음

따라서 생성자를 만들면  { } 기호를 사용해 멤버를 직접 초기화 할 수 없다.