C++에서 기본적으로 비어있는 클래스를 만들었을경우, 컴파일러가 자동으로 선언해주는 함수가 있습니다.

이함수들은 모두 public이고 inline함수 입니다.

 

즉!

class Empty()

{

 // 이렇게 빈 껍데기 클래스를 만들게 되면

};

 

class Empty()

{

    public:

      Empty() { }                                              // 기본 생성자

      Empty( const Empty& rhs ){ }                     // 복사 생성자

      ~Empty() { }                                            // 기본 소멸자

      Empty& operator=( const Empty& rhs) { }     // 복사 대입 연산자

};

위의 클래스와 같이 자동으로 생성이 됩니다.

 

Empty e1;       // 기본 생성자, 기본소멸자

Empty e2(e1); // 복사생성자

e2 = e1;         // 복사 대입 연산자

 

 

만약에 클래스 멤버 변수가 참조자이거나 상수일 경우에는 기본 복사 대입 연산자가 생성되지 않고,

호출할경우 에러를 내버린다.

 

이유는?

기본 생성 된 복사 대입 연산자는 호출될 때 넘겨받은 인자의 내용을 모든 멤버 변수에 복사하게되는데,

멤버변수가 참조자(&)라면 대입 이전에 처음생성되는 시점에서 이미 다른위치를 참조하게 되고, 그이후에는 이 참조되는 위치를 바꿀 수 없기 때문이다.즉! 참조자는 처음에 참조된 객체 외에 다른 객체를 다시 참조할 수 없기 때문 입니다. 

 

또한, 상수일 경우에도 마찬가지이다. 생성된 이후에 값의 변경은 불가능 하기 때문에 사용자가 직접 복사 대입 연산자를 정의해 주어야 한다.

 

그리고

복사 대입 연산자를 private로 선언한 기본 클래스로부터 파생된 클래스일 경우, 기본 복사 대입 연산자는 생성되지 않는다.

 

이유는?

파생된 클래스의 객체에서 복사 대입 연산자가 호출될때, 기본클래스의 복사 생성자를 호출해야하는데 private 영역에 있어서 호출 할 수가 없기 때문이다.

 


이것 만은 잊지 말자!


◆ 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있습니다.


 


C++ 규칙에 의하면 어떤객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다고 명시되어 있습니다.

 

또한 int, float, double등 기본 제공 타입의 경우에는( 생성자 안에서) 대입되기 전에 초기화되리란 보장이 없습니다. 무엇보다도 기본제공타입의 경우 초기화는 반드시 이루어져야한다.

 

ex) 초기화가 아닌 생성자 내에서 대입

Test::Test( const std::string& name )  { // 생성자

   theName = name; // 초기화가 아니라 대입하고 있습니다.

}

 

ex) 위의 코드 대신 해결방안

Test::Test( const std::string& name ) : theName(name)

{

    // 이처럼 멤버 초기화 리스트를 활용 하면 된다. (멤버 이니셜라이져)

}

초기화 리스트에 들어가는 인자는 바로 데이터 멤버에 대한 생성자의 인자로 쓰입니다.

즉! theName은 name으로 부터 복사생성자에 의해 초기화가 됩니다. 이렇게 되면 기본생성자를 호출하고 곧바로 복사생성자를 호출하는 ( ex)초기화가 아닌 생성자내에서 대입 ) 보다는 효율적입니다.

 

상수이거나 참조자로 되어있는 데이터 멤버일 경우 반드시 멤버 초기화 리스트로 초기화 되어야 한다.

대입이 불가능하기 때문입니다.

 

ex) 생성자가 많고, 초기화 리스트에 공통되는 데이터가 많을경우

class Test{

  void initialize();

public:

    Test();

   ~Test();

};

Test::Test(){

initialize(); // 초기화 변수가 담긴 private 멤버 함수

}

 

위의 코드와 같이 별도의 초기화 함수로 옮기는 것도 나쁘지 않습니다. initialize() 함수는 대개 private의 멤버로써 모든생성자에서 이함수를 호출 합니다.

 

