'3. Implementation/C / C++'에 해당되는 글 23건

  1. 2012.02.17 상속 관계가 있는 Base 클래스의 소멸자는 반드시 virtual 선언하기
  2. 2012.01.14 인스턴스 멤버함수를 인자로 받고 호출하는 방법
  3. 2009.07.25 반환값 최적화 (return value optimization)
  4. 2009.07.25 타입변환 함수에 대한 주의를 놓지 말자!
  5. 2009.07.16 참조자와 포인터의 사용 구분
  6. 2009.07.15 싱글톤 구현시 함수 정적객체 이용의 이점
  7. 2009.07.13 C++ 와 C를 함께 사용하는 방법
  8. 2009.07.13 순수 가상 함수의 의미와 순수 가상 소멸자
  9. 2009.07.13 Exception Specification - 예외 지정(명시)
  10. 2009.07.10 리소스 누수를 피하는 방법 : auto_ptr<>
  11. 2009.07.10 delete 널 포인터
  12. 2009.07.09 C++ 스타일의 형변환 (Type cast)
  13. 2009.07.09 volatile ?
  14. 2009.07.09 골치아픈 C++ const 용법 정리
  15. 2009.06.30 비트 연산자 (Bitwise Operators)
  16. 2009.03.25 생성자에서 멤버 초기화 시 파라미터가 있는 생성자 호출하기
  17. 2009.01.22 발생한 예외는 참조자로 받아내자
  18. 2008.08.26 동적으로 할당되는 메모리를 갖는 클래스를 위해서는 복사 생성자와 치환 연산자를 선언하라
  19. 2008.08.24 new와 delete의 사용시 동일한 형식을 이용한다.
  20. 2008.08.24 <stdio.h> 보다는 <iostream>을 사용한다
2012. 2. 17. 06:51

상속 관계가 있는 Base 클래스의 소멸자는 반드시 virtual 선언하기

요즘 C++ 은 잘 사용하지 않고, C# 과 Java 만 하다보니 C++ 의 감각이 떨어지고 있습니다. 상속관계에 있는 Base 클래스의 소멸자에 virtual 을 선언하지 않아서 생기는 메모리 릭을 한참에서야 찾았답니다.

virtual ~ Base() { }


2012. 1. 14. 05:35

인스턴스 멤버함수를 인자로 받고 호출하는 방법

C 에서는 함수 포인터를 이용하여 전역함수를 저장하고 필요시에 호출할 수 있습니다. C++ 에서도 마찬가지로 함수 포인터를 이용하지만 지금 소개할 내용은 흔히 사용되는 static 또는 전역 함수가 아닌 인스턴스 멤버 함수를 호출하는 방법에 대한 좋은 예제입니다.
(출처는 "GOF : Design patterns - Elements of resuable Object-Oriented Software" 이며 Command 패턴 구현시 Subclassing 대신 Template 을 이용하는 방법에 대해서 소개하면서 나오는 내용입니다.)

template <class Receiver> 
class SimpleCommand : public Command { 
public: 
	typedef void (Receiver::* Action)(); 
	
	SimpleCommand(Receiver* r, Action a) : 
	_receiver(r), _action(a) { } 
	
	virtual void Execute(); 

private: 
	Action _action; 
	Receiver* _receiver; 
};

virtual void Execute() 를 구현한 코드는 다음과 같습니다

template <class Receiver> 
void SimpleCommand&ltReceiver>::Execute () 
{ 
	(_receiver->*_action)(); 
}
 
이 템플릿을 사용하는 클라이언트 코드는 다음과 같습니다.

 MyClass* receiver = new MyClass; 

// ... 
Command* aCommand = 
new SimpleCommand<MyClass>(receiver, &MyClass::Action); 

// ... 
aCommand->Execute(); 


 
2009. 7. 25. 12:30

반환값 최적화 (return value optimization)

값 반환시 생성자를 사용하는 코드와 그렇지 않은 코드의 차이점을 살펴보자.

class Int {

public:

        Int(int num=0) : _num(num) {

               cout << "constructed" << endl;

        }

        Int(const Int & copy)

        {

               cout << "constructed" << endl;

               _num = copy._num;

        }

 

// friend

        friend const Int operator*(const Int & lhs, const Int & rhs);

private:

        int _num;

};


Int 를 테스트하는 코드는 다음과 같다.

Int a = 10;

Int b = 2;

 

Int c = a * b;


operator* 를 구현함에 있어 지역 객체를 먼저 선언하여 반환하는 경우와 생성자를 통하여 바로 반환하는 경우를 살펴보자.

+. 지역 객체를 생성하여 반환하는 경우

const Int operator*(const Int & lhs, const Int & rhs)

{

        Int temp;

        temp._num = lhs._num * rhs._num;

 

        return temp;

}



이 때 테스트 코드를 수행하면 Int 객체가 4번 생성된다. 실행결과를 캡처한 화면이다.


+. 생성자를 이용하여 바로 반환하는 경우

const Int operator*(const Int & lhs, const Int & rhs)

