C++_클래스의 선언

최강림·2022년 11월 2일
0

C++개념정리

목록 보기
5/9
post-thumbnail

클래스는 멤버와 함수의 집합체로서, 변수에 개성을 부여하는 특수한 자료형입니다.
다음의 간단한 Rabbit클래스를 살펴봅시다.

각자의 코드가 무슨 역할은 하는지는 아직 정확히는 몰라도,
이전목차에서 클래스의 선언방식을 한번봤다면 좀 더 눈에 익으실꺼라 기대해요 :)
(물론 당장 모르셔도 뒤에서 차차 설명할꺼니 걱정마세요.)

class Rabbit {
private: //접근제한자(private, public, protected가 있다.)
	int year;
public: 
	string name;

	//Rabbit생성자 and 소멸자
	Rabbit() :year{ 0 }, name{ "None" }{ cout << name << " is created!\n"; }
	Rabbit(int y, string n) :year{ y }, name{ n }{ cout << "Rabbit has year and name!\n"; }
	Rabbit(int y) :Rabbit(y, "None") { cout << "생성자 위임을 이용한 기본생성자 초기화\n"; }
	~Rabbit() { cout << "Rabbit is dead\n"; } //생성자와 반대되는 소멸자, 객체가 할당해제되면 자동으로 호출됨.

	//method
	void jump() { cout << "Rabbit jump!\n"; } //토끼 점프함수
	void rabbit_inform();
};

void Rabbit::rabbit_inform() {
	cout << "Rabbit name is " << name << '\n';
	cout << "Rabbit is " << year << "years old\n";
}

많이 복잡해보이죠? 접근제한자니, 생성자니 처음들어보는 단어가 나와 당황할 수 있을꺼같아요

📌I4 클래스 설명 + 접근제한자

📖 클래스 간단사용법

만약 구조체를 사용해봤다면 대충은 감이 올 꺼 같아요. (구조체에 대한 경험이 없다면 구조체부터 찾아보세요.) 클래스는 구조체의 상위호환으로 위 코드처럼 클래스를 선언하면 메인함수에서 객체, 즉 클래스의 변수를 선언한 후 ( ex: Rabbit testRabbit; )
이 객체에 있는 것들을 사용할 수 있어요. (ex: testRabbit.jump();)

int main(){
  Rabbit testRabbit1; //객체의 선언
  testRabbit1.jump(); //객체의 멤버함수 사용
}

🖥️출력
Rabbit jump!

우리는 이 클래스에 있는 함수들과 변수들을 아래와 같이 불러요

  • 멤버함수(혹은 메소드)
  • 멤버변수(혹은 field)라고 불러요.

📖접근제한자

하지만 객체를 선언한다고해서 이러한 멤버들에 아무때나 접근할 수 있는게 아니예요.
바로 접근제한자(private,protect,public) 키워드에 의해 멤버들의 사용이 제한되거든요.

private: //접근제한자 private =>클래스 내부에서만 호출가능
	int year;
public: //접근제한자 public => 어디서든 호출가능
	string name;

접근제한자 키워드 아래에 있는 코드들은 각 키워드에 따라 접근에 제한을 받습니다.

int main(){
	testRabbit.year = 10 //error! 접근불가능. private
    testRabbit.name = "Bob" //가능
}

비쥬얼 스튜디오를 쓰신다면 "testRabbit."을 입력해보면 멤버가 public에 있는 name,jump만 제공하고 private에 있는 year은 제공하지 않는 점으로 알 수 있습니다.
private은 클래스 외부에서 접근 할 수 없기 때문에 외부 프로그램에 의해 접근하기 쉽지 않아요.

📖 I2 private, public에 넣는 멤버구분