객체를 구성하는 데이터의 초기화 순서

 

1. 기본 클래스(Base Class)는  파생클래스(Child Class)보다 먼저 초기화 됩니다.

2. 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화 된다.( 따라서 멤버 초기화 리스트도 멤버 선언순서에 맞게 작성하도록 합니다. )

 

정적객체란?

자신이 생성된 시점에서부터 프로그램이 끝날때까지 살아있는 객체 ( main 함수 종료시 소멸 )

 

정적객체의 종류

1. 전역객체

2. 네임스페이스 유효범위에서 정의된 객체

3. 클래스 안에서 static으로 선언된 객체

4. 함수 안에서 static으로 선언된 객체

5. 파일 유효범위에서 static으로 정의된 객체

 

번역단위란?

컴파일을 통해 하나의 목적 파일(object file)을 만드는 바탕이 되는 소스코드, 기본적으로 소스파일 하나가 되는데, 그소스파일안에 #include하는 파일들까지 합쳐서 하나의 번역 단위가 됩니다.

 

여기서 중요한 사실 하나

 

만약에 별도로 컴파일된 두개 이상의 소스가 있는데, 각각의 소스파일에는 비지역 정적객체가 한개 이상 들어있을경우 문제가 되는데 이유는 "별개의 번역 단위에서 즉! 각각의 소스파일이 컴파일된 오브젝트에서 정의된 비지역 정적 객체(전역객체)들의 초기화 순서는 정해져 있지 않다." 때문입니다.

 

난 객체 B에서 객체 A를 쓰고 싶다고 한다면, 당연히 객체 A가 먼저 초기화 되어야 할텐데 초기화 순서가 정해져있지 않기때문에 이 상황은 어떻게 될지 모릅니다.

 

즉!

이 상황을 해결하기 위해서는 설계를 디자인패턴에 (싱글톤 패턴)으로 구현하여야 합니다.

 

ex ) 탬플릿 동적 싱글톤 패턴

template <typename T> class Singleton
{
   public:
     inline static T* getSingletonPtr()

    {
       static T* ms_Singleton = new T;
       return ms_Singleton;

    }

}

 

ex ) 탬플릿 정적 싱글톤 패턴

template <typename T> class Singleton
{
   public:
    inline static T& getSingletonPtr() // 비정적 객체는 -> 지역 정적객체로 바꿈
    {
       static T ms_Singleton;
       return ms_Singleton;

     }

}

 

위의 템플릿 코드로 구현하게 되면, 지역 정적객체( 함수에서 static 선언한 객체)는 함수 호출중에 그 객체의 정의에 최초로 닿았을때 초기화 되기 때문에, 초기화 순서 문제를 방지 할 수 있게 됩니다.

 

한가지 문제점이 있습니다.

싱글톨 패턴인 참조자 반환함수는 내부적으로 정적 객체를 쓰기 때문에, 다중스레드 시스템에서는 동작에 장애가 생길 수도 있습니다. 당연합니다.

 

이와같은 문제의 해결방법으로는 프로그램이 다중스레드로 돌입하기 전의 시동 단계에서 참조자 반환 함수를 전부 손으로 호출해서 초기화 시키는 것 입니다.

 

이것 만은 잊지 말자!

◆ 기본제공 타입의 객체는 직접 손으로 초기화합니다. 경우에 따라 저절로 되기도 하고 안되기도 하기때문입니다.

◆ 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 즐겨 사용합시다. 그리고 초기화 리스트에 데이터멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 나열합시다.

◆ 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 합니다. 비지역 정적 객체를 지역 정적 객체로 바꾸면 됩니다.

 


const char * p; or char const * p;

const 키워드가 *의 왼쪽에 있으면 포인터가 가르키는 대상이 상수

 

char * const p;

const 키궈드가 *의 오른쪽에 있으면 포이터 자체가 상수

 

STL반복자에 const를 쓸 경우

 

