포털:컴퓨터공학/C++/"한꺼번에" 쓰기

위키배움터
둘러보기로 가기 검색하러 가기

"한꺼번에" 쓰기[편집]

C++은 프로그래머가 소스 코드를 작성할때 반복적인 작업을 줄일 수 있는 몇가지 방법을 제공합니다. 그중 C에서는 사용하지 못했던 대표적인 문법적인 기능은 다음과 같습니다.

  1. default parameter
  2. function overloading
  3. templete

순서대로 배워봅시다.

default parameter[편집]

다음 소스를 봅시다.

5.01 불편했던 소스
#include <iostream>

using namespace std;

void first(int *arr, int end)
{
 for(int i=0; i<end; i++)
  arr[i] = 10;
}

void second(int *arr, int end, int start)
{
 for(int i=start-1, i<end; i++)
  arr[i] = 10;
}

int main(void)
{
 int one[10];
 int two[20];

 first(one, 10);
 second(two, 20, 10);

 cout<<"end"<<endl;

 return 0;
}
 
end

first함수와 second함수의 모습이 아주 비슷해 보입니다. 만약 second함수가, 마지막 인자를 전달받지 못했을때(second(one, 10); 처럼 호출되었을 때), 자동으로 int start=0 해준다면 first함수를 대체할 수 있을 것입니다.

C++에서는 이런 기능을 해주고, 예상하셨듯이 이 기능의 이름이 바로 default parameter입니다.



5.02 편해진 소스! 디폴트 인자 사용하기
#include <iostream>

using namespace std;


void second(int *arr, int end, int start=0)
{
 for(int i=start-1, i<end; i++)
  arr[i] = 10;
}

int main(void)
{
 int one[10];
 int two[20];

 second(one, 10);
 second(two, 20, 10);

 cout<<"end"<<endl;

 return 0;
}
 
end


사용법은 간단합니다. 함수를 선언할 때 매개변수를 초기화하는 것이다. 이제 second함수를 호출할때 3번째 인자를 쓰면 start에 쓴 값이 들어가고, 3번째 인자를 쓰지 않으면 start에 0이 들어갑니다.

다른 예를 보면서 좀 더 확실하게 이해해 봅시다.

5.03 디폴트 인자 또다른 예
#include <iostream>

using namespace std;


void makeArr(int *arr=NULL, int size=5;)
{

 int arrSample[5]={};
 if(arr==NULL)
 {
  arr=arrSample;
 }

 for(int i=0; i<5; i++)
  arr[i] = 100;

 for(int i=0; i<size; i++)
  cout<<"["<<arr[i]<<"]";

 cout<<endl;
}
int main(void)
{
 
 int one[]={10,10,10,10,10};
 int two[]={20,20,20,20,20};



 makeArr(one); // 첫번째 makeArr호출
 makeArr(two,2); // 두번째 makeArr 호출
 makeArr(); // 세번째 makeArr 호출


 return 0;
}
 
[100][100][100][100][100]
[100][100][20][20][20]
[100][100][100][100][100]

첫번째 makeArr호출은 첫번째 인자만 썼습니다. 첫번째 makeArr에서 arr은 전달받은 첫번째 인자인 one으로 초기화되고, size는 전달받지 못했으니 5로 초기화 될 것이다. 결과적으로 배열 arr(=one)의 값은 100으로 모두 초기화 될 것입니다.

두번째 makeArr호출은 첫번째와 두번째 인자가 모두 써있습니다. 두번째 makeArr에서 arr은 전달받은 첫번째 인자인 two로 초기화되고, size는 전달받은 두번째 인자인 2로 초기화 될 것입니다. 결과적으로 배열 arr(=two)의 값 중 0,1번째 값만 100으로 초기화 될 것입니다.

세번째 makeArr호출은 아무 인자도 써져있지 않습니다. 세번째 makeArr에서 arr은 전달받지 못했으니 NULL일테고, if문을 만족해 arrSample로 초기화 될 것입니다. size 또한 전달받지 못했으니 5로 초기화 될것입니다. 결과적으로 배열 arr(=arrSample)의 값은 모두 100으로 초기화 될 것입니다.


