여러가지 데이터를 하나로 묶기 위해 구조체가 등장하였습니다. 예를 들면 X 좌표와 Y좌표, 가로값과 세로값 그리고 높이값, 이름과 주소, 전화번호, 나이등 연관있는 데이터들을 하나로 묶어서 관리하면 참 편하겠죠~? 소프트웨어 개발 과정에서는 표현하는 데이터들은 그룹으로 관리를 하게됩니다.

 struct MAN
{
    char name[10];
    int age;
 };

 MAN은 구조체의 이름이고 중괄호 안에있는 문장들은 구조체 MAN의 맴버입니다. 이러한 정의가 이뤄지고 난 다음부터 MAN은 변수의 선언에 사용되는 자료형의 이름으로 인식이 됩니다.

 struct MAN boxbop;

 cf) typedef struct MAN MAN; 이라고 추가로 작성하면
      struct MAN boxbop 대신 단순히 MAN boxbop으로 사용할 수 있다.

 이라고 한다면 자료형이 MAN인 새로운 변수 boxbop이 생성됩니다. boxbop 내부에는 name과 age라는 맴버가 존재하겠죠? 그렇다면 이 내부에 있는 맴버변수에 어떻게 접근을 할까요?  .(dot) 연산자를 사용합니다.

 boxbop.age = 10;
 strcpy(boxbop.name, "boxbop");

 변수명.맴버변수 의 형태로 접근을 하게됩니다^-^
그렇다면 이제 포인터의 개념을 조금 적용하여 살펴보도록 하겠습니다.

 MAN boxbop;
 MAN* boxbopPtr;

 boxbopPtr = &boxbop;

 이번엔 포인터 변수도 등장을 했습니다. 포인터 변수는 boxbop의 주소값을 가지고 있군요~ 그렇다면 포인터 변수를 사용하여 boxbop의 맴버변수에 접근하는 방법은 어떻게 해야될까요..? '->' 연산자를 사용합니다.

 구조체 이름에서 접근하는 방법은

boxbop.name
boxbop.age
boxbop.number

 이렇게 '.(dot)' 연산자를 사용합니다.
반면에 포인터를 이용한 구조체 맴버의 접근은

boxbopPtr->name
boxbopPtr->age
boxbopPtr->number

 위와 같이 나타냅니다. 아래와 같은 방법도 옳은 접근방법입니다.

(*boxbopPtr).name
(*boxbopPtr).age
(*boxbopPtr).number


 왜냐하면 (*boxbopPtr)는 boxbop와 동일하기 때문입니다~

 그렇다면 구조체 변수의 선언과 초기화는 어떻게 이루어질까요? 다른 자료형처럼 선언과 동시에 초기화가 가능합니다. 마치 배열의 초기화 방법과 유사합니다. 초기화할 값을 중괄호 안에 쉼표로 구분 지어 표시하면 됩니다.

 struct __person
{
   char name[10];
   char ID[10];
   unsigned int age;
 };
typedef struct __person person;

int mian(void)
{
   person andrew = {"andrew", "anw1234", 10};
 }

 위 예제와 같이 초기화가 이루어 집니다. 간단하죠?

 사실 구조체의 정의 부분에서 typedef 연산을 사용하는 등 조금은 뭔가 불필요한 느낌이 들고 있습니다. 뭔가 좀 더 간단하게 나타낼 방법은 없을까요? 다음 두 예제는 동일합니다.

  struct __person
{
   char name[10];
   char ID[10];
   unsigned int age;
 };
typedef struct __person person;

==>
        
 typedef struct __person
{
   char name[10];
   char ID[10];
   unsigned int age;
 }person;

 구조체는 struct라는 키워드를 사용해서 정의하는 반면, 공용체는 union이라는 키워드를 사용해서 정의합니다. 외관상 차이점은 그게 다입니다.

  typedef union __number
{
   char str;
   int number;
   double dnumber;
 }num;

 그러나 각각의 변수가 메모리 공간에 할당되는 방식과 접근방식에는 많은 차이가 있습니다. 구조체는 각각의 맴버 변수에 개별적인 공간이 할당되지만 공용체 변수는 맴버 중에서 크기가 가장 큰 맴버의 바이트만큼만 메로리 공간이 할당됩니다. 위에서는 double형이 가장 크므로 8바이트만 할당이 되겠죠~?

 공용체의 유용함은 다양한 접근방식을 제공하는데 있습니다. 8바이트를 할당받지만 8바이트를 char형, int형, double형으로 모두 사용이 가능합니다. 

 사용자로부터 int형 데이터를 하나 입력받아서 입력 받은 정수의 상위, 하위 2바이트씩 나누어 출력하고, 최상위 1바이트와 최하위 1바이트 저장된 값을 출력하는 프로그램을 작성해야된다고 한다면 이럴때 공용체가 아주 유용하게 사용됩니다. 뭔가 자잘하게 나눈다는 느낌 안드나요~?ㅋㅋㅋ 다음 예제를 보시고 대충 그까이꺼 이해해보세요

 typedef struct __updownshort
{
   unsigned short up;
   unsigned short lower;
 }udshort;

 typedef union __number
{
   int i;
   char c[4];
   udshort s;
 }number;

int main (void)
{
   number buf;
   buf.i=13451345;

   buf.s.up;
   buf.s.down;
   buf.c[0];
   buf.c[3];
 
   return 0;
 }

 따로 설명하지는 않겠습니다. 진하게 표시되어 있는 부분이 중요하다는 뜻 입니다^-^ 열거형도 구조체나 공용체와 마찬가지로 자료형을 정의하는 방법입니다.

 enum value
 {
     A = 1, B = 2, C = 3, D = 4
 };

==>

