
std::initializer_list
int array[5];
이러한 배열을 초기화 하고 싶다면 초기화 리스트 구문을 사용하여 초기화가 가능하다
int array[]{ 1, 2, 3, 4, 5 }; //초기화 리스트
물론 동적 할당 배열에도 동일하게 동작한다
int array[]{ new int[5]{1, 2, 3, 4, 5} };
delete[] array;
하지만 사용자 정의 클래스에서는 어떨까?
class IntArray
{
private:
int m_length{};
int* m_data{};
public:
IntArray() = default;
IntArray(int length)
: m_length{ length }
, m_data{ new int[static_cast<std::size_t>(length)] {} }
{
}
~IntArray() {
delete[] m_data;
}
int& operator[](int index) {
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; }
};
int main()
{
IntArray array{ 1, 2, 3, 4, 5 }; //compile error
return 0;
}
IntArray는 element가 index인 배열을 간단하게 구현해본 클래스이다, 여기서 IntArray array{ 1, 2, 3, 4, 5 };로 리스트 초기화는 불가능하다
왜냐하면 이와 같은 인자를 처리할 생성자가 따로 없기 때문이다
그렇다면 array[0], array[1]... 해서 값을 할당하는 방법만 있을까? 그렇지 않다
이러한 문제를 해결하기 위해서 STL에서 제공하는 std::initializer_list를 사용한다
컴파일러는 { } 형태의 초기화 리스트를 보면 이를 자동으로 std::initializer_list 타입 객체로 캐스팅한다
따라서 std::initializer_list를 매개변수로 가지는 생성자를 추가하면 사용자 정의 클래스 타입 객체도 초기화 리스트로 초기화가 가능하다
< initializer_list >헤더에 정의되어 있다
std::initializer_list는 템플릿 클래스로 < >를 통해 원하는 타입을 지정해서 사용한다, size() 멤버함수로 해당 리스트에 포함된 element 개수를 return할 수 있다
std::initializer_list는 std::string_view와 같이 동작한다, 실제 데이터 덩어리가 아닌 가벼운 참조와 같기 때문에 실제로 std::intializer_list가 복사된다 하더라도 내부 element들이 전부 복사되는게 아니고 view 자체만 복사되는 느낌이기 때문에 비용이 낮아 값 타입으로 전달한다
class IntArray
{
private:
int m_length{};
int* m_data{};
public:
IntArray() = default;
IntArray(int length)
: m_length{ length }
, m_data{ new int[static_cast<std::size_t>(length)] {} }
{
}
IntArray(std::initializer_list<int> InList)
: IntArray(static_cast<int>(InList.size())) //위임 생성자 호출로 동적 배열 할당
{
std::copy(InList.begin(), InList.end(), m_data); //인자로 받은 list의 시작 ~ 끝까지 iterator를 이용하여 m_data 포인터가 가리키는 메모리 공간에 copy한다
}
~IntArray() {
delete[] m_data;
}
int& operator[](int index) {
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; }
};
int main()
{
IntArray array{ 1, 2, 3, 4, 5 }; //std::initializer_list를 매개변수로 가지는 생성자를 정의했기 때문에 가능하다
return 0;
}
이때 std::initializer_list 객체는 각각의 element에 접근할 때 operator[]를 제공하지 않기 때문에 []로 접근이 불가능하다
따라서 foreach loop를 사용하거나 iterator를 사용해야 한다
IntArray(std::initializer_list<int> InList)
: IntArray(static_cast<int>(InList.size()))
{
for (auto element : InList)
{
std::cout << element << '\n';
}
for (std::size_t i{}; i < InList.size(); ++i)
{
std::cout << *(InList.begin() + i) << '\n';
//std::cout << InList.begin()[i]; 도 가능
}
std::copy(InList.begin(), InList.end(), m_data);
}
{ }를 사용한 리스트 초기화는 다른 생성자보다 std::initializer_list를 매개변수로 가지는 생성자(리스트 생성자)를 우선으로 선택한다
IntArray(5); //IntArray(int) 생성자 호출 -> 크기가 5인 배열 생성
IntArray{ 5 }; //std::initializer_list 생성자 호출 -> 크기가 1이고 5로 초기화 된 배열 생성
이는 리스트 생성자와 다른 생성자를 모두 가진 STL 컨테이너 클래스에서 동일하게 동작한다
기존에 사용하던 클래스에 리스트 생성자를 추가하는건 좀 위험할 수 있다
class Foo
{
public:
Foo(int a, int b)
{
std::cout << "Foo(int, int)" << '\n';
}
};
int main()
{
Foo f1{ 10, 20 };
return 0;
}
이 상태에서는 Foo(int, int) 생성자가 호출되었지만 만약 아래와 같은 생성자가 추가되었다면
Foo(std::initializer_list<int> InList)
{
std::cout << "Foo Initializer list" << '\n';
}
Foo f1{10, 20};은 변경이 없지만 호출되는 생성자는 변경되게 된다, 이는 의도치 않은 동작을 일으킬 가능성이 생긴다
std::initializer_list는 생성자 뿐 아니라 대입 연산자인 operator=를 오버로딩 하여 사용할 수 있다
class IntArray
{
private:
int m_length{};
int* m_data{};
public:
IntArray() = default;
IntArray(int length)
: m_length{ length }
, m_data{ new int[static_cast<std::size_t>(length)] {} }
{
}
IntArray(std::initializer_list<int> InList)
: IntArray(static_cast<int>(InList.size()))
{
std::copy(InList.begin(), InList.end(), m_data);
}
//std::initializer_list를 사용한 operator= 오버로딩
IntArray& operator=(const std::initializer_list<int>& InList)
{
delete[] m_data;
m_data = new int[static_cast<int>(InList.size())];
std::copy(InList.begin(), InList.end(), m_data);
return *this;
}
~IntArray() {
delete[] m_data;
}
int& operator[](int index) {
assert(index >= 0 && index < m_length);
return m_data[index];
}
int getLength() const { return m_length; }
};
int main()
{
IntArray array{};
array = { 1, 2, 3, 4, 5 }; //operator=가 오버로딩 되었기 때문에 사용 가능
return 0;
}
리스트 생성자를 정의했다면 std::initializer_list를 받는 operator= 연산자 오버로딩을 하는게 좋다
만약 std::initializer_list를 받는 operator= 연산자 오버로딩이 없다면
array = { 1, 2, 3, 4, 5 };를 할 때 operator=(const std::initializer_list)가 있는지 없는지 확인 후 없기 때문에 컴파일러가 암시적으로 생성한 operator=(const IntArray&)를 사용하려고 한다
이때 우리는 리스트 생성자를 정의했기 때문에 { 1, 2, 3, 4, 5 }를 리스트 생성자로 생성하여 임시 객체가 만들어지고 결국 array = 임시객체;가 된다
컴파일러가 암시적으로 생성한 복사 대입 연산자는 deep copy가 안되기 때문에 같은 m_data 포인터를 공유하게 되고 임시 객체는 결국 해당 라인에서 소멸하기 때문에 array는 dangling pointer를 가지게 된다, 따라서 array.m_data에 접근하게 되면 undefined behavior가 발생하게 된다
상속
C++에서 상속은 클래스들 사이에서 일어난다, 상속을 하는 클래스를 부모 클래스(parent), 기반 클래스(base class), super class라고 부르며 상속을 받는 클래스는 자식 클래스(child), sub class라고 부른다
예를들면 Animal이 부모 클래스면 Dog, Cat이 자식 클래스가 되는것이다
자식 클래스는 부모 클래스로부터 public:, protected: 함수, 변수를 물려받으며 이렇게 물려받은 함수, 변수는 자식 클래스의 멤버가 된다 또한 자식 클래스만의 고유한 함수, 변수도 가질 수 있다
class Animal
{
public:
std::string name{};
int age{};
protected:
Animal(std::string_view inName, int inAge) : name{ inName }, age{ inAge }
{
}
};
Animal클래스는 동물이 공통적으로 가지는 나이와 이름을 정의한 클래스이다, 해당 Animal 클래스를 상속받은 자식 클래스들은 전부 name과 age 변수를 가지게 된다
따라서 Dog나 Cat클래스를 정의할 때 Animal클래스를 상속받으면 name과 age 변수를 따로 멤버로 추가할 필요가 없다
상속을 받기 위해서는 자식 클래스의 옆에 : public 부모클래스를 써주면 된다
class Dog : public Animal
{
public:
int dogA{};
};
이제 Dog 클래스는 name, age, dogA라는 3개의 멤버를 가지게 된다

