25.05.29 (2) - Class 개념

김영하·2025년 5월 29일

C++

목록 보기
12/32

클래스(Class)

이제는 기본 문법 : 변수, 반복문, 조건문, 포인터, 레퍼런스 etc.
~에서 벗어나 객체지향 에 대해서 학습한다.
"객체지향"은 C++가 일반 C와 궤를 달리하는 부분.

객체지향적으로 코드를 구현하기 용이하게 제공되는 대표적인 문법이
Class


이번에 배울 것

실제로 프로젝트를 진행하다 보면,
한번 완성된 프로젝트는 계속해서 사용되는 경우가 많다
이러다 보니 '개발을 해야하는 시간' 보다
'유지 보수를 해야하는 시간' 이 더 길어지고
이에 필요한 비용도 크게 증가하게 된다.

따라서 유지보수를 용이하게 하고 "재사용성"을 높이는 방향으로
코드를 구현해야 하는데,
C++에선 이를 해결하기 위해 "객체지향 프로그래밍"을 활용한다.


객체 없이 성적 관리 프로그램 만들기

지금까지는 기본 문법을 배우면서
"코드를 완성" 하는 데에 초점을 두었지만,
이젠 어떻게 하면 좀더 유지보수가 편한 "객체지향적" 코딩이 가능할지 생각해야 한다.

가장 쉽게 생각할 수 있는 건:
국어, 영어, 수학 점수 3개를 받아서
평균과 최대 점수를 구하는 2개의 함수를 만드는 것.

클래스를 사용하지 않음)

#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[i], eng[i], math[i]) << endl;
    }
    return 0; 
}

요구사항도 만족하고 잘 동작하지만
"재사용성이 높은" 코드는 아니다. = 수정사항이 발생했을 때 영향이 크다

1) 과목이 늘어나면 함수 인자가 늘어나므로
기존 코드가 동작하지 않게 된다

2) 성적 관리 데이터가 모두 노출되어 있다

이러면 다른 코드에 의해서 데이터가 변경될 수 있다


클래스의 정의

예를 들어, 우리가 운전을 할 때,
자동차의 종류에 따라 각자 세부 스펙 등은 다르겠지만
아래 이유 등으로 인해 다른 자동차라고 해도 운전하는 게 그렇게 어렵지는 않다.

  • 페달을 밟으면 속도가 올라간다
  • 브레이크를 밟으면 속도가 줄어든다
  • 기타 자동차의 세부사항을 몰라도 운전 제어에는 큰 문제가 없다

만약 자동차의 방대한 옵션들을 다 알아야만 운전이 가능하다고 하면
그만큼 머리가 아플 수 밖에 없다.

  • "사용성이 떨어지고" :
    사용자가 알아야할 정보가 늘어나, 고려해야 할 예외가 증가해서
  • "구현 난이도가 증가하니까" :
    사용 시 발생할 수 있는 예외사항들을 전부 고려해야 해서 코드가 복잡해진다

따라서 필수적인 기능을 제외한 세부사항은 그냥 숨겨두는 게 좋다


클래스에는 2가지가 존재하는데,
1) 멤버 함수
필요한 동작은 멤버 함수로 정의,
보통 외부에서도 접근 가능하게 만들어준다

2) 멤버 변수
세부 데이터는 멤버 변수로 관리,
멤버 함수에서 필요한 정보 혹은
클래스 자체에서 내부 연산 시 필요한 정보를 멤버 변수로서 관리한다.
보통은 외부에서의 접근을 막아둔다.


이 정보를 토대로 '학생 클래스'를 만들자면:

  • 동작 : 가장 큰 점수 반환
  • 데이터 : 각 과목 점수 => 세부사항 이 되는 거고, 직접 제어 불가능
class Student
{
    //동작 정의(멤버함수)
    double getAvg();
    int getMaxNum();
    
