Class 개념

주상돈·2024년 12월 26일

TIL

목록 보기
4/53

클래스

C++은 객체지향 프로그래밍언어이다. 그 첫걸음으로 class에 대해 배워보겠다.

class없는 성적 관리 프로그램 코드이다.

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

//3 과목의 평균을 구하는 함수
double getAvg(int kor, int eng, int math)
{
    return (kor + eng + math ) / 3.0f;
}
//두 개의 수중 최대값을 반환하는 함수
int maxNum(int num1, int num2)
{
    if(num1 >= num2) return num1;
    else return num2;
}
//3과목중 가장 높은 점수를 반환하는 함수
int getMax(int kor, int eng, int math)
{
   return maxNum( maxNum(kor, eng), math);  
}

int main()
{
    int kor[3];
    int eng[3];
    int math[3];
    
    //점수 입력
    for(int i = 0 ; i < 3; i++)
    {
        cin >> kor[i] >> eng[i] >> math[i];
    }
    //각 학생의 평균 점수와 과목 최대 점수를 출력
    for(int i = 0 ; i < 3; i++)
    {
        cout << getAvg(kor[i], eng[i], math[i]) << endl;
        cout << getMax(kor, eng, math) << endl;
    }
    return 0; 
}

이 코드에는 어떤 문제가 있을까?
1. 과목의 개수가 늘어나면 함수의 호출 방식이 변경된다. 즉 기존의 코드는 동작x
2. 성적 관리를 하는 데이터가 모두 노출되어 있다. 사용자의 다른 코드와 혼용되어서 문제를 발생시킬 수 있음.

class의 정의

자동차를 생각해보자. 브레이크를 밟으면 차가 멈추고, 페달을 밟으면 가속이 된다는 걸 우리는 알고 있다. 우리는 운전 할 때 이것만 알면 크게 무리가 없다. 브레이크의 세부 스펙을 알필요는 없다.
class도 동일하다. 우리는 동작을 공개하고 세부 데이터는 공개하지 않는다. 그래야 변화에 유연하게 대처가 가능하기 때문이다.
class는 크게 동작과 데이터로 구성된다. 위 성적 관리 프로그램을 기준으로 동작과 데이터를 나눠보면 아래와 같다.
1 동작
•가장 큰 점수 반환/ 과목의 평균 계산
2 데이터
•각 과목의 점수

코드로 한번 학생 클래스를 정의 해보자.

class Student
{
    //동작 정의(이를 멤버함수라고 한다.)
    double getAvg();
    int getMaxNum();
    
    //데이터 정의(이를 멤버변수라고 한다.)
    int kor[3];
    int eng[3];
    int math[3];
};

Class 내부,외부 구현

멤버함수구현(클래스 내부)

#include <iostream>
#include <algorithm> //max 함수 사용
#include <string>
using namespace std;
class Student
{
    //동작 정의(이를 멤버함수라고 한다)
    double getAvg()
    {
        return (kor + eng + math ) / 3.0; 
    }
    int getMax()
    {
        return max(max(kor, eng), math); 
    }
    
    //데이터 정의(이를 멤버변수라고 한다.)
    int kor;
    int eng;
    int math;
};

멤버함수구현(클래스 외부)

#include <iostream>
#include <algorithm> //max 함수 사용
#include <string>
using namespace std;
class Student
{
    //동작 정의(이를 멤버함수라고 한다)
    double getAvg();
    int getMaxScore();
    //데이터 정의(이를 멤버변수라고 한다.)
    int kor;
    int eng;
    int math;
};
Student::getAvg()
{
    return (kor + eng + math) / 3.0;
}
Student::getMax()
{
    return max(max(kor, eng), math); 
}

접근제어

class는 사용자에게 접근을 허용여부 제어가 가능하다. 접근 제어는 크게 public,private가 있다.
아무것도 작성하지 않으면 접근 제어는 private가 된다.
private는 외부에서 접근 시 에러가 발생한다.

