소시지 공장 알고리즘을 풀어보자.




단순하기 짝이없다.

1. 길이가 긴 순으로 줄을 세우고, 넓이에서 시간 소요가 발생하는 부분을 카운트

2. 넓이가 넓은 순으로 줄을 세우고, 길이에서 시간 소요가 발생하는 부분을 카운트

3. 가장 짧은 시간대를 출력.




알고리즘이라 하기에 민망한 수준의 풀이다.

또한 위의 코드는 제대로 동작하지 않는다. 

다음 포스팅에 무엇이 잘못 되었는지 분석해보고

제대로 된 풀이를 올리도록 하겠다.


알고리즘을 풀 때마다 느끼는거지만 내 뇌는 알고리즘적 사고에 아예 진화가 안된거 같다.

지금은 형편없이 풀었지만 점점 배워나가면서 좀 더 우아하고 멋진 풀이를 선보이겠다.



C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자.



생성자

새로운 객체를 메모리에 만드는 데 필요한 과정을

제어하고 객체의 초기화를 맡는 함수


소멸자

객체를 없앰과 동시에 그 객체가 메모리에서

적절히 사라질 수 있도록 하는 과정을 제어하는 함수


대입 연산자

기존의 객체에 다른 객체의 값을 줄 때 사용하는 함수


컴파일러가 저절로 선언해 주도록 되어 있는 것들

생성자, 복사 생성자, 복사 대입 연산자, 소멸자

--> 모두 public, inline

기본적인 내용이기 때문에 예시는 생략한다.



1. C++의 참조자는 원래 자신이 참조하고 있는 것 외에 다른 객체를 참조할 수 없다.

(초기화 시에 이미 참조를 결정한다.)

2. const 또한 참조와 마찬가지로 한번 정해진 값을 바꿀 수 없다.


이러한 부분들이 컴파일러에 의해 만들어지는 기본 대입연산자로

처리하기가 상당히 애매한 부분이 될 수 있다.

--> 자신이 직접 대입 연산자를 정의하여 사용해야 한다.


짚고 넘어가기

대입 연산자를 private로 선언할 경우

이 클래스로부터 파생된 클래스는

기본 대입 연산자를 가질 수 없다.



POINT!!


컴파일러는 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를

암시적(디폴트) 으로 만들어 놓을 수 있다.

객체를 사용하기 전에 반드시 그 객체를 초기화 하자!!


이 항목을 위의 한 줄로 끝나는 단원이라해도 무방하다.

이번 장에서는 고맙게도 3가지의 유의점을 콕 찝어 설명해주고 있다.

하나하나 들여다보면서 진행해보도록 하겠다.


1. 멤버가 아닌 기본제공 타입 변수는 직접 초기화하자!


Tip.

배열의 경우 초기화가 런타임에 비용이 소모될 수 있는

 상황이라면 초기화에 대한 보장이 없다.

하지만 vector는 그러한 보장을 가질 수 있다.


초기화 방법

int x = 0;

const char* text = "C_Style";

double d;

cin >> d;


간단하다.

그냥 변수를 생성했다면 바로 초기화해서 사용하자 이런 뜻이다.



2. 객체의 초기화는 초기화 리스트를 이용하자!!


주의점.

" 대입을 초기화와 헷갈리지 않는 것이 가장 중요하다. "


객체의 초기화는 그렇다. 바로 생성자로 귀결된다.

먼저 위에서 언급했던 대입의 경우를 보도록 하자.



C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는

생성자의 본문이 실행되기 전에 초기화되어야 한다라고 명기되어 있다.


위의 예제는 초기화가 아닌 단순 대입이다.

초기화는 단계는 진작에 지나간 셈이다.


여기서 잠깐!

기본 제공 타입인 hp, mp도 미리 초기화 되었을까?

기본 제공 타입의 경우 대입되기 전에 초기화되리란 보장이 없다.


자 그럼 대입이 아닌 멤버 초기화 리스트를 사용해서

대입문 없이 진짜 초기화를 시켜보도록 하겠다.


초기화리스트를 사용하여 깔끔하게 초기화 완료!


대입의 경우 기본 생성자를 호출해서 초기화를 미리 해놓은 후

복사 대입 연산자를 연달아 호출하는 방법이라 하면

초기화 리스트는 복사 생성자 한 번만을 호출하는 방법이다.

당연히 초기화 리스트의 효율이 더 좋다는 뜻이다.

(대부분의 데이터 타입에서는)




대부분의 타입에 포합되지 않는 타입이

앞에서 말한 hp와 같은 기본제공 타입이다.(int)

기본 제공 타입인 경우 초기화나 대입에 드는

비용이 얼마 차이가 나지 않는다고 한다.


하지만!!