    //데이터 정의(멤버변수)
    int kor[3];
    int eng[3];
    int math[3];
}; // 마지막 세미콜론 필수

클래스 내부 구현

클래스의 멤버함수 를 구현하는 방법은 2가지가 있는데:

  • 클래스 내부에서 멤버 함수의 본문까지 직접 정의하기
  • 내부에서 멤버 함수 선언만 해주고, 클래스 외부에서 구현
    결론적으로는 두번째 방법을 많이 쓴다

내부에서 구현)

#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> 
#include <string>
using namespace std;
class Student
{
    //동작 정의(이를 멤버함수라고 합니다)
    double getAvg();
    int getMaxNum();
    //데이터 정의(이를 멤버변수라고 합니다.)
    int kor;
    int eng;
    int math;
};

double Student::getAvg()
{
    return (kor + eng + math) / 3.0;
}
int Student::getMaxNum()
{
    return max(max(kor, eng), math);
    // 다른 방법 return max({ kor, eng, math });
}

double Student::getAvg()
int Student::getMaxNum()
= 중요한 건 Student:: 부분으로,
"Student 클래스에 속해 있는 것"이라는 구문이다.

나중에는,
클래스 부분을 헤더 파일 로 구현하고
클래스 외부를 소스 파일 로 구현해서
파일을 나눠서 작성할 예정.


접근제어

클래스에만 있는 개념.

클래스의 멤버 함수나 멤버 변수에 접근할 때는,
객체 뒤에 "멤버 접근 연산자" . 을 사용한다.

또한 클래스에선 "접근 지정자" 를 사용해서 멤버로의 접근 권한을 제어할 수 있다.
C++ 접근 지정자에는 public private protected 등이 있다.
클래스를 사용할 때 접근 지정자를 명시하지 않으면 디폴트는 private.
'일반적으로' 멤버함수는 public으로 지정하고
멤버변수는 private으로 지정한다.

이 접근제어는 "외부로부터의 접근" 제어라서,
클래스 내부에서는 서로 문제없이 접근 가능하다

int main()
{
    Student s; // 메인에서 클래스가 변수로 선언되며 "메모리에 올라감"
    // 이 상태를 객체라 하고, 인스턴스화 했다고 한다
    s.getAvg(); // public 이면 정상작동, private이면 에러
    
    return 0;
}

클래스객체 를 간혹 혼용하는 경우가 있는데
클래스 는 = 청사진, 설계도, 붕어빵 기계 등으로 보통 비유되고 (설계만 된 상태)
객체 는 = 클래스가 메모리에 올라간 상태 를 말한다


getter / setter

블루프린트에서 get 노드 set 노드 와 유사한 느낌으로,
멤버 변수를 바꿀 때 setter를, 값을 가져올 때 getter

아까 말했듯 보통
멤버 함수는 public 이고 멤버 변수는 private 인데,
그러면 우리가 성적관리를 할 때 멤버 변수, 각 학생의 성적을 어떻게 입력하는가?

여기서 getter setter 를 활용해
멤버 변수를 다루는 함수를 구현해 둔다. (함수는 public 이니까)
=> 외부에서 변수를 직접 다루지 않고, 클래스에서 구현한 함수로만 다룰 수 있도록
직접 제어하지 않고 멤버함수로 다룬다는 게 핵심.

class Student
{
public:
    double getAvg();
    int getMaxScore();

	void setMathScore(int math)
    {
        this->math = math; 
        // "this" 이 클래스 내부의 변수 math에
        // 외부에서 들어온 int math == math 를 대입해라
    }
    void setEngScore(int eng)
    {
        this->eng = eng;
  
    }
    void setKorScore(int kor)
    {
        this->kor = kor;
    }
    // void 부분이 set
    
    // 이게 get
    int  getMathScore() { return math; }
    int  getEngScore() { return eng; }
    int  getKorScore() { return kor; }

private:
    int kor;
    int eng;
    int math;
};

