1. 구조체 사용의 장점
 간단한 성적 처리 프로그램을 작성한다고 가정했을 때, 학생의 이름, 국어, 영어, 수학, 평균 점수를 저장할 변수가 필요하다.
단순하게 변수를 선언하면 다음과 같다.

 char name[10];
 int korean, english, math;
 double average;
하지만 위의 변수들은 모두 1명의 학생을 위한 것이다. 만약 학생이 2명이라면 또 다른 변수를 선언해야하고 학생이
100명이라면 각 변수를 100개씩이나 선언해야 한다. 이런 불상사를 막을 수 있는 방법이 구조체이다.
구조체는 각 변수들을 하나로 묶어서 관리할 수 있도록 한다.

2. 구조체의 정의
 struct 태그명{
 데이터형 멤버명;
 ...
};

ex)
 struct grade{
 char name[10];
 int korean, english, math;
 double average;
}; //구조체의 정의도 C 문장이므로 마지막에 세미콜론(;)을 빼먹지 말자.

※ Tip
기본 데이터형(primitive data type) : char, short, int, float, double 등
파생 데이터형(derived data type) : 기본형으로부터 만들어낸 배열, 포인터, 구조체, 공용체, 열거체
사용자 정의형(user-defined data type) : 구조체처럼 프로그래머에 의해서 새로 만들어진 데이터형

※ 구조체의 크기
  struct grade{
 char name;
 short no;
 int korean;
 double average;
};
 grade 구조체의 크기는 단순히 멤버들의 크기의 합, char(1byte)+short(2byte)+int(4byte)+double(8byte) = 15byte일까?
무조건 그렇지는 않다. 구조체의 크기는 모든 멤버들의 합보다 크거나 같다. 그 이유는 메모리 정렬(alignment) 때문인데
대부분의 CPU는 메모리 접근을 할 때 1바이트, 2바이트, 4바이트 단위 등으로 접근하기 때문에 단위를 맞춰서 변수를
할당하면 효율적 접근이 가능해진다. 이때 메모리가 정렬되었다고 하는데 C 컴파일러는 효율적 메모리 접근을 위해 실제로
사용되지 않는 데이터 바이트를 삽입하기도 한다. 이것을 패딩(padding)이라고 한다. 이런 패딩으로 인해서 구조체의
크기가 더 커질 수 있는 것이다. 따라서 구조체의 정확한 크기를 구하려면 단순히 멤버들 크기의 합을 구하면 안되고
sizeof 연산자를 사용해야 한다.

3. 구조체 변수의 선언
 struct 태그명 변수명1, 변수명2, ...;
 ex) struct grade student1;
      struct grade student1, student2;
 구조체는 새로운 데이터형을 만드는 것이므로 구조체를 정의하는 것만으로 구조체 변수가 메모리에 할당되지 않는다.
구조체의 멤버들은 구조체 변수가 선언될 때에 비로소 메모리에 할당된다.

struct grade student1;
           name           korean           english               math           average 
           char[20]                      int                             int                                int                        double

                                                                           student1

 위와 같이 메모리에 할당되며 보는 바와 같이 구조체 안에는 여러 변수들이 존재한다. 각 멤버 변수에 접근하려면 멤버
접근 연산자인 .를 이용한다.

 strcpy(student1.name, "홍길동");

 student1.korean = 95;
 student1.english = 100;
 student1.math = 98;
 student1.average = (double) (student1.korean+student1.english+student1.math) / 3;
 
 다른 학생(student2)에 관한 변수가 필요하다면 구조체 변수를 하나 더 선언해주면 된다.
 struct grade student2;
 strcpy(student2.name, "효도르");
 student2.korean = 90;
 student2.english = 95;
 student2.math = 100;
 student2.average = (double) (student2.korean+student2.english+student2.math) / 3;

 구조체 정의와 동시에 구조체 변수를 선언하는 것도 가능하며 이때에는 구조체의 태그명이 생략 가능하지만 되도록
태그명을 지정하는 것이 좋다.
 struct gender{
 int m;
 int f;
 }g1, g2;

 struct {
 int m;
 int f;
 }g1, g2;
 
 구조체 변수도 초기화 하지 않으면 쓰레기 값을 가지므로 초기화하는 것이 좋다.
 struct grade student1 = {"홍길동", 100, 100, 100, 0.0};
 초기값이 멤버의 수보다 적으면 나머지 멤버들은 0으로 초기화 된다.
 struct grade student2 = {"효도르"}; //나머지 멤버는 모두 0으로 초기화 됨.