디폴트 인자에 대해서 공부했습니다. 디폴트 인자를 사용할때는 다음 사항을 주의해야 합니다.

전달받은 인자의 개수로 디폴트 인자 적용 여부를 결정하기 때문에, 매개변수의 오른쪽부터 디폴트 문자를 적용해야 한다

다음 소스를 봅시다.

5.04 잘못된 디폴트 인자의 정의
#include <iostream>

using namespace std;

void print(int a, int b=0; int c, int d=0)
{

 cout<<a<<b<<c<<d<<endl;

}
int main(void)
{
 
 print(1,2)


}
 

이 소스는 제대로 실행되지 않을 것이다. print(1,2)라고 했는데, 1은 a에 들어간다치고, 2는 어디에 들어가야 하는 건가? b에 들어가야 할지, c에 들어가야 할지, d에 들어가야 할지 알 수 없다.

만약 b와 d가 디폴트 인자로 처리되었다고 c에 들어간다면 인자를 전달받을 때는 전달받은 것으로, 전달받지 못했을 때는 디폴트 인자로 초기화되는 디폴트 인자 자체의 의미를 잃어 버리는 것이다.

이런 식으로 문제가 되기 때문에, 디폴트 인자는 무조건 오른쪽부터 정의되어야 한다.

5.05 올바른 디폴트 인자를 사용한 함수의 정의
 
void one(int a, int b, int c) // 디폴트 인자 사용 안함 : 올바름
void two(int a, int b, int c=0) // 맨 오른쪽 하나만 사용 : 올바름
void three(int a, int b=0, int c=0) // 오른쪽부터 2개 사용 : 올바름 
void four(int a=0, int b=0, int c=0) // 모두 사용 : 올바름

//void five(int a=0, int b, int c) 왼쪽 하나만 사용 : 올바르지 않음
//void six(int a, int b=0, int c) 가운데 사용 : 올바르지 않음
 예제 5.01번 
  문자열을 받아 문자열과 문자열의 길이를 출력해주는 함수를 정의해봅시다.

  단, 문자열을 받지 못하면 "Hello world!"로 문자열을 초기화하는 걸로 합니다.

 연습 문제 5.01번 
 (x,y)좌표 2개를 받아서 두 점을 지나는 일차 함수를 출력해줍니다. 일차 함수는 다음과 같이 구합니다.

  전달받지 못하며 0으로 초기화합니다.   상수 함수는 전달받지 않는다고 가정합니다.

function overloading[편집]

이번엔 함수 오버로딩에 대해서 배워봅시다.

다음 소스를 봅시다.

5.06 비슷한데 다른 함수
#include <iostream>

using namespace std;

int func1(int input)
{return input*10;}

double func2(double input)
{return double*10;}

int main(void)
{
 int int_value=10;
 double double_value=3.14;
 
 int_value=func1(int_value);
 double_value=func2(double_value);

 cout<<int_value<<endl;
 cout<<double_value<<endl;

}
100
31.4

func1과 func2함수는 기능을 동일한데도 불구하고 매개변수와 반환 변수의 자료형이 틀려 하나의 함수로 만들 수 없습니다. C++은 자료형 검사에 철저하니, 둘을 하나의 함수로 만들어 버리는 건 좋은 방법이 아닌 것 같습니다.

그럼 두 함수를 호출할 때만 동일한 이름으로 호출할 순 없을까요?

그 방법이 바로 함수 오버로딩입니다.

다음 소스를 봅시다.

5.06 비슷한데 다른 함수
#include <iostream>

using namespace std;

int func1(int input)
{return input*10;}

double func1(double input)
{return double*10;}

int main(void)
{
 int int_value=10;
 double double_value=3.14;
 
 int_value=func1(int_value);
 double_value=func1(double_value);

 cout<<int_value<<endl;
 cout<<double_value<<endl;

}
100
31.4