데이터 멤버는 초기화 리스트에 모두 올려주는

정책을 지향하며 실수를 방지하는 것도 좋은 방법일 것이다.




기본 제공 타입의 경우에도 초기화 리스트가

의무화 되어지는 경우가 있다. 어떤 경우냐면

바로 const 혹은 참조자로 되어있는 경우이다.

(상수와 참조자는 대입 자체가 불가능)

이 경우 초기화리스트는 선택이 아닌 필수!


아까도 말했지만 기본 제공 타입은

대입과 초기화의 비용이 별반 차이 나지 않는다.

초기화리스트에 주렁주렁 메달려있는 모습이 보기 싫다면


(초기화 해야하는 생성자가 많을 경우 초기화 리스트 또한 늘어나기 때문)


위와 같이 멤버 함수를 통해 초기화 해주는 방법이 있다.




Tip.

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

1. 기본 클래스는 파생 클래스보다 먼저 초기화 된다.

2. 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화 된다.


따라서 초기화 리스트 순서대로 초기화 되는 것이 아니라

선언된 순서와 같다. 때문에 혼동을 막기 위해

선언 순서와 초기화 순서를 일치 시키도록 하자!



3. 별개의 번역 단위에 정의된 비지역 정적 객체에

영향을 끼치는 불확실한 초기화 순서를 염두해두자!


이게 도대체 뭔 소린가 싶지만 책을

 찬찬히 읽어보며 해석 해보도록 하자.


들어가기에 앞서 용어 정리


정적객체란?

1. 전역 객체

2. 네임스페이스 유효범위 안의 객체

3. 클래서 내의 static 객체

4. 함수 내의 static 객체

5. 파일 유효범위 내의 static 객체


이들 중 함수라는 지역성을 가진 4번만

지역 정적 객체라고 한다.

위의 5가지의 정적객체는

프로그램이 끝날 때 자동 소멸된다.


번역 단위란?

쉽게 말해 하나의 소스 파일

#include하는 파일들까지 합침




==========================문제 제기


두 개의 번역 단위 중 한 곳에서

1. 비정적 객체를 초기화 하려고 보니

2. 다른 번역단위의 비지역 정적 객체를 필요로 한다.

3. 하지만 이 비지역 정적객체는 초기화 되어 있는지 모른다.


별개의 번역단위에서 정의된 비지역 정적 객채들의

초기화 순서는 정해져 있지 않다!!


===========================해결책


1. 비지역 정적 객체를 하나씩 맡는 함수를 준비하고

2. 이 안에 각 객체를 정적 객체로 선언한다.

3. 그리고 함수는 이들에 대한 참조자를 반환한다,


--> 비지역 정적 객체를 직접 참조하는 폐단은 버리고

이제는 함수호출로 대신하게 된다.

비지역 정적 객체 ---> 지역 정적 객체로 바뀌었다.




무슨 말인가??

지역 정적 객체는 함수 호출 중에 그 객체의 정의에

최초로 닿았을 때 객체를 초기화 해버린다는 것이다.

따라서 비지역 정적 객체에 직접 접근하지 않고


함수 내에서 지역 정적객체를 정의하고 초기화 하여

지역 정적 객체에 대한 참조자를 반환하게 만들었다면

그 참조자는 반드시 초기화된 형태라는 것이다.


번역 단위 1


class File {                                  

public:

size_t nimDisks() const;

 }  


File& tf(){                                   

static File f;                      ----> 2. 지역 정적 객체를 정의 하고 초기화

return f                            ----> 3. 이 객체에 대한 참조자를 반환

}    


번역 단위 2


class Dir { 

...

size_t disks = tf().numDiskes();   ---> 1. 함수를 호출

...

}


Dir& tempDir(){

static Dir td;

return rd;

}

--> 결론

객체에 직접 접근하지 않고 함수를 통해 접근하면 된다.


초기화 순서 문제를 방지하기 위해 이처럼 참조자 반환 함수를

사용하는 것은 객체들이 초기화 순서를 제대로 맞춰 둔다는

전제조건이 뒤받침되어 있어야 가능한 말이다.



POINT!!


그냥 무조건 초기화해서 쓰자!!




내용이 조금 길어진 듯하다.

하지만 책에서 초기화에 대한 내용을

매우 강조하고 있다는 점을 보아

한번쯤 제대로 짚고 넘어가야할 부분인 듯 하다.


낌새만 보이면 const를 들이대자!


-> 어떤 값이 불변이어야 한다는 제작자의 의도를 컴파일러 및 다른 프로그래머와 나눌 수 있는 수단.


1. const는 전역 혹은 네임스페이스 유효범위 내의 상수를 선언하는 데 사용할 수 있다.

--> const int Width = 10;


