시험공부

(2025-2) Introduction to programming(2) 6. Classes

norepinephrine 2025. 12. 18. 14:50

0. 미리 알면 좋은 것 (30초 정리)

  • 객체: 어떤 “상태(데이터)”와 “행동(함수)”를 한 덩어리로 묶은 것
  • 클래스: 객체를 만들기 위한 “설계도”
  • 캡슐화: 내부 데이터는 숨기고, 밖에는 필요한 기능만 공개
  • 접근 지정자: public(밖에서 호출 OK), private(클래스 안에서만)

1. 가장 작은 클래스 만들어 보기

코드

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

class Person {
public:                // <-- 바깥에서 쓸 수 있는 영역(인터페이스)
    string name;       // (처음엔 공개로 두고 느낌만 봅시다)
    int age;

    void introduce() { // 멤버 함수: 객체가 하는 행동
        cout << "Hi, I'm " << name << " and I'm " << age << " years old.\\n";
    }
};

int main() {
    Person p;          // 객체 생성(설계도=클래스, 실제물건=객체)
    p.name = "Minji";  // 점(.)으로 멤버 접근
    p.age = 20;
    p.introduce();     // 멤버 함수 호출
}

한 줄 설명

  • class Person { ... }; : Person이라는 “설계도” 정의
  • public: : 이 아래는 밖에서 접근 가능
  • name, age : 데이터(상태), introduce() : 행동(연산)
  • Person p; : 실제 인스턴스(객체) 만들기
  • p.name, p.introduce() : 점 연산자로 멤버 접근/호출

의사코드

사람 객체 p를 만든다
이름과 나이를 채운다
자기소개 함수를 호출한다 -> 화면에 문장 출력

첫 느낌 잡기용이니 이해만 하고 넘어갑시다. 실제로는 내부 데이터를 보통 private으로 숨겨요.


2. 캡슐화: 데이터를 숨기고 함수로만 조작

코드

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

class Person {
public:
    // 생성자(객체를 만들 때 초기화하는 함수)
    Person(string n, int a) : name(n), age(a) {} // 멤버 초기화 리스트

    // 읽기 전용(조회자) 함수들: 객체 상태를 안전하게 외부에 제공
    string getName() const { return name; } // const: 이 함수는 상태를 바꾸지 않음
    int    getAge()  const { return age;  }

    // 행동(연산)
    void haveBirthday() { age += 1; }

    void introduce() const {
        cout << "Hi, I'm " << name << " (" << age << ")\\n";
    }

private:
    // 내부 상태: 바깥에서 직접 못 건드림(오직 멤버 함수로만)
    string name;
    int age;
};

int main() {
    Person p("Minji", 20);
    p.introduce();         // "Minji (20)"
    p.haveBirthday();      // 내부에서 안전하게 나이 변경
    cout << p.getAge() << "\\n"; // 21
}

핵심 포인트

  • private 멤버는 외부에서 직접 수정 금지 → 잘못된 상태를 방지
  • 상태 변화는 반드시 검증 로직을 가진 멤버 함수를 통해서만
  • const 멤버 함수(...() const)는 “읽기만 한다”는 약속

의사코드

사람을 (이름, 나이)로 초기화
자기소개 출력
생일 처리(나이 +1)
현재 나이 조회하여 출력


3. this 포인터와 체이닝(연쇄 호출)

체이닝은 “연달아 점점점 찍어서” 코드를 간결하게 만드는 관용구입니다.

코드

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

class Counter {
public:
    Counter() : value(0) {}

    Counter& inc() {   // 자기 자신을 참조로 반환 -> 체이닝 가능
        value += 1;
        return *this;  // *this: 객체 자신
    }

    Counter& add(int x) {
        value += x;
        return *this;
    }

    int get() const { return value; }

private:
    int value;
};

int main() {
    Counter c;
    c.inc().add(3).inc();  // 연쇄 호출
    cout << c.get() << "\\n"; // 5
}

핵심 포인트

  • 멤버 함수 안의 this는 “나 자신”을 가리키는 포인터
  • return *this; → “나 자신(참조)”을 돌려주므로 c.inc().add(3).inc() 가능

의사코드

카운터를 0으로 시작
1 증가 → 3 더하기 → 1 증가
현재 값 출력


4. const 멤버 함수 & 상수 객체

코드

struct Box {
    int w = 0, h = 0;

    int area() const {      // const: 객체를 바꾸지 않는 함수
        return w * h;
    }
};

int main() {
    const Box b{3,4};       // 상수 객체
    // b.w = 10;            // 컴파일 에러: 상수 객체는 수정 불가
    cout << b.area() << "\\n"; // const 함수만 호출 가능(OK)
}