프로그래머들의 암무적인 룰로 보통 멤버변수들은 private에 관리하고,
멤버함수들은 public에 관리하는게 기본입니다.
(지금은 설명을 위해 name멤버변수를 public에 뒀지만요.)
그게 프로그램의 안정성이 높다고 많은 프로그래머가 생각합니다.
예를 들어 메이플 캐릭터의 공격력 변수가 public으로 되어있다면 다른 나쁜 프로그래머들이 메이플 캐릭터의 객체를 불러온 후 마음대로 변수를 손쉽게 바꿀 수 있겠죠?
(ex: hero.ATK = 9999999999999;)

📌 I4 클래스 생성자

📖 I3 클래스 생성자

잠깐 다시 위의 클래스를 보고오시면 public에 분명 함수처럼 생겼는데 반환형이 없는 함수가 있을꺼예요.

Rabbit() :year{ 0 }, name{ "None" }{ cout << name << " is created!\n"; }
Rabbit(int y, string n) :year{ y }, name{ n }{ cout << "Rabbit has year and name!\n"; }
Rabbit(int y) :Rabbit(y, "None") { cout << "생성자 위임을 이용한 기본생성자 초기화\n"; }

바로 클래스 생성자함수입니다. 이 함수는 클래스가 생성되는 동시에 호출되는 함수예요.
주로 클래스의 멤버변수를 초기화하는 용도로 사용해줍니다.

먼저 설명에 앞서 생성자도 함수라는 것을 기억하면서 읽어주세요.
별건 아니지만 꽤 많은 친구들이 생성자와 함수를 구분해서 생각하더라고요.
생성자도 함수이므로 함수에서 할 수 있는 것들 호출, 오버로딩 모두 가능합니다.

생성자 옆에 ": year{0},name{"None"}" 이거는 생성자의 호출과 동시에 멤버변수를 초기화하는 방법이에요.
"Rabbit(){ year=1; name="Rabbit"; ...}"와 결과적으로는 같지만,
전자는 객체의 생성과 동시에 초기화가 되고
후자는 객체의 생성 후 생성자의 내부코드가 하나하나 실행되며 초기화됩니다.

객체의 생성과 동시에 초기화되는 것이 안정성이 더 높다고하여, 보통은 전자의 초기화 방식을 선택합니다.

📖 I5 생성자 오버로딩과 위임

멤버함수도 함수같이 오버로딩 가능합니다.
생성자도 멤버함수니 마찬가지구요.
Rabbit()는 아무 매개변수도 없기 때문에 객체를 선언하면 자동으로 year과 name을 정의합니다.

Rabbit() :year{ 0 }, name{ "None" }{ cout << name << " is created!\n"; }
Rabbit(int y, string n) :year{ y }, name{ n }{ cout << "Rabbit has year and name!\n"; }
Rabbit(int y) :Rabbit(y, "None") { cout << "생성자 위임을 이용한 기본생성자 초기화\n"; }

때문에 우리는 Rabbit객체 선언과정에서 이름과 나이를 정의 하고싶을 때 생성자 오버로딩을 합니다.

위 코드의
Rabbit(int y, string n)
Rabbit(int y) :Rabbit(y, "None")
가 그 예시이죠.

int main(){
  cout << "\n**생성자 오버로딩\n";
  Rabbit testRabbit3; //Rabbit() 호출
  Rabbit testRabbit4{ 3,"Sarah" }; //Rabbit(int y, string n) 호출
  testRabbit3.rabbit_inform();
  testRabbit4.rabbit_inform();
}

🖥️출력
None is created!
Rabbit has year and name!
Rabbit name is None
Rabbit is 0years old
Rabbit name is Sarah
Rabbit is 3years old

📖기본생성자에 대한 고찰

만약 우리가 생성자를 하나도 선언하지 않은 채 메인함수에서 Rabbit객체를 선언하게 된다면, 컴파일은 자체적으로 "기본생성자"라는 것을 만들어 Rabbit()함수를 호출합니다.

하지만!! 기본생성자 Rabbit()는 따로 선언해주는게 무조건 좋습니다.
기본생성자가 아닌 생성자(Rabbit(int y))가 하나라도 오버로딩되어있으면 컴파일러가 자동으로 기본생성자가 생성되지 않아, 자칫 의도치않은 코드오류가 생길 수 있습니다.

