본문 바로가기

C++/윤성우의 열혈 프로그래밍 C++

윤성우의 열혈 C++ chpt6.

생성자의 이해

C에선 변수 사용선언과 동시에 초기화를 하거나 필요시 선언 후 값을 대입하는 과정을 사용했음

또는 객체 프로그래밍에서 '생성자라는 개념없는 전제'에선

별도의 함수를 호출을 통해 객체의 해당 멤버 변수의 값을 대입 시켜주는 행위로 변수의 값을 설정해줄 수 있음

 

객체 프로그래밍에서 객체의 멤버 변수를 함수로 값을 설정해준다면

선 선언/후 대입이라는 매번 별도로 값을 대입해줘야 되는 행위의 불편함이 존재하며, 이 불편함을 매 사용시마다 겪어야함

 

이걸 정리 해주는 즉, 객체 멤버 변수의  생성과 초기화를 동시에 자동으로 처리할 수 있게 해주는 함수가 '생성자' 함수

 

생성자객체가 생성될 때 자동으로 호출되는 것으로

(반대로 객체가 소멸될 때 자동으로 호출되는 것은 소멸자라고 한다)

 

C++ 의 생성자는 멤버 변수 선언 시 '명시적인 값이 없을 때'

(기본형 타입 변수(int, double, char 등)들은 초기화 없이 쓰레기 값을 그대로 유지함)

 

생성자에 위치한 초기화 리스트를 통해 값이 정해지지 않은 변수들의 초기화 값을 설정해주거나,

생성자 내부에서 대입을 통해서 값을 할당해주는 역할을 하는 것이 생성자의 역할임

멤버 변수에 '생성자가 존재하는 객체'에 한해서 자동으로 초기화도 이루어준다

 

즉, 사용자가 객체 생성 시 사용자 임의로 해당 객체의 멤버 변수들에게 일일히 값을 설정해주지 않아도 된다는 뜻

 

또한, 생성자는 멤버 객체의 생성자를 호출하는 개념으로써

객체 내의 멤버 변수.. 내의 객체 멤버 변수 .. 내의 객체 멤버 변수 이런식으로

객체의 중첩 구조라면, 제일 하단의 객체 멤버 변수의 초기화부터 진행하면서 상위로 초기화 과정을 진행함.

 

멤버 객체 초기화무조건 '멤버 이니셜라이저'를 통한 '생성자 초기화 리스트'로하는 것이 중요

초기화 리스트를 쓰지 않으면, 객체는 먼저 메모리가 정의된 후 별도의 대입되므로 비효율적인데

초기화 리스트생성자 호출을 직접 트리거하므로 성능적으로도, 명확성 측면에서도 좋다.

// 생성자 본문에서 초기화
class Person {
    std::string name;
public:
    Person(const std::string& n)
    {
        name = n;  // 대입(assignment)
    }
};
// name은 먼저 기본 생성자로 초기화된 후 
// n의 값이 대입(assignment) 되는 구조
// 불필요하게 두 번의 작업(메모리 할당 + 기본 생성 → 복사 대입 = 비효율적)
------------------------------------------------
// 초기화 리스트 사용
class Person {
    std::string name;
public:
    Person(const std::string& n) : name(n) {}  // 직접 초기화
};
// name이 객체 생성과 동시에 n으로 초기화
// 복사 생성자 또는 이동 생성자가 한 번만 호출 (단 한 번의 생성으로 끝)


선언 = 컴파일에 사용을 알리는 행위로 심볼 테이블에 이름 + 타입 등록

 

정의 = 선언된 변수등에 메모리를 할당하는 행위로 컴파일러 또는 링커 단계에서 실제 메모리 레이아웃을 구성

 

초기화 = 변수 정의시 값을 명시 했을 경우 해당 값으로 저장

 

