brunch

매거진 Sinclair

You can make anything
by writing

C.S.Lewis

by Sinclair Aug 01. 2016

Advanced Pointer I

록 삼총사와 친구들 IV




사실 슬프게도 록 삼총사를 제대로 다루고 있는 프로그래머는 정말 드뭅니다. 그리고 그 중에서도 가장 홀대를 받고 있는 녀석을 꼽자면 바로 realloc() 함수 입니다.


그나마 앞에 설명한 두 놈들은 종종 언급이라도 해주지만 아예 realloc() 함수는 거의 등장하지도 않습니다. 아주 찬밥 신세라고 할 수 있습니다. 그러나 실제 개발 환경에서는 realloc() 함수의 중요성이 새롭게 대두 되고 있습니다. 어떤 프로그램이 실행 중에 많은 메모리를 오랫동안 잡고 있으면서 고작 데이터 한 두 개만을 담고 있다면 그것은 좋은 프로그램이라고 할 수 없습니다. 때문에 실행 중에 필요한 메모리가 있다면 더 늘려준다거나 아니면 더 이상 필요 없는 부분을 해제할 수 있다면 메모리를 적절히 사용할 수 있는 좋은 방법이 될 겁니다. 그러한 방법을 제공하는 녀석이 바로 realloc() 함수입니다.


         

void * realloc(void * , size_t) ;     


이미 할당된 메모리 공간의 주소 값과 새로운 사이즈를 인자로 넘겨주면 새로운 사이즈에 맞게 재할당하고 그곳의 주소를 리턴 합니다. 만약에 데이터가 들어있는 상태에서 realloc() 함수로 사용 가능 메모리를 늘려주고 싶을 때, 연속된 공간에 뒤 이어서 메모리를 늘려 줄 수 없다면 새로운 공간에 할당을 하고 데이터를 옮기고 앞의 영역 해제 후에 새로 할당 받은 공간의 주소를 리턴 합니다. 역시 재할당에 실패하면 널 포인터를 리턴 합니다. 때문에 realloc() 함수를 호출하기 전에 반드시 원래 주소 값을 복사해 두어야 합니다.     



int * i , * temp ;     

if( getMemory2(&i , sizeof(int) * 10) ) {

    return -1 ;

} // end if     

// do something~     

temp = i ;

if( !( i = (int * )realloc( i , sizeof(int) *20 ) ) ) {

    i = temp ; // 원상 복구하여 사용하거나     

    // free(temp) ; // 메모리 해제 후 종료하거나 둘 중 선택

    // return -1 ;     

} // end if     


free(i) ;               




멋진 사실은 realloc() 함수가 이렇게 재할당뿐만 아니라 첫 인자로 널 포인터를 넣어주면 malloc() 함수와 같은 동작을 한다는 것입니다. 우리가 앞에서 마방진 프로그램을 작성할 때 malloc() 함수를 사용하는 getMemory2() 함수를 호출했는데, 이것을 realloc() 함수로 바꾸면 매번 마방진을 다시 만들 때 마다 free()를 해야 하는 번거로움이 사라지고 이제는 프로그램이 종료하기 전에 호출하면 됩니다. 단 처음에 사용할 때 반드시 널 포인터를 인자로 넘겨야 한다는 것을 기억해야 합니다. realloc() 함수로 작성한 getMemory4() 함수의 사용 예제입니다.          



type * first ;

type * second ;     

first = (type*) malloc(sizeof(type)) ;

second = (type*)realloc(NULL , sizeof(type)) ;

// 같은 동작을 한다.     

// do something ~     

free(first) ;

free(second) ;               




홀수 마방진을 만들어 주는 프로그램 iv: 동적 할당 3     


#include <stdio.h>     

/*

* copyleft (l) 2006 - 2017 programmed by Sinclair

*/     

// prototypes

extern int onlyInt(int *) ;

extern int fgets4(register char * , int) ;

extern int magic(register int * , int);

extern int presentation(register const int * , int) ;     

int getMemory4(void * , size_t) ;          