std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin(); // T* const 와 같은 동작을 합니다.

*iter = 10 ; // OK! iter가 가리키는 대상을 변경합니다.

++iter;       // Error! iter는 상수입니다.

 

std::vector<int>::const_iterator cIter = vec.begin();// const T*와 같은 동작을 합니다.

*cIter = 10; // Error! cIter가 상수이기 때문에 안됩니다.

++cIter;      // OK!

 

함수의 반환값을 상수로 지정하면 이점이 있습니다.

안전성이나 효율을 포기하지 않고도 사용자측의 에러 돌발 상황을 줄이는 효과를 얻을 수 있습니다.

 

const Rational operator* ( const Rational& lhs, const Rational& rhs );

 

Rational a, b, c;

(a * b) = c;    //Error! 우리가 원하는 코드는 ( a * b) == c, 분명한 실수!

 

우리는 const 키워드를 쓰면, 이같은 실수를 미연에 방지 할 수 있습니다.

 

즉!

매개변수 혹은 지역 객체를 수정할 수 없게 하는 것이 목적이라면 const로 선언하는 것을 잊지 맙시다.

 

상수멤버함수

멤버 함수에 붙는 const 키워드의 역할은?

"해당 멤버 함수가 상수 객체에 대해 호출될 함수 이다"라는 사실을 알려주는 것입니다.

 

 

상수멤버함수의 중요성

 

1. 클래스의 인터페이스를 이해하기 좋게 하기위해서

 - 그 클래스로 만들어진 객체를 변결할 수 잇는 함수는 무엇이고, 변경할 수 없는 함수는 무엇인가?

2. 이 키워드를 통해 상수객체를 사용할 수 있게 하자는 뜻

 

C++ 프로그램의 실행 성능을 높이는 핵심 기법 중 하나가 객체 전달을 상수 객체에 대한 참조자를 사용하는 것이다. 이 기법이 제대로 살아 움직이려면 상수 상태로 전달된 객체를 조작할 수 있는 const 멤버 함수, 즉 상수 멤버 함수가 준비되어 있어야 한다는 것

 

"const 키워드가 함수에 있고, 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능하다"

 

1번. const char& operator[](std::size_t position) const ;

2번. char& operator[](std::size_t position);

 

const TextBlock ctb("World");

std::cout<<ctb[0]; //1번 호출

ctb[0] = 'X'; // Error! 리턴타입이 const char&이기 때문인데, 이 이유뿐만아니라, 상수 객체로 선언하였기 때문에, 말그대로 데이터를 변경한다는것은 말이 안된다. 그래서 리턴타입도 const 를 붙여줍니다.

 

TextBlock tb("Hello");

std::cout<<tb[0]; // 2번 호출

tb[0] = 'X'; // OK!

 

여기서 중요한점 또한가지는 리턴형이 char& 라는것인데, 말 그대로 우리가 변경할 값을 변경하기 위해서는 &로 리턴을 해야합니다. &로 리턴하지 않으면, 그냥 리턴되는 복사본에다가 우리는 대입하는것이기때문에 실질적으로 변경하려는 값에는 변경이 안되는 것이죠.

 

 

어떤 멤버함수가 상수 멤버라는 것이 대체 어떤 의미를 지닐까요?

 

1. 비트수준 상수성 ( 물리적 상수성 )

 - 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 const 임을 인정

 

ex ) 비트 수준 상수성 검사를 통과하는 멤버함수

class CTextBlock {

public:

  char& operator[] (std::size_t position) const  // 내부 데이터에 대한 참조자를 반환

  { return pText[position]; }

private:

  char *pText;

};

 

위의 코드에서 operator[] 는 pText를 건드리지는 않습니다. 컴파일 오류도 없습니다.

이로써 컴파일러는 내부의 데이터를 건드리지 않았기 때문에 비트수준 상수성에서 통과 해버립니다.

사실상 위의 operator[]는 비트수준 상수성에 위배되는 것입니다.

 

