• 8.13.1 익명 객체(Anonymous Objects)

8.13.1 익명 객체(Anonymous Objects)

 

객체에 특정 이름을 붙여서 인스턴스화를 하지 않고 멤버 함수를 사용할 수 있는 방법이 존재하는데 바로 익명 객체를 사용하는 것이다.

#include <iostream>

using namespace std;

class A
{
public:
	int m_value;

	A(const int& input)
		: m_value(input)
	{
		cout << "Constructor" << endl;
	}

	~A()
	{
		cout << "Destructor" << endl;
	}

	void print()
	{
		//cout << "Hello" << endl;
		cout << m_value << endl;
	}
};

int main()
{
	A a(3);
	a.print();

	A(1).print(); // 생성자 소멸자 호출
	A(2).print(); // 생성자 소멸자 호출

	return 0;
}

위의 코드를 실행시켜보면 두 번째와 세 번째 멤버 함수를 호출할 때 생성자와 소멸자를 호출하는 것을 볼 수가 있다. 첫 번째 멤버 변수의 경우 인스턴스화를 하면서 변수에 이름을 짓게 되는데, 그 외의 함수를 호출할 때는 그러지 않았다. 이처럼 익명 객체를 사용하게 되면 객체를 생성해서 멤버 함수를 사용하고 바로 파괴하는 과정을 거치게 된다.

 

#include <iostream>

using namespace std;

class Cents
{
private:
	int m_cents;

public:
	Cents(int cents) 
	{
		m_cents = cents;
	}
	
    	// 멤버 변수를 변경하지 않기 때문에 const가 붙는다.(멤버 함수에 붙는다.)
	int getCents() const
	{
		return m_cents;
	}
};

Cents add(const Cents& c1, const Cents& c2)
{
	return Cents(c1.getCents() + c2.getCents());
}

int main()
{
	cout << add(Cents(6), Cents(8)).getCents() << endl;

	return 0;
}

이런 방식으로 익명 객체를 활용할 수도 있겠다. 함수 뒤에 const는 멤버 함수가 멤버 변수를 변경하지 않는다는 의미를 컴파일러에게 표시해주는 것으로 유지보수에 도움이 되니 기억해두도록 하자! 참고로 개발자 면접에도 간혹 나오는 개념이다!

  • 8.2.1 캡슐화(Encapsulation)
  • 8.2.2 접근 지정자(Access Specifiers)
  • 8.2.3 접근 함수(Access Functions or Getter/Setter)
복잡해 보이는 것을 깔끔하게 정리를 하면 뛰어난 프로그래머가 될 수 있다.

8.2.1 캡슐화(Encapsulation)

 

객체지향 프로그래밍이 가지는 장점 중 하나가 바로 캡슐화이다. 클래스를 사용하면서 멤버 변수와 멤버 함수를 사용하게 되는데, 이들에 대해서 접근 권한을 지정할 수가 있다. 클래스 내부에서만 접근 가능하게 구현한다던가, 어디서든지 접근 가능하게 구현한다던가, 추후에 배울 상속 관계까지만 접근이 가능하도록 할 것인가. 이러한 경우들이 바로 캡슐화에 속하는 이야기이다.

 

캡슐화를 하는 이유 중 하나는 바로 복잡하고 큰 프로그램일수록 유지보수가 뛰어난 코드로 만들 수 있다는 점이다. 아래의 코드를 예시로 보자.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Date
{
public:
	int m_month;
	int m_day;
	int m_year;

public:
	void setDate(const int& month_input, const int& day_input, const int& year_input)
	{
		m_month = month_input;
		m_day = day_input;
		m_year = year_input;
	}
};

int main()
{
	Date today;
	today.setDate(8, 4, 2025);


	cout << today.m_month << endl;
	today.m_month = 1;
	today.m_month = -100

	return 0;
}

현재 코드는 클래스로 구현을 해서 멤버 함수와 멤버 변수가 모여있지만 어디서든지 접근을 할 수 있도록 접근 권한을 낮춰두었다. main 함수에서도 Date 클래스로 생성한 인스턴스인 today에 대한 멤버 변수를 무작위로 수정할 수 있다. 지금은 코드가 짧아서 모든 상황을 한눈에 볼 수 있지만 실무에서 다루는 코드는 수백수천 줄에 달하며 여러 코드들이 모여있다. 이렇게 어디서든지 중구난방으로 접근하게 둔다면 추후에 클래스 멤버를 수정할 일이 생기면 여러 곳에서 문제가 생기게 된다.


8.2.2 접근 지정자(Access Specifiers)

 

앞의 상황을 해결해주기 위해 접근 지정자를 사용하여 캡슐화를 제대로 사용할 수 있다. 흔히 알고 있는 감기 알약 같은 경우 우리는 감기를 견뎌내기에 좋은 기능을 하는 정도만 알지 실제로 캡슐이 어떤식으로 몸속에서 작용하는지는 알지 못한다. 이처럼 결과적인 것만 알게 해주고 내부를 감추는 것이 캡슐화의 특성 중 하나가 되며, 이를 프로그래밍적으로 구현한 것이 접근 지정자이다.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Date

