1. 초기화 여부를 파악하긴 난해하다.
C++의 변수 초기화가 중구난방은 아니다.
초기화가 보장되는 경우와 아닌경우가 분명하게 존재한다.
그러나 그 조건을 전부 알고있기엔 너무나 복잡하다.
기본적으로 C부분만 보면 초기화에 런타임 비용이 소모된다면 초기화가 보장되지 않습니다.
EX) : C의 배열은 초기화 보장이 안되지만 C++의 STL인 vector는 초기화 보장이 됩니다.
결론은 초기화 되는지 안되는지 다 파악하는 것은 상당히 난해하기에
그냥 항상 초기화를 해주는 것이 가장 좋습니다.
이러한 방식들이 초기화입니다.
int x=0;
const char* text = "Hello Yeoul";
double d;
std::cin >> d;
2.대입, 초기화를 혼동하지 말자
아래의 경우는 초기화를 하는 것이 아니라 대입을 하고있습니다.
물론 우리가 원하는 값이 되긴 하겠지만 썩 좋은 방법은 아닙니다.
사실 생성자에 진입하기도 전에 이미 초기화는 지나갔습니다.
class Yeoul {
private:
int age;
std::string name;
double height;
bool isStudent;
public:
// 생성자 - 초기화 리스트를 사용하지 않고 대입으로 값 설정
Yeoul() {
age = 0;
name = "";
height = 0.0;
isStudent = false;
}
}
초기화가 이미 지나갔다는게 무슨 말일까요
멤버 변수의 기본 초기화 : 생성자 본문이 실행되기 전에 클래스의 멤버 변수들은 이미 기본 초기화(default initialization)가 됩니다:
- 내장 타입(int, double, bool 등)은 자동으로 초기화되지 않고 쓰레기 값을 가집니다.
- 클래스 타입(std::string과 같은)은 해당 클래스의 기본 생성자에 의해 초기화됩니다.
즉 이미 초기화됐는데 또다시 대입을 해주고 있는 것이었죠 (name의 경우)
3. 초기화 리스트를 사용하자
그래서 우리는 초기화 리스트를 사용해야 합니다.
class Yeoul {
private:
int age;
std::string name;
double height;
bool isStudent;
public:
// 생성자 - 초기화 리스트를 사용하여 값 설정
Yeoul() : age(0), name(""), height(0.0), isStudent(false) {
}
}
이렇게 하면 기본 생성자 호출후에 복사대입 연산자를 쓰는것이 아니라 적절한 생성자를 단 한번만 호출합니다.
또한 데이터 멤버를 기본 생성자로 초기화 하고 싶을 때에도 멤버 초기화 리스트를 사용하는 것이 좋습니다.
물론, 멤버 초기화 리스트에 없고, 데이터 타입이 사용자 정의 타입이라면 컴파일러가 자동으로 기본 생성자를 호출하긴 합니다.
그러나 초기화 리스트를 다 써줘야 하는 이유는 어떠한 멤버가 초기화 되지 않았다는 사실을 끌고갈 필요가 없다는 것입니다.
즉 초기화 됐을지 안됐을지 고민할 필요가 없다는 것이죠
물론 초기화 리스트는 선택이긴 하지만 반드시 써줘야 하는 경우가 있는데
그것은 바로 상수이거 참조자로 되어 있는 데이터 멤버의 경우엔 반드시 초기화 되어야 합니다.
왜냐하면 참조자와 상수는 대입 자체가 불가능하기 때문입니다.
- 효율성: 초기화 리스트를 사용하면 기본 생성자 호출 후 대입하는 두 단계 과정이 아니라, 적절한 생성자를 한 번만 호출하여 더 효율적입니다.
- 명확성: 모든 멤버를 초기화 리스트에 포함시키면, 어떤 멤버가 초기화되었는지 한눈에 파악할 수 있어 코드 가독성이 향상됩니다.
- 안전성: "초기화 됐을지 안됐을지 고민할 필요가 없다"는 것은 매우 중요한 점입니다. 모든 멤버를 명시적으로 초기화하면 초기화 누락으로 인한 버그 위험이 줄어듭니다.
- 필수 사용 케이스: 상수 멤버와 참조 멤버는 반드시 초기화 리스트를 통해 초기화해야 합니다. 이들은 생성 후 대입이 불가능하기 때문입니다.
그런데 이게 멤버 변수가 너무 많아지다 보면은 초기화 리스트가 주렁주렁 포도덩쿨처럼 길어질 수 있습니다.
그러면 대입으로 초기화가 가능한 아이들을 빼서 함수로 묶는 것도 가능합니다.
class Yeoul {
private:
const int id; // 상수 - 초기화 리스트로만 초기화 가능
int age; // 일반 멤버 - 대입으로 초기화 가능
std::string name; // 대입으로 초기화 가능
double height; // 대입으로 초기화 가능
bool isStudent; // 대입으로 초기화 가능
public:
Yeoul(int personId)
: id(personId) // 상수는 반드시 초기화 리스트에서 초기화
{
initializeDefaultValues(); // 나머지 멤버는 함수로 초기화
}
// 기본값 초기화 함수
void initializeDefaultValues() {
age = 0;
name = "";
height = 0.0;
isStudent = false;
}
};
이 방법은 멤버의 초기값을 파일 OR 데이터베이스에서 읽어올때 유용합니다.
그러나 초기화 리스트를 쓰는 것이 가장 좋습니다!
4. C++에서 변덕스럽지 않은 부분
그것은 바로 객체를 구성하는 데이터의 초기화 순서입니다.
이것은 어떠한 컴파일러를 막론하고 전부 같습니다.
- 기본 클래스는 파생클래스 보다 먼저 초기화
- 클래스 데이터 멤버는 선언된 순서대로 초기화 (초기화 리스트에 넣는 순서도 클래스에 선언한 순서와 동일하게 맞춰주자)
5. 정적 객체 / 번역 단위
비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해집니다.
정적객체 - 프로그램 시작부터 끝날 때까지 살아 있는 객체 (스택, 힙기반 객체 x)
정적 객체 범주
- 전역 객체
- 네임스페이스 유효범위 에서 정의된 객체
- 클래스 안에서 static으로 선언된 객체
- 함수 안에서 static으로 선언된 객체
- 파일 유효범위에서 static으로 정의된 객체
// 1. 전역 객체
int globalVar = 10;
// 2. 네임스페이스 유효범위 객체
namespace MyNamespace {
int nsVar = 20;
}
class MyClass {
public:
// 3. 클래스 안에서 static으로 선언된 객체
static int classStaticVar;
};
// 클래스 정적 변수 정의
int MyClass::classStaticVar = 40;
void myFunction() {
// 4. 함수 안에서 static으로 선언된 객체
static int functionStaticVar = 50;
}
// 5. 파일 유효범위 정적 객체
static int fileStaticVar = 30;
이들중 함수 안에 있는 정적 객체는 지역 정적 객체(local static object)
나머지는 비지역 정적 객체(non-local static object)
라고 합니다.
이 5가지 아이들은 전부 프로그램이 종료되면 사라집니다.
번역 단위(translation unit)
컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드
번역 : 소스 언어를 기계어로
#include하는 파일들 까지 합쳐서 하나의 번역 단위가 된다.
그래서 문제가 어떻게 생기나?
별도로 컴파일된 소스 파일이 2개 이상 있다.
각 소스 파일에 비지역 정적 객체가 한개 이상 있으면 어떻게 될까?
한쪽 번역 단위의 비정적 객체가 초기화 되며 다른쪽 정적 객체를 사용하려 하는데 초기화 되어있지 않을지도 모른다는게 문제다. (순서를 알 수 없기 때문에)
이 순서를 결정하는것은 사실상 불가능에 가깝다.
6. 정적 객체 초기화 순서 문제 해결
비지역 정적 객체를 하나씩 맡는 함수를 만들고 함수 안에서도 정적 객체로 선언한다.
그리고 함수에서는 이들에 대한 참조자를 반환하게 한다.
이제 프로그래머는 비지역 정적 객체를 직접 참조하지 않고 함수를 통해서 호출하게 됩니다.
비지역 정적 객체가 지역 정적 객체가 됨
이게 사실 Singleton Pattern입니다.
지역 정적 객체는 함수 호출중 객체의 정의에 최초로 닿으면 초기화되도록 되어 있다.
결국 함수를 통해서 접근하게 되면 반드시 초기화가 보장되어 있다.
그리고 비지역 정적 객체를 맡는 함수에 접근하지 않으면 초기화 되지 않음으로 초기/소멸 비용도 없습니다.
Lazy Initialization의 개념입니다. 객체가 쓰이기 전엔 초기화 되지 않습니다.
Singleton& getInstance() {
static Singleton instance; // 문제의 지점
return instance;
}
만약 멀티 스레드 환경에서 getInstance를 호출하게 되면 모두 instance가 초기화 되어있지 않다고 판단해서
동시에 객체를 초기화하려고 하는 문제가 생길 수 있습니다.
그래서 eager initialization을 사용할 수 있습니다.
아래처럼 모두 전부 초기화 하고 프로그램을 시작하는 방법입니다.
// 싱글톤 접근 함수들
Database& getDatabase() {
static Database instance;
return instance;
}
Logger& getLogger() {
static Logger instance;
return instance;
}
Config& getConfig() {
static Config instance;
return instance;
}
// 프로그램 시작 시 모든 싱글톤 초기화
void initializeAllSingletons() {
getDatabase(); // 데이터베이스 싱글톤 초기화
getLogger(); // 로거 싱글톤 초기화
getConfig(); // 설정 싱글톤 초기화
}
int main() {
// 프로그램 시작 시 모든 싱글톤을 미리 초기화
initializeAllSingletons();
// 이후 프로그램 실행...
return 0;
}
물론 Lazy Initialization의 이점이 사라집니다.
이것만은 잊지 말자!
- 기본제공 타입의 객체는 직접 손으로 초기화하자 경우에 따라 저절로 되기도 하고 안되기도 하기 때문이다
- 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 초기화하지 말고 멤버 초기화 리스트를 즐겨 쓰자. 초기화 리스트에 데이터 멤버를 나열할 때 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 한다.
- 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 한다. 비지역 정적 객체를 지역 정적 객체로 바꾸어서 해결 가능하다.
항목 5
https://yeoul0714.tistory.com/31
[Effective C++] 항목 5: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자
1. 컴파일러가 저절로 선언하는 함수들 컴파일러는 직접 선언하지 않으면 자동으로 선언해주는 함수들이 있습니다. 복사 생성자생성자복사 대입 연산자소멸자class Yeoul {private: int* data; public: //
yeoul0714.tistory.com
'Effective C++ > Chapter 1: C++에 왔으면 C++의 법을 따릅시다.' 카테고리의 다른 글
[Effective C++] 항목 3: 낌새만 보이면 const를 들이대 보자! (0) | 2025.04.15 |
---|---|
[Effective C++] 항목 2: #define을 쓰려거든 const, enum, inline을 떠올리자 (0) | 2025.04.12 |
[Effective C++] 항목 1: C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2025.04.12 |