2. 파일, 함수, 블록 유효범위에서 static으로 선언한 변수에도 사용할 수 있다.

--> void SetX(){

static const int count = 1;

}


3. 클래스 내부의 데이터 멤버

-->  class AAA{

private:

const int count;

static const int Size = 10;   --> 객체 내부의 정적 멤버도 당연 적용할 수 있다.

}


4. 포인터

1. const int* aptr;                 --> 포인터로 데이터 조작을 하지 않겠다. 가리키는 대상을 상수화 --> 가리키는 대상은 바꿀 수 있음.

2. int* const aptr;                --> 하나만 가리키겠다. 포인터를 상수화 --> 데이터 조작은 가능

3. const int* const aptr;      --> 가리키는 대상도 바꾸지 못할 뿐더러, 포인터를 통한 데이터 조작까지 허용하지 않겠다.


5. STL의 반복자

STL의 반복자 즉 iterator는 포인터를 본뜬 것이기 때문에, 기본 동작원리가 T* 포인터와 흡사하다.

반복자를 const로 선언하는 것은 T* const 와 같다. --> 즉 포인터를 상수화 하겠다는 말이다.

그렇다면 가리키는 대상을 상수화 하려면 어떻게 해야 할까? --> const_iterator를 쓰면 const T* 와 같이 동작한다.

아래 예제를 보자.


vector<int> vec;


1. const vector<int>::iterator iter = vec.begin();          --> iter는 T* const 처럼 동작. 즉 위의 2번과 같이 포인터를 상수화한 형태

*iter = 10;                                                               -->  따라서 데이터 조작은 가능하다.

++iter;                                                                    --> 하지만 가리키는 대상을 바꾸는 것은 불가능!


2. vector<int>::const_iterator cIter = vec.begin();        --> cIter는 const T* 처럼 동작. 즉 위의 1번과 같이 가리키는 대상을 상수화한 형태

*cIter = 10;                                                             --> 따라서 데이터 조작이 불가능하다.

++cIter;                                                                  --> 가리키는 대상을 바꾸는 것은 가능하다!


6. 함수

ex)

const AAA operator(const AAA& lhs, const AAA& rhs);

AAA a, b, c;

1.   (a*b) = c;  

2.   if(a*b = c)  

---> 1,2번과 같은어처구니 없는 코드를 예방할 수 있다.


상수 멤버 함수


1. 이 함수를 통해서 멤버 변수의 값이 변경되는 것을 허용하지 않겠다!!

2. 상수화된 함수는 상수화되지 않은 함수의 호출을 허용하지 않는다.          --> 상수화 되지 않은 함수 내에서 데이터 조작의 가능성이 있기 때문

3. 또한 멤버 변수의 포인터를 리턴하는 것도 허용하지 않는다.                    --> 반환된 포인터로 조작의 가능성이 있기 때문

--> 따라서 const int* GetPtr () const  --> 이런 형식으로 진행하도록 하자.  4항목의 2번 참고

4. const 키워드에 유무에 따라 함수 오버로딩이 가능하다.


//////////////////// 멤버 함수

void show() const;                    1번

void show();                              2번

//////////////////// 객체 선언

AAA aaa;                

const AAA bbb;     


aaa. show();    --> 비상수 객체 --> 두 개의 show()를 모두 불러올 수 있다. -->우선순위가 높은 비상수 함수를 불러온다. 2번

bbb.show();    --> 상수 객체  --> 오직 상수 함수만을 불러올 수 있다. 1번


5. 클래스의 인터페이스 향상 --> 멤버 변수를 조작할 수 있는 함수는 무엇이고, 조작할 수 없는 함수는 무엇인가? 를 알아야한다.

6. 상수 객체를 사용할 수 있게 된다. --> 객체 전달을 상수 객체에 대한 참조자로 진행하는 것 --> 복사손실 복사비용을 없애주어 코드 효율 UP


7. 상수 객체

1. 상수 멤버 함수만 불러올 수 있다.


실제 프로그래밍에서 상수 객체가 생기는 경우

1. 상수 객체에 대한 포인터 혹은

2.상수 객체에 대한 참조자로 객체가 전달될 때이다.  (위의 6번 참고)


void print (const Player& cp){ ... }  


참고)

const int& GetInt(int& a) { return a; };      1번

int & GetInt(int& a) { return a; }               2번


1번의 경우

int a = 10;

int b = GetInt(a);   --> 가능

int &b = GetInt(a)  ---> 불가능. 데이터 조작의 위험성이 있다. 반환된 레퍼런스로 어떠한 데이터 조작도 할 수 없다.


2번의 경우 모두 가능.


Player& 리턴과

Player 리턴의 차이