private에 접근 시도

#include <iostream>
#include <algorithm> //max 함수 사용
#include <string>
using namespace std;

class Student
{
    //동작 정의(이를 멤버함수라고 한다)
    double getAvg();
    int getMaxScore();
    //데이터 정의(이를 멤버변수라고 한다.)
    int kor;
    int eng;
    int math;
};

double Student::getAvg()
{
    return (kor + eng + math) / 3.0;
}

int Student::getMaxScore()
{
    return max(max(kor, eng), math);
}

int main()
{
    Student s;
    s.getAvg();
    
    return 0;
}


private멤버에 액세스할 수 없다고 뜬다. 당연하다.

public, private으로 접근제어

#include <iostream>
#include <algorithm> //max 함수 사용
#include <string>
using namespace std;

class Student
{
public:
    //동작 정의(이를 멤버함수라고 한다)
    double getAvg();
    int getMaxScore();

private:
    //데이터 정의(이를 멤버변수라고 한다.)
    int kor;
    int eng;
    int math;
};

double Student::getAvg()
{
    return (kor + eng + math) / 3.0;
}

int Student::getMaxScore()
{
    return max(max(kor, eng), math);
}

int main()
{
    Student s;
    s.getAvg();
    
    return 0;
}


오류가 나지않는다. 코드에서 오류가 발생하지 않는 이유는 Student 클래스의 getAvg()와 getMaxScore() 메서드가 클래스 내부에서 정의된 멤버 함수이기 때문이다. C++에서 private 접근 지정자는 같은 클래스 내부에서만 접근할 수 있도록 제한한다. 하지만 멤버 함수는 클래스 내부에 정의되므로 private 멤버 변수에 접근할 수 있다.

getter와 setter

그렇다면 private에 있는 변수를 제어해야 할 때는 어떤 방법을 사용할까?
이 때 사용 하는게 getter와 setter이다.
멤버변수를 바꿀 때 setter를, 값을 가져올 때 getter를 사용한다.

getter와 setter를 적용한 class

#include <iostream>
#include <algorithm> //max 함수 사용
#include <string>

using namespace std;

class Student
{
public:
    //동작 정의(이를 멤버함수라고 한다)
    double getAvg();
    int getMaxScore();

	    void setMathScore(int math)
    {
        this->math = math;
    }
    void setEngScore(int eng)
    {
        this->eng = eng;
  
    }
    void setKorScore(int kor)
    {
        this->kor = kor;
    }

    int  getMathScore() { return math; }
    int  getEngScore() { return eng; }
    int  getKorScore() { return kor; }

private:
    //데이터 정의(이를 멤버변수라고 한다.)
    int kor;
    int eng;
    int math;
};

double Student::getAvg()
{
    return (kor + eng + math) / 3.0;
}

int Student::getMaxScore()
{
    return max(max(kor, eng), math);
}

int main()
{
    Student s;

    //점수 입력(변수로 해도 되지만 간단히 
    s.setEngScore(32);
    s.setKorScore(52);
    s.setMathScore(74);

    //평균 최대점수 출력
    cout << s.getAvg() << endl;
    cout << s.getMaxScore() << endl;

    return 0;
}

생성자

생성자는 객체가 생성될 때 한번 호출 된다.
보통 필요한 멤버 변수를 초기화 하거나 객체가 동작할 준비를 하기 위해 사용한다. 생성자는 반환형이 없고, class 이름과 같은 함수 형태이다.

기본 생성자

#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    // 기본 생성자
    Person() {
        name = "Unknown";
        age = 0;
    }

    void display() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    Person p; // 기본 생성자 호출
    p.display();
    return 0;
}

매개변수가 있는 생성자

#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    // 매개변수가 있는 생성자
    Person(string n, int a) {
        name = n;
        age = a;
    }

    void display() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    Person p("Alice", 25); // 매개변수가 있는 생성자 호출
    p.display();
    return 0;
}

