devRenio

중간고사 예상문제

객체지향프로그래밍II(김정준) 중간고사 예상문제

1. this 포인터 (4장)

  • 정의: 객체 자신을 가리키는 포인터
  • 작동: 멤버 함수 내에서만 사용, 컴파일러가 묵시적 매개 변수로 전달
  • 용도 1: 멤버 변수와 매개 변수 이름 구분
  • 용도 2: 객체 자신의 주소(참조) 리턴 (연산자 중복 시 유용)
  • 제약: static 멤버 함수에서 사용 불가 (객체 생성 전 호출 가능성)
class Circle {
  int radius;
public:
  Circle(int radius) {
    this->radius = radius; // 용도 1
  }
  Circle& setRadius(int radius) {
    this->radius = radius;
    return *this; // 용도 2
  }
};

2. 객체의 동적 생성 및 해제 (4장)

  • 동적 할당: new 연산자, 힙(heap) 메모리 사용, 프로그램 실행 중 할당
  • 동적 해제: delete 연산자, 힙에 메모리 반환
  • 단일 객체: new Circle(), new Circle(20) (생성자 호출), delete pCircle (소멸자 호출)
  • 객체 배열: new Circle[n] (기본 생성자 호출), delete [] pArray (각 원소 소멸자 호출)
  • 주의 사항:
    • deletedelete [] 구분 (비정상 반환)
    • 배열은 동적 할당 시 초기화 불가능
    • 메모리 누수(Memory Leak): delete 누락 시
    • 이중 해제 오류 / 정적 메모리 해제 오류
class Circle {
    int radius;
public:
    Circle() : radius(1) { }
    Circle(int r) : radius(r) { }
    ~Circle() { }
};

void dynamic_test() {
    Circle *pDonut = new Circle(20);
    Circle *pArray = new Circle[10];

    pDonut->getArea();
    pArray[0].getArea();

    delete pDonut;
    delete [] pArray;
}

3. 참조(Reference) (5장)

  • 정의: 이미 존재하는 변수(객체)에 대한 다른 이름(별명)
  • 선언: & 참조자 사용 (예: int &refn = n;)
  • 특징:
    • 이름만 생성, 별도 공간 할당 없음
    • 원본 변수의 메모리 공간 공유
    • 선언 시 반드시 초기화 필요
  • 주요 용도 1: 참조에 의한 호출 (Call by Reference)
    • 참조 매개 변수 사용 (예: void swap(int &a, int &b))
    • 매개 변수 공간이 생성되지 않고 실인자 변수 공간 공유
    • 함수 내에서 참조 매개 변수 조작 시 실인자 변수 변경됨
    • 장점: 값에 의한 호출(Call by Value)의 객체 복사 비용(생성/소멸) 없음
  • 주요 용도 2: 참조 리턴
    • 값이 아닌 변수 공간 자체(에 대한 참조)를 리턴 (예: char& find(…))
    • 리턴값을 l-value로 사용 가능 (예: find(name, 0) = ‘S’;)
    • (주의) 함수 종료 시 사라지는 지역 변수의 참조를 리턴하면 안 됨
class Circle {
    int radius;
public:
    Circle() : radius(1) {}
    void setRadius(int r) { radius = r; }
};

// 용도 1: 참조에 의한 호출 (Call by Reference)
void readRadius(Circle &c) {
    int r;
    cin >> r;
    c.setRadius(r);
}

// 용도 2: 참조 리턴
int arr[] = {10, 20, 30};
int& getElement(int index) {
    return arr[index];
}

4. 복사 생성자(Copy Constructor) (5장)

  • 정의: 복사 생성자(Copy Constructor), 객체의 복사본이 생성될 때 호출되는 특별한 생성자
  • 형식: ClassName(const ClassName& c) (자기 클래스에 대한 참조 매개 변수를 가짐)
  • 얕은 복사 (Shallow Copy):
    • 디폴트 복사 생성자 (사용자가 선언하지 않으면 컴파일러가 자동 삽입)
    • 멤버 변수 값을 1:1로 단순 복사
    • 포인터 멤버의 경우 주소 값만 복사 (동적 할당된 메모리 공유)
    • 문제점: 소멸자 호출 시, 공유하던 메모리를 이중 해제(double-delete)하여 오류 발생
  • 깊은 복사 (Deep Copy):
    • 얕은 복사 문제를 해결하기 위해 복사 생성자를 명시적으로 정의
    • 포인터 멤버를 위해 별도의 메모리 공간을 동적 할당한 후, 원본의 내용을 복사
    • 원본과 사본이 독립적인 메모리를 가져 메모리 공유 문제 없음
  • 복사 생성자 자동 호출 시점 (3가지):
    1. 기존 객체를 이용해 새 객체를 초기화할 때 (예: Person son = father;)
    2. 값에 의한 호출(Call by Value)로 함수에 객체를 전달할 때 (예: void f(Person p);)
    3. 함수가 객체를 값으로 리턴할 때 (예: return mother;)
