멤버 변수 초기화

생성자 본문에서 대입

생성자 내부에서 대입 연산자 = 를 사용해 클래스 멤버 변수에 값을 저장할 수 있다. 생성자가 실행 될때 멤버 변수가 생성되며 본문에서 멤버 변수에 값이 대입된다.

#include <iostream>

class Example
{
public:
	Example()
	{
		// 생성자 본문에서 대입
		Value1 = 1;
		Value2 = 2;
		Value3 = 3;
	}

	void PrintInfo()
	{
		std::cout << Value1 << std::endl;
		std::cout << Value2 << std::endl;
		std::cout << Value3 << std::endl;
	}

private:
	int Value1;
	int Value2;
	int Value3;

};

int main(void)
{
	Example NewExample = Example();
	NewExample.PrintInfo();

	return 0;
}
1
2
3

그러나 위 방법의 경우 선언과 동시에 초기화 되어야하는 const 상수 또는 참조 변수에는 사용할 수 없다.

멤버 이니셜 라이저 (Member Initializer)

멤버 이니셜 라이저를 이용해 클래스 멤버 변수를 초기화 할 수 있다.

멤버 이니셜 라이저를 이용하면 선언과 동시에 초기화가 이루어지는 형태로 바이너리 코드가 생성되어 생성자 내에서 대입 연산을 이용한 초기화보다 성능이 조금 향상된다.

#include <iostream>

class Example
{
public:
	Example(int v1, int v2, int v3)
		: Value1(v1), Value2(v2), Value3(v3) // 멤버 이니셜라이저
	{

	}

	void PrintInfo()
	{
		std::cout << Value1 << " ";
		std::cout << Value2 << " ";
		std::cout << Value3 << " ";
	}

private:
	int Value1;
	int Value2;
	const int Value3;

};

int main(void)
{
	Example NewExample = Example(1, 2, 3);
	NewExample.PrintInfo();

	return 0;
}
1 2 3

기본 초기값 할당

클래스의 멤버 변수에 직접 기본 초기값을 할당할 수도 있다.

#include <iostream>

class Example
{
public:
	void PrintInfo()
	{
		std::cout << Value1 << " ";
		std::cout << Value2 << " ";
		std::cout << Value3 << " ";
	}

private:
	int Value1 = 1;
	int Value2 = 2;
	const int Value3 = 3;

};

int main(void)
{
	Example NewExample = Example();
	NewExample.PrintInfo();

	return 0;
}
1 2 3

이 방법과 멤버 이니셜 라이저를 동시에 사용할 경우 멤버 이니셜 라이저가 우선시 된다.

#include <iostream>

class Example
{
public:
	Example(int v1, int v2, int v3)
		: Value1(v1), Value2(v2), Value3(v3) // 멤버 이니셜라이저
	{

	}

	void PrintInfo()
	{
		std::cout << Value1 << " ";
		std::cout << Value2 << " ";
		std::cout << Value3 << " ";
	}

private:
	int Value1 = 0;
	int Value2 = 0;
	const int Value3 = 0;

};

int main(void)
{
	Example NewExample = Example(1, 2, 3);
	NewExample.PrintInfo();

	return 0;
}
1 2 3

this 포인터

C++ 에서 동일한 클래스에서 생성된 객체들은 각각 자신만의 멤버 변수를 가지지만, 멤버 함수의 경우 모든 객체가 함수를 공유하게 된다. 이때 멤버 함수는 어느 객체에서 작동해야 하는지를 알아야 한다.

아래의 코드를 살펴보면 Example 클래스의 PrintValue 함수는 인자로 아무것도 받지 않지만 자신을 호출한 객체의 멤버 변수를 참조하는 것을 확인할 수 있다.

#include <iostream>

class Example
{
public:
	Example(int _V, std::string _Name)
		: Value(_V), Name(_Name)
	{}

	void PrintInfo()
	{
		std::cout << Name << std::endl;
		std::cout << "Value : " << Value << std::endl;
	}

private:
	std::string Name = "";
	int Value = 0;
};

int main(void)
{
	Example NewExample1 = Example(5, "NewExample1");
	Example NewExample2 = Example(10, "NewExample2");

	NewExample1.PrintInfo();
	NewExample2.PrintInfo();

	return 0;
}
NewExample1
Value : 5
NewExample2
Value : 10

