상속 (Inheritance)

  • 클래스를 구현할 때, 다른 클래스나 객체에 구현되어 있는 동작을 유지하기 위해 그것을 기반으로 만드는 것.

  • 일반적으로는 기반이 되는 것을 부모(Parent) 클래스, 기반을 활용하여 만드는 클래스를 자식(Child) 클래스라고 함.

  • 상속을 하는 이유

    • 기반 클래스의 코드를 재활용 하기 위함

    • 다형성(Polymorphism)을 활용하기 위함.

  • 상속의 종류

    • 단일 상속(Single Inheritance): 하위 클래스에서 한 개의 상위 클래스로부터 상속 받는 것.

    • 다중 상속(Multiple Inheritance): 하위 클래스에서 두 개 이상의 상위 클래스로부터 상속 받는 것.

    • 다 단계 상속(Multilevel Inheritance): 하위 클래스에서 다른 하위 클래스로부터 상속 받는 것.

// Parent class
class FirstParent { }
class SecondParent { }

// Single Inheritance
class FirstChild : public FirstParent { }
class SecondChild : public SecondParent { }

// Multiple Inheritance
class ThirdChild : public FirstParent, pulbic SecondParent { }

// Multilevel Inheritance
class FirstDescendant : public FirstChild { }
class SecondDescendant : public SecondChild { }

// Multiple + Multilevel Inheritance
class ThirdDescendant : public FirstChild, public SecondChild { }
  • 상속 관계에서 하위 클래스는 상위 클래스의 멤버 변수들을 모두 갖기 때문에, 하위 클래스의 객체를 생성할 때 하위 클래스와 상위 클래스의 생성자가 모두 호출되어야 하며, 객체가 파괴될 때 상위 클래스와 하위 클래스의 소멸자가 모두 호출되어야 함.

    • 생성자 호출 순서: 상위 클래스 생성자 호출 --> 하위 클래스 생성자 호출

    • 소멸자 호출 순서: 하위 클래스 소멸자 호출 --> 상위 클래스 소멸자 호출

  • 하위 클래스들은 자신의 고유한 멤버 변수나 함수 뿐만 아니라 상위 클래스의 멤버 변수와 함수까지 가질 수 있음.

    • Programmer와 Designer가 Person을 상속받으면, 두 클래스 모두 Person의 멤버 변수 및 함수를 사용 가능.

#include <iostream>
using namespace std;

class Person {
private:
    string name;
protected:
    int age;
public:
    Person(string _name, int _age) : name(_name), age(_age) {
        cout << "Person Constructor." << endl;
    }
    Person(const Person& src) : name(src.name), age(src.age) { }
    ~Person() { cout << "Person Destructor." << endl; }

    void introduce() { cout << name << ": Hi!" << endl; }
    void setName(string _name) { name = _name; }
    string getName() const { return name; }
    void setAge(int _age) { age = _age; }
    int getAge() const { return age; }
};


class Programmer : public Person {
private:
    int numOfLang;
public:
    Programmer(string _name, int _age, int _numOfLang)
        : Person(_name, _age), numOfLang(_numOfLang) {
        cout << "Programmer Constructor." << endl;
    }
    ~Programmer() { cout << "Programmer Destructor." << endl; }
    void setNumOfLang(int _numOfLang) { numOfLang = _numOfLang; }
    int getNumOfLang() const { return numOfLang; }
};


class Designer : public Person {
private:
    int numOfTools;
public:
    Designer(string _name, int _age, int _numOfTools)
        : Person(_name, _age), numOfTools(_numOfTools) {
        cout << "Designer Constructor." << endl;
    }
    ~Designer() { cout << "Designer Destructor." << endl; }
    void setNumOfTools(int _numOfTools) { numOfTools = _numOfTools; }
    int getNumOfTools() const { return numOfTools; }
};


int main() {
    Programmer pr("ke2ek", 24, 4);
    Designer dg("2ekke2", 40, 3);
    pr.introduce();
    cout << "I'm " << pr.getAge() << " years old." << endl;
    dg.introduce();
    cout << "I'm " << dg.getAge() << " years old." << endl;
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Person Constructor.         <---------- 중복 호출, virtual keyword 사용 시 한 번만 호출 가능
Designer Constructor.
ke2ek: Hi!
I'm 24 years old.
2ekke2: Hi!
I'm 40 years old.
Designer Destructor.
Person Destructor.
Programmer Destructor.
Person Destructor.         <---------- 중복 호출, virtual keyword 사용 시 한 번만 호출 가능

  • 상속의 접근 범위

    • 클래스의 멤버 변수나 함수에 접근 제한자를 두는 것처럼, 상속 관계 안에도 접근 제한자를 통해, 하위 클래스에서 상위 클래스의 멤버 변수의 접근 범위를 정할 수 있음.

    • 상속 관계 안에서의 접근 제한자에 따라 3가지의 상속 관계로 분류.

    • public 상속: 하위 클래스에서는 상위 클래스의 접근 제한자의 특성을 그대로 물려 받음.

    • protected 상속: 하위 클래스에서는 상위 클래스의 public 접근 제한자도 protected로 바꿔서 물려 받음. 따라서 외부에서는 하위 클래스의 객체로 상위 클래스의 public 멤버 함수를 호출할 수 없음.

    • private 상속: 하위 클래스에서는 상위 클래스의 모든 접근 제한자를 private으로 바꿔서 물려 받음. 따라서 하위 클래스 내부에서도 상위 클래스의 protected나 public 멤버 변수 및 함수를 사용할 수 없음.

 

함수 재정의 (Function Overriding)

  • 상속 관계일 때, 상위 클래스의 함수를 하위 클래스에서 재정의 하는 것을 의미.

  • 상위 클래스의 함수 이름, 반환형, 파라미터의 형태가 같은 함수를 동일하게 하위 클래스에서 정의.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    ~Person() { cout << "Person Destructor." << endl; }
    void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() {
        cout << "Programmer Constructor." << endl;
    }
    ~Programmer() {
        cout << "Programmer Destructor." << endl;
    }
    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Person p;
    p.doWork(); // Working ...
    Programmer pr;
    pr.doWork(); // Programming ...
    return 0;
}

 

Up/Down Casting

  • 하위 클래스의 객체를 상위 클래스의 객체에 대입할 대 발생하는 형 변환을 업 캐스팅(Up-casting)이라고 하며, 일반적으로 업 캐스팅을 하면 하위 클래스의 데이터가 손실될 수 있음.

    • 따라서 데이터 손실을 방지하기 위해 참조자 또는 포인터를 사용하여 업 캐스팅을 함.

    • 그대로 참조자나 포인터 변수를 사용하면 안되고 클래스 내부에서 virtual 키워드를 사용해야 함.

// the same as above

int main() {
    Programmer pr;
    Person p = pr;
    p.doWork(); // Working ...
    
    // better than previous
    Programmer pro;
    Person &pRef = pro; // reference
    pRef.doWork(); // Working ...
    
    Person *pPtr = new Programmer; // pointer
    pPtr->doWork();; // Working ...
    
    // BUT, Data loss occurred still.
    // We need virtual keyword!
    return 0;
}
  • 상위 클래스의 객체를 하위 클래스의 객체에 대입할 때 발생하는 형 변환을 다운 캐스팅(Down-casting)이라고 하며, 일반적으로 상위 클래스의 객체를 하위 클래스 객체로 형 변환 하는 것은 상대적으로 메모리가 작은 상위 클래스 객체에서 메모리가 큰 하위 클래스의 객체로 변환하기 때문에 정상 동작을 보장하지 않음.

// the same as above

