본문 바로가기

프로그램

포인터와 배열의 애증관계?


저자:
한동훈

닷넷만 한다고 해서 어느덧 닷넷돌이로 알려진 제게, 때는 바야흐로 2004년 여름! "IT Cookbook, C 프로그래밍 입문"에 대한 온라인 강의 요청이 들어왔습니다. 실은, 한빛미디어를 달달 볶아서 "효율적인 DB 설계와 웹 응용프로그램 구축"과 같은 강의를 하자고 제안했는데, 최소 수강 강의(?)라는 기록을 남기게 되어, 미흡하나마 온라인 강의 요청에 응했습니다. 책을 한 권 받아 들고, 책을 검토할 시간을 받고, 표준 C(ANSI C99)라는 흐름에 맞춰 넣어야 할 것과 뺄 것들을 추가하면서 강의를 만들게 되었고, 지금은 "C 프로그래밍 입문"이라는 이름으로 남아있습니다.

그때도 포인터에 대해서 열심히 설명은 했지만, 덕분에 어려운(?) 강의라는 느낌을 주게 되어, 결국은 2연속 "최소 수강 강의"가 되는 기록을 갖게 되었습니다.
여느 C 언어 책들과 마찬가지로 강의를 맡은 책도 C 언어의 포인터에 대해 많은 설명을 할애하고 있었고, 여기에 추가로, 포인터를 해석하는 방법에 대해서 설명했던 기억이 납니다. 어떻게 강의를 했었는지, 어떤 내용을 얘기했는지 조차 기억하지 못하지만, 지금도 이 강의를 수강하는 사람들이 드물게는 나타나고 있습니다. "C 언어 입문 강의"가 아니라 "C 언어 중급 강의"로 이름을 바꿨어야 했다는 담당자님의 질책과 함께 아무것도 모르지만 C 언어를 학습하고자하는 열의를 가진 분들께서 "C 프로그래밍 입문"이라는 말에 현혹되어 수강하신 분들의 고통을 생각하면 고개가 숙여집니다.

온라인 강의도 수강하고, 여러 C 언어 책을 보는 분의 게시판 질문을 봤습니다. 그리고, 친절하게도(?) 한빛미디어에서 답변을 부탁한다는 연락을 받았습니다. 그 이후는 여러분이 질의응답 게시판에서 보는 바와 같습니다. 질문은 학습자의 시각이 느껴지고, C 언어에 대한 새로운 해석이 보이기 때문에 흥미진진하기도 하고, 어려운 질문을 만나면 해결하는 재미도 있기 마련입니다.

그러다가! 결정적인 C 언어의 포인터를 이용한 함수에 대한 글을 봤습니다. 포인터에 대한 오해로 뒤범벅이 된 함수를 보고, 흥분하여 답변을 남겼지만, 이번에는 제대로 설명하는 글을 작성해서 다른 사람들과 나누면 어떨까라는 생각을 했습니다.

제목이 "포인터와 배열의 애증 관계"인 이유는 많은 사람들이 포인터와 배열을 같은 것으로 설명하기 때문입니다. 실은, 서로 다른 것인데 말이죠. 이제, 한 번 살펴볼까요?

1. 문자열 복사 함수

질문의 발단은 문자열 복사함수입니다. 이 함수는 다음과 같습니다.(저는 이 함수의 출처가 무엇인지 모릅니다. 책인지, 강의자료인지, 직접 작성한 코드인지 모릅니다)

char *My_strcpy(char *dest,const char *src)
{
  if(dest==(int)NULL||src==(int)NULL)
  {
    if(*dest!=(int)NULL) *dest=(int)NULL;
    return NULL;
  }

  do
  {
    *dest++ = *src;
  }
  while(*src++!=(int)NULL);
  return dest;
}

이 함수는 포인터에 대한 몇 가지 오해들을 보여주고 있습니다. 이 오해들이 무엇인지 살펴보겠습니다.

1.1 문자열 복사 함수의 유례

