열혈 강의 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의 인자를 전달



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




C++ 레퍼런스에 대해 정리해보는 시간을 가지자.

사실 정의만 놓고 봐서는 어려운 내용이 아니지만

정확하게 알고가지 않으면 나중에 상당히 혼동될 염려가 있을 것 같다.

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



1. 레퍼런스란 쉽게 말해서 변수에 하나의 이름을 더 붙여주는 것. 하나의 메모리 공간에 여러 이름을 붙여줄 수 있다.


& 연산자는 포인터에 주소값을 전달하기 위해 사용할 수 있고

레퍼런스 선언을 위해 사용할 수 있다.


ex)

int a = 10;

int *ptr = &a;

int &ref = a;


변수란? 메모리 공간에 할당된 이름 --> 레퍼런스는 그러한 이름에 또 다른 이름을 붙여준다.

--> 그 이름으로도 변수를 원래대로 사용할 수 있다.


int i = 5;

int &j =i;


선언과 동시에 초기화가 되어야 하며

선언된 후에는 원래 변수와 완전 똑같이 사용 가능하다.

이름이 존재하지 않는 대상을 레퍼런스로 선언할 수 없다. (ex 상수)


int fct(){


int val = 20;

int &ref = val;

return ref;


}

--> int형 레퍼런스를 int형으로 반환해도 문제없다! --> 레퍼런스는 완전히 똑같은 것으로 생각하면 된다.

--> 만들어지는 방식만 차이가 날 뿐 똑같다.


반대로


int& Get(int& x){ return x; }


int main(){


int _x = 10;

int y = Get(_x);  ---> Get은 레퍼런스를 리턴받아서 바로 변수에 대입. --> 레퍼런스는 변수와 똑같이 사용가능하기 때문


}

------------------------------------------------------------------------------ex2


int i = 0;

int &ref1 = i;

int &ref2 = ref1;


--> i에 ref1 ,ref2라는 이름을 붙여주고 있다. (하나의 메모리 공간에 이름이3개 --> (C에서는 상상도 못하는 일)

--> 역시 문제 없다.


-------------------------------------------------------------------------------



2. call by value , call by reference


1. 포인터를 이용한 call by reference --> 포인터 연산에 의한 것이기 때문에 위험성이 존재한다.


void swap (int *xxx, int *yyy){

...

xxx++;  ---> 이러한 코드가 잘못들어갈 경우 심각한 오류가 생길 수 있다.

...

}



2. 레퍼런스를 이용한 call by reference --> 함수의 호출 형태를 구분하기 어렵다.


void swap (int &xxx, int &yyy){ ... }


int main(){

int a,b;

....

...

swap(a,b);             --> a,b 변수가 xxx, yyy 라는 별명으로 함수내에서 적용된다.

--> call by value인지 call by reference 인지 구분이 모호하다.

}



포인터 연산을 할 필요가 없으므로 보다 안정적이다.



3. call by value


void show(int _a){


cout<< _a <<endl;

}


int main(){


int a = 5;

show(a);    --> 변수a를 매개변수 _a에 복사하게 된다. --> 전달 내용이 커질 수록 복사량이 많아짐 --> 기능저하


}


void show(int &_a){ ... }

--> 레퍼런스를 이용하면 복사가 일어나지 않는다. --> 성능 향상

--> 레퍼런스를 이용할 경우 함수내에서 데이터가 조작될 가능성이 있다.


void show(const int &_a) { ... } --> 조작될 위험을 방지 (const)


-----------------------------------------------------------------------------------------------


3. 레퍼런스를 리턴하는 함수


int& GetInt(int &val){


return val;

}


int main(){


int n = 10;

int &ref = GetInt(n);  -----------> 1. n이라는 메모리 공간에 val이라는 이름을 붙여준다

---> 2. 레퍼런스를 리턴하는 함수이기 때문에 n은 ref라는 이름을 하나 더 갖게 된다. ( n = val = ref )

---> 3. 함수가 종료되면 함수 내의 레퍼런스도 일반 변수와 마찬가지로 스택에서 없어지게 된다. --> (이름만 없어짐, 메모리는 그대로)

---> 4. 결과적으로 n이라는 메모리 공간에 ref라는 이름을 하나 더 가지게 된다. (val은 함수 종료와 함께 없어짐)

}


------------------------------------------------------------------------------------------------------------------------------------


int GetInt(int &val){ 

...

return val;      ------------> int형으로 리턴된다. 위의 경우에 에러가 난다. 왜?? 레퍼런스는 상수에 정의를 할 수 없다!!

 } 


-------------------------------------------------------------------------------------------------------------------------------------


int& fct(){


int val = 10;

return val;


}


int main(){


int &ref  = fct(); ------------> 오류가 난다. 왜?? 함수가 끝나면 val이라는 메모리 공간은 없어지기 때문에

--> 없는 공간에 이름을 붙일 수 없다!


}


--> 지역변수는 레퍼런스로 리턴할 수 없다.

--> 지역객체 또한 레퍼런스로 리턴할 수 없다.



?? 객체 레퍼런스를 리턴하는 함수 대한 이해가 필요함  --> 복사생성자에 해답이 있는 듯 하다


AAA 클래스{

int x,y;


public:

AAA(int _x, int _y) : x(_x), y(_y){};


}


AAA Get(){


AAA temp(3,4);

return temp;

}


int main(){


AAA a(1,2);

AAA &f = GetA();     ---> 이게 왜 실행이 가능한거지?


}



AAA& Get(){


AAA temp(3,4);     -----> 이것도 실행가능한데 쓰레기 값이 들어간다.

return temp;


}


-------------------------------------


AAA& ref_AAA(AAA& _a){


return _a;

}

---------->> 일반 변수와는 다르게 클래스는 메인 함수에서 AAA &ref = GetAAA(a); 로 호출해도 문제가 없다. 왜??

AAA GetAAA(AAA & _a){            ---------->> 예를 들어 int형이라면 문제가 생긴다. 상수를 리턴하기 때문. 클래스는 뭐가 다른거지?


return _a;


}

+ Recent posts