(흔히들 초기화시 값을 지정하지 않으면 '쓰레기 값으로 초기화' 라고들 하지만,

이는 정확히 초기화되지 않은(uninitialized) 상태일 뿐이며

이후 대입 연산으로 값을 주는 행위는 초기화라기 보단 '대입'이다.
즉, 이미 정의된 메모리 공간에 값을 복사하는 행위로 초기화는 엄연히 구분된다)

 

int z; // 선언 + 정의 + 초기화
static x; // 선언 + 정의 + 초기화
extern y; // 선언

int main()
{
    int a; // 선언 + 정의
    int b = 5 // 선언 + 정의 + 초기화
    
    y = 5; // 정의 + 초기화

    reutrn 0;
}

 

 

생성자는 따로 명시하지 않아도 자동으로 생성되는 데, 이 때도 생성자는 클래스와 동일한 이름을 갖는다. 
컴파일러가 컴파일 과정에서 객체내의 생성자 유무를 판단한 후, 생성자의 명시가 없으면 해당 객체의 이름에 대한 심볼을 통해서 동일한 이름의 'default 생성자'를 암시적으로 만듦


이 때, 생성자가 메모리 상에는 객체와 동일한 구역에 존재하지 않지만 심볼을 이용한 '이름 맹글링'을 통해 논리적으로 같은 구역으로 인지하게 함으로써 해당 객체가 실행될 때 호출 가능하겠끔 메커니즘이 정립되어 있음

 

이름 맹글링(Name Mangling)이란?
- C++ 컴파일러가 함수, 변수, 클래스 등의 이름에 타입 정보 등을 추가해서 고유한 심볼 이름으로 변환하는 과정


생성자의 함수적 특징

생성자가 오버로딩이 가능하다는 건 생성자도 둘 이상 만드는 것도 가능하다는 것

즉, 어떠한 객체의 생성자가 다수 존재한다는 것은 해당 객체 생성시의 방법이 다수가 존재한다는 것

일반 객체의 생성자를 호출할 때 생성자에 () 를 사용하면, 

함수의 선언과 겹치기 때문에 컴파일러에서 구분이 불명확(most vexing parse:가장 성가신 구문 해석)해져  에러를 일으킴

SimpleClass 인스턴스 src 의 생성자를 호출한 것인지

SimpleClass 를 반환하는 src 함수를 선언한 것인지가 구분인 안된다는 것

 

(동적 객체는 new 키워드가 함수와 공존할 수 없기 때문에 판별 가능)


Point, Rectangle 클래스에 생성자 적용, 멤버 이니셜라이저 기반의 멤버 초기화

 

멤버 객체를 소유할 경우, 컴파일러는 소유주인 객체의 생성자를 분석하면서 초기화리스트(: 콜론 이후)에 멤버 객체의 이니셜라이저가 명시되어 있는지 확인한다.

 

초기화 리스트에 멤버 객체의 이니셜라이저가 명시되지 않았을 경우 아래와 같이 두 가지 조건에 따라 동작이 나눠진다.

 

1. 컴파일러가 객체 멤버 타입 확인 과정에서 default 생성자의 존재 확인(생성자가 하나도 없을 경우 default 생성자 암시적 생성)

컴파일러는 default 생성자를 호출하는 기계어 코드 소유주 객체의 생성자 진입 직전에 삽입

 

2. 컴파일러가 객체 멤버 타입 확인 과정에서 dafault 생성자의 명시 없이 오버로딩된 생성자만 존재할 경우

컴파일러는 호출할 수 있는 생성자가 없다고 판단하여 컴파일 에러를 발생시킴

 

그러한 이유로 초기화리스트에서 해당 멤버 객체의 생성자 형식에 맞게 인자값을 전달해줘야 한다.

 

위의 코드에서 Rectangle 생성자에 파리미터가 4 개인 이유 또한

Rectangle 객체의 멤버 객체들이 각각 인자 값을 2 개씩 요구하기 때문에 설정된 것이다.

 

 

 