int main() {     

    //int matrix[19*19] ;

    int * matrix ;

    int digit ;

    char toBeContinue ;     

    matrix = NULL ; // 반드시 시작할 때 처음 한번은 널 포인터로 초기화

    while(1) {

        toBeContinue = 0 ;     

        while (1) {

            if( toBeContinue++ > 5 ) {

                puts("정녕 마방진을 구하구 싶은 게냐? 콱 이걸 증말...") ;

                exit(-1) ;

            } // end if

            puts("마방진 구해드립니다.. 3 이상의 홀수를 입력하세요..") ;                

            if(onlyInt(&digit)) {

                puts("어허~ 숫자가 아닌 걸 입력하면 아니 되오..") ;

                continue ;

            } // end if     

            // if( !(digit % 2) || (digit < 3 || digit > 20 ) ) {

            if( digit < 3 || !(digit % 2) ) {    

                puts("한글 못 읽어요? 홀수 몰라? odd number말야~") ;

                continue ;

            } // end if

            break ;

         } // end inner while            

        if( getMemory4( &matrix , sizeof(int) * digit * digit ) ) {

             puts("메모리가 부족하여 프로그램이 더 이상 진행되지 못합니다.\n"

                  "죄송합니다. 다음에 다시 이용해주세요.\n\n") ;

             free(matrix) ; // 이젠 여기도 필요

             return -1 ;

        } // end if     


        magic(matrix , digit) ;     

        presentation(matrix , digit) ;     

        // free(matrix) ; // 이젠 여기 말구     

        puts("한 번 더 해드릴까요?(y/any key) 웬만하면 좀 그만두시죠..") ;

        fgets4( (char*)&digit , 2/*sizeof digit*/) ;

        if( tolower(*(char*)&digit) != 'y' ) break ;

        system("clear") ; // on DOS system("cls") ;     

    } // end outer while     

    free(matrix) ; // 마지막에 한 번 만            

    return 0 ;     

} // end main()     



// realloc()함수를 사용하는 메모리 할당 함수


#define M_USLEEP_TIME    300

#define M_DOING_AGAIN     5

#define M_ERROR_MSG     "돈이 엄써서 메모리도 몬사고.. 죄송합니데이.."

int getMemory4(void * p , size_t size) {     

    register tag = 0 ;

    void * temp = p ;     

    while( !( *(char**)p = (char*) realloc(*(char**)p , size) ) ) {

        perror(M_ERROR_MSG) ;

        // == stderr을 사용하는 puts()

        if( ++tag > M_DOING_AGAIN ) {

            p = temp ;

            return -1 ;

        } // end if

        usleep(M_USLEEP_TIME) ; // 0.3ms 동안 쉬었다가 다시 잡는다.

    } // end while     

    return 0 ;     

} // end getMemory4()               




당연히 여기에도 정답은 없습니다. 물론 여러분들이 저보다 더 잘 만들 거라고 믿고 있습니다. 다만 여기서 여러분들의 상상력을 자극하기 위해 여러 가지 다양한 것들을 보여줄 뿐입니다. 여러분들이 "아 이런 방법도 있구나!"라고 느낄 수 있다면 여기서 제가 해야 할 일은 다한 셈입니다.          




void free(void *) ;     


사실 기회가 된다면 제가 하루 종일 free() 함수의 중요성을 얘기해도 부족할 정도입니다. 제게 능력이 있어 할 수만 있다면 하늘의 별을 다 따다가 여기에 붙여도 좋을 정도로 중요한 함수입니다. 심지어 free()를 잊으면 우리가 free해 질 수 없습니다. 물론 C++의 smart pointer나 Java의 garbage collector 등이 등장하면서 현재는 free() 함수의 필요성이 사라지고 있는 것처럼 보입니다만 생각을 조금 바꿔 도대체 왜 그런 것들이 나오게 되었을까 고민해 본다면 결론은 역시 free() 함수가 중요하기 때문이라는 것을 알 수 있을 겁니다. 프로그램은 메모리에서 실행되기 때문에 메모리가 부족하게 되면 더 이상 프로그램을 실행할 수 없게 되며, 결국에는 급기야 기기를 껐다 켜야 하는 사태가 발생하게 됩니다. 그래서 무엇 보다 중요한 것이 바로 록 삼총사로 할당한 메모리 공간에 대한 적절한 메모리 해제라는 것입니다. 더욱 안타까운 사실은 차라리 free()를 안 했을 때마다 에러가 팍팍 터져줬음 좋겠는데 free()함수를 호출하지 않아도 프로그램은 아주 잘 동작한다는 것입니다. 물론 잘 동작하는 게 아니라 잘 동작하는 것처럼 보이기 때문에 더 큰 문제입니다.