아무 문제가 없어 보이지만, 이코드에는 커다란 문제점이 있습니다.

 

const CTextBlock cctb("Hello");// 상수객체 선언

char *pC = &cctb[0];               // operator[]를 호출하여 cctb의 내부 데이터에 대한 포인터

   // 를 얻습니다. 

*pC = 'J';                               // 값을 J로 변경할 수 있습니다.

 

이처럼 반환된 포인터나 참조자에 의해서 외부에서 값이 변경될 우려가 있는것 입니다.

물론 함수 앞에 const 키워드를 붙여주면, 외부에서 값이 변경될 우려는 사라지게 됩니다.

 

코드를 보자는게 아니라, 가장 중요한 부분은  pText[position]; 멤버를 건드렸다는 사실입니다. 이것은 즉

비트수준 상수성을 위배한것입니다.

 

그래서

일부 몇비트정도만 상수멤버에서 바꿀수 있게 하는 개념인 논리적 상수성 이라는 개념이 나온 것입니다.

 

2. 논리적 상수성

 - 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다.

 

class CTest {

private:

  char * pText;

  std::size_t num;   // mutable std::size_t num; 선언시 상수함수에서 접근 가능

public:

   std::size_t plus() const {

     num = 10;    //Error! 상수 멤버 함수다. 데이터를 변경할 수 없다.

  }

};

 

위의 코드는 논리적 상수성에서 말하는 개념을 그대로 반영한 것이다. 몇 비트만 바꾼것이다.

하지만 위의 코드는 컴파일 에러가 납니다. 당연합니다. 또한 비트수준상수성과는 거리가 멉니다.

 

컴파일 에러를 수정하기 위해선 mutable 이라는 키워드를 변수앞에 붙여 주면 해결됩니다. 

mutable std::size num; // OK!

 

즉!

컴파일러는 비트수준상수성을 지키고, 우리는 논리적 상수성으로 일부 비트만 수정할 수 있게 그때 마다 프로그래밍을 해야합니다. 완벽하게 비트수준상수성을 지킨다면 좋겠지만, 프로그램을 하다보면 그렇지 않은 경우가 많습니다. ( mutable을 난무하게되면, 아무의미 없음을 강조합니다 )

 

상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법

 

같은 수행을 하는 함수지만, const 상수여부에따라 오버로딩된 함수일경우 똑같은 코드가 중복되게 됩니다. 이중복을 피하기 위해서는 비상수버전 에서 상수버전 함수를 호출하도록 만들어야합니다.

 

ex ) 비상수 버전에서 상수버전 함수를 호출하는 예

 

class Test{

  const char& operator[] ( std::size_t position ) const
  { 

      ...

       ...

      return text[position];

  }

 

  char& operator[] ( std::size_t position )

  {

     /**

    *  const_cast는 const를 띠는 역할을 하고, static_cast는 const를 붙이는 역할을 한다.

    */ 

     return const_cast< char& >( static_cast< const Test& >(*this)[position] );

  }

}

 

위의 코드를 해석하자면,

먼저 this포인터를 const를 붙여서 []연산자를 이용해 상수함수를 호출하고, 상수함수의 리턴값을 받고 리터값에 달리 const를 띠어내면서 값을 참조 리턴하고 있다.

 

위의 코드 처럼 하게되면, 안정성도 유지하면서 코드중복도 피할 수 있습니다.

 

 

이것 만은 잊지 말자!

◆ const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 줍니다. const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을수 있으며, 멤버함수에도 붙을 수 있습니다.

◆ 컴파일 쪽에서 보며녀 비트수준 상수성을 지켜야 하지만, 여러분은 개념적인(논리적인)상수성을 사용해서 프로그래밍 해야 합니다.

이것 만은 잊지 말자!

◆ 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만드세요


  

 " 가급적 선행 처리자보다 컴파일러를 더 가까이 하자 "

 

 #define의 단점

 

1. 컴파일 에러가 발생하면 찾아내기가 쉽지 않다.