Dog d1{};
d1.name = "Kelvin"; //Dog클래스에 명시적으로 name을 추가하지 않았지만 상속을 받기 때문에 가능
return 0;
이러한 상속은 체인으로 사용이 가능하다, 한 마디로 A클래스로부터 파생된 B클래스를 또 상속시켜 C클래스를 만들 수 있다는 것이다
class Animal
{
public:
std::string name{};
int age{};
protected:
Animal() = default;
Animal(std::string_view inName, int inAge) : name{ inName }, age{ inAge }
{
}
};
class Cat : public Animal
{
public:
Cat() = default;
int catA{};
};
class RussianBlue : public Cat
{
};
이렇게 되면 RussianBlue는 Animal, Cat 클래스의 public:, protected 멤버 데이터를 멤버로 가지게 된다
이러한 상속은 결국 base class로 부터 멤버 데이터를 받을 수 있기 때문에 derived class에서 해당 데이터를 다시 정의할 필요가 없어진다, 따라서 코드가 간결해지고 재사용성이 높아지며 유지보수성이 향상된다 (부모 클래스만 수정하면 자식 클래스도 같이 수정되기 때문)
상속 관계에서의 생성,소멸 순서
일반적으로 자식 클래스는 부모 클래스의 멤버 함수, 멤버 변수를 상속받기 때문에 부모 클래스의 멤버 데이터가 자식 클래스로 복사된다고 생각할 수 있지만 그렇지 않다, 이 둘은 구분되어 있다
자식 클래스 객체가 메모리에 생성될 때 부모 클래스의 멤버 데이터를 위한 메모리 공간과 자식 클래스의 멤버 데이터를 위한 메모리 공간이 함께 할당된다 (자식 클래스가 부모 클래스를 포함하는 느낌)
그렇기 때문에 자식 클래스 객체를 생성하게 되면 부모 클래스의 생성자와 자식 클래스의 생성자가 호출되게 된다
최상위 부모 클래스 객체 생성 -> 그 다음 자식 클래스 객체 생성 -> 그 다음 -> 최하위까지
이런 순서로 생성된다
따라서 ParentClass의 생성자가 호출되고 그 다음에 ChildClass의 생성자가 호출되게 된다
class Animal
{
public:
std::string name{};
int age{};
protected:
Animal()
{
std::cout << "Animal()" << '\n';
}
};
class Cat : public Animal
{
public:
Cat()
{
std::cout << "Cat()" << '\n';
}
};
int main()
{
Cat c1{}; //Animal(), Cat() 순서대로 호출됨
return 0;
}
자식 클래스는 부모 클래스의 멤버 데이터를 사용하기 때문에 부모 클래스 객체가 먼저 생성되는게 안전한 방식이다
만약 Cat을 상속받는 class RussianBlue : public Cat이 있었고 RussianBlue 클래스 객체를 생성했다면 Animal() -> Cat() -> RussianBlue() 생성자 순으로 호출되게 된다
상속 관계에서의 초기화
class Base
{
public:
int m_id{};
Base(int id = 0)
: m_id{ id }
{
}
int getId() const { return m_id; }
};
class Derived : public Base
{
public:
double m_cost{};
Derived(double cost = 0.0)
: m_cost{ cost }
{
}
double getCost() const { return m_cost; }
};
int main()
{
Derived d1{ 10.5 };
return 0;
}
다음과 같은 구조에서 만약 Base클래스 객체가 인스턴스화 된다면 다음과 같은 단계를 거치게 된다
int main()
{
Base b1{ 10 };
}
하지만 Derived클래스 객체가 인스턴스화 될 때는 조금 다르다
그렇다면 위 코드에서 Derived클래스 객체를 생성할 때 부모 클래스인 Base클래스의 m_id는 어떻게 초기화 시킬 수 있을까?
Derived(double cost = 0.0, int id = 0)
: m_cost{ cost }, m_id{ id }
{
}
이렇게 작성하면 완벽하게 초기화 될 것 같지만 C++은 생성자의 멤버 초기화 리스트에서는 상속받은 멤버 변수를 초기화 하는것을 금지하기 때문에 컴파일 에러가 발생한다
왜 이러한 제한이 있는걸까? 라고 생각한다면 이유는 다음과 같다
const 변수와 참조 변수 때문이다, 만약 Base 클래스의 m_id가 const변수라고 가정하고 Derived클래스의 생성자의 멤버 초기화 리스트에서 초기화 된다고 가정하게 되면 결국 Base 클래스 생성자에서 const변수인 m_id가 초기화 되고 Derived클래스 생성자의 멤버 초기화 리스트에서 값이 또 초기화된다는 의미가 된다, 이는 결국 const변수를 수정하는 셈이 되기 때문에 불가능하게 제한을 걸어놓는 것이다
const가 아닌 부모 클래스의 변수는 생성자에서 대입 연산으로 할당이 가능은 하다
Derived(double cost = 0.0, int id = 0)
: m_cost{ cost }
{
m_id = id; //가능은 함
}
하지만 Base의 멤버 변수가 참조타입이나 const라면 이러한 코드는 동작하지 않게된다
(참조나 const는 생성자의 멤버 초기화 리스트에서 초기화 되어야 하기 때문)
만약 const나 참조타입 변수를 자식 클래스 생성자의 멤버 초기화 리스트에서 초기화 할 수 있게 된다면 Base클래스의 생성자에서 해당 변수를 사용할 수 없게 될 것이다 (부모 클래스 생성자가 자식 클래스 생성자보다 먼저 호출되기 때문)
그렇다면 자식 클래스에서 부모 클래스 멤버 데이터를 어떻게 하면 올바르게 초기화 할 수 있을까?
Base Class 생성자 호출
위에서 정리했듯 자식 클래스 객체가 인스턴스화 될 때 부모 클래스의 생성자를 명시하지 않으면 기본 생성자가 호출된다고 했다, 따라서 원하는 부모 클래스 생성자를 명시하여 부모 클래스의 멤버 데이터를 초기화 할 수 있다
class Derived : public Base
{
public:
double m_cost{};
Derived(double cost = 0.0, int id = 0)
: Base(id), m_cost{ cost } //호출할 부모 클래스 생성자를 명시함
{
}
double getCost() const { return m_cost; }
};
int main()
{
Derived d1{ 10.5, 5 };
return 0;
}

이러한 방식으로 원하는 부모 클래스의 생성자를 호출할 수 있다
이제는 m_id를 public:에 선언할 이유가 없어졌다 (private:에 두고 부모 클래스 생성자를 이용하여 초기화 하면 되기 때문)
중요한 점은 생성자는 자신의 직속 부모 클래스의 생성자만 호출할 수 있다
ex) A -> B -> C 상속관계에서 C클래스 생성자에서 A클래스 생성자를 호출할 수 없다
상속 관계에서의 소멸자
자식 클래스가 소멸될 때 각 소멸자는 생성자 호출 순서의 역순으로 호출된다
Derived클래스 객체가 소멸되면 Derived클래스 소멸자 호출 -> Base클래스 소멸자가 호출되게 된다