typedef enum __value
{
    A = 1, B = 2, C = 3, D = 4
}value;

 A를 상수 1로 정의합니다. 마찬가지로 B, C, D는 2, 3, 4로 정의되구요~ 열거형은 솔직히 별거 없네요....ㅋㅋㅋ 즉 A, B, C, D라는 이름의 상수를 각각 1, 2, 3, 4로 정의하고, 이 값들을 value형 변수가 저장할 수 있는 값들로 제한합니다.

 구조체, 공용체, 열거형 모두 정의하면서 동시에 변수를 선언하는 것이 가능합니다. 

 struct point
{
   int x;
   int y;
 }p1, p2;   -> 변수 p1, p2가 생성

 enum number

   A, B, C
 }num; -> 변수 num이 생성

 그리고 자료형의 이름도 생략할 수 있습니다. 사실상 열거형은 자료형의 이름을 자주 생략해서 사용하곤 합니다^-^

 struct
{
   int x;
   int y;
 }p1, p2;

 enum

   A, B, C
 }num; 

 p1.x =1; 이나 num = A와 같은 코드를 작성해 볼 수 있겠죠? 오늘은 여기까지 하도록 하겠습니다. C언어가 거의 마무리 되가네요~ 앞으로 1~2장 정도만 복습하면 끝날 듯 싶습니다~~!!!! 으 지겨워 그 다음엔 C++에 대해서 복습할껀대요 다른 과목들도 같이 복습을 병행해 나갈거라 아주 간단간단하게 설명할 것 같습니다ㅠ 이만 마치도록 하겠습니다(--)(__)(--)!!!
   



 

 


 처음에 설명했던 부분 기억나시나요? 실행파일이 컴파일과 링크의 과정을 거쳐서 만들어지는 것으로 설명했습니다. 실제로는 컴파일 이전에 '전처리'라는 과정을 거치게 됩니다. 즉, 소스파일은 선행처리기에 의해 전처리 과정을 한번 거친 후에 컴파일 거치게 됩니다. 사실 선행처리기가 하는 일은 아주 간단합니다. 우리가 작성해 놓은 선행처리 명령문대로 소스코드의 일부를 수정하는 것이 전부입니다.

 지시자   매크로    매크로몸체

 위와 같이 선행처리 명령문은 기본적으로 세 부분으로 나뉩니다. 아래 문장을 예로들어 설명하겠습니다.

 #define A 10

