1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <utility>
using namespace std;
 
int main(void){
    
    
    // make_pair 사용
    pair<intint> p1;
    p1 = make_pair(1020);
 
   
    // 생성자 이용
    pair<intint> p2(30,40);
    
    
    // ( (x, y) , (z, w) )
    pair<pair<intint>pair<doubledouble>> pp2;
 
 
cs


'C++ > C++ 문법' 카테고리의 다른 글

#09 - 1 가상함수와 추상 클래스  (1) 2018.10.08
#8-5 가상 상속  (0) 2018.10.07
#8-3 상속과 생성자, 소멸자  (0) 2018.10.07
#8-2 상속과 객체 포인터  (0) 2018.10.07
#8-1 상속  (0) 2018.10.07

|9.1| 상속 관계에서의 함수 중복


< 예시 : 상속 관계에서 함수를 중복하는 경우 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
 
class Base {
public:
    void f() { cout << "Base::f() called" << endl; }
};
 
class Derived : public Base {
public:
    void f() { cout << "Derived::f() called" << endl; }
};
 
void main() {
    Derived d, *pDer;
    pDer = &d; // 객체 d를 가리킨다.
    pDer->f(); // Derived의 멤버 f() 호출 
 
    Base* pBase;
    pBase = pDer; // 업캐스팅. 객체 d를 가리킨다.
    pBase->f(); // Base의 멤버 f() 호출
}
cs


실행 결과


Derived::f() called

Base::f() called


=> 파생 클래스에서 기본 클래스와 동일한 식의 함수를 중복 작성하는 겨우, 

기본 클래스에 대한 포인터로는 기본 클래스의 함수를 호출하고(up-casting의 경우에도),

파생 클래스의 포인터로는 파생 클래스에 작성된 함수를 호출한다.




|9.2| 가상 함수와 오버라이딩


: 가상 함수(virtual function)오버라이딩(overriding)은 상속에 기반을 둔 기술로 객체 지향 언어의 꽃이다!



● 오버라이딩


: 오버라이딩은 파생 클래스에서 기본 클래스에 작성된 가상 함수를 중복 작성하여, 기본 클래스에 작성된 가상 함수를 무력화시키고,

객체의 주인 노릇을 하는 것이다.


기본 클래스의 포인터를 이용하든 파생 클래스의 포인터를 이용하든 가상 함수를 호출하면, 파생 클래스에 오버라이딩된 함수가 항상 실행된다.


파생 클래스에서 기본 클래스의 가상 함수와 완전히 동일한 원형의 함수를 재정의 하는 것을 '함수 오버라이딩(function overriding)' 이라고 한다.



● 가상 함수


: 가상 함수(virtual function)란 virtual 키워드로 선언된 멤버 함수로,

virtual 키워드는 컴파일러에게 자신에 대한 호출 바인딩을 실행 시간까지 미루도록 지시하는 키워드이다.



● Overloading(함수 중복) vs Overriding 



< overloading >


1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
    void f() {
        cout << "BASE::f() called" << endl;
    }
};
 
class Derived : public Base {
public:
    void f() {
        cout << "Derived::f() called" << endl;
    }
};
cs


=> Base의 f()와 Derived 의 f()는 각각 동등한 호출 기회를 가짐



< overriding >


1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
    virtual void f() {
        cout << "BASE::f() called" << endl;
    }
};
 
class Derived : public Base {
public:
    virtual void f() {
        cout << "Derived::f() called" << endl;
    }
};
cs


=> Base의 f()는 존재감을 잃고, 항상 Derived의 f()가 호출됨



※ 변수 오버라이딩이란 용어는 없다. 항상 멤버 함수 에게만 적용된다.




< 예시 : 오버라이딩 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
 
class Base {
public:
    virtual void f() { cout << "Base::f() called" << endl; }
};
 
class Derived : public Base {
public:
    virtual void f() { cout << "Derived::f() called" << endl; }
};
 
void main() {
    Derived d, *pDer;
    pDer = &d; // 객체 d를 가리킨다.
    pDer->f(); // Derived::f() 호출 
 
    Base* pBase;
    pBase = pDer; // 업캐스팅. 객체 d를 가리킨다.
    pBase->f(); // 동적 바인딩 발생!! Derived::f() 실행
}
cs


실행결과


Derived::f() called

Derived::f() called



함수 오버로딩의 경우 pBase가 Base 타입의 포인터이므로 다음 코드는 Base의 f()을 호출할 것 으로 예상되지만,

pBase가 가리키는 객체는 overriding한 Derived의 f()를 포함하므로 동적 바인딩 통해 Derived의 f()가 호출된다.


-> Base의 f()에 대한 모든 호출은 실행 시간 중에 Derived의 f()함수로 동적 바인딩된다.




● 오버라이딩의 목적


: 파생 클래스들이 자신의 목적에 맞게 가상 함수를 재정의 하도록 하는 것이다. ( 객체 지향 언어의 다형성(polymorphism)을 실현한다. )


 

< 예시 :  ' 오버라이딩을 통한 다형성 '  의 이해 >



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Shape {
protected:
    virtual void draw(){ }
};
 
class Circle : public Shape {
protected:
    virtual void draw() { } // Circle 그린다.
    
};
 
class Rect : public Shape {
protected:
    virtual void draw() { } // Rect 그린다.
 
};
 
class Line : public Shape {
protected:
    virtual void draw() { } // Line 그린다.
 
};
 
void paint(Shape *p) {
    p->draw();
}
 
paint(new Circle()); // Circle을 그린다
paint(new Rect()); // Rect를 그린다
paint(new Line()); // Line을 그린다
cs




● 동적 바인딩 : 오버라이딩된 함수가 무조건 호출


: 가상 함수를 호출하는 코드를 컴파일 할 때, 컴파일러는 바인딩을 실행 시간에 결정하도록 미루어둔다.


  나중에 가상 함수가 호출되면, 실행 중에 객체 내에 오버라이딩된 가상 함수를 동적으로 찾아 호출한다.


  이 과정을 동적 바인딩(dynamic binding) 이라고 부른다. 


  오버라이딩은 파생 클래스에서 재정의한 가상 함수의 호출을 보장받는 선언이다.



● 동적 바인딩이 발생하는 구체적 경우


: 동적 바인딩은 파생 클래스의 객체에 대해, 기본 클래스의 포인터로 가상 함수가 호출될 때 일어난다.


- 기본 클래스 내의 멤버 함수가 가상 함수 호출

- 파생 클래스 내의 멤버 함수가 가상 함수 호출

- main()과 같은 외부 함수에서 기본 클래스의 포이터로 가상 함수 호출

- 다른 클래스에서 가상 함수 호출



● 동적 바인딩 예시



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
 
class Shape {
public:
    void paint() { 
        draw();                        // 얘가
    }
    virtual void draw() {            // 얘를 호출함
        cout << "Shape::draw() called" << endl
    }
};
 
int main() {
    Shape * pShape = new Shape();
    pShape->paint(); 
    delete pShape;
}
cs


실행 결과


Shape::draw() called



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;
 