#define ASPECT_RATIO 1.653

 

위의 코드는 컴파일 되기 전에 선행처리자가 소스에서 ASPECT_RATIO라는 이름을 밀어버리고 1.653으로 전부 바꿔버리기 때문에 에러메시지에서 ASPECT_RATIO 라는 이름을 확인 할 수 없고, 특히 남이 쓴 코드일때 이값에 대한 출처를 파악하기 어렵다.

 

 

 

2. 코드의 크기가 커진다.

 매크로를 쓰면 코드에 ASPECT_RATIO가 등장하기만 하면 선행 처리자에 의해 1.653으로 모두 바뀌면서 사본의 갯수가 늘어난다.  

 

위의 1, 2번 해결방안은?

 

const double AspectRatio = 1.653;

위의 코드처럼 상수를 쓰는 것 입니다. 상수이기때문에 당연히 컴파일러에서 알아 차릴 수 있습니다.

또한 상수는 아무리 여러 번 쓰이더라도 사본은 딱 한 개만 생깁니다.

 

 

여기서 잠깐 #define을 const 변수로 바꿀때 주의할 점이 있습니다.

1. 상수포인터를 정의할때, 특히 문자열 상수일 경우 해당문자열의 변경이 없으므로

const char * const cClass = "학급"; // 포인터 앞뒤로 변경을 막아준다, ( 값, 주소값 모두 X )

2. 클래스 멤버로 상수를 정의 하는 경우

어떤 상수의 유효번위를 클래스로 한정하고자 할 때는 그 상수를 멤버로 만들어야 하는데, 그 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면, 정적 멤버로 만들어야한다.

class GamePlayer {

private:

 static const int NumTurns = 5;

 int scores[NumTurns];

};

 

위의 클래스 처럼 상수를 멤버로 가질때, 구식 컴파일러에선 작동을 하지 않을 때가 있다.

이때 나열자 둔값술(enum)을 많이 사용합니다.

class GamePlayer {

private:

 enum { NUM_TURN = 5 };

 int scores[NUM_TURN];

};

 

enum을 쓰는데도 이점이 있습니다.

 

혹시 enum대신 const 정수를 선언해서 쓰는데 다른사람이 이 상수변수의 주소를 얻는다든지 참조자를 쓰는것이 싫다면, 나열자 둔값술(enum)은 좋은 자물쇠가 됩니다.

enum은 #define처럼 어떤 형태의 쓸데 없는 메모리 할당도 절대 저지르지 않습니다.

 

 

 

3. 매크로 함수 사용시 문제를 야기 할 수 있다.

 

매크로 함수를 작성할때는 ()는 무조건 씌워 줍니다.

예를들어 #define MUL(a, b) a * b

MUL( 1+1, 2+2 )->1 + 1 * 2 + 2로 바뀌게 연산자 우선순위에 따라 우리가 의도했던 계산식이 아닐수 있다.

 

또한,

#define CALL_WITH_MAX(a, b) function( (a) > (b) ? (a) : (b) )// a와 b 중 큰것을 function에 넘겨 호출

 

int a = 5, b = 0;

CALL_WITH_MAX(++a, b);          // ++a > b ? ++a : b ( ++a가 두번 증가 된다, a > b 크므로 )

CALL_WITH_MAX(++a, b + 10);   // ++a > b+10 ? ++a : b+10 ( ++a가 한번 증가된다 )

 

위의 코드 처럼 우리는 ++a를 한번 하고 싶지만, 선행처리기에 의해 바뀌면서, 이상한 현상이 되므로 주의를 귀울여야 합니다.

 

물론 위의 해결방안도 제시하는데, 바로 inline(인라인)함수에 대한 템플릿을 준비하는 것입니다.

template<typename T>

inline void callWithMax( const T& a, const T& b)

{

  function( a > b ? a : b );

}

 

매크로 함수를 작성하는 이유?

함수호출의 오버헤드를 일으키지 않으려고 함에 있습니다.

 