기본매개변수가 있는 생성자

#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    // 기본 매개변수가 있는 생성자
    Person(string n = "DefaultName", int a = 18) {
        name = n;
        age = a;
    }

    void display() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    Person p1;              // 기본값 사용
    Person p2("Bob", 30);   // 값을 지정
    p1.display();
    p2.display();
    return 0;
}

잘못된 매개변수 전달로 에러 발생

#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    // 매개변수가 있는 생성자
    Person(string n, int a) {
        name = n;
        age = a;
    }
};

int main() {
    Person p("Tom"); // 에러: 생성자에 필요한 매개변수 부족
    // 컴파일 에러: "no matching function for call to 'Person::Person(const char [4])'"
    // 매개변수 두 개를 요구하는 생성자에 하나의 매개변수만 전달하여 매칭되지 않음
    p.display();
    return 0;
}


Person의 매개변수는 string타입의 n과 int타입 a 두개인데 main()함수에서 Person p("Tom");즉 string타입의 n만 매개변수 전달로 오류가난다. 또한 Person클래스에 display메서드도 정의되어 있지않기때문에 오류가난다.

선언만 하고 정의하지 않아 에러 발생

#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    // 생성자를 선언만 하고 정의하지 않음
    Person(string n, int a);
};

int main() {
    Person p("Alice", 25); // 선언된 생성자의 정의가 없으므로 컴파일 에러 발생
    cout << "Name: " << p.name << ", Age: " << p.age << endl;
    return 0;
}

기본 생성자를 잘못 호출해서 에러 발생

#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    void temp() {}
    // 생성자를 선언만 하고 정의하지 않음
    Person(string n, int a) {}
};

int main() {
    Person p("a", 30);
    Person p2;

    p.temp();
    p2.temp();


    return 0;
}

이제 Student에 생성자를 적용해보자.
어차피 3과목의 점수는 무조건 있어야 한다.
하지만. 경우에 따라서는 3과목 점수를 모두 입력하고 싶지 않을 때가 있다. 생성자는 객체가 생성되는 순간 한번 호출됨.

#include <iostream>
#include <algorithm> //max 함수 사용
#include <string>

using namespace std;

class Student
{
public:
    //값이 주어지지 않을경우 기본값을 할당하는 생성자
    Student(int math = 32, int eng = 17, int kor = 52)
    {
        this->math = math;
        this->eng = eng;
        this->kor = kor;
    }
    double getAvg();
    int getMaxScore();

    //동작 정의(이를 멤버함수라고 합니다)
    void setMathScore(int math)
    {
        math = math;
        //this.math = math;와 동일
    }
    void setEngScore(int eng)
    {
        eng = eng;
        //this.eng = eng;와 동일
    }
    void setKorScore(int math)
    {
        math = math;
        //this.math = math;와 동일
    }

    int  getMathScore() { return math; }
    int  getEngScore() { return eng; }
    int  getKorScore() { return kor; }

private:
    //데이터 정의(이를 멤버변수라고 합니다.)
    int kor;
    int eng;
    int math;
};

double Student::getAvg()
{
    return (kor + eng + math) / 3.0;
}

int Student::getMaxScore()
{
    return max(max(kor, eng), math);
}


int main()
{
    Student s;
    //아래와 같이 사용할 수도 있음
    //Student s(1);
    //Student s(1, 2);
    //Student s(32, 52, 74);

    //평균 최대점수 출력
    cout << s.getAvg() << endl;
    cout << s.getMaxScore() << endl;

    return 0;
}

코드 파일 나누기

