.. Cover Letter

ㅇ 공부#언어/(C++)기초

(기초 C++) 3장. C++에서 클래스 사용법

BrainKimDu 2023. 2. 25. 22:19

주의 : 이 글은 C의 기초문법에 대해 상세하게 다루지 않습니다. (즉 C언어에서 배울 수 있는 기초내용은 생략합니다)

※ 명품 C++ Programming 의 책을 참고하여 개인적으로 정리한 글입니다. 
이 글의 목적은 해당 책의 내용을 인용하여 더 쉽게 이해하고자 정리하고, 더 쉬운 예제를 통해 이해하는 것을 목표로 하고 있습니다.
명품 C++ Programming의 예제문제와 실습문제가 정말 좋으므로, 깊게 공부하고 싶다면 책을 구매하는 것을 추천드립니다.
책의 저작권 등등 각종 권한은 출판사와 지은이/옮긴이에 있습니다.

- 출판사: (주)생능 출판사
- 지음: 황기태

명품 C++ Programming - YES24


C++ 클래스 만드는 방법과 캡슐화

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
public:
	string name;
	string setName();
};

C++ 의 클래스는 다음처럼 구성됩니다. 클래스 내부에는 3가지의 접근지정자가 있습니다. 이 접근 지정자는 총 3개가 있으며,  C++의 경우 default는 private입니다.
public : 클래스 외부로 부터 접근을 허용합니다.
protected : 상속관계일 때 접근을 허용합니다.
private : 외부에서 접근을 금지합니다. (클래스 내부에서만 사용할 수 있습니다.)
그래서 다음처럼 public을 빼먹으면 private로 선언되어 접근이 불가능합니다.

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
	string setName(string name) {
		this->name = name;
	}
};

int main() {
	Animal dog;
	dog.setName("dog");
}

 
그래서 다음처럼 코드를 수정합니다.

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
public:
	string setName(string name) {
		this->name = name;
	}
};

int main() {
	Animal dog;
	dog.setName("dog");
}

코드 해석을 하면서 클래스의 기초에 대해서 알아봅시다.  우선 Animal dog; 코드를 통해서 Animal 클래스에서 객체화를 합니다. 영어로 instantiation 한다. 라고 하면 보통 객체화를 진행한다로 되고, 그냥 쉽게 Animal 클래스의 객체를 생성한다라고 이해해도 됩니다. (보통 실무에서 누군가와 소통할때 "인스턴시에이션 시킨다" 라는 용어를 많이 듣게됩니다. 그냥 클래스 객체를 하나 생성하면 으로 이해하시면됩니다. ("~ 모듈을 외부에서 인스티에이션 시킨다")

그러면 instantiation 시킨 객체의 각 메소드와 변수에 접근하기 위해서는 "." 을 찍습니다. 그러면  클래스의 각 변수와 메소드에 접근이 가능합니다. 메소드를  맴버함수로  변수를 멤버 변수로 부르는 책도 있습니다. (자바에서는 메소드라고 부릅니다.)

여기서 객체지향의 개념이 하나 등장합니다. 위의 코드에서 한가지 의문을 가져야합니다. 왜 굳이 메소드를 불러서 이름을 수정을 하는 건가요? 다음처럼 코드를 작성해도 되지 않나요?

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
public:
	string name;
};

int main() {
	Animal dog;
	dog.name = "dog";
}

맞습니다. 이렇게 작성해도 코드는 정상적으로 돌아갑니다. 그러나 클래스를 왜쓰는가? 를 생각해봐야합니다. 

#include <iostream>
#include <cstring>
using namespace std;

class money {
public:
	int myMoney;
};

int main() {
	money kim;
	kim.myMoney = 99999999;
	cout << kim.myMoney;
}

다음처럼 kim이라는 사람의 돈을 9999999로 만드는 경우 문제가 발생할 수 있습니다. 또한 외부에서 접근한 사람이 kim의 돈이 얼마인지 쉽게 알 수 있습니다.
그렇기 때문에 다음처럼 코드를 작성할 수 있습니다. 