그러면 Rectangle 의 생성자 함수 내에서 멤버 객체의 생성자 호출이 가능한가?

 

정답은 No 이다.

 

앞서 설명했듯, 컴파일러는 Rectangle 클래스의 생성자를 분석하면서 멤버 객체의 타입을 확인하고, 

해당 타입에 적절한 생성자가 존재하는지를 사전에 검토한다.  
만약 생성자가 정의되어 있지 않으면 default 생성자를 암시적으로 생성한다.

 

그 과정에서 default 생성자를 호출하려는 코드 또한 삽입하는데,

Rectangle 생성자 본문에서 멤버 객체의 생성자를 명시하려 한다면,  
이는 이미 생성된 객체에 대해 생성자다시 호출하려는 '행위'로 간주될 수 있다.

그러나 실제 컴파일 에러는 '생성자 중첩 호출' 이 아닌  '존재하지 않는 함수 호출' 이라는 형태로 발생한다.  
이는 클래스 생성자 본문 내 또는 본문 이후에서 멤버객체()와 같이 괄호를 사용할 경우,  
이를 객체 초기화가 아닌 '일반적인 함수 호출' 로 해석하기 때문이다.


이니셜라이저를 이용한 변수 및 상수의 초기화

C++ 에선 일반 타입 변수 또한 (다이렉트)이니셜라이저를 통해서 초기화가 가능하다

단, 이 역시 지역/전역/초기화리스트 내에서만 '변수 정의와 동시에 사용' 할 때 유효하며,

다른 위치에서는  함수 호출로 인식하여 컴파일러가 오류를 발생시킨다.

그리고 초기화리스트에서 =(대입연산) 으로 초기화시 Syntax 에러 처리된다.

 

대입연산 초기화 VS  (다이렉트)이니셜라이저 초기화

 

기본 타입 변수(int, float, char, ...) 에 한해서라면, 이 둘 사이의 차이는 사실상 미미하다고 볼 수 있다.


흔히 이 둘을 구분할 때 다음과 같이 설명하곤 한다.

 

[ 클래스 내에선 ]

"대입 연산객체 생성 이후 즉, 변수의 정의(메모리 할당) 이후 대입이 일어나고,

이니셜라이저는 '객체 생성시' 변수의 정의(메모리 할당) 와 동시에 초기화가 이루어진다"

 

[ 지역변수 등에선 ]

'기본 타입'의 경우엔 이 과정이 실제로 적용되는 방식이 명확하게 드러나지 않는다.

특히 값이 저장되는 과정에서 레지스터를 경유하느냐의 차이가 있다고는 하나,
이 역시 컴파일러의 최적화 설정이나 즉시값 사용여부, 어셈블리 수준에서 상황마다 달라지므로
레지스터 사용 여부를 기준 삼아 명확하게 구분짓는 것도 어렵다.

 

결국, 기본 타입 변수에서는 두 방식 모두 동등한 초기화 방식으로 간주할 수 있으며,
실제 동작상의 차이도 대부분 컴파일러 내부의 구현 세부에 의존한다.
따라서 코드 작성자 입장에선 의미 있는 성능 차이나 동작 차이를 느끼긴 어렵다.

 

단, 위의 예시 코드에서도 보이듯이

const 로 선언된 변수를 초기화할 때 두 방식의 차이가 명확하게 드러난다.

 

대입 연산 변수의 정의 이후 따로 대입 연산을 시도하기 때문에

불변(const) 속성을 가진 메모리에 값을 할당하려 하게 되어 컴파일 오류가 발생한다.

 

반면, 이니셜라이저는 변수의 정의와 동시에 초기화가 이루어지므로,

불변 속성을 가지는 메모리 공간에 값을 직접 초기값으로 설정할 수 있다.

 

따라서 const 변수의 경우에는 반드시 이니셜라이저 방식을 사용해야 하며,
이것이 두 초기화 방식 간의 의미 있는 차이가 실제로 존재하는 대표적인 예라 할 수 있다.

 