int main() {
    /* Error
    Person p;
    Programmer pr = p; 
    pr.doWork();
    */
    
    Person p;
    Programmer *pr = reinterpret_cast<Programmer*>(&p);
    pr->doWork();
    
    return 0;
}

 

가상 함수 (Virtual Function)

  • 부모 클래스의 멤버 함수 앞에 virtual 이라는 키워드를 붙이고, 자식 클래스에서 그 함수를 오버라이딩(Overidding)할 때, 포인터 또는 참조자 타입의 업 캐스팅(Up-casting)하여 객체를 사용하는 경우 자식 클래스의 함수를 호출함.

  • 업 캐스팅 시 virtual 키워드를 붙여주지 않으면 부모 클래스의 함수가 호출 되는데, 그 이유는 함수의 호출 위치를 컴파일 타임(early time)에 결정하기 때문.

  • virtual 키워드를 붙여 주면, 해당 함수의 호출 위치가 코드가 컴파일 되는 시점에 결정되는 것이 아니라 런타임(late time)에 결정되기 때문에 하위 클래스의 오버라이딩 된 함수를 호출하게 됨.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    ~Person() { cout << "Person Destructor." << endl; }
    
    virtual void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }
    
    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Programmer pro;
    Person &pRef = pro; // reference
    pRef.doWork(); // Programming ...
    
    Person *pPtr = new Programmer; // pointer
    pPtr->doWork(); // Programming ...

    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Programming ...               <----- 재정의 된 함수 호출
Person Constructor.
Programmer Constructor.
Programming ...                <----- 재정의 된 함수 호출
Programmer Destructor.  <----- 레퍼런스 변수에 의해 호출된 소멸자
Person Destructor.           <----- 레퍼런스 변수에 의해 호출된 소멸자

  • 소멸자 역시 함수이므로 오버라이딩 된 소멸자가 호출되게 하려면 반드시 상위 클래스에 virtual 키워드를 붙여야 함.

    • 소멸자의 경우 제대로 처리하지 않으면 메모리 누스(Memory Leak)이 발생할 수 있으니 주의 깊게 다루어야 함.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Person *pPtr = new Programmer;
    pPtr->doWork(); // Programming ...
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Programming ...

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Person *pPtr = new Programmer;
    pPtr->doWork(); // Programming ...
    delete pPtr;
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Programming ...
Person Destructor.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Person *pPtr = new Programmer; // pointer
    pPtr->doWork(); // Programming ...
    delete pPtr;
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Programming ...
Programmer Destructor.
Person Destructor.

 

추상 클래스 (Abstract Class)

  • 순수 가상 함수(Pure Virtual Function)

    • 부모 클래스에서 상속받을 자식 클래스의 공통 기능의 정의만 선언해 놓고, 실제 구현은 자식 클래스에게 위임하기 위한 함수.

    • 문법: virtual  반환형  함수명(매개변수)  =  0;

  • 추상 클래스란 순수 가상 함수를 하나 이상 포함하는 클래스를 의미

  • 추상 클래스는 객체화(인스턴스화)를 할 수 없으며, 시도하게 될 경우 컴파일 오류가 발생함.

  • 상위 클래스에 대한 객체 생성 요구가 발생하지 않는 상황이라면 상위 클래스에 순수 가상 함수를 선언하여 추상 클래스로 만드는 것이 권장됨.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() = 0;
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    // Error
    // Person p;
    Person *pPtr = new Programmer;
    pPtr->doWork(); // Programming ...
    delete pPtr;
    return 0;
}

 

가상 상속 (Virtual Inheritance)

  • 다중 상속(Multiple Inheritance)을 구현하다 보면, 다음과 같은 문제가 발생함.

    • Parent 클래스를 상속한 FirstChild와 SecondChild 클래스를 Descendant 클래스가 다중 상속하는 경우

    • Parent 클래스가 추상 클래스이기 때문에 FirstChild와 SecondChild 클래스의 멤버함수가 동일한 형태를 띈다면

    • Descendant 클래스에서 그 멤버 함수를 사용하지 않더라고 컴파일 오류가 발생함.

    • 또는 Descendant 클래스를 만들거나 파괴할 때, Parent 클래스의 생성자와 소멸자가 2번 호출됨.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    virtual ~Programmer() { cout << "Programmer Destructor." << endl; }
};


class Designer : public Person {
public:
    Designer() : Person() { cout << "Designer Constructor." << endl; }
    virtual ~Designer() { cout << "Designer Destructor." << endl; }
};


class Freelancer : public Programmer, public Designer {
public:
    Freelancer() : Programmer(), Designer() {
        cout << "Freelancer Constructor." << endl;
    }
    ~Freelancer() { cout << "Freelancer Destructor." << endl; }
};


int main() {
    Freelancer f;
    f.doWork();
    return 0;
}

[결과]

// the same as above

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() = 0;
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    virtual ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


class Designer : public Person {
public:
    Designer() : Person() { cout << "Designer Constructor." << endl; }
    virtual ~Designer() { cout << "Designer Destructor." << endl; }

    void doWork() { cout << "Designing ..." << endl; }
};

// the same as above

[결과]

// the same as above

int main() {
    Freelancer f;
    //f.doWork();
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Person Constructor.
Designer Constructor.
Freelancer Constructor.
Freelancer Destructor.
Designer Destructor.
Person Destructor.
Programmer Destructor.
Person Destructor.

  • 상속 문법에서 접근 제한자 뒤에 virtual 키워드를 붙이면 위의 문제가 해결됨.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public virtual Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    virtual ~Programmer() { cout << "Programmer Destructor." << endl; }
};


class Designer : public virtual Person {
public:
    Designer() : Person() { cout << "Designer Constructor." << endl; }
    virtual ~Designer() { cout << "Designer Destructor." << endl; }
};


class Freelancer : public Programmer, public Designer {
public:
    Freelancer() : Programmer(), Designer() {
        cout << "Freelancer Constructor." << endl;
    }
    ~Freelancer() { cout << "Freelancer Destructor." << endl; }
};


int main() {
    Freelancer f;
    f.doWork();
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Designer Constructor.
Freelancer Constructor.
Working ...
Freelancer Destructor.
Designer Destructor.
Programmer Destructor.
Person Destructor.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() = 0;
};


class Programmer : public virtual Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    virtual ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


class Designer : public virtual Person {
public:
    Designer() : Person() { cout << "Designer Constructor." << endl; }
    virtual ~Designer() { cout << "Designer Destructor." << endl; }

    void doWork() { cout << "Designing ..." << endl; }
};


class Freelancer : public Programmer, public Designer {
public:
    Freelancer() : Programmer(), Designer() {
        cout << "Freelancer Constructor." << endl;
    }
    ~Freelancer() { cout << "Freelancer Destructor." << endl; }

    // You must implement duplicate function overidding again.
    void doWork() { cout << "Working too much ..." << endl; }
};