...

int main()
{
    Student s;

    s.setEngScore(32);
    s.setKorScore(52);
    s.setMathScore(74);

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

    return 0;
}

void 부분, setter 함수에서 나오는 this-> 부분은 :
"클래스 내부의" 라고 해석하면 된다
this->math = math; 는 = 멤버 변수 math 에 외부에서 받은 math 값을 대입해라.

변수를 직접 수정하게 되면 어떻게 잘못 사용될 우려가 있고,
이렇게 함수를 통해서만 접근할 수 있게 해두면서 더 안전한 관리 가능
"코드 작성자가 의도한 대로만 값이 수정될 수 있게"
추가로 함수에 조건을 추가해주면 더욱 안전.

getter 함수 부분은 짧고 명확한데,
그냥 함수 이름에 리턴값만 있어서
멤버 변수의 값을 '확인만' 시켜주는


생성자

생성자
: 객체를 생성할 때마다 단 한번 자동으로 호출되는 특별한 멤버 함수
= 객체가 생성되고 나면 마음대로 호출할 수 없다

생성자는 멤버 변수를 초기화하거나 객체가 동작할 준비를 위해 사용되며,
반환형을 명시하지 않고
클래스 이름과 '동일한 이름을 가진 함수'로 정의된다.

  1. 정의된 클래스를 변수로 선언하면
    => 해당 객체가 메모리에 올라간다 = 인스턴스화
    : 클래스는 설계도, 객체 혹은 인스턴스는 설계도에 의해 만들어진 실제 결과물.

  2. 객체가 생성될 때, 멤버 변수를 포함해
    필요한 정보들이 메모리에 올라간다. => 이 작업이 완료되면 생성자가 호출됨.

기본 생성자)

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;
}

매개변수가 있는 경우)

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;
}

기본 매개변수가 있는 경우)

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;
}

구조만 다르고 멤버 변수를 초기화해준다는 결과는 비슷

cf. 생성자를 선언만 하고 정의해주지 않으면)

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;
}

생성자 적용

이제 위에서 구상한 Student 클래스에 생성자를 만들어주는데,
이 클래스는 '3개 과목의 점수'를 받아 '제일 큰 점수'와 '평균'을 반환해야 한다.

따라서 이 클래스는
= 점수만 있으면 동작을 해서 최대점수와 평균을 반환해주는
= 3개 과목을 인자로 받으면 함수를 써먹을 수 있는 생성자가 필요.
++ 기본값까지 만들어주기
ex)

class Student {
public:
Student(int x = 3, int y = 5, int z = 7) { // 3과목 점수 받기=>멤버변수 초기화
cout << x << " " << y << " " << z << endl;
}

private:
int x,y,z; // 멤버 변수 선언
}; // 클래스 마무리에 세미콜론 필수.

ex2)

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

using namespace std;

class Student
{
public:
    // 생성자, 기본값은 50으로
    Student(int math = 50, int eng = 50, int kor = 50)
    {
        this->math = math;
        this->eng = eng;
        this->kor = kor;
    }
    
    //멤버함수 선언
    double getAvg();
    int getMaxScore();

    //setter
    void setMathScore(int math)
    {
        this->math = math;
        //this.math = math;와 동일
    }
    void setEngScore(int eng)
    {
        this->eng = eng;
    }
    void setKorScore(int kor)
    {
        this->kor = kor;
    }

    //getter
    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(100);
    //Student s(100, 27);
    //Student s(32, 52, 74);

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

    return 0;
}

코드 나누기

앞서 언급했듯이 이제는
클래스에 있는 선언 부분과 구현-정의 부분을 분리해서
헤더 파일(내부 구현, 사용자가 알 필요 없는 부분)과 소스 파일 로 나누어줄 예정
이렇게 나누어준 뒤 필요한 곳에서 헤더를 include 해서 활용