참으로 신기한 것은 오히려 free() 함수를 호출했더니 실행 에러가 나는 경우입니다. 그 때마다 free() 함수를 아무리 들여다 봐도 잘못될 게 전혀 없어 보입니다. 그래서 고민 또 고민하다가 free()함수를 감 쪽같이 지워 버립니다. 그러면 신기하게도 에러가 사라집니다. 하지만 그걸 좋아해서 될 일이 아닙니다. 지금부터 free() 함수의 모든 진실을 파헤쳐 보겠습니다.     



free() 함수와 관련되어 발생하는 에러는 크게 두 가지 입니다. free() 함수를 호출하지 않아서 사용하지 못하는 메모리가 누적되어 더 이상 프로그램이 실행되지 못하는 무시무시한 사태와 free() 함수를 호출했을 때 segmentation fault가 발생하는 경우입니다.


일단 저는 첫 번째 문제를 해결하기 위해 항상 록 삼총사를 부를 때 마다 옆에 함께 free()를 적어 두고 있습니다. 나중에 소스를 살피다가 free() 함수를 제대로 호출하고 있는지 확인하기 위해서입니다. 제대로 할 자신이 있다면 저와 같이 할 필요는 없습니다. 저는 제 자신을 믿지 못하기 때문에 이렇게라도 해야겠습니다. (하하하~)


free() 함수를 부를 때 에러가 나는 이유는 실제 잡은 메모리 양 보다 더 많은 공간을 사용했기 때문입니다. 열 개를 잡고 스무 개를 사용했다면 몇 개를 해제해야 할까요? 음 글쎄요~ 저도 궁금합니다. 그래서 이럴 때는 메모리 인덱싱 하는 부분을 체크하거나 좀더 넉넉하게 메모리를 할당하면 대부분 해결됩니다. 사용량을 정확하게 체크해 보니 이상이 없다면 free() 함수를 부를 때 에러가 나는 다른 이유는 이미 해제된 공간의 주소를 다시 free() 함수의 인자로 넘겼을 때 입니다. 저 같은 경우 free()에 강박관념이 걸려있다고 할 정도로 거의 free() 함수 호출에 목숨을 걸고 있습니다. 그런데 바로 이럴 때 이미 해제 한 것을 다시 또 해제 하려는 사태가 발생합니다. 게다가 저도 나이를 먹으니 이젠 자꾸 깜박깜박 합니다. 앞에서 분명히 했는데 안 한 것 같고 막 그럽니다. 이미 해제한 것을 다시 또 해제한다고라고라야? 쉽게 말하면 그것은 메모리를 두 번 죽이는 일입니다. 그래서 저는 이 문제는 이렇게 해결합니다.     



#define FREE(pointer) ( free(pointer) , (pointer) = NULL )     


이런 매크로 선언을 사용하면 free() 함수를 호출하여 메모리를 해제한 후에 그곳의 주소 값을 알고 있는 변수에 널 포인터를 대입합니다. 그러면 나중에 그곳을 다시 참조하거나 할 수 없습니다. 그리고 이미 널 포인터가 들어 있기 때문에 나도 모르게 두 번 free() 함수를 호출하더라도 처음엔 제대로 동작하고 나중엔 아무 일도 하지 않습니다. 널 포인터를 인자로 넣으면 free() 함수는 아무 일도 안 한다고 표준에 나와 있습니다. 메모리를 해제하는 일은 실제 그 곳의 데이터를 정리하거나 변수에 들어 있는 주소 값을 지우는 일이 결코 아닙니다. 단지 OS(운영체제)에게 "이 주소의 영역을 이제는 다른 용도로 사용해도 좋습니다. 그 동안 잘 썼습니다." 라고 인사하는 정도의 의미입니다. 실제로 free() 함수를 호출하기 전이나 부르고 난 다음이나 포인터 변수의 주소 값은 변경 없이 동일합니다. 심지어 다시 참조도 가능하고 그곳의 데이터도 그냥 그대로 살아(?) 있기도 합니다. 그래서 그러한 문제를 원천 봉쇄하고 있는 이 매크로는 백 만 불짜리 매크로라 불립니다.          



char strdup(const char *) ;     