class Shape {
public:
    void paint() { 
        draw();                                // 얘가
    }
    virtual void draw() {                    // 얘를 호출하지 않고
        cout << "Shape::draw() called" << endl
    }
};
 
class Circle : public Shape {
public:
    virtual void draw() {                    // 얘를 호출함
        cout << "Circle::draw() called" << endl
    }
};
 
int main() {
    Shape *pShape = new Circle();
    pShape->paint(); 
    delete pShape;
}
cs


실행 결과


Circle::draw() called



-> 기본 클래스에서 자신의 멤버를 호출하더라도 그것이 가상 함수이면 역시 동적 바인딩이 발생한다.




● 오버라이딩의 성공 조건


: 함수 이름, 매개 변수 타입, 개수 , 그리고 리턴 타입까지 일치해야 오버라이딩이 성공한다.




● virtual 지시어 생략 가능


: 클래스의 virtual 키워드는 파생 클래스로 상속되므로, 파생 클래스에서 virtual 키워드를 생략할 수 있다.



● 범위 지정 연산자(::)로 존재감을 상실한 함수를 호출하자



< 예시 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
 
class Shape {
public:
    virtual void draw() { 
        cout << "--Shape--"
    }
};
 
class Circle : public Shape {
public:
    int x;
    virtual void draw() { 
        Shape::draw(); // 기본 클래스의 draw() 호출
        cout << "Circle" << endl;
    }
};
 
int main() {
    Circle circle;
    Shape * pShape = &circle;
 
    pShape->draw();  // 동적 바인딩 발생. draw()는 virtual이므로
    pShape->Shape::draw(); // 정적 바인딩 발생. 범위지정연산자로 인해
}
cs




● 가상 소멸자


: 파생 클래스의 객체가 기본 크래스에 대한 포인터로 delete되는 상황에서도 정상적인 소멸이 되도록 하기 위해서

  기본 클래스의 소멸자를 만들 때 가상 함수로 작성할 것을 권한다.



1) 소멸자를 가상 함수로 선언하지 않은 경우


1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
    ~Base();
};
 
class Derived : public Base {
public:
    ~Derived();
};
 
int main() {
    Base *= new Derived();
    delete p; // ~Base()만 실행됨
}
cs


-> p가 Base 타입이므로 ~Base() 소멸자는 실행되나,  ~Derived()는 실행되지 않는다.



2) 소멸자를 가상 함수로 선언한 경우


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
public:
    virtual ~Base();
};
 
class Derived : public Base {
public:
    virtual ~Derived();
};
 
int main() {
    Base *= new Derived();
    delete p; /* 1. ~Base() 호출
                 2. ~Derived()실행(동적 바인딩)
                 3. ~Base() 실행*/
}
cs


-> 소멸자를 가상 함수로 선언하면, 객체를 기본 클래스의 포인터로 소멸하든, 파생 클래스의 포인터로 소멸하든 파생 클래스와 기본 클래스의 소멸자를 모두 실행하는 정상적인 소멸 과정이 진행된다.


따라서 클래스의 소멸자를 작성할 떄 고민없이 무조건 virtual로 선언 하는 것이 뒤탈이 없겠다.



< 예시 : 소멸자를 가상 함수로 선언 >



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
 
class Base {
public:
    virtual ~Base() { cout << "~Base()" << endl; }
};
 
class Derived: public Base {
public:
    virtual ~Derived() { cout << "~Derived()" << endl; }
};
 
int main() {
    Derived *dp  = new Derived();
    Base *bp = new Derived();
 
    delete dp; // Derived의 포인터로 소멸
    delete bp; // Base의 포인터로 소멸
}
 
cs


실행 결과


~Derived()

~Base()

~Derived()

~Base()


※ 생성자는 가상 함수가 될 수 없으며, 생성자에서 가상 함수를 호출해도 동적 바인딩이 일어나지 않는다.

  하지만 소멸자는 가상 함수가 될 수 있으며 가상 함수로 만드는 것이 바람직하다.



'C++ > C++ 문법' 카테고리의 다른 글

pair 사용  (0) 2019.05.17
#8-5 가상 상속  (0) 2018.10.07
#8-3 상속과 생성자, 소멸자  (0) 2018.10.07
#8-2 상속과 객체 포인터  (0) 2018.10.07
#8-1 상속  (0) 2018.10.07


● 다중 상속의 문제점


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BaseIO {
public:
    int mode;
};
 
class In : public BaseIO {
public:
    int readPos;
};
 
class Out : public BaseIO {
public:
    int writePos;
};
 
class InOut : public In, public Out {
public:
    bool safe;
};
 
int main() {
    InOut ioObj;
 
    ioObj.readPos = 10;
    ioObj.writePos = 20;
    ioObj.safe = true;
    ioObj.mode = 5;
}
cs


In과 Out 중 어느 클래스의 기본 클래스의 mode 인지 모호해진다.


=> 다이아몬드 형의 다중 상속 구조는 기본 클래스의 멤버가 중복 상속 되어 객체 속에 조재하는 상황을 초래하여 컴파일 오류를 발생시킨다.




● 가상 상속


: virtual 키워드를 이용하여 가상 상속을 선언하여 다중 상속에서 생기는 멤버 중복 생성 문제를 해결한다.


: virtual 키워드는 컴파일러에게 파생 클래스의 객체가 생성될 때 기본 클래스의 멤버 공간을 오직 한 번만 할당하고,

이미 할당되어 있다면 그 공간을 공유하도록 지시한다.



1
2
3
4
5
6
class In : virtual public BaseIO { // BaseIO클래스를 가상 상속함
    ...
};
class Out : virtual public BaseIO{ // BaseIO클래스를 가상 상속함
    ...
};
cs






'C++ > C++ 문법' 카테고리의 다른 글

pair 사용  (0) 2019.05.17
#09 - 1 가상함수와 추상 클래스  (1) 2018.10.08
#8-3 상속과 생성자, 소멸자  (0) 2018.10.07
#8-2 상속과 객체 포인터  (0) 2018.10.07
#8-1 상속  (0) 2018.10.07

● 파생 클래스와 기본 클래스의 생성자 호출 및 실행


Q1)  파생 클래스의 객체가 생성될 때, 파생 클래스의 생성자와 기본 클래스의 생성자가 모두 실행되는가?


-> 둘 다 실행된다. 파생 클래스, 기본 클래스 모두 각각의 멤버 초기화나 필요한 초기화를 각각 수행한다.


Q2) 파생 클래스의 생성자와 기본 클래스의 생성자 중에서 누가 먼저 실행되는가?


-> 기본 클래스의 생성자가 먼저 실행된다.



다음을 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
 
class  A {
public:
    A() { cout << "생성자 A" << endl; }
    ~A() {cout << "소멸자 A" << endl; }
};
 
class  B : public A {
public:
    B() { cout << "생성자 B" << endl; }
    ~ B() {cout << "소멸자 B" << endl; }
};
 
class  C : public B {
public:
    C() { cout << "생성자 C" << endl; }
    ~ C() { cout << "소멸자 C" << endl; }
};
 