지시자(#define)는 선행처리기에 이어서 등장하는 매크로를 마지막에 등장하는 매크로 몸체로 치환하라는 명령을 내립니다. 즉, A 를 10으로 치환하라는 의미입니다.
 
 ex) #define NAME "boxbop"
      #define AGE 100

 또한 define 매크로는 다음과 같은 형태로도 사용이 가능합니다.

 #define   FUNCTION(X)   X*X

 즉, FUNCTION(123) 이라는 문장이 나올때 123*123 의 형태로 치환하라는 의미입니다. 함수와도 비슷하죠~? 참고로 매크로를 정의할 때는 먼저 정의되어 있는 매크로도 사용이 가능합니다^-^

 그렇다면 매크로 함수의 장점에는 무엇이 있을까요~?

 1. 매크로 함수는 일반 함수에 비해 실행속도 빠르다
 2. 자료형에 따라서 별도로 함수를 정의하지 않아도 된다.

 알다시피 함수가 호출되면 호출된 함수를 위한 스택 메모리의 할당, 실행위치의 이동과 매개변수로의 인자 전달, return문에 의한 값의 반환등 복잡한 절차를 필요로 합니다. 그렇지만 매크로 함수는 이러한 과정이 불필요하다는 것 입니다^^ 하지만 정의하기가 까다롭기 때문에 무분별하게 사용해서는 안됩니다. 함수의 크기, 길이가 짧거나 호출이 되는 빈도수가 높은 함수를 매크로로 정의하면 좋습니다~

 <조건부 컴파일을 위한 매크로>

 매크로 지시자 중에는 특정 조건에 따라 소스코드의 일부를 삽입하거나 삭제할 수 있도록 디자인 된 지시가자 존재합니다.

 [#if ... #endif]

조건부 코드 삽입을 위한 지시자 입니다. #if문 뒤에는 반드시 #endif문이 존재해야 합니다. 예제를 보시면 이해가 빠르실 겁니당~

 #define CODE_A 1
 #define CODE_B 0

int main(void)
{
   int number1, number2;
   number1 = 10;
   number2 = 20;

  #if CODE_A
     number1 += number2;
  #endif

  #if CODE_B
    number1 -= number2;
  #endif

    return 0;
  }

CODE_A의 값이 참이라면 #if ~ #endif 문까지가 코드에 삽입되어 실행되고 그렇지 않다면 실행되지 않습니다. 즉 위의 예제에서는 CODE_A가 1이기 때문에 두개의 피연산자의 덧셈이 실행되고 CODE_B는 0이기 때문에 뺄셈코드는 삭제되어 무시되겠죠~?

 [#ifdef ... #endif]

 위의 지시자도 #if ... #endif 지시자와 비슷합니다. #if는 매크로가 참인지 거짓인지를 기준으로하여 작동하지만 #ifdef는 매크로가 정의되어 있는지 아닌지를 기준으로 동작합니다. 

 #define CODE_A 1
 //#define CODE_B 0  (주석처리)

int main(void)
{
   int number1, number2;
   number1 = 10;
   number2 = 20;

  #ifdef CODE_A
     number1 += number2;
  #endif

  #ifdef CODE_B
    number1 -= number2;
  #endif

    return 0;
  }

CODE_B를 주석처리했기 때문에 정의되어있지 않다고 판단하여 두 피연산자의 뺄셈은 생략이 되겠죠~? 사실상 코드에서 그냥 소멸되어 버립니다. 반면 CODE_A는 정의되어있기 때문에 덧셈코드는 정상적으로 작동하구요~

 [#ifndef ... #endif]

 위의 매크로는 정의되어있지 않!!!았다면 이라는 뜻입니다. #ifdef와는 반대의 개념이죠? 따로 설명하지는 않겠습니다~

 [#else의 삽입]

#if, #ifdef, #ifndef에서 모두 #else를 사용할 수 있습니다.

 #if CODE_A == 2
  .....
 #else
  .....
 #endif

 이런식으로 기존의 if문과 동일하게 사용할 수 있습니다. 그리고 if문에는 esle if문이 여러개 존재할 수 있죠? else if 역할을 하는 매크로도 존재합니다.

 [#elif]

 위 매크로가 기존의 else if 역할을 합니다. 당연히 여러번 사용할 수 있구요~

 #if CODE_A == 1
   .....
 #elif CODE_A == 0
   .....
 #elif CODE_B == 1
   .....
 #else
   .....
 #endif

그러나 #if와 #ifdef에는 한계가 존재합니다. 예를들어 매크로 NUMBER의 값이 5이고, 매크로 CODE가 정의되어 있으며, 매크로 COUNT의 값이 1이 아닌 경우에 어떤 문자열을 출력한다고 가정해봅시다~

 #define NUMBER 5
 #define CODE
 #define COUNT 2

int main(void)
{
#ifdef CODE
   #if COUNT != 1 && NUMBER == 5
          .....
   #endif
#endif

   return 0;
}

위와 같이 복잡하게 됩니다. 그렇다면 매크로의 참, 거짓과 정의의 유무를 한줄로 표현할 수 있는 것이 defined 연산자 입니다.

 [#if와 함께 사용할 수 있는 defined 연산자]

#if defined(CODE)
  .......
#endif

 defined 연산자는 괄호안의 매크로가 존재하는지 존재하지 않는지 판단합니다. 때문에 좀 전에 복잡했던 예제를 다음과 같이 표현할 수 있습니다.

#if defined(CODE) && COUNT != 1 && NUMBER ==5
  .....
#endif

 보기에도 좀 더 간단해 졌죠~? defined처럼 유용한 매크로 연산자가 존재합니다.

 [매크로 # 연산자]

#define   STR(ABC)   #ABC

 ->매개변수 ABC에 전달되는 인자를 문자열 "ABC"로 치환한다

 매크로 ## 연산자

 #define   MIXNUMBER(A, B, C)   A ## B ## C

 ->매개변수 A, B, C에 전잘되는 인자를 ABC 하나로 만들어준다

 만약 12, 23, 22를 전달하면 122322라는 새로운 수가 만들어집니다.
단순히 A ## B ## C를 ABC나 A B C로 사용해버리면 의도하지 않은 결과를 초례합니다~

 [그외 정의되어 있는 매크로들]

 __FILE__  :  현재 소스코드의 파일명을 나타내는 문자열
 __TIME__ :  선행처리 시작을 "시:분:초" 의 형태로 나타내는 문자열
 __DATE__ : 선행처리 날짜를 "월:일:년" 의 형태로 나타내는 문자열
 __LINE__ : 현재 소스코드의 행 번호를 나타내는 상수

 
ex)  printf("현재 행 : %d ", __LINE__);
        printf("파일 명 : %s ", __FILE__);

 [행 번호와 파일 이름을 지정하는 #line]

 단순히 행의 기준을 사용자가 원하는 기준으로 정하고, 전체경로를 포함한 파일의 이름이 아닌, 그냥 파일의 이름만 출력을 할 수 있도록 합니다.

 ex) #line  0
        ->현재 행 번호를 0으로 설정한다.
       #line  1  "test.c"
        ->현재 행 번호를 1으로 설정하고, 파일이름을 test.c로 설정한다.



 

 


[문자열 함수]

1.
int puts(const char* s);
성공시 0이아닌 값을, 실패 시 EOF 반환
ex) puts("hello boxbop!");

2.
int putchar(int c);
성공 시 출력된 문자 정보를, 실패 시 EOF 반환
ex) putchar('A');

cf)EOF는 End Of File의 약자로 상수 값은 -1입니다. 파일의 끝을 표시하기 위해 정의된 상수입니다.

3.
char* gets(char* s);
성공 시 매개변수로 전달된 값을, 실패 시 NULL 포인터 반환
ex) char str[10];
      gets(str);
이함수를 호출하면서 메모리의 주소 값을 인자로 전달하면, 키보드로부터 입력되는 문자열이, 전달된 주소의 메모리에 저장됩니다.

4.
int getchar(void);
성공 시 입력된 문자를, 실패시 EOF 반환
ex) int ch;
      ch = getchar();

[문자열 컨트롤 함수]

1.
size_t strlen(const char* s);
문자열의 길이 정보 반환

2.
char* strcpy(char* dest, const char* src);
char* strncpy(char* dest, const char* src, size_t n);
->문자열을 배열에 복사합니다.
ex) char src[10]="hi boxbop";
      char dest[10];
      strcpy(dest, src);

3.
char* strcat(char* dest, const char* src);
char* strncat(char* dest, const char* src, size_t n);
첫 번째 매개변수에 전달된 주소 값 반환
->문자열 뒤에 문자열을 복사

4.
int strcmp(const char* s1, const char* s2);
int strncmp(const char* s1, const char* s2, size_t n);
두 문자열이 동일하면 0, 동일하지 않으면 0이 아닌 값 반환
->두 문자열이 서로 동일한지 비교

[문자열 정보 함수]

1.
char* strchr(const char* s, int c);
char* strrchr(const char* s, int c);
문자를 찾을 시 해당 문자의 포인터를, 못 찾을 시 NULL 포인터 반환
->문자열에서 특정 문자가 등장하는 위치를 찾는다.
strchr은 처음 등장하는 위치를, strrchr은 마지막 위치를 찾는다.

2.
char* strstr(const char* src, const char* sub);
문자열을 찾을 시 해당 문자열의 시작 주소를, 못 찾을시 NULL 포인터를 반환
->문자열 안에 특정 문자열이 존재하는지 확인

3.
char* strtok(char* str, const char* set);
다음 번 토큰의 주소 값을 반환하며, 반환할 토큰이 없으면 NULL포인터 반환
->기준을 기준으로 문자열을 나눌경우 사용
ex) char str[] = "aaa-bbb";
      char* tok;
      tok = strtok(str,"-_!"); -, _, ! 을 기준으로 사용

      while(tok != NULL)
  {
     printf("토큰 : %s ", tok);
     tok=strtok(NULL, "-_!");
   }

3번째 줄을 보시면 문자열 str은 기호 -, _, !을 기준으로 토큰이 나뉩니다. 그리고 이렇게 함수가 호출이 되고 나면, 첫 번째 토큰의 주소 값이 반환됩니다. while문 내에 있는 strtok함수를 보면 두 번째 토큰의 정보를 얻기위해 다음과 같은 형태로 함수를 호출하고 있습니다. 첫 번째인자로는 NULL을 전달하여 3번째 줄에서 전달한 str의 정보를 함수 내에서 유지합니다. 두 번째 인자인 기준은 동일합니다.

[문자열 변환 함수]

double atof (const char* str);
int atoi (const char* str);
long atol (const char* str);
long long atoll (const char* str);
->각각 double, int long, long long형 데이터로 변환합니다.
ex) int num;
      num = atoi("123");


[sptrinf & sscanf]

1.
int sprintf(char* restrict s, const char* restrict format,...);
성공 시 저장된 문자열의 길이, 실패 시 EOF 반환
->문자열 배열에 저장한다.

ex)
int num1 = 1;
char str[] = "hello";
float num2 = 1.23;

char strBuf[20];
sptrinf(strBuf,"%d %s %f", num1, str, num2);
puts(strBuf);

2.
int sscanf(char* restrict s, const char* restrict format,...);
성공 시 읽어 들인 데이터의 개수, 실패 시 EOF 반환
->문자열 배열로부터 데이터를 추출한다.

ex)
int num1;
char str[10];
float num2;

sscanf("hello 1 1.23", "%s %d %f", str, &num1, &num2);


 


 포인터와 관련이 있는 내용들을 설명하겠습니다. 여기서 한장자라고 하면 제한을 걸때 사용되는 키워드라고 할 수 있습니다.

 포인터의 const 선언
const는 변수를 상수화 하는 용도로 사용이 됩니다. 이번에는 포인터에 적용해보도록 하겠습니다.

 int number =10;
 int* ptr = &number;

 여기서 const가 들어갈 수 있는 자리는 2군데 있습니다.

const int* const ptr = &num;

포인터 선언 앞에 사용될 수 있고, 포인터 변수의 이름 앞에 삽입될 수도 있습니다. 두 군데 모두 사용할 수도 있습니다.

 int number = 10;
 const int* ptr = &number;

이렇게 사용이 된다면 포인터 ptr은 가리키는 대상에 대한 값의 변경이 허용되지 않습니다. 예를들어 *ptr=20; 이런 문장이 컴파일 에러를 발생키기죠. 즉, 포인터를 이용한 값의 변경을 허용하지 않겠다는 뜻이 됩니다.

 int number = 10;
 int* const ptr = &number;

 이번에는 포인터 변수의 이름 앞에 사용이 되었습니다. 이 선언은 포인터 변수 ptr을 상수화 시킨다는 의미입니다. 즉, 포인터 변수 ptr에 저장된 값의 변경이 불가능 하다는 뜻이 됩니다. 때문에 이러한 선언을 하게되면 포인터 ptr은 끝까지 변수 num만 가리키게 됩니다.

 결론적으로 첫 번째 선언은 ptr이 가리키는 변수에 저장된 값을 변경할 수 없고, 두 번재 선언은 ptr 자체가 number의 주소값으로 상수화 되어 변경할 수 없다는 의미입니다.

 volatile 과 restrict 키워드는 간단하게 넘어가겠습니다.
volatile 키워드는 변수의 선언 앞에 사용됩니다. 해당 변수와 관련해서는 코드 최적화를 수행하면 안된다는 의미입니다. restrict은 그 반대로 최적화를 우도하는 선언입니다. 포인터 선언에서의 restrict 키워드는 이 포인터가 가리키는 메모리 공간은 이 포인터만으로 접근이 가능하다는 것을 의미합니다. 때문에 해당 포인터가 가리키는 메모리 영역의 접근 연산은 캐쉬 메모리를 기반으로 최고의 성능을 낼 수 있도록 코드가 최적화됩니다.

 다음은 메모리 컨트롤 관련 함수입니다.

 memmove : 어떠한 경우에도 사용할 수 있는 메모리 복사 함수
cf) void* memmove(void* dest, const void* src, sizt_t len);
->매개변수 src로 전달된 값을 시작주소로 하여 len바이트를 읽어 들여서, 매개변수 dest로 전달된 주소에 복사를한다.

 memcpy : 제한된 상황에서의 메모리 복사
cf) void* memcpy(void* restrict dest, const void* restrict src, size_t len);
->memmove와 기능상 차이는 없다. 그러나 dest와 src가 restrict으로 선언되었다. 따라서 함수가 호출되면서 dest와 src로 전달된 주소 값의 메모리는 각각 dest와 src로만 접근이 가능해야 한다.(원본대상과 복사본이 겹치는 경우 사용할 수 없다)

 


 포인터의 개념은 충분히 이해가 가셨으면 이번엔 포인터의 활용 방법과 메모리에서의 공간 할당을 공부해보겠습니다.

 call by value & call by reference

포인터를 이용하여 함수 내에서 외부에 있는 변수에 직접 접근이 가능하다.

 void CallByValue(int number)
{
    number++;
 }

 void CallByReference(int* ptr)
 {
     (*ptr)++;
 }

 첫 번째 함수와 두 번째 함수의 차이점은 무엇일까요? 가장 눈에 뛰는건 포인터의 사용입니다. 첫 번째는 매개변수를 인자로 사용하지만 두 번째는 포인터를 사용해 주소 값을 매개 변수로 입력 받습니다.

 첫 번째 함수는 임시로 함수 내부에서 number이라는 매개변수를 생성합니다. 즉 인자로 10이라는 수, 또는 10을 저장하는 매개변수를 넘겨주었다면 그 값을 임시적으로 number라는 int형 변수에 저장해 number를 증가시킵니다. 만약 외부 value = 10; 이라는 값, 인자로 value를 넘겨주었다면 실질적으로 value의 값이 증가하는 것이 아니라 number가 value값을 저장해 number값을 증가시키는 것 입니다. 때문에 함수를 빠져나온 후, value의 값은 그대로 10이 되는거죠.

 반면에 두 번째 함수는 value 의 주소값을 인자로 넘겨주기 때문에 함수 내부에서 뿐 만 아니라 외부까지도 영향력을 갖습니다. value의 주소값 자체를 넘겨 주기때문에 value값을 증가시키죠^-^

 첫 번째 함수형태를 값에 의한 호출(call by value)라고 합니다. 값을 복사해서 넘기는 것 뿐이기 때문이죠~ 두 번째 함수형태는 훔수 내에서 주소 값을 참조하여 주소 값이 가리키는 변수의 값을 1증가시킵니다. 이러한 형태를 참조에 의한 호출(call by reference)라고 합니다. 포인터에 의한 호출이라고 부르기도 하죠~~ 상당히 중요한 개념 입니다.

 typedef 키워드