지금까지 Student class를 구현해봤다 지금까지 모든 class를 하나의 파일에 구현했는데 말씀드렸다시피 내부구현은 사용자가 알필요 없기 때문에 include해서 사용할 수 있는 방식으로 구현해서 제공 한다.
글도 마찬가지다. 소설을 쓰는데 한문장으로 200페이지를 쓰는 경우는 없다. 코드도 한 파일에만 작성하지 않고 파일을 나눠서 작성한다.
헤더 파일과 소스 파일로 크게 나뉠 수 있으며 보통 헤더 파일에 선언을 하고, 소스 파일엔 구현을 한다. 이전에 구현했던 Student클래스를 기준으로 알아보겠다. 최종적으로 파일의 구조는 아래 처럼 될 것이다.

헤더 파일에 Class 정의

#ifndef STUDENT_H_
#define STUDENT_H_
class Student
{
public:
    //값이 주어지지 않을경우 기본값을 할당하는 생성자
    Student(int math = 32, int eng = 17, int kor = 52)
    {
        this->math = math;
        this->eng = eng;
        this->kor = kor;
    }
    double getAvg();
    int getMaxScore();

    //동작 정의(이를 멤버함수라고 합니다)
    void setMathScore(int math)
    {
       this->math = math;
    }
    void setEngScore(int eng)
    {
        this->eng = eng;
    }
    void setKorScore(int math)
    {
      this->math = math;
    }

    int  getMathScore() { return math; }
    int  getEngScore() { return eng; }
    int  getKorScore() { return kor; }

private:
    //데이터 정의(이를 멤버변수라고 합니다.)
    int kor;
    int eng;
    int math;
};
#endif

class를 헤더 파일에 정의할 때 가장 중요한 것은 class가 중복 선언되지 않게 하는 것 이다. 내가 만든 헤더 파일을 여러 파일에서 사용 하다 보면 class가 여러번 정의 될 수 있다. 이를 방지 하기 위해서 #ifndef 구문을 활용 한다.

  1. #ifndef STUDENTH의 의미는 STUDENTH가 정의 되어 있지 않은 경우에만 아래 코드를 수행하라는 의미이다.
  2. #Define STUDENTH는 STUDENTH를 정의한다. #ifndef일 때만 #define이 수행되므로 단 한번만 수행 될 수 있다.
  3. ifnedf가 끝났다는 것을 알려주기 위해 #endif를 작성 한다.
  4. 최종적으로 Student Class는 중복 정의 될 수 없게 된다.

Student class를 헤더파일에 정의(Student.cpp)

#include "Student.h"
#include <algorithm> // max 함수

using namespace std;

double Student::getAvg()
{
    return (kor + eng + math) / 3.0;
}

int Student::getMaxScore()
{
    return max(max(kor, eng), math);
}

소스 파일에서 정의한 멤버함수중 아직 구현되지 않은 부분을 구현하면 된다. 이 때 당연히 구현한 헤더파일을 include 해줘야 한다.
1. 정의된 class가 포함된 Student.h를 include한다.
본인이 만든 헤더 파일을 double quote로(큰 따옴표) C++에서 제공하는 헤더 파일은 <>로 포함한다.
2. 멤버 함수를 정의할때는 클래스 이름::멤버함수와 같이 하고 정의 하면 된다.

Student class를 헤더파일에 정의(main.cpp)

이제 class의 정의와 구현이 끝났으므로 실제 main함수에서 사용해보자.
정의된 class가 포함된 Student.h를 include한다.

#include <iostream>
#include "Student.h"

using namespace std;

int main()
{
    Student s;
    Student s2(1);
    Student s3(1,2);
    Student s4(32,52,74);

    //평균 최대점수 출력
    cout << s.getAvg() << endl;
    cout << s.getMaxScore() << endl;

    return 0;
}

지금까지 배운 내용으로 두 문제정도 풀어볼려고한다.

배터리 관리 클래스 만들기