int main() {
    C c; // 객체 c 생성
    return 0// 객체 c 소멸
 
cs



C c; 

에서 파생 클래스의 생성자가 먼저 호출되지만, 결국 기본 클래스의 생성자가 먼저 실행되고 나서야 파생 클래스의 생성자가 실행된다.


why? : 파생 클래스의 생성자에서 기본 클래스의 생성자를 호출할 때 인자 값을 전달해야 하기 때문이다.



● 소멸자의 실행 순서


: 생성자의 실행 순서와 반대로 실행된다. 


why? : 파생 클래스의 소멸자 코드를 실행한 후 기본 클래스의 소멸자를 호출하도록 컴파일 하기 때문이다.




● 파생 클래스에서 기본 클래스 생성자 호출


파생 클래스를 작성하는 개발자는 함께 실행할 기본 클래스의 생성자를 지정하여야 한다.

하지 않을 경우, 묵시적으로 기본 클래스의 기본 생성자가 실행되도록 컴파일한다.


※ 만일 기본 생성자가 선언되어 있지 않으면 '사용할 수 있는 적절한 기본 생성자가 없습니다. ' 라는 오류를 발생시킨다.




아래는 명시적으로 생성하는 경우의 예제


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
 
class  A {
public:
    A() { cout << "생성자 A" << endl; }
    A(int x) {
        cout << "매개변수생성자 A" << x << endl
    }
};
 
class  B : public A {
public:
    B() { // A() 호출하도록 컴파일됨
        cout << "생성자 B" << endl;
    }
    B(int x) : A(x+3) {
        cout << "매개변수생성자 B" << x << endl;
    }
};
 
int main() {
    B b(5);
}
cs



명시적으로 기본 생성자 호출 코드를 작성하지 않는 경우 컴파일러가 다음과 같이 기본 생성자 호출 코드를 삽입한다.


1
2
3
4
5
6
7
8
class B {
    B() : A() {
        cout << "생성자 B" << endl;
    }
    B(int x) : A() {
        cout << "매개변수생성자 b" << x << endl;
    }
};
cs


'C++ > C++ 문법' 카테고리의 다른 글

#09 - 1 가상함수와 추상 클래스  (1) 2018.10.08
#8-5 가상 상속  (0) 2018.10.07
#8-2 상속과 객체 포인터  (0) 2018.10.07
#8-1 상속  (0) 2018.10.07
#7-B 연산자 중복  (0) 2018.10.07

● 상속과 객체 포인터


업 캐스팅기본 클래스의 포인터 -> 파생 클래스의 객체

클래스 객체를 파생 클래스로 변환하는것



=> 파생 클래스의 객체를 기본 클래스의 객체처럼 다룰 수 있게 한다.


1
2
3
4
5
6
7
8
9
10
11
12
int main() {
    ColorPoint cp;
    ColorPoint *pDer = &cp;
    Point* pBase = pDer; // up-casting
    
    pDer->set(34);
    pBase->showPoint();
    pDer->setColor("Red");
    pDer->showColorPoint();
 
    pBase->showColorPoint(); // 컴파일 오류
}
cs


위에서 showColorPoint()는 부모 클래스인 Point의 멤버가 아니므로 오류가 난다.

-> pBase로는 ColorPoint클래스 중 Point에서 상속 받은 멤버에만 접근이 가능하다.



up-casting 시 다음과 같은 명시적 타입 변환이 필요 없다.


1
Point* pBase = (Point*)pDer; // (Point*) 생략이 
cs


( 왜냐하면 cp객체는 ColorPoint 타입이지만 Point 타입이기도 하기 때문이다. )




다운 캐스팅  :  파생 클래스의 포인터 -> 기본 클래스 포인터가 가리키는 객체

클래스 객체를 기반 클래스로 변환하는것


1
2
3
4
5
6
7
8
9
10
11
12
int main() {
    ColorPoint cp;
    ColorPoint *pDer;
    Point* pBase = &cp;
 
    pBase->set(34);
    pBase->showPoint();
 
    pDer = (ColorPoint*)pBase; // down-casting
    pDer->setColor("Red"); // 정상 컴파일
    pDer->setColorPoint();
}
cs


실행 결과 : 

(3,4)

Red(3,4)


※ down-casting은 명시적으로 타입 변환을 지정해야 한다.



※ down-casting 주의사항


1
2
3
4
5
6
7
8
9
ColorPoint *pDer;
Point *pBase, po;
pBase = &po;
pDer = (ColorPoint*)pBase; // 다운 캐스팅
 
pDer->set(34);
pDer->setColor("Red"); /*setColor는 ColorPoin의 멤버이므로
                       컴파일 오류는 없으나 실행 중 오류가 생김*/
 
cs


=> pDer은 ColorPoint 타입의 포인터이므로 setColor()함수를 호출하는 데에는 문법적인 오류가 없다.

but 이 라인이 실행되면 pDer이 가리키는 객체 공간에는 setColor() 함수가 없기 때문실행중에 오류가 발생하여 비정상 종료한다.

'C++ > C++ 문법' 카테고리의 다른 글

#8-5 가상 상속  (0) 2018.10.07
#8-3 상속과 생성자, 소멸자  (0) 2018.10.07
#8-1 상속  (0) 2018.10.07
#7-B 연산자 중복  (0) 2018.10.07
#07-A 프렌드  (0) 2018.10.05

● 상속 선언



< 예시 : Point 클래스를 상속받는 ColorPoint 클래스 만들기 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string>
using namespace std;
 
class Point { // 2차원 평면에서 한 점을 표현하는 클래스 Point 선언
    int x, y; //한 점 (x,y) 좌표값
public:
    void set(int x, int y) { this->= x; this->= y; }
    void showPoint() {
        cout << "(" << x << "," << y << ")" << endl;
    }
};
 
class ColorPoint : public Point { // 2차원 평면에서 컬러 점을 표현하는 클래스 ColorPoint. Point를 상속받음
    string color;// 점의 색 표현
public:
    void setColor(string color)  {    this->color = color; }
    void showColorPoint();
};
 
void ColorPoint::showColorPoint() {
    cout << color << ":";
    showPoint(); // Point의 showPoint() 호출
}
 
int main() {
    Point p; // 기본 클래스의 객체 생성
    ColorPoint cp; // 파생 클래스의 객체 생성
    cp.set(3,4); // 기본 클래스의 멤버 호출
    cp.setColor("Red"); // 파생 클래스의 멤버 호출
    cp.showColorPoint(); // 파생 클래스의 멤버 호출
}
cs



※ parent 클래스는 public으로 하면 됨


=> 실행 결과


Red:(3,4)



※ 기본 클래스의 private 멤버의 접근


:   Parent class의 private 멤버는 Child class의 함수로는 접근할 수 없다! => Parent 클래스에서 상속받은 함수로만 간접적으로 접근 가능하다!



※ private 멤버는 해당 클래스 내의 멤버 함수들에게만 접근이 허용된다.

'C++ > C++ 문법' 카테고리의 다른 글

#8-3 상속과 생성자, 소멸자  (0) 2018.10.07
#8-2 상속과 객체 포인터  (0) 2018.10.07
#7-B 연산자 중복  (0) 2018.10.07
#07-A 프렌드  (0) 2018.10.05
#6 함수 중복과 static 멤버  (0) 2018.10.05

|7.2| 연산자 중복 (operator overloading)


:  C++의 다형성(polymorphism)이다.  피연산자에 따라 서로 다른 연산을 하도록 동일한 연산자를 중복해서 작성하는 것.



● 연산자 중복의 특징


1) C++에 원래 있는 연산자만 중복 가능