class Person {
    char* name;
    int id;
public:
    Person(int id, const char* name) {
        this->id = id;
        int len = strlen(name);
        this->name = new char [len+1];
        strcpy(this->name, name);
    }
    ~Person() {
        if(name) delete [] name;
    }

    // 깊은 복사 생성자
    Person(const Person& p) {
        this->id = p.id;
        int len = strlen(p.name);
        this->name = new char [len+1]; // 1. 별도 메모리 할당
        strcpy(this->name, p.name);  // 2. 내용 복사
    }
};

void f(Person p) {
    // 2. 값에 의한 호출 시 p의 복사 생성자 호출
}

Person g() {
    Person mother(2, "Jane");
    return mother; // 3. 값 리턴 시 복사 생성자 호출
}

int main() {
    Person father(1, "Kitae");
    Person son = father; // 1. 객체로 초기화 시 복사 생성자 호출
    f(father);
    Person daughter = g();
}

5. 함수 중복(Function Overloading) (6장)

  • 정의: 동일한 이름의 함수가 공존하는 것 (다형성의 한 종류)
  • 성공 조건: 함수 이름은 같으나 매개 변수의 개수타입이 달라야 함
  • 실패 조건: 리턴 타입만 다른 경우는 중복 실패 (컴파일러가 구분 불가)
  • 적용 범위: 일반 함수, 클래스 멤버 함수, 생성자 함수
  • 소멸자: 중복 불가 (매개 변수를 가지지 않음)
  • 디폴트 매개 변수: 함수 중복을 간소화하는 데 사용 가능 (예: Circle(int r=1))
  • 함수 중복의 모호성: 컴파일러가 호출할 함수를 결정하지 못하는 경우
    1. 형 변환 모호성 (예: int 인자를 floatdouble 중 무엇으로 변환할지 모호)
    2. 참조 매개 변수 모호성 (예: int bint &b 구분 모호)
    3. 디폴트 매개 변수 모호성 (예: msg(int)msg(int, string s=””) 구분 모호)
// 1. 형 변환 모호성
float square(float a) { return a*a; }
double square(double a) { return a*a; }

// 2. 참조 매개 변수 모호성
int add(int a, int b) { return a+b; }
int add(int a, int &b) { b=a+b; return b; }

// 3. 디폴트 매개 변수 모호성
void msg(int id) { ... }
void msg(int id, string s="") { ... }

// 생성자 중복 (간소화)
class Circle {
    int radius;
public:
    Circle(int r = 1) { radius = r; }
};

6. static 멤버 (6장)

  • 정의: 프로그램 시작 시 생성, 클래스 당 하나만 생성되며 모든 객체가 공유하는 멤버
  • 용어: 클래스 멤버 (vs. non-static인스턴스 멤버)
  • 생명 주기: 프로그램 시작 시 생성, 프로그램 종료 시 소멸 (객체와 무관)
  • 선언 및 초기화: 클래스 내 static 키워드로 선언, 전역 범위에서 :: 연산자로 초기화 필수 (예: int Person::sharedMoney = 10;)
  • 주요 활용:
    1. 공유: 객체 간 데이터 공유 (예: 원의 총 개수)
    2. 캡슐화: 전역 변수나 전역 함수를 클래스 내부에 캡슐화 (예: Math::abs())
  • 접근 방식:
    1. 객체명으로 접근 (예: han.sharedMoney)
    2. 클래스명으로 접근 (예: Person::sharedMoney)
  • 제약 사항 (static 멤버 함수):
    • non-static 멤버 (변수, 함수) 접근 불가
    • this 포인터 사용 불가
    • (이유: 객체가 생성되기 전에도 호출 가능하기 때문)
  • 허용 (non-static 멤버 함수):
    • static 멤버 (변수, 함수) 접근 가능
class Circle {
    int radius;
    static int numOfCircles;
public:
    Circle(int r = 1) : radius(r) {
        numOfCircles++;
    }
    ~Circle() {
        numOfCircles--;
    }

    double getArea() {
        return 3.14 * radius * radius;
    }

    static int getNumOfCircles() {
        return numOfCircles;
    }
};

int Circle::numOfCircles = 0;

int main() {
    Circle *p = new Circle[10];

    cout << Circle::getNumOfCircles() << endl; // 10

    delete [] p;

    cout << p[0].getNumOfCircles() << endl; // 0
}