매크로 함수보다 인라인함수의 이점은 ?

 

1. 함수 본문에 괄호로 분칠할 필요가 없고

2. 이자를 여러번 평가할지도 모른다는 걱정도 없어집니다.

3. 함수이기 때문에 유효범위 및 접근 규칙을 그대로 따라갑니다.

 

 

이것 만은 잊지 말자!

◆ 단순한 상수를 쓸때는, #define 보다 const 객체 혹은 enum을 우선 생각합시다.

◆ 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각합시다.



explicit

 

단일 인자를 갖는 생성자가 자동 형변환이 일어나는 것을 막을수 있습니다. 

이것은 일종의 권한이라고 말할 수 있습니다.
보통은 컬파일러가 허용되는 한도내에서 암묵적으로 형변환을 해주어컴파일을 하게 됩니다.

컴파일러는 똑똑한 녀석입니다.
프로그래머의 의도를 파악하려 한다기 보다는 어떻게든 소스를 컴파일 하여
실행하도록 만들려고 모든 수단을 동원하는 녀석이죠.

그렇다면 이녀석이 머하는 녀석인 줄은 알겠는데 왜 추가되었는지에 대해서도
알아야 하지 않을까요?

C언어는 위대합니다.
아주 오래전, 그러니까 객체지향 프로그래밍이 유행하기 이전에는 절차적 프로그래밍이
유행했었습니다.

이에대한 선두주자가 C언어였었죠.
C 언어의 강력함은 누구나 알고있을 겁니다.
이러한 강력함을 만들어 주는 특징 중 하나가 바로 제어권 입니다.

C언어는 제어력이 좋고 제한이 없기 때문에 프로그래머의 생각을 마음껏
표현할 수 있다는 점이 있지만 그만큼 오류를 범할 일도 많다는 겁니다.

그래서 C++로 넘어오면서 일종의 장치를 만들게 되었죠.
이의 목적은 프로그래머에게 컴파일러가 알려줄 수 있도록 시스템을 마련하는 것이었습니다.

그래서 이러한 권한은 컴파일러에게 많이 넘어가 버렸고 지금도 충분히 느끼시겠지만
컴파일러는 갈수록 똑똑해 졌습니다.

아무튼 이러한 노력의 증거품이 define를 inline화 하자 일 겁니다.
누구나 알듯이 define는 전처리기가, inline는 컴파일러가 처리해 줍니다.
즉, 에러가 발생한다면 inline는 컴파일러가 이를 집어 줄수 있다는 거죠.

프로그래머가 미처 신경쓰지 않는 부분까지 해결해 주기 때문에 무척 편해졌죠.
시키지 않은 일을 하여 버그를 만들어 내기 전까지는요.

그래서 프로그래머의 자율성을 존중하는 뜻에서 프로그래머가 반드시 구현해야 하는것이 아니라
필요에 의해 프로그래머가 직접 구현할 수 있도록 만들게 하는 장치가 만들어 졌죠.

그중 하나가 explicit입니다.
이것이 explicit의 탄생 배경입니다.

이녀석의 사용방법에 대한 한 예를 보자면..

#include <iostream.h>

#include <string.h>

class X {
private:
int age;
char name[80];

public:
explicit X(int); //생성자 앞의 explicit은 함축적 변환을 방지한다.
explicit X(const char*, int = 0);
// explicit 키워드는 파라미터가 1개인 생성자에만 사용될 수 있다는 사실에 주의하라.
// 만약 두 번째 파라미터 int가 디폴드 값을 가지지 않는다면, explicit은 문법 에러이다.
};

X::X(int a) {
age=a;
cout << "construct X(int) = " <<a<< endl;
}

X::X(const char* n,int a) {
strcpy(name,n);
age=a;
cout << "construct X(const char*,int)" << endl;
}

void main() {
X a = X(1); //construct X(int) =1 만약 X a = 1;이라고 쓴다면 컴파일 시간 에러가 발생한다.
//X a = 1;
X b = X("Jessie"); //construct X(const char*,int)
a = X(2); //construct X(int) =2

}