2) 정수/실수 가 아닌 객체나 값

3) 연산자 중복으로 연산자의 우선 순위를 바꿀 수 없음

4) 안되는 연산자도 있음



● 연산자 함수 작성 방법


1) 클래스의 멤버 함수로 구현

2) 외부 함수로 구현하고 클래스의 프랜드 함수로 선언



리턴타입 operator 연산자(parameter);



1) 클래스의 멤버 함수로 선언되는 경우


1
2
3
4
5
6
7
class Color {
    ...
        Color operator + (Color op2); /* 왼쪽 피연산자는 객체 자신이고
                                      오른쪽 피연산자가 op2에 전달*/
    bool operator == (Color op2); /*왼쪽 피연산자는 객체 자신
                                  오른쪽 피연산자가 op2에 전달*/
};
cs



=> + 나 == 의 오른쪽 피연산자만 매개 변수 op2에 전달되고, 왼쪽 피연산자는 객체 자신이므로 매개 변수에 전달되지 않는다.



2) 외부 함수로 구현하고 클래스에 프렌드 함수로 선언하는 경우


1
2
3
4
5
6
7
8
Color operator + (Color op1, Color op2); // 외부 전역 함수
bool operator == (Color op1, Color op2); // 외부 전역 함수
...
class Color {
...
    friend Color operator + (Color op1, Color op2); // 프렌드 선언
    friend bool operator == (Color op1, Color op2); // 프렌드 선언
};
cs




|7.3| 이항 연산자 중복


: 예시를 통해 연산자 중복에 대해 제대로 이해해보자!


1
2
3
4
5
6
7
8
9
10
11
class Power { // 에너지를 표현하는 파워 클래스
    int kick;    // 킥 파워
    int punch;    // 펀치 파워
 
public:
    Power(int kick = 0int punch = 0){
        this->kick = kick;
        this->punch = punch;
    }
 
};
cs


● 1) + 연산자 중복


※ 우선 이 예제에서 +의 의미를 결정해야 하는데, 이 것은 전적으로 개발자의 몫이다.  
   여기서는 +연산을 두 객체의 kick과 punch를 더하는 것으로 정의한다.

1
2
3
Power a(35), b(46), c;
 
= a + b; // 두 객체 a, b의 파워를 더하는 + 연산
cs



※ 컴파일러는 a + b 의 연산이 C++의 기본 더하기로 처리될 수 없음을 판단한다. 따라서 Power 클래스에 Power 객체를 더하는

   + 연산자 함수가 새로 선언되어 있는지 찾는다.

   이를 위해 컴파일러는 a + b를 다음과 같이 변형한다.


1
a . + ( b );
cs


※ 이 식은 다음과 같이 Power 객체 a의 멤버 함수 operator+()를 호출하며, b를 매개 변수로 넘겨주는 함수 호출이다.

우리는 이 호출이 성공할 수 있도록 다음과 같이 operator+()함수를 Power클래스의 멤버 함수로 선언한다.


1
2
3
class Power() {
    Power operator+ (Power op2); // + 연산자 함수 선언
}
cs


※ operator+() 함수는 리턴 타입을 Power로 하고, 더한 결과인 새로운 Power 객체를 리턴한다.


※ 위의 operator+() 함수의 구현은 다음과 같다.


1
2
3
4
5
6
Power Power::operator+(Power op2) {
    Power tmp;
    tmp.kick = this->kick + op2.kick;
    tmp.punch = this - punch + op2.punch;
    return tmp;
}
cs


=> 이 연산자 함수는 더한 결과인 tmp 객체를 리턴한다.



< 예시 구현 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;
 
class Power {
    int kick;
    int punch;
public:
    Power(int kick=0int punch=0) {
        this->kick = kick; this->punch = punch;
    }
    void show();
    Power operator+ (Power op2); // + 연산자 함수 선언
};
 
void Power::show() {
    cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}
 
Power Power::operator+(Power op2) { 
    Power tmp; // 임시 객체 생성
    tmp.kick = this->kick + op2.kick; // kick 더하기
    tmp.punch = this->punch + op2.punch; // punch 더하기
    return tmp; // 더한 결과 리턴
}
 
int main() {
    Power a(3,5), b(4,6), c;
    c = a + b; // 파워 객체 + 연산
    a.show();
    b.show();
    c.show();
}
cs



● 2) == 연산자 중복


: a == b 가 true인 경우 각각의 kick과 punch가 같은 경우로 정의한다. 따라서 operator==()의 리턴 타입은 bool이다.



※ 컴파일러는 a==b의 식을 다음과 같이 변형하여 Power 클래스의 멤버로 작성된 operator==()함수를 찾는다.


1
a. == (b)
cs


※ 이 식은 Power 객체 a의 operator==() 연산자 함수를 호출하고 b를 매개 변수로 넘긴다.


< 예시 구현 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;
 
class Power {
    int kick;
    int punch;
public:
    Power(int kick=0int punch=0) {
        this->kick = kick; this->punch = punch;
    }
    void show();
    bool operator== (Power op2);  // == 연산자 함수 선언
};
 
void Power::show() {
    cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}
 
bool Power::operator==(Power op2) {
    if(kick==op2.kick && punch==op2.punch) return true;
    else return false;
}
 
int main() {
    Power a(3,5), b(3,5); // 2 개의 동일한 파워 객체 생성
    a.show();
    b.show();
    if(== bcout << "두 파워가 같다." << endl;
    else cout << "두 파워가 같지 않다." << endl;
}
cs



● 3) += 연산자 중복


: 언뜻 보면 아무 값도 리턴할 필요가 없어 void로 하면 될 것 같지만 

  c = a += b; 처럼 사용될 수도 있다.

  (이 문장은 a += b를 먼저 계산하고 결과를 c에 치환하므로, a와 b를 더한 a를 리턴하도록 작성되어야 한다.)


※ 컴파일러는 a+=b;의 식을 다음과 같이 변형한다.


1
a . += ( b ) ;
cs



< 예시 구현 >

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;
 
class Power {
    int kick; 
    int punch; 
public:
    Power(int kick=0int punch=0) {
        this->kick = kick; this->punch = punch;
    }
    void show();
    Power& operator+= (Power op2); // += 연산자 함수 선언
};
 
void Power::show() {
    cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}
 
Power& Power::operator+=(Power op2) {
    kick = kick + op2.kick; // kick 더하기
    punch = punch + op2.punch; // punch 더하기
    return *this// 합한 결과 리턴
}
 
int main() {
    Power a(3,5), b(4,6), c;
    a.show();
    b.show();
    c = a += b; // 파워 객체 더하기
    a.show();
    c.show();
}
cs



※ this를 이용하여 return한다는 점에 주목하자.



● 4) + 연산자 b = a + 2 ;


※ 컴파일러는 우선 a + 2 를 다음과 같이 변형할 것이다.