📖 생성자 위임

하지만 생성자 오버로딩을 하나하나 하다보면 꽤나 지친답니다.. 그래서 프로그래머들은 꾀를 내어 이 수작업을 아~주 조금이라도 줄이는 방법을 생각해냈어요.

바로 생성자의 초기화 과정에서 다른 오버로딩된 생성자를 호출하는 것 이예요.

Rabbit() :year{ 0 }, name{ "None" }{ cout << name << " is created!\n"; }
Rabbit(int y, string n) :year{ y }, name{ n }{ cout << "Rabbit has year and name!\n"; }
Rabbit(int y) :Rabbit(y, "None") { cout << "생성자 위임을 이용한 기본생성자 초기화\n"; }

위 코드에서 Rabbit(int y)함수의 초기화에서 Rabbit(int y, string n)함수를 호출하는 것입니다.

생성자가 최대로 담을 수 있는 매개변수를 모두 초기화해주는 생성자를 선언한 후 다른 생성자를 호출할 때 그 매개변수가 많은 생성자를 호출하는 방식으로 생성자를 초기화해줍니다.
이러한 방식을 "생성자 위임"이라고 불러요.

📌 I2 클래스 프로토타입

using namespace std; 
class Rabbit {
private: 
	int year;
public: 
	string name;
	//method
	void rabbit_inform();
};

void Rabbit::rabbit_inform() {
	cout << "Rabbit name is " << name << '\n';
	cout << "Rabbit is " << year << "years old\n";
}

제가 Rabbit객체의 정보를 보기 위해 호출하는 rabbit_inform()함수를 보시면

이것은 우리가 함수를 만들때 프로토타입으로 메인함수의 위에 함수의 이름만 정의 한 후 메인함수 아래에 그 함수를 정의하는 것과 정확히 일치해요.

다만 그냥 메인함수 밑에 함수를 정의하면 되는 프로토타입과는 달리,
어떤 클래스의 멤버함수를 정의하는 것인지 명시하기 위해 함수 이름 앞에 "클래스이름::"을 붙여줍니다.
ex) void Rabbit::rabbit_inform()

//I5 외부에서 선언 참고사항
초반에는 살짝 헷갈릴 수 있는 부분이 rabbit_inform()함수는 클래스 내부에 있는것으로 인식하기 때문에,
Rabbit클래스의 private에 있는 멤버들(year,name)에게 자유롭게 접근가능합니다!

함수를 보시면 private인 year에 자유롭게 접근하는 것을 확인 가능합니다.

📌 I4 소멸자

위의 클래스를 보시면 ~Rabbit()함수가 있는데 "소멸자"라고 부릅니다.

~Rabbit() { cout << "Rabbit dead\n"; } //생성자와 반대되는 소멸자, 객체가 할당해제되면 자동으로 호출됨.

객체가 할당해제될때 자동으로 호출되어요.
지금까지 위에서 선언한 함수들은 모두 스택메모리에 할당되기 때문에
메인함수가 끝나면 모두 소멸자가 호출됩니다.

소멸자에 대해서는 다음 목차에 객체를 힙메모리에 선언할 때 어떻게 다루는지를 설명하며,
더욱 자세하게 다룰 예정입니다. 아주 중요한 내용이니 잘 정리해보겠습니다!
소멸자는 기본적으로 객체가 선언된 순서대로 호출됩니다.(testRabbit1,2,3,4...순으로 호출)

🖥️ 출력 (객체가 소멸하는 시점에 호출)
Rabbit dead
Rabbit dead
Rabbit dead
Rabbit dead
Rabbit dead

이상으로 정리를 마칩니다.

📌코드 전문

//안녕하세요! WHO스터디 부장 최강림입니다. 4주차 개념정리 시작하겠습니다.

