C++/Class

C++ Classes

Soul-Learner 2016. 12. 23. 11:32

C++ 프로그래밍, 클래스 ( Classes )


C++에서 클래스는 구조체와 유사한 형태를 하고 있으며, 구조체가 확장되어 클래스라는 개념으로 발전된 것이다. 

클래스 안에 선언할 수 있는 것(Members)

  • Constructors (생성자)
  • Destructors (소멸자)
  • Member Variables (멤버변수)
  • Member Functions (멤버함수)

클래스가 반드시 위의 모든 것으로 채워져야 하는 것은 아니며, 클래스 안에 아무것도 없어도 되고 위의 어느 한개만 있어도 되고 모두 있어도 된다. 필요에 따라서 선언하고 사용하면 되는 것이다


접근 제한자 ( Access Specifiers ) : 클래스 멤버에 대한 외부접근 제한 정도를 나타내는 3개의 키워드

접근 제한자를 사용하지 않은 멤버는 디폴트로 private

  • private : 현재 클래스와 friend 만 접근 가능
  • protected : 현재 클래스, 자식 클래스, friend 가 접근 가능
  • public : 모든 클래스에서 접근 가능. 접근제한 없음


클래스 사용 예

#include <iostream>
#include <locale>
#include <string>

using namespace std;

class CUser
{
	int num; // private 멤버변수
	wstring name;

	public: // 아래는 모두 public 멤버함수

	void print_user(); // 선언부: 클래스 밖에 정의부가 있어야 함

	//inline 함수 선언(선언과 정의가 클래스 블럭 안에 있으면 자동으로 인라인 함수가 됨)
	void set_num(int n){
		num = n;
	}
	void set_name(wstring nm){
		name = nm;
	}
	int get_num() {
		return num;
	}
	wstring get_name() {
		return name;
	}
};

// 멤버함수의 정의부(반드시 함수의 리턴타입을 다시 선언해야 함)
// 전역함수와 구별하기 위해 '::'(scope operator)를 사용하여 해당 클래스를 지정함
void CUser::print_user(){
	wcout << "num=" << num << endl;
	wcout << "name=" << name << endl;
}

// 전역함수(Global Functions): CUser 클래스의 멤버가 아님
void change(CUser& u){
	u.set_num(20);
}

// 전역함수
int main()
{
	setlocale(LC_ALL,"");
	wcout << L"C++ 클래스 사용하기" << endl;

	CUser user;              // 클래스의 객체 생성
	user.set_num(11);        // 객체의 초기화
	user.set_name(L"김인철");

	change(user);            // 전역함수 호출
	user.print_user();       // 멤버함수 호출

    return 0;
}


생성자( Constructors )의 필요성

위의 예에서는 main 함수 내에서 CUser 클래스의 객체를 생성하고 set 함수를 호출하여 객체를 초기화한 후에 print_user 함수를 호출했기 때문에 객체의 내용이 제대로 출력되는 것을 확인할 수 있다. 그러나 set_num(), set_name()을 호출하기 전에 print_user()를 호출한다면 아무것도 출력되지 않을 것이다. 이런 경우에는 객체를 생성하고 set 함수를 호출하지 않아도 객체의 멤버 변수들이 초기화된다면 좋겠다.

객체를 생성할 때 지정된 값으로 객체가 최기화될 수 있도록 객체 생성시 자동으로 호출되는 특별한 함수와 같은 생성자(Constructor)을 정의할 수 있다. 클래스 안에 생성자를 선언하면 객체가 생성될 때 자동으로 생성자가 호출되어 실행되므로 생성자의 파라미터에 값을 전달하여  멤버변수를 초기화할 수 있다. 생성자는 클래스이름가 동일해야 하며 리턴타입도 없어야 한다. void 라는 키워드도 사용해서는 안된다. 또 한 생성자는 개발자가 명시적으로 호출할 수 없다.

생성자를 사용하여 멤버변수를 초기화하는 예

#include <iostream>
#include <locale>
#include <string>

using namespace std;

class CUser
{
	int num; // private 멤버변수
	wstring name;

	public: // 아래는 모두 public

	CUser(int,wstring); // 생성자(Constructor)

	void print_user(); // 선언부: 클래스 밖에 정의부가 있어야 함
};

// 생성자 정의(클래스명과 동일, 리턴타입 있으면 생성자 아님)
CUser::CUser(int n, wstring nm) {
	num = n;
	name = nm;
}

// 멤버함수의 정의부(반드시 함수의 리턴타입을 다시 선언해야 함)
// 전역함수와 구별하기 위해 '::'(scope operator)를 사용하여 해당 클래스를 지정함
void CUser::print_user(){
	wcout << "num=" << num << endl;
	wcout << "name=" << name << endl;
}