1
a . + ( 2 )
cs


위의 + 연산자 중복 예제에서 파라미터를 int op2로 바꿔주면 되겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;
 
class Power {
    int kick; 
    int punch; 
public:
    Power(int kick=0int punch=0) {
        this->kick = kick; this->punch = punch;
    }
    void show();
    Power operator+ (int op2); // + 연산자 함수 선언
};
 
void Power::show() {
    cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}
 
Power Power::operator+(int op2) {
    Power tmp; // 임시 객체 생성
    tmp.kick = kick + op2; // kick에 op2 더하기
    tmp.punch = punch + op2; // punch에 op2 더하기
    return tmp; // 임시 객체 리턴
}
 
int main() {
    Power a(3,5), b;
    a.show();
    b.show();
    b = a + 2// 파워 객체와 정수 더하기
    a.show();
    b.show();
}
cs




|7.4| 단항 연산자 중복


: 피연산자가 하나인 단항 연산자의 연산자 함수

1) 전위 연산자 : !op, ~op, ++op, --op
2) 후위 연산자 ; op++, op--

● 전위 ++ 연산자 중복

1
2
Power a(3,5), b;
= ++a;
cs


: 객체 a의 멤버들의 값을 각각 1씩 증가시킨 후 변경된 객체 a를 리턴시킨다.


※ 컴파일러는 ++a 식을 다음과 같이 변형한다


1
a . ++ ( );
cs



< 예시 구현 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;
 
class Power {
    int kick;
    int punch; 
public:
    Power(int kick=0int punch=0) {
        this->kick = kick; this->punch = punch;
    }
    void show();
    Power operator++ (); // 전위 ++ 연산자 함수 선언
};
 
void Power::show() {
    cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}
 
Power Power::operator++() {
    kick++;
    punch++;
    return *this; // 변경된 객체 자신(객체 a)의 참조 리턴
}
 
int main() {
    Power a(3,5), b;
    a.show();
    b.show();
    = ++a; //  전위 ++ 연산자 사용
    a.show();
    b.show();
}
cs



< 예시 : ! 연산자 작성 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;
 
class Power {
    int kick;
    int punch; 
public:
    Power(int kick=0int punch=0) {
        this->kick = kick; this->punch = punch;
    }
    void show();
    bool operator! (); // ! 연산자 함수 선언
};
 
void Power::show() {
    cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}
 
bool Power::operator!() {
    if(kick == 0 && punch == 0return true;
    else return false;
}
 
int main() {
    Power a(0,0), b(5,5);
    if(!acout << "a의 파워가 0이다."  << endl// ! 연산자 호출
    else cout << "a의 파워가 0이 아니다." << endl;
    if(!bcout << "b의 파워가 0이다." << endl// ! 연산자 호출
    else cout << "b의 파워가 0이 아니다." << endl;
}
cs



● 후위 ++ 연산자 중복


: a++는 증가 이전의 객체 a를 리턴하는 것으로 정의한다.


※ a++ vs ++a


1
2
Power operator++();        // ++a
Power operator++(int x);    // a++
cs


※ 후위 연산자 함수에서 x에는 의미 없는 값이 전달되므로 무시해도 된다.


※ 컴파일러는 a++식을 다음과 같이 변형한다.


1
a .++(임의의 정수); // 임의의 정수가 operator++(int)의 매개 변수로 넘겨진다.
cs


< 예시 구현 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;
 
class Power {
    int kick; 
    int punch; 
public:
    Power(int kick=0int punch=0) {
        this->kick = kick; this->punch = punch;
    }
    void show();
    Power operator++ (int x); // 후위 ++ 연산자 함수 선언
};
 
void Power::show() {
    cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}
 
Power Power::operator++(int x) {
    Power tmp = *this; // 증가 이전 객체 상태를 저장
    kick++;
    punch++;
    return tmp// 증가 이전 객체 상태 리턴
}
 
int main() {
    Power a(3,5), b;
    a.show();
    b.show();
    b = a++// 후위 ++ 연산자 사용
    a.show(); // a의 파워는 1 증가됨
    b.show(); // b는 a가 증가되기 이전 상태를 가짐
}
cs



● (다시) 전위 연산자 vs 후위 연산자 함수 비교



전위 연산자


1
2
3
4
5
Power Power::operator++() {
    kick++;
    punch++;
    return *this// 변경된 객체 자신 리턴
}
cs



후위 연산자


1
2
3
4
5
6
Power Power::operator++(int x) {
    Power tmp = *this// 증가 이전의 객체 상태 저장
    kick++;
    punch++;
    return tmp; // 증가 이전의 객체 리턴
}
cs





|7.5| 프렌드를 이용한 연산자 중복


: 지금까지는 ' 1) 클래스의 멤버 함수로 구현 의 방법' 으로 연산자 함수를 작성하였다.

이번에는 ' 2) 외부 함수로 구현하고 클래스의 프랜드 함수로 선언 ' 으로 작성한다 (외부 전역 함수로 작성)



● 2 + a를 위한 + 연산자 함수 작성


1
2
Power a(34), b;
= 2 + a;
cs


위의 내용에 따르면, 컴파일러는 2 + a 의 연산을 다음과 같이 변형하여 operator+() 함수를 호출하려고 할 것이다.


1
2 . + ( a )
cs


근데 2가 객체가 아니네??


-> 컴파일러는 다음과 같은 식으로 변환한다. 


1
+ ( 2 , a )
cs



=> 사실 컴파일러에게 두 개의 옵션이 있는 셈이다.


위의 식이 성공적이기 위해서는 operator+()함수를 Power클래스의 외부 함수로 밖에 구현할 수 없다.


1
2
3
4
5
6
Power operator+ (int op1, Power op2) {
    Power tmp;
    tmp.kick = op1 + op2.kick;
    tmp.punch = op1 + op2.punch;
    return tmp;
}
cs



● 외부 연산자 함수의 프렌드 선언



: 근데 문제가 있음. 함수 내에서 Power의 private 멤버인 kick과 punch를 자유롭게 접근하기 떄문에 연산자 함수에 컴파일 오류가 생길 것이다.


그럼, kick과 punch를 public으로 선언하면 어떨까?


-> 근데 그럼 연산자 함수를 쓰려고 Power 클래스의 캡슐화의 원칙을 무너뜨리게 되는 치명적인 판단 미스가 된다!



===> friend를 사용하면 깔끔하게 해결된다. 외부에 구현된 operator+(int, Power)의 연산자 함수를 Power 클래스에 프렌드로 초대한다.



1
2
3
4
5
6
7
8
9
10
11
class Power {
    int kick;
    int punch;
public:
    ...
        friend Power operator+(int op1, Power op2); // 프렌드 선언
};
 
Power operator+(int op1, Power op2) { // 외부 함수로 구현된 연산자 함수
    ...
}
cs



< 예시 구현 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;
 
class Power {
    int kick; 
    int punch; 
public:
    Power(int kick=0int punch=0) {
        this->kick = kick; this->punch = punch;
    }
    void show();
    friend Power operator+(int op1, Power op2); // 프렌드 선언
};
 
void Power::show() {
    cout << "kick=" << kick << ',' << "punch=" << punch << endl;
}
 
Power operator+(int op1, Power op2) {
    Power tmp; // 임시 객체 생성
    tmp.kick = op1 + op2.kick; // kick 더하기
    tmp.punch = op1 + op2.punch; // punch 더하기
    return tmp; // 임시 객체 리턴
}
 
int main() {
    Power a(3,5), b;
    a.show();
    b.show();
    b = 2 + a; // 파워 객체 더하기 연산
    a.show();
    b.show();
}
cs




● a + b 를 외부 프렌드 함수로 작성


1
2
3
4
5
6
Power operator+(Power op1, Power op2) {
    Power tmp;
    tmp.kick. = op1.kick + op2.kick;
    tmp.punch = op1.punch + op2.punch;
    return tmp;
}
cs


를 외부 전역 함수로 구현하고, Power 클래스 내에


1
friend Power operator+(Power op1, Power op2); 
cs


를 구현하면 되겠다.


※ 동일한 연산자 함수를 멤버 함수와 프렌드 함수로 동시에 구현할 수 없다.




● 단항 연산자 ++를 프렌드로 작성하기



전위 연산자


1
2
3
4
5
Power operator++(Power& op) {
    op.kick++;
    op.punch++;
    return op;
}

cs



후위 연산자


1
2
3
4
5
6
Power operator++(Power& op, int x) {
    Power tmp = op;
    op.kick++;
    op.punch++;
    return tmp;
}
cs



클래스 내에


1
2
friend Power operator++(Power& op);
friend Power operator++(Power& op, int x);
cs


※ 참조 매개 변수를 사용하지 않으면 복사본이 전달되므로 op 객체의 kick과 punch의 값이 변하지 않는다.

'C++ > C++ 문법' 카테고리의 다른 글

#8-2 상속과 객체 포인터  (0) 2018.10.07
#8-1 상속  (0) 2018.10.07
#07-A 프렌드  (0) 2018.10.05
#6 함수 중복과 static 멤버  (0) 2018.10.05
#5-B 복사 생성자  (0) 2018.10.05

7.1 C++의 프렌드 개념

※ 프렌드 함수를 선언할 수 있는 경우


1) 클래스 외부에 작성된 함수를 프렌드로 선언
2) 다른 클래스의 멤버 함수를 프렌드로 선언
3) 다른 클래스의 모든 멤버 함수를 한번에 프렌드로 선언