우리는 앞에서 문자열을 공부하면서 strdup()에 대해 잠깐 소개를 했습니다. strcpy()를 사용하다 보면 항상 문제가 되는 것이 포인터 문자열의 경우 입니다. 포인터 문자열은 메모리 할당이 되어 있지 않으면 치명적인 에러가 발생할 수도 있습니다. 대부분 프로그램을 작성하는 동안에는 입력 받을 문자열의 사이즈를 알 수 없습니다. 그래서 사용자 버퍼를 만들고 우선 그곳에 입력 받고 입력 받은 만큼의 메모리를 동적 할당한 후에 입력된 문자열을 복사 하게 됩니다. 이 작업을 한번에 해주는 것이 바로 strdup() 함수입니다. 표준 함수가 아니지만 거의 대부분의 컴파일러가 이 함수를 지원하고 있습니다. 그렇지 않다면 우리가 직접 만들어 사용해야 합니다. 이가 없으면 잇몸으로라도 살아야 하지 않겠습니까?          



#include <stdio.h>

#include <stdlib.h>

// #include <string.h>     

/*

* copyleft (l) 2006 - 2017 programmed by Sinclair

*/     

// prototypes

extern int strlen2(const register char *) ;

extern int strcpy2(register char * , const register char *) ;

extern int getMemory2(void * , size_t) ;

#define FREE(p) ( free(p) , (p)=NULL )     

char * strdup2(const char *) ;          

int main() {        

    char * names[5] ; // 실제 입력 받아야 할 문자열

    char buffer[80] ;  // 이름 길이가 다른 문제를 해결하기 위한 사용자버퍼

    // char names2[5][80] ;     

    int i ;     

    for ( i = sizeof names / sizeof * names ; i > 0 ; )     {

        // scanf("%s" , names[--i]) ;

        // gets(names[--i]) ;     

        fgets(buffer , sizeof buffer , stdin) ;          

        // names[--i] = buffer ;

        // strcpy(names[--i] , buffer) ;     

        if( !(names[--i] = strdup2(buffer)) ) { // FREE(names[i])

            register int j;

            for( j = sizeof names / sizeof * names ; j > i ; ) {

                FREE(names[--j]) ;

            } // end inner for

            return -1 ;

        } // end if     

        // fgets(names2[i] , sizeof names2[--i] , stdin) ;     

        /*     

        // 메모리 때려잡고 문자열 복사하고~

        if( getMemory2(&names[--i] , strlen2(buffer)+1) ) {

            // FREE(names[i]) ;

            register int j;

            for( j = sizeof names / sizeof * names ; j > i ; ) {

                FREE(names[--j]) ;

            } // end inner for

            return -1 ;

        } // end if

        strcpy2(names[i] , buffer) ;     

        */     

    }// end outer for                  

    for( i = sizeof names / sizeof * names ; i > 0 ; )     {

        printf("%s[%d]: %s\n" , "names" , i , names[--i]) ;

        FREE(names[i]) ;

    } // end for            

    return 0 ;     

} // end main()          


char * strdup2(const char * s) {     

    char * p ;        

    if(getMemory2(&p , strlen2(s)+1)) {

        return (void*)0 ;

    } // end if     

    strcpy2(p , s) ;     

    return p ;     

} // end strdup2()                 




그밖에 나머지 메모리 관리 함수들은 다음과 같습니다. 모두 <string.h>에 포함되어 있습니다. 록 삼총사가 있는 <stdlib.h>가 아니라는 것을 확실히 기억해야 합니다.문자열 관리함수와 거의 같은 용도입니다. 그러나 널 문자로 길이를 알 수 있는 문자열의 경우 대부분 사이즈 없이 사용하지만 단순히 연속된 바이트의 나열인 메모리의 경우 사이즈를 명확하게 지정해줘야 합니다. 모든 함수의 처리 단위는 바이트입니다.          


void *memchr(const void * , int , size_t);

int memcmp(const void * , const void * , size_t);

void *memcpy(void * , const void * , size_t);

void *memmove(void * , const void * , size_t);

void *memset(void * , int , size_t);     



memchr()은 첫 번째 인자의 주소 값으로부터 세 번째 인자의 지정 사이즈만큼의 공간에서 두 번째 인자의 문자(바이트 코드 값)를 찾아서 그곳의 주소 값을 리턴 합니다. 찾을 수 없다면 널 포인터를 리턴 합니다.