--> 함수의 반환 타입이 객체의 참조형식일 경우 리턴된 객체가 그 자리에 반환되게 된다.

--> 참조형식이 아닐 경우 리턴된 객체의 복사본이 오게 된다.



const 키워드는 프로그램을 안정적으로 작성하는데 많은 도움을 준다.

기능적인 측면 말고도 오류를 쉽게 찾아낼 수 있다는 이점까지 줄 수 있다.

따라서 const 키워드는 가급적이면 많이 사용하도록 하자.


const에 대해 상당히 많은 내용이 정리되어 있다.

나름 쉽게 풀어쓴다고 썼지만 포스팅이 아직 어색하여

보기 썩 좋은 모양새는 아닌 듯 하다.


비트수준 상수성, 물리적 상수성 그리고 상수 비상수 멤버의

코드 중복현상을 피하는 방법에 대해서는 다음에 포스팅 하도록 하겠다.

1. 보글 게임 알고리즘


일명 무식하게 풀기라는 완전탐색 알고리즘에 대해 알아보겠다.

모든 경우의 수를 다 생각해보자는 것이다. 제대로 이름 값 하는 놈이라 할 수 있다.

이 알고리즘에서는 (앞으로도 계속일테지만) 재귀 호출이 알고리즘에서 상당히 효율적으로 쓰이게 된다.

들어가기에 앞서 간단한 재귀 호출에 대해서 복습을 하고 가자!!



너무나도 간단한 예제이다.

함수 내에서 자기자신을 호출하고 있다.

위의 경우 for문을 써도 간단하게 해결되는 문제지만

문제의 특성에 따라 적절하게 사용한다면

코딩을 훨씬 간단하게 만들 수 있다.


재귀에 대해 어렴풋이 생각이 났다면 바로 들어가보자.


대충 문제 설명

5X5 보드판에 알파벳이 채워져 있고

1. 시작 위치 (x,y) 와 보드판에서 찾을 단어를 입력한다. (ex. word)

2. 시작 위치부터 해서 상하좌우 대각선까지 인접한 부분으로 다음 단어를 찾게 된다.

3. 한 글자가 두번 사용되어 질 수도 있다.

ex)

q w e r d

p m o f t

b w e a n

y w q s s

h m o c c

-->> 좌표 입력 : 1 0

-->> 찾을 단어 : worord

-->> 단어를 찾았습니다. 



string name("abcdefg");

name.strsub(1);                -->bcdefg   --> 매개변수를 하나 전달하면 그 놈만 빼고 (왜 첫 글자가 1이지?)

name.strsub(0,2);           --> abc        --> 매개변수를 두개 전달하면 그 범위에 있는 놈만 살아남는다.


위의 경우에서 

1번. bool hasWord(int, int, string&);    

2번. bool hasWord(int, int, string);

3번. bool hasWord(int, int, const string&);


hasWord( ....  , _word.substr(1));

2번, 3번은 실행이 되는데 1번이 실행이 안됨.

왜 그런건지 꼭 찾아봐야함!



C++ API design 이란 책을 처음 펼쳐보았다.

처음 느낌으론 Effective C++ 상당히 유사하다.

하지만 더욱 알기 쉽게 표현되어 있다.

책 초반부에는 테크니컬한 부분이 없기 때문에

읽다가 중요하다 싶은 부분에 대해 포스팅을 진행해보기로 한다.


1. C++ API는 일반적으로 다음과 같은 요소들을 포함하고 있다.


API를 설계하고 구현하는 일은 대게 일반적인 앱의 개발보다 더 많은 작업을 요구한다.

API의 목적은 강력하고 안정적인 인터페이스를 다른 개발자에게 제공하는 데 있기 때문이다.

품질, 계획, 문서화, 테스트, 지원과 유지보수에 필요한 요구 수준이 높다.


-> 헤더

인터페이스를 정의하는  .h 파일의 모음으로 클라이언트는 인터페이스를 통해 코드를 컴파일 할 수 있다.

오픈 소스 API의 경우 API의 구현체인 소스 코드 (.cpp)가 포함 된다.

오픈 소스의 의미 --> 구현부를 클라이언트가 볼 수 있는 것.


-> 라이브러리

API를 구현하는 하나 이상의 정적 또는 동적 라이브러리를 말한다.

클라이언트는 필요한 기능을 추가하기 위해서 이 라이브러리를 참조한다.


-> 문서화

API를 어떻게 사용하는지 설명하는 개괄적인 정보를 가지고 있으며

대게는 API의 모든 클래스와 함수에 관해 자동으로 생성된 메타 데이터를 제공한다.


API 디자인 주의 사항

1. API가 상당히 광범위하게 사용되어 질 수 있다는 점을 명심해야한다.