사용시 주의점

// 함수 | 메인 내 지역 변수 초기화

int a; // 선언 + 정의
int b; // 선언 + 정의
int c(5) // 선언 + 정의 + 초기화
int d = 7 // 선언 + 정의 + 초기화

a = 8; // 대입

b(9) //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 에러!!!!!!!!!!!!!

---------------------------------

// 클래스 멤버 변수 초기화

class test
{
private:
    int c; // 선언
    int x; // 선언
    int y; // 선언
    int z; // 선언
public:     
     // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 생성자부턴 객체(인스턴스) 생성시 동작
     // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 객체 생성시 c, x, y, z 의 메모리 정의
    test() : x(4), // 초기화
             c = 5  // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 에러!!!!!!!!!!!    
    {
        y = 6; // 대입
        z(7); // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 에러!!!!!!!!!!!  
    }
}

 


멤버 변수로 참조자 선언하기

이니셜 라이저의 초기화는 객체 생성시 정의(메모리 할당)와 동시에 초기화되는 형태이므로, 참조자의 초기화도 가능하다.

(참조자는 반드시 정의(메모리 할당) + 초기화가 같이 명시되어야 함, 초기화 값이 없으면 에러)

 

만약 생성자 내부 또는 이후에 참조값을 초기화 하려하면, 참조자 ref 는 그전에 이미 정의(메모리 할당)가 이뤄지기 때문에 초기화 값이 전달되지 않으므로 컴파일러에서 에러를 발생시킴


private 생성자

class AAA
{
private:
    int num;
public:
    AAA() : num(0) {}
    AAA& CreatInitObj(int n) const // 객체의 참조를 반환하는 const 멤버 함수, 객체의 멤버 변수 수정불가
    {
        AAA * ptr = new AAA(n);  // private 생성자 호출 가능
        return *ptr;
    }
private:
    AAA(int n) : num(n) {}  // 생성자가 private 이므로 클래스 외부에서는 호출불가
};

 

AAA 클래스의 멤버함수 내에서도 AAA 클래스의 객체 생성이 가능하다
생성자가 private 라는 것은 외브에서의 객체 생성을 허용하지 않겠다는 뜻

 


소멸자의 이해

소멸자는 오버로딩을 할 수 없음

즉, 하나의 객체에는 하나의 소멸자만 존재

 

소멸자의 활용

 


생성자와 소멸자와 C++ 도입 배경


C++ 의 창시자 Bjarne Stroustrup 이 직접 쓴  < The Design and Evolution of C++ > 책의 내용에

Bjarne Stroustrup 이  영국 케임브리지 대학교 CAP 컴퓨터와 운영 체제에서 비롯된 박사 학위 논문 작업을 수행하며

분산 시스템을 위한 시스템 소프트웨어 구성 대안을 연구하였으며

과정에서 분산 시스템에서 실행되는 소프트웨어를 시뮬레이션하기 위한 시뮬레이터를 Simula 언어로 설계하고 구현하였음

 

이 Simula 시뮬레이터 작업을 통해 그는 Simula의 프로그램 조직 기능(클래스, 타입 시스템 등)에 깊은 인상을 받았지만,

Simula의 구현 자체에서 심각한 문제점들을 인지함

  • 성능 문제
  • 메모리 관리(가비지 컬렉션이 시뮬레이터 실행 시간의 80% 이상을 소비) 문제
  • 별도 컴파일(separate compilation) 지원 부족
  • 다른 언어로 작성된 코드와의 링크 문제 

이러한 문제들로 인해 Simula 구현이 대규모 시스템 구축에는 적합하지 않다는 결론에 도달하여 시스템 프로그래밍을 위한 적합한 도구의 필요성을 절감하였으며,

UNIX 커널 분석 및 모듈화라는 구체적이고 실질적인 문제에 직면하여