1) 클래스 외부에 작성된 함수를 프렌드로 선언


※ 예제 1)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
 
class Rect; // forward decalaration. 이 라인이 없으면, 다음 라인에서 Rect를 참조하는 전방 참조(forward reference) 문제 발생
bool equals(Rect r, Rect s); // equals() 함수 선언
 
class Rect { // Rect 클래스 선언
    int width, height;
public:
    Rect(int width, int height)  {     this->width = width; this->height = height;    }
    friend bool equals(Rect r, Rect s); //프렌드 함수 선언
};
 
bool equals(Rect r, Rect s) { // 외부 함수
    if(r.width == s.width && r.height == s.height) return true
    else return false;
}
 
int main() {
    Rect a(3,4), b(4,5);
    if(equals(a, b)cout << "equal" << endl;
    else cout << "not equal" << endl;
}
cs



※ 전방 참조(forward reference)를 해결하기 위해 전방 선언(forward declaration)를 해주었다.



2) 다른 클래스의 멤버 함수를 프렌드로 선언


※ 예제 2)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;
 
class Rect; 
 
class RectManager { // RectManager 클래스 선언
public:
    bool equals(Rect r, Rect s);
};
 
class Rect { // Rect 클래스 선언
    int width, height;
public:
    Rect(int width, int height) { this->width = width; this->height = height; }
    friend bool RectManager::equals(Rect r, Rect s); // 프렌드 함수 선언
};
 
bool RectManager::equals(Rect r, Rect s) { // RectManager::equals() 구현
    if(r.width == s.width && r.height == s.height) return true
    else return false;
}
 
int main() {
    Rect a(3,4), b(3,4);
    RectManager man;
    
    if(man.equals(a, b)) cout << "equal" << endl;
    else cout << "not equal" << endl;
}
cs



3) 다른 클래스의 모든 멤버 함수를 한번에 프렌드로 선언



※ 예제 3)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;
 
class Rect;
 
class RectManager { // RectManager 클래스 선언
public:
    bool equals(Rect r, Rect s);
    void copy(Rect& dest, Rect& src);
};
 
class Rect { // Rect 클래스 선언
    int width, height;
public:
    Rect(int width, int height)  { this->width = width; this->height = height; }
    friend RectManager; // RectManager 클래스의 모든 함수를 프렌드 함수로 선언
};
 
bool RectManager::equals(Rect r, Rect s) { // r과 s가 같으면 true 리턴
    if(r.width == s.width && r.height == s.height) return true
    else return false;
}
 
void RectManager::copy(Rect& dest, Rect& src) { // src를 dest에 복사
    dest.width = src.width;  dest.height = src.height;
}
 
int main() {
    Rect a(3,4), b(5,6);
    RectManager man;
    
    man.copy(b, a); // a를 b에 복사한다.
    if(man.equals(a, b)) cout << "equal" << endl;
    else cout << "not equal" << endl;
}
cs



※ 프렌드 선언은 클래스 내에 private, public 등 아무 위치에서나 가능하다.

'C++ > C++ 문법' 카테고리의 다른 글

#8-1 상속  (0) 2018.10.07
#7-B 연산자 중복  (0) 2018.10.07
#6 함수 중복과 static 멤버  (0) 2018.10.05
#5-B 복사 생성자  (0) 2018.10.05
#5-A 함수와 참조  (0) 2018.10.04

|6.1| 함수 중복



※ 중복 함수 조건  


1) 함수 이름 동일

2) 매개 변수 타입 or 개수 달라야 함


but!  return 타입은 고려되지 않음



※ 이미 생성자 함수 중복으로 함수 중복을 해본 바 있음


※ string 클래스 또한 다양한 생성자를 제공함


1
2
3
4
5
string str; // 빈 문자열을 가진 객체
string address("Hermine Portejoie");
string copyname(frenchkebab); /* string객체인 frenchkebab의 문자열을 복사한
                              별도의 copyname객체 생성*/
 
cs



|6.2| 디폴트 매개 변수


※ 조건


1) 디폴트 매개 변수에 값을 전달하는 것은 선택 사항이지만, 일반 매개 변수는 반드시 값을 전달해야 한다.


2) 디폴트 매개 변수는 오른쪽 끝에 몰려 선언되어야 한다.


1
2
void func(int a, int b = 5int c); // 컴파일 오류
void funct(int a = 0int b); // 컴파일 오류
cs



※ 예시 : 디폴트 매개 변수의 원형 선언과 구현을 분리


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
using namespace std;
 
// 원형 선언
void star(int a=5);
void msg(int id, string text="");
 