2. API에서 발생되는 문제는 의존 관계를 맺는(API를 사용하는) 모든 앱에 영향을 미친다.

3. API를 변경할 때에는 반드시 하위 호환성에 신경 써야 한다. 

(이전 버전을 쓰고있는 앱을 위해)

4. API는 지속성이 강하다. 때문에 계획, 설계, 버전 관리 및 리뷰에 많은 비용을 투자해야 한다.

5. 사용자가 API에 대한 정보를 쉽게 얻기 위해 잘 정리된 문서화가 필요하다.

(사용자는 소스 코드를 볼 수 없기 때문에)

6. 테스트 자동화? 회귀 테스트? 안정적인 릴리즈


초기의 소프트 웨어 개발 --> 제품의 모든 코드를 작성

--> 요즘에는 상업용 라이브러리와 오픈 소스의 보급으로 개발된 기능을

재사용 하는 것이 일반적. 필요한 부분이 있다면 가져다 쓰면 끝.



1장을 읽는 내내 이게 뭔 뜬 구름 잡는 소린가 하는 느낌이 많이 든다.

그래서 그런지 포스팅도 두서 없이 주저리주저리 떠든 감이 있다.

일단은 쭉 읽어 나가보기로 한다.

오늘의 포스팅은 여기서 마치겠다.


'C++' 카테고리의 다른 글

[C++] 전방 선언  (0) 2016.12.19
[C++] 팩토리 함수  (0) 2016.12.19
[C++] 예외처리 // 열혈강의  (0) 2016.12.14
[C++] 템플릿 // 열혈강의  (0) 2016.12.12
[C++] 복사생성자 정리  (0) 2016.12.07


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

#define SIZE 10

--> 컴파일러에겐 보이지 않는다. 전처리기가 컴파일러에게 넘어가기 전에 숫자 상수로 대입해 버린다.

--> 기호 테이블에 들어가지 않는다.

--> 에러가 날 경우 SIZE라 표시 되지 않고 10으로 표시 된다 --> 디버깅이 어려움


해결책

const int Size = 10;

--> 매크로 대신 상수를 쓴다.

--> 컴파일러의 눈에도 보이며, 기호테이블에 들어간다.

--> 등장 횟수 만큼 사본을 만드는 매크로와는 달리 단 하나의 원본만 있으면 된다.


tip!!

상수의 정의는 대부분 헤더파일에 넣는다.

ex)

define.h 헤더 파일에 const double Width = 5.4;  정의를 해놓고

  cost double Height = 7.7;  


사용하고자 하는 파일에 가져다가 사용한다.

#include "define.h" 



#define를 상수로 교체할 때 주의점.


1. 상수포인터를 정의하는 경우


const char* const name = "Kim";

const stirng name("Kim");


참고)

1. char* name;    

2. const char* name --> 가리키는 대상을 상수화 시킨다.

3. char* const name --> 포인터 자체를 상수화 시켜 다른 객체를 가리키지 못하게 한다.

4. const char* const name --> 2,3 모두 해당


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


--> 상수의 유효범위를 클래스 내로 한정하고자 할 때 사용

--> 상수의 사본 개수가 한 개를 넘지 못하게 만들고 싶다면 static멤버로 만든다.


class Cost{

private:

static const int Size;    //  방법 1 : static const int Size = 10 --> 으로 해줄 수도 있으나 지원하지 않는 컴파일러가 있을 수 있다.

int arr[Size];

};

방법 2 : 

const int Cost::Size = 10;  --> static 멤버 초기화 문법 --> main호출 전에 실행된다. 다른 멤버 변수를 이렇게 정의할 수 없다.


====================================================================문제 제기


2번째 방법으로 정의를 해줄 경우 int arr[Size]; 에서 오류가 발생한다.


해결책 --> 나열자 둔갑술 (enum hack)


class Player{

private:

enum { Size = 10 };

int Item[Size];

};

--> 나열자 둔감술은 동작방식이 const보다 #define에 가깝다.

--> const는 주소값을 얻어 낼 수 있지만, enum은 주소값을 얻어낼 수 없다.

--> 따라서 상수를 가지고 주소를 얻는다든지 참조자를 쓴다든지 하는 경우를 방지할 수 있다.

--> enum은 #define처럼 어떤 형태의 쓸모없는 메모리를 할당하지 않는다. --> 그럼 const는 한단 말인가?

--> 나열자 둔갑술은 상당히 상용화된 기술이며 템플릿 메타프로그램의 핵심 기법이다.


클래스 상수를 #define로 만들 경우

--> 유효범위가 없다.

--> 컴파일이 끝날 때까지 유효하다.

--> 캡슐화의 개념이 없다. (private)

--> 따라서 임의의 클래스 안에서만 쓸 수 있는 매크로를 만들 수 없다.