Simula의 프로그램 조직 능력과 C의 효율성결합한 새로운 언어, 즉 "C with Classes"를 설계하게 되었음.

 

이라고 C++ 의 도입 배경을 서술해뒀으며,


2.11.1 Constructors and Destructors  섹션에서 

더보기

One way I often explained the concept at the time was that a 'new function' (a constructor) created the environment in which the member functions would operate and the 'delete function' (a destructor) would destroy that environment and release all resources acquired for it.3 At the time, mid-1979, neither the modesty nor the preposterousness of that goal was realized. The goal was modest in that it did not involve innovation, and preposterous in both its time scale and its Draconian demands on efficiency and flexibility.4 The new operator was called new because that was the name of the corresponding Simula operator. The new operator invokes some allocation function to obtain memory and then invokes a constructor to initialize that memory. The combined operation is often called instantiation or simply object creation; it creates an object out of raw memory. However, the notational convenience offered by operator new is significant (§3.9). However, error reporting mechanisms led to some practical problems. Handling and reporting errors in constructors was rarely critical, though, and the introduction of exceptions (§16.5) provided a general solution.

라고 서술되어 있는데, 요약하자면

생성자(Constructors)
  • 객체 생성 시 초기화를 보장하기 위해 도입, 객체 생성 시 메모리 할당생성자 호출을 모두 수행하여 초기화를 보장하는 new 연산자는 Simula에서 아이디어를 빌려온 것.
  • 생성자는 멤버 함수가 작동할 환경을 설정하는 역할로 당시의 'new function'(생성자)이 멤버 함수가 작동할 환경을 만든 역할을 오늘날 new 로 사용
  • 초기화 보장은 Simula 구현의 특징이었으며, C++의 설계에서도 중요하게 고려되었음.
소멸자(Destructors)
  • 가비지 컬렉션에 의존하지 않기 위해 생성자의 필수적인 보완 기능으로 도입되었습니다.
  • 'delete function'(소멸자)은 그 환경을 파괴하고 객체가 획득한 모든 리소스를 해제하는 역할을 합니다.
  • 책에서는 1985년 이전 자동 가비지 컬렉션이 여러 차례 고려되었으나, 실시간 처리 및 장치 드라이버와 같은 하드코어 시스템 작업에는 부적합하다고 판단되었음을 명시

 

사실 소스코드 작성하는 순간에는 생성자와 소멸자의 존재감이 크게 와닿지는 않음.

그럴 수 밖에 없는게, 이 둘은 '작성자' 관점이 아닌 '사용자' 편의를 기준으로 설계되었기 때문.

 

간단하게 설명하자면

 

C 와 C++ 의 구조체 생성 과정에서 멤버 변수에 동적 할당을 한다는 가정을 한다면,

 

C 의 경우 '사용자'는 해당 멤버 변수를 어디쯤에서 free 시켜야 되는지 고민을 해야되고, 직접 명시를 해줘야 됨

 

하지만

 

C++ 에선 별도의 delete 위치를 생각하거나 명시적 호출할 필요없이 해당 구조체(객체)가 사용된 스코프가 종료되면  자동으로 소멸자가 호출되어 객체 내부에 명시된 delete 를 실행해줌

 

물론 객체의 생성/소멸 과정에서의 효율성이 생겼을 뿐, 소스 코드의 본문 영역에서 new 사용시 delete 호출기존 C 와 동일하게 적용됨

 

'C++ > 윤성우의 열혈 프로그래밍 C++' 카테고리의 다른 글

윤성우의 열혈 C++ chpt8.  (0) 2025.07.02
윤성우의 열혈 C++ chpt7.  (0) 2025.06.26
윤성우의 열혈 C++ chpt5.  (0) 2025.05.16
윤성우의 열혈 C++ chpt4.  (0) 2025.05.12
윤성우의 열혈 C++ chpt3.  (0) 2025.05.12