#include <iostream>
#include <cstring>
using namespace std;

class money {
	int myMoney = 0;
public:
	void get_salary() {
		myMoney += 2000000;
	}

	void show_myMoney(){
		cout << myMoney;
	}
};

int main() {
	money kim;
	kim.get_salary();
	kim.show_myMoney();
}

myMoney 를 private으로 설정함으로서 외부에서 접근을 할 때는 메소드를 이용해야지 수정이 가능해집니다. 그리고 myMoney에 다이렉트로 접근하는 것은 제한됩니다. 그래서 메소드에 다음과 같은 보안장치를 넣으면 지정된 사용자 이외에 수정이 불가능하게 됩니다.

#include <iostream>
#include <cstring>
using namespace std;

class money {
	int myMoney = 0;
public:
	void get_salary() {
		string password;
		cin >> password;
		if (password == "관리자")
			myMoney += 2000000;
		else
			cout << "비밀번호가 틀립니다.";
	}

	void show_myMoney(){
		cout << myMoney;
	}
};

int main() {
	money kim;
	kim.get_salary();
	kim.show_myMoney();
}

즉 이렇게 객체의 구성요소들을 보호하는 것을 캡슐화 라고 합니다.  그래서 보통 멤버변수는 private로 선언을 하는 것이 일반적입니다.

 

생성자

다시 코드를 보시겠습니다.

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
public:
	string setName(string name) {
		this->name = name;
	}
};

int main() {
	Animal dog;
	dog.setName("dog");
}

한가지 의문이 들 수도 있습니다. 그러면 Animal dog 에서 도대체 어떤 놈이 클래스의 객체로 만들어주는 것인가요? 그리고 변수를 선언한다라는 표현처럼 클래스 객체를 선언한다 라고 표현하지 않고 클래스의 객체를 만든다라고 표현한 이유가 뭘까요?
다음 코드에는 한가지 메소드가 생략되어 있습니다.

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
public:
	Animal() {}

	string setName(string name) {
		this->name = name;
	}
};

int main() {
	Animal dog;
	dog.setName("dog");
}

바로  Animal()이라는 멤버함수입니다. 클래스의 이름과 같은 메소드가 컴파일러에 의해 자동으로 넣어져 실행되게 됩니다. 그래서 이를 생성자라고 부릅니다. 
그래서 밑의 코드에서 setName함수가 없어도 dog 객체를 바로 만들 수 있습니다. 다음의 코드를 확인합시다.

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
public:
	Animal(string name) {
		this->name = name;
	}

	string getName() {
		return name;
	}
};

int main() {
	Animal dog("dog");
	cout << dog.getName();
}

다음처럼 객체를 만들때 생성자에 dog를 넣어 name을 dog로 바로 초기화 합니다. 주의해야할 점은 이 순간부터는 Animal 객체를 생성할때는 꼭 생성자의 매개변수 규칙을 따라주어야합니다.

다음처럼 표시되게 됩니다. 그러면 나는 name을 초기화하기 싫을때는 어떻게 할까요? 방법은 간단합니다. 생성자는 중복하여 선언이 가능합니다.

다음처럼 생성자를 선언하면 cat 객체가 만들어지는 모습을 보여줍니다. 그래서 이는 응용이 가능합니다.
동물의 나이를 추가해봅시다.

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
	int Nai;
public:
	Animal() {}

	Animal(string name) {
		this->name = name;
	}
	
	Animal(string name, int Nai) {
		this->name = name;
		this->Nai = Nai;
	}

	string getName() {
		return name;
	}
};

int main() {
	Animal dog("dog");
	cout << dog.getName();

	Animal cat("cat", 1);
}

다음처럼 생성자를 추가하면 됩니다. 여기서 한가지 더 알아보면 this->name = name이라는 친구입니다. (파이썬에서는 self, 자바는 this. 입니다.) this 라는 친구는 클래스내의 변수에 접근하는 것이라 생각하면됩니다.  
메소드 내에서 선언한 변수는 메소드내에서만 사용합니다.  this-> 를 붙이면 클래스내의 변수를 뜻합니다.
(파이썬을 하다와서 그런가 확인이 필요합니다. 이름이 다르면 this를 넣지 않아도 될 수도 있습니다)
 