int main() {
    Freelancer f;
    f.doWork();
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Designer Constructor.
Freelancer Constructor.
Working too much ...
Freelancer Destructor.
Designer Destructor.
Programmer Destructor.
Person Destructor.

 

다형성 (Polymorphism)

  • 하나의 모습으로 다양한 형태를 가질 수 있는 성질.

  • 클래스를 상속받아 사용하는 가장 큰 이유.

  • Person *pPtr = new Programmer; 또는 Person *pPtr = new Designer; 등 하나의 클래스 타입으로 다양한 형태를 가질 수 있다.

  • 특히, 함수의 매개변수로 클래스 타입을 받을 때, 여러 하위 클래스를 처리해야 한다면 다형성이 꼭 필요하다.

 

복사 생성자(Copy Constructor)

  • 동일한 클래스 타입의 객체를 매개변수로 받아 현재 객체에 덮어씌우는 생성자.

  • 매개변수 타입은 const 레퍼런스 변수로, 객체 복사로 인한 오버로드를 피하고 안전성을 높일 수 있음.

  • 실제로 만들지 않아도 Person p1; Person p2(p1); 처럼 사용할 수 있는데, 디폴트 생성자처럼 명시적으로 복사 생성자를 만들지 않으면 컴파일러가 자동으로 만들어 줌.

  • 복사 생성자를 구현하는 방법은 멤버 변수들을 전부 복사하는 것으로, 멤버 이니셜라이저를 이용하는 것과 생성자 본문을 이용하는 것이 있음.

    • Person(const Person& src) : height(src.height), weight(src.weight) { }

    • Person(const Person& src) { height = src.height; weight = src.weight; }

  • 대부분은 복사 생성자를 직접 만들 필요는 없음.

#include <iostream>

class Person {
private:
    double height;
    double weight;
public:
    Person(const Person& src);
};

Person(const Person& src)
    : height(src.height), weight(src.weight) { }

Person(const Person& src) {
    height = src.height;
    weight = src.weight;
}

int main() {
    Person p1(183.4, 78.5);
    Person p2(p1);
    return 0;
}

 

복사 대입 연산자(Copy Assignment Operator)

  • 동일한 클래스 타입의 객체를 현재 객체에 대입하는 연산자.

  • 복사 대입 연산자는 = 연산자를 각 클래스에서 오버로딩해서 구현함.

    • Person& operator=(const Person& rhs);

  • 복사 대입 연산자도 명시적으로 선언하지 않으면 객체 간 대입이 가능하도록 컴파일러가 자동으로 만들어 줌.

  • 복사 생성자와 달리 복사 대입 연산자는 Person의 참조 객체를 반환하는데, 그 이유는 대입 연산이 중첩되어 수행될 수 있기 때문임.

    • 예를 들어, obj1 = obj2 = obj3; 라는 코드가 있을 때, 먼저 obj2의 대입 연산자가 obj3를 우변 항목 인자로 호출함.

    • 그 다음, obj1의 대입 연산자가 호출되는데 이 때 우변 항목 인자는 obj2가 아니며, obj1의 대입 연산자는 obj2의 대입 연산자가 obj3를 인자로 해 실행된 반환값을 우변 항목 인자로 취함.

    • 만약 대입 연산이 실패해서 반환값이 없다면, obj1으로 전달할 인자가 없어지므로 오류가 발생함.

    • obj1의 대입 연산자가 그냥 obj2를 인자로 취할 경우, =기호가 멤버 함수 호출을 래핑(Wrapping)만 하기 때문에 obj1 = obj2 = obj3; 는 사실상 obj1.operator=(obj2.operator=(obj3)); 가 실행되는 것과 같으므로 결국 obj2.operator=의 올바른 반환값은 obj2 그 자체가 되어야 함.

    • 결국 객체 반환에 따른 임시 객체로의 복제 오버로드를 피하려면 참조형으로 반환되는 것이 바람직함.

  • 복사 대입 연산자의 구현 방법은 복사 생성자의 구현 방법과 비슷하나, 다음과 같은 중요한 차이점이 존재함.

    • 복사 생성자는 객체 초기화 시점에만 호출되기 때문에 대상 객체들의 멤버들이 아직 유효하지 않음.

    • 복사 대입 연산자는 이미 생성된 객체를 대상으로 하기 때문에 멤버들의 메모리 할당 완료 여부에 신경쓰지 않고도 값을 덮어 쓸 수 있음.

    • C++에서는 객체가 자기 스스로 대입하는 것이 문법적으로 가능하나, 문제가 발생할 수 있음을 유의해야 함.

    • 복사 대입 연산자가 실행되면 가장 먼저 인자가 자기 자신인지 검사하고, 그렇다면 복제 작업을 하지 않고 그대로 반환하게 만드는 형태가 가장 적절함.

#include <iostream>
using namespace std;

class Person {
private:
    double height;
    double weight;
public:
    Person& operator=(const Person& rhs);
    
    void print() {
        cout << "H = " << height;
        cout << ", W = " << weight << endl;
    }
};

Person& operator=(const Person& rhs)
{
    if (this == &rhs) return *this;
    
    height = rhs.height;
    weight = rhs.weight;
    
    return *this;
}

int main() {
    Person p1(183.4, 78.5), p2(175.6, 68.3);
    p1.print(); // H = 183.4, W = 78.5
    p1 = p2;
    p1.print(); // H = 175.6, W = 68.3
    return 0;
}

 

얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)

  • 복사 생성자, 복사 대입 연산자의 경우 컴파일러가 자동으로 만들어 주기 때문에 대부분 직접 구현할 필요가 없음.

  • 컴파일러가 자동으로 생성한 멤버 함수들은 멤버 변수들에 대해 재귀적으로 복사 생성자 또는 복사 대입 연산자를 호출함.

  • 단, int나 double, 포인터와 같이 기본 데이터 타입에 대해서는 복사 생성자나 복사 대입 연산자 대신 얕은 복사가 일어남.

  • 얕은 복사(shallow copy): 포인터가 가리키는 데이터는 빼놓고 주소 값만 복사하는 방식

  • 깊은 복사(deep copy): 포인터만 복사하지 않고 변수의 맥락에 맞게 연관된 데이터까지 재귀적으로 복사하는 방식

  • 그러나, 얕은 복사는 객체가 동적으로 할당받은 메모리를 가지고 있을 경우 문제가 됨.

    • 예를 들어, Person 클래스에서 사람이 가지고 있는 방의 개수(int numRooms;)를 받아 각 방의 면적을 나타내는 배열(int *roomWidth;)을 동적으로 생성한다고 가정했을 때, p1 = p2; 이후 소멸자가 호출될 때 p2의 소멸자가 먼저 호출되기 때문에 p1의 소멸자가 호출되어 roomWidth 을 delete할 때 이미 해제된 메모리를 다시 해제하므로 오류가 발생함. 

    • 또한, p1이 참조하던 원래 메모리는 가리키는 포인터가 없어짐. 이를 댕글링 포인터(dangling pointer)가 되며, 이는 심각한 메모리 누수 문제로 이어질 수 있음.

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

class Person {
private:
    int numRooms;
    int* roomWidth;
    string name;
public:
    Person(int numRooms, string name);
    ~Person();
    int getNumRooms() const;
    int* getRoomWidth() const;
    void setName(double _name);
    string getName() const;
};

Person::Person(int numRooms, string name) {
    this->numRooms = numRooms;
    this->name = name;
    this->roomWidth = new int[numRooms];
    for (int i = 0; i < this->numRooms; ++i) {
        this->roomWidth[i] = (i + 1) * 10;
    }
}

Person::~Person() {
    delete[] roomWidth;
    cout << "Destroyed!!" << endl;
    roomWidth = nullptr;
}

int Person::getNumRooms() const { return numRooms; }
int* Person::getRoomWidth() const { return roomWidth; }
void Person::setName(double _name) { name = _name; }
string Person::getName() const { return name; }

void printRoom(const Person& p) {
    string name = p.getName();
    cout << p.getName() << "'s room width:" << endl;
    const int n = p.getNumRooms();
    const int* roomWidth = p.getRoomWidth();
    for (int i = 0; i < n; ++i) {
        cout << "[Room" << i + 1 << "] width = " << roomWidth[i] << endl;
    }
}

int main() {
    Person p1(3, "Person 1");
    Person p2(5, "Person 2");
    printRoom(p1);
    printRoom(p2);
    p1 = p2;
    printRoom(p1);
    return 0;
}

 

[결과]

더보기