메크로 함수


#define CALL_WITH_MAX (a,b) f((a) > (b) ? (a) : (b))   --> 인자마다 괄호를 씌워주어야 한다.


int main(){

int a = 5;, b = 0;

CALL_WITH_MAX(++a, b);               //a가 두 번 증가   --> 매크로 함수 내에서 a가 두번 불리게 된다.

CALL_WITH_MAX(++a,b+10);          //a가 한 번 증가

}


해결책 --> 인라인함수에 대한 템플릿


template<typename T>

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

f(a > b ? a : b);

}


1. 이 함수는 템플릿이기 때문에 동일 계열 함수군을 만들어낸다.

2. 구현의 어려움 ( 괄호와 같은 )도 없고 인자를 여러번 평가할 (위와 같은 문제) 요소가 사라졌다.

3. 진짜 함수이기 때문에 유효범위 및 접근규칙을 따를 수 있다.


동일 계열 함수군이란?

--> 하나의 템플릿으로 int, double, char 등 여러개의 함수가 만들어 질 수 있다.

      따라서 동일 계열 함수군은 하나의 템플릿으로 만들어 질 수 있는 모든 함수들을 통칭한다.



POINT!!

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

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

열혈강의 13장 예외처리

---> 예외가 발생하는 지역과 처리되어지는 영역이 다른 경우

---> 기존 예외처리 방식은 예외가 방생하는 지역에서 해결을 한다.


1. 예외처리 메커니즘

try {

if(){ //예외 발생 예상 지역

throw ex;  --> ex를 가리켜 '예외'라고 표현한다. 예외상황이 발생됨을 알려주기 위해 사용되는 키워드

}

}

catch(처리 되어야 할 예외의 종류){ --> 위 try 블록의 ex가 전달된다.

//예외를 처리하는 코드

}


int main(){

int a, b;

cin >> a >> b;


try{

if(b==0) throw b;   ----> 예외가 발생하면 b가 catch로 전달 된다.

....                            -----> 예외가 발생하면 try블록안의 다음 문장들은 실행되지 않고 catch가 실행된다.

....                            -----> 반대로 예외가 발생하지 않으면 catch가 실행되지 않고 try블록을 끝까지 실행하게 된다.

}

catch(int exception){

cout<<exception<<"을 입력했습니다."<<endl;

cout<<"입력 오류! 다시 실행 하세요."<<endl;

}

}


2. Stack Unwinding (스택 풀기)


int divide(int a, int b);

int main(){

int a, b;

cout <<"두개의 숫자 입력 : "; cin >>a>>b;


try{

cout<<divide(a,b)<<endl;

}

catch(int exception){

....

....

}

}

int divide(int a, int b){

if(b==0) throw b;    ----------> b가 diveide가 호출되는 영역으로 넘어간다.

return a/b;

}


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


int main(){ 

try{ 

divide();

....

}

catch(int ex) { .... }

}

int divide(){ divide2(); }

int divide2(){ divide3(); }

int divide3(){

throw b;      -------->

}


스택

divide3()   ---> 예외 발생 throw b; try catch 블록이 없네? 자신을 호출한 함수를 찾아간다.   ---> 전달해주면서 할당된 스택영역 해제

divide2()   ---> b를 전달 받음. try catch 블록이 없네? 자신을 호출한 함수를 찾아간다.          ---> 이 또한 마찬가지

divide1()   ---> b를 전달 받음. try catch 블록이 없나? 자신을 호출한 함수를 찾아간다.          ----> 이 또한 마찬가지

main()    ---> b를 전달 받음. try catch 블록이 있네? 예외처리 실행.


3. ex


1. throw 를 통해 예외를 전달 했으나 어디에도 try catch 블록이 없을 때 --> abort()함수가 호출.

   --> 예외는 반드시 처리되어야 한다!!


2. throw b;  ---> b는 int 형

   catch(char ex) { ... }   --> abort()함수가 호출된  --> 발생되는 타입과 처리되어지는 타입이 같아야 한다.


3. throw (int, double, char*)    ---> int , double, char* 형태로만 예외를 발생시키겠다!!


4. throw      --> 다양한 타입으로 예외를 발생시킬 경우

catch(char exp){ ... }

catch(int exp) { ... }

---> 함수 오버로딩의 형태가 아니라 순차적으로 물어보면서 자기 타입에 맞는 catch를 찾는다.


3. 예외 클래스 디자인

--> 예외는 클래스의 객체로 표현되어 지는 것이 일반적이다.

--> throw 예외객체 --> 일반적인 형태  --> 객체로 표현되어질 경우 예외가 왜 발생되어 졌는지에 대한 상황을 충분히 담을 수 있다.


------------------------------------------------------------- 주의 사항