<예제> grade 구조체의 정의 및 사용
 #include <stdio.h>

struct grade{
 char name[10];
 int korean, english, math;
 double average;
};

int main(void){
struct grade student1 = {"홍길동", 95, 100, 98, 0.0};
struct grade student2 = {"효도르", 90, 95, 100};

student1.average = (double) (student1.korean+student1.english+student1.math) / 3;
student2.average = (double) (student2.korean+student2.english+student2.math) / 3;

printf("이름 : %s, 평균 : %5.2f\n", student1.name, student1.average);
printf("이름 : %s, 평균 : %5.2f\n", student2.name, student2.average);

return 0;
}

4. 구조체 간의 초기화 및 대입
 같은 구조체형의 변수들끼리는 서로 초기화나 대입이 가능하다.
 struct point{
int x;
int y;
};
struct point p1 = {1,2};
struct point p2 = {3,4};
struct point p3 = p1; //p3.x와 p3.y는 각각 p1.x와 p1.y로 초기화 됨. 이것을
멤버 대 멤버 초기화
struct point p4;
p4=p2; //p2.x를 p4.x에 대입하고 p2.y를 p4.y에 대입함. 이것을 멤버 대 멤버 대입

하지만 구조체 변수 간에 비교 연산은 불가능하다.
 if(p1==p2) //컴파일 에러
 printf("좌표가 같습니다.\n");

 두 구조체 변수의 값을 비교하려면 멤버 대 멤버로 비교해야 한다.
 if(p1.x==p2.x && p1.y==p2.y)
 printf("좌표가 같습니다.\n");

5. 구조체 배열
 struct grade arr[3];

struct grade arr[3];
 name korean english  math  average   name  korean  english   math   average   name  korean  english   math  average  
<-------------arr[0]---------------><-------------arr[1]-----------------><-------------arr[2]----------------->
<--------------------------------------------------------arr[3]------------------------------------------------>

구조체 배열의 메모리 할당은 위와 같이 도식화할 수 있다.

구조체 배열도 일반 배열처럼 인덱스를 이용해서 배열의 원소에 접근할 수 있다.
 for(i=0;i<3;i++)
 arr[i].average = (double) (arr[i].korean + arr[i].english + arr[i].math) /3;

구조체 배열의 초기화는 배열의 각 원소가 각각 구조체 변수이므로 다음과 같이 한다.
 struct grade arr[3] = {
{"홍길동",100,100,99};
{"효도르",94,100,99};
{"크로캅",85,90,95};
};

6. 구조체 포인터
 구조체 포인터는 구조체 변수를 가리키는 포인터이다.
 struct grade student1 = {"홍길동",100,100,99};
 struct grade *p = &student1; //구조체 포인터 선언 및 초기화

 구조체 변수에 접근하려면 간접 참조 연산자인 *를 이용하는데 *p는 구조체 변수가 되므로 멤버에 접근하려면 .연산자가
필요하다. 이때에 (*p).name과 같이 사용한다. *p.name이라고 하면 *(p.name)이라는 의미가 되므로 주의가 필요하다.
 다른 방법으로는 간접 멤버 접근 연산자인 ->연산자를 이용할 수 있다.
 (*p).name = 100;
 p->name = 100;
 (*p).name 보다는 p->name이 더 간단하여 자주 사용된다.

7. 비트필드
 구조체 정의 중에 특별한 방법으로 비트필드가 있다. 비트필드는 구조체의 멤버를 비트 단위로 사용하도록 하는 것이다.
예를 들어 시간 정보를 저장하는 구조체를 생각해보자.
 struct time{
 int hour;
 int min;
 int sec;
};
 위와 같은 time 구조체의 크기는 12바이트가 된다. 이때 비트필드를 이용하면 2바이트나 4바이트 크기의 데이터형을 비트
단위로 나눠 사용하여 크기를 줄일 수 있다.
 struct time{
 unsigned int hour : 5; //5비트에 hour 멤버를 저장함.
 unsigned int min : 6;
 unsigned int sec : 6;
};
 비트필드 정의 시에는 멤버 이름 다음에 클론(:)을 쓰고 비트 수를 적으면 된다. 전체가 17비트이므로 unsinged int 하나만