./test
Person 1's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
Person 2's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
[Room4] width = 40
[Room5] width = 50
Person 2's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
[Room4] width = 40
[Room5] width = 50
Destroyed!!                              <---- p2 의 것만 해제되고, 이후 p1의 소멸자에서 오류 발생.
test(3798,0x110f29dc0) malloc: *** error for object 0x7fecd3405810: pointer being freed was not allocated
test(3798,0x110f29dc0) malloc: *** set a breakpoint in malloc_error_break to debug
[1]    3798 abort      ./test

  • 위와 같은 문제를 피하려면 대입 대상인 좌변항의 객체가 참조하고 있는 메모리를 반환한 후에 새로 메모리를 준비해서 복사하는 깊은 복사(deep copy)를 해야 함.

// Copy Constructor
Person::Person(const Person& src)
    : numRooms(src.numRooms), name(src.name)
{
    this->roomWidth = new int[numRooms];
    for (int i = 0; i < this->numRooms; ++i) {
        this->roomWidth[i] = src.roomWidth[i];
    }
}

// Copy Assignment Operator
Person& Person::operator=(const Person& rhs)
{
    if (this == &rhs) return *this;
    delete[] this->roomWidth;
    this->roomWidth = nullptr;

    this->numRooms = rhs.numRooms;
    this->name = rhs.name;

    this->roomWidth = new int[this->numRooms];
    for (int i = 0; i < this->numRooms; ++i) {
        this->roomWidth[i] = rhs.roomWidth[i];
    }

    return *this;
}

[결과]

더보기

Person 1's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
Person 2's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
[Room4] width = 40
[Room5] width = 50
Person 2's room width:
[Room1] width = 10
[Room2] width = 20
[Room3] width = 30
[Room4] width = 40
[Room5] width = 50
Destroyed!!
Destroyed!!                              <---- 정상적으로 p1의 메모리를 해제하였음.

 

0의 법칙, 그리고 3의 법칙

  • Rule of Zero: 소멸자, 복사 생성자, 복사 할당 연산자 모두 명시적으로 만들지 않으면 컴파일러가 모두 자동으로 만들어준다는 법칙

  • Rule of Three: 소멸자, 복사 생성자, 복사 할당 연산자 중 하나를 명시적으로 만들면, 나머지 모두 명시적으로 만들어야한다는 법칙

 

멤버 변수 (Member Variable)

  • static

    • C언어에서 전역 변수와 유사하지만 클래스에 종속된다는 점이 다름.

    • 일반적으로 객체별로 변수를 가지기보다 모든 객체가 함께 사용하는 하나의 변수가 필요할 때 사용함.

    • 예를 들어, 하나의 변수가 바뀔 때마다 모든 객체의 멤버 변수를 동기화하는 작업이 비효율적일 때 사용됨.

    • "클래스명::변수명"으로 접근할 수 있음. 범위 지정 연산자 사용에 유의할 것.

  • const

    • 생성 시점에 초기값을 부여한 뒤, 더 이상 수정할 수 없는 변수.

    • 클래스가 아닌 객체에 종속됨.

    • 객체 수준에서 상수값을 보유하는 것은 대부분 메모리 낭비이지만, static const 멤버 변수를 이용해 객체 간에 상수값을 공유할 수 있음.

    • 예를 들어, GUI 프로그램의 초기 창 크기(가로, 세로 길이)는 static const 로 선언되는 것이 편리함.

  • reference (&)

    • 메모리를 참조할 때 사용하는 멤버 변수로, 생성과 동시에 다른 객체를 참조하도록 초기화 되어야 함.

    • 특정 클래스에서 다른 클래스를 참조할 때 포인터 또는 레퍼런스형을 사용할 수 있는데, 포인터보다는 레퍼런스를 사용하는 것이 바람직함.

    • 포인터와 달리 레퍼런스 타입은 적합한 객체로 초기화되어야만 존재할 수 있기 때문에 훨씬 안전함.

  • const reference

    • 레퍼런스형 멤버 변수는 const 객체를 참조할 수 있음.

    • const 객체를 참조하는 경우 const 객체의 const 멤버 함수만 호출 가능.

#include <iostream>
using namespace std;

class Vault {
private:
    int money;
public:
    Vault(int _memory) : money(_memory) { }
}

class Bank {
private:
    static double interestRate;
    const Vault& vault;
    int width, height;
public:
    static const int maxWidth = 300;
    static const int maxHeight = 300;
    
    Bank(const Vault& _vault, int _width, int _height);
    Bank(const Bank& src);
};

// Initialize static member variable.
double Bank::interestRate = 3.5;

// Constructor with Member Initializer
Bank::Bank(const Vault& _vault, int _width, int _height)
    : vault(_vault), width(_width), height(_height)
{ }

// Copy constructor
Bank::Bank(const Bank& src)
    : vault(src.vault), width(src.width), height(src.height)
{ }

int main() {
    Vault hanaVault(200'000'000);
    Bank hanaBank(hanaVault, 50, 50);
    
    cout << "Bank::maxWidth = " << Bank::maxWidth << endl;
    cout << "Bank::maxHeight = " << Bank::maxHeight << endl;
    cout << "hanaBank.vault = " << hanaBank.vault << endl;
    
    // Error
    // hanaBank.vault = 0;
    
    return 0;
}

 

멤버 함수 (Member Function)

  • 멤버 함수는 다른 말로 메서드(Method)라고도 함.

  • static

    • 객체에 종속되는 부분이 없다면 static 멤버 함수로 선언 가능.

    • 예를 들어, 소수값을 버리는 함수는 객체 종속되지 않으므로 static 멤버 함수로 선언하는 것이 바람직함.

    • 멤버 함수에서는 static 과 const 를 함께 사용할 수 없음.

    • 선언 시 static 을 맨 앞에 추가하지만, 구현 부분에서는 추가할 필요 없음.

    • static 멤버 함수는 연결된 객체가 없기 때문에 코드 구현부에서 this 포인터를 이용 불가.

    • static 멤버 변수만 이용 가능.

    • 클래스 내부의 멤버 함수들은 static 멤버 함수를 보통 멤버 함수처럼 호출 가능.

    • 클래스 외부에서는 static 멤버 함수 호출 시 "클래스명::함수명()"처럼 범위 지정 연산자(::)를 이용 해야 함.

  • const

    • 멤버 함수가 객체의 멤버 변수 값을 바꾸지 않는다고 보장하기 위해 사용함.

    • const 멤버 함수 안에서 객체의 멤버 변수를 변경하면 컴파일 오류 발생.

    • static 멤버 함수에는 const 제한자 적용 불가. 클래스 공통으로서 연계되는 객체가 없으므로 const 선언이 무의미하기 때문.

    • const가 아닌 객체에 대해서는 모든 메서드(const 포함) 호출 가능.

    • const 객체에 대해서는 const 메서드만 가능.

    • 가능하면 객체를 변경하지 않는 모든 멤버 함수에 const 적용을 권장함. 이유는 const 객체에서도 호출할 수 있도록 하기 위함.

    • const 제한자는 함수 선언에 포함되기 때문에 구현부에서도 똑같이 적용해야 함.

    • "반환형  클래스명::함수명()  const  { ... }"

  • default parameter

    • 디폴트 매개변수는 멤버 함수의 프로토타입을 선언할 때 각 매개변수에 디폴트 값을 지정하는 것을 의미.

    • 만약 사용자가 해당 인자를 직접 제공하면 디폴트 값은 무시됨.

    • 만약 해당 인자를 공란으로 해서 호출하면 디폴트 값이 자동으로 적용됨.

    • 디폴트 매개변수는 가장 마지막(오른쪽) 매개변수부터 시작해 파라미터 건너뛰지 않고 연속적으로만 적용 가능. 예를 들어, void func(int param1, int defaultParam1 = 200, int param2); 은 불가능. 반대로 void func2(int param1, int param2, int defaultParam1 = 200, int defaultParam2 = 400); 은 가능

    • 그렇게 하지 않으면 멤버 함수가 호출될 때 어느 매개변수에 디폴트 값을 적용할지 컴파일러가 판단할 수 없음.

    • 매개변수에 디폴트 값은 지정해도 구현부는 바뀌지 않음. 즉, 함수 프로토타입 선언할 때만 지정 가능하고 구현부 정의에서는 지정할 수 없음.

    • 생성자가 디폴트 매개변수를 가진다면, 하나의 생성자로 인자가 여러 개인 경우 모두 이용 가능.

    • 디폴트 매개변수로 할 수 있는 일은 멤버 함수 오버로딩으로도 할 수 있음. (선택적)