추가적으로 클래스와 메소드를 분리할 수 있습니다.

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
	int Nai;
public:
	Animal(string name) {
		this->name = name;
	}
	
	string getName(); 
};

string Animal::getName(){
	return name;
}

int main() {
	Animal dog("dog");
	cout << dog.getName();
}

다음처럼 외부에서 작성이 가능합니다. 메소드가 길어기는 경우 주로 사용하는게 좋다고 합니다.
저는 길지 않은 이상은 분리안하지만, 클래스 선언부와 구현부를 분리하는 것이 좋다고 언급합니다.
책에서 위임생성자라는 개념이 등장합니다만

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
	int Nai;
public:
	Animal() : Animal("cat") {}

	Animal(string name) {
		this->name = name;
	}
	
	string getName() {
		return name;
	}
};

int main() {
	Animal dog("dog");
	cout << dog.getName();
	Animal cat;
	cout << cat.getName();
}

근데 저라면 이렇게 코드를 짤 것 같습니다. 

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
	int Nai;
public:
	Animal(string name = "dog") {
		this->name = name;
	}
	
	string getName() {
		return name;
	}
};

int main() {
	Animal dog;
	cout << dog.getName();
	Animal cat("cat");
	cout << cat.getName();
}

위임생성자로 귀찮게 생성자 여러개를 돌리는 것보다 다음처럼 맴버변수를 미리 지정하면 

됩니다. 위임생성자는 생성자를 여러개 선언해야할 경우 쓰는 것이라 생각됩니다. 

 

다른 책에서는 위임생성자가 아니라 다음과 같은 형식으로 사용하는 것을 본 것 같습니다.

class cla
{
	int x;
public:
	explicit cla(int _x):x(_x) { }
	void Print() const { std::cout << x << std::endl; }
};

 


 
추가적으로 생성자를 public으로 선언하지 않으면 어떻게 될까요?

엑세스 할 수가 없어짐으로 객체 생성이 불가능해집니다.
 
 

소멸자

자바에서는 프로그램이 도는 중에 안쓰는 객체는 가비지컬렉션이 가져가서 알아서 처리를 하지만 C++ 프로그램이 종료될 때 객체가 소멸합니다. 그래서 다음 클래스도 소멸자가 숨어있는 상태입니다. 여튼 소멸자의 역할은 다음과 같습니다.
- 소멸자의 목적은 객체가 사라질 때 필요한 마무리 작업
- 소멸자의 이름은 클래스 이름 앞에 ~를 붙인다.
- 소멸자는 어떠한 값도 리턴하면 안된다.
- 소멸자는 오직 한개만 존재하며 매개변수를 가지지 않는다.
- 소멸자가 없으면 기본 소멸자가 자동 생성된다.

#include <iostream>
#include <cstring>
using namespace std;

class Animal {
	string name;
	int Nai;
public:

	Animal(string name) {
		this->name = name;
	}
	
	string getName() {
		return name;
	}
	~Animal() {
		cout << name << "이 집으로 돌아갔습니다." << endl;
	}

};

int main() {
	Animal dog("dog");
	cout << dog.getName() << endl;
	Animal cat("cat");
	cout << cat.getName()<<endl;
}

다음처럼 소멸자가 추가된 경우 어떻게 되는지 살펴보자.

객체는 전역이건 지역이건 생성된 순서대로 소멸된다.
 

인라인 함수

함수를 호출하는 과정은 다음과 같다.