여기에 대한 정답은 this 라는 숨겨진 포인터에서 찾을 수 있다.

this 포인터란 멤버 함수가 호출된 객체의 주소를 가리키는 포인터로 호출된 멤버 함수의 숨은 인수로 전달된다. 이를 통해 호출된 멤버 함수는 자신을 호출한 객체가 무엇인지 알 수 있다.

#include <iostream>

class Example
{
public:
	void Address()
	{
		std::cout << this << std::endl;
	}
};

int main(void)
{
	Example NewExample1 = Example();
	NewExample1.Address();
	std::cout << &NewExample1 << std::endl;

	return 0;
}
0000007E086FF764
0000007E086FF764

this 포인터는 다음과 같은 특징을 갖는다.

숨겨진 this 포인터

객체의 멤버 함수가 호출될 때 C++ 컴파일러는 컴파일 과정에서 멤버 함수의 첫 번째 인수로 호출한 객체의 주소를 전달한다.

#include <iostream>

class Example
{
public:
	Example(int _V)
		: Value(_V)
	{}

	void PrintValue()
	{
		std::cout << Value << std::endl;
	}

private:
	int Value = 0;
	
};

int main(void)
{
	Example NewExample = Example(5);
	NewExample.PrintValue();

	return 0;
}
5

위의 코드에서 멤버 함수 PrintValue 는 인수 없이 호출되는 것처럼 보이지만 실제로는 한 개의 인수를 가지고 있다. 컴파일러는 컴파일 과정에서 위 코드를 다음과 같이 변환한다.

NewExample.PrintValue(&NewExample);

멤버 함수의 첫 번째 인수로 객체의 주소가 전달되게 된다. 따라서 멤버 함수의 정의 부분도 컴파일러에 의해 다음과 같이 변환된다.

void PrintValue(Example* const this)
{
	cout << this->Value << endl;
}

즉, 클래스의 멤버 함수는 호출 시 컴파일러에 의해 멤버 함수가 호출된 객체의 주소를 가리키는 숨겨진 this 포인터를 첫 번째 인자로 받아 호출된 객체의 멤버 함수를 참조할 수 있게 된다.

위의 함수 호출에서 컴파일러 수행 과정을 요약하면 다음과 같다.

  1. NewExample.PrintValue() 함수를 호출하면 컴파일러는 NewExample.PrintValue(&NewExample) 로 변환해서 호출한다.
  2. PrintValue 멤버 함수 내부에서 this 포인터는 NewExample 객체의 주소를 가리킨다.
  3. PrintValue 내부의 모든 멤버 변수 앞에는 this-> 가 붙는다.

static 멤버

클래스의 static 멤버는 다음과 같은 특징을 갖는다.

static 멤버 변수

#include <iostream>

class A
{
private:
	static int Value; // static 멤버 변수
};

int main()
{
	A A1 = A();
	A A2 = A();
	A A3 = A();

	return 0;
}

위의 코드에서 A1, A2, A3 객체는 static 멤버 변수 Value 를 공유한다. 그렇다고 객체 내부에 Value 가 존재하는 것은 아니며 Value 는 객체의 외부에 존재하고, 객체에게 멤버 변수처럼 접근권한을 주는 것이다.

image.png

static 멤버 변수는 프로그램 시작 시 할당되는 변수이다. 따라서 static 멤버 변수의 선언을 전역 공간에 한번 더 해야한다.

#include <iostream>

class A
{
public:
	static int Value; // 선언
};

// 선언
int A::Value = 10; // 10으로 초기화

int main()
{
	// 객체 생성 없이도 접근이 가능
	A::Value++;

	A A1 = A();
	std::cout << A1.Value << std::endl;

	return 0;
}
11

static 멤버 함수

클래스의 static 멤버 함수는 위의 특징과 더불어 다음과 같은 특징을 갖는다.

#include <iostream>

class A
{
public:
	A()
	{
		Count++;
	}

	// static 멤버 함수
	static void Count_A()
	{
		std::cout << "생성된 클래스A : " << Count << std::endl;
		//Val++; // 에러
		//this   // 에러
	}

private:
	int Val = 0;
	static int Count;
};

int A::Count = 0;

int main()
{
	// 객체 생성 없이도 접근이 가능
	A::Count_A();

	A A1 = A();
	A A2 = A();

	A::Count_A();

	return 0;
}
생성된 클래스A : 0
생성된 클래스A : 2