memcmp() 함수는 strncmp() 함수처럼 지정 사이즈만큼의 메모리 공간에 있는 코드 값들을 바이트 단위로 서로 뺍니다. 결과가 0 이면 같은 것입니다.

memset() 함수는 첫 번째 인자의 주소 값으로부터 세 번째 인자의 지정 사이즈만큼의 메모리 공간을 지정 문자(바이트 코드 값)으로 모두 변경하는 함수입니다. 디폴트 값으로 초기화 할 때 주로 많이 사용합니다.

끝으로 memcpy() 함수와 memmove() 함수는 거의 같은 일을 합니다. 현재 대부분의 컴파일러에서 두 함수는 동일한 동작을 하고 있습니다. 하지만 표준에 의하면 memcpy() 함수는 메모리가 겹치는 부분에 대한 동작이 정의되어 있지 않기 때문에 겹치는 경우 그 데이터 값이 보장되지 않을 수도 있습니다. 하지만 memmove() 함수는 메모리 영역이 겹치는 경우에 대해 반드시 오버랩 되지 않고 그 데이터 값이 보장 되어야 합니다.     


int array[19] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 0 } ;



memcpy(array , array+5 , sizeof(array) ) ;

// 이렇게 동작할 가능성이 높다는 것입니다. 이렇게 동작한다는 게 절대로 아닙니다.          



memmove(array , array+5 , sizeof(array) ) ;

// 반드시 이렇게 동작해야 합니다.        



앞에서 우리는 call by value와 call by reference를 공부하면서 swap() 함수를 작성했습니다. 그 때 작성한 swap() 함수도 매우 멋지지만 정수형 데이터만 교환 가능하다는 문제가 있습니다. 그래서 모든 타입의 데이터를 교환할 수 있도록 범용 swap() 함수를 작성해 보도록 하겠습니다.               



#include <stdio.h>

#include <stdlib.h>     

/*

* copyleft (l) 2006 - 2017 programmed by Sinclair

*/     

// prototypes

void swap1st(void * , void * , char) ;

int swap (void * , void * , size_t) ;     

#define FREE(p) ( free(p) , (p)=NULL )          

void swap1st(void * a , void * b , char type) {     

    int temp;

    long templ;

    float tempf;

    double tempd;

    char tempc;     

    switch (type) {

        case 'i' :

            temp = * ((int *)a) ;

            * ((int *)a) = * ((int *)b) ;

            * ((int *)b) = temp;

            break;

        case 'l' :

            templ = * ((long *)a) ;

            * ((long *)a) = * ((long *)b) ;

            * ((long *)b) = templ;

            break;

        case 'f' :

            tempf = * ((float *)a) ;

            * ((float *)a) = * ((float *)b) ;

            * ((float *)b) = tempf;

            break;

        case 'd' :

            tempd = * ((double *)a) ;

            * ((double *)a) = * ((double *)b) ;

            * ((double *)b) = tempd;

            break;

        case 'c' :

            tempc = * ((char *)a) ;

            * ((char *)a) = * ((char *)b) ;

            * ((char *)b) = tempc;

            break;

    }    // end of switch     

// 다른 타입이나 새로운 구조체 타입에 대해서는 다시 작성하여 컴파일 해야 합니다.     

} // end swap1st()          


int swap(void * first , void * second , size_t size) {        

    void * temp ;        

    if( !(temp = (void*) malloc( size )) ) { // FREE(temp)

          return -1 ;

    } // end if            

    memcpy(temp , first , size) ;

    memcpy(first , second , size) ;

    memcpy(second , temp , size) ;        

    FREE(temp) ; // 이거 아주 중요합니다.     

    return 0 ;     

} // end swap()



               

어때요? 정말 멋지지 않습니까? 이제 구조체를 비롯한 이 세상 모든 타입의 데이터를 타입과 상관없이 교환할 수 있는 멋진 함수가 완성되었습니다.     

여기에서 동적 할당에 대한 모든 것을 이야기했습니다.


마지막으로 한번 더 부탁할 것은 사이즈가 일정하고 실행 중에 변경되는 것이 없다면 정적 할당을 하는 것이 보다 나은 해법이 될 수 있습니다. 정적 할당 한 그것을 수식에서는 포인터처럼 사용하면 됩니다.