{
	int m_month;
	int m_day;
	int m_year;

public:
	void setDate(const int& month_input, const int& day_input, const int& year_input)
	{
		m_month = month_input;
		m_day = day_input;
		m_year = year_input;
	}
};

int main()
{
	Date today;
    
	today.setDate(8, 4, 2025);

	return 0;
}

이전 코드에서도 사용했지만 클래스 내부에 public 키워드가 바로 접근 지정자이다. public으로 지정할 경우 그 아래의 멤버들은 전부 해당 클래스의 인스턴스에 접근할 수 있는 코드 내에서 모두 사용할 수 있게 된다.

 

반면 클래스의 멤버 변수는 public 키워드를 작성해주지 않았는데 이 경우 자동으로 private 키워드로 설정된다. 클래스가 구조체와 다른점 중 하나가 바로 기본 설정이 private 키워드라는 점이다. 키워드 이름에서도 알 수 있듯이 접근에 제한이 생기는데 해당 클래스에 속하는 멤버 이외에는 private 멤버에 접근할 수 없게 된다. 같은 클래스 멤버라면 public이라도 private 멤버에 접근할 수 있다.

 

그럼 접근 제한이 걸린 멤버는 외부에서 어떻게 활용할 수 있을까?


8.2.3 접근 함수(Access Functions or Getter/Setter)

 

멤버 변수에 대한 세팅은 setter라 불리는 접근 함수로 구현을 한다. 외부에서 매개변수로 값들을 받아 필요한 멤버 변수를 설정해주는 것이다. 추후에 배울 생성자를 통하여 해당 기능을 초기화하는 것처럼 구현할 수 있다.

#include <iostream>
#include <string>

using namespace std;

class Date
{
	int m_month;
	int m_day;
	int m_year;

	void setDate(const int& month_input, const int& day_input, const int& year_input)
	{
		m_month = month_input;
		m_day = day_input;
		m_year = year_input;
	}

	const int& getDay() // getters
	{
		return m_day;
	}
    
	void copyFrom(const Date& original)
	{
		m_month = original.m_month;
		m_day = original.m_day;
		m_year = original.m_year;
	}
};

int main()
{
	Date today;
    
	today.setDate(8, 4, 2025);

	cout << today.getDay() << endl;
    
	Date copy;
    
	copy.copyFrom(today);

	cout << copy.getDay() << endl;
    
	return 0;
}

setDate 함수를 통하여 멤버 변수를 설정해주었다. 하지만 멤버 변수는 private이기 때문에 외부에서 해당 값을 들여다보지 못한다. 이를 위해서 getter라고 부르는 접근 함수를 멤버 변수로 만들어 준다. 결론적으로 getDay라는 함수를 통해서 private 멤버 변수인 m_day의 값을 불러올 수 있다.

 

getDay 함수는 값을 복사하도록 int로 return 값을 설정할 수 있지만 복사하는 것이 마음에 들지 않는다면 const와 참조 연산자 &를 함께 사용하여 return 값으로 설정해주자. 참조 연산자를 통해 해당 변수에 직접 접근이 가능해지지만 이는 private 키워드를 무시하게 되는 효과를 가져오므로 const를 통해서 바꾸는 것을 차단하는 것이다.


또한 같은 클래스에서 생성된 인스턴스의 경우 자신의 멤버 함수로 다른 인스턴스에 대한 private 멤버들에 접근을 할 수 있게 된다.

  • 8.1.1 객체지향 프로그래밍이 아닌 코드의 경우
  • 8.1.2 객체지향 프로그래밍
  • 8.1.3 인스턴스(instance)

8.1.1 객체지향 프로그래밍이 아닌 코드의 경우

 

객체지향 프로그래밍이 아닌 기존의 C/C++ 코드를 보고 객체지향 프로그래밍으로 넘어가 보자. 친구에 대한 다양한 정보(이름, 주소, 나이, 키, 몸무게)를 저장하고 출력하고자 한다. 이럴 경우 가장 가볍게 생각할 수 있는 것이 구조체이다. 단순히 친구 한 명이라면 main 함수에 각 정보를 담을 메모리를 할당하고 출력하면 되겠지만 친구가 여러 명이 생길 수 있을 것이라 생각하자.

#include <iostream>
#include <string>
#include <vector>
using namespace std;


struct Friend
{
	string	name;
	string	address;
	int		age;
	double	height;
	double	weight;

};

void print(const Friend& fr)
{
	cout << fr.name << " " << fr.address << " "
		<< fr.age << " " << fr.height << " " << fr.weight << endl;
}

int main()
{
	Friend jj{ "Jack Jack", "Uptown", 2, 30, 10 };

	print(jj);

	return 0;
}

이처럼 구조체를 하나 만들어서 초기화하고 출력 함수를 구현할 수 있겠다.

 

여기까지만 해도 나름 깔끔하게 잘 만들었지만 print 함수가 너무 지저분해 보인다. 이를 줄이고 싶어서 구조체 안에 print 함수를 넣어서 정리한다.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

struct Friend
{
	string	name;
	string	address;
	int		age;
	double	height;
	double	weight;