자료형에 새 이름을 부여하는 키워드 입니다. 포인터 개념에는 조금 벗어나지만 후반부에 소개하는 메모리 관련 함수들의 이해를 위해서 간단하게 살펴보겠습니다.

    typedef       TYPE            NAME;
     키워드       자료형     자료형의 새 이름

예를 들어 보겠습니다~

 typedef int INT; 라고 하면 int라는 자료형과 우리가 새로 정의한 INT라는 자료형이 동일합니다. 때문에 int number 이나 INT number은 동일합니다.
 tyepdef int asdfasdf; 라고 해도 int 와 asdfasdf는 동일하죠 ^-^ 그렇다고 의미 없이 asdfasdf라고 정하지는 말자구요~ㅋㅋㅋ 예를 들어본 것 뿐이니...ㅋ
 typedef unsigned int UINT; 라고하면 unsigned int 형을 UINT로 재 정의 한 것 입니다. 의미는 당연히 동일하구요. unsigned int를 UINT로 줄여 사용하기 때문에 좀 더 편리하기는 하겠죠?
 마찬가지로 포인터형 변수도 typedef 정의가 가능합니다. typedef int* P_INT;라고하면 int* 와 P_INT는 동일하겠죠~?
 배열도 역시나 사용 가능합니다만 조금 주의해서 사용하셔야 됩니다. double array[5];라고 하면 길이가 5인 double형 배열의 선인입니다. 하지만 typedef double array[5];라고 선언하면 길이가 5인 double형 배열의 자료형을 의미하게 됩니다. array가 새로운 자료형의 이름이 되구요^-^ 때문에 array arr; 와 double arr[5]는 완전하게 동일합니다. 즉, 배열 선언 앞에 typedef를 붙여주면 배열의 이름이 typedef에 의해 선언된 자료형의 이름으로 인식됩니다.

 메모리 공간의 동적 할당
여기서 기억하셔야 될 부분이 하나 있습니다. 크게 네 개의 영역으로 가상 메모리가 나뉘는 것을 설명한 바 있습니다.

 코드 영역 : 실행할 프로그램의 코드를 올려 놓을 공간
 데이터 영역 : 프로그램이 종료될 때까지 유지해야 할 데이터를 저장할 공간
 스택 영역 : 일시적으로 잠깐 사용하고 삭제할 데이터의 저장공간
 힙 영역 : 프로그래머가 원하는 형태로 쓸 수 있는 공간

 여기서 우리는 힙 영역에 대한 내용을 공부할 것 입니다. 전역 변수는 데이터 영역에 할당이 되어 프로그램이 종료될 때까지 남아있는 변수, 지역변수는 스택에 할당이 되었다가 해당 변수를 선언한 함수가 종료되면 소멸이 되는 변수입니다. 그러나 이 두가지 특성의 변수로는 충족되지 않는 부분이 있는데, 이 부분은 힙 영역을 통해서 해결해야 합니다.

 int* MakeIntArray (int len, int init)
{
    int arr[len];
    for(int i=0 ; i<len ; i++) arr[i]=init;

    return arr;
 }

 위와 같은 함수가 있습니다. 함수의 return값이 배열이 이름(주소 값) 이기 때문에 함수의 반환형은 int*가 됩니다. 함수 내부에 새로운 배열 arr가 선언되고 활용되었습니다. 그리고 이 배열을 리턴합니다. 여기 까지는 문제가 없겠지요. 그러나 이 함수를 빠져나오고 외부에서 이 함수값을 가지고 사용하는 것이 문제가 됩니다. MakeIntArray(5,0)은 배열의 길이가 5이고 모든 요소의 초기 값을 0으로 초기화 시킨 배열을 반환해야되지만 문제가 있습니다. arr라는 배열은 함수 내에만 존재하기 때문에 함수를 벗어나면 소멸되기 때문이죠. 때문에 int* new_arr = MakeIntArray(5,0);과 같은 문장은 사용할 수 없습니다. 함수가 새로운 배열을 만들어서 반환한 후에 new_arr에 저장해야 되지만 반환 하자마자 배열은 없어지기 때문이죠, 즉 함수내에서 정의되어 있기 때문에 함수를 빠져나오면 소멸됩니다. main함수에서 이 주소 값을 가지고 배열에 접근할 시점이면 이미 배열은 소멸된 상태라~~ 이겁니다!!

 이러한 문제점은 배열을 스택에 할당했다는 것 입니다. 함수를 빠져나간 다음에도 할당된 메모리 공간이 소멸되지 않아야 배열을 만들어서 제공하는 함수로서의 역할을 할 수 있습니다. 이러한 문제점을 해결할 수 있는 메모리 공간이 힙 영역입니다. 우리는 원하는 순간에 힙 영역에 메모리 공간을 할당할 수 있고, 원하는 순간에 할당된 메모리의 공간을 소멸시킬 수 있습니다.

 힙 영역에 메모리 공간을 할당하는 함수 malloc
참고로 이 함수는 헤더파일 stdlib.h에 선언되어 있으므로 호출을 위해서 이 헤더 파일을 반드시 포함해줘야 합니다.

 void* malloc(size_t size);  

 성공 시 할당된 메모리의 주소 값, 실패 시 NULL을 반환합니다. size_t는 typedef에 의해 만들어진 자료형입니다. 단순하게 unsigned int, unsigned long라고 생각하시면 되구요 반환형이 포인터라는 것을 기억해 둡시다~

 malloc(12);라고 선언했다면 힙 영역에 12바이트를 할당했다는 이야기 입니다. 어렵지 않죠~? malloc은 인자를 오직 '크기'만 받습니다. 이것이 무슨 이야기 일까요~? 반환되는 주소 값의 포인터 형을 결정하지 못한다는 이야기 입니다. 단순히 12바이트만 할당할 뿐 길이가 3인 int형 1차원 배열일 수 있고, 길이가 12인 char형 문자열 배열일 수 있습니다. 그러나 malloc은 단순히 공간만 할당할 뿐 주소 값의 포인터 형을 결정하지 못한다는 얘기이죠~

 void* ptr = malloc(sizeof(int));
 *ptr =10;

 이 예제에서는 4바이트 int형 변수를 힙에 할당하고자 하는 의도가 담겨있습니다. int형 변수의 크기를 인자로 전달하면서 malloc함수를 호출하고 있습니다. 그리고 void형 포인터 ptr을 선언해서 반환되는 값을 저장합니다. 그러나 *ptr = 10에서 컴파일 에러가 발생합니다. 앞서 void형 포인터는 포인터가 가리키는 대상에 대한 정보 없이 그냥 주소 값만을 저장하고 있는 변수라고 하였습니다. 그러나 10을 저장하는 순간에, 해당 메모리 공간에 4바이트 정수의 형태로 10을 저장해야 될지, 8바이트 정수의 형태로 10을 저장해야될지, 8바이트 실수의 형태로 10을 저장해야 할지 모르는 겁니다!!! 때문에 void형 포인터는 메모리 참조를 위한 * 연산이 불가능합니다.

 void* vPtr = malloc(sizeof(int));
 int* iPtr = (int*)vPtr;
 *iPtr = 10;

 위 예제는 정상 작동을 합니다. void형 포인터를 int형 포인터로 변환하여 사용하였기 때문입니다. 실제 프로그래밍 코드에서는 다음과 같이 사용합니다.

 int* Ptr = (int*) malloc(sizeof(int));

 이해가 좀 가셨나요~? 그렇다면 이렇게 할당된 메모리 공간을 해제하는 방법을 보도록 하겠습니다. 메모리 공간의 해제는 할당보다는 좀 더 간결합니다.

 void free(void* ptr);

 앞서 할당했던 Ptr의 메모리 공간을 해제하기 위해서는 free(Ptr);이라고 해주면 간단하게 해제됩니다~~~ㅋㅋㅋ아후 힘드네요ㅠㅠㅠㅠ 이번 장은 여기까지 하도록 하겠습니다^-^!!!

 


 새벽인대 잠이 안와서 포스팅이나 하렵니다~~ㅋㅋㅋㅋ 포스팅 하다보면 잠이 오겠죠^-^ 저번 포인터 그리고 포인터  배열 내용에 계속 되는 부분 입니다~

 * : 메모리 참조
 & : 주소 값 반환

 요 2가지 연산자는 저번 시간에 배웠죠~? 복습합시다~! 포인터를 피연산자로 하는 곱셈과 나눗셈은 불가능하지만 덧셈과 뺄셈은 가능합니다. 포인터에서의 덧셈과 뺄셈은 일반적인 산술연산과는 조금 다른 의미를 가지고 있습니다.

 int number = 1;
 int* ptr = &number;

 여기서 ptr의 값을 10진수로 출력 시켜보고 그리고 ptr++ 을 통해서 값을 증가시켜보고 출력시켜보세요. 결과는 어떻게 나올까요? 처음의 값보다 4가 증가됩니다. 즉 ptr에 1을 더하면 1이 증가해야되지만 4가 증가한다는 얘기입니다. 포인터는 주소값입니다!!! 일반적인 산술연산과는 다르죠. 

 int형 포인터 변수의 값을 1증가시키면, int형 변수의 크기인 4가 증가되고,
double형 포인터 변수의 값을 1증가시키면, double형 변수의 크기인 8이 증가됩니다.

 여기서 우리가 알 수 있는 결론은? 어떠한 특정 자료형의 포인터 값을 1증가 및 감소시, 자료형의 바이트 크기만큼 증가 및 감소합니다.

int array[3] = {1, 2, 3};
int* ptr = &array[1];

배열을 선언하고 포인터 변수 ptr에 배열의 2번째 요소의 주소값을 저장했습니다. 이 상태에서 ptr은 array[1]의 주소값을 가지고 있습니다. *ptr은 2번째 요소인 2를 나타내구요. 그럼 ptr++의 연산 결과는 어떻게 될까요~? array[2]의 주소값을 저장하게 됩니다. 반대로 ptr--의 연산결과는 array[0]의 주소값을 가지고 있겠죠~?

 *array 와 array[0]의 값은 같을까요? 다를까요? 결론은 같습니다. 배열의 이름은 포인터이기 때문입니다. 따라서 array[0], array[1], array[2]는 *array, *(array+1), *(array+2)와 동일 합니다. 즉 array[i]와 *(array+i)는 배열의 이름 관점에서 완전히 동일합니다. 물론 포인터의 관점에서두요^-^

 때문에!!! ptr[0], ptr[1], ptr[2]로도 나타낼 수 있습니다. 왜냐하면 배열의 이름 array가 가지고 있는 주소값과 ptr의 주소값은 동일하기 때문에, 그리고 배열의 이름은 포인터기 때문에 가능하다는 겁니다~!!

 그러나 2차원 배열에서는 조금? 달라집니다.