7. friend 함수 (7장)

  • 정의: 클래스의 멤버는 아니지만, friend 키워드로 선언되어 해당 클래스의 모든 멤버(private, protected 포함)에 접근할 수 있는 함수 또는 클래스
  • 필요성: 클래스 멤버로 선언하기는 부적절하나, 클래스의 private 멤버에 접근해야 하는 외부 함수가 있을 때 사용 (예: 연산자 중복)
  • 특징: friend는 멤버 자격을 부여받은 것이지, 멤버 함수는 아님 (호출 방식이 다름)
  • friend 선언 3가지 유형:
    1. 전역 함수를 프렌드로 선언
    2. 다른 클래스의 멤버 함수를 프렌드로 선언
    3. 다른 클래스 전체를 프렌드로 선언 (해당 클래스의 모든 멤버 함수가 프렌드가 됨)
  • 전방 선언(Forward Declaration): 프렌드 선언 시, 클래스나 함수가 정의되기 전에 이름이 사용될 경우 컴파일 오류 방지를 위해 필요 (예: class Rect;)
class RectManager {
public:
    bool equals(Rect r, Rect s);
    void copy(Rect& dest, Rect& src);
};

bool equals(Rect r, Rect s);

class Rect {
    int width, height;
public:
    Rect(int w=0, int h=0) : width(w), height(h) {}

    // 유형 1: 전역 함수를 프렌드로 선언
    friend bool equals(Rect r, Rect s);

    // 유형 2: 다른 클래스의 멤버 함수를 프렌드로 선언
    friend bool RectManager::equals(Rect r, Rect s);

    // 유형 3: 다른 클래스 전체를 프렌드로 선언
    friend class RectManager;
};

// 유형 1 구현
bool equals(Rect r, Rect s) {
    return (r.width == s.width && r.height == s.height);
}

// 유형 2 구현
bool RectManager::equals(Rect r, Rect s) {
    return (r.width == s.width && r.height == s.height);
}

// 유형 3 구현
void RectManager::copy(Rect& dest, Rect& src) {
    dest.width = src.width;
    dest.height = src.height;
}