/**
이번 주는 객체지향 프로그래밍의 시작을 알리는 클래스에 대해 정리합니다.
클래스를 main함수 위에다 선언해야돼서 왔다갔다 좀 정신사나울 수 있어요 ㅠㅠ
메인함수를 보다가 주석에 클래스를 다시보라는 말이있으면 올려서 확인해보세요.
클래스를 사용해줄 때마다 디버그하여 f11을 눌러가며 이해하면 훨씬 잘 이해될꺼예요. (중요!)
차근차근 이해해보면서 잘 이해할 수 있으면 좋겠네요.
*/

/**
클래스는 멤버와 함수의 집합체로서, 변수에 개성을 부여하는 특수한 자료형입니다.
다음의 간단한 Rabbit클래스를 살펴봅시다. 각자의 코드가 무슨 역할은 하는지는 아직 정확히는 몰라도,
저번에 클래스의 선언방식을 한번봐서 좀 더 눈에 익으실꺼라 기대해요 :)
(물론 당장 모르셔도 뒤에서 차차 설명할꺼니  걱정마세요.)
*/

#include <iostream>
//I2 Rabbit클래스 선언
//밑의 클래스 많이 복잡해보이죠? 접근제한자니, 생성자니 처음들어보는 단어가 나와 당황할 수 있을꺼같아요
//I4 클래스 설명 + 접근제한자 (line 31~40, line 93만 잠깐 살펴보세요.)
/**
그래도 구조체를 사용해봤으니 대충은 감이 올 꺼 같아요. 클래스는 구조체의 상위호환으로
아래처럼 클래스를 선언하면 메인함수에서 객체, 즉 클래스의 변수를 선언한 후(ex: Rabbit testRabbit;)
이 객체에 있는 것들을 사용할 수 있어요. (ex: testRabbit.jump();)

우리는 이 클래스에 있는 함수들과 변수들을 멤버함수(혹은 메소드), 멤버변수(혹은 field)라고 불러요.
하지만 객체를 선언한다고해서 이러한 멤버들에 아무때나 접근할 수 있는게 아니예요.
바로 접근제한자(private,protect,public) 키워드에 의해 멤버들의 사용이 제한되거든요.

메인함수부터 보면서 차근차근 봐봅시다.line 113에 메인함수 있어요.
*/
using namespace std; //참고로 코테할때는 상관없지만, 개인적인 공부할때는 namespace사용 자제하는게 좋다고 생각해요.
class Rabbit {
private: //접근제한자 private =>클래스 내부에서만 호출가능
	int year;
public: //접근제한자 public => 어디서든 호출가능
	string name;

	//클래스의 생성자(클래스와 이름이 같은 함수!!), 객체가 생성되면 호출되는 함수, 반환형이 없다.
	Rabbit() :year{ 0 }, name{ "None" }{ 
		cout << name << " is created!\n";
	}
	//I2 생성자 초기화 (메인함수에서 보라고하면 보세요.)
	/**
	먼저 설명에 앞서 생성자도 함수라는 것을 기억하면서 읽어주세요.
	별건 아니지만 꽤 많은 친구들이 생성자와 함수를 구분해서 생각하더라고요.
	생성자도 함수이므로 함수에서 할 수 있는 것들 호출, 오버로딩 모두 가능합니다.
	
	생성자 옆에 ": year{0},name{"None"}" 이거는 생성자의 호출과 동시에 멤버변수를 초기화하는 방법이에요.
	"Rabbit(){ year=1; name="Rabbit"; ...}"와 결과적으로는 같지만,
	전자는 객체의 생성과 동시에 초기화가 되고
	후자는 객체의 생성 후 생성자의 내부코드가 하나하나 실행되며 초기화됩니다.

	객체의 생성과 동시에 초기화되는 것이 안정성이 더 높다고하여, 보통은 전자의 초기화 방식을 선택합니다.
	다시 메인함수로 돌아갑시다. (line 142)
	*/