{

        return Int(lhs._num * rhs._num);

}



이 때는 반환값 최적화가 수행되어 임시객체가 생성되지 않는다.

아래는 캡처한 화면.


효율성이 중시되는 경우 이런 임시 객체 생성은 무시할수 없을 것이다.

참고 : More Effective C++
2009. 7. 25. 10:48

타입변환 함수에 대한 주의를 놓지 말자!

+. 컴파일러가 사용하는 타입변환 함수

  • 단일 인자 생성자 (single-argument constructor)
  • 암시적 타입변환 연산자 (implicit type conversion operator)
위 두가지 종류를 이용하여 컴파일러는 암시적 타입변환을 수행한다. 이제 암시적 타입변환의 위험성에 대하여 살펴보자.

+. 타입변환 연산자의 함정


class Rational {

public:

        ...

        operator double() const;      // Rational double 로암시적으로변경

};


와 같이 double 에 대한 타입 변환 연산자가 정의 되어 있다. 다음을 살펴보자.

Rational r(1, 2);

 

cout << r;             // "1/2"를출력해야한다.


위 코드는 "1/2"를 출력하거나, 만약 operator << 가 정의되어 있지 않다면 컴파일 에러가 나야 할 것이다. 하지만 Rational 객체를 받는 operator<< 를 호출해야 하는 시점에서 이 함수를 찾지 못하지만, 어떻게든 함수 호출을 성공시키기 위해 암시적인 타입변환 함수를 찾아 맞추어 본다. 이 경우에는 Rational::operator double() 을 찾아내어 암시적인 타입변환을 수행하고, 결국 operator << 함수 호출은 double 타입이 출력되는 의도하지 않은 상황이 발생한다.

이러한 문제를 해결하기 위해서는 이 연산자를 아래와 같이 멤버 함수로 정의하는 것이다.

class Rational {

public:

        ...

        double ToDouble() const;      // Double 타입을반환함.

};


그리곤 이 멤버 함수를 직적 호출한다.

Rational r(1, 2);

 

cout << r;                                   // 에러!

 

cout << r.ToDouble();         // 반환된double 값을출력한다.


이때문에, 보통 C++ 프로그래머들은 연륜이 쌓일수록 타입변환 연산자를 꺼린다. C++ 표준 라이브러리를 제정하고 있는 위원회 임원들을 예로 들어도 그렇다. 업계 최고의 전문가인 그 사람들이 만든 string 클래스에는 string 타입을 C 스타일의 char* 로 바꿔주는 암시적 변환 연산자가 없다. 대신에 직접 호출할 수 있는 c_str 을 두고 이것으로 타입을 변환하도록 한다. 이것이 우연일까?

+. 단일 인자 생성자를 통한 암시적 타입변환

단일 인자 생성자때문에 일어나는 문제는 암시적 타입변환 연산자 때문에 발생하는 문제보다 많은 경우에 더 고약하다.

template <class T>

class Array{

public:

        Array(int lowBound, int highBound);

        Array(int size);       // 단일인자생성자

 

        T & operator[] (int index);

 

        ...

};


에 대하여 다음의 경우를 살펴보자.

bool operator==(const Array<int> & lhs, const Array<int>& rhs);

Array<int> a(10);

Array<int> b(10);

 

...

 

for(int i=0; i<10; i++)

{

        if(a == b[i]) {               // a a[i] 가되어야하지만오타

               ...

        }

        else {

               ...

        }

}


어쩌다가 실수로 a 옆에 배열 인덱스 연산자를 붙이지 않았다. 에러가 나야 하지만 int 하나만 받아들이는 단일 인자 생성자 때문에 암시적 타입변환이 수행되어 다음과 비슷한 코드가 생긴다.

for(int i=0; i<10; i++)

{

        if( a == static_cast<Array<int> >(b[i]) ) {

               ...

        }

}


이것이 의도한 동작일 가능성은 없을 것이다. 게다가 이 코드는 짜증날 정도로 비효율적이다. 루프가 끝날때까지 매번 임시 Array<int> 객체를 만들었다가 없애기 때문이다.

이러한 단일인자 생성자때문에 일어나는 암시적 타입변환을 막는 방법에는 두가지가 있다.

하나는 explicit 키워드를 사용하는 쉬운 방법이고, 두 번째 방법은 explicit 를 지원하지 않는 컴파일러의 경우에 proxy class 를 사용하는 방법등이 있다.

+. explicit 키워드 사용

template <class T>

class Array{

public:

        ...

        explicit Array(int size);     // 매개변수와호출이명확할때에만호출하라.

                                      // 암시적타입변환에사용되지않는다.

        ...

};

 

explicit 키워드는 암시적 타입변환의 문제를 막기 위해 만들어진 것이다. 매개변수와 호출이 명확할 때에만 이 생성자를 호출하라고 알려주는 뜻이므로, explicit 로 선언된 생성자는 암시적 타입변환에 사용되지 않는다.

+. Proxy Class 사용

template <class T>

class Array{

public:

        class ArraySize {      // 새로만든Proxy Class