array1[3][2], array2[3][3], array3[3][4] 이렇게 3개의 2차원 배열이 존재한다고 가정해봅시다. array1 과 array1 + 1의 주소값은 얼마나 차이 날까요? 물론 int형 배열이라는 가정하에 말이죠~ 정답은 8만큼입니다. array2 와 array2 + 1은? 12만큼 array3과 array3 + 1 은 16만큼의 차이를 나타냅니다.

 array2를 예로 들어 보겠습니다. 2차원 배열이고 3X3행렬의 모습을 나타내므로 아래와 같이 표현했습니다.
 array2      ->■□□
 array2 + 1 ->■□□
                    array2 + 2 ->■□□

 즉, 1을 더할 때마다 반환되는 주소 값의 크기는 행의 바이트 크기만큼 증가합니다. 배열의 요소가 int형이므로 4, 가로의 길이가 3이므로 3, 각각 곱하면 12가 됩니다. 따라서 12만큼 크기가 증가하겠죠~? 넵 여기까지 입니다~ㅋㅋㅋㅋ

 포인터는 배열과 아주 큰 연관성을 가지고 있기 때문에 그만큼 배열을 자주 언급하게 되네요~ㅋㅋㅋ 다음 장에서는 포인터와 함수의 관계 그리고 메모리의 할당에 대하여 공부하도록 하겠습니다


 

 


 포인터 변수도 변수의 한 형태이기 때문에 선언 시 메모리 영역에 4바이트의 메모리 공간 할당이 이뤄지기 때문에 당연히 포인터 변수 자체애 대한 주소 값도 존재를 하겠죠~?

 int number = 1;
 int* ptr = &number;

 printf("ptr의 저장 값은? %#x", ptr);
 printf("ptr의 주소 값은? %#x", &ptr);

 위의 소스 코드를 출력해보세요~
분명히 ptr의 저장 값과 주소 값은 다릅니다. ptr의 저장 값은 number변수의 주소 값이고 ptr의 주소 값은 말 그대로 포인트 변수 ptr의 주소 값이겠지요? 그렇습니다. 그리고 이 값을 다시 포인터 형 변수에 저장 할 수 있습니다. 즉, 포인터의 포인터(주소값)을 저장할 수 있는 것이죠^-^!!

 int number = 1;
 int* ptr = &number;
 int** dptr = &ptr;

 int** 라는 자료형이 보이시나요? 이것이 포인터의 포인터를 저장할 수 있는 자료형 입니다. 간접 참조 연산자 '*' 가 2개 붙어있는 모습이죠. 즉, 포인터 형이 int*인 변수의 주소 값은 포인터 형이 int**인 변수에 저장해야 합니다.

 dptr은 ptr의 주소값을 나타냅니다. *dptr은 무엇을 나타낼까요? ptr이 가지고 있는 값을 나타냅니다. number의 주소값이 되겠죠? 그렇다면 **dptr은 무엇일까요? number가 가지고 있는 값을 나타냅니다. 1이 되겠죠~? 천천히 잘 생각해 보시길 바랍니다.

 포인터 배열은 포인터 변수로 이뤄진 배열을 의미합니다. 즉 주소값들을 저장할 수 있는 배열이죠^-^

 int* arrayPtr[3];

이러한 코드는 int형 변수의 주소값을 저장할 수 있는 배열입니다. 이렇게 선언된 배열은 총 3개의 주소 값을 저장할 수 있겠네요~ 포인터 배열은 메모리 공간 절약에 있어서도 효율적입니다.

char array[3][9] = {"hello", "boxbop", "world"};
char* array[3] = {"hello", "boxbop", "world"};

 일단 모양은 비슷합니다만 char형 배열은 문자열의 크기를 넉넉하게 잡아줘야 됩니다. 총 27바이트가 할당되었죠. 그렇다고 모든 공간을 사용하지도 않습니다. 여분의 공간이 남아있는거죠. 메모리 상에서는 이 부분이 낭비입니다. 그러나 char* 포인터 형으로 선언을 해주었을때는 얘기가 달라집니다. 일단 문자는 메모리 자체에 우선적으로 저장이되고 주소값이 반환된다고 했죠?

char* array[3] = { 0x11, 0x22, 0x33 }; 이런식으루요 그럼 단순히 그 주소값을 포인터 배열에 저장해주고 있습니다. 즉 메모리 공간의 낭비가 없다는거죠~!
넵!! 사실 별거 없습니다. 단지 개념의 확장일 뿐!!!...... 오늘은 이만 입니다^-^


 

 


 드디어 C언어의 '포인터'에 입문할 차례입니다.
사실 손으로 쓰면 이해하기 쉽도록 그림이라도 그리기 편할텐대 이놈의 귀차니즘이 포토샵을 켜질 않내요...... 나름 개인적인 복습이니까~ 라는 사고방식이 합리화를 시키기는 하지만 뭔가 걸리적 거리기는 합니다.

 포인터란 무엇일까?