	//I5 생성자 오버로딩 (일단 건너뛰고, 메인함수에서 보라고 할 때 보세요.)
	Rabbit(int y, string n) :year{ y }, name{ n }{
		cout << "Rabbit has year and name!\n";
	}
	/**
	Rabbit생성자를 오버로딩하였습니다. 이를 통해 우리는 생성자를 초기화 할 때, 멤버변수를 조작할 수 있습니다.
	우리는 매개변수가 없는 생성자를 기본생성자(Rabbit())라고 부릅니다.
	기본생성자(Rabbit())은 우리가 굳이 선언하지 않아도 
	컴파일러 내부적으로 객체가 생성될 때 기본생성자를 자동으로 생성하고 객체를 선언해요. 

	I5
	하지만!! 기본생성자는 따로 선언해주는게 무조건 좋습니다.
	기본생성자가 아닌 생성자가 하나라도 오버로딩되어있으면 컴파일러가 자동으로 기본생성자가 생성되지 않아,
	자칫 의도치않은 코드오류가 생길 수 있습니다.

	line 151를 보고 한번 디버그해본 후, 기본생성자(Rabbit(),line38)를 주석처리한 후 다시 컴파일 해보세요.
	참고로 ctrl+c+k를 누르면 드래그 되어 있는 부분이 모두 주석처리됩니다.

	해본후 다시 메인함수로 돌아갑시다. (line 158)
	*/

	//I5생성자 위임
	Rabbit(int y) :Rabbit(y, "None") {
		cout << "생성자 위임을 이용한 기본생성자 초기화\n";
	}
	/**
	생성자 오버로딩의 초기화에서 ": year{y}"와 같이 초기화 하는 대신
	Rabbit(int y, string name) 생성자를 호출하여 초기화하는 거예요.

	이러면 그래도 매번 오버로딩을 만드는것보다 편해지므로 많은 프로그래머들이 이 생성자 위임을 애용합니다.

	원리를 간단하게 설명드리자면,
	1. 초기화가 필요한 모든 멤버변수를 매개변수로 하는 생성자 하나를 만든다.
	=> 이 코드에서는 Rabbit(int y, string n)생성자에 해당
	2. 생성자를 오버로딩하고 초기화 할 때 1번에서 만든 생성자를 호출한다.
	=> ex : Rabbit():Rabbit(0,"None"){}, Rabbit(int y):Rabbit(y,"None"){} 등등

	다시 메인함수로 돌아갈까요? line 165
	*/

	void jump() { cout << "Rabbit jump!\n"; } //토끼 점프함수
	void rabbit_inform();

	~Rabbit() { cout << "Rabbit dead\n"; } //생성자와 반대되는 소멸자, 객체가 할당해제되면 자동으로 호출됨.
};

void Rabbit::rabbit_inform() {
	cout << "Rabbit name is " << name << '\n';
	cout << "Rabbit is " << year << "years old\n";
}