#include <iostream>
using namespace std;

class Vault {
private:
    int money;
public:
    Vault(int _memory) : money(_memory) { }
}

class Bank {
private:
    static double interestRate;
    const Vault& vault;
    int width, height;
    
    // new member variable
    string branchName;
public:
    static const int maxWidth = 300;
    static const int maxHeight = 300;
    
    Bank(const Vault& _vault, int _width, int _height);
    Bank(const Bank& src);
    
    // new member function
    static int roundDown(double val);
    // getter & setter
    string getBranchName() const;
    void setBranchName(string _bName);
};

// Initialize static member variable.
double Bank::interestRate = 3.5;

// Constructor with Member Initializer
/*
Bank::Bank(const Vault& _vault, int _width, int _height)
    : vault(_vault), width(_width), height(_height)
{ }
*/
// using default parameters
Bank::Bank(const Vault& _vault,
           int _width = maxWidth,
           int _height = maxHeight)
    : vault(_vault), width(_width), height(_height)
{ }

// Copy constructor
Bank::Bank(const Bank& src)
    : vault(src.vault), width(src.width), height(src.height)
{ }

int Bank::roundDown(double val) {
    return static_cast<int>(val);
}

string Bank::getBranchName() const {
    return branchName;
}

void Bank::setBranchName(string _bName) {
    branchName = _bName;
}

int main() {
    Vault hanaVault(200'000'000);
    
    Bank b1(hanaVault);
    Bank b2(hanaVault, 50);
    Bank b3(hanaVault, 50, 50);
    
    Bank hanaBank(hanaVault, 50, 50);
    hanaBank.setBranchName("Hana");
    cout << hanaBank.getBranchName() << endl; // Hana
    cout << Bank::roundDown(3.4) << endl; // 3
    
    return 0;
}

 

 

클래스와 객체(Class and Object)

  • C언어에서는 절차 지향 프로그래밍만 지원함. 절차 지향 프로그래밍이란 프로그램을 단순히 데이터와 이를 처리하는 방법으로 작성하는 것을 의미함.

  • C++에서는 객체 지향 프로그래밍도 지원하는데, 객체 지향 프로그래밍이란 실세계의 특정 대상을 객체로 나타내어 수많은 객체들의 상호작용으로 프로그램을 서술하는 방식을 의미함.

  • 객체(Object)의 개념

    • 객체 지향 프로그래밍에서는 데이터와 기능을 하나로 묶어서 다룰 수 있는 자료구조.

    • 프로그램을 처리하는 절차보다도 동작되는 자료에 중점을 둔 것.

    • 객체는 프로그램의 기본 단위이며, 객체 간의 상호 관계로 프로그램의 처리를 표현함.

    • 객체는 멤버 변수(특성, Attribute)와 이를 대상으로 처리하는 동작인 멤버 함수(메서드, Method)를 하나로 묶어 만든 요소

  • 구조적 프로그래밍에서는 변수와 함수가 합쳐져 프로그램을 만들지만, 객체 지향 프로그래밍에서는 멤버 변수와 멤버 함수가 합쳐져 하나의 객체를 만들고, 객체와 객체가 합쳐져 하나의 프로그램을 만듬.

  • C언어에서 구조체는, 서로 다른 타입의 변수들을 저장할 수 있는 사용자 정의 타입으로만 사용되었으며, 함수를 저장할 수 없어서 구조체를 매개변수로 받는 함수를 따로 만들어 처리해야 했음.

  • C++에서는 변수와 함수를 함께 저장할 수 있는 클래스(Class)라는 개념을 도입하였는데, 클래스를 통해 만들어지는 변수를 객체라고 부름. 참고로 구조체(struct)도 클래스로 분류되며, 함수를 저장할 수 있음.

  • 클래스는 설계도, 객체는 이 설계도를 기반으로 만들어진, 런타임(Run-time) 시 동작하는 실체로 정의. 클래스에서 객체로 바꾸는 작업을 인스턴스화라고 함.

  • 클래스 선언 방법

    • class 클래스 이름 { 멤버 변수; 멤버 함수; ... } -> 외부에서 멤버 변수나 함수에 접근하는 범위를 지정 가능

    • struct 클래스 이름 { 멤버 변수; 멤버 함수; ... } -> 외부에서 항상 멤버 변수나 함수에 접근 가능

  • 클래스의 멤버 함수

    • 함수의 내용은 클래스 내부나 클래스 외부에서 구현 가능.

    • 클래스 내부에서 구현된 멤버 함수들은 모두 인라인 함수(inline function)로 정의. 인라인 함수란 C언어에서 매크로 함수와 비슷한 개념으로 프로그램의 실행 속도를 높이기 위해 도입되었음.

    • 함수의 본체가 긴 경우, 멤버 함수를 외부에서 정의하는 것이 편리함. -> 클래스 이름::함수 이름(매개변수) {...}

  • 데이터 추상화: 현실 세계의 사물을 데이터적인 측면과 기능적인 측면을 통해서 정의하여 추상화하는 방법

    • 예를 들어, 강아지는 다리가 4개이고, 종류(말티즈 등)와 색상(흰색 등) 등의 데이터로 정의할 수 있으며, 짖을 때는 "멍멍"이라는 소리를 내며 달릴 수도 있음.

    • 다리 개수, 종류, 색상이라는 멤버 변수와 짖기, 뛰기라는 멤버 함수로 강아지라는 클래스를 작성할 수 있음.

#include <iostream>
using namespace std;

// Define class
class Dog {
public:
    int numLegs;
    string kind;
    string color;
        
    void bark();
    void run();
};

// Instantiation: create object!
int main() {
    Dog dog = { 4, "시고르자브종", "검은색" };
    dog.bark();
    dog.run();
    return 0;
}

void Dog::bark() {
    cout << kind << ": 멍멍!" << endl;
}

void Dog::run() {
    cout << numLegs << "개 다리로 후다닥!" << endl;
}
  • C++에서는 구조체를 이용해서 클래스를 정의할 수 있음.

struct Dog {
    int numLegs;
    string kind;
    string color;

    void bark(); // { cout << "멍멍!" << endl; }
    void run(); // { cout << "후다닥!" << endl; }
};

void Dog::bark() {
	cout << kind << ": 멍멍!" << endl;
}

void Dog::run() {
	cout << numLegs << "개 다리로 후다닥!" << endl;
}
  • C언어의 경우 위와 같은 데이터 추상화는 다음과 같이 표현됨.

#include <stdio.h>

typedef struct Dog {
    int numLegs;
    char kind[100];
    char color[10];
} Dog;

void bark(Dog* dog) {
    printf("%s: 멍멍!\n", dog->kind);
}

void run(Dog* dog) {
    printf("%d개 다리로 후다닥!\n", dog->numLegs);
}

int main() {
    Dog dog = { 4, "시고르자브종", "검은색" };
    bark(&dog);
    run(&dog);
    return 0;
}

 

접근 제한자(Access Modifier)

  • C++에서는 접근 제한자를 통해 클래스 외부에서 멤버 변수나 멤버 함수에 접근할 수 있는 범위를 정함.

  • private: 자신의 멤버 함수 외에는 접근할 수 없음.

  • protected: 자식 클래스의 멤버 함수로부터의 접근만 허용

  • public: 모든 곳으로부터의 접근을 허용

  • 접근 제한 정도: private > protected > public

Class Parent
{
private:
    int priv;
protected:
    int prot;
public:
    int pub;
    void accessAllMembers();
};

void Parent::accessAllMembers() {
    priv = 100; // Success
    prot = 100; // Success
    pub = 100; // Success
}

// 자식 클래스
class Child : public Parent {
public:
    void accessParents() {
        int n;
        n = priv; // Fail
        n = prot; // Success
        n = pub; // Success
    }
};

int main() {
    Parent parent;
    int n;
    n = parent.priv; // Fail
    n = parent.prot; // Fail
    n = parent.pub; // Success
}

 

정보 은닉과 캡슐화

  • 정보 은닉(Information Hiding)

    • 프로그램을 사용하는 사용자가 알아야 하는 것은 프로그램을 사용하는 방법이기 때문에, 내부 동작이나 상세 구조를 드러낼 필요는 없음.

    • 정보 은닉이란 사용자가 굳이 알 필요가 없는 정보를 숨김으로써, 최소한 정보만으로 프로그램을 쉽게 사용하도록 하는 방법.

    • C++에서는 클래스의 정보 은닉 기능을 지원하기 위해 접근 제한자 키워드를 제공함.

    • 숨길 멤버와 공개할 멤버의 블록을 구성하여 공개된 멤버는 외부에서 자유롭게 접근 가능하나 숨겨진 멤버를 직접 참조하려고 하면 오류를 발생하도록 처리.

    • 따라서 간접적으로 접근할 필요가 있을 때는 메소드를 활용.

    • Getter: 값을 읽기 위한 멤버 함수

    • Setter: 값을 쓰기 위한 멤버 함수

    • 일반적으로, 멤버 변수는 private이고 멤버 함수는 public이며, 프로그램 설계에 따라 달라질 수 있음.

class Point {
private:
    double x;
    double y;

public:
    double getX();
    double getY();
    void setX(double _x);
    void setY(double _y);
};
  • 캡슐화(Encapsulation)

    • 관련 있는 데이터와 함수를 하나의 단위로 묶는 것.

    • 정보 은닉과는 다른 개념.

// 정보 은닉 없이 캡슐화만 된 경우
class Point {
public:
    double x;
    double y;
};

 

 

생성자(Constructor)

  • 객체가 선언될 때 실행되는 코드로, 멤버 변수들을 초기화하는 역할을 함.

  • 클래스와 같은 이름을 함수의 이름으로 가지며, 인수나 리턴 값이 없는 상태로 선언.

  • 생성자는 public 이지만 싱글턴 패턴과 같이 private 으로 선언하는 경우도 존재함.

  • 생성자가 호출되는 시기

    • 객체를 만들 때: Person p;

    • 메모리를 할당할 때: Person* p = new Person;

  • 함수 오버로딩과 같은 조건으로 생성자들도 오버로딩이 가능함.

  • 디폴트 생성자(default constructor)

    • 생성자를 만들지 않을 경우 C++에서 자동으로 만들어주는 생성자.

    • 디폴트 생성자는 인수를 가지지 않지만, 멤버 변수들이 초기화됨.

    • 단, int나 char 등의 기본 타입은 초기화되지 않음.

    • 단, 생성자를 하나라도 선언할 경우 디폴트 생성자는 만들어지지 않음.

#include <iostream>

class Person {
private:
    double height;
    double weight;
public:
    Person(double _height, double _weight) {
        height = _height;
        weight = _weight;
    }
};

int main() {
    Person p(178.5, 70.8);
    Person p; // Error
    return 0;
}
  • const나 레퍼런스 멤버 변수는 선언과 동시에 초기화를 해야하므로, 디폴트 생성자가 아닌 멤버 이니셜라이저(Member Initializer)로 초기화를 해주어야 함.

class Person {
private:
    const string SSN;
    double height;
    double weight;
public:
    Person(const string _SSN, double _height, double _weight)
        : SSN(_SSN), height(_height), weight(_weight)
    { }
};

 

소멸자(Destructor)

  • 생성자가 객체를 초기화하듯, 반대로 객체가 더 이상 필요하지 않을 때 정리하는 코드.

    • 객체가 삭제될 때 파괴: Person* p = new Person(); delete p;

    • 블록 밖으로 넘어갈 때 파괴: if (true) { Person p; } // 여기서 p의 소멸자 호출

    • 다른 소멸자에 의한 파괴: A 클래스가 멤버 변수로 B 클래스를 가질 때, A의 소멸자가 호출된 후 B의 소멸자가 호출됨.

  • 생성자가 메모리를 할당하면 사용 후에는 운영체제로 리턴되어야 함.

  • 소멸자는 생성자의 이름 앞에 ~를 붙여서 정의함.

class IntPointer {
private:
    int* intPtr;
    
public:
    IntPointer() {
        pi = new int[5];
    }
    
    ~IntPointer() {
        delete[] pi;
        pi = nullptr;
    }
};
  • 생성 순서와 소멸 순서는 반대

    • Person a; Person b; 가 있으면 생성 순서는 a, b가 되고 소멸 순서는 b, a가 된다.

    • 먼저 선언한 객체가 먼저 생성

    • 먼저 선언한 객체가 나중에 소멸

#include <iostream>
using namespace std;

class A {
public:
    A() { cout << "A() called!" << endl; }
    ~A() { cout << "~A() called!" << endl; }
};

class B {
public:
    B() { cout << "B() called!" << endl; }
    ~B() { cout << "~B() called!" << endl; }
};

int main() {
    A a;
    B b;
    return 0;
}

/* output:
 A() called!
 B() called!
 ~B() called!
 ~A() called!
 */

 

this 포인터

  • 클래스에서 사용할 수 있는 특별한 포인터로, 현재 멤버 함수가 호출된 인스턴스의 메모리 주소를 가리킴.

  • 멤버 변수들과 멤버 함수들을 연결해주는 이유.

    • 멤버 변수들은 각각의 인스턴스에서 저장할 내용이 다르기 때문에 반드시 별도로 존재해야 하지만, 멤버 함수들은 인스턴스가 늘어나더라도 바뀔 필요가 없음.

    • 따라서, 프로세스 구조상 멤버 변수들이 보관되는 영역(스택, 힙, 데이터)과 멤버 함수들이 존재하는 영역(코드)은 나누어져 있음.

    • 코드 영역은 실행 중에 변경을 막기 위해 보호되어 런타임 중에는 수정될 수 없음.

    • 즉, 멤버 함수는 멤버 변수에 접근하기 위해 인스턴스를 식별할 필요가 있음.

  • 인스턴스는 독자적인 멤버 변수들을 가지지만, 클래스 공통의 멤버 함수와 매칭됨.

  • 멤버 함수를 호출하게 되면, 멤버 함수에 호출한 인스턴스의 포인터를 같이 보내고, 멤버 함수는 인스턴스의 포인터(this)를 가지고 멤버 변수들에 접근함.

  • 멤버 함수 내에서 명칭의 우선순위: 지역 변수 > 멤버 변수 > 전역 변수

#include <iostream>
using namespace std;

class Person {
private:
    double height;
    double weight;
public:
    Person() { }
    Person(double _height, double _weight)
        : height(_height), weight(_weight) { }
        
    void setHeight_wrong(double height) {
        height = height; // Not changing
    }
    
    void setHeight_1(double height) {
        this->height = height;
    }
    
    double getHeight() { return height; }
};

int main() {
    Person p(183.4, 78.5);
    p.setHeight_wrong(182.8);
    cout << p.getHeight() << endl; // 183.4
    p.setHeight(182.8);
    cout << p.getHeight() << endl; // 182.8
    return 0;
}

 

입출력 함수

  • C 언어

#include <stdio.h>

int main() {
    int nbr = 1;
    char sentence[1024];
    print("Enter some characters: ");
    scanf("%s", sentence);
    printf("%d: %s\n", nbr, sentence);
    printf("Hello World!\n");
    return 0;
}
  • C++

#include <iostream>

int main() {
    int nbr = 1;
    char sentence[1024];
    std::cin >> sentence;
    std::cout << x << ": " << sentence << std::endl;
    std::cout << "Hello World!" << std::endl;
    return 0;
}
  • 헤더 파일의 변화: <stdio.h> 에서 <iostream>

  • C언어에서 사용된 헤더파일은 대부분 앞에 c를 붙이고 ".h"를 제외하면 똑같이 사용 가능.

    • <stdio.h> == <cstdio> 또는 <stdlib.h> == <cstdlib> 또는 <string.h> == <cstring> 등

  • C++에서 표준 입출력 함수는 입력이나 출력 형식을 지정하지 않고 사용함. 소수점 자릿수 등을 따져야할 경우 C언어의 표준 입출력을 가져다 써야함.

  • 속도는 C언어의 표준 입출력(printf/scanf)가 C++ 것(cin/cout)보다 빠름.

  • C++ 에서 개행문자는 std::endl 로도 표현 가능.

 

bool type

  • C언어에서는 조건문의 참, 거짓을 표현하기 위해 int 타입을 사용.

    • 참: 0을 제외한 모든 값

    • 거짓: 0

#include <stdio.h>

int main() {
    int x = 1;
    
    if (x) {
        printf("x is true.\n");
    } else {
        printf("x is false.\n");
    }
    return 0;
}
  • C++에서는 참과 거짓을 명확하게 사용하기 위해 bool 타입이 추가됨.

    • 참: true

    • 거짓: false

#include <iostream>

int main() {
    bool b = true;
    
    if (b) {
        std::cout << "b is true." << std::endl;
    } else {
        std::cout << "b is false." << std::endl;
    }
    
    // 표준 출력 시 int 타입
    std::cout << b << std::endl; // 1
    std::cout << !b << std::endl; // 0
    
    // bool 포맷을 유지하기
    std::cout.setf(std::ios::boolalpha);
    std::cout << b << std::endl; // true
    std::cout << !b << std::endl; // false;
    
    // 다시 int로 출력하기
    std::cout.unsetf(std::ios::boolalpha);
    std::cout << b << std::endl; // 1
    std::cout << !b << std::endl; // 0
    
    return 0;
}

 

auto keyword

  • C++11 부터 추가된 auto 타입 연산자를 사용하면, 컴파일 타임에 타입을 추론해 어떤 타입인지 결정함.

  • 기본 내장 타입을 포함해 컴파일 타임에 추론 가능한 모든 타입에 사용 가능.

  • 만약 컴파일 타임에 추론이 불가능하다면, 오류가 발생함.

#include <iostream>
#include <vector>

int main() {
    int i1 = 10;
    double d1 = 3.14;
    
    auto i2 = 10; // integer
    auto d2 = 3.14; // double
    
    // 복잡한 타입을 가지는 경우
    std::vector<std::pair<int, double> > vPairs;
    for (std::vector<std::pair<int, double> >::iterator iter = vPairs.begin();
        iter != vPairs.end(); ++iter)
    {
        std::cout << "So complex!" << std::endl;
    }
    
    // auto keyword
    for (auto iter = vPairs.begin(); iter != vPairs.end(); iter++)
    {
    	std::cout << "auto keyword is good." << std::endl;
    }
    
    return 0;
}

 

범위 기반 for문(Range-based for)

  • for 문은 지정된 횟수만큼 반복하는 작업에도 사용하나, 배열이나 각종 컨테이너에 있는 각 요소들에 무언가를 수행하기 위해 순회하는 목적으로도 많이 사용함.

  • 그러나, 조건문에 배열이나 컨테이너에 몇 개의 요소가 들어있는지 명시해야 한다는 단점이 있음.

  • 범위 기반 for문은 요소들의 개수에 상관없이 반복문을 수행함.

#include <iostream>

auto mean(const Sequence& seq)
{
    auto n = 0.0;
    for (auto& x : seq) {
        n += x;
    }
    
    return n / seq.size();
}

int main() {
    int arr[] = { 1, 2, 3, 4, 5 };
    for (auto& i : arr) {
        std::cout << i << std::endl;
    }
    
    // same as
    for (int i = 0; i < 5; ++i) {
        std::cout << i << std::endl;
    }
    return 0;
}

 

메모리 할당과 해제

  • C언어

#include <stdio.h>
#include <stdlib.h>

int main() {
    int i;
    int** arr = (int**) malloc(sizeof(int*) * 5);
    for (i = 0; i < 5; ++i) {
        arr[i] = (int*) malloc(sizeof(int) * 5);
    }
    
    for (i = 0; i < 5; ++i) {
        free(arr[i]);
    }
    free(arr);
    arr = NULL;
    
    return 0;
}
  • C++

#include <iostream>

int main() {
    int** arr = new int*[5];
    for (int i = 0; i < 5; ++i) {
        arr[i] = new int[5];
    }
    
    for (int i = 0; i < 5; ++i) {
        delete[] arr[i];
    }
    delete[] arr;
    arr = NULL;
    
    int* p1 = new int;
    delete p1;
    p1 = NULL;
    
    int* p2 = new int[10];
    delete[] p2;
    p2 = NULL;
    
    return 0;
}

 

널 포인터

  • C언어에서 널 포인터를 나타내기 위해 NULL이나 상수 0을 사용.

    • #define NULL 0

    • 따라서 NULL == 0

  • NULL 은 포인터가 아니므로, 함수에 인자로 넘기는 경우 int 타입으로 추론되는 문제가 발생함.

  • C++에서는 nullptr 키워드로 널 포인터를 표현함.

  • nullptr은 널 포인터 리터럴(Literal) 이며, 실제 타입은 std::nullptr_t 이다.

#include <iostream>

// function overloading
void fn(int a) {
    std::cout << "fn(int)" << std::endl;
}

void fn(int* a) {
    std::cout << "fn(int*)" << std::endl;
}

int main() {
    fn(0); // fn(int)
    fn(NULL); // fn(int)
    fn(nullptr); // fn(int*)
    
    std::cout << sizeof(nullptr) << std::endl; // 4
    std::cout << typeid(nullptr) << std::endl; // std::nullptr_t
    
    int* p1 = new int;
    delete p1;
    p1 = nullptr; // DO NOT USE NULL
    
    int* p2 = new int[10];
    delete[] p2;
    p2 = nullptr; // DO NOT USE NULL
    
    return 0;
}

 

명시적 캐스팅

  • C언어에서는 int(..) 또는 char(...)를 통해 명시적(explicit) 캐스팅을 할 수 있으나, 개발자의 실수를 그대로 용납하기 때문에 차후에 오류가 생길 수 있음.

  • C++에서는 보다 명시적인 캐스팅 방법으로 캐스팅의 목적을 명확하게 명시함으로써 개발자의 의도를 컴파일러에게 전달하여 오류를 방지함.

  • static_cast<타입>(표현식);