문자열 복사 함수는 많은 C 언어 책에서 포인터를 설명하면서 등장합니다. 그 원본은 C 언어를 만든 두 사람이 쓴 책이기 때문에 K&R이라 불리기도 하는 "The C Programming Language, Prentice Hall"의 5장. 129페이지에 설명한 strcpy() 함수이며, 세 가지 버전 중에 최종버전인 세번째 버전이며 다음과 같습니다.

void strcpy( char *s, char *t )
{
    while( *s++ = *t++ );
}

이 strcpy() 함수는 구현상에도 문제가 많고, 보안상의 문제가 있습니다. 게다가, 입력으로 들어온 것을 변경할 수 있기 때문에 strcpy()의 선언도 다음과 같이 변경하였습니다.

void strcpy( char *s, const char *t )

마찬가지로 while 문의 안에 쓰인

*s++ = *t++;

표현도 풀어쓰면 다음과 같습니다.

*s = *t;
s++;
t++;

여기서 사용된 ++은 값을 1 증가시키는 증분연산자라 하며, 사용된 위치에 따라 전위연산자, 후위연산자라 부릅니다. s++와 같이 사용된 경우는 후위연산자이며, 값을 사용한 후에 증가시키는 역할을 합니다. 축약형은 축약형 자체에 많은 의미를 내포하고 있기 때문에 가독성을 떨어뜨리며, 그 자체로도 버그를 내포할 가능성이 높다는 것을 의미합니다.

따라서, K&R의 책에 소개된 것이라고, 거기에 소개된 모든 코드가 옳다고 볼 수 없으며, 이런 악습(?)과도 같은 코드의 사용은 피해야 합니다.

게다가, 현대 컴파일러는 최적화를 잘 수행하기 때문에, 이런 부분은 컴파일러에 맡기는 것이 현명합니다.

다음으로 문제가 되는 부분은 (int)NULL 타입캐스팅입니다. 이는 무의미합니다.

1.2 (int)NULL 캐스팅으로 알아본 NULL

C 언어에서는 문자열 형식이 없습니다. 대신, 문자의 배열을 이용해서 문자열과 같은 처리를 대신하고 있으며, 문자열의 끝을 나타내기 위해 널문자(NULL)를 사용합니다.

널문자는 흔히 0이나 \0으로 알고 있지만 이것 또한 잘못된 것입니다.

널문자의 정의는 컴파일러에 따라 다릅니다. 어떤 컴파일러는

#define NULL 0

과 같이 선언하고 있으며, 어떤 컴파일러는

#define NULL  ((void*)0)

과 같이 선언하고 있습니다. 이는 전혀 다른 의미입니다. 첫번째는 NULL을 0으로 정의하고 있는 것이며, 두번째는 널 포인터로 정의하고 있는 것입니다.
불행히도, ANSI C에서는 NULL의 값을 0으로 정의해야 하는지, 널 포인터로 정의해야 하는지 명시하지 않고 있습니다. 따라서, NULL이 0으로 가정되어 있다고 가정하는 것도 잘못된 것입니다.

1.2.1 0과 \0의 관계

다음 두 정의는 완전히 같은 표현입니다.

#define NULL 0
#define NULL '\0'

C 언어에서는 이것을 int 형으로 해석합니다. 따라서, 문제의 소스 코드에서 (int)NULL을 사용한 것은 NULL을 C 언어에서 int 형으로 해석한다는 사실을 알기 때문일 것입니다. 실제로는, 없어도 되는 코드입니다

널 문자를 표현할 때는 \0을 사용하지만, \0은 문자 정수이기 때문에 실제 정수 0과 동일합니다. 형식이 char가 아니라 int가 됩니다. C에서 "0이라는 상수는 포인터로 취급해야 할 문맥에서는 포인터로 취급"합니다. \0도 실제로는 0이며, 0이라는 상수가 포인터로 취급해야 되는 곳에서는 포인터로 바뀐다는 사실을 생각해보면 \0이 0과 같으며, 같은 방식으로 처리되는 것을 알 수 있습니다.

