PART 2. 객체 지향 프로그래밍
- chapter 7. 클래스의 활용
1. 객체 포인터 (Object Pointer)
- 객체도 일반 변수처럼 정적 메모리 할당이나 동적 메모리 할당을 할 수 있다.
- 정적 생성된 객체에 접근할 때는 .(dot) 연산자를 사용하고, 동적 생성된 객체에 접근할 때는 ->(Arrow)연산자를 사용한다.
2. this 포인터 (this Pointer)
- 자기 자신을 가리키는 객체 포인터.
- this는 C++의 예약어이므로 식별자로 사용할 수 없으며, 따로 선언하지 않아도 자동으로 생성된다.
- 멤버 변수의 이름과 멤버 함수의 매개 변수의 이름이 동일한 경우 멤버 변수의 이름 앞에 'this->'를 붙여야한다.
- 그 외에는 함수의 매개변수나 반환값으로 자기자신을 사용할 때 등의 경우에 사용된다.
3. const 한정자 (const modifier)
- const 멤버 변수 (const member variable) : 한번 초기화하면 이후 값을 변경할 수 없는 매개 변수.
- 멤버 초기화 목록을 사용해서 초기화시킨다.
- const 멤버 함수 (const member function) : 멤버 변수의 값을 변경시킬 수 없는 멤버 함수.
- 비 멤버 함수에는 한정자를 붙일 수 없다.
- const 멤버 함수내에서 선언한 변수의 값은 변경할 수 있다.
- const 멤버 함수와 일반 멤버 함수는 중복 정의가 가능하다.
- 중복 정의시에는 const 객체가 호출하는 함수는 const 멤버 함수가 되고, 일반 객체가 호출하는 함수는 일반 멤버 함수가 된다.
- const 객체 (const object) : 멤버 변수의 값을 변경할 수 없는 객체.
- 객체를 생성할 때 class name 앞에 const를 붙인다.
- const 객체로는 const 멤버 함수만 호출 할 수 있다.
- 함수의 매개 변수로 객체 참조자를 사용하는 경우에 객체 참조자 앞에 const를 붙이는 경우가 많다.
4. 객체와 연산자의 관계 (Relation with object and function)
- 객체가 함수의 매개변수로 전달되는 경우
- 객체 포인터가 함수의 매개변수로 전달되는 경우
- 객체 참조자가 함수의 매개변수로 전달되는 경우
- 함수가 객체를 반환하는 경우
- 함수가 객체 포인터를 반환하는 경우
- 함수가 객체 참조자를 반환하는 경우
- ...
5. 임시 객체 (temporary object)
- 임시객체는 수식의 중간 계산 결과를 저장하거나 함수가 객체를 반환하는 등의 경우에 생성된다.
- 임시 객체는 주소값을 따로 저장하지 않으면 다음 문장으로 넘어갈 때 소멸된다.
6. 정적 멤버 (static member)
- 정적 멤버 변수 (static member variable) = 전역 멤버 변수 : class의 모든 객체가 공유하는 변수.
- 정적 멤버 변수는 전역 변수처럼 사용되지만 사용 범위가 해당 클래스 내부로 제한된다.
- 특정 객체의 소속이 아니므로 객체를 생성하지 않아도 사용할 수 있다.
- static멤버는 class 내부에서 초기값을 줄 수 없으므로 class 외부에서 초기화를 시켜줘야 한다.
- 멤버 함수내에서 값을 변경하는 것도 가능하나 생성자 등에서 초기화하면 매번 초기화되므로 의미가 없다.
- 정적 멤버 함수 (static member function) = 전역 멤버 함수 : class의 모든 객체가 공유하는 함수.
- 정적 멤버 함수도 특정 객체의 소속이 아니므로 객체를 생성하지 않아도 호출 할 수 있다.
- 정적 멤버 함수는 객체가 생성되기 전에 이미 메모리를 할당 받음으로 일반 멤버 변수나 일반 멤버 함수, this 포인터를 사용할 수 없다.
- 대신 정적 변수와 지역 변수, 정적 함수는 사용 가능하다.
- 정적 상수 멤버 변수 (const static member variable) : class의 모든 객체가 공유하는 상수.
- 상수는 어차피 class의 모든 객체가 사용하는 변하지 않는 값이므로 정적 상수 변수로 선언하면 메모리를 아낄 수 있다.
- 정수형 정적 상수 멤버 변수는 클래스 내부에서 초기값을 줄 수 있다.
- 정수형이 아닌 경우 멤버 초기화 목록을 사용해서 초기값을 줘야한다.
7. 객체 배열(object array)
- 객체도 일반 변수처럼 배열을 만들 수 있다.
- 초기화를 할 때는 생성자를 호출해서 초기화한다.
- 배열의 이름은 포인터 첫번째 요소를 가리키는 포인터 상수이다.
- 따라서 객체 배열의 이름은 객체 배열의 첫번째 객체에 대한 포인터와 같다.
8. 클래스와 클래스 간의 관계 (relate with class and other class)
- 사용(use) : 하나의 클래스가 다른 클래스를 사용한다.
- A클래스의 멤버 함수에서 B클래스의 멤버 함수들을 호출하는 관계. 그러므로 A클래스에서 B클래스의 객체를 갖고 있어야 한다. (포함)
- 포함(has-a) : 하나의 클래스가 다른 클래스를 포함한다.
- A클래스가 다른 클래스(들)을 갖고 있는 관계.
- 상속(is-a) : 하나의 클래스가 다른 클래스를 상속한다.
- 다음 장에서 설명.
car.h
#pragma once #include <iostream> #include <string> using std::cout; using std::cin; using std::endl; using std::string; class Car { protected: int speed; int gear; string color; public: int pub; void print() const; // void print(); int getSpeed() { return speed; } void setSpeed(int speed); void isFaster(Car* p); Car(int s = 0, int g = 1, string c = "white"); ~Car(); }; class SerialCar : public Car { const int serial; //const 멤버 변수 public: int temp; //const 객체 실험용 int getSerial() const { return serial; } void print() const; //const 멤버 함수 SerialCar(int Num, int s = 0, int g = 1, string c = "white"); }; class Temp { int t; public: Temp(int n) { t = n; cout << "temp 생성자 호출 값은 " << t << endl; } }; class StaticClass { static int n; // static int n2 = 100; //불가 public: //정수형 정적 상수 변수는 in-class initializer가 가능 static const int R = 1000; //static const double d = 100.23; //불가 static void showN(); //static void showR() const; //에러! 정적 멤버 함수에서 형식 한정자를 사용할 수 없습니다. StaticClass(); ~StaticClass(); }; class useclass { Car useCar; public: void useT() { useCar.print(); } };
car.cpp
#include "Car.h" void Car::print() const { cout << "속도 : " << speed << " 기어 : " << gear << " 색상 : " << color << endl; } // //void Car::print() //{ // cout << "오버로드된 print 함수" << endl; //} void Car::setSpeed(int speed) { if (speed > 0) { this->speed = speed; // this->speed는 멤버 변수, speed는 매개 변수 } else { this->speed = 0; } } void Car::isFaster(Car* p) { if (this->getSpeed() > p->getSpeed()) { //현재 객체의 멤버 함수 > 매개 변수의 객체 함수 cout << this->color; //현재 객체의 멤버 변수 } else { cout << p->color; //매개 변수의 멤버 변수 } cout << " color 자동차가 더 빠름" << endl; } Car::Car(int s, int g, string c) { cout << "생성자 호출" << endl; speed = s; gear = g; color = c; } Car::~Car() { cout << "소멸자 호출" << endl; } void SerialCar::print() const { cout << "시리얼 넘버" << serial; Car::print(); //const 함수 내에서는 const 함수만 호출 가능하다. //speed = 10; //const 함수 내에서 변수의 값 변경 불가 //setSpeed(50); //const가 아닌 함수 사용 불가(설정자) //cout << getSpeed() << endl; // 값을 변경하지 않는 함수여도 const 함수가 아니면 사용할 수 없다. cout << getSerial() << endl; //그러므로 접근자의 경우에는 const 함수로 만드는 것이 일반적이다. } SerialCar::SerialCar(int Num, int s, int g, string c) : Car(s, g, c), serial(Num) {} int StaticClass::n = 1; void StaticClass::showN() { cout << n << endl; } StaticClass::StaticClass() { n++; } StaticClass::~StaticClass() { n--; }
main.cpp
#include "Car.h" void swapObject1(Car c1, Car c2); void swapObject2(Car* c1, Car* c2); void swapObject3(Car& c1, Car& c2); void swapObject4(Car* c1, Car& c2); Car returnObject1(); int main() { cout << "객체의 정적 생성과 동적 생성" << endl; Car myCar; // 객체의 정적 생성 (기본 생성자로 초기화) myCar.print(); // 정적 생성된 객체의 메소드 호출 Car* pCar = new Car(60, 3, "black"); // 객체의 동적 생성 pCar->print(); // 동적 생성된 객체의 메소드 호출 Car c1(0, 1, "blue"); Car c2(100, 3, "red"); c1.isFaster(&c2); //정적 생성된 객체의 주소값을 받기 위해서 &연산자 사용 myCar.isFaster(pCar); //동적 생성된 객체는 &연산자가 필요하지 않음 cout << endl << "일반 객체와 const 객체" << endl; SerialCar sc(0000); //일반 객체 const SerialCar csc(0001); //const 객체 //const 객체로는 public 멤버 변수의 값도 변경할 수 없다. sc.temp = 10; //csc.temp = 10; //const 객체로는 const 함수만 호출 가능하다. //cout << csc.getSpeed(); cout << csc.getSerial(); // csc.print(); cout << endl << "디폴트 대입 연산자에 의한 객체 대입" << endl; //객체끼리의 대입은 따로 연산자 재정의를 하지 않아도 디폴트 대입 연산자에 의해서 가능하다. c1 = myCar; c1.print(); myCar.print(); cout << endl << "객체를 함수의 매개변수로 전달하는 경우" << endl; cout << endl; swapObject1(c1, c2); cout << "swapObject1 밖에서 호출" << endl; cout << "c1 : "; c1.print(); cout << "c2 : "; c2.print(); cout << endl; swapObject2(&c1, &c2); cout << "swapObject2 밖에서 호출" << endl; cout << "c1 : "; c1.print(); cout << "c2 : "; c2.print(); cout << endl; swapObject3(c1, c2); cout << "swapObject3 밖에서 호출" << endl; cout << "c1 : "; c1.print(); cout << "c2 : "; c2.print(); cout << endl; swapObject4(&c1, c2); cout << "swapObject4 밖에서 호출" << endl; cout << "c1 : "; c1.print(); cout << "c2 : "; c2.print(); cout << endl << "임시 객체" << endl; string s1 = "temporary "; string s2 = "object"; const char* sp = (s1 + s2).c_str(); // .c_str() : string의 첫번째 문자의 주소를 반환한다. cout << "sp : " << sp << endl; // sp는 임시 객체의 시작 주소를 가지고 있으므로 이 문장에서는 이미 사라진 주소를 가지고 있는 셈이다. // 쓰레기 값이 출력됨 string s3; s3 = s1 + s2; // 대입 연산자는 임시객체의 값을 복사한다. sp = s3.c_str(); // 여기서는 사라진 임시 객체가 아니라 새롭게 생성된 s3의 주소를 가지고 출력하는 셈이다. cout << "sp : " << sp << endl; Car(10, 2, "wow"); // 명시적으로 생성한 임시객체. 이 문장이 끝나면 사라진다. cout << endl << "임시 객체를 참조자에 저장" << endl; //Car& rc = Car(10, 5, "rc"); // 컴파일 에러! 비const 참조에 대한 초기 값은 lvalue여야 합니다. { const Car& rc = Car(10, 5, "rc"); // 임시객체를 참조자로 저장. 참조자가 존재하는 동안에는 사라지지 않는다. ( rc.print(); } //rc.print(); //범위를 벗어나면 참조자도 소멸된다. Temp(3); // 임시 객체를 생성할 때는 생성자가 호출된다. Temp tempC = Temp(5); // 임시 객체를 객체 변수에 복사한다는 의미. //(객체 변수라는 표현이 생소한데 그냥 그 객체를 받을 수 있는 공간이 만들어진 것으로 보임, 생성자 호출 X) cout << endl << "함수가 객체를 반환하는 경우" << endl; returnObject1().print(); // 반환된 임시 객체를 통해 멤버 함수 호출 //임시 객체의 포인터나 참조를 반환하는 경우는 어차피 의미가 없으므로 생략. cout << endl << "정적 멤버 변수와 정적 멤버 함수" << endl; //정적 멤버 변수나 함수는 객체 생성 전에도 호출 가능 cout << StaticClass::R << endl; StaticClass::showN(); StaticClass sca; StaticClass scb; StaticClass scc; //정적 멤버 변수이므로 값은 모두 같다. sca.showN(); scc.showN(); //scope를 벗어난 변수도 사라지긴 하는데 아무래도 동적 할당, 해제 한 수를 세는게 일반적이다. StaticClass* scp = new StaticClass; StaticClass::showN(); delete scp; StaticClass::showN(); cout << endl << "객체 배열" << endl; //객체 배열 선언과 초기화 (생성자를 호출해서 초기값을 준다. 각각 다른 생성자를 사용할 수도 있다.) Car objArray[3] = { Car(1000,2,"obj"),Car(200) }; objArray[0].print(); objArray[1].print(); objArray[2].print(); //초기값을 주지 않은 객체 배열의 멤버는 디폴트 생성자로 초기화 된다. //객체 배열을 이용한 멤버 함수 호출 cout << "objArray->speed : " << objArray->getSpeed() << endl; //객체 배열을 이용한 멤버 변수 접근 objArray[1].pub = 1; cout << "(objArray+1)->pub : " << (objArray+1)->pub << endl; //+연산이 ->연산보다 먼저 실행되어야 하므로 괄호가 꼭 필요하다. //*를 안 붙이는 이유는 아직 정리가 안되므로 패스 cout << endl << "객체 배열과 포인터" << endl; //배열의 이름은 포인터이다. cout << objArray->getSpeed() << endl; cout << (objArray+1)->getSpeed() << endl; //*()을 쓰지 않는다. cout << endl << "클래스와 클래스 간의 관계" << endl; useclass use, has_a; SerialCar is_a(0000); //사용을 하려면 포함을 하고 있을 수 밖에 없다. cout << "사용 관계(use) : "; use.useT(); cout << "포함 관계(has-a) : "; has_a.useT(); cout << "상속 관계(is-a) : "; cout << is_a.getSpeed(); } //함수의 인자로 객체를 전달 할 수 있다. //물론 그냥 전달하면 call by value이므로 값이 복사된다. //즉 실제 값은 변경되지 않는다. void swapObject1(Car c1, Car c2) { Car temp; temp = c1; c1 = c2; c2 = temp; cout << "swapObject1 안에서 호출" << endl; cout << "c1 : "; c1.print(); cout << "c2 : "; c2.print(); } //함수의 인자로 객체의 포인터도 전달 할 수 있다. //포인터로 전달하면 주소가 전달되므로 call by reference이다. //즉 실제 값을 변경할 수 있다. //다만 *연산자를 사용하여야 하므로 조금 번거롭다. void swapObject2(Car* c1, Car* c2) { Car temp; temp = *c1; *c1 = *c2; *c2 = temp; cout << "swapObject2 안에서 호출" << endl; cout << "c1 : "; (*c1).print(); cout << "c2 : "; (*c2).print(); } //함수의 인자로 객체의 참조자를 전달 할 수 있다. //참조자로 전달하면 call by reference이므로 값을 변경할 수 있다. //포인터 변수를 만들지도 않고, 안에서 *연산자도 쓰지 않으므로 C++에서는 가장 많이 쓰이는 방식이다. void swapObject3(Car& c1, Car& c2) { Car temp; temp = c1; c1 = c2; c2 = temp; cout << "swapObject3 안에서 호출" << endl; cout << "c1 : "; c1.print(); cout << "c2 : "; c2.print(); } //참조자와 레퍼런스를 같이 쓸 수도 있다. void swapObject4(Car* c1, Car& c2) { Car temp; temp = *c1; *c1 = c2; c2 = temp; cout << "swapObject4 안에서 호출" << endl; cout << "c1 : "; (*c1).print(); cout << "c2 : "; c2.print(); } Car returnObject1() { Car temp(100, 3, "ro1"); return temp; }