//이의 결과는 주석대로 이다.

X a = 1 이라면 어떻게 될까?
당연히 에러가 난다. 암시적 형변환이 exclipt에 의해 제한된 상태이기 때문이다

물론 exclipt를 제거를 한 후에 X a =1; 을 하면?
예러까 나지 않을 것이다.

컴파일러가 스스로 암시적 형변환을 하였기 때문이다.
exclipt가 없다면

X a = X(1);

a = 5;

이러한 문법이 가능해 진다.

C++의 암시적 형변환 규칙을 보면 클래스의 생성자 중에서 하나의 인자를 가지는
생성자는 프로그래머의 의사에 상관없이 컴파일러가 암시적 형변환을 하도록 되어있습니다.
그래서 a = 5 같은 코드를 만나면 알아서 X(int) 같은 애를 호출해서 임시 객체를 만들고
그 임시객체를 가지고 복사 할당 연산자를 호출하겠죠.

어떻습니까?

왠지 에러를 일으킬 소지가 보이지 않나요??.

 

 

복사생성자(copy constructor)와 복사 대입 연산자(copy assignment operator)

class Widget {

public:

 Widget();                                          // 기본생성자

 Widget(const Widget& rhs);                 // 복사생성자

 Widget& operator=(const Widget& rhs); // 복사 대입 연산자

};

 

Widget w1;  // 기본 생성자 호출

Widget w2;  // 기본 생성자 호출

w1 = w2;    // 복사 대입 연산자 호출

 

Widget w3 = w2; //여기서는 복사 생성자가 호출

 

복사생성자 호출과, 복사 대입연산자 호출을 구분하는건 어렵지 않습니다.

어떤객체가 새로 정의될 때(이를테면 위 문장의 w3처럼)는 생성자 호출이 먼저되므로 이때는 복사생성자가 호출 되는 것입니다.

 

또한 함수의 인자로 값에 의한 객체 전달을 하게 되는 경우 복사생성자가 호출 됩니다.


 

 

 


Effective C++에서 본 내용인데 우연히 인터넷속에서 또 보게 되었다..
책에서 볼 땐 별로 와 닿지 않았었지만,
이렇게 충분한(?) 예제를 보니깐, 다시한번 마음에 와 닿는다.

//1. 매크로
01: #include
02: #define MAX(a,b) (a>b? a:b)
03:
04: void main()
05: {
06: int x=20, y=80,r;
07: r= max(x++,y++);
08: cout << "x= " << x << "y= " << y << "r= "<< r << endl;
09: }

결과 x=21 y=82 r=81


//2. 인라인
01: #include
02: inline int max(int a, int b) { return(a>b? a:b); }
03:
04: void main()
05: {
06: int x=20, y=80, r;
07: r=max(x++,y++);
08: cout<< "x= "<< x << "y= " << y << "r= " << r;
09: }

결과 x=21 y=81 r=80

------------------------<<설명선>>------------------------

매크로함수의 홰궤한 결과가 보이는가?
위의 결과는 왜그럴까? 사실 매크로함수와 인라인함수의 컴파일처리과정만 알면 무지무지 간단한 이야기이다.

< 실행파일이 만들어지는 4 단계 >
1) 코딩(coding)
2) 전처리기(Preprocessor)에서의 전처리(preprocessing)
3) 컴파일러(Compiler)에서의 컴파일링(compiling)
4) 링커(Linker)에 의한 링킹(linking)

사실 Tool에서 코딩하고 그냥 컴파일만 시키면 2)~4)작업은 한꺼번에 이루어지며 실행파일이 생성된다. 그렇다고 2)랑 3)작업단계의 차이를 무시하면 위와같은 실행결과의 차이를 가져온다는 말이다.

매크로함수는 2)해서 전처리기에 의해 치환이되고
인라인함수는 3)에서 컴파일러에 의해 처리된다.