핵심 포인트

  • 상수 객체const 멤버 함수만 호출 가능
  • “읽기 전용” 메서드는 반드시 ...() const로 표시

5. 생성자(Constructors) 제대로 이해하기

코드

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

class Sales_data {
public:
    // 1) 기본 생성자: 비워 두면 컴파일러가 합성
    Sales_data() = default;

		// 2) 한 멤버만 받는 생성자
    Sales_data(const string& s) : bookNo(s) {}

    // 3) 모든 정보로 초기화
    Sales_data(const string& s, unsigned n, double price)
        : bookNo(s), units_sold(n), revenue(n * price) {}

    string isbn() const { return bookNo; }

    double avg_price() const {
        return units_sold ? revenue / units_sold : 0;
    }

    Sales_data& combine(const Sales_data& rhs) {
        units_sold += rhs.units_sold;
        revenue    += rhs.revenue;
        return *this;
    }

private:
    string  bookNo;
    unsigned units_sold = 0; // 인클래스 초기화(C++11)
    double   revenue    = 0.0;
};

int main() {
    Sales_data a;                       // 기본 생성자
    Sales_data b("ISBN-1234");         // ISBN만
    Sales_data c("ISBN-1234", 10, 12); // 수량 10, 단가 12 → revenue 120
    cout << c.isbn() << ", avg=" << c.avg_price() << "\\n";
}

핵심 포인트

  • 멤버 초기화 리스트(: bookNo(s), ...)는 “대입”이 아니라 “초기화”라서 정확·효율적
  • = default로 “기본 동작 그대로”를 명시적으로 허용
  • 멤버에 인클래스 초기값을 둘 수 있음(C++11+)

6. 멤버 vs 비멤버(자유 함수), 그리고 friend

입출력이나 두 객체의 대칭 연산은 보통 비멤버로 두는 게 인터페이스가 더 자연스럽습니다. 대신 내부 접근이 필요하면 friend로 “특별 허용”.

코드

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

class Sales_data {
public:
    Sales_data() = default;
    Sales_data(const string& s, unsigned n, double p)
        : bookNo(s), units_sold(n), revenue(n*p) {}

    string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data& rhs) {
        units_sold += rhs.units_sold;
        revenue    += rhs.revenue;
        return *this;
    }

    double avg_price() const {
        return units_sold ? revenue / units_sold : 0;
    }

    // 비멤버 함수들이 private에 접근할 수 있게 허용
    friend istream& read(istream&, Sales_data&);
    friend ostream& print(ostream&, const Sales_data&);
    friend Sales_data add(const Sales_data&, const Sales_data&);

private:
    string  bookNo;
    unsigned units_sold = 0;
    double   revenue    = 0.0;
};

// ---- 비멤버들 ----
istream& read(istream& is, Sales_data& item) {
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price; // 입력: ISBN 수량 단가
    item.revenue = item.units_sold * price;
    return is;
}
ostream& print(ostream& os, const Sales_data& item) {
    os << item.bookNo << " " << item.units_sold
       << " " << item.revenue << " " << item.avg_price();
    return os;
}
Sales_data add(const Sales_data& lhs, const Sales_data& rhs) {
    Sales_data sum = lhs;   // 복사본
    sum.combine(rhs);       // 누적
    return sum;             // (RVO/NRVO)
}

int main() {
    Sales_data total;
    if (read(cin, total)) {        // 첫 레코드 읽기
        Sales_data trans;
        while (read(cin, trans)) { // 다음 레코드들
            if (total.isbn() == trans.isbn())
                total.combine(trans);   // 같은 책이면 누적
            else {
                print(cout, total) << "\\n"; // 묶음 출력
                total = trans;              // 새 묶음 시작
            }
        }
        print(cout, total) << "\\n";        // 마지막 묶음
    } else {
        cerr << "No data?!\\n";
    }
}

의사코드(입력 파일을 ISBN별로 묶어 합치기)

total = 첫 줄 읽기
반복(다음 줄 trans 읽기 성공):
    만약 total.isbn == trans.isbn:
        total에 trans 누적
    아니면:
        total 출력
        total = trans 로 교체(새 묶음 시작)
반복 종료 후 total 출력

핵심 포인트

  • 왜 비멤버?
    • print(cout, x)처럼 스트림이 왼쪽에 오면 자연스럽습니다.
    • 두 피연산자(또는 인자)가 대칭일 때 멤버보다 비멤버가 깔끔합니다.
  • *friend*로 내부 접근 허용하되, 남용은 금지(캡슐화 유지가 기본)

7. const/비const 오버로드 & mutable

화면을 나타내는 Screen 예제로 “상수/비상수 버전”과 “체이닝”을 함께 보겠습니다.

코드

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

class Screen {
public:
    using pos = string::size_type;