단지 있어 보이려고 동적 할당을 사용한다면 그것은 좋은 해결책이라 말할 수 없습니다. 노파심에 다시 한번 부탁합니다. 모든 필살기의 마지막 주의 사항은 반드시 필요한 곳에 적절히 사용하는 것임을 기억하길 바랍니다. 과유불급(過猶不及)입니다.     


이번 글타래를 마치기 전에 여러분들 스스로 이차방정식의 근을 구하는 프로그램의 getResult() 함수에서 두 근을 동적 할당을 사용하여 두 개의 문자열로 만들어 주는 함수로 바꿔 보시길 바랍니다. 이렇게 문자열로 만들어 놓으면 나중에 네트워크를 통해 메시지 전달도 가능하고 GUI 화면에 적용하면 화면의 적당한 위치에 뿌려줄 수도 있습니다.
절대로 정답은 없습니다. 두려워하지 말고 작성해 보길 바랍니다.

제가 다 만들어 보여주고 싶지만 여러분들의 상상력이 자꾸 줄어들고 있습니다. 그러면 안되지 않겠습니까? 혹시라도 나중에 결과가 궁금하신 분들은 Shalom.Sinclair@gmail.com으로 메일을 보내주시면 답장하도록 하겠습니다. 아싸~    









100 - 1 = 0




어느 음식점에 들어가면 이렇게 적혀 있다고 합니다. 백 번 잘해도 한 번 못하면 아무것도 아니라는 말이겠지요. 손님 한 사람, 한 사람이 들어와서 음식을 먹고 나갈 때까지 최선의 서비스를 제공하겠다는 의지의 표현이겠지요.

제가 알기에도 분명히 백 가지 선행을 수포로 돌아가게 만드는 한 가지 행동이 있습니다. 프로그램에서도 마찬가지 입니다. 아무리 멋지고 좋은 프로그램도 갑자기 꺼지거나, 에러 다이얼로그 창에 "오류 보고 하시겠습니까?" 따위 메시지가 떠버리면 말짱 도루묵입니다. 고객은 그 동안 잘 사용해 왔던 것을 기억하고 고마워하며 그것을 참지 않습니다. 그 프로그램에 있는 단 한 개의 버그였을지 몰라도 고객의 입장에서는 전체가 다 버그로 보이는 순간입니다. 게다가 중요한(?) 채팅 중이었다거나 중요한 데이터를 처리 중이었다면 사태는 더욱 심각해집니다.


100 - 1 = 0 제가 언제나 프로그래밍을 가르칠 때 마다 하는 이야기 입니다. 프로그램의 어떤 기능이든 완벽할 수 없다면 차라리 없는 게 더 낫습니다.


그런데 어느 날 이 수식 100 - 1 = 0 이 새롭게 다가왔습니다. 백 가지 잘못을 모두 덮어 주는 한 가지 행동도 있다는 것입니다.


어렵죠? 그래서 세상에서 가장 어려운 일중 하나가 바로 사람의 마음을 얻는 일입니다.




때때로 우리가 이미 잘 알고 있는 지식들이 문제 해결을 방해할 수 있다는 것입니다. 파리와 벌을 빈 병을 가로로 뉘어 놓고 가둔 채 입구 반대 쪽에 빛을 쪼이면서 뚜껑을 열어두어 출구를 찾도록 하는 실험이 있었습니다. 대부분의 파리는 방황하듯이 여기저기 헤매다가 결국 모두 빠져 나옵니다. 그러나 일반적으로 학습능력도 있으며 파리보다 똑똑(?)하다고 알려진 벌들은 빛이 있는 곳으로 모여들 뿐 반대 편의 입구를 찾지 못해 빠져 나오지 못하고 그곳에서 모두 죽는다고 합니다. 이미 벌들은 태양을 중심으로 위치를 찾는 학습으로 그 지식(?)때문에 오히려 반대쪽에 있는 출구를 찾지 못한다는 안타까운 사실입니다. 때때로 정말 아는 게 병이 되기도 하는가 봅니다.











C언어 및 기타 프로그래밍 관련 질문은 오픈 카톡으로

group talk - https://is.gd/yourc

1:1 talk - https://is.gd/aboutc

#Sinclair #씽클레어 #싱클레어 #씽클레어도씨 #씨언어 #씨프로그래밍  #C언어 #Cprogramming #C_Programming #C #Programming #Clanguage #C_Language

매거진의 이전글 Advanced Pointer II
작품 선택
키워드 선택 0 / 3 0
댓글여부
afliean
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari