2016년 2월 22일 월요일

놓치기 쉬운 C++ 디테일들


  • malloc이 생성자를 호출하지 않는 것과 마찬가지로 free는 파괴자를 호출하지 않는다. 객체를 동적 할당할 때는 반드시 new/delete 연산자를 사용하여 생성자와 파괴자를 적절하게 호출하도록 해야 한다.



  • 정수 하나를 인수로 취하는 생성자가 정의되어 있다면, 이 클래스는 디폴트 생성자를 가지지 않는다. 이 경우 디폴트 생성자 선언문은 적절한 생성자를 찾을 수 없으므로 에러로 처리될 것이다. 별도의 생성자를 제공했다는 것은 클래스를 만든 사람이 이 객체는 이런 식으로 초기화해야 한다는 것을 분명히 명시한 것이므로 컴파일러는 이 규칙을 어긴 코드에 대해 사정없이 에러로 처리한다. 디폴트 생성자가 없는 클래스는 객체 배열을 선언할 수 없다. (이건 가능 Position There[3]={Position(1,2,'x'),Position(3,4,'y'),Position(5,6,'z')};)   



  •  ' Person Boy("강감찬",22); Person Young=Boy; ' 이 코드는 정상적으로 컴파일되며 실행도 되지만 종료할 때 파괴자에서 실행중 에러가 발생하는데 왜 그런지 보자. Young 객체가 Boy객체로 초기화될 때 멤버별 복사가 발생하며 Young의 Name멤버가 Boy의 Name과 동일한 번지를 가리키고 있다. 두 객체가 같은 메모리를 공유하고 있기 때문에 한쪽에서 Name을 변경하면 다른 쪽도 영향을 받게 되어 서로 독립적이지 못하다. 이 객체들이 파괴될 때 문제가 발생하는데 각 객체의 파괴자가 Name 번지를 따로 해제하기 때문이다. new는 Boy의 생성자에서 한 번만 했고 delete는 각 객체의 파괴자에서 두 번 실행하기 때문에 이미 해제된 메모리를 다시 해제하려고 시도하므로 실행중 에러가 된다. 이 문제를 해결하려면 초기화할 때 얕은 복사를 해서는 안되며 깊은 복사를 해야 하는데 이때 복사 생성자가 필요하다. 얕은 복사가 문제의 원인이었으므로 깊은 복사를 하는 복사 생성자를 만들어 해결할 수 있다.
    Person(const Person &Other)   {
        Name=new char[strlen(Other.Name)+1];
        strcpy(Name,Other.Name);
        Age=Other.Age; } 
복사 생성자는 자신과 같은 타입의 다른 객체에 대한 레퍼런스를 전달받아 이 레퍼런스로부터 자신을 초기화한다. Person복사 생성자는 동일한 타입의 Other를 인수로 전달받아 자신의 Name에 Other.Name의 길이만큼 버퍼를 새로 할당하여 복사한다. 새로 메모리를 할당해서 내용을 복사했으므로 이 메모리는 완전한 자기 것이며 안전하게 따로 관리할 수 있다. 복사 생성자의 인수는 반드시 객체의 레퍼런스여야 하며 객체를 인수로 취할 수는 없다. 포인터 형식도 안된다.


  • 함수의 인수로 객체를 넘기는 경우는 아주 흔한데 이때도 복사 생성자가 호출된다. 함수 호출 과정에서 형식 인수가 실인수로 전달되는 것은 일종의 복사생성이다. 함수 내부에서 새로 생성되는 형식인수 AnyBody가 실인수 Boy를 대입받으면서 초기화되는데 이때 복사 생성자가 없다면 AnyBody가 Boy를 얕은 복사하며 두 객체가 동적 버퍼를 공유하는 상황이 된다. AnyBody는 지역변수이므로 PrintAbout 함수가 리턴될 때 AnyBody의 파괴자가 호출되고 이때 동적 할당된 메모리가 해제된다. 이후 Boy가 메모리를 정리할 때는 이미 해제된 메모리를 참조하고 있으므로 에러가 발생할 것이다. 복사 생성자가 정의되어 있으면 AnyBody가 Boy를 깊은 복사하므로 아무런 문제가 없다.