    Screen(pos h, pos w, char ch) : height(h), width(w), contents(h*w, ch) {}

    // 위치 이동 & 문자를 설정 -> 체이닝 위해 참조 반환
    Screen& move(pos r, pos c) { cursor = r * width + c; return *this; }
    Screen& set(char ch)       { contents[cursor] = ch;  return *this; }

    // const/비const 버전 모두 제공: 상황에 맞게 선택됨
    const Screen& display(ostream& os) const { do_display(os); return *this; }
    Screen&       display(ostream& os)       { do_display(os); return *this; }

private:
    void do_display(ostream& os) const { os << contents << "\\n"; }

    string contents;
    pos cursor = 0;
    pos height = 0, width = 0;
    mutable size_t access_count = 0; // const 함수에서도 수정 OK(예: 디버깅/통계)
};

int main() {
    Screen scr(3, 5, '.');          // 3행 5열, '.'으로 채움
    scr.move(1,2).set('#').display(cout); // 이동→'#'설정→출력
    const Screen cscr = scr;
    cscr.display(cout);             // const 버전 호출
}

핵심 포인트

  • 같은 이름이라도 매개변수/const 여부로 다른 함수가 됨(오버로딩)
  • 상수 객체는 const 버전만 호출 가능
  • mutable은 “논리적 상수성”을 위해 예외적으로 허용(접근 횟수 카운트 등)
  • using pos = string::size_type; 를 int 대신 쓰는 이유
    using pos = string::size_type;
    
    
    이건 단순히 "이름 바꾸기"가 아니라,
    • string::size_type은 문자열 길이나 인덱스를 나타내는 데 쓰이는 타입이에요.
    • 즉, “이 변수는 음수가 될 수 없는 인덱스용 변수” 라는 걸 코드 수준에서 표현해줍니다.
    int i;               // 그냥 정수 (음수도 가능)
    string::size_type n; // 인덱스 전용 정수 (unsigned)
    
    
    → 컴파일러와 협업하는 "의미 부여"인 셈이죠.
    🔹 2️⃣ 안전성(safety) 측면
    string s = "hello";
    for (int i = 0; i < s.size(); ++i)  // ❗ 비교 타입 불일치 경고
    
    
    s.size()의 반환 타입은 string::size_type (즉 unsigned)이에요.warning: comparison between signed and unsigned integer expressionsunsigned는 0 이상만 표현, signed는 음수도 표현.
    🔹 3️⃣ 플랫폼 독립성 (Portability)시스템 size_type 내부 실제 타입
    32비트 unsigned int
    64비트 unsigned long long
    만약 int를 쓰면,
    • 64비트 환경에서 문자열 길이가 int 최대값(약 21억)을 넘으면 오버플로우가 발생합니다.
    • 하지만 string::size_type을 쓰면 자동으로 그 환경에 맞게 충분한 크기의 타입이 사용돼요 ✅
    즉, 코드가 운영체제/컴파일러에 독립적으로 동작합니다.
    🔹 4️⃣ 의미적으로 일관성 유지그렇다면 height, width, cursor 같은 인덱스 관련 멤버들도
    class Screen {
    public:
        using pos = string::size_type;  // 인덱스 타입 일관성
    private:
        pos height = 0;
        pos width = 0;
        pos cursor = 0;
        string contents;
    };
    
    
    👉 이렇게 하면 “화면의 한 칸”도, “문자열의 한 칸”도
    ✅ 요약 비교표항목 int string::size_type (pos)
    부호 여부 signed (음수 가능) unsigned (0 이상)
    경고 발생 가능성 있음 (s.size() 비교 시) 없음
    플랫폼 호환성 한정적 (32bit 기준) 표준적, 64bit 안전
    의미 표현력 단순 정수 "문자열 인덱스" 의미 명확
    일관성 라이브러리와 불일치 std::string과 일관

    💬 정리하자면:
  • pos를 typedef 또는 using으로 정의하는 이유는라는 걸 코드 수준에서 명확히 표현하고, 플랫폼 차이를 흡수하기 위함이에요.
  • “이 변수는 문자열이나 화면의 위치(position)를 나타내는 안전한 타입이다”
  • 모두 같은 인덱스 타입으로 관리할 수 있어서 설계가 깔끔하고 안전해집니다.
  • string::size_type으로 선언하는 게 논리적으로 맞습니다.
  • Screen 클래스 내부에서는 문자열(std::string contents)을 사용하죠.
  • string::size_type은 표준 라이브러리 구현에 따라 다를 수 있습니다.
  • 비교할 때 내부적으로 형변환이 일어나서 예상치 못한 결과가 생길 수 있어요.
  • 🔸 이유
  • 그런데 i는 int (signed)이니까 컴파일러가 이렇게 경고를 냅니다:
  • ⚠️ 문제점 — int를 쓰면 이런 경고가 뜰 수 있음
  • 👉 의미 있는 타입으로 제약을 주는 역할을 합니다.
  • 🔹 1️⃣ pos는 단순한 미관용 별칭이 아니다