참고: C의 모태가 된 B 언어는 인터프리터 형식의 언어였습니다. 모든 것을 워드(WORD, 2바이트) 단위로 처리했다고 합니다. 인터프리터 형식의 언어란 쉽게 말해 스크립트 언어와 같으며, 데이터 형식을 갖지 않았습니다. 여기에 데이터 형식을 도입한 것이 C 언어입니다.

char *p = '\0';
char *p2 = 0;
char *p3 = 3;

이런 문장을 두고 gcc -std=c99 -Wall -o null null.c 와 같이 컴파일 해 보기 바랍니다.

첫번째와 두번째는 컴파일이 되지만, 세번째는 경고가 발생합니다. char *p3 = 3에서 문제가 발생하는 이유는 캐스팅없이 정수형에서 포인터형으로 변환했기 때문입니다. 이는 정의되지 않은 것입니다. 이제, 0이라는 상수는 포인터가 필요한 곳에서 자동 변환된다는 것을 알 수 있겠죠?

화두였던 0과 \0의 관계입니다. 이 둘은 같습니다. \0에서 \를 사용한 이유는 문자라는 것을 의미하기 위한 것인데, 문자정수는 결국 int로 변환됩니다. 따라서, 이 둘은 완전히 같으며, \0으로 사용하는 것은 K&R 시절부터 내려오는 관례일 뿐입니다.

1.2.2 NULL 포인터는 0번지를 가리키는가?

포인터에 NULL 값을 넣어서 초기화하는 경우가 있습니다.

char *p = NULL;

우리는 이것을 널 포인터라고 부릅니다.

char *p = 0;

과도 같은 표현으로 볼 수 있습니다.

포인터는 주소만을 관리하기 때문에 널 포인터의 의미는 0번지를 가리키는 포인터라고 해석할 수 있습니다.

미안하게도, 이것은 사실이 아닙니다. 대부분의 환경에서는 널 포인터는 0 번지를 가리키게 하지만, 그렇지 않은 플랫폼도 있습니다. 게다가, 실수형의 경우 모든 비트가 0이어도 그 값은 0이 아닙니다.

널포인터의 값이 0이 아닌 환경에서는 NULL의 값도 0이 아닙니다. 한 가지만 기억하세요.

"NULL 값은 모든 환경에서 0인 것은 아니다."

1.2.3 memset()을 이용한 초기화?

초기화하는 구조체안에 포인터가 포함되어 있을 때, 초기화한 결과 널포인터로 초기화될 것인지, 아닌지는 컴파일러에 따라 다릅니다. 실수형의 경우 모든 비트가 0이라고 해서 0이 아니라는 것을 생각하면 이 부분은 memset()으로 초기화했다고 반드시 널포인터로 초기화되었다고 볼 수 없습니다.

1.3 malloc()과 타입 캐스팅

malloc()과 관련된 오해는 타입 캐스팅입니다. 다음 코드를 봅니다.

struct student *kim;
kim = (struct student *)malloc( sizeof( struct student ) );

malloc()에 대해서는 보통 이와 같은 코드를 사용합니다. C 언어에서는 일반 포인터(void *)은 모든 포인터 형식의 변수에 값을 할당할 수 있습니다. C++에서는 일반 포인터에 값을 할당할 수 없기 때문에 malloc()을 수행한 이후에 반드시 타입 캐스팅을 해야하지만, C 언어에서는 일반 포인터에 직접 값을 할당할 수 있기 때문에 타입 캐스팅이 필요없는 과정입니다. 따라서, C에서는 다음과 같이 고쳐 쓸 수 있습니다.

struct student *kim;
kim = malloc( sizeof( struct student ) );