결론적으로 포인터는 주소 값을 담고 있는 변수이다.
그렇다면 왜 굳이 포인터라는 어려운 개념을 알아야 할까? 간단합니다. 좀 더 프로그램에 깊숙히 접근해서 보다 더 자유롭게 프로그래밍을 하기 위해서 입니다.

 일반적으로 메모리는 1바이트 블록 단위로 아주 자자자자잘하게 나뉘어 있습니다. 각각의 바이트마다 고유한 주소값을 가지고 있습니다. 즉 0x11번지라고 하면 1바이트, 8개의 비트를 가리키고 있습니다.

 그럼이제 실제 프로그래밍 코드를 살펴봅시다.

 int value = 10;
 printf("변수의 저장위치는? %#x", &value);
 printf("변수 주소값의 크기는? %d",sizeof(&value));

 변수 앞에 '&'연산자를 붙여서 반환된 결과 값을 16진수의 형태로 출력합니다. '%#x' 라는 서식문자 배웠죠~? (
# : 8진수, 16진수 출력 시 각각 0과 0x를, 실수의 경우 소수점 이하 0 출력) 변수 주소값의 크기는 4가 출력됩니다. 실제 주소 값의 크기가 4바이트로 표현된다는 사실을 확인한거죠~

 포인터 변수란 포인터를 저장할 수 있는 변수입니다. 포인터 변수 선언의 규칙을 한번 살펴 봅시다. int형 변수를 선언 할 때 int value; 라고 적습니다. 이때! 이 변수의 주소 값을 저장하기 위한 포인터 변수의 선언은 다음과 같이 합니다.

            
int* value;

 이 뜻은, int형 변수의 주소 값을 저장하는 포인터 변수 value라고 생각하시면 됩니다. 이렇게 선언한 후에는

           
value = &number;

 라고 선언해주시면 number의 주소값이 포인터 변수 value에 저장됩니다.
다른 자료형도 마찬가지 입니다.

 
char* charValue;
 double* doubleValue;

 똑같죠? 근대 중요한 사실이 하나 있습니다. 각각의 자료형을 달리하여 변수의 주소값의 크기를 출력해보세요~ int형이건 char형이건 double형이건 모두 변수의 크기는 4바이트가 출력됩니다. 중요한 사실을 하나 발견했습니다.
'포인터는 상수이건 변수이건 항상 4바이트이다!!!!'라는 사실이죠. 일단은 알고 있으세요~

 변수에는 서로다른 자료형이 있듯이 포인터 변수에도 각각의 자료형이 있습니다. 그러나 이는 변수의 자료형과 포인터의 자료형은 동일합니다. 단지 '*'만 붙여주면 됩니다. char*, int*, long*, float*, double* ..등등 이처럼 포인터에도 포인터 형이 존재하죠~

 이번에는 연산자 '*' 에 대해서 알아보도록 하겠습니다. 이 연산자를 간접 참조 연산자라고 부릅니다. 이항연산자로 사용이 되면 곱셈을 의미합니다. 그러나 단항 연산자로 사용될 경우에는 포인터가 가리키는 메모리 공간의 접근을 의미합니다.

 int num = 10;
 int* ptr;

 ptr = &num;
 printf(" ptr이 가리키는 변수의 값 : %d", *ptr);
 printf(" num에 저장된 값 : %d", num);

 *ptr = 20;
 printf(" ptr이 가리키는 변수의 값 : %d", *ptr);
 printf(" num에 저장된 값 : %d", num);

 위와 같은 예제를 살펴봅시다. printf 문의 출력을 보면 포인터 변수 ptr앞에 연산자 '*'가 붙었습니다. 즉, 포인터 변수 ptr에 저장된 값을 참조하라는 의미입니다. ptr은 변수 num의 주소값을 가지고 있기때문에 이 주소값이 참조하는 값은 num의 값, 10이 되겠죠? *ptr = 20; 이문장은 ptr이 가리키는 변수에 20을 저장하를 의미입니다. 즉, *ptr 과 변수 num은 동일한 의미가 됩니다.

 아까 포인터의 형에 대하여 하다만 얘기를 좀 더 해봅시다. 예를들어
변수 100을 저장하기 위해 포인터 변수를 int형 char형 double형으로 선언하고 위의 예제와 같이 printf문으로 포인터 변수가 참조하는 값을 출력한다고 가정한다고 가정해봅시다. 물론 정수 100이 저장된다는 사실은 동일합니다. 그러나 저장되는 방식!!! 다시한번 ㅋㅋㅋ 저장되는 방식에는 아주아주 큰 차이가 있습니다. 즉, 포인터의 형은 포인터가 가리키는 메모리 공간의 데이터 저장 및 참조 방식을 결정한다. 아주 중요한 사실이니까 꼭 기억해두세요~

  이번에는 포인터의 형 변환에 대한 내용입니다.

 int num = 10;
 char * p =&num;

 이러한 코드는 정상적으로 작동됩니다. 포인터의 형을 암시적으로 변환시켜 주고 있는 모습이죠. 원래 num의 자료형은 int입니다. 때문에 int형 포인터 변수를 선언해주어 주소값을 저장해야되지만 char으로 선언했습니다. 아까 얘기했듯이 포인터 변수 자체는 무조건 4바이트, 형으로 따지면 int형이므로 크기에 따른 문제는 발생하지 않습니다. char형 포인터 변수나 int형 포인터 변수나 4바이트로 크기는 동일하다는 이야기죠. 위의 코드는 int형 변수의 주소값을 char형 포인터에 저장합니다. 때문에 원래 &num이라는 주소값은 int* 형 이지만 char*으로 변환해서 저장합니다. char * p = (char *)&num; 이 코드와 동일하죠 다만 괄호 내의 부분은 생략되어있는 것 뿐입니다. 마찬가지로 변수에 저장되어 있는 값에는 변함이 없으나 메모리 공간의 데이터 저장 및 참조 방식에서는 분명한 차이를 보여주게 됩니다.

 마지막으로 문자열을 참조하는 포인터에 대해서 알아보고 마치도록 하겠습니다. 일단 C언어에서 문자열을 표현하는 방식은 크게 2가지 입니다. 변수 형태로 표현하는 방식과 상수 형태로 표현하는 방식입니다.

 배열을 이용해서 선언하는 문자열은 변수 형태의 문자열이다!
char stringValue[30] = "welcome boxbop";
여기서는 char형 배열을 통해서 문자열이 표현됩니다. 각각의 문자가 각각의 배열에 요소에 저장이 되기때문에 변수 형태의 문자열이라고 합니다.

 
포인터를 이용해서 선언하는 문자열은 상수 형태의 문자열이다!
char* stringValuePtr = "welcome boxbop";
사실 이 코드를 정확하게 이해하기 위해서는 한가지 더 알아야 할 것이 있습니다. 배열로 표현되지 않은 문자열은 상수의 형태로 메모리 공간에 저장이 됩니다. 즉 'welcome boxbop'이라는 문자는 메모리에 저장이되고 문자열의 시작 주소, 즉 문자가 저장된 주소의 값이 반환 됩니다. 그럼 다음과 같겠죠~? char* stringValue = 0x12; 또 한가지! 왜 char* 형을 사용했을까요? int*형도 있고 다른 형들도 많은대? 사실 0x12가 실제로 가리키는 대상은 첫번째 문자인 char형 값인 'w'이므로 char형 포인터에 값을 저장하는 것 입니다^-^

+ Recent posts