으로 time 구조체를 저장할 수 있다. 따라서 time 구조체의 크긴느 4바이트이다. 6비트로 표현 가능한 숫자가 0~63이고
분이나 초가 0~59사이의 값이므로 6비트만으로 모든 값을 표현할 수 있다. 5비트는 0~23사이의 값을 갖는다.
 그렇다면 비트필드의 메모리 할당은 어떤 식으로 될까?

                         16                                              10                                              4                                   0

                                                                                                             
                                              sec                                              min                                     hour
비트필드의 메모리 할당 도식화.

비트필드 정의 시에 중간에 일부 비트를 비워두고 멤버를 특정 비트에 할당할 수 있다.

 struct time{
 unsigned int hour : 5; //5비트에 hour 멤버를 저장함.
 unsigned int : 2;
 unsigned int min : 6;
 unsigned int : 5;
 unsigned int sec : 6;
};
위와 같이 선언하면 hour 변수를 할당하고 2비트를 비워두고 min 변수를 할당한다.

'프로그래밍 > C' 카테고리의 다른 글

malloc & free 기본 사용법  (0) 2013.07.23
typedef  (0) 2011.01.22
공용체와 열거체  (0) 2011.01.19
배열과 포인터의 관계  (0) 2010.10.09
포인터 (Pointer)  (0) 2010.10.09
선택정렬 (Selection Sort)  (0) 2010.10.09
배열 (array)  (0) 2010.10.09
1.포인터 연산과 배열과의 관계
 int *p;가 선언되어 있고 p에 주소 0x1000전지가 저장되어 있다고 가정하면 p+1은 어떤 값을 가질까? 0x1001이 될 것이라고
생각할 수도 있는데 여기서 +1은 int형 하나의 크기를 의미한다. 즉, int형은 4바이트이므로 주소 값에 +4가 되어 0x1004가 된다.
 결국 포인터 변수에 N을 더하거나 뺄 경우는 '포인터가 가리키는 데이터형 x N'만큼 주소를 더하거나 뺀 값이 된다.

 이 시점에서 배열과 포인터의 관계에 대한 감이 조금 잡힐 것이다. 아래의 코드를 보자.
 int arr[5];
 int *p = &arr[0];
 int i;
 for(i=0;i<5;i++,p++)
 printf("%d\n",*p);
포인터 변수 p는 배열 arr의 첫 번째 원소의 주소 값을 가졌으므로 *p는 첫 번째 원소의 값을 가리키게 된다. 그러므로 p++가
 되면 앞서 말했듯이 int형 포인터 변수의 증감이므로 4바이트 이동해서 정확히 배열 arr의 다음 원소를 가리킨다. 즉, arr[1]을
가리키게 된다. 도식화 하면 아래와 같다.

 
         
0x1000                    0x1004                        0x1008                        0x100c                       0x1010

  ↑                      ↗                               ↗                              ↗                              ↗
                                                               p

 배열의 이름은 배열의 시작 주소가 된다. 즉, int arr[5];에서 인덱스 없이 arr는 배열의 시작 주소이다. 이 말은 arr는 arr[0]의
주소와 같다는 것이다. 따라서 arr==&arr[0]=true가 된다. 여기서 배열의 이름인 arr은 주소이므로 포인터처럼 이용할 수 있다. arr+1이 되면 앞서 설명했듯이 arr[1]을 가리키게 되므로 *(arr+1)은 arr[1]의 값을 나타낸다.
 &arr[i] == arr+i;
 arr[i] == *(arr+i);

이것을 이용하여 배열을 포인터로 가리키면 포인터를 배열처럼 사용할 수 있다.
 int arr[5];
 int *p = arr;
 arr는 주소이므로 포인터 변수에 대입이 가능하다.
 여기서 p+1을 하면 arr[0](=arr)을 가리키던 것이 arr[1]을 가리키게 된다. 그리고 이것을 &p[1]로 표현할 수 있다.
 *(p+i) == p[i];
 p+i == &p[i];
 이렇게 포인터 변수를 배열 이름인 것처럼 사용할 수 있다.

 하지만 배열과 포인터의 중요한 차이점이 있다.