두 함수 모두 func1라는 이라는 이름을 가지고 있다. 호출할때도 동일한 이름으로 사용합니다. 이것이 바로 함수 오버로딩입니다. 둘 이상의 함수가 같은 기능을 할때 같은 이름으로 만들어 두면 함수 오버로딩이 되며, 호출 할때 자료형마다 다른 함수를 호출할 필요 없이 자료형과 관련없이 한 함수를 호출하면 돼 편리해집니다.

 T I P 
  다른 기능을 하는 함수도 같은 이름으로 만들면 함수 오버로딩이 됩니.

  그러나 이는 프로그래밍을 헷갈리게 할 뿐입니다.
  함수 오버로딩은 반드시 같은, 최소한 비슷한 기능을 하는 함수를 묶어서 만들도록 합시다.

 예제 5.02번 
  예제 5.01번을 함수 오버로딩으로 해결하도록 해봅시다.

예제 5.01은 다음과 같습니다.

  문자열을 받아 문자열과 문자열의 길이를 출력해주는 함수를 정의해봅시다.
  단, 문자열을 받지 못하면 "Hello world!"로 문자열을 초기화하는 걸로 합니다.

template[편집]

template는 함수 오버로딩의 묶음 이라고 할 수 있습니다. 다음 소스를 봅시다.

5.06 지겨운 오버로딩
#include <iostream>

using namespace std;

void function(int a, int b)
{cout<<a<<b<<endl;}

void function(int a, double b)
{cout<<a<<b<<endl;}

void function(double b, double b)
{cout<<a<<b<<endl;}

void function(double a, int b)
{cout<<a<<b<<endl;}

void function(int a, char b)
{cout<<a<<b<<endl;}

void function(char a, int b)
{cout<<a<<b<<endl;}

void function(char a, char b)
{cout<<a<<b<<endl;}

int main(void)
{
 cout<<"이제 또 short, long, long long, float, long double형에 대해 정의해야 한다!"<<endl;

return 0;
}
이제 또 short, long, long long, float, long double형에 대해 정의해야 한다!

템플릿은 이 지루하고 반복적인 작업을 한방에 해결해 줍니다.


5.07 템플릿의 출현!
#include <iostream>

using namespace std;

template <typename T1, typename T2>

void function(T1 a, T2 b)
{cout<<a<<", "<<b<<endl;}

int main(void)
{
 funcion(10, 20);
 function('a', 16.7);
 cout<<"이렇게 쉽게 끝!!"<<endl;
return 0;
}
10, 20
a, 16.7
이렇게 쉽게 끝!

이제 function을 모든 자료형(short, char, int, double 등등. 생각하지 못했던 int*, double*****같은 것들에 대해서도!)에 대해 사용할 수 있습니다.

템플릿 함수를 프로그래머가 적어두면, 컴파일러가 소스 코드를 컴파일 할때, 전달받은 인자에 해당하는 함수를 새로 정의해 줍니다.

즉 위 5.07 소스는 컴파일하면서 다음과 같은 소스처럼 된다는 것입니다. 가로안의 (순서)대로 주석을 읽으시면 도움이 될 것입니다.

5.08 템플릿의 출현! 컴파일후 이렇게 바뀐다!
#include <iostream>

using namespace std;

template <typename T1, typename T2>

void function(int a, int b) // (2) int, int형 함수를 만듬!
{cout<<a<<", "<<b<<endl;}

void function(char a, double b) // (4) char, double형 함수를 만듬!
{cout<<a<<", "<<b<<endl;}
int main(void)
{
 funcion(10, 20); // (1) function()이 int, int형으로 인자를 전달받았다!
 function('a', 16.7); // (3) function()이 char, double형으로 인자를 전달받았다!
 cout<<"이렇게 쉽게 끝!!"<<endl;
return 0;
}
10, 20
a, 16.7
이렇게 쉽게 끝!


이해가 되셨나요? 말하자면 템플릿이란 자료형에 맞게 함수를 선언해주고, 필요하면 오버로딩 시키도록 컴파일러에 내리는 명령정도 됩니다.

 주의하세요! 
  template <typename T>이라고 하면, T끼리는 동일한 자료형입니다. 만약 위 소스에서
template <typename T>