stdlib.h 헤더 파일을 누락시킨 경우 malloc() 함수의 정의를 찾지 못합니다. C 언어에서는 이처럼 선언이 없는 함수에 대해서는 반환값의 형식을 int 형이라고 가정합니다. 만약, int의 크기와 포인터형의 크기가 다른 환경에서는 프로그램이 망가져 버립니다. 따라서, C 언어에서는 malloc()에 대해서 타입 캐스팅을 사용하지 않습니다. 타입 캐스팅을 사용하면 경고를 제거하는 역할을 하기 때문에 문제를 발견하는 것을 어렵게 만들 수 있습니다.

1.4 *와 []의 관계

보통 []은 배열을 나타내기 위해 사용하며, *은 포인터를 나타내기 위해 사용합니다.

char *str = "Hello";
str[1];       // 'e'
*(str + 1);   // 'e'

포인터는 "Hello"라는 문자열이 저장되어 있는 공간을 가리키는 포인터일 뿐입니다. 그리고, 포인터를 따라가서 위치 연산을 적용한 후에 값을 읽어들이게 되어 있습니다. 보통은 *(str + 1)과 같이 사용합니다. 포인터 str에서 0번째, 1번째, 2번째와 같이 위치를 셉니다. 여기서는 str + 1 이므로 1번째의 주소를 의미합니다. *(str + 1)은 "(str + 1)이 가리키는 주소의 값"을 의미합니다. 그러나, 보통 이렇게 포인터 연산으로 쓰면 코드가 읽기 어렵기 때문에 간단히 축약형을 도입했습니다. 그것이 str[1]입니다. 여기서 []은 "첨자 연산자"이지 "배열 연산자"가 아닙니다.

불행히도, 많은 사람들은 str[1]은 배열로 봤을 때의 요소이고, *(str + 1)은 포인터로 봤을 때의 요소이다라고 설명하고, 둘을 같은 것으로 설명합니다. 그러고 보니, 저는 온라인 강의에서 뭐라고 했는지 기억이 잊어버렸습니다.

1.4.1 배열의 탈을 쓴 포인터로 오해 받다

다음과 같은 코드를 살펴보겠습니다.

int *p;
int array[3];
array[0] = 0;
array[1] = 1;
array[2] = 2;

p = array;
p = &array[0];

여기서 마지막 두 줄이 중요합니다. p = array; 와 같이 사용할 수 있습니다. 이때, C에서는 array를 포인터로 변환합니다. 따라서, 실제 의미는 p = &array[0]; 과 같습니다.

자동 변환해준다고 해서 array를 "배열의 첫 번째를 나타내는 포인터"라고 설명하는 것은 적절하지 않습니다.

C 언어에서 사용되는 식 안에서는 배열을 나타내는 array는 []이 있든, 없든 포인터로만 해석됩니다. array[2]에서 array는 포인터로 읽히며, 결국, *(array + 2)로 해석됩니다. []만 붙으면 배열이다라고 얘기하는 것은 설명하는 입장에서는 쉽지만 정확한 것은 아닙니다.

따라서, 다음은 모두 같은 표현입니다.

array[2];
*(array + 2 );
p[2];
*(p + 2 );

gcc 4.x에서는 gcc -std=c99 -ansi -Wall로 컴파일하고, gcc 3.x에서는 gcc -std=c99 -Wall로 컴파일하면 됩니다. 식 안에서 array를 평가할 때는 포인터로 해석한다는 것에 주의해야 합니다.

1.4.2 사실 전 포인터의 스파이였다고 고백한 []

함수 안에서 배열을 선언할 때는 다음과 같습니다.

char name[] = "Hello, World";

초기화를 함께 하는 선언의 경우에는 맞는 표현입니다. 컴파일시에 할당되는 크기를 알 수 있습니다. 이런 종류를 complete array type이라 합니다.

char name[];
scanf( "%s", name );

이와 같은 표현을 접할 때가 있습니다. 두 줄 모두 잘못된 코드입니다. 메모리 할당도 안했고, 입력받을 수 있는 최대 문자열 길이도 지정하지 않았습니다. "%10s"와 같은 표현을 사용할 수도 있고, sscanf() 등을 사용할 수도 있습니다.