배열은 일단 메모리에 할당되면 시작 주소를 변경할 수 없다.
 int a[5];
 int b[5];
 a=b; //배열 a의 시작 주소를 배열 b의 시작 주소로 변경하려고 하면 컴파일 에러 발생
 a++; //배열 a의 시작 주소를 증가시키므로 (4바이트만큼 이동) 컴파일 에러

 반면 포인터 변수는 변수이므로 보관된 주소를 변경할 수 있다.
 int a[5];
 int b[5];
 int* p=a; //포인터 p를 배열 a의 시작 주소로 초기화
 p=b; //포인터 변수 p에 배열 b의 주소를 저장, p는 b를 가리킴
 p++; //포인터 p를 1만큼 증가시켜 b[1]을 가리킴

2.포인터와 문자열
 'A', 3.14와 같은 리터럴 상수는 메모리에 저장하지 않고 필요할 때마다 CPU 레지스터에 만들어지고 사용 후에 없어지는
임시 값(temporary value)이다. 하지만 CPU 레지스터는 보통 크기가 정해져 있는데 문자열 리터럴은 길이가 정해지지
않아 특별히 메모리에 보관해두고 사용한다. 하지만 리터럴 상수이므로 값을 읽어볼 수만 있고 변경할 수 없는 메모리
영역에 존재하며 "abc";는 실제로 문자열 리터럴 주소를 의미한다.
 char *p = "abc"; //p에 문자열 리터럴 주소를 보관
 p[0] = 'A'; //문자열 리터럴은 변경 불가
 strcpy(p,"hello"); //문자열 리터럴은 변경불가
 p="hello"; //포인터 변수 p에 다른 문자열 주소 대입은 가능

 문자열을 비교할 때 strcmp()를 쓰는 이유를 이제 알 수 있다.
 p == "abc"는 문자열의 주소를 비교하는 것이다. 당연히 주소는 다르다. 따라서 strcmp()를 이용해서 문자열의 내용을
비교하는 것이다.

3.const 포인터
 const char* p; //포인터가 가리키는 값 변경 불가
 char* const p; //포인터 변수 자신의 값(주소) 변경 불가
 const char* const p; //가리키는 값, 주소 둘 다 변경 불가

'프로그래밍 > C' 카테고리의 다른 글

typedef  (0) 2011.01.22
공용체와 열거체  (0) 2011.01.19
구조체  (0) 2010.10.23
포인터 (Pointer)  (0) 2010.10.09
선택정렬 (Selection Sort)  (0) 2010.10.09
배열 (array)  (0) 2010.10.09
비트 연산자  (0) 2010.10.03
1.기본 개념
 포인터는 다른 변수의 주소(address)를 저장하는 변수이다. 즉, 포인터 변수는 변수의 값으로 주소를 저장한다.

 형식 : 데이터형 *변수명;
 ex) char *ch; //또는 char* ch;
       int *num; //또는 int * num;

 위와 같이 데이터형을 쓰고 *을 쓰고 변수명을 적어준다. char*은 char형 변수의 주소, int*은 int형 변수의 주소를 저장한다는
의미이다. 여기서 포인터 변수는 포인터형에 관계없이 항상 크기가 4바이트다. char*형 변수, int*형 변수에 관계없이 "주소"를
저장하기 때문에 4바이트(32비트 플랫폼 기준) 크기를 갖는다. 하지만 포인터 변수가 가리키는 변수는 char형은 1바이트,
int는 4바이트와 같이 각 데이터형에 알맞은 크기를 가진다.

 #include <stdio.h>
 int main(void)
{
 char x;
 char *p = &x;

printf("char 의 크기 = %d\n",sizeof(x)); //char의 크기는 1바이트이므로 1이 출력됨
printf("char*의 크기 = %d\n",sizeof(p)); //char*의 크기는 4바이트이므로 4가 출력됨

return 0;
}

 포인터 변수 선언 시 유의할 점은 다수의 포인터 변수 선언 시에 int* a,b;라고 하면 int *a; int b;로 선언된 것과 같다.
 따라서 int *a, *b; 이렇게 포인터 변수마다 *을 써주어야 한다.

2.포인터의 사용
 포인터 변수를 사용하기 위해서 일단 두 가지 연산자가 필요하다. 첫 번재는 주소 구하기 연산자