8. 전방 선언(Forward Declaration)과 불완전 타입

둘이 서로를 참조해야 할 때, “이름만 먼저 알려주고” 나중에 정의합니다.

코드(개념 예)

#include <vector>
using namespace std;

class Screen; // 전방 선언: 아직 크기/구성 모름

class Window_mgr {
public:
    using ScreenIndex = vector<Screen>::size_type;
    void clear(ScreenIndex i);
};

// 이제야 Screen 정의
class Screen {
    // 특정 멤버 함수만 친구로
    friend void Window_mgr::clear(ScreenIndex);
    // ...
};

핵심 포인트

  • 전방 선언만 있으면 포인터/참조 선언은 가능하지만, 실제 객체 생성은 불가(크기 모름)
  • 상호 참조 시 선언 순서를 잘 짜야 컴파일 에러를 피합니다

9. class vs struct 단 하나의 차이

  • 기본 접근 지정자만 다릅니다.
    • class의 기본은 private, struct의 기본은 public
  • 나머지 기능은 동일. 팀 스타일에 따라 선택합니다.

10. 파일 분할(헤더/소스)과 컴파일 모델

  • 헤더(.h/.hpp): 클래스 선언(인터페이스)만 둠
  • 소스(.cpp): 멤버 함수 정의(구현) 둠
  • 여러 .cpp에서 같은 헤더를 #include하여 하나의 선언을 공유
  • 인클루드 가드(또는 #pragma once)를 사용해 중복 포함 방지
// Sales_data.h
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
#include <iostream>
class Sales_data {
    // 선언들...
};
std::istream& read(std::istream&, Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);
Sales_data add(const Sales_data&, const Sales_data&);
#endif


11. 처음 공부하는 학생용 연습 로드맵 (따라하면 끝)

  1. 클래스 뼈대 만들기
    • class Student { public: string name; int score; };
    • 객체를 만들고 값 채워서 출력
  2. 캡슐화 적용
    • private로 바꾸고 set/get 함수 추가
    • 점수는 0~100만 허용(검증 로직 추가)
  3. 생성자와 초기화 리스트
    • Student(string n, int s) 생성자 추가
    • 생성자에서 범위 체크 후 잘못되면 기본값으로
  4. 체이닝 구현
    • addBonus(int b)가 this를 반환하여 stu.addBonus(5).addBonus(5) 가능하게
  5. const 멤버 연습
    • print()는 출력만 하므로 const 붙여보기
    • const Student cs(...); cs.print(); 테스트
  6. 비멤버 + friend 패턴
    • read(cin, stu), print(cout, stu)를 비멤버로 만들고,
    • 내부 접근이 필요하면 friend 선언
  7. 작은 프로젝트
    • Sales_data 예제를 그대로 타이핑
    • 입력 파일(ISBN 수량 단가...)을 읽어 ISBN별 합산 출력

12. 자주 막히는 포인트, 즉답 가이드

  • 왜 굳이 private로 숨겨?
  • → 객체의 “유효한 상태”를 스스로 지키게 하려는 것. 외부에서 막 바꾸면 불변식이 깨짐.
  • const 멤버 함수는 꼭 필요한가?
  • → 읽기 전용이라는 계약이 생기고, 상수 객체에서도 호출 가능해짐.
  • 멤버 vs 비멤버 선택 기준?내부 접근이 필요하면 friend로 예외 허용.
  • → 두 인자가 대칭(덧셈, 비교 등)·입출력처럼 스트림이 좌측이면 비멤버가 자연스러움.
  • 초기화 리스트를 왜 써? 대입하면 안 돼?const/참조 멤버는 초기화 리스트 필수.
  • → 대입은 “이미 만들어진 후 바꾸기”, 초기화는 “만들 때부터 값 주기”.

13. 마지막으로: 오늘 배운 핵심을 한 화면 요약

  • 클래스 = 상태 + 행동 (설계도)
  • 캡슐화: private로 숨기고 public 함수로만 조작
  • this / 체이닝: return *this; 패턴
  • const 멤버 함수: 읽기 전용 계약, 상수 객체 호환
  • 생성자 & 초기화 리스트: 정확·효율적인 초기 상태 보장
  • 비멤버 + friend: 인터페이스를 더 자연스럽게, 캡슐화는 유지
  • 전방 선언: 상호 참조 시 선언 순서 정돈
  • 파일 분할: 선언(헤더) / 정의(소스)로 깔끔하게