C++로 간단한 배터리 관리 클래스를 만들어보자.
• 기능
-배터리 잔량(0~100)을 관리한다.
-배터리 사용 및 충전을 할 수 있어야 한다.
-초기 배터리는 0이다.
• 구현할 메서드
-배터리 잔량을 반환하는 메서드
-배터리를 사용하는 메서드(한번 호출될때마다 배터리 잔량 5씩 감소)
-배터리를 충전하는 메서드(한번 호출될때마다 배터리 잔량은 7씩 충전)
• 초기 상태
배터리 잔량의 기본값은 100이다. 해당 값은 생성자에서 설정할 수 있어야 한다.
아래는 완성된 모습

[배터리 관리 클래스 시나리오]
1. 프로그램 시작 시, 초기 배터리 잔량은 100%로 설정됩니다.
2. 배터리를 두 번 사용하여 각 사용 시 5%씩 잔량이 감소합니다.

  • 첫 번째 사용 후 잔량: 95%
  • 두 번째 사용 후 잔량: 90%
  1. 배터리를 한 번 충전하여 7% 잔량이 증가합니다.
    • 충전 후 잔량: 97%
  2. 다시 배터리를 한 번 사용하여 잔량이 5% 감소합니다.
    • 사용 후 잔량: 92%
  3. 각 단계에서 배터리 잔량의 변화가 출력됩니다.

위 시나리오대로 실행했을 때 출력 값은 아래와 같습니다.

Initial charge: 100%
Battery used. Current charge: 95%
Battery used. Current charge: 90%
Battery charged. Current charge: 97%
Battery used. Current charge: 92

Battery.h

#ifndef BATTERY_H_
#define BATTERY_H_

class Battery
{
private:
	int charge; // 배터리 잔량
public:
	// 초기화 리스트를 사용한 생성자: 기본값은 100으로 설정 
	Battery(int initialCharge = 100): charge(initialCharge) {
		if (charge < 0) {
			charge = 0;
		}
		else if (charge > 100) {
			charge = 100;
		}
	}
	// 배터리 사용
	void useBattery();
	// 배터리 충전
	void chargeBettery();
	//배터리 잔량 반환
	int getCharge() const{
		return charge;
	}

};
#endif

Battery.cpp

#include "Battery.h"
#include <iostream>

using namespace std;

void Battery::useBattery() {
	if (charge >= 5) { // 배터리가 5퍼이상이면 5를뺌
		charge -= 5;
	}
	else {		// 5미만이면 0
		charge = 0;
	}
	cout << "Battery used. Current charge: " << charge << "%" << endl;		// 문구 출력
}
void Battery::chargeBettery() {
	if (charge <= 93) {	// 배터리가 93퍼 이하면 7을 더함
		charge += 7;
	}
	else {
		charge = 100;	// 배터리가 93퍼 미만이면 100%
	}
	cout << "Battery charged. Current charge: " << charge << "%" << endl;			// 문구 출력
}

main.cpp

#include <iostream>
#include "Battery.h"

using namespace std;

int main()
{
    Battery battery;

    cout << "Initial charge: " << battery.getCharge() << endl;
    battery.useBattery();
    battery.useBattery();
    battery.chargeBettery();
    battery.useBattery();

    return 0;
}

배터리 헤더파일을 확인하면 생성자를 좀 다르게 선언한다 이게 무엇이냐 하면 초기화 리스트를 사용한 생성자이다.
초기화리스트 초기화 리스트에 관한 내용이다.

실행 결과

두 분수의 곱셈을 할 수 있는 클래스 만들기

C++로 간단하게 두 분수의 곱셈을 하는 클래스를 만들어 보자.
• - Fraction 클래스를 만들어 분수를 표현 하세요
- 분자는 numerator, 분모는 denominator로 관리 합니다.
- 두 분수의 곱의 결과는 기약분수로 출력 합니다.
• 구현할 메서드
- 기본 생성자를 만드세요(numerator = 0 / denominator = 1 )
- 분자의 분모의 수를 입력받는 생성자를 구현하세요
- 배터리 잔량을 반환하는 메서드
- 현재 분수를 출력하는 함수 display를 를 구현하세요
- 기약분수 형태로 만드는 함수 simplify를 구현하세요 이 때 최대공약수를 구하는 함수가 필요할 수 있는데 아래를 참조하세요
아래는 완성된 모습이다.