	void print()
	{
		cout << name << " " << address << " " 
			<< age << " " << height << " " << weight << endl;
	}
};

int main()
{
	Friend jj{ "Jack Jack", "Uptown", 2, 30, 10 };

	jj.print();

	return 0;
}

이제 print 함수의 매개변수도 사라지고 출력 부분에서 구조체 내부의 데이터에 접근하기 위하여 불필요한 코드도 사라졌다. 이 정도면 나름 잘 정리한 것 같은데...?


8.1.2 객체지향 프로그래밍

 

이제 C++에서 말하는 객체지향 프로그래밍의 기본적인 구조를 한번 살펴보자. 데이터와 기능이 묶여 있는 것을 객체라고 말한다. 이런 객체(object)를 프로그래밍으로 구현한 것이 class이다.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Friend
{
public:
	string	name;
	string	address;
	int		age;
	double	height;
	double	weight;

	void print()
	{
		cout << name << " " << address << " " 
			<< age << " " << height << " " << weight << endl;
	}
};

int main()
{
	Friend jj{ "Jack Jack", "Uptown", 2, 30, 10 };

	jj.print();

	return 0;
}

이전 코드 블럭과의 차이점은 두 가지다. struct 키워드 대신 class 키워드를 썼다는 점, class 시작 부분에 public:이라는 키워드가 붙었다는 점이다.

 

많은 사람들이 처음 클래스를 접하게 되면 구조체로도 비슷하게 구현이 가능한데 굳이 클래스로 구현해야 하는 것인가라는 의문점을 가지게 된다. 클래스의 경우 public이라는 access specifier라는 것 덕분에 많은 추가적인 이점을 가질 수가 있다. 구조체는 기본적으로 public이지만 클래스의 경우 private이나 protected로 설정이 가능하다. 이 부분은 다음 포스팅에서 다루도록 하겠다.

 

일반적으로 프로그래밍을 하면 구조체는 데이터만 담고 클래스에는 데이터와 기능을 함께 담는다.


8.1.3 인스턴스(instance)

 

객체지향 프로그래밍에 대해 다루다 보면 인스턴스와 클래스의 개념이 혼동될 수 있다. 객체는 데이터와 기능을 담아두어서 추상화시킨 구조일 뿐 물리적으로 메모리를 지니고 있는 존재는 아니다. 반면 인스턴스는 물리적 메모리 주소를 할당받은 것이고 구조체가 지닌 데이터나 기능을 사용할 수 있는 존재이다.

int main()
{
	Friend jj{ "Jack Jack", "Uptown", 2, 30, 10 };

	jj.print();

	return 0;
}

이 코드에서 jj가 인스턴스이다. 이처럼 메모리를 가지기 시작하는 것을 instanciation이라고 부른다.

열거형 타입 enum에 대해 이전에 포스팅하였는데, 편리하지만 생각보다 단점이 많이 존재한다. 이를 어느 정도 해소시켜줄 수 있는 것이 바로 열거형 클래스 enum class이다. 영역 제한 열거형이라고도 부르며 enum struct도 존재하지만 주로 enum class만 사용한다.


enum이 발생시키는 상황

 

enum은 마치 #define 매크로처럼 제한이 없이 넓은 영역에 영향을 미친다고 볼 수 있다. 서로 다른 enum일지라도 이름을 같게 해서는 안되며, 서로 다른 이름에 다른 enum일지라도 같은 값을 가지면 조건문에서 쓸 때 같다고 판단해버린다.

#include <iostream>

int main()
{
	using namespace std;

	enum Color
	{
		RED,
		BLUE
	};

	enum Fruit
	{
		BANANA,
		APPLE
	};

	Color color = RED;
	Fruit fruit = BANANA;

	if(color==fruit)
		cout << "Color is fruit ? " << endl;

	return 0;

}

위의 코드를 실행하면 if문에서 참으로 판단하고 출력을 수행한다.


enum class

 

enum class를 사용하면 애초에 위와 같은 조건문을 사용할 수 없도록 막아버린다. 아래는 enum class로 변경한 코드이다.

#include <iostream>

int main()
{
	using namespace std;

	enum class Color
	{
		RED,
		BLUE
	};

	enum class Fruit
	{
		BANANA,
		APPLE
	};

	Color color = Color::RED;
	Fruit fruit = Fruit::BANANA;
	
	if(color==fruit)
		cout << "Color is fruit ? " << endl;

	return 0;
}

위의 코드는 실행되지 않는다. 실행할 경우 다음과 같은 에러 메시지가 뜬다.


Error C2676 binary '==': 'main::Color' does not define this operator or a conversion to a type acceptable to the predefined operator

서로 다른 클래스의 멤버이기 때문에 막혀버린다.

if (static_cast<int>(color) == static_cast<int>(fruit))

물론 조건문을 위처럼 static_cast를 사용하여 int로 변경한 뒤 사용할 수 있겠지만 좋은 생각은 아니다.

 

enum class를 통해 같은 enum class 내부에 있는 값들로만 비교할 수 있게 되므로 휴먼 에러를 줄일 수 있는 방법이 되겠다.

+ Recent posts