문제의 char name[]을 살펴보겠습니다. 이것은 많은 분들이 배열로 생각하는데, 이것은 포인터입니다. 선언은 배열이지만, C에서는 이 동작에 대해 정의되어 있지 않습니다. 대부분의 컴파일러는 이것을 포인터로 처리하고 있습니다. char name[]과 같이 사용하는 것을 incomplete array type이라고 하며, 커널 소스에서도 너무 많이 사용되었습니다. 현재, gcc 3.3.6 이상의 버전과 gcc 4.x에서는 incomplete array type을 허용하지 않고 있습니다.

error: array type has incomplete element type

리눅스 커널 소스도 2.4의 가장 최신 버전인 2.4.32 커널 소스도 gcc 3.3.5이하 버전에서만 컴파일할 수 있으며, 2.6 커널도 2.6.10 이전 버전은 gcc 3.3.5 이하 버전에서 컴파일이되며, 2.6.11 이후 버전부터 gcc 3.3.6 또는 gcc 4.x에서 컴파일할 수 있게 되었습니다. 이는, 가장 좋은 실력을 가진 커널 프로그래머들조차도 incomplete array type을 남용하고 있었다는 사실이며, 증거입니다.

참고: 각 커널별 소스는 http://kernel.org에서 받을 수 있으며, 데비안에서는 alternatives를 이용해서 다양한 버전의 gcc로 커널 컴파일 테스트를 할 수 있습니다.

1.4.3 함수 선언의 []의 진실은 포인터였어요.

함수에 char str[]과 같이 전달할 수 있습니다. 이는 배열로 사용하고 싶다는 프로그래머의 의지의 표현이지만, 이는 포인터로 해석됩니다.

int main( int argc, char *argv[] )



int main( int argc, char **argv )

와 같습니다.

int func( int list[ 10 ] )



int func( int *list )

와 같습니다.

int func( int list[] )도 같습니다. 모두 포인터로 해석되며 [10] 같은 인수는 무시됩니다.

[]은 첨자 연산자일 뿐이며 배열과 아무 관계가 없습니다.

1.4.4 내가 누구게? 너와 나([ ]과 *)의 숨바꼭질

가장 간단한 테스트입니다.

char *list[];
char **list;
char (*list)[];

어떤 것인지 읽을 수 있나요? 선언에 빈 []은 incomplete array type이며, 포인터로 읽힌다고 했습니다. 따라서, *list[]는 **list로 읽힙니다. 다른 것은 char (*list)[] 뿐입니다.

이해가 어렵다면 다음 예제로 하나씩 살펴봅시다. 첫번째는 *list[]입니다.

#include <stdio.h>

int main(void)
{
  char *list[3];
  list[0] = "World";
  list[1] = "Of";
  list[2] = "Warcraft";

  printf( "%s %s %s\n", list[0], list[1], list[2] );

  return 0;
}

두번째는 char *list[3] 대신에 char **list로 바꾼후에 gcc로 컴파일합니다. 사용한 컴파일러의 버전은 gcc 4.0.2 입니다. gcc -std=c99로 컴파일합니다. -ansi 옵션은 gcc의 버전에 따라 적용되는 ANSI C 표준이 다릅니다.

char *list[]은 array of pointer to char 입니다. 즉, char*로 이뤄진 배열입니다. 각각의 char *는 문자열을 가리킬 수 있습니다. 즉, 어떤 문자열이든 가리킬 수 있으며, 배열은 사각형의 모습이 아니라 지그재그 형태가 됩니다. list[0]이 가리키는 곳은 "World"와 널문자를 포함해서 6 바이트이며, list[1]은 3바이트, list[2]는 9바이트입니다.

포인터가 아니라 char list[][]와 같이 선언된 배열이라면 "World"가 들어간 곳도 9바이트, "Of"가 들어간 곳도 9바이트, "Warcraft"가 들어간 부분도 9바이트가 할당됩니다. World나 Of 이후의 남는 공간은 널 문자로 채워야 합니다.