class AAA{}

class BBB : public AAA{}

class CCC : pubic BBB {}


int main(){

try{

throw AAA();

throw BBB();

throw CCC();

}

catch(AAA& ex) { ... }   ---> 위의 세 경우 모두 이 블럭이 실행된다. 왜?? is-a관계에 의해 derived클래스가 모두 처리되어 질 수 있기 때문이다. (CCC는 AAA다가 성립)

catch(BBB& ex) { ... }    ---> 해결챌은 간단하다. catch의 순서를 거꾸로 해주면 된다.

catch(CCC& ex) { ... }   ---> AAA는 CCC이다가 성립하지 않기 때문에 AAA는 자신의 catch를 찾아 마지막까지 내려오게 된다.

}



-->throw AAA() --> 임시객체로 던졌는데 어떻게 참조 형식으로 받을 수 있지? 임시객체는 다음 줄이면 바로 없어져 버리는 거 아닌가?

'C++' 카테고리의 다른 글

[C++] 팩토리 함수  (0) 2016.12.19
[C++] API란?  (0) 2016.12.15
[C++] 템플릿 // 열혈강의  (0) 2016.12.12
[C++] 복사생성자 정리  (0) 2016.12.07
[C++] 레퍼런스 정리  (0) 2016.12.07

열혈 강의 12장 : 템플릿


template <typename T>

T Add(T a, T b){

return a+b;

}


쉽게 말해 자료형을 결정 짓지 않는 것.


1. 함수 템플릿

2. 템플릿 함수  --> 함수 템플릿에 의해 실제 호출이 가능한 함수.


template <typename T>

void show(T a, T b){

cout<<a<<'  '<<b<<endl;

}


int main(){

show(5, 5.5);   --> 컴파일 에러, 어느 장단에 맞춰야 할지 모른다.

}


======================= 해결책


template <typename T1 , tepename T2>

void show (T1 a, T2 b){ ... }   --> 두개의 템플릿을 선언해 준다.



2. 함수 템플릿의 특수화


template <typename T>

void SizeOf(T a){

cout<<sizeof(a)<<endl;

}


int main(){

int a = 10;

float f = 10f;

double d = 10.0;

char* name = "name";


SizeOf(a);                       /// 4

SizeOf(f);                        /// 4

SizeOf(d);                        /// 8

SizeOf(name);                 ///4 --------> 포인터의 크기가 출력된다. 함수 내에서 return strlen(name); 이 반환되기를 원할 것이다.

}


============================해결책


tempalte<typename T>

void SizeOf(T a) { ... }


template<>                             -------------> 특수화, 원래는 한줄에 같이 쓴다. template<> void SizeOf(char* _name) { ... }

void SizeOf(char* _name){

return strlen(_name);

}


--> template<> void SizeOf<char*> (char* _name){ ... }  --> 올바른 선언 방법



3. 클래스 템플릿


template<typename T>   --> 다음에 정의하는 클래스를 템플릿화 하겠다.

class Data{                       --> 클래스가 아니라 템플릿이다.

private:

T data;

public:

Data(T a) { data = a; }

T GetData(){ return data; }

void SetData(T a){ data = a; }

};


int main(){

Data<int> d1(0);

Data<char> d2('a');

}


----> 객체 생성 시 결정하고자 하는 자료형을 명시적으로 선언해 주어야 한다.

--> 객체 생성 순서 --> 메모리 할당 --> 생성자 호출

--> 메모리 공간의 할당이 우선적으로 진행되어야 하기 때문에 T의 자료형을 알고 있어야 한다.

--> 때문에 명시적으로 선언을 해주어야 한다.


선언과 정의 분리


template<typename T>

Data<T>:: Data(T a) { data = a; }


template<typename T>

T Data<T>:: GetData() { return data; }


teplate <typename T>

void Data<T>:: SetData(T a) { data = a; }


--> 멤버 함수를 정의할 때마다 반드시 붙여 줘야 한다.

--> Data<T> --> 클래스 Data가 아닌 클래스 템플릿 Data를 의미한다.


4. 스택 클래스의 템플릿화


template <typenaem T> 

class Stack{

private:

int len;

T* data;

public:

Stack() : len (-1) { data = new T[100]; }

void push(T a);

T pop();

}


template <typename T>

void Stack<T>:: push(T a){ data[++len] = a; }

tempalte <typename T>

T Stack<T>:: pop() { return data[len--]; }



int main(){

Stack<int> a;

a.push(10);

a.push(20);


Stack<char> c;

c.push('a');

c.push('b');

}



5. 템플릿의 원리 이해

--> 템플릿 함수의 인스턴스화

--> 템플릿 클래스의 인스턴스화


template<typename T>

T Add(T a, T b){

return a+b;

}


