오 놀라워라~ C언어의 확장성 II
멤버 연산자의 사용을 제외하면 사실 구조체 자체 만으로는 큰 어려움이 없습니다. 그런데 구조체가 포인터와 만나게 되면 이야기가 달라집니다. 사실 포인터를 연산자로 제대로 잘 이해하고 있다면 그다지 어렵지 않은 문제입니다.
그러나 포인터가 단독으로 등장 하는 것도 부담이 되는 판국에 구조체와 함께 있는 것을 보게 되면 안타깝게도 예전 악몽이 다시 떠오르게 됩니다.
하지만 결론은 하나 입니다. 구조체 멤버로서의 포인터도 일반 포인터와 결코 다르지 않다는 것입니다. 포인터로서의 속성을 그대로 가지고 있습니다. 설마 포인터가 나오면 두 가지만 기억하라고 했던 것을 벌써 잊지는 않았겠죠? 포인터는 초기화와 사이즈 이 두 가지만 반드시 기억하면 다~ 됩니다. 진짭니다.
자, *Structure.member라는 수식을 만났습니다. 여기에서 * 연산자는 Structure에 적용될까요? member에 적용될까요? 아니면 Structure가 주소 값일까요? member가 주소 값일까요?
(반드시 * 연산자는 주소 값과 사용한다고 했습니다.)
우리는 a + b * c 라는 수식에서 +보다 *연산자를 먼저 한다는 것을 잘 알고 있습니다. (기억이 가물가물 하겠지만 연습장을 빼곡히 채워가며 적었던 수식들 덕입니다.)
마찬가지로 *Structure.member 수식에서는 .연산자를 통해 Structure의 member를 먼저 찾아가게 되고 그 녀석이 주소 값이므로 그 다음에 * 연산을 하게 됩니다. Structure의 사이즈가 얼마인지는 모르겠습니다. 하지만 member의 사이즈는 32bit OS에서 4바이트가 나올 거라는 것은 확실히 알고 있습니다.
*Structure.member != (*Structure).member
*Structure.member == Structure.member[0]
**Structure.member == Structure.member[0][0]
이렇게 구조체 멤버가 포인터라면 그 멤버의 초기화를 절대 잊으면 안됩니다. 만약에 그 포인터 멤버가 char * 일 경우에는 언제라도 strlen() 함수를 사용하여 사이즈를 알 수 있습니다. 그러나 다른 타입의 경우 단순히 데이터 한 개의 주소만 기억할 것이 아니라면 반드시 그 주소 값이 몇 개의 데이터를 참조할 수 있는지 그 개수를 저장할 멤버도 함께 가지고 있어야 합니다. 그 포인터 멤버변수에 메모리 할당을 한 후에 그 사이즈를 아예 저장하지 않거나 구조체와 따로 다른 일반 변수에 저장하게 되면 얘기치 못한 문제가 발생할 수도 있습니다. 그리고 대입 연산자를 사용해 구조체를 복사할 경우 포인터 멤버는 데이터가 제대로 복사 되지 않아 아래와 같이 매우 심각한 문제를 만들기도 합니다.
#include <stdio.h>
/*
* copyleft (l) 2006 - 2017 programmed by Sinclair
*/
// prototypes
extern int fgets4(register char *, int) ;
extern char * strdup2(const char *) ;
extern int onlyInt(int *) ;
typedef struct human {
int age ;
char * name ;
double salary ;
} Human ;
#define __BUFFSIZ___ 80
#define FREE(p) (free(p),p=NULL)
int main() {
Human sinclair = {0}, demian ;
char buffer[__BUFFSIZ___] ;
printf("%s" , "What is your name? ") ;
//scanf("%s" , sinclair.name ) ; // ERROR!!! 제발 이러는 거 아냐~
fgets4(buffer , __BUFFSIZ___) ;
if(!(sinclair.name = strdup2(buffer))) {
return -1 ;
} // end if
printf("%s" , "How old are you? ") ;
if( onlyInt(&sinclair.age) ) {
FREE(sinclair.name) ;
return -2 ;
} // end if
demian = sinclair ; // 구조체끼리는 대입 연산자, 열라 잘 먹는다~
printf("sinclair: %d %s %f\n",
sinclair.age, sinclair.name, sinclair.salary) ;
printf("demian: %d %s %f\n",
demian.age, demian.name, demian.salary) ;
FREE(sinclair.name) ; // 중간에 sinclair의 이름은 free()
printf("sinclair: %d %s %f\n",
sinclair.age, sinclair.name, sinclair.salary) ;
printf("demian: %d %s %f\n",
demian.age, demian.name, demian.salary) ;
return 0 ;
} // end main()
// 그래서 이럴 때 깊은 복사를 사용해야 합니다.
#include <stdio.h>
/*
* copyleft (l) 2006 - 2017 programmed by Sinclair
*/
// prototypes
extern int fgets4(register char *, int) ;
extern char * strdup2(const char *) ;
extern int onlyInt(int *) ;
typedef struct human {
int age ;
char * name ;
double salary ;
} Human ;
#define __BUFFSIZ___ 80
#define FREE(p) (free(p),p=NULL)
int main() {
Human sinclair = {0}, demian ;
char buffer[__BUFFSIZ___] ;
printf("%s" , "What is your name? ") ;
fgets4(buffer , __BUFFSIZ___) ;
if(!(sinclair.name = strdup2(buffer))) {
return -1 ;
} // end if
printf("%s" , "How old are you? ") ;
if( onlyInt(&sinclair.age) ) {
FREE(sinclair.name) ;
return -2 ;
} // end if
demian = sinclair ; // 나머지 멤버는 대입 연산자로
// 포인터 멤버는 이렇게 따로~
if( !(demian.name = strdup2(sinclair.name)) ){
FREE(sinclair.name);
return -3 ;
} // end if
// 이것을 구조체 안 포인터 멤버에 대한 깊은 복사라고 부릅니다.
printf("sinclair: %d %s %f\n",
sinclair.age, sinclair.name, sinclair.salary) ;
printf("demian: %d %s %f\n",
demian.age, demian.name, demian.salary) ;
FREE(sinclair.name) ; // 중간에 sinclair의 이름은 free()
printf("sinclair: %d %s %f\n",
sinclair.age, sinclair.name, sinclair.salary) ;
printf("demian: %d %s %f\n",
demian.age, demian.name, demian.salary) ;
FREE(demian.name) ; // 이거 빼 먹으면 안됩니다.
return 0 ;
} // end main()
이런 문제를 해결하는 가장 좋은 방법은 구조체를 복사하는 함수를 작성하여 대입 연산자 대신 사용하는 것입니다. 그 함수 안에서 다른 일반 멤버는 서로 대입하고 포인터 멤버에 대해서만 따로 깊은 복사를 하도록 작성하면 멋진 해결 방안이 될 수 있습니다. 당연히 구조체 값이 바뀌어야 하니 사본이 될 구조체의 주소 값을 인자로 넘겨야 합니다. 만약에 멤버들이 많아져서 덩치가 크다면 원본 구조체도 주소 값으로 넘기면 효율적일 겁니다.
그리고 C++에서는 아예 + 연산자 자체를 overriding(재정의)하여 사용할 수도 있습니다.
// if( copyHuman(&demian, &sinclair) ) { ~~~ } 요따구로 사용
int copyHuman(Human * target, const Human * source) {
target[0] = source[0] ;
if( !(target[0].name = strdup2(source[0].name)) ){
return -1 ;
} // end if
return 0 ;
} // end copyHuman()
만약에 포인터 멤버 대신 배열 멤버를 사용하면 이런 문제는 결코 발생하지 않습니다.
게다가 사이즈를 따로 저장하지 않아도 됩니다. 배열이라면 언제나 sizeof 연산자를 사용해 알아낼 수 있기 때문입니다. 하지만 원래 프로그램이란 것이 자로 재듯이 정확히 딱 맞아 떨어져 원하는 대로 실행되는 경우는 그다지 많지 않습니다.
국내 대기업 회사에 이름이 한글로 여섯 자(13bytes 문자열)인 사원이 입사하자 이름을 10bytes 문자 배열로 잡은 사원 관리 프로그램이 다운됐다는 이야기는 프로그래머들에게 정말 많은 것을 느끼게 해줍니다. 감히 10년 앞을 내다 보라고 말하지 않겠습니다. 적어도 3년은 내다 볼 수 있어야 하지 않겠습니까?
#Sinclair #씽클레어 #싱클레어 #씽클레어도씨 #씨언어 #씨프로그래밍 #C언어 #Cprogramming #C_Programming #C #Programming #Clanguage #C_Language