함수호출 -> 돌아올 주소 저장 -> CPU 레지스터 값 저장 -> 함수의 매개변수를 스택에 저장 -> 함수 실행 -> 함수의 리턴값을 임시 저장장소에 저장 -> 저장한 레지스터 값 CPU에 복귀 -> 돌아올 주소를 알아내어 리턴 
함수호출과 함수실행을 제외한 과정을 오버헤드라고 부르며 이에 대한 시간을 해결하고자하는 것이 중요하다고 한다. 여러번 실행되야 하는 함수의 경우 이처럼 함수 호출에 대한 실행속도 저하가 계속해서 발생할 것이다. 그래서 실행속도를 개선하고자 인라인 함수가 등장한다.
함수 앞에 inline을 넣으면 된다. 다음 코드의 경우 어떻게 될까?

#include <iostream>
#include <cstring>
using namespace std;


int plusOne(int i) {
	return i + 1;
}

int main() {
	int su = 0;
	for (int i = 0; i < 1000; i++)
		su = plusOne(su);
	cout << su;
}

여기서 함수를 inline으로 설정을 하자.

#include <iostream>
#include <cstring>
using namespace std;


inline int plusOne(int i) {
	return i + 1;
}

int main() {
	int su = 0;
	for (int i = 0; i < 1000; i++)
		su = plusOne(su);
	cout << su;
}

 
시간이 줄어드는 모습..  여튼 어떻게 변환되는가 하면

int main() {
	int su = 0;
	for (int i = 0; i < 1000; i++)
		su = su + 1;
	cout << su;
}

이런식으로 코드가 변화되고 이상태로 실행이되는 방식이다. 인라인 함수를 사용할때는 가능한 한 간단하게 사용해야한다. 또한 컴파일러가 판단하기에 inline을 넣기 불필요하면 선언을 무시할수 있다. 또한 inline 함수는 클래스 내의 메소드에도 적용될 수 있다.
 

구조체

C++에서도  C언어를 지원한다. 클래스의 선언과 비슷하게 할 수 있다.

struct Animal {
private:
	string name;
	int years;
public:
	string sound;
};

그래서 클래스의 경우 구조체형식으로 재작성될 수 있다. 그러나 그럴거면 그냥 클래스를 쓰는게 맞다.

 

바람직한 C++ 작성을 위해서는

클래스 선언부와 구현부는 분리되어 작성되는 것이 좋다.
 

실습문제

10886번: 0 = not cute / 1 = cute (acmicpc.net)

 

10886번: 0 = not cute / 1 = cute

준희는 자기가 팀에서 귀여움을 담당하고 있다고 생각한다. 하지만 연수가 볼 때 그 의견은 뭔가 좀 잘못된 것 같았다. 그렇기에 설문조사를 하여 준희가 귀여운지 아닌지 알아보기로 했다.

www.acmicpc.net

1. 준희가 귀엽지 않다는 의견이 더 많을 경우 "Junhee is not cute!"를 출력하고 귀엽다는 의견이 많을 경우 "Junhee is cute!"를 출력하라. 우리는 이를 클래스를 사용해서 작성해보자.

 

#include <iostream>
#include <cstring>
using namespace std;

class junhee_survey {
	int junhee_cute = 0;
	int junhee_notcute = 0;

public:
	junhee_survey(int junhee_cute = 0, int junhee_notcute = 0) {
		this->junhee_cute = 0;
		this->junhee_notcute = 0;
	}

	void survey_response(int num) {
		if (num == 1)
			this->junhee_cute += 1;
		else if (num == 0)
			this->junhee_notcute += 1;
	}

	string survey_result() {
		if (junhee_cute > junhee_notcute)
			return "Junhee is cute!";
		else
			return "Junhee is not cute!";
	}
};

int main() {
	int t; cin >> t;
	int num;
	junhee_survey cute(0, 0);
	while (t--) {
		cin >> num;
		cute.survey_response(num);
	}
	cout << cute.survey_result();
}

다음처럼 작성을 할 수 있을 것 입니다. 가끔씩 어떻게든 돌아가는코드를 만드는 것보다 클래스로 만들면 재미있군요.
 
 


참고 문헌
Scott Douglas Meyers(2015). Effective C++. Protec Media(프로텍 미디어)
Robert C. Martin(2021).UML 실전에서는 이것만 쓴다(UML for Java Programmers). 인사이트
황기태(2021). 명품 C++ Programming. (주)생능출판사