참고: char list[][]는 설명을 위한 의사 코드지만, 배열 선언의 경우에는 항상 사각형 형태의 공간을 할당한다고 생각할 수 있고, 각 요소가 포인터로 구성된 배열은 지그재그 형태의 공간을 사용한다고 할 수 있습니다.

char (*list)[]는 다소 다릅니다. 이것은 pointer to array of char 입니다. array of char만 해석한다면 char name[10];과 같이 타입에 해당합니다. 이것을 가리키는 포인터입니다.

필요에 따라 이런 종류를 쓰기는 하지만, 상당히 드문 일입니다. 그런데, 어쩐 일인지 포인터를 설명하는 부분에서는 자주 등장합니다. 여러분을 골탕 먹이기 위해서일까요?

#include <stdio.h>

int main(void)
{
  char array[] = "World of warcraft";
  char (*list)[];

  list = &array;

  printf( "%s\n", *list );

  return 0;
}

위 코드는 앞의 코드와는 사뭇 다릅니다. char (*list)[]는 pointer to array of char입니다. 기억하세요!  char array[]는 array of char 입니다. 이 앞에 "pointer to"라는 말을 붙이고 싶을 때는 & 주소 연산자(Address Operator)를 사용합니다. 왜 주소 연산자를 사용하면 "pointer to"라는 말이 붙는가? 그건 포인터가 주소 그 자체이기 때문입니다. &를 통해 주소로 변환되면 그것이 바로 포인터인 셈입니다.

printf()에서는 list를 바로 출력할 수 없습니다. %s 라는 형식 지정자가 요구하는 것은 pointer to char 즉, char *이지, pointer to array of char는 아닙니다. 이제 "pointer to"를 제거하려면 *를 사용합니다. 따라서, *list를 인자로 사용합니다. 왜 *을 사용하면 "pointer to"가 사라지느냐? *는 해당 주소가 가리키는 곳의 값을 가져오는 연산자입니다. 주소를 따라가 값을 가져오면, 가리키는 대상은 더 이상 주소가 아니며 값 자체입니다.

쉽게 말해 "pointer to char"는 char가 있는 곳의 주소를 가리키는 형식입니다. 근데, * 연산자를 사용해서 주소를 따라가 버리면 이미 "pointer to" 연산을 해버린겁니다. 그러면 남는 것은?? "char" 뿐입니다. 따라서, 간단하게는 * 연산자를 붙여서 "pointer to"를 날려버린다고 설명합니다.

위 세 가지 선언들을 보고 있으면, 식 안의 []은 결국 *로 해석되는 데도 불구하고, 읽을 때는 서로 다르게 읽어들이기 때문에 숨바꼭질을 하는 것과 같다고 느낍니다.

2. strcpy() 구현으로 본 라이브러리 작성의 원칙

K&R 책에 소개된 strcpy()가 나쁘다는 이야기도 했고, 질문에서 NULL 값을 검사하는 strcpy에 대해서도 얘기했지만, 실제로는 빈 문자열을 복사하는 경우도 있기 때문에 NULL 값을 검사하는 것도 필요없습니다.

많은 사람들이 함수를 작성할 때, 함수 안에서 모든 예외를 처리해야 한다고 생각하는 것 같습니다. 그러나, 반드시 필요한 예외처리를 제외한 나머지는 처리하지 않는 것이 더 좋습니다. 문제가 생겼을 때, 문제가 발생한 부분이 내가 작성한 코드인지, 라이브러리인지 쉽게 알 수 있기 때문입니다. 또한, 라이브러리의 문제라는 것을 판단할 수 있는 경우, 그에 대한 대책도 쉽게 작성할 수 있습니다. 만약, 라이브러리에서 모든 예외를 꿀꺽 삼켜버렸다면, 문제가 어디서 발생했는지 알기도 어렵고, 자신의 코드만 의심하는 경우도 많습니다.

예를 들어, 현재 gcc에서 구현한 표준 라이브러리에서 strcpy()를 찾아보면 K&R 코드와 마찬가지로 지정된 주소 범위를 검사하는 것 외에는 특별한 예외처리가 없다는 것을 알 수 있습니다.