입력 시나리오:

  • 입력은 사용자로부터 직접 받지 않으며, 프로그램에 하드코딩된 값으로 실행됩니다.
  • Fraction f1(1, 2): 첫 번째 분수는 1/2로 설정됩니다.
  • Fraction f2(3, 4): 두 번째 분수는 3/4로 설정됩니다.
    출력:
    곱한 결과: 3/8

Fraction.h

#ifndef FRACTION_H_
#define FRACTION_H_

// Fraction클래스 정의
class Fraction
{
private:
	int numerator;		// 분자
	int denominator;	// 분모
	int gcd(int a, int b);							// gcd 함수는 Fraction 클래스 내부에서만 사용되므로 private으로 설정
public:
	Fraction() : numerator(0), denominator(1) {}	// 기본 생성자, 분자를0, 분모를1로 초기화.(0/1은 0을 나타냄)
	Fraction(int num, int denom);					// 매개변수가 있는 생성자 사용자로부터 분자와 분모를 입력받아 초기화한다.
	void display();									// 현재 분수를 보여주는 함수
	void simplify();								// 기약분수로 변환하는 함수
	Fraction multiply(const Fraction& other) const;	// 두 분수를 곱하는 함수
};

#endif

Fraction.cpp

#include "Fraction.h"
#include <iostream>

using namespace std;

Fraction::Fraction(int num, int denom)
	: numerator(num), denominator(denom) {
	if (denom == 0) {
		throw std::invalid_argument("Denominator cannot be zero.");
	}
}

int Fraction::gcd(int a, int b) {		//최대 공약수를 계산하는 유클리드 호제법 구현
	while (b != 0) {
		int temp = b;
		b = a % b;
		a = temp;
	}
	return a;	// b가0이 되었을때 최대공약수
}
void Fraction::simplify(){
	int gcd = Fraction::gcd(numerator, denominator);	// 분자와 분모의 최대공약수 계산
	numerator /= gcd;		// 분자를 최대공약수로 나눔
	denominator /= gcd;		// 분모를 최대공약수로 나눔
}

void Fraction::display() {
	cout << numerator << "/" << denominator << endl;
}

Fraction Fraction::multiply(const Fraction& other) const{
	// 새로운 분자
	int newNumerator = numerator * other.numerator;
	// 새로운 분모
	int newDenominator = denominator * other.denominator;
	// 새로운 분수
	Fraction newFraction = Fraction(newNumerator, newDenominator);
	// 새로운 분수를 기약분수로 만듦
	newFraction.simplify();
	return newFraction;
}

main.cpp

#include <iostream>
#include "Fraction.h"

using namespace std;

int main()
{
    // 두 분수 생성
    Fraction f1(1, 2); // 첫 번째 분수: 1/2
    Fraction f2(3, 4); // 두 번째 분수: 3/4

    // 두 분수를 곱하기
    Fraction product = f1.multiply(f2); // f1 * f2

    // 결과 출력
    cout << "곱한 결과: ";
    product.display(); // 결과 분수 출력
    cout << endl;

    return 0;

}

실행 결과

정리

오늘은 클래스에대해 공부해보았다. 헤더파일 소스파일 메인소스파일 이렇게 구현하는게 습관이 안되어있다보니 조금 애를 먹은 것 같다. 그냥 메인소스파일에다가 전부 다 구현하던 습관이있어서.. 하지만 좋은 개발자가 되기위해서는 이렇게 나누어서 구현하는게 좋으니 계속 연습해서 익숙해지는게 목표이다.

0개의 댓글