8. 연산자 중복(Operator Overloading) (7장)

  • 정의: 기존 C++ 연산자에 새로운 의미를 정의 (객체에 적용), 다형성의 한 종류
  • 목적: 프로그램 가독성 향상
  • 구현: 연산자 함수(operator function)를 통해 구현 (예: operator+)
  • 구현 방식 (2가지):
    1. 클래스의 멤버 함수로 구현 (예: a + b -> a.operator+(b))
    2. friend를 이용한 외부 함수로 구현 (예: 2 + a -> operator+(2, a))
  • 제약 조건:
    • C++에 본래 있는 연산자만 중복 가능 (예: %%, ## 불가)
    • 피연산자의 개수 변경 불가 (예: 이항연산자를 단항으로 변경 불가)
    • 연산자 우선순위 변경 불가
    • 중복 불가능 연산자: ::, ., .*, ?: (삼항 연산자)
  • 단항 연산자 구분:
    • 전위(Prefix): 매개 변수 없음 (예: Power& operator++())
    • 후위(Postfix): int 타입의 더미 매개 변수로 구분 (예: Power operator++(int x))
  • 참조 리턴: +=, =, 전위 ++, « 등 연속 연산이나 원본 변경 시 Power& 처럼 참조 리턴 ( return *this; )
class Power {
    int kick;
    int punch;
public:
    Power(int k=0, int p=0) : kick(k), punch(p) {}

    // 1. 이항 연산자 (멤버 함수)
    Power operator+ (Power op2) {
        Power tmp;
        tmp.kick = this->kick + op2.kick;
        tmp.punch = this->punch + op2.punch;
        return tmp;
    }

    // 2. 이항 연산자 (friend 함수)
    friend Power operator+ (int op1, Power op2);

    // 3. 단항 전위 연산자
    Power& operator++ () {
        kick++;
        punch++;
        return *this;
    }

    // 4. 단항 후위 연산자
    Power operator++ (int x) {
        Power tmp = *this;
        kick++;
        punch++;
        return tmp;
    }
};

// 2. friend 함수 구현
Power operator+ (int op1, Power op2) {
    Power tmp;
    tmp.kick = op1 + op2.kick;
    tmp.punch = op1 + op2.punch;
    return tmp;
}

int main() {
    Power a(1,2), b(3,4), c;
    c = a + b; // 1. a.operator+(b) 호출
    c = 2 + a; // 2. operator+(2, a) 호출

    b = ++a; // 3. a.operator++() 호출
    c = a++; // 4. a.operator++(0) 호출
}

9. 상속(Inheritance)에서의 접근 지정과 캐스팅 (8장)

  • 접근 지정자 (멤버):
    • private: 선언된 클래스 내에서만 접근 가능. 파생 클래스에서 접근 불가
    • protected: 선언된 클래스 + 파생 클래스에서만 접근 가능. 외부 접근 불가
    • public: 모든 곳에서 접근 가능
  • 상속 지정 (계승): 기본 클래스 멤버의 접근 속성을 파생 클래스에서 어떻게 계승할지 지정
    • public 상속: public -> public, protected -> protected (멤버 속성 그대로 계승)
    • protected 상속: public -> protected, protected -> protected
    • private 상속: public -> private, protected -> private
  • 캐스팅 (객체 포인터):
    • 업 캐스팅 (Up-casting): 파생 클래스 포인터를 기본 클래스 포인터로 치환 (예: Person* p = new Student();)
      • 자동 형 변환. 안전함.
      • 기본 클래스의 멤버만 접근 가능 (파생 클래스 멤버 접근 불가)
    • 다운 캐스팅 (Down-casting): 기본 클래스 포인터를 파생 클래스 포인터로 치환 (예: Student* s = (Student*)p;)
      • 강제 형 변환 필요. 위험할 수 있음.
      • 파생 클래스의 멤버까지 접근 가능
class Person {
protected:
    int age;
public:
    void show() { }
};

class Student : public Person {
    double score;
public:
    void showStudent() {
        cout << age;
        show();
    }
};

int main() {
    Student mark;
    Person* pBase;
    Student* pDer;

    // 1. 업 캐스팅
    pBase = &mark;
    pBase->show();
    // pBase->showStudent(); : ERROR

    // 2. 다운 캐스팅
    pDer = (Student*)pBase;
    pDer->showStudent();
}

10. 상속(Inheritance)에서의 생성자와 소멸자 (8장)

  • 생성자 실행: 파생 클래스 객체 생성 시, 기본 클래스와 파생 클래스의 생성자 모두 실행
  • 생성자 순서: 기본 클래스 생성자 -> 파생 클래스 생성자 순서로 실행
  • 컴파일러의 묵시적 호출:
    • 파생 클래스 생성자에서 기본 클래스의 기본 생성자(매개 변수 없는)를 묵시적으로 호출
    • (참고) 기본 클래스에 기본 생성자가 없으면 컴파일 오류 발생
  • 명시적 생성자 선택:
    • 생성자 초기화 리스트를 사용해 호출할 기본 클래스 생성자를 명시적으로 선택 가능
    • 형식: Derived(int x) : Base(x + 3) { … }
  • 소멸자 순서: 생성자의 반대 순서로 실행 (파생 클래스 소멸자 -> 기본 클래스 소멸자)
class Base {
    int a;
public:
    Base() { cout << "Base() 생성자" << endl; }
    Base(int x) : a(x) { cout << "Base(int) 생성자" << endl; }
    ~Base() { cout << "Base 소멸자" << endl; }
};

class Derived : public Base {
    int b;
public:
    // 1. 묵시적으로 Base() 호출
    Derived() : b(0) {
        cout << "Derived() 생성자" << endl;
    }

    // 2. 명시적으로 Base(x) 호출
    Derived(int x, int y) : Base(x), b(y) {
        cout << "Derived(int, int) 생성자" << endl;
    }

    ~Derived() { cout << "Derived 소멸자" << endl; }
};

int main() {
    Derived d(1, 2);

    return 0;
}

11. 다중 상속(Multiple Inheritance) (8장)

  • 정의: 하나 이상의 기본 클래스로부터 상속받는 것 (예: class Intern : public Student, public Employee)
  • 특징: 나열된 모든 기본 클래스의 멤버를 상속받음
  • 문제점: 다이아몬드 문제 (Diamond Problem)
    • 공통의 기본 클래스(예: Person)를 둔 두 개 이상의 클래스(예: Student, Employee)를 다중 상속 시 발생
    • 공통 기본 클래스의 멤버가 이중으로 상속되는 문제
    • 중복된 멤버에 접근 시 모호성(Ambiguity) 발생
  • 해결책: 가상 상속 (Virtual Inheritance)
    • 중간 파생 클래스 선언 시 virtual 키워드 사용 (예: class Student : virtual public Person)
    • 공통 기본 클래스의 멤버가 최종 파생 클래스에 오직 한 번만 생성되도록 보장
    • 모호성 해결
class Person {
public:
    int age;
};

// 1. 문제 상황 (모호성 발생)
class Student : public Person { };
class Employee : public Person { };

class Intern : public Student, public Employee {
public:
    void setAge(int a) {
        Student::age = a;
    }
};

// 2. 해결 (가상 상속)
class StudentV : virtual public Person { };
class EmployeeV : virtual public Person { };

class InternV : public StudentV, public EmployeeV {
public:
    void setAge(int a) {
        age = a;
    }
};