라이브러리를 작성할 때는 반드시 기본적인 동작만 하게 하고, 나머지 예외처리와 같은 코드의 작성은 라이브러리를 이용하는 개발자에게 맡겨야 합니다.
gcc에서 구현한 표준 라이브러리의 strcpy()는 세 가지 구현을 담고 있습니다. 인라인으로 정의한 경우에 사용하는 매크로 형태의 strcpy(), 문자열 처리를 지원하는 CPU - 인텔 CPU 등은 문자열 처리를 CPU 차원에서 지원합니다. 특별한 것은 아니고, 문자열의 시작 위치와 종료 위치, 길이등을 알려주면 해당 길이만큼을 CPU에서 복사해주는 것입니다 -인 경우에 인라인 어셈블리 형태로 구현한 strcpy(), 문자열 처리를 지원하지 않는 CPU를 위해 K&R 스타일처럼 구현한 strcpy()입니다. 즉, 이 함수는 시스템에 따라 다르게 구현됩니다. 다음은 gcc의 표준 라이브러리 구현 중에 포인터를 이용해서 문자열을 복사하는 strcpy()의 소스 코드입니다.

char *
strcpy (dest, src)
     char *dest;
     const char *src;
{
  reg_char c;
  char *__unbounded s = (char *__unbounded) CHECK_BOUNDS_LOW (src);
  const ptrdiff_t off = CHECK_BOUNDS_LOW (dest) - s - 1;
  size_t n;

  do
    {
      c = *s++;
      s[off] = c;
    }
  while (c != '\0');

  n = s - src;
  (void) CHECK_BOUNDS_HIGH (src + n);
  (void) CHECK_BOUNDS_HIGH (dest + n);

  return dest;
}

닷넷을 비롯한 현대 언어들의 라이브러리, MFC 등은 나름대로의 문자열 처리를 지원하고 있지만, 기본적인 방법은 C와 같습니다.

닷넷에서는 문자열을 위해 String 클래스를 제공하고 있으며, 문자열을 복사하는 String.Copy() 같은 메서드를 제공합니다. 닷넷에서는 String.Copy()를 비롯한 문자열 복사 함수들은 내부 구현 함수인 InternalCopy() 함수를 이용하고 있으며, InternalCopy() 함수는 memcpyimpl() 함수를 이용하고 있습니다. InternalCopy()의 구현은 다음과 같습니다.

  internal unsafe static void InternalCopy(String src, IntPtr dest,int len)
  {
      if (len == 0)
    return;
      fixed(char* charPtr = &src.m_firstChar) {
    byte* srcPtr = (byte*) charPtr;
    byte* dstPtr = (byte*) dest.ToPointer();
    System.IO.__UnmanagedMemoryStream.memcpyimpl(srcPtr, dstPtr, len);
      }
  }

InternalCopy에서 이용하는 memcpyimpl() 함수도 내부 전용 함수이며, 구현은 다음과 같습니다. 아마도, 매우 익숙한 K&R의 strcpy()를 보는 착각이 들 겁니다.

  internal unsafe static void memcpyimpl(byte* src, byte* dest, int len) {

      // Portable naive implementation
      while (len-- > 0)
    *dest++ = *src++;
  }

마치면서

제가 했던 "C 프로그래밍 입문" 온라인 강의는 이런 이야기들을 꽤 많이 했다고 생각했고, 그로 인해 "자네 강의는 입문 강의가 아니라 중급 강의야!"라는 오명 아닌 오명(?)을 들어야 했고, 최소 수강 강의의 자리를 굳건히 지키게 되었습니다. 그러나, 그때도 부족한 부분들이 있었던 것 같고, 이제 시간이 흘러 질문을 받으면서, 이런 부분을 잘못하진 않았는지, 이런 부분의 설명은 미흡하지 않았는지 하는 생각에 시달렸습니다. 제 온라인 강의를 선택해 주신 분들에게 이 글을 바칩니다