- [C++] 멤버 이니셜라이저2024년 09월 10일 13시 09분 19초에 업로드 된 글입니다.작성자: Le마리
함수 오버로드에 이어서 작성
지난 글을 보고오자
➥ 2024.09.09 - [공부/C++] - 함수 오버로드 (overLoad)
멤버 이니셜라이저란 ?
이니셜라이저를 사용하면 생성자가 실행될 때, 멤버 변수를 선언과 동시에 값을 할당할 수 있게 된다. 이를 통해 코드가 더 간결해지고, 초기화 과정이 더 효율적으로 진행된다.
멤버 이니셜라이저는 생성자의 중괄호 {}가 시작되기 전에, 멤버 변수의 초기값을 설정하는 방식이다. 특히 **상수(const)**처럼 한 번 값이 설정되면 변경할 수 없는 변수는 꼭 이 방식으로 초기화를 해야 한다. 이걸 사용하지 않으면 오류가 발생할 수 있다.
MyCharacter::MyCharacter(int hp, int dmg) { hitPoint = hp; baseDamage = dmg; }
이것을 멤버 이니셜라이저를 사용하면 더 간결하게 바꿀 수 있다.
MyCharacter::MyCharacter(int hp, int dmg) : hitPoint(hp), baseDamage(dmg) { // 생성자 블록 내부는 비워도 됩니다. }
사용해보기
1. 매개변수 없이 생성할 때
1-1. 헤더파일 정의
#pragma once class initialtest { public: // 생성자 생성 initialtest(); // 출력 void ShowMe(); private: const int id; // id를 상수로 int hitPoint; // HP int baseDamage; // 기본 피해량 };
1-2. cpp에서 구현#include "initialtest.h" initialtest::initialtest() { };
오류가 난다?
오류가 나는 이유
오류가 발생하는 이유는 id가 const 상수로 선언되었기 때문이다. const로 선언된 변수는 반드시 초기화가 필요하다. const는 한 번 값이 설정되면 변경할 수 없기 때문에, 생성자에서 초기화를 해주지 않으면 값을 설정할 수 없고, 오류가 발생하게 된다.
왜 멤버 이니셜라이저를 써야 할까?
- 상수(const) 변수는 한 번 값이 설정되면 바꿀 수 없다. 그래서 생성될 때 바로 값을 넣어줘야 한다.
- 만약 {} 안에서 값을 설정하려고 하면, 이미 변수가 선언된 이후라서 값을 넣는 게 너무 늦은 것이다. 그래서 컴파일러가 오류를 낸다.
- 이 문제를 해결하려면 {} 전에 변수를 초기화할 수 있는 멤버 이니셜라이저를 사용한다. 이건 마치 프로그램이 시작하기 전에 미리 변수를 준비해두는 것과 같다.
// 초기화를 멤버 이니셜라이저로 처리하는 생성자 initialtest::initialtest() : id(1), hitPoint(100), baseDamage(10) { // 중괄호 {} 전에 :id(1) 이런 식으로 상수를 초기화 // 만약 멤버 이니셜라이저를 사용하지 않으면 상수 id에 값을 줄 수 없어서 오류가 발생 }
아주 쉽게 비유하자면:
- 멤버 이니셜라이저는 아침에 일어나기 전에 미리 옷을 준비해놓는 것과 같다. 아침에 일어나서 준비하려면 시간이 부족할 수 있잖으니까? 마찬가지로, {} 안에서 값을 설정하면 이미 늦는다. 그래서 그 전에 값을 미리 설정해둬야 한다.
핵심 포인트:
- const 변수는 꼭 생성될 때 바로 값이 설정돼야 하고, 그 방법이 바로 멤버 이니셜라이저.
- 중괄호 {} 안에서 값을 설정하려고 하면, 이미 변수가 선언된 이후라서 오류가 생길 수 있다.
1-3. cpp에서 제대로 구현
#include "initialtest.h" #include <iostream> initialtest::initialtest() : id(0) // : 다음 부분이 변수 초기화 (멤버 이니셜라이징) { // id가 상수로 지정되고, 초기값이 없으면 여기서 오류 확인 가능 // 이유 : {}가 실행되면 이미 늦었기 때문 (상수 선언 후, 값이 없는 상태) // 해결 방법? 컴퓨터가 함수를 이해하는 형태 + 생성자의 실행 시점에 힌트 // 컴퓨터가 함수의 구분을 하는 시점 = 파일을 읽을 때 // 생성자의 실행 시점 = 최초로 클래스가 만들어질 때 // -> 둘 다, 프로그래밍을 확정하기 "이전" (심지어 하나는 다 읽기도 전) // -> "늦기 전에 값을 정해주면 되겠네?" = {} 나오기 전에 코딩하면 되겠네? // c++에서 함수의 이름을 확정하고 = () 이후 // 함수의 실행을 시작하기 전에 = {} 이전에 변수를 미리 할당해두는 방법 지원 // = 변수 초기화 수단 (혹은 발음대로 '멤버 이니셜라이저' member initializer) // ...작성법은 위 참조 hitPoint = 100; baseDamage = 10; std::cout << "[기본 생성자로 생성한 멤버 이니셜라이징]" << std::endl; } void initialtest::ShowMe() { std::cout << "id : " << id << std::endl; std::cout << "hitPoint : " << hitPoint << std::endl; std::cout << "baseDamage : " << baseDamage << std::endl; }
1-4. main에서 출력int main() { // 이니셜라이저 호출을 통한 인스턴스 생성 initialtest* initialTest1 = new initialtest(); // 값은 cpp에서 기본값을 할당해줬으니 출력만 하면 된다 initialTest1->ShowMe(); }
== 결과 ==
2. 숫자 둘을 사용하는 멤버 이니셜라이저
2-1. 헤더에서 선언#pragma once class initialtest { public: // 생성자 생성 initialtest(); // 출력 void ShowMe(); private: const int id; // id를 상수로 int hitPoint; // HP int baseDamage; // 기본 피해량 public: // 숫자 둘을 사용하는 생성자 선언 initialtest(int hp, int dmg); };
2-2. cpp 에서 구현// 오버로드를 통한 생성자 작성 initialtest::initialtest(int hp, int dmg) : id(0) { // : 표시 = '벰버 이니셜라이저 실행' // () 밖 변수 이름 : 멤버 변수 // () 안 데이터 : 초기 값 hitPoint = hp; // 블록 안에서 멤버 변수에 값을 할당 baseDamage = dmg; }
2-3. main에서 출력// 매개변수로 hp와 dmg를 썼으니 () 안에 숫자를 할당해줘야 한다. initialtest* initialTest2 = new initialtest(1234, 5678); initialTest2->ShowMe();
== 결과 ==
3. id를 포함한 숫자 셋짜리 선언 (제일 권장되는 방법)
+ ShoMe() 함수도 오버로드 해보자.
3-1. 헤더에서 선언#pragma once class initialtest { public: // 생성자 생성 initialtest(); // 출력 void ShowMe(); // ShowMe의 오버로드 void ShowMe(bool showId); // id 출력 여부를 따로 계산하는 함수 private: const int id; // id를 상수로 int hitPoint; // HP int baseDamage; // 기본 피해량 public: // 숫자 둘을 사용하는 생성자 선언 initialtest(int hp, int dmg); // id를 포함한 숫자 셋짜리 선언 initialtest(int id, int hp, int dmg); };
3-2. cpp 에서 구현void initialtest::ShowMe(bool showDID) { if (showDID) // 매개변수로 받은 값이 참이면 std::cout << "[ID 가 있는 함수 출력]" << std::endl; std::cout << "id : " << id << std::endl; // 나머지 정보는 그대로 출력 std::cout << "htiPoint : " << hitPoint << std::endl; std::cout << "baseDamage : " << baseDamage << std::endl; } // 오버로드를 통한 생성자 작성 initialtest::initialtest(int hp, int dmg) : id(0) { // : 표시 = '벰버 이니셜라이저 실행' // () 밖 변수 이름 : 멤버 변수 // () 안 데이터 : 초기 값 hitPoint = hp; // 블록 안에서 멤버 변수에 값을 할당 baseDamage = dmg; } // 멤버 이니셜라이저 응용 : 모든 멤버 변수 써보기, 매개변수 써보기 initialtest::initialtest(int id, int hp, int dmg) : id(id), hitPoint(hp), baseDamage(dmg) // : 표시 = 이니셜라이징 시작 // 왼쪽의, () 밖 이름 : 멤머 변수를 의미 // 오른쪽의, () 속 데이터 : 초기 값, 함수의 매개변수여도 OK { // 변수를 모두 이니셜라이저로 초기화해서 이 안에는 이제 코드가 필요없음 }
3-3. main에서 출력// 세 개의 인자를 받는 함수 구현 initialtest* initialTest3 = new initialtest(1, 2345, 2427272); // id를 외부에서 설정 가능한 // 숫자 세 개짜리 생성자 실행 // -> 작성할 때는 이니셜라이저로 다르게 썼지만 // 불러올 때는 그냥 평범하게 불러도 된다 // bool 데이터 쓴 함수 출력 initialTest3->ShowMe(true);
== 결과 ==
💡 3번이 제일 권장되는 이유
더보기1. 멤버 이니셜라이저를 사용하면 "두 번 일하는 것을 막을 수 있다"
일반적으로 멤버 변수를 초기화할 때, 생성자 내부에서 값을 할당하면 변수가 처음에 기본값으로 초기화된 후 다시 원하는 값으로 덮어쓰는 작업이 발생할 수 있다. 이렇게 하면 두 번 일하는 셈!
비유: 만약 카페에서 커피를 주문한다고 생각해보자. 처음에 아무말도 하지 않으면 기본적으로 블랙 커피가 나올 것이다. 그런데 나중에 "우유랑 설탕 추가해 주세요!"라고 하면, 그 블랙 커피에 추가로 우유와 설탕을 넣어야 한다다.
그러나 멤버 이니셜라이저를 사용하면, 처음부터 "우유와 설탕을 넣은 커피"를 바로 받을 수 있다.
2. 상수나 참조자와 같은 멤버 변수는 생성자 블록 안에서 값을 할당할 수 없다
**상수(const)**는 한 번 값이 정해지면 바꿀 수 없기 때문에, 생성자가 실행되기 전에 값을 정해줘야 한다. 이를 위해서는 반드시 멤버 이니셜라이저를 사용해야 한다.
만약 상수나 참조자가 있다면 멤버 이니셜라이저 없이 생성자 블록 안에서 초기화할 수 없다.
비유: 만약 커피를 주문했는데, "우유가 절대 추가될 수 없는" 상황이라면, 처음부터 "우유가 포함된 커피"를 요청해야만 하는 것과 같다!
3. 더 명확한 코드
멤버 이니셜라이저를 사용하면 어떤 변수에 어떤 값이 들어가는지가 생성자 코드 안에서 한눈에 명확하게 보인다. 이는 코드를 읽는 다른 사람이나 미래의 나에게 친절한 방식이다.
4. 더 효율적인 메모리 사용
컴퓨터가 변수를 초기화할 때 한 번에 바로 원하는 값으로 초기화할 수 있어서, 메모리를 더 효율적으로 사용할 수 있다.
💡꼭 main에서 값을 넣어줘야 할까?
더보기이니셜라이저를 사용하여 매개변수를 통해 값을 할당하는 방식에서는, 반드시 main에서 값을 전달해줘야 한다. 즉, 이 방식에서는 값을 넘겨주지 않으면, 초기화할 데이터가 없기 때문에 객체가 생성될 때 오류가 발생하거나 쓰레기 값이 들어갈 수 있다.
다시 말해서, 다음과 같이 main에서 값을 넣어줘야 한다:
initialtest* initialTest3 = new initialtest(1, 2345, 2427272); // initialTest3는 id가 1, hitPoint가 2345, baseDamage가 2427272인 상태로 생성됩니다.
만약 main에서 값을 넣지 않고 객체를 생성하려고 한다면, 기본 생성자가 필요하고, 그 경우에는 미리 정해둔 기본값을 설정하게 됩니다.
예시 (기본 생성자도 같이 사용)
// 기본 생성자 initialtest::initialtest() : id(0), hitPoint(100), baseDamage(50) { // 기본값을 사용 } // 매개변수를 사용하는 생성자 (이니셜라이저) initialtest::initialtest(int id, int hp, int dmg) : id(id), hitPoint(hp), baseDamage(dmg) {}
이렇게 하면, main에서 기본 생성자를 사용할 경우에는:
int main() { initialtest obj1; // 기본 생성자 사용 -> id = 0, hitPoint = 100, baseDamage = 50 initialtest obj2(1, 100, 200); // 매개변수 생성자 사용 -> id = 1, hitPoint = 100, baseDamage = 200 }
정리
- 매개변수 생성자를 사용할 때는 main에서 값을 꼭 전달해야 한다.
- 기본 생성자를 함께 정의해두면, 값을 전달하지 않고도 객체를 생성할 수 있다.
이니셜라이저의 장점과 한계
1. 멤버 이니셜라이저가 좋은 이유
- 컴퓨터 효율성: 이니셜라이저는 변수를 바로 초기화하므로 메모리 사용 면에서 더 빠르고 효율적이다.
- 상수 멤버 처리:
const
로 선언된 변수나reference
는 생성자 본문에서 값을 할당할 수 없기 때문에, 이니셜라이저를 사용해야 한다. - 직관성: 이니셜라이저는 멤버 변수를 생성자 호출 시 바로 초기화하므로 코드가 간결하고 초기화 과정이 명확히 드러난다.
2. 항상 좋은 건 아니다
- 다른 클래스보다 나중에 초기화가 필요한 경우: 예를 들어, 어떤 클래스가 다른 클래스의 데이터를 참고해야 할 때 이니셜라이저로 즉시 초기화하는 것은 적절하지 않다. 온라인 게임에서 퀘스트나 등장인물 데이터를 먼저 불러온 후에 클래스를 초기화하는 경우가 그렇다.
- 가독성 문제: 때로는 a = n;과 같이 직접 할당하는 방식이 더 명확할 수 있다. 이 방식은 사람이 나중에 코드를 읽을 때 이해하기 쉬워 가독성을 높인다.
3. 이니셜라이저의 한계
- 이니셜라이저는 해당 클래스의 멤버만 초기화 가능: 이니셜라이저는 해당 클래스 내에서 선언된 멤버 변수만 초기화할 수 있다. 만약 다른 클래스의 데이터를 초기화해야 한다면 평범한 코딩 방식이 더 적합하다.
결론
- 상황에 맞게 선택: 이니셜라이저는 효율적일 수 있지만 모든 경우에 적합하지 않다. 변수의 초기화 시점과 관계에 따라 이니셜라이저를 사용할지, 아니면 일반적인 코딩 방식을 사용할지 결정해야 한다.
'공부 > C++' 카테고리의 다른 글
[C++] 함수 오버라이딩 (Overriding) (5) 2024.09.17 [C++] 상속 (inheristance) (3) 2024.09.10 [C++] 함수 오버로드 (overLoad) (0) 2024.09.09 [C++] 생성자를 이용해 캐릭터 만들기 (0) 2024.09.09 [C++] 소멸자 (0) 2024.09.09 다음글이 없습니다.이전글이 없습니다.댓글