void function(T a, T b)
{cout<<a<<b<<endl;}

  이라고 정의했다면, function('a', 16.7)처럼 호출할 수 없었을 것입니다.
  왜냐하면 template <typename T>이라고 하면, T끼리는 동일한 자료형이기 때문이죠!
  동일한 T형이기 때문에, a, b모두 둘다 한꺼번에 int형이거나, double형이거나, char형이여야지, 둘중 하나는 int, 하나는 double일 수는 없습니다.
  다시 한번 말씀드릴께요. template <typename T>이라고 하면, T끼리는 동일한 자료형이다!

 T I P 
  주의하세요!에서 말한 이유때문에, 소스 5.07과 5.08에서 template <typename T1, typename T2>라 하여 function함수에 쓰일 첫번째와 두번째 매개변수 자료형을 따로 정의한 것입니다.


 T I P 
  템플릿에서 typename을 class라고 쓸 수도 있습니다.

  template <class T1>처럼요. typename이라 쓰든 class 쓰라 쓰든 완전히 동일합니다.

마지막으로 배워야 할 것이 있다. 다음 소스를 보자.

5.09 그냥 읽어 보세요
#include <iostream>

using namespace std;

template <typename T>

struct job{
 char name[40];
 double salary;
 int floor
};

void swap(T a, T b)
{
 T temp = a;
 a = b;
 b = temp;
}

int main(void)
{
 int a=10, b=20;
 char first='A', second='B';
 job one={"YangHyeonSik", 100, 1}, two={"HanTaeHyeon", 99, 2};

 swap(a, b);
 swap(first, second);
 swap(one, two);

 cout<<"end"<<endl;

 return 0;
}
end

여러가지 자료형의 swap을 템플릿 함수로 만들어 편하게 처리하고 있다. 그런데 당신에게 이런 요청이 들어왔다고 하자.

struct job의 swap에서 name의 값은 변경되지 않게 해주세요!

어떻게 해결할 것인가? 지금까지 우리가 알고 있는 방법으로는, 템플릿 함수로의 정의를 포기하고 struct job만 다르고 나머지는 모두 같은 기능을 가진 함수를 자료형 별로 다 정의하는 것뿐이다.

템플릿의 명시적 특수화는 이 문제를 템플릿을 사용하면서도 해결하게 해준다. 소스 5.10을 보자.

5.10 그냥 읽어 보세요 : 요청한 코드를 만들었습니다
#include <iostream>

using namespace std;

template <typename T>

struct job{
 char name[40];
 double salary;
 int floor
};

void swap(T& a, T& b)
{
 T temp = a;
 a = b;
 b = temp;
}

template <> void swap<job>(job& a, job& b)
{
 job temp;
 temp.salary = a.salary;
 temp.floor = a.floor;

 a.salary = b.salary;
 a.floor = b.floor;

 b.salary = temp.salary;
 b.floor = temp.floor;
}

int main(void)
{
 int a=10, b=20;
 char first='A', second='B';
 job one={"YangHyeonSik", 100, 1}, two={"HanTaeHyeon", 99, 2};

 swap(a, b);
 swap(first, second);
 swap(one, two);

 cout<<"end"<<endl;

 return 0;
}
end

이미 템플릿으로 정의된 함수에 대하여, 특수한 자료형 K에 관해서만 따로 함수를 정의하고 싶다면 K에 대한 명시적 특수화 템플릿 함수를 정의하면 됩니다.

위에서 본 것과 같습니다. 좀 더 상세하기 쓰면 이렇습니다.

templete <> 리턴타입 함수이름<템플릿을 적용할 자료형>(템플릿을 적용할 자료형으로 정의된 파라미터(매개변수)들)


 예제 5.03번 
  5.10 소스를 참고하면서 풀어보세요.

  struct people을 정의합니다. people은 이름, 전화번호, 나이를 멤버로 가지고 있습니다.
  템플릿을 통해 swap함수를 정의하고, people에 대한 swap은 명시적 특수화를 이용해 전화번호만 swap되도록 하세요.


마지막으로 템플릿의 명시적 구체화에 대해서 배워봅시다.

