객체지향프로그래밍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 (각 원소 소멸자 호출)
- 주의 사항:
- delete와 delete [] 구분 (비정상 반환)
- 배열은 동적 할당 시 초기화 불가능
- 메모리 누수(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가지):
- 기존 객체를 이용해 새 객체를 초기화할 때 (예: Person son = father;)
- 값에 의한 호출(Call by Value)로 함수에 객체를 전달할 때 (예: void f(Person p);)
- 함수가 객체를 값으로 리턴할 때 (예: 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))
- 함수 중복의 모호성: 컴파일러가 호출할 함수를 결정하지 못하는 경우
- 형 변환 모호성 (예: int 인자를 float과 double 중 무엇으로 변환할지 모호)
- 참조 매개 변수 모호성 (예: int b와 int &b 구분 모호)
- 디폴트 매개 변수 모호성 (예: 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;)
- 주요 활용:
- 공유: 객체 간 데이터 공유 (예: 원의 총 개수)
- 캡슐화: 전역 변수나 전역 함수를 클래스 내부에 캡슐화 (예: Math::abs())
- 접근 방식:
- 객체명으로 접근 (예: han.sharedMoney)
- 클래스명으로 접근 (예: 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가지 유형:
- 전역 함수를 프렌드로 선언
- 다른 클래스의 멤버 함수를 프렌드로 선언
- 다른 클래스 전체를 프렌드로 선언 (해당 클래스의 모든 멤버 함수가 프렌드가 됨)
- 전방 선언(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가지):
- 클래스의 멤버 함수로 구현 (예: a + b -> a.operator+(b))
- 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;)
- 강제 형 변환 필요. 위험할 수 있음.
- 파생 클래스의 멤버까지 접근 가능
- 업 캐스팅 (Up-casting): 파생 클래스 포인터를 기본 클래스 포인터로 치환 (예: Person* p = new Student();)
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;
}
};