// 전역함수
int main()
{
	setlocale(LC_ALL,"");
	wcout << L"C++ 클래스 사용하기" << endl;

	CUser user(11, L"김인철");   //클래스의 객체 생성, 생성자 자동호출됨

	user.print_user();       // 멤버함수 호출

    return 0;
}


소멸자(Destructors)의 필요성

소멸자는 객체가 메모리에서 제거될 때 자동으로 호출되는 특별한 함수이다. 생성와 같이 클래스 이름과 동일해야 하며 이름 앞에 ~ 기호를 붙여서 생성자와 구별한다. 소멸자는 주로 클래스 안에서 사용한 동적 메모리를 해제하는 등의 더이상 사용하지 않는 리소스를 해제하는 용도로 사용된다. 현재 클래스에서 동적 메모리를 사용했다면 현재 객체와 함께 자동으로 해제되지 않으므로 소멸자에서 명시적으로 동적 메모리를 해제해야 문제가 없고, 또한 현재 클래스 외부에 선언된 데이터가 현재 객체 안으로 전달된 경우도 있는데, 만약 현재 객체가 제거되고 나면 외부에서 전달된 데이터도 더이상 필요가 없는 경우에는 소멸자를 이용하여 외부에 선언된 데이터도 삭제해 줄 수 있다

사용된 객체는 생성된 코드 블럭이 종료되면 그 안에서 생성된 객체도 메모리에서 제거되는데, 이 때 소멸자가 자동으로 호출된다. 

다음은 외부에 선언된 데이터를 현재 객체가 사용하고 소멸자에서 그 데이터를 해제하는 예이다.

소멸자의 선언 예

#include <iostream>
#include <locale>
#include <string>

using namespace std;

class CUser
{
    int * num;
    wstring * name;

    public:

    CUser(int*,wstring*); // 포인터를 파라미터로 선언함
    ~CUser();

    void print_user();

};

CUser::CUser(int* n, wstring* nm) {  // 포인터
    num = n;
    name = nm;
}

CUser::~CUser() {
    delete num; // num 실제의 값은 이 클래스 외부에 있으며 더 이상 사용하지 않을 것이므로 해제함
    delete name;
    wcout << L"리소스 해제 완료!" << endl;
}

void CUser::print_user(){
    wcout << "num=" << *num << endl;
    wcout << "name=" << *name << endl;
}

int main()
{
    setlocale(LC_ALL,"");
    wcout << L"C++ 클래스 사용하기" << endl;

    int num = 11;
    wstring name = L"김인철";

    CUser user(&num, &name); // 변수의 주소를 전달함

    user.print_user();

    return 0;
}


생성자 오버로딩 ( Constructor Overloading )

함수 오버로딩( Function Overloading )과 마찬가지로 생성자도 오버로딩할 수 있다. 오버로딩할 때는 파라미터의 순서나 갯수, 자료형 등이 서로 달라야 한다. 위의 코드에서 작성자는 생성자를 정의했고 그 생성자를 통해 객체를 초기화할 수 있었다. 그런데 생성자를 선언하기 전에 제대로 실행되던 다음과 같은 코드에서 이젠 오류가 발생한다

CUser user;  //no matching function for call to 'CUser::CUser()'

위의 오류를 분석해보면 파라미터가 없는 생성자( 기본 생성자, Default Constructor )가 클래스 안에 없다는 것이다. 

클래스 작성자가 생성자를 정의하지 않으면 컴파일러는 기본 생성자를 하나 정의해주고 컴파일 작업을 수행하도록 되어 있다. 그래서 모든 클래스에는 반드시 생성자가 포함되어 있는 것이다. 컴파일러가 파라미터 없이 정의해 주는 생성자를 기본생성자(Default Constructor)라고 한다.

클래스 작성자가 커스텀 생성자를 한개라도 정의하면 컴파일러는 기본 생성자를 정의하지 않으므로 이후에 기본 생성자를 사용하는 문장에서는 오류가 발생하게 된다

커스텀 생성자와 기본 생성자 모두 사용하여 객체를 생성하고자 한다면 이들 2개의 생성자를 작성자가 정의해주면 된다. 이러한 경우 생성자는 2개가 되며 각 생성자의 파라미터는 서로 달라야 한다. 즉 오버로딩 문법을 따라야 한다

생성자 오버로딩의 예

#include <iostream>
#include <locale>
#include <string>

using namespace std;

class CUser
{
	int * num; // private 멤버변수
	wstring * name;

	public: // 아래는 모두 public