void PrintAbout(Person AnyBody) {
 AnyBody.OutPerson(); }


  • 멤버에 단순히 값을 대입하기만 하는 경우 본체에서 = 연산자를 쓰는 대신 초기화 리스트(Member Initialization List)라는 것을 사용할 수 있다.
Position(int ax, int ay, char ach) : x(ax),y(ay),ch(ach){
     // 더 하고 싶은 일 }
상수는 선언할 때 반드시 초기화해야 한다. const int year=365; 의 형식으로 상수를 선언하는데 여기서 =365를 빼 버리면 다시는 이 상수값을 정의할 수 없으므로 에러로 처리된다. 단, 클래스의 멤버일 때는 객체가 만들어질 때까지 초기화를 연기할 수 있으며 생성자의 초기화 리스트에서만 초기화 가능하다. 
레퍼런스 멤버는 다음과 같이 대입 연산자로 초기화할 수 없다. 포함된 객체를 초기화할 때도 초기화 리스트를 사용한다.
class Some {
        int &ri;
Position Pos;
Some(int &i,int x,int y) { ri=i; Pos(x,y); } // correction ---> Some(int &i) : ri(i),Pos(x,y) { }
}


  • 프렌드 지정은 전이(A->B->C != A->C)되지 않으며 친구의 친구 관계는 인정하지 않는다.friend class B, C; 이런 문법은 허용되지 않는다. 프렌드 관계는 상속되지 않는다.



  • Num은 여전히 Count 클래스 내부에 선언되어 있되 static 키워드를 붙여 정적 멤버임을 명시했다. 클래스 선언문에 있는 int Num; 선언은 어디까지나 이 멤버가 Count의 멤버라는 것을 알릴 뿐이지 메모리를 할당하지는 않는다. 그래서 정적 멤버 변수는 외부에서 별도로 선언및 초기화해야 한다. Count 클래스 선언문 뒤에 Num 변수를 다시 정의했는데 이때 반드시 어떤 클래스 소속인지 :: 연산자와 함께 소속을 밝혀야 한다. 외부 정의에 의해 메모리가 할당되며 이때 초기값을 줄 수 있다.
class Count {
    private:
int Value;
static int Num;
public: static void InitNum() { }; };
int Count::Num=0; 
정적 멤버 변수는 객체와 논리적으로 연결되어 있지만 객체 내부에 있지는 않다. 정적 멤버 변수를 소유하는 주체는 객체가 아니라 클래스이다. 그래서 객체 크기에 정적 멤버의 크기는 포함되지 않으며 sizeof(C) = sizeof(Count)는 객체의 고유 멤버 Value의 크기값인 4가 된다.


  • 정적 멤버 함수는 정적 멤버만 액세스할 수 있으며 일반 멤버(비정적 멤버)는 참조할 수 없다. 왜냐하면 일반 멤버 앞에는 암시적으로 this->가 붙는데 정적 멤버 함수는 this를 전달받지 않기 때문이다. 정적 멤버 함수인 InitNum에서 비정적 멤버인 Value를 참조하는 것은 불가능하다.
static void InitNum() {
     Num=0;
     Value=5; // Value를 불법으로 참조했다는 에러 메시지가 출력된다.
}


  • 상수 멤버 함수는 멤버값을 변경할 수 없는 함수이다. 멤버값을 단순히 읽기만 한다면 이 함수는 객체의 상태를 바꾸지 않는다는 의미로 상수 멤버 함수로 지정하는 것이 좋다. 클래스 선언문의 함수 원형 뒤쪽에 const 키워드를 붙이면 상수 멤버 함수가 된다. 함수의 앞쪽에서는 리턴값의 타입을 지정하기 때문에 const를 함수 뒤에 붙이는 좀 별난 표기법을 사용한다.
     int GetValue() const;             // 상수 멤버 함수
단, 상수 멤버 함수라도 정적 멤버 변수의 값은 변경할 수 있는데 정적 멤버는 객체의 소속이 아니며 객체의 상태를 나타내지도 않기 때문이다.
     const Position There(3,4,'B');
     There.MoveTo(40,10);           // 에러 발생
 There는 상수 객체로 선언되었므로 상수 멤버 함수인 OutPosition만 호출할 수 있으며 MoveTo 호출문은 에러로 처리된다.

  • 다형성 : 재정의될 수 있는 부모클래스의 메서드를 가상화 함으로써 , 차후에 특정 메서드를 동작시켰을 때 경우에 따라 다른 동작을 할 수 있는 능력. ( 부모 클래스형의 포인터로부터 멤버 함수를 호출할 때 비가상 함수는 포인터가 어떤 객체를 가리키는가에 상관없이 항상 포인터 타입 클래스의 멤버 함수를 호출한다. 반면 가상 함수는 포인터가 가리키는 실제 객체의 함수를 호출한다는 점이 다르다. 그래서 파생클래스에서 재정의하는 멤버 함수, 또는 앞으로라도 재정의할 가능성이 있는 멤버 함수는 가상으로 선언하는 것이 좋다. 그래야 부모 클래스의 포인터 타입으로 자식 객체의 멤버 함수를 호출해도 정확하게 호출된다. )

  • 동적 바인딩 : 컴파일러는 gotoxy 함수가 어떤 주소에 있는지 알고 있으며 그래서 gotoxy 호출문을 이 함수의 주소로 점프하는 코드로 번역할 것이다. 컴파일하는 시점(정확하게는 링크 시점)에 이미 어디로 갈 것인가가 결정되는 이런 결합 방법을 정적 결합(Static Binding) 또는 이른 결합(Early Binding)이라고 한다. 결합(Binding)이란 함수 호출문에 대해 실제 호출될 함수의 번지를 결정하는 것을 말하는데 지금까지 작성하고 사용했던 일반적인 함수들은 모두 정적 결합에 의해 번역된다. 그런데 가상 함수는 포인터가 가리키는 객체의 타입에 따라 호출될 실제 함수가 달라지므로 컴파일시에 호출할 주소가 결정되는 정적 결합으로는 정확하게 호출할 수 없다. 왜냐하면 포인터가 실행중에 어떤 타입의 객체를 가리킬지 컴파일 중에는 알 수 없기 때문이다.  실행중에 호출할 함수를 결정하는 이런 결합 방법을 동적 결합(Dynamic Binding) 또는 늦은 결합(Late Binding)이라고 한다. 이렇게 해야 전달된 객체에 따라 각기 다른 동작을 할 수 있는 다형성을 구현할 수 있다. 동적 결합은 멤버 함수를 포인터(또는 레퍼런스)로 호출할 때만 동작한다.


  • 가상 함수 :  "동적 결합 함수". 이 함수에 virtual이라는 용어를 쓴 이유는 전통적인 함수처럼 정적 결합을 하지 않으며 파생 클래스가 재정의해도 안전하다는 뜻이다. ( 즉, 래퍼런스로써 부모 포인터가 자식 클래스를 가리키고 있을 경우, 동적 결합에 의해 , 부모 메서드를 오버라이딩 하고있는 자식 메서드가 동작할것을 약속한다. )


  • 파괴자를 가상함수로 선언하지 않는것은 잠재적으로 메모리 누수를 일으킬 여지가 있다.

  • 가상 함수를 써야할 경우 1. 해당 클래스가 자식 클래스로 파생될 가능성이 조금이라도 있을경우 2. 파생 클래스에서 함수의 동작을 재정의할 가능성이 조금이라도 있는경우 3. 부모 클래스 타입의 포인터로부터 자식 클래스가 호출당할 가능성이 조금이라도 있는경우.



  • 순수 가상 함수 : 파생 클래스에서 반드시 재정의해야 하는 함수이다. 순수 가상 함수는 일반적으로 함수의 동작을 정의하는 본체를 가지지 않으며 따라서 이 상태에서는 호출할 수 없다. 본체가 없다는 뜻으로 함수 선언부의 끝에 =0이라는 표기를 하는데 이는 함수만 있고 코드는 비어 있다는 뜻이다 " virtual void Draw()=0; "하나 이상의 순수 가상 함수를 가지는 클래스를 추상 클래스(Abstract Class)라고 한다. 추상 클래스는 동작이 정의되지 않은 멤버 함수를 가지고 있기 때문에 이 상태로는 인스턴스를 생성할 수 없다.








댓글 없음:

댓글 쓰기