5.11 실행되지 않는 코드
#include <iostream>

using namespace std;

template <typename T>


void swap(T& a, T& b)
{
 T temp = a;
 a = b;
 b = temp;
}

int main(void)
{
 int a=10;
 double b=3.14;


 swap(a, b);

 cout<<"end"<<endl;

 return 0;
}
end

앞에서 주의하세요를 잘 읽어보신 분이라면 5.11 소스가 왜 작동하지 않는지 아실 수 있을 것입니다.

명시적 구체화는 템플릿 함수로 가는 자료형을 하나로 통일해주는 역할을 합니다. 다음 소스를 보면서 이해해 봅시다.

5.12 실행되는 코드! 명시적 구체화!
#include <iostream>

using namespace std;

template <typename T>


void swap(T& a, T& b)
{
 T temp = a;
 a = b;
 b = temp;
}

int main(void)
{
 int a=10;
 double b=3.14;


 swap<double>(a, b);

 cout<<"end"<<endl;

 return 0;
}
end

소스 5.12에서 swap함수를 호출할 때 <double>이라는 걸 가운데(정확히 말하면, 함수 이름 명시 후, 전달 인자 명시 전)에 썼습니다.

이렇게 쓰면 swap템플릿에 들어가는 모든 인자는 double로 형변환 됩니다.

이 경우 typename T는 double로 결정될 것이고, 이 소스 코드는 문제 없이 실행 될 것입니다.

 T I P 
  다음 내용은 이해하기 어려울 수 있습니다. 강의 들어가기에 말한 것처럼, 어려운 내용은 후반부에 다시 한번 설명할 것입니다.

  따라서 이 내용이 이해가 가지 않는다면 무시하셔도 좋습니다.

하나의 함수 이름이 있을때, 일반 함수, 명시적 특수화 템플릿 함수, 템플릿 함수가 존재할 수 있으면, 이들은 각각 오버로딩된 버전이 존재할 수 있습니다.
컴파일러가 호출된 함수를 결정하는 기준은 다음과 같습니다.
1.일반 정의 함수 (ex : void func(job& one, job& two);)
2.명시적 특수화 템플릿(ex : template <>void func<job>(job& one, job& two);)
3.템플릿 (ex : void func(T &a, T &b);)

1번이 없으면 2번, 2번이 없으면 3번을 선택하는 꼴입니다.
 연습 문제 5.02번 
 템플릿으로 전달받은 인자 1개의 값을 출력하는 함수를 정의하세요. 다음과 같은 main함수를 사용합니다.
int main(void)
{
 /*...생략...*/ 

 for(int i=0; i<26; i++)
   //이곳에서 함수를 호출

 return 0;
}

main 템플릿 함수에서 int형 자료형을 처리하는 함수를 호출하되, 이 소스가 실행되면 알파벳이 출력되도록 작성해 보세요. 이 문제는 조금 어렵습니다.

정리[편집]

지금까지 배웠던 내용을 다시 기억해 보세요. 복습은 최고의 공부법!

  1. C++에서는 C에서 사용하지 못했던, 반복적인 소스를 줄이기 위한 여러가지 방법'을 제공합니다.
  2. 디폴트 인자는 정말 쉬운 방법이었습니다.
  3. 오른쪽부터 디폴트 인자로 설정해야 된다는 것만 기억하면요!
  4. 함수 오버로딩은 그냥 같은 기능을 하는 함수는 같은 이름으로 해도 된다는 거였죠.
  5. 템플릿은 함수 오버로딩을 더 한꺼번에 한 것이었죠.
  6. 명시적 특수화는 템플릿에서 어떤 자료형에 대해서만 오버로딩을 따로 하는 것이었어요.
  7. 명시적 구체화는 템플릿으로 정의된 함수를 호출할때, 전달할 인자의 자료형을 모두 명시적으로 바꿔주는 것이었어요.
  8. 명시적 특수화화 명시적 구체화의 사용법, 다 기억나세요? 조금이라도 머뭇 거리셨다면 지금 다시 한번 살펴보고 오세요!