int main() {
	//I3 접근제한자 예시
	cout << "**클래스 생성\n";
	Rabbit testRabbit1;
	testRabbit1.jump();
	//cout << testRabbit1.year; //error!
	testRabbit1.rabbit_inform();
	/**
	"testRabbit."을 입력해보면 멤버가 public에 있는 name,jump만 제공하고
	private에 있는 year은 제공하지 않는 점으로 알 수 있어요.
	private은 클래스 외부에서 접근 할 수 없기 때문에 외부 프로그램에 의해 접근하기 쉽지 않아요.

	//I2
	프로그래머들의 암무적인 룰로 보통 멤버변수들은 private에
	멤버함수들은 public에 관리하는게 기본입니다. (지금은 설명을 위해 name멤버변수를 public에 뒀지만요.)
	그게 프로그램의 안정성이 높다고 많은 프로그래머가 생각합니다.
	예를 들어 메이플 캐릭터의 공격력 변수가 public으로 되어있다면 다른 나쁜 프로그래머들이
	메이플 캐릭터의 객체를 불러온 후 마음대로 변수를 바꿀 수 있겠죠? (ex: hero.ATK = 9999999999999;)
	*/
	
	//I4 클래스 생성자 (line 41에 브레이크 포인트를 걸고 디버그해보세요)
	cout << "\n**생성자 예시\n";
	Rabbit testRabbit2; 
	/**
	잠깐 다시 클래스를 보고오시면 public에 분명 함수처럼 생겼는데 반환형이 없는 함수가 있을꺼예요.(Rabbit(),line 41)
	바로 클래스 생성자함수입니다. 이 함수는 클래스가 생성되는 동시에 호출되는 함수예요.
	주로 클래스의 멤버변수를 초기화하는 용도로 사용해줍니다.
	*/

	//I5 생성자 오버로딩과 위임
	/**
	멤버함수도 함수같이 오버로딩 가능합니다. 생성자도 멤버함수니 마찬가지구요.
	line41의 생성자는 아무 매개변수도 없기 때문에 객체를 선언하면 자동으로 year과 name을 정의합니다.

	때문에 우리는 Rabbit객체 선언과정에서 이름과 나이를 정의 하고싶을 때 생성자 오버로딩을 합니다.
	위에 클래스의 line 60을 봅시다.
	*/
	cout << "\n**생성자 오버로딩\n";
	Rabbit testRabbit3; //Rabbit() 호출
	Rabbit testRabbit4{ 3,"Sarah" }; //Rabbit(int y, string n) 호출
	testRabbit3.rabbit_inform();
	testRabbit4.rabbit_inform();

	

	//I5 생성자 위임
	/**
	하지만 생성자 오버로딩을 하나하나 하다보면 꽤나 지친답니다.. 그래서 프로그래머들은 꾀를 내어
	이 수작업을 아~주 조금이라도 줄이는 방법을 생각해냈어요.
	line 80을 봅시다.

	*/
	cout << "\n**생성자 위임\n";
	Rabbit testRabbit5{ 3 }; //브레이크걸고 디버그해보세요.

	//I3 멤버함수 클래스 외부에서 선언하기
	cout << "\n**멤버함수 프로토타입\n";
	testRabbit5.rabbit_inform();
	/**
	앞의 내용의 흐름에 방해되어 뒤에서 설명하게 되는데,
	제가 Rabbit객체의 정보를 보기 위해 호출하는 rabbit_inform()함수를 보시면
	클래스안에서 만들어진 것이 아닌 밖에서 만들어진 것을 볼 수 있습니다. (line93, line98)

	이거는 우리가 함수를 만들때 프로토타입으로 메인함수의 위에 함수의 이름만 정의 한 후
	메인함수 아래에 그 함수를 정의하는 것과 정확히 일치해요.

	다만 그냥 메인함수 밑에 함수를 정의하면 되는 프로토타입과는 달리,
	어떤 클래스의 멤버함수를 정의하는 것인지 명시하기 위해 함수 이름 앞에 "클래스이름::"을 붙여줍니다. (line105)

	//I5 외부에서 선언 주의사항
	초반에는 살짝 헷갈릴 수 있는 부분이 rabbit_inform()함수는 클래스 내부에 있는것으로 인식을 하기 때문에,
	Rabbit클래스의 private에 있는 멤버들(year,name)에게 자유롭게 접근가능합니다!
	*/
	
	//I4 소멸자
	cout << "\n**소멸자 호출\n";
	/**
	line 102를 보면 ~Rabbit()함수가 있는데 "소멸자"라고 부릅니다.

	객체가 할당해제될때 자동으로 호출되어요. 
	지금까지 위에서 선언한 함수들은 모두 스택메모리에 할당되기 때문에
	메인함수가 끝나면 모두 소멸자가 호출됩니다.

	소멸자에 대해서는 다음주에 객체를 힙메모리에 선언할 때 어떻게 다루는지를 설명하며,
	더욱 자세하게 다룰 예정입니다. 아주 중요한 내용이니 잘 정리해보겠습니다!
	소멸자는 기본적으로 객체가 선언된 순서대로 호출됩니다.(testRabbit1,2,3,4...순으로 호출)
	*/

	//이상으로 정리를 마칩니다.
}
profile
개발잘하고싶다

0개의 댓글