선언부 = 헤더 / 구현부 = 소스파일 이렇게도 말하고
위 예시들에서도 봤듯이 보통
클래스 안에서는 멤버함수의 선언만 하고
멤버함수의 동작내용은 클래스 외부에서 작성하기 때문에
클래스 부분 = 헤더 / 외부 = 소스 파일 이라고도 할 수 있겠다
다만 개념 자체는 선언부/구현부 로 생각하는게.

  • 왜 굳이 파일을 나눠주는가?>
    '책' 을 예로 들어서
    헤더 #include <~> 는 책의 목차 같은 것이고
    소스 파일은 책의 내용인 건데,
    목차 자체가 우리가 알듯이 간단하게 무슨 내용을 다루는지 제목처럼 적혀있는 거고
    목차를 구구절절 적지 않는 것처럼.

글도 방대한 분량으로 글을 쓰려면,
한번에 몇백몇천 자를 쭉 적는 것보다
테마로 나누어서 내용을 적어나가는 편이 더 쉬움.
또, 테마, 내용, 테마, 내용 이렇게 나누어 놓아야지
테마-내용-테마-내용 이렇게 한 곳에 다 적다가 파일이 날아가면...

이렇게 헤더 파일 은 "목차" 의 역할을 해주고
본 내용은 소스 파일

대표적으로 우리가 '함수' 를 중복 정의하게 되면 => 에러가 발생하는데
코드 파일 하나에 클래스의 선언과 정의를 같이 두었을 때,
우리가 클래스를 (만든 의도대로) 다른 소스 파일에서 동일한 클래스를 사용하다가
"다른 파일에서 사용하는 라이브러리에 중복된 함수가 나타나 버리면,
중복 정의가 되어버려서 어떻게 할 방법이 없어진다"
==>
그래서 함수의 선언부만 헤더로 분리해서 include 하는 방식으로 사용을 해서
라이브러리를 다수 사용할 때 중복 정의 를 회피한다.
이렇게 분리해주면 "선언만 한 상태"니까


따라서 이제부터는
1) 클래스 내부를 파악하기 쉽고
2) 중복 정의를 막기 위해

선언부를 => 헤더로, 구현부를 => 소스로 나눠주자.


student.h 에 클래스 정의

student.h 는 위 사진에 나온 헤더파일명

클래스를 헤더 파일에 정의할 때
가장 중요한 것은 =
해당 클래스가 "중복 선언" 되지 않도록 하는 것.

만든 헤더 파일을 여러 파일에서 가져다 사용하다 보면
클래스가 여러 번 정의될 수 있기 때문에,
이를 방지하기 위해 #ifndef 라는 구문을 사용한다.
#ifndef = if not defined 라는 뜻으로,
"클래스가 정의되지 않은 상태인지" 확인하는 역할을 한다.

  • #ifndef STUDENT_H_의 의미는 =
    STUDENT_H_가 정의되어 있지 않은 경우에만
    아래 코드를 수행하라는 의미

  • #define STUDENT_H_STUDENT_H_를 정의하는데,
    #ifndef일 때만 #define이 수행되므로
    단 한 번만 수행됨.

  • #ifndef가 끝났다는 것을 알려주기 위해 #endif를 작성합니다.

이러면 최종적으로 클래스가 중복 정의될 수 없게 된다.
아래는 현재 헤더 파일 예시)

#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();

private:
    int kor;
    int eng;
    int math;
};
#endif

이렇게 student.h 헤더 파일을 만들었으니
소스 파일에 적용, #include "헤더파일명"
그리고 소스파일에서 클래스 멤버함수들의 내용을 작성해주면 된다.


만든 클래스를 메인 함수에서 사용

방금 두 과정으로 클래스의 선언부와 구현부 작성이 끝났기 떄문에,
실제 main 함수에서의 사용례

#include <iostream>
#include "student.h" // 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;
}
profile
내일배움캠프 Unreal 3기

0개의 댓글