이제, 저 높은 곳을 향하여 II
모든 프로그램의 요소는 다 메모리에 올라가야 실행됩니다. 그리고 메모리에 존재하는 모든 요소들은 다 주소 값을 갖습니다. 이 말은 다시 말해 함수도 주소 값을 갖는다는 얘기입니다.
함수 포인터는 범용 함수 작성, 메뉴 프로그램, 그리고 RPC(Remote Procedure Call) 프로토콜의 기본이 되는 중요한 개념입니다. () 연산자를 통해 함수를 부르지 않을 경우 함수이름은 그 자체만으로도 함수 포인터 역할을 하게 되며 함수포인터는 다른 일반 포인터와는 다르게 * 연산자로 접근하지 않고 바로 () 연산자를 사용할 수 있습니다. 일반적으로 함수의 포인터는 다음과 같은 형태를 갖습니다.
리턴타입 (* 함수포인터)(인자타입들) ;
// 인자타입들의 인자를 받고 리턴타입을 리턴 하는 함수 포인터
// 함수포인터(인자들) 과 (*함수포인터)(인자들) 두 가지 형태로 사용 가능
// 함수포인터(인자들)을 주로 사용
리턴타입 * 함수(인자타입들) ;
// 인자타입들의 인자를 받고 리턴타입*를 리턴 하는 함수의 프로토타입
#include <stdio.h>
/*
* copyleft (l) 2006 - 2017 programmed by Sinclair
*/
// prototypes
int first(int, int) ;
int * second(int, int) ;
int main() {
int (*fPointer)(int, int) ;
fPointer = first ;
// fPointer = second ; // warning!!
printf("%x %x %x\n" , first , second , fPointer) ;
// 401149 401154 401149 // in my computer
printf("%x %x %x\n" , &first , &second , &fPointer) ;
// 401149 401154 22ccd8 // in my computer
printf("%d %d %d %x %d\n" , first(1,2) , fPointer(10,20) ,
(*fPointer)(100,200) , second(1000,2000) , *second(1000,2000)) ;
// 3 30 300 403010 3000 // in my computer
return 0 ;
} // end main()
int first(int a , int b) {
return a + b ;
} // end first()
int * second(int a , int b) {
static int r ; // 자동 변수의 주소 값은 리턴 해도 소용없기 때문에 반드시!!
r = a + b ;
return &r ;
} // end second()
고작 덧셈이나 하려고 함수 포인터를 설명하는 것이 아닙니다.
함수 포인터의 가장 일반적인 용도는 타입과 관계없이 사용 가능한 범용 함수를 작성하는 일입니다.
자료구조의 꽃이라 할 수 있는 검색과 정렬을 한마디로 표현하면 비교와 교환입니다. 그런데 교환에 대한 범용 함수는 이미 우리가 앞에서 함께 작성해 보았습니다.
문제는 바로 비교하는 부분인데 비교하는 방법이 데이터 타입에 따라 달라지게 되기 때문입니다. 결과값은 같은데 비교하는 방법이 달라지는 문제 때문에 매번 함수를 다시 작성해야 한다면 문제가 더욱 심각해집니다.
그래서 변하지 않는 로직은 그대로 두고 변하는 부분에 대해서만 함수 밖으로 뽑아서 필요할 때 마다 새로운 함수를 만들고 그 함수의 포인터를 인자로 넣어주면 외부의 함수를 함수 안에서 호출할 수 있습니다. 함수를 작성할 때는 어떤 함수를 부를지 모릅니다. 다만 인자로 넣어주는 함수에 따라 다른 함수를 부를 수 있습니다.
표준 함수 bsearch()는 이진 검색을 통해 원하는 데이터가 존재하는지의 여부를 알려주는 함수입니다. 존재하면 찾은 데이터의 주소 값을 리턴하고 존재하지 않으면 널 포인터를 리턴합니다. 모든 데이터는 연속된 메모리 공간에 정렬되어 있어야 이진 검색이 가능합니다. 그래서 bsearch() 함수는 주로 표준함수 qsort()와 함께 사용하게 됩니다. 그리고 인자로 사용할 함수 포인터의 경우 비교해야 할 타입들이 서로 다를지라도 반드시 함수의 프로토타입은 같아야 합니다.
그래서 void*를 사용하여 타입을 일정하게 맞춰놓고 각 함수 안에서 원하는 타입으로 바꿔서 비교해야 합니다.
void *bsearch( const void * key, /* 검색 키 */
const void * base, /* 검색할 곳 */
size_t nelem, size_t size, /* 개수, 데이터사이즈 */
int (*cmp)(const void *ck, const void *ce)); /* 비교함수 */
void qsort( void * base, /* 정렬할 데이터의 시작 위치 */
size_t nelem, size_t size, /* 개수, 데이터사이즈 */
int (*cmp)(const void *e1, const void *e2)); /* 비교함수 */
위의 bsearch() 함수와 qsort() 함수를 사용하여 문자열을 검색하는 프로그램을 작성해 보도록 하겠습니다.
#include <stdio.h>
#include <stdlib.h>
/*
* copyleft (l) 2006 - 2017 programmed by Sinclair
*/
// prototypes
int compareString1(const void * , const void *) ;
int compareString2(const void * , const void *) ;
int compareString3(const void * , const void *) ;
extern int fgets4(register char *, size_t) ;
int main() {
char words1[][10] = { "Sinclair" , "linux" ,
"dream" , "Veritas" , "Juliet" } ;
char * words2[] = { "Sinclair" , "linux" ,
"dream" , "Veritas" , "Juliet" } ;
int i ;
int number , size ;
char key[20] ;
char * find ;
char ** find2 ;
puts("특별히 찾으시는 단어라도?") ;
fgets4(key , sizeof key) ;
qsort(words1 , number = sizeof words1 / sizeof * words1 ,
size = sizeof * words1 , compareString1) ;
for ( i = 0 ; i < number ; ++i ) {
puts(words1[i]) ;
} // end for
if( find = (char*)bsearch(key, words1, number, size, compareString1) )
{
printf("%s found..\n" , find) ;
} // end if
else {
puts("오메, 고마 못찾아써라..") ;
} // end else
qsort(words2 , number = sizeof words2 / sizeof * words2 ,
size = sizeof * words2 , compareString2) ;
for ( i = 0 ; i < number ; ++i ) {
puts(words2[i]) ;
} // end for
find = key ;
if( find2 =
(char**)bsearch(key,words2,number,size,compareString3)
// (char**)bsearch(key,words2,number,size,compareString2)실행에러
// (char**)bsearch(&find,words2,number,size,compareString2) OK!!
) {
printf("%s found..\n" , *find2 ) ;
} // end if
else {
puts("오메, 고마 못찾아써라..") ;
} // end else
for ( i = 0 ; i < number ; ++i ) {
puts(words2[i]) ;
} // end for
return 0 ;
} // end main()
int compareString1(const void * s , const void * t) {
return strcmp((const char*)s , (const char*)t) ;
} // end compareString1()
int compareString2(const void * s , const void * t) {
return strcmp(*(const char**)s , *(const char**)t) ;
} // end compareString2()
int compareString3(const void * s , const void * t) {
return strcmp((const char*)s , *(const char**)t) ;
} // end compareString3()
아래는 그림은 cygwin에서 gcc로 컴파일 한 후 실행한 결과입니다.
bsearch(key,words2,number,size,compareString3) 나
bsearch(&find,words2,number,size,compareString2) 일 때
bsearch(key,words2,number,size,compareString2) 일 때
이때 실행에러가 발생하는 까닭은 key를 인자로 넘기면 인자타입이 char*타입이고 words2를 인자로 넘기면 인자타입이 char**이지만 compareString2() 함수는 받을 수 있는 두 인자가 모두 char**이기 때문입니다. 그래서 배열인 key를 그대로 사용하려면 char*와 char**를 비교하는 compareString3()를 사용하거나 char*타입의 변수 find를 사용하여 그 주소 값을 넘기게 되면 char**타입이 인자가 되므로 compareString2()를 그대로 사용할 수 있는 것 입니다.
그리고 bsearch()함수의 리턴하는 값의 타입도 인자로 넘겨준 base의 타입과 동일하다는 것을 주의해야 합니다.
요즘 프로그램은 대부분 메뉴를 선택하고 선택에 따라 다양한 다른 일을 하도록 되어있습니다. 그런데 자꾸 그 메뉴가 늘었다 줄었다 합니다. 하다못해 지금 이 글타래들을 작성하고 있는 워드프로세서의 경우도 버전에 따라 메뉴가 자주 바뀝니다. 그럴 때마다 고치는 것도 한 두 번입니다. 그것이 자꾸 반복되면 열라 짜증이 날 수도 있습니다. 더욱이 고치다 보면 잘못 건드리게 되고 그러면 bug's life에 빠지는 경우도 발생합니다. 그럴 때 함수 포인터는 좋은 해결 방안이 될 수 있습니다. 그리고 서비스를 제공하는 회사에 따라 유사한 기능을 하지만 다른 메뉴를 사용하는 휴대용 무선 단말기의 경우 함수 포인터와 void*는 매우 유용하게 사용될 수 있습니다. 아래 예제는 함수 포인터를 사용하여 메뉴 프로그램을 작성한 프로그램입니다.
#include <stdio.h>
/*
* copyleft (l) 2006 - 2017 programmed by Sinclair
*/
// 함수 포인터...
// 리턴타입 (* 함수포인터명) (인자들... ) ;
// 위의 함수의 포인터를 위한 함수의 프로토타입..
// 리턴타입 함수명 (인자들... )
// 같은 리턴 타입 같은 인자를 갖는 함수가 무수히 많을때..
// 메뉴 프로그램...
void menu1(void) {
printf("메뉴 일번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu1()
void menu2(void) {
printf("메뉴 이번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu2()
void menu3(void) {
printf("메뉴 삼번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu3()
void menu4(void) {
printf("메뉴 사번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu4()
void menu5(void) {
printf("메뉴 오번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu5()
void menu6(void) {
printf("메뉴 육번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu6()
void menu7(void) {
printf("메뉴 칠번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu7()
void menu8(void) {
printf("메뉴 팔번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu8()
void menu9(void) {
printf("메뉴 구번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu9()
void menu10(void) {
printf("메뉴 십번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu10()
void menu11(void) {
printf("메뉴 십일번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu11()
/* // 갑자기 늘어난 메뉴
void menu12(void) {
printf("메뉴 십이번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu12()
void menu13(void) {
printf("메뉴 십삼번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu13()
void menu14(void) {
printf("메뉴 십사번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu14()
void menu15(void) {
printf("메뉴 십오번입니다.. 탁월한 선택이십니다.\n") ;
} // end menu15()
*/
void (* menus[]) (void) = { menu1 , menu2 , menu3 , menu4 ,
menu5 , menu6 , menu7 , menu8 , menu9 , menu10 ,
menu11 /*, menu12 , menu13 , menu14 , menu15 */ } ;
// menus == &menus[0] type==> void (**)(void)
// void (* mp)(void) ; //<- 이넘은 함수의 포인터당..
typedef void (* mp)(void) ; //<- 이넘은 함수의 포인터타입이당..
mp menus2[] = { menu1 , menu2 , menu3 , menu4 , menu5 , menu6 ,
menu7 /*, menu8 , menu9 , menu10 , menu11 ,
menu12 , menu13 , menu14 , menu15*/ } ;
// menus2 == & menus2[0] type==> mp *
// menus는 11개의 메뉴를 모두 사용할 수 있지만
// menus2는 7개의 메뉴 밖에 사용 못합니다.
// 이것을 잘 활용하면 회원등급에 따른 서비스 메뉴를 다르게 지정할 수도 있습니다.
void disply() {
puts("1 : 메뉴 1..." ) ;
puts("2 : 메뉴 2..." ) ;
puts("3 : 메뉴 3..." ) ;
puts("4 : 메뉴 4..." ) ;
puts("5 : 메뉴 5..." ) ;
puts("6 : 메뉴 6..." ) ;
puts("7 : 메뉴 7..." ) ;
puts("8 : 메뉴 8..." ) ;
puts("9 : 메뉴 9..." ) ;
puts("10 : 메뉴 10..." ) ;
puts("11 : 메뉴 11..." ) ;
/* // UI부분은 주로 따로 작성합니다.
puts("12 : 메뉴 12..." ) ;
puts("13 : 메뉴 13..." ) ;
puts("14 : 메뉴 14..." ) ;
puts("15 : 메뉴 15..." ) ;
*/
puts("0 : exit / 종료" ) ;
} // end disply()
extern int onlyInt(int *) ;
int main() {
int select ;
while(1) {
select = 0 ;
disply() ;
onlyInt(&select) ;
#ifdef __SaPZZiL___
// switch case는 삽질의 대명사, 게다가 실행 파일의 사이즈가 늘어납니다..
switch (select)
{
case 0 : exit(0) ;
case 1 : menu1() ; break ;
case 2 : menu2() ; break ;
case 3 : menu3() ; break ;
case 4 : menu4() ; break ;
case 5 : menu5() ; break ;
case 6 : menu6() ; break ;
case 7 : menu7() ; break ;
case 8 : menu8() ; break ;
case 9 : menu9() ; break ;
case 10 : menu10() ; break ;
case 11 : menu11() ; break ;
/* // 이 부분이 늘어나는 코드들
case 12 : menu12() ; break ;
case 13 : menu13() ; break ;
case 14 : menu14() ; break ;
case 15 : menu15() ; break ;
*/
default : ;
} // end switch
// 메뉴가 늘어나면 함께 case도 늘어나고 프로그램도 자꾸 커집니다.
#endif
// typedef로 선언한 menus2를 사용하는 경우
//if(select > 0 && select <= sizeof menus2 / sizeof *menus2)
// menus2[--select]() ;
// 그냥 배열로 선언한 menus를 사용하는 경우
if(select > 0 && select <= sizeof menus / sizeof *menus)
menus[--select]() ;
else if(!select) exit(0) ;
else puts("이런 뒈안당.. 잘못 입력했자너.. 쓰읍..") ;
// 메뉴가 갑자기 늘어나거나 줄어도 이 부분이 절대 바뀌진 않습니다.
// 그냥 함수를 배열에 넣거나 빼거나 하면 됩니다.
puts("press enter key to continue...") ;
getchar() ;
system("clear");
// system("cls") ; // on M$ windows or DOS
} // end while
return 0 ;
} // end main()
여기에서는 조금 어려운 얘기를 꺼내야 하겠습니다. 몇해전 부터 객체지향이니 디자인 패턴이니 하는 것들이 대세였던 것 같습니다. 때문에 여러분들도 어느 정도 객체지향에 대한 지식을 가지고 있을 거라고 믿고 있습니다. 혹시 아직 객체지향이 뭔지 모르는 분은 우선 객체지향을 공부하신 후에 읽으면 됩니다.
간단하게 객체지향을 정의하면 기존의 작업과 함수 중심의 프로그래밍 방법을 데이터 중심으로 전환한 것이라고 말하며, 이러한 객체지향의 일반적인 네 가지 특징은 상속(inheritance), 은닉(encapsulation), 다형성(polymorphism), 추상화(abstraction)입니다.
여기에서는 추상화에 대한 이야기를 좀 해야겠습니다. 추상화는 객체지향이든 절차지향이든 관계없이 프로그램을 설계하는데 있어 아주 중요한 개념이라고 생각하고 있습니다. 간단히 설명하자면 문제 해결을 위하여 실체, 즉 실제로 존재하는 사물에 대한 명사형과 동사형을 추출하고 의미 있는 명사형은 속성 또는 멤버변수로 의미 있는 동사형은 함수 또는 메소드로 추출하여 각 언어 문법에 맞게 클래스로 묶어주는 작업을 추상화라고 합니다.
추상화의 대가인 피카소의 우는 여인이라는 그림을 보면 오 만 가지 감정이 교차하는 우는 여인의 모습이 느껴지지 않던가요? 개인적으로는 슬픔이라는 실체를 가장 잘 추상화한 작업중에 하나라고 생각이 됩니다.
마찬가지로 프로그래밍의 추상화는 실제 세계에 존재하는 실체를 컴퓨터가 알 수 있도록 의미 있는 명사형과 동사형을 추출하는 작업을 말합니다. 그것을 각 언어의 문법 맞게 작성한 것이 클래스가 됩니다. 그런데 바로 이때 구조체가 단순히 데이터의 집합이라면 클래스는 데이터와 그 데이터를 사용하는 함수를 하나로 묶은 것이라고 생각하면 쉽습니다. (이곳은 객체지향 프로그래밍을 설명하려는 곳이 아니니 좀 더 자세한 내용은 객체지향 서적들을 참고하기 바랍니다.)
그 클래스가 선언 또는 new라는 연산자를 통해 메모리에 올라가면 그것을 객체 또는 Object라고 부릅니다. 물론 객체지향의 마인드는 결코 이렇게 간단하지는 않습니다. 좀더 복잡한 내용을 이해해야 합니다. 그러나 C++나 Java가 결코 하늘에서 뚝 떨어진 언어가 아니라는 것은 누구나 공감하는 사실입니다. 그래서인지 요즘은 C언어로도 객체지향을 흉내 내는 일이 유행처럼 번져가고 있습니다. "C로 객체지향 흉내 내기" 말 만들어도 굉장히 있어 보이지 않습니까? 어떻게 하는지 알고 싶으시죠?
그건 바로 데이터들의 묶음인 구조체 안에 함수나 함수의 프로토타입을 넣을 수는 없지만 함수 포인터를 넣을 수는 있습니다. 함수의 프로토타입이나 함수를 직접 넣으면 따로 초기화 하지 않아도 바로 구조체 멤버연산자로 그 함수를 호출할 수 있지만 함수 포인터의 경우라면 반드시 먼저 초기화를 통해 실제 함수와 함수의 포인터를 연결해야 한다는 것을 기억해야 합니다. 그리고 프로그램 중간에 함수 포인터를 바꿔주거나 또는 함수 포인터배열을 사용하면 일종의 다형성을 구현하는 것이 되기도 합니다.
이것은 실제로 멋져 보이긴 하지만 굳이 효율을 따지자면 그냥 C언어는 C언어답게 사용하시고 C++은 C++답게, Java는 Java답게 사용하시는 것이 좋습니다. 약간의 이익을 위해 너무 많은 대가를 지불해야 한다면 그건 옳지 않은 방법입니다.
간혹 현장에서 개발을 하다 보면 객체지향의 개념을 너무 쉽게 생각한 나머지 문법만 C++이고 컴파일러만 Java일 뿐 내용은 그냥 C처럼 작성하는 것을 많이 보았습니다. 이런 얼토당토 않은 프로그래밍을 하지 않으려면 객체지향도 제대로 공부해야 합니다. 자, 공부하세요. 배워서 남줘야 합니다. 저도 지금 이렇게 남 주고 있지않습니까?
#Sinclair #씽클레어 #싱클레어 #씽클레어도씨 #씨언어 #씨프로그래밍 #C언어 #Cprogramming #C_Programming #C #Programming #Clanguage #C_Language