        public:

               ArraySize(int numElements) : theSize(numElements) { }

               int size() const { return theSize; }

        private:

               int theSize;

        };

 

        Array(int lowBound, int highBound);

        Array(ArraySize size);        // 선언형식이바뀌었다.

        ...

};


ArraySize 를 Array의 내부 클래스로 만든이유는 이 클래스가 Array 하고만 사용된다는 사실을 강조하기 위해서이다.

아래 문에서 어떤 일이 일어날지 생각해 보자.

Array<int> a(10);      // 인자는정수이다.


컴파일러는 int 를 받아들이는 Array<int> 클래스의 생성자를 호출하려 하지만 이런 생성자는 없다. 궁지에 몰린 컴파일러는 미친 듯이 클래스 선언부를 찾다가 '옳거니' 하면서 int 인자를 임시 ArraySize 객체로 바꿀 수 있다는 사실을 발견한다. ArraySize 객체는 Array<int> 생성자가 원하던 바로 그것이기 때문에, 컴파일러는 부담 없이 타입변환을 수행한다. 결국 생성자 호출은 성공이다.

다음과 비슷한 코드로 호출될 것이다.

Array<int> a(static_cast<ArraySize>(10));


앞에 보았던 코드를 다시 보도록 하자.

bool operator==(const Array<int> & lhs, const Array<int>& rhs);

Array<int> a(10);

Array<int> b(10);

 

...

 

for(int i=0; i<10; i++)