// 함수 구현
void star(int a) {
    for(int i=0; i<a; i++cout << '*';
    cout << endl;
}
 
void msg(int id, string text) {
    cout << id << ' ' << text << endl;
}
 
int main() {
    star(); // star(5);와 동일
    star(10);
 
    msg(10); // star(10, "");과 동일
    msg(10"Hello");
}
 
cs


※ 디폴트 매개 변수의 장점 : 함수 중복을 간소화 할 수 있다.



1
2
3
4
5
6
7
class Circle {
    .........
public:
    Circle() { radius = 1; }
    Circle(int r) { radius = r; }
    ............
};
cs

얘를


1
2
3
4
5
class Circle {
    .........
public:
    Circle(int r = 1) { radius = r; }
};
cs

이렇게 간소화 할 수 있다.




|6.3| 함수 중복의 모호성


※ 함수 중복으로 인한 모호성 3가지

1) 형 변환으로 인해
2) 참조 매개 변수로 인해
3) 디폴트 매개 변수로 인해



1) 형 변환으로 인한 모호성


1
2
3
double square(double a);
....
square(3); // double 타입 매개 변수에 int 타입 전달 but 오류 X
cs


※ 형 변환 참조 => '여기'


but!


1
2
float square(float a);
double square(double a);
cs


이렇게 2개의 중복된 함수가 있을 경우


1
square(3); // float? double? => 컴파일 오류 
cs


이렇게 컴파일러가 오류를 발생시킨다.



※ 예제


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
 
float square(float a) {
    return a*a;
}
 
double square(double a) {
    return a*a;
}
 
int main() {
    cout << square(3.0); // square(double a); 호출
    cout << square(3); // 중복된 함수에 대한 모호한 호출로서, 컴파일 오류
}
cs


※ 성돈tip : 실수 default가 double이므로, 3.0f 로 입력해야 float값으로 들어간다.



2) 참조 매개 변수로 인한 모호성



1
2
3
4
5
int add(int a, int b);
int add(int a, int &b);
 
int s = 10, t = 20;
add(s, t); // 컴파일 오류
cs



3) 디폴트 매개 변수로 인한 모호성


1
2
3
4
void msg(int id)
void msg(int id, string s = "")
 
msg(6); // 컴파일 
cs








|6.4| static 멤버


● static의 특성


- 생명주기 : 프로그램이 시작할 때 생성, 종료할 때 소멸

- 사용 범위 : global or local




※ non-static 멤버와 구별
: non-static 멤버는 객체가 생성될 때 각 객체마다 개별적으로 생성된다. ( 객체와 생명 주기를 같이 한다. )
 하지만 static 멤버는 객체가 생기기 이전에 이미 생성되어 있고, 객체가 사라져도 소멸되지 않고, 모든 객체들의 공통된 멤버로서 공유된다.
 -> static 멤버는 클래스 당 하나만 생기고 모든 객체들이 공유한다.(class 멤버 라고도 부른다. )

● static 멤버 선언


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
public:
    double money; // 개인 소유의 돈
    void addMoney(int money) {
        this->money += money;
    }
 
    static int sharedMoney; // 공금
    static void addShared(int n) {
        sharedMoney += n;
    }
 
};
 
int Person::sharedMoney = 10; // 반드시 전역 공간에 선언한다
cs


※ static 멤버 변수는 외부에 전역 변수로 선언되어야 한다.



● static 멤버 사용 : 객체의 멤버로 접근하는 방법


:  객체 이름 or 객체 포인터 사용


1
2
3
4
5
6
Person lee;
lee.sharedMoney = 500// 객체 이름으로 접근
 
Person *p;
= &lee;
p->addShared(200); // 객체 포인터로 접근
cs




● static 멤버 사용 : 클래스명과 범위지정 연산자(::)로 접근하는 방법


1
2
Person::sharedMoney = 200// 클래스명으로 접근
Person::addShared(200);
cs


참고로,


1
2
3
4
5
6
7
8
Person::sharedMoney = 200;
hermine.sharedMoney = 200// 위와 동일한 표현
 
 
Person::addShared(200);
junghyun.addShared(200); // 위와 동일한 표현
cs



※ 당연하지만 non-static 멤버는 클래스명으로 접근할 수 없다.


Person 객체가 하나도 안생겼을 때에도 static 멤버를 접근하여 사용할 수 있다.




● static의 활용



1) 전역 변수나 전역 함수를 클래스에 캡슐화


1
2
int abs(int a) { return a > 0 ? a : -1; }
int max(int a, int b) { return (a > b) ? a : b; } 
cs


=> 클래스로 캡슐화되어 있지 않고 전역 함수들이 존재하는 좋지 않은 코드 사례이다.



2) 객체 사이에 공유 변수를 만들고자 할 때 


: 당연함. 



※ static 멤버 함수는 오직 static 멤버들만 접근이 가능하다

-> 당연한 것이, 아직 생기지도 안은 객체의 non-static 멤버에 접근한다는 것이 말이 안된다.


※ 하지만 non-static 멤버 함수는 static 멤버를 접근하는데 전혀 문제가 되지 않는다

-> 이것도 당연한 것이 static멤버는 이미 생성되어 있기 때문이다.

his를 사용할 수 없다.

※ static 멤버 함수는 t

-> static 멤버 함수는 객체가 생기기 전부터 호출 가능하므로 this를 사용할 수 없다.

'C++ > C++ 문법' 카테고리의 다른 글

#8-1 상속  (0) 2018.10.07
#7-B 연산자 중복  (0) 2018.10.07
#07-A 프렌드  (0) 2018.10.05
#5-B 복사 생성자  (0) 2018.10.05
#5-A 함수와 참조  (0) 2018.10.04

|5.5| 복사 생성자

● 얕은 복사와 깊은 복사 (shallow copy & deep copy)

Person 객체의 멤버 변수로 

int id;
char *name;

이 있다고 가정하자.

얕은 복사의 경우, name 포인터로 문자열 배열을 공유하기 때문에 사본 객체에서 name 문자열을 변경하면,
원본 객체의 name 문자열이 변경되는 문제가 발생한다.

깊은 복사는 원본의 name 포인터가 가리키는 메모리까지 복사하여 원본과 사본의 name은 별개의 메모리를 가리킨다.

=> 가능하면 얕은 복사가 일어나지 않도록 해야 한다.


● 복사 생성 및 복사 생성자



복사 생성자 선언


1
2
3
4
class ClassName {
    ClassName(ClassName& c); // 복사 생성자
};
 
cs



※ 복사 생성자의 매개변수는 오직 하나이며,

자기 클래스에 대한 참조로 선언된다.

또한, 복사 생성자는 클래스에 오직 한 개만 선언이 가능하다.


복사 생성자 실행


1
2
3
4
Circle src(30); // 일반적인 생성자 호출
 
Circle dest(src); /* src 객체를 복사하여 dest 객체 생성
                  복사 생성자 Circle(Circle& c) 호출 */
cs



※ 예시 : Circle 크래스의 복사 생성자와 객체 복사


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
 
class Circle {
private:
    int radius; 
public:
    Circle(Circle& c); // 복사 생성자 선언
    Circle() { radius = 1; }
    Circle(int radius) { this->radius = radius; }
    double getArea() { return 3.14*radius*radius; }
}; 
 