다시말하면 1.매크로의 08번줄은 컴파일에 들어가기전에
r= max(x++,y++);에서 r = x++ > y++ ? x++ : y++;로 바뀌게 된다.
즉, x와 y값의 크기를 비교한 후에 두 변수의 값을 각각 1씩 증가시키고 증가된 x와 y값중에서 큰 값만을 r에 저장한 후에 저장된값만을 1증가 시킨다.

이에 반해 2. 인라인의 경우 일반 함수와 동일하다
call by value를 쓰고 있고, 호출이 이루어진 다음에 변수 x와 y의 값을 1씩 증가시키므로 증가시킨 값이 함수 max()를 호출한 매개변수 값에는 영향을 미치지않았다.
따라서 x와 y의 값은 1씩 증가하여도 r에 저장되는 값에는 변화가 없었던 것이다.-

- 교훈 -
1. 매크로 함수를 사용할 경우에는 보다 신중하자.
매크로문은 선행처리기가 처리하므로 컴파일러가 오류를 찾아내지 못한다. 즉, 잠깐 딴 생각하면서 코딩을 하면 의도했던 바와 다른 결과가 나올수 있다.

2. 인라인화 하고 안하고는 컴파일러 마음이다.
인라인 함수도 컴파일러에 의해 코드의 치환이 일어난다.
일 반함수처럼 원형을 보고 검사하고 호출하는 단계가 없어져 빠르고, 컴파일러가 수행하므로 잘못된 것이 있다면 컴파일타임에 즉시 알수도 있다. but "inline"이라고 지시자를 줘도 인라인화를 시키는 것은 컴파일러맘이다. inline선언을 했다고 다~ inline이 되는 것은 아니다.

3. 실행파일크기를 고려하자
인라인함수이나 매크로함수나 코드의 치환이 일어나기 때문에 loop내에서 호출한다거나 하면 소스의 크기가 무진장 길어진다. 당연히 실행 파일의 덩치가 엄청커진다. 루프에서 쓰지말자

4. 인라인함수는 헤더파일에서...
인라인 함수는 내부 연결성을 가지기 때문에 헤더 파일에 넣어야 어러 소스 파일에서 불러 쓸 수 있다. 따라서 인라인 함수의 정의가 바뀌면 그 인라인 함수와 의존 관계에 있는 사용자 코드는 모두 컴파일을 다시 해야 한다



'0x0001 > C, C++' 카테고리의 다른 글

[Command] Batch Shell Command  (0) 2019.02.23
[C언어] 주석 TIP  (0) 2019.02.09
[C언어] n 에서 m 까지의 합을 재귀로 작정  (0) 2019.02.09
[C++] Vector와 List의 차이점  (0) 2019.02.08
[C++] 함수객체 ( Functor )  (0) 2019.02.08

//*/        (앞에서 / 하나 빼보시오.)
소스코드
소스코드
소스코드
/*/          (가운데 별 표시를 빼보시오.)
소스코드
소스코드
소스코드
//*/ 


#include <stdio.h>


int sumloop(int,int,int); // 재귀함수의 명시적 선언
int sumloop(int min, int max, int sum) { // 재귀함수 작성

  while (min <= max) // 재귀 종료 조건
  {
     sum += min++;
     sumloop(min,max,sum); // 재귀호출
  }
  return sum; // 마지막 합계 반환
}

void main() { 메인함수
  printf("%d\n",sumloop(1,10,0)); // 최소값, 최대값, 합계 파라미터 전달후 바로 결과를 출력하는 구조
}



'0x0001 > C, C++' 카테고리의 다른 글

[C, C++] 매크로함수 VS 인라인함수  (0) 2019.02.09
[C언어] 주석 TIP  (0) 2019.02.09
[C++] Vector와 List의 차이점  (0) 2019.02.08
[C++] 함수객체 ( Functor )  (0) 2019.02.08
[C++] 콜백함수( CallBack Function )  (0) 2019.02.08

+ Recent posts