#include <iostream>

int main() {
    char* str = "Hello, World!";
    int* intPtr = static_cast<int*>(str);
    char* charPtr = static_cast<char*>(*intPtr);
    
    std::cout << charPtr << std::endl;
    
    /* In the case of C
       int* intPtr = (int*) str;
       char* charPtr = (char*)*pi;
     */
    
    return 0;
}

 

범위 지정 열거체(Scoped enum)

  • C/C++ 에서 기존에 사용하던 열거형은 전역(global) 범위로 선언되기 때문에 A 열거체에서 사용한 T라는 이름은 B 열거체에서는 사용할 수 없음.

  • 또한, 기본적으로 열거체의 타입은 int이기 때문에 묵시적으로 변환되어 서로 다른 열거체 변수끼리 더하거나 비교할 수 있음.

#include <stdio.h>

/* C/C++98 */
int main() {
    enum TrafficLight { Red, Yellow, Green };
    enum Job { Warrior, Ranger, Wizard };
    enum Coffee : unsigned char { Latte = 10, Mocha = 25 };
    
    int jobNum = Warrior;
    int i = Green + Latte; // 12
    
    if (Yellow == Ranger) {
        printf("Same!\n");
    } else {
        printf("Different!\n");
    }
    
    return 0;
}
  • C++11부터 위의 문제점들을 보완하기 위해 범위 지정 열거체를 제공.
    • enum 대신 enum class를 사용.
    • 묵시적 변환이 되지 않으므로, 명시적으로 타입을 지정하지 않으면 오류 발생.
    • 서로 다른 열거체 변수끼리 연산할 수 없음.
#include <iostream>

// C++11
int main() {
    enum class TrafficLight { Red, Yellow, Green };
    enum class Job { Warrior, Ranger, Wizard, Green };
    enum class Coffee : unsigned char { Latte = 10, Mocha = 25 };
    
    Job jobNum = Job::Warrior;
    int i = static_cast<int>(TrafficLight::Green)
        + static_cast<int>(Coffee::Latte);
    
    // TrafficLight::Yellow != Job::Ranger
    
    return 0;
}

 

Binary literal, separator

  • C/C++에서는 10진수 외에 8진수, 16진수를 표현할 수 있음.

    • 8진수: 061 (=49) --> 0으로 시작

    • 16진수: 0x3A (=58) --> 0x로 시작

  • C++14부터 2진수를 표현하는 방법이 추가됨. 0b로 시작 --> 0b110101

  • C++14부터 구분자(separator)가 추가되어 큰 숫자를 읽기 쉬움.

    • int INT_MAX = 21'4748'3647

#include <iostream>

int main() {
    int decimal = 52;
    int octal = 064;
    int hexadecimal = 0x34;
    int binary = 0b110100; // C++14
    
    int maxInt_Cpp98 = 2147483647;
    int maxInt_Cpp14 = 21'4748'3647; // C++14
    
    return 0;
}

 

문자열(string) 자료구조

  • C언어에서는 문자열을 나타내기 위해 char[] 또는 char* 를 사용하였으나, 이는 비교(strcmp), 연결(strcat), 길이(strlen) 등 문자열 관련 함수를 사용할 때 불편함.

#include <stdio.h>

int main() {
    char str1[30] = "Hello, World!";
    char* str2 = "Hello, ke2ek!";
    
    if (strcmp(str1, str2) == 0) printf("Same!\n");
    else printf("Different!\n");
    
    strcat(str1, str2);
    printf("%s\n", str1); // Hello, World!Hello, ke2ek!
    printf("%d, %d\n", strlen(str1), strlen(str2));
    
    return 0;
}
#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello, World!";
    std::string str2 = "Hello, ke2ek!";
    
    if (str1 == str2) std::cout << "Same!" << std::endl;
    else std::cout << "Different!" << std::endl;
    
    str1 = str1 + str2;
    std::cout << str1 << std::endl; // Hello, World!Hello, ke2ek!
    std::cout << str1.size() << ", " << str2.length() << std::endl;
    
    return 0;
}

 

레퍼런스(Reference) 변수

  • 다른 변수를 가리키는 변수 (포인터 기능의 일부)로, 다른 변수와 동일한 메모리 위치를 공유함.

  • 레퍼런스 변수로 연산할 때는 포인터와 달리 *나 &를 추가해야 하는 문법이 없음.

  • 레퍼런스는 항상 유효한 메모리 주소를 참조해야 하므로, const처럼 선언과 동시에 초기화를 해주어야 함.

#include <iostream>

// Pass-by-pointer
void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// Pass-by-reference
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

void print(int a, int b) {
    std::cout << "a = " << a;
    std::cout << ", b = " << b << std::endl;
}

int main() {
    // int& ref; NEED TO ASSIGN.
    int i = 10;
    int &ret = i;
    
    std::cout << "ref = " << ref << std::endl; // 10
    
    ret += 10;
    std::cout << "i = " << i << std::endl; // 20
    std::cout << "ref = " << ref << std::endl; // 20
    
    int a = 10, b = 20;
    print(a, b); // a = 10, b = 20
    swap(&a, &b); // Pass-by-pointer
    print(a, b); // a = 20, b = 10
    
    int c = 30, d = 40;
    print(c, d); // c = 30, d = 40
    swap(c, d); // Pass-by-reference
    print(c, d); // c = 40, d = 30
    
    return 0;
}

 

함수 오버로딩

  • C언어와 달리, C++에서는 여러 함수를 동일한 이름으로 선언할 수 있는 함수 오버로딩이라는 개념을 지원함.

  • 함수 오버로딩 규칙

    • 오버로딩된 함수는 인수의 개수가 서로 달라야 함.

    • 인수의 개수가 같은 경우, 인수의 타입이 서로 달라야 함.

    • 리턴 타입이 다른 경우, 같은 이름의 함수를 사용할 수 없음.

#include <iostream>

int sum(int num1, int num2);
int sum(int num1, int num2, int num3);
int sum(short num1, short num2);

// Compile Error!
// short sum(short num1, short num2);

int main() {
    int a = 10, b = 20, c = 30;
    int d = sum(a, b);
    int e = sum(a, b, c);
    short f = 40, g = 50;
    int h = sum(f, g);
    return 0;
}

 

네임스페이스(Namespace)

  • 어떤 프로젝트에서 A와 B가 구현한 코드를 합치는데 동일한 이름의 변수나 함수가 있을 경우, 기본 타입을 확장해서 구분해줄 필요가 있음. C언어에서는 이런 문제를 해결할 수 없으나, C++에서는 네임스페이스를 이용해서 해결 가능.

  • 네임스페이스는 namespace 이름 { ... } 으로 정의하며, 중첩해서 사용 가능.

  • 네임스페이스 A에 있는 변수 a에 접근하려면, 범위 지정 연산자(::)를 사용해야 함. A::a

  • 표준 라이브러리의 네임스페이스는 std로, cin/cout 같은 표준 입출력 사용 시 std::cin 또는 std::cout으로 접근해야 함.

  • 네임스페이스를 매번 적기 귀찮을 경우, using namespace 이름; 을 헤더파일 밑에 선언해주면 됨.

    • 표준 라이브러리 사용 시, using namespace std; 를 많이 사용함.

#include <iostream>
using namespace std;

namespace A {
    void print() {
        cout << "A's print()" << endl;
    }
}

namespace B {
    void print() {
        cout << "B's print()" << endl;
    }
}

int main() {
    A::print(); // A's print()
    B::print(); // B's print()
    
    return 0;
}

 

 

+ Recent posts