Circle::Circle(Circle& c) { // 복사 생성자 구현
    this->radius = c.radius;
    cout << "복사 생성자 실행 radius = " << radius << endl;
}
 
int main() {
    Circle src(30); // src 객체의  보통 생성자 호출
    Circle dest(src); // dest 객체의 복사 생성자 호출
 
    cout << "원본의 면적 = " << src.getArea() << endl;
    cout << "사본의 면적 = " << dest.getArea() << endl;
}
 
cs



실행 결과 :

복사 생성자 실행 radius = 30

원본의 면적 = 2826

사본의 면적 = 2826



※ 만약 복사 생성자를 따로 정의하지 않는다면?


=> 컴파일러가 디폴트 복사 생성자(default copy constructor)를 묵시적으로 삽입한다.


※   하지만 default copy constructor 코드는 shallow copy를 실행하도록 만들어진 코드이다.

포인터 타입 변수가 없는 클래스의 경우, 얕은 복사는 전혀 문제가 없다.

" 공유 " 의 문제가 발생하지 않기 때문이다.


※ 예제 : 얕은 복사 생성자를 사용하여 문제가 생기는 경우 ( 프로그램이 비정상 종료 )



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#define _CRT_SECURE_NO_WARNINGS //비주얼 스튜디오에서 strcpy로 인한 오류를 막기 위한 선언문
#include <iostream>
#include <cstring>
using namespace std;
 
class Person { // Person 클래스 선언
    char* name;
    int id;
public:
    Person(int id, const char* name); // 생성자
    ~Person(); // 소멸자
    void changeName(const char *name);
    void show() { cout << id << ',' << name << endl; }
};
 
Person::Person(int id,const char* name) { // 생성자
    this->id = id;
    int len = strlen(name); // name의 문자 개수
    this->name = new char [len+1]; // name 문자열 공간 핟당
    strcpy(this->name, name); // name에 문자열 복사
}
 
Person::~Person() {// 소멸자
    if(name) // 만일 name에 동적 할당된 배열이 있으면
        delete [] name; // 동적 할당 메모리 소멸
}
 
void Person::changeName(const char* name) { // 이름 변경
    if(strlen(name) > strlen(this->name))
        return// 현재 name에 할당된 메모리보다 긴 이름으로 바꿀 수 없다.
    strcpy(this->name, name);
}
 
int main() {
    Person father(1"Kitae");            // (1) father 객체 생성
    Person daughter(father);            // (2) daughter 객체 복사 생성. 복사 생성자 호출
 
    cout << "daughter 객체 생성 직후 ----" << endl;
    father.show();                        // (3) father 객체 출력
    daughter.show();                    // (3) daughter 객체 출력
 
    daughter.changeName("Grace");        // (4)     daughter의 이름을 "Grace"로 변경
    cout << "daughter 이름을 Grace로 변경한 후 ----" << endl;
    father.show();                        // (5) father 객체 출력
    daughter.show();                    // (5) daughter 객체 출력
 
    return 0;                            // (6), (7) daughter, father 객체 소멸
}
cs

  





※    main 함수 종료 시, daughter 객체가 먼저 소멸되고, father 객체의 소멸자가 name에 할당된 메모리를 힙에 반환하려고 할 때!!!

이미 반환한 메모리를 반환하게 되므로, 오류가 발생하고 프로그램이 비정상 종료된다.



※ 예시 : 깊은 복사 생성자를 가진 정상적인 Person 클래스




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#define _CRT_SECURE_NO_WARNINGS //비주얼 스튜디오에서 strcpy로 인한 오류를 막기 위한 선언문
#include <iostream>
#include <cstring>
using namespace std;
 
class Person { // Person 클래스 선언
    char* name;
    int id;
public:
    Person(int id, const char* name); // 생성자
    Person(Person& person); // 복사 생성자
    ~Person(); // 소멸자
    void changeName(const char *name);
    void show() { cout << id << ',' << name << endl; }
};
 
Person::Person(int id,const char* name) { // 생성자
    this->id = id;
    int len = strlen(name); // name의 문자 개수
    this->name = new char [len+1]; // name 문자열 공간 핟당
    strcpy(this->name, name); // name에 문자열 복사
}
 
Person::Person(Person& person) { // 복사 생성자
    this->id = person.id; // id 값 복사
    int len = strlen(person.name);// name의 문자 개수
    this->name = new char [len+1]; // name을 위한 공간 핟당
    strcpy(this->name, person.name); // name의 문자열 복사
    cout << "복사 생성자 실행. 원본 객체의 이름 " << this->name << endl;
}
 
Person::~Person() {// 소멸자
    if(name) // 만일 name에 동적 할당된 배열이 있으면
        delete [] name; // 동적 할당 메모리 소멸
}
 
void Person::changeName(const char* name) { // 이름 변경
    if(strlen(name) > strlen(this->name))
        return// 현재 name에 할당된 메모리보다 긴 이름으로 바꿀 수 없다.
    strcpy(this->name, name);
}
 
int main() {
    Person father(1"Kitae");            // (1) father 객체 생성
    Person daughter(father);            // (2) daughter 객체 복사 생성. 복사 생성자 호출
 
    cout << "daughter 객체 생성 직후 ----" << endl;
    father.show();                        // (3) father 객체 출력
    daughter.show();                    // (3) daughter 객체 출력
 
    daughter.changeName("Grace"); // (4)     // daughter의 이름을 "Grace"로 변경
    cout << "daughter 이름을 Grace로 변경한 후 ----" << endl;
    father.show();                        // (5) father 객체 출력
    daughter.show();                    // (5) daughter 객체 출력
 
    return 0;                                // (6), (7) daughter, father 객체 소멸
}
cs




● 묵시적 복사 생성 주의 상황들


1
Person daughter(father); // 복사 생성자를 명시적으로 호출
cs




1. 객체로 초기화하여 객체가 생성될 때


1
2
3
4
Person son = father;
// Person son(father);
// 이 묵시적으로 호출된다
 
cs



1
2
3
// 위와 헷갈리지 말자
Person = son;
son = father; // 복사 생성자 호출되지 않음
cs


=> '복사 생성' 과 '복사'의 차이 ★★★★ (성돈이한테 질문함)


2. 'call by value' 로 객체가 전달될 때


=> 앞에서 call by value로 매개 변수 객체가 생성될 때, 생성자가 실행되지 않는다고 하였는데,

생성자 대신에 복사 생성자가 실행된다.



3. 함수가 객체를 리턴할 때



1
2
3
4
5
6
Person g() {
    Person mather(2"Jane");
    return mother; /* mother의 복사본을 생성하여 복사본 리턴
                   사본이 만들어 질 떄 복사 생성자 호출*/
}
g();
cs



=> g()가 mother 객체를 리턴할 때, mother 객체의 복사본을 만들어 넘겨준다. 복사본을 만들 때 복사 생성자가 호출된다.






'C++ > C++ 문법' 카테고리의 다른 글

#8-1 상속  (0) 2018.10.07
#7-B 연산자 중복  (0) 2018.10.07
#07-A 프렌드  (0) 2018.10.05
#6 함수 중복과 static 멤버  (0) 2018.10.05
#5-A 함수와 참조  (0) 2018.10.04