	CUser();            // 기본 생성자
	CUser(int,wstring); // 생성자(Constructor)
	~CUser();           // 소멸자(Destructor)

	void print_user(); // 선언부: 클래스 밖에 정의부가 있어야 함

};

// 기본 생성자 정의
CUser::CUser(){
	wcout << L"기본 생성자 실행됨" << endl;
}

// 생성자 정의(클래스명과 동일, 리턴타입 있으면 생성자 아님)
CUser::CUser(int n, wstring nm) {
	num = &n;
	name = &nm;
}

// 소멸자 정의(생성자와 동일하나 이름 앞에 ~기호 사용)
CUser::~CUser() {
	delete num;
	delete name;
	wcout << L"리소스 해제 완료!" << endl;
}

// 멤버함수의 정의부(반드시 함수의 리턴타입을 다시 선언해야 함)
// 전역함수와 구별하기 위해 '::'(scope operator)를 사용하여 해당 클래스를 지정함
void CUser::print_user(){
	wcout << "num=" << *num << endl;
	wcout << "name=" << *name << endl;
}

// 전역함수
int main()
{
	setlocale(LC_ALL,"");
	wcout << L"C++ 클래스 사용하기" << endl;

	CUser user1; // 자동으로 기본 생성자가 호출됨

	CUser user2(11, L"김인철");   //클래스의 객체 생성, 생성자 자동호출됨

	user2.print_user();       // 멤버함수 호출

    return 0;
}


복사 생성자 ( Copy Constructor )

컴파일러는 개발자가 작성한 클래스를 컴파일하기 전에 복사 생성자를 추가하고 컴파일 작업을 수행한다. 그러므로 모든 클래스의 객체는 복사생성자를 갖고 있는 셈이다. 컴파일러가 주가해 주는 복사 생성자는 기능면에서 다음과 비슷한 내용으로 되어 있다

CUser::CUser(const CUser& user) {
	num = user.num;
	name = user.name;
}

앞서 작성한 코드 중에서 main 함수 부분에 복사 생성자가 제대로 작동하는 알아보기 위해 다음과 같은 코드를 추가하고 실행해 보면 객체가 제대로 복사되고 기능하는 것을 확인할 수 있다

int main()
{
    setlocale(LC_ALL,"");
    wcout << L"C++ 클래스 사용하기" << endl;

    int num = 11;
    wstring name = L"김인철";

    CUser user(&num, &name);

    user.print_user();

    CUser user2(user);  // 복사 생성자를 이용한 객체 복사
    user2.print_user();

    return 0;
}


객체를 가리키는 포인터 변수, 참조변수

참조변수, 포인터 변수의 문법은 클래스의 객체에도 그대로 적용된다. 그러므로 다음과 같은 표현이 가능하다

CUser user();
CUser * pUser = &user;


객체를 가리키는 포인터, 참조변수의 예

#include <iostream>
#include <locale>
#include <string>

using namespace std;

class CUser
{
	int * num;
	wstring * name;

	public:

	CUser();
	CUser(int*,wstring*);
	~CUser();

	void print_user();

};

// 기본 생성자 정의
CUser::CUser(){
	wcout << L"기본 생성자 실행됨" << endl;
}

// 생성자 정의(클래스명과 동일, 리턴타입 있으면 생성자 아님)
CUser::CUser(int * n, wstring * nm) {
	num = n;
	name = nm;
}

// 소멸자 정의(생성자와 동일하나 이름 앞에 ~기호 사용)
CUser::~CUser() {
	delete num;
	delete name;
	wcout << L"리소스 해제 완료!" << endl;
}

// 멤버함수의 정의부(반드시 함수의 리턴타입을 다시 선언해야 함)
// 전역함수와 구별하기 위해 '::'(scope operator)를 사용하여 해당 클래스를 지정함
void CUser::print_user(){
	wcout << "num=" << *num << endl;
	wcout << "name=" << *name << endl;
}

// 전역함수 선언(포인터를 파라미터로 선언)
void print(CUser* u){
	u->print_user();
}

// 전역함수 (참조변수를 파라미터로 선언)
void print(CUser& u){
	u.print_user();
}

// 전역함수
int main()
{
	setlocale(LC_ALL,"");
	wcout << L"C++ 클래스 사용하기" << endl;

	int num = 11;
	wstring name = L"김인철";

	CUser user1(&num, &name);
	print(&user1);				// 전역함수 호출

	int num2 = 12;
	wstring name2 = L"박지성";

	CUser user2(&num2, &name2);
	print(user2);				//전역함수 호출

	int num3 = 13;
	wstring name3 = L"김연아";

	CUser user3(&num3, &name3);
	CUser* pUser = &user3;
	print(pUser);

    return 0;
}