{

        if(a == b[i]) {               // a a[i] 가되어야하지만오타

               ...

컴파일러는 Array<int> 객체에 대해 호출할 operator == 연산자의 우변으로 Array<int> 타입이 왔으면 했지만, int 객체를 받는 생성자가 이제는 없다. 게다가, 컴파일러는 int 를 임시 ArraySize 객체로 바꾼 다음에 그 객체로부터 다시 Array<int> 객체를 생성하는 고단수의 처리는 생각할 줄 모른다.

ArraySize 처럼 쓰이는 클래스를 가리켜 프록시 클래스(proxy class)라고 하는데, 다른 객체를 대신한다고 해서 붙은 이름이다.

 여기까지의 내용을 요약하면, 결국 의도하지 않은 상황을 초래할 수 있는 암시적 타입변환을 사전에 예방하기 위해 다음과 같이 구현해야 한다는 내용이다.

1. 되도록이면 타입변환 연산자 대신 특정 타입을 반환하는 멤버 함수를 정의한다.
2. 단일 인자 생성자에는 explicit 를 선언하여 암시적 타입변환을 막는다. (구 컴파일러의 경우 proxy class 사용)

이와 같이 정리하면 이해가 빠를 것이다.


참고 : More Effective C++
2009. 7. 16. 06:37

참조자와 포인터의 사용 구분

다음과 같은 경우 참조자를 사용한다.

  • 참조하고자 하는 객체가 항상 유효한 객체일 때,
  • 다른 객체를 참조할 일이 없을 때,
  • 포인터를 사용하면 문법상 의미가 어색할 때,

이 세 가지의 경우를 제외하고는 무조건 포인터이다.

자세히 살펴보자.

1.    참조하고자 하는 객체가 항상 유효한 객체일 때

참조자에서는 “널 참조자(null reference)”가 없다. 그래서 널 테스트를 할 필요가 없다.

포인터라면 아래처럼 null test 를 해야 하지만,

void printDouble(const double * pd)

{

        if(pd) {       // Null test

               cout << *pd;

        }

}

참조자는 아래와 같이 구현한다.

void printDouble(const double & rd)

{

        cout << rd;

}



2.    다른 객체를 참조할 일이 없을 때

포인터는 다른 객체의 주소값으로 얼마든지 바꿀 수 있지만, 참조자는 초기화될 때의 객체만 참조한다.

string s1("Nancy");

string s2("Clancy");

 

string & rs = s1;

// &rs = s2; // 에러

 

string * ps = &s1;

ps = &s2;

3.    포인터를 사용하면 문법상 의미가 어색할 때,

가장 흔한 예로 operator[] 를 구현할 때이다. 만약 operator[] 가 포인터를 반환한다고 하면 아래처럼 조금 어색한 형태가 쓰일 것이다.

vector<int> v(10);

 

// v는 포인터의 벡터가 아님에도 불구하고 포인터의 벡터인 것 처럼 보이게 한다.

*v[5] = 10;

고로, operator[] 는 참조자를 반환하도록 구현한다.

vector<int> v(10);

 

v[5] = 10;

참고 : More Effective C++
2009. 7. 15. 23:31

싱글톤 구현시 함수 정적객체 이용의 이점

싱글톤이란 클래스의 유일한 인스턴스만 생성하게 하는 "의도"를 가진 디자인 패턴이다. 보통은 그냥  아래와 같이 클래스의 정적 객체로 구현하여 사용하였다.

(생성자를 private 으로 하여 외부에서 객체 생성을 막는것에 주의)

class Printer{

private:

        Printer(){

               cout << "created printer \n";

        }

        Printer(const Printer & printer);

        static Printer printer;

public:

        static Printer & thePrinter() {

               return printer;

        }

};


Printer Printer::printer;


이렇게 구현하여 사용함에 있어 별 문제를 못느꼈으나, 다음과 같은 상황일때 고민을 했던적이 있다.

static 정적 객체가 여러개이고, 각 정적 객체가 연관성을 가지게 되어 객체의 초기화 순서가 중요하게 될 경우 골치가 아프게 된다. 예로 A, B 객체가 모두 클래스 정적 객체인데 B는 A이후에 생성되어야 한다면 이때는 클래스 정적 객체로는 해결하기 어렵다.

다시 Printer 클래스를 아래처럼 구현해 보자.

class Printer{

private:

        Printer(){

               cout << "created printer \n";

        }

        Printer(const Printer & printer);

 

        friend Printer & thePrinter();

};

 

Printer & thePrinter()

{

        static Printer printer;

        return printer;

}



friend 함수를 이용하여 클래스 정적 객체가 아닌 함수 정적 객체를 이용하였다. 이렇게 구현하면 thePrinter() 라는 함수가 호출되기 전까지는 함수 정적 객체는 생성되지 않는다. 따라서,

1. 함수가 호출되지 않으면 객체가 생성되지 않는다.
2. 함수 호출 순서를 조정함으로써 객체의 초기화 순서를 조정할 수 있다.

는 장점을 얻을 수 있다.

주의할 점은 thePrinter 를 inline 으로 선언하면 안된다는 것이다. inline 으로 선언되면 한 프로그램안에서 중복될 수 있다. 따라서 중복된 함수 코드마다 정적 객체가 들어가게 되므로 정적 객체를 선언한 비멤버 함수는 절대로 inline 으로 선언하면 안 된다.

참고 : More Effective C++

2009. 7. 13. 21:59

C++ 와 C를 함께 사용하는 방법

같은 프로그램 안에서 C++ 와 C를 섞어서 사용하려면, 다음의 사항을 꼭 기억하라.

  • 각자가 사용하는 C++ 와 C 컴파일러가 생성하는 목적 코드가 서로 호환됮는지 확인한다.
  • 두 개의 언어에서 동시에 사용되는 함수는 extern "C"로 선언한다.
  • 가능하면 main 은 C++ 로 작성한다.
  • new 로 할당한 메모리는 delete로 해제하고, malloc 으로 할당한 메모리는 free 로 해제한다.
  • 두 개의 언어로 작성한 함수 사이에 전달할 수 있는 데이터 타입은 C 컴파일러로 컴파일되는 것으로만 한정한다. C와 호환이 가능한 C++ 구조체는 비가상 멤버 함수까지만 가질 수 있다.

참고 : More Effective C++


2009. 7. 13. 21:29

순수 가상 함수의 의미와 순수 가상 소멸자

순수 가상 함수?

virtual void func() = 0 과 같이 선언하는 순수 가상 함수의 의미는 '구현을 하지 않는다'는 뜻이 아니라, 진짜 뜻은 다음과 같다.

  • 이 클래스는 추상클래스이고,
  • 이 클래스를 상속한 모든 구체 클래스는 이 함수를 구현해야 한다.

보통 순수 가상 함수는 구현하지 않는 것이 보통이지만, 순수 가상 소멸자는 예외이다.

순수 가상 소멸자?

순수 가상 함수로 만들만한 멤버 함수가 하나도 없는 아주 드문 경우가 있다. 이러한 경우에는 보통 소멸자를 순수 가상함수로 선언해서 해결한다.

주의할 점은 파생 클래스의 소멸자가 호출될 때 결국 이 순수 가상 소멸자도 호출되기 때문에 반드시 구현해야 한다.

class AbstractAnimal {

protected:

        AbstractAnimal & operator=(const AbstractAnimal & rhs);

 

public:

        virtual ~AbstractAnimal() = 0 {

 

        }

};

 

class Animal : public AbstractAnimal {

public:

        Animal & operator=(const Animal & rhs);

};

 

class Lizard : public AbstractAnimal {

public:

        Lizard & operator(const Lizard & rhs);

};

 

class Chicken : public AbstractAnimal {

public:

        Chicken & operator(const Chicken & rhs);

};


참고 : More Effective C++
2009. 7. 13. 10:14

Exception Specification - 예외 지정(명시)

예외 지정은 함수에서 어떤 예외가 던져질 수 있는가에 대한 요약 정보를 제공하기 위해서 사용된다(MSDN).

// MyFunction1 은예외를던질수있음

void MyFunction1(void) throw(...) ;

 

// MyFunction2 int 형예외를던질수잇음.

// Visual C++ throw(type) throw(...)로처리한다.

void MyFunction2(void) throw(int);   

 

// MyFunction3 는예외를던지지않음. __declspec(nothrow) 와동일

void MyFunction3(void) throw();

void __declspec(nothrow) MyFunction4(void);



참고 : MSDN


2009. 7. 10. 04:11

리소스 누수를 피하는 방법 : auto_ptr<>

아래 예를 살펴보자.

void processAdoption(istream & dataSource)

{

        while(dataSource) {

               ALA * pa = readALA(dataSource);

               pa->processAdoption();

               delete pa;

        }

}


만약 pa->processAdaoption(); 에서 예외가 발생한다면 어떤 일이 일어날까?

pa 가 삭제되지 않아 메모리 누수가 생길 것이다. 메모리 릭을 방지하기 위해 다음과 같이 작성할 수 있다.

void processAdoption(istream & dataSource)

{

        while(dataSource) {

               ALA * pa = readALA(dataSource);

 

               try {

                       pa->processAdoption();

               } catch(...) {

                       delete pa;

                       throw;   // 예외를다시caller 에게전파

               }

 

               delete pa;

        }

}


위 코드는 try - catch 블록으로 코드가 복잡하고, delete 문을 두번 작성해야 한다.

위 코드는 아래처럼 auto_ptr 을 이용하면 코드를 깔금하게 만들 수 있다.

void processAdoption(istream & dataSource)

{

        while(dataSource) {

               auto_ptr<ALA> pa = readALA(dataSource);

               pa->processAdoption();

        }

}


위에서 pa 는 지역객체이므로 pa->processAdoption() 에서 예외가 발생하더라도 함수 종료시 auto_ptr 의 소멸자가 호출되어 pa 에 할당된 메모리가 삭제된다.

이처럼 지역 리소스를 조작할 때 쓰이는 포인터는 auto_ptr 을 사용하자.

만약 여기까지 읽다보면 이런 의문이 들 수 있다. 혹, 생성자 또는 소멸자에서 예외가 발생하면 어떻게 될까?
생성자와 소멸자 작성시 반드시 생성자에서는 리소스 누수가 일어나지 않게 하고, 소멸자에서는 예외가 탈출 못하도록 코드를 작성해야 한다.

참고 : More Effective C++
2009. 7. 10. 03:14

delete 널 포인터

습관적으로 소멸자에서 NULL 체크후에 delete 하곤 했다.

~SimpleString() {

        if(pchTemp != NULL) delete pchTemp;

}


delete 널 포인터가 구문상 가능한 것이기에 아래 예처럼 깔끔하게 작성할 수 있다.

class SimpleString {

private:

        char * m_pchTemp;

public:

        SimpleString() : m_pchTemp(NULL) {

 

        }

        ~SimpleString() {

               // 습관적으로...

               // if(pchTemp != NULL) delete pchTemp;

 

               delete m_pchTemp;

        }

};


위에서 pchTemp 는 항상 NULL로 초기화 되기때문에 delete m_pchTemp 는  reasonable 하다.

참고 : More Effective C++
2009. 7. 9. 21:14

C++ 스타일의 형변환 (Type cast)


C++ 스타일의 캐스트 연산자에는 4가지가 있다.

 static_cast C 스타일 캐스트와 똑같은 형변환 능력. 
 const_cast  표현식의 상수성이나 휘발성(volatileness)를 없애는 데 사용.
 dynamic_cast  상속 계층 관게를 가로지르거나 하향시킨 클래스 타입으로 안전하게 캐스팅할 때 사용. 기본 클래스의 객체에 대한 포인터나 참조자의 타입을 파생 클래스, 혹은 형제 클래스(다중 상속시) 의 타입으로 변환.

캐스팅 실패시 포인터의 경우 널 포인터, 참조자 캐스팅시는 예외를 던진다.

가상 함수가 없는 타입에는 적용할 수 없다. 
                                                                                                                    
 reinterpret_cast 강제 형변환. 이 연산자가 적용된 후의 변환 결과는 거의 항상 컴파일러에 따라 다르게 정의되어 있다. 따라서, 이 연산자가 쓰인 소스는 직접 이식이 불가능하다.

사용 예는 함수 포인터 타입을 서로 바꾸는 경우이나, 가급적 사용 자제.

참고한 책에는 형변환이 필요한 경우 C++ 스타일의 형변환 연산자를 사용할 것을 권한다.

참고 : More Effective C++
2009. 7. 9. 16:21

volatile ?

하드웨어 관련 프로그래밍이나 멀티쓰레드 프로그래밍을 하다 보면 volatile 을 접하는 경우가 있다. volatile 에 대해서 약간의 잡담을 하려 한다.

먼저 int 형 변수 하나를 선언한 후, 반복해서 임의의 숫자를 입력하는 코드를 작성해 보자. 이렇게 작성한 후 릴리즈로 빌드 후 printf 에다가 중단점을 설정한 후 디스어셈블리어를 본다.

 원본 소스 릴리즈 빌드 후 디스어셈블리 코드 

int _tmain(int argc, _TCHAR* argv[])

{

        int i = 0;

        i = 1;

        i = 2;

        i = 4;

        i = 6;

        i = 7;

 

        printf("%d", i);

 

        return 0;

}

 

int _tmain(int argc, _TCHAR* argv[])

{

         int i = 0;

         i = 1;

         i = 2;

         i = 4;

         i = 6;

         i = 7;

 

         printf("%d", i);

00401000  push        7   

00401002  push        offset string "%d" (4020F4h)

00401007  call        dword ptr [__imp__printf (4020A0h)]

0040100D  add         esp,8

 

         return 0;

00401010  xor         eax,eax

}

00401012  ret


위에 릴리즈 빌드로 인해 최적화된 코드를 보면 i 에다가 여러 값을 대입하는 코드는 모두 제거되고, 단지 숫자 7을 printf 함수에다가 넘긴다. 어셈블리 코드를 보면 i 를 선언하지도 않는다.

종료 시키고, int 앞에 volatile 을 선언 한후 다시 중단점을 설정하고, 디스어셈블리어를 보자.

 원본 소스  릴리즈 빌드 후 디스어셈블리 코드
 

int _tmain(int argc, _TCHAR* argv[])

{

        volatile int i = 0;

        i = 1;

        i = 2;

        i = 4;

        i = 6;

        i = 7;

 

        printf("%d", i);

 

        return 0;

}

 

int _tmain(int argc, _TCHAR* argv[])

{

00401000  push        ecx 

         volatile int i = 0;

00401001  mov         dword ptr [esp],0

         i = 1;

00401008  mov         dword ptr [esp],1

         i = 2;

0040100F  mov         dword ptr [esp],2

         i = 4;

00401016  mov         dword ptr [esp],4

         i = 6;

0040101D  mov         dword ptr [esp],6

         i = 7;

00401024  mov         dword ptr [esp],7

 

         printf("%d", i);

0040102B  mov         eax,dword ptr [esp]

0040102E  push        eax 

0040102F  push        offset string "%d" (4020F4h)

00401034  call        dword ptr [__imp__printf (4020A0h)]

 

         return 0;

0040103A  xor         eax,eax

}


volatile 선언자 하나 추가햇을 뿐인데, 어셈블리 코드는 완전히 달라졌다. 최적화는 전혀 적용되지 않고 입력한 할당문을 전부 수행하도록 기계어를 생성하였다.

이와 같이, 컴파일러 최적화로 인한 의도하지 않는 상황을 사전에 예방하고, 항상 변수의 주소에서 값을 읽어오고 할당하도록 한다.

실전 사용 예로는, 시리얼 버프에 데이터를 쓰거나 읽을 때 시리얼 버프를 가리키는 주소를 volatile 변수로 선언한 후 원하는 데이터를 시리얼에 전송한다. 이 때 만약 volatile 이 선언되지 않는다면, 수 바이트를 시리얼에 전송하고자 할 때 간혹 최적화로 인해 원하는 데이터가 쓰여지지 않거나 읽혀지지 않을수가 있을 것이다.
2009. 7. 9. 14:26

골치아픈 C++ const 용법 정리

잘 잊어먹는 C++ 용법을 다시 한번 정리하였다.

int temp1 = 100;

int temp2 = 200;

 

//////////////////////////////////////////////////////////////////////////

// 1. 변수

//////////////////////////////////////////////////////////////////////////

const int i=100;

 

// i = 200;

// &i l-value 가아니므로대입자체가무효.

 

//////////////////////////////////////////////////////////////////////////

// 2. 포인터형

//////////////////////////////////////////////////////////////////////////

// * 를기준으로왼쪽이냐오른쪽이냐에따라서다음과같다.

 

// * 의왼쪽

const int * piConst = &temp1;

// int const * piConst = &temp1;      <- *의왼쪽으로위문장과동일.

 

// *piConst 상수이다.

// *piConst = 200;     // error

// 하지만ipConst 는상수가아니므로주소는변경이가능하다.

piConst = &temp2;

 

// * 의오른쪽

int * const  piConst2 = &temp1;

// piConst2 가상수이다.

// piConst2 = &temp2;  // error

// 하지만*piConst2 는상수가아니므로값은변경이가능하다.

*piConst2 = 200;

 

// 복합적

int const * const piConst3 = &temp1;

//const int * const const 와동일

 

// 완전상수다. ^^

// piConst3 = &temp2; // error

// *piConst3 = 200;    // error              

 

//////////////////////////////////////////////////////////////////////////

// 3. 참조형

//////////////////////////////////////////////////////////////////////////

int const & refVal = temp1;

 

// refVal = 200; error;

// 역시&refVal l-value 가아니므로대입자체가무효

 

int & const refVal2 = temp1;

// --> 이렇게사용하는것은VC 에서테스트해본결과아무효과가없어보인다. why?

 

//////////////////////////////////////////////////////////////////////////

// 4. 함수

//////////////////////////////////////////////////////////////////////////

// class 의멤버함수에만const 선언가능.

class A {

private:

        int m_i;

        mutable int m_j; // const 로선언된멤버함수에서도변경이가능함. C++ 표준임.

public:

        A() {

               m_i = 3;

               m_j = 5;

        }

        int GetValue() const {

               // m_i = 2;   // error. const 로선언된멤버함수는멤버변수변경불가.

               m_j = 4;

               return m_i;

        }

} a;

 

int k = a.GetValue();

 

//////////////////////////////////////////////////////////////////////////

// 5. 클래스

//////////////////////////////////////////////////////////////////////////

const class B {

private:

        int m_i;

public:

        int GetValue()

        {

               return m_i;

        }

};

 

B b;

const B & constB = b;

// constB.GetValue();  // error. GetValue const 가아니면사용할수없다.

 

const A & constA = a;

constA.GetValue();     // GetValue() const 이므로사용가능하다.

 

//////////////////////////////////////////////////////////////////////////

// 6. const만차이가나는멤버함수들이오버로딩될수있다는사실

//////////////////////////////////////////////////////////////////////////

class C {

        int Add(int i, int j) {

               return (i+j);

        }

        int Add(int i, int j) const {

               return (i+j);

        }

        /* 아래는성립하지않음.

        int Add(const int i, const int j) {

               return (i+j);

        }

        */

};



책 : Effective C++
참고 사이트 : http://elky.tistory.com/99
2009. 6. 30. 11:45

비트 연산자 (Bitwise Operators)

잘 사용하지 않아서 잘 기억나지 않는 ~ 와 ^ ...

 & ( Bitwise AND )
    0110 & 0101 => 0100

| ( Bitwise OR)
    0110 & 0101 => 0111

~ ( Bitwise Not )
    ~(1010) => 0101

^ (Bitwise Exclusive OR)
    0110 ^ 0101 => 0011



2009. 3. 25. 08:14

생성자에서 멤버 초기화 시 파라미터가 있는 생성자 호출하기

C++ 한지가 언젠데... 이제서야  생성자 초기화시 파라미터가 생성자를 호출할 수 있다는 것을 알게 되었다. 원래 가능한건지 확장된건지는 모르지만...

아직도 2% 부족하군... 쩝.

 class A

{

public:

        A(int i) {

               m_i = i;

        }

        void Print() {

               printf("Class A : %d \n", m_i);

        }

private:

        int m_i;

};

 

class Client

{

public:

        Client(int i) : m_pA(new A(i)) {

 

        }

        ~Client() {

               delete m_pA;

        }

        void Print() {

               m_pA->Print();

        }

private:

        A * m_pA;

};

 

int _tmain(int argc, _TCHAR* argv[])

{

        Client c(5);

 

        c.Print();

 

        return 0;

}



2009. 1. 22. 13:49

발생한 예외는 참조자로 받아내자

예외 객체가 전달되는 방식을 설정하는 방법은 3가지가 있다

 

1.      포인터

2.     

3.      참조자

 

비록 이렇게 3가지라고 하나 예외는 무조건 참조자로 받아야만 한다. 그 이유를 살펴보자.

 

1.      포인터

void someFunction()

{

exception ex;

throw &ex;           // ex 객체는 유효범위를 벗어나면 소멸됨

}

 

void doSomething()

{

try {

someFunction();

}

Catch(exception * ex)         // 소멸된 예외 객체를 가리킴

}

}

 

만약 new로 할당하여 예외 객체를 던지게 되면 예외 객체의 소멸 문제가 남아 있으며, 해당 예외가 new로 할당한 것인지 아님 전역 객체인지를 구분할 수 있는 방법이 없음.

 

2.     

l       Slicing Problem (잘림)

class exception {

virtual const char * what() throw();

}

 

class runtime_error : public exception { … };

class validation_error : public exception { … };

 

void someFunction () {

throw validation_error();

}

 

void doSomething() {

try {

someFunction();

}

catch(exception ex) {

             cerr << ex.what();

}

}

 

발생한 예외가 validation_error 타입이고, validation_error에서 가상함수 what을 재정의하였다고 해도, 호출되는 what은 기본클래스인 exception what이 된다.

 

3.      참조자

참조자에 의한 예외받기는 두 방법이 가지고 있는 문제를 겪지 않는다. “포인터에 의한 예외받기와 달리, 객체 삭제에 대한 고민도 할 필요 없고, C++ 표준 예외를 처리하는 데에도 무리가 없슴. 그뿐 아니라, “값에 의한 예외받기와 달리 슬라이스 문제도 없고 예외 객체는 한 번만 복사된다. (값에 의한 예외 받기는 두번 복사되는데 이에 대한 자세한 내용은 More Effective C++ 항목 12 참고)

 

참고 : More Effective C++ - 스캇 마이어스 저


2008. 8. 26. 00:16

동적으로 할당되는 메모리를 갖는 클래스를 위해서는 복사 생성자와 치환 연산자를 선언하라

클래스 내에 포인터가 있는 경우 자신만의 복사 생성자와 치환 연산자를 작성하는 것이다.

* 이 함수들 안에서, 포인터에 의해 가리켜지고 있는 데이터 구조를 복사해서 객체마다 자신만의 복사본을 갖게 하거나,

* 일종의 참조 계수(reference counting) 방식을 사용해서 특정 데이터 구조가 얼마나 포인트되고 있는지 계속적으로 유지하는 방법이 있을 수 있다.

참조 계수 방법은 좀더 복잡하고, 생성자와 소멸자에서 더 많은 처리를 해야 하지만(전부는 아니더라도), 대부분의 응용 프로그램에서 많은 메모리 사용량 감소와 속도 향상을 가져올 수 있다.

어떤 클래스들에 대해서는, 복사 생성자나 치환 연산자를 만들지 않는 것이 만드는 경우보다 나을 수 있다.
이런 경우 함수들을 private으로 선언하기만 하면 될 것이다. 이렇게 하면, 클래스의 사용자가 호출하는 것을 막을 수도 있고, 컴파일러가 기본 함수들을 발생시키는 것도 막을 수 있다.

class String {

public:

           String (const char * value = 0);

           ~String();

private:

           char * data;

}

 

String::String(const char * value)

{

           if( value ) {

                     data = new char[ strlen( value ) + 1 ];

                     strcpy( data, value );

           }

           else {

                     data = new char[1];

                     *data = 0;

           }

}

 

inline String::~String() { delete [] data; }


생성자의 코드에서 new를 호출할 때, 하나의 객체를 만드는 경우에도 []를 사용하는 주의를 기울였다. new와 delete 에는 동일한 형식을 사용하는 것이 매우 중요하므로, new에 대한 코드를 작성할 때 []를 사용한 것이다. 이 점은 반드시 잊지 말아야 한다. 항상 해당하는 new에 []를 사용한 경우에만 delete에도 []를 사용해야 한다.

참고 : Effective C++, Scott Meyers 저
2008. 8. 24. 23:17

new와 delete의 사용시 동일한 형식을 이용한다.

만일 new를 호출할 때 []를 이용했다면 delete의 호출시에도 []을 이용한다. 만일 new의 호출시 []을 이용하지 않았다면 delete의 호출시 []을 이용하지 않는다.

이 법칙은 typedef 계통을 위해서도 중요하다. typedef의 저자는 new가 typedef 타입의 객체들을 불러내기 위해 이용될 때, 어떠한 형태의 delete가 사용되어야 하는지를 문서화해야 한다. 예를 들어, 다음과 같은 typedef 을 생각해보자.

typedef string AddressLines[4]; // 개인 주소는 4개의 줄을 차지하고 그들의 각각은 스트링임

AddressLines는 배열이기 때문에 다음과 같은 new의 사용은,

string * pal = new AddressLines;

// "new AddressLines"가 "new string[4]"와 마찬가지로
// string * 을 반환함을 주목

배열 형태의 delete와 일치해야만 한다.

delete pal; // 예측 불능

delete [] pal; // 양호

이와 같은 혼란을 피가히 위해선 배열 타입들에 대한 typedef를 피하는 것이 최선일 것이다.

하지만, 표준 C++ 라이브러리는 내부 배열의 필요성을 거의 느끼지 못하게 할 정도의 string과 vector 템플릿들을 포함하고 있기 때문에 이와 같은 것은 쉽게 해결될 수 있다. 예를 들어, 여기서 AddressLines는 string의 vector로 정의될 수 있다. 즉, AddressLines는 vector<string> 타입이 될 수 있다.

출처 : Effective C++, Scott Meyers 저
2008. 8. 24. 23:02

<stdio.h> 보다는 <iostream>을 사용한다

int i;
Rational r; // r은 유리수이다.

...
cin >> i >> r;
cout << i << r;

이 코드가 컴파일되기 위해서는 타입 Rational 객체와 동작할 수 있는 함수 oepator >> 와 operator << 가 있어야 한다.

class Rational {

public:

           Rational(int numerator = 0, int denominator = 1);

          

private:

           int n, d; // numerator denominator

           friend ostream & operator(ostream& s, const Rational & r)

           {

                     s << r.n << ‘/’ << r.d;

                     return s;

           }

};


이 버전의 operator << 는 다소 미묘하면서도 중요한 사항을 증명하고 있다.

예를 들어,
oerator<<은 멤버 함수가 아니다.
그리고 출력될 Rational 객체는 객체로서가 아닌 상수 레퍼런스로서 operator<< 에 전달된다.

표준 위원회는 다른 C 비표준 헤더명들을 정리하면서 <iostream>을 찬성하여 <iostream.h>를 없애 버렸다. 만일 여러분의 컴파일러가 <iostream>과 <iostream.h> 양쪽을 모두 지원한다면 헤드들이 미묘하게 차이가 있다는 사실이다. 특히, 만일 #include <iostream>을 이용한다면 네임스페이스 std 내에 위치한 iostream 라이브러리의 구성요소를 얻게 된다. 반면, 만일 #include <iostream.h>을 이용한다면 전역공간에서 그러한 같은 구성요소들을 얻는다. 전영공간에서 그들을 취하는 것은 이름 충돌(name conflict)을 유발시킬 수 있다. 네임스페이스의 이용은 이러한 이름 충돌을 막기 위해 설계되었다.