--> 메인 에서 int형으로 호출했을 경우 

int Add(int a, int b){

return a+b;

}

--> 메인에서 double형으로 호출했을 경우

int Add(char a, char b){

return a+b;

}

                                 --> 와 같은 함수가 만들어 진다. --> 템플릿을 기반으로 함수가 만들어진다. (컴파일러에 의해)

--> 다음 번에 또 int 형으로 불려졌을 때, 다시 만들어지지 않고 만들어져 있는 놈이 불려온다.

--> double형으로 호출 했을 경우 위와 같이 double형으로 함수 템플릿이 인스턴스화 된다.

--> 클래스도 똑같은 원리


--> 이와 가이 함수 템플릿을 기반으로 실제 호출이 가능한 함수들을 가리켜 템플릿 함수라고 한다.

--> 함수가 만들어 지는 현상을 가리켜서 함수 템플릿의 인스턴스화 라고 부른다.


--> 템플릿은 컴파일러에 의해 처리된다.

--> 따라서 헤더와 정의를 분리할 수 없다. --> 분리 컴파일을 가능하게 하는 것은 링커이기 때문에

--> 하나의 파일 내에 정의와 선언이 같이 있어야 한다.





'C++' 카테고리의 다른 글

[C++] API란?  (0) 2016.12.15
[C++] 예외처리 // 열혈강의  (0) 2016.12.14
[C++] 복사생성자 정리  (0) 2016.12.07
[C++] 레퍼런스 정리  (0) 2016.12.07
[C++] 연산자 오버로딩 // 열혈강의  (0) 2016.12.07


C++를 공부하는 부분에 있어서

상속과 더불어 상당히 골치 아픈 부분 중 하나.

이번 포스팅에서 완벽하게 처리하고 가겠다.


1. 복사 생성자의 형태 --> 복사생성자도 생성자중에 하나


class AAA{

private:

int n;

public:

AAA(){}

AAA(int i) : n(i){};


AAA(const AAA& _a){

n = _a.n;

}

}


int main(){

AAA aaa(10);

AAA bbb(aaa);  ---> 멤버 변수가 복사된다.

}


---> 복사생성자도 생성자와 같이 디폴트 복사생성자가 존재한다. (얕은 복사)



2. 얕은 복사의 문제점, 깊은 복사


--> 클래스를 디자인할 때, 당장은 깊은 복사를 하지않더라도 확장성을 위해 (먼 미래를 위해)

복사생성자를 정의해두자


class AAA{

private:

char* name;

public:

AAA(char *_name) {

name = new char(strlen(_name)+1];

strcpy(name, _name);

}


~AAA(){ delete[] name }

};


int main(){

AAA aaa("kim");

AAA bbb(AAA);    --->> 디폴트 복사생성자가 호출된다. --> 오류가 난다 왜??

}


---> AAA 클래스 내의 멤버변수 name은 포인터로 힙 영역을 가리키고 있다.

따라서 bbb는 aaa가 가르키는 힙 영역을 그대로 가리키게 된다. (멤버 대 멤버 복사를 하기 때문) --> 주소값을 복사하게 된다.

--> bbb객체가 없어지면서 delete에 의해 메모리가 해제가 된다.

또한 여전히 그 공간을 가르키고 있는 aaa 객체에서도 같은 공간을 해제 하려한다. 

--> 하나의 메모리 공간을 두번 소멸하려 했기 때문 --> 문제 발생


AAA (const AAA& _a){

name = new char[strlen(_a.name)+1];

strcpy(name, _a.name);  

}


--> 디폴트 복사생성자는 얕은 복사를 제공하기 때문에, 깊은 복사를 사용자가 정의를 해주어야 한다.



3. 복사생성자의 호출시기


1. 

int main(){

AAA obj1(10);

AAA obj2(obj1);

}


기존의 객체가 새로운 객체를 초기화 하는 경우


2. 

void fct(AAA a){ ... };


int main(){

AAA obj(10);

fct(obj);             ----> 1. 메모리 공간 할당

2. 값 전달 --> 초기화 --> 값을 전달할 때 복사생성자가 호출된다.

}                                             --> 레퍼런스로 전달받으면 상관없는 일


함수에 매개변수로 전달이 될 때 (값에 의한 전달)


3. 


AAA fct(){

AAA a(10);

return a;

}


int main(){

fct();                 ---> 복사본이 리턴된다.    1. 함수 내의 a객체와 똑같은 형태로 메모리 공간 할당

fct().show();                                                2. 쓰레기값으로 초기화가 되어있다.

}                                                                          3. 복사본의 복사생성자를 호출하면서 a의 인자를 전달



함수 내에서 객체를 값에 의해 리턴



+ Recent posts