(address of operator)
로 변수 이름 앞에 &을 사용하여 변수의 주소를 구할 수 있다.

 int x;
 int *p;
 p = &x; //x의 주소를 포인터 변수 p에 대입

 &연산자는 주소를 구하는 것이므로 상수나 수식에는 사용이 불가능하며 변수에만 사용이 가능하다.
그리고 반드시 int형 변수의 주소는 int*형의 변수에 저장해야 하며 다른 형의 변수에 저장하면 컴파일 경고가 발생한다.
 두 번째는 간접 참조 연산자(indirection operator)이다. 포인터 변수 앞에 *을 사용하면 포인터 변수가 가리키는
변수의 값을 읽어오거나 변경할 수 있다.

 int x;
 int *p = &x; //x의 주소를 포인터 변수 p에 대입
 *p = 10; //p가 가리키는 변수, 즉 x에 10을 대입

아래의 코드로 공부한 내용을 직접 확인해보자.

#include <stdio.h>
int main(void)
{
 int x;
 int *p = &x;
 *p = 10; //x에 10을 대입

 printf("x =%d\n",x); //x의 값인 10이 출력됨
 printf("*p =%d\n",*p); //p가 가리키는 변수 x의 값인 10이 출력됨

 printf("&x =%d\n",&x); //변수 x의 주소 값이 출력됨
 printf("p =%d\n",p); //p는 변수 x의 주소 값을 저장하고 있으므로 변수 x의 주소 값이 출력됨

 printf("&p =%d\n",&p); //포인터 변수 p의 주소 값이 출력됨

 return 0;

 포인터 변수 p 역시 변수이므로 메모리에 할당된 공간이 있고 그에 대한 주소를 가지므로 printf("&p= %d\n",&p);에서는
포인터 변수 p가 할당받은 메모리의 주소 값이 출력된다.

※ 이중 포인터(double pointer)
 int x;
 int *p = &x;
 int **dp = &p;
 **dp = 10;

//즉, *(*dp) = *(p) =  x = 10

삼중, 사중 포인터도 만들 수는 있으나 잘 사용되지 않는다.

3.포인터 사용 시 주의사항
 앞서 말했듯이 포인터 변수의 데이터형은 포인터 변수가 가리키는 변수의 데이터형과 반드시 일치해야 한다.
이유를 살펴보면 다음과 같다.

 short a;
 int *p = &a;
 *p=10;
위와 같은 선언 시에 포인터 변수 p는 자신이 가리키는 곳에 int형 변수가 있다고 가정하는데 실제로는 short형 변수인 a가
있기 때문에 short형 변수의 크기인 2바이트에 추가로 그다음 주소의 2바이트까지 합쳐서 총 4바이트, 즉 int형 변수의
크기로 10을 저장한다. 그렇게 되면 a 변수뿐 아니라 다음 주소의 값까지 변경되어서 실행 에러가 발생한다.
 또다른 주의사항은 포인터 변수를 초기화 하지 않아 쓰레기 값을 가지고 있을 때이다. 포인터 변수도 변수이므로
초기화 해주지 않으면 쓰레기 값을 갖는다.
 int *p;
 *p = 10;
 위와 같은 선언 시 p가 쓰레기 값을 가지고 있을 때는 10을 메모리의 어느 위치에 저장하게 될지 모른다. 따라서 심각한 문제를 일으킬 수도 있다.
 그렇다면 초기화하는 방법은 무엇일까? 포인터가 아무것도 가리키지 않을 때는 변수에 0번지를 저장하고, 이것을
널 포인터(null pointer)라고 부른다.
 int *p = NULL; //널 포인터로 초기화

 if(p!=NULL) //널 포인터가 아닌지 확인 후에 사용
 *p=10;

 if(p) //0이 아닌 값은 참이므로 이렇게 확인하는 방법도 가능
 *p=10;

'프로그래밍 > C' 카테고리의 다른 글

공용체와 열거체  (0) 2011.01.19
구조체  (0) 2010.10.23
배열과 포인터의 관계  (0) 2010.10.09
선택정렬 (Selection Sort)  (0) 2010.10.09
배열 (array)  (0) 2010.10.09
비트 연산자  (0) 2010.10.03
삼각형 출력하기  (0) 2010.10.03

+ Recent posts