많은 프로그래밍 작업은 구조체(structures)라고 하는 C의 데이터형을 통해서 단순화될 수 있다. 구조체는 프로그램에서 필요한 데이터형을 프로그래머가 직접 구성하여 사용하는 데이터 저장 방식의 한 가지이다. 오늘은 다음과 같은 내용을 배운다.
·단순 구조체와 복합 구조체에 대해서
·구조체를 정의하고 선언하는 방법
·구조체에 포함되어 있는 데이터를 사용하는 방법
·배열을 포함하는 구조체와 구조체의 배열을 생성하는 방법
·구조체 내에서 포인터를 선언하는 방법과 구조체에 대한 포인터를 선언하는 방법
·함수의 인수로 구조체를 전달하는 방법
·공용체를 정의하고, 선언하고, 사용하는 방법
·구조체로 새로운 데이터형을 정의하는 방법
1. 단순 구조체
: 구조체(structure)는 여러 개의 변수를 쉽게 사용할 수 있도록 하나의 이름으로 묶은 하나 이상의 변수의 집합이다. 구조체에 포함되는 변수는 배열에서와는 달리 여러 가지 데이터형 이 될 수 있다. 구조체는 배열이나 다른 구조체 등 C의 모든 데이터형을 포함할 수 있다. 구조체에 포함되는 각각의 변수를 구조체 멤버(member)라고 한다. 우선, 단순 구조체에 대해서 알아보도록 하자. 이렇게 구조체를 두 가지 형태로 구분하여 설명하는 것이 이해하기 쉽지만, 실제로 C 언어에서는 단순 구조체와 복합 구조체를 구분해서 사용하지 않는다는 것을 기억하기 바란다.
1.1 구조체의 정의와 선언
: 만약 그래픽 프로그램을 작성한다면, 프로그램은 화면 상에 출력되는 점의 좌표를 다룰 필요가 있을 것이다. 화면 좌표는 수평 좌표를 나타내는 x값과 수직 좌표를 나타내는 y값으로 구성된다. 다음과 같이 화면에서 점의 위치를 x와 y값으로 나타내는 coord라는 이름의 구조체를 정의할 수 있다.
struct coord {
int x;
int y;
};
구조체 정의가 시작된다는 것을 알려주는 struct 키워드 다음에는 반드시 구조체의 이름이나 태그(tag)가 포함되어야 한다. 이것은 C에서 다른 변수를 선언할 때와 같다. 구조체 이름 뒤에 있는 중괄호에는 구조체 멤버인 변수의 목록이 포함된다. 각각의 구조체 멤버에 대해서 변수의 형태와 이름이 사용되어야 한다. 앞의 예제는 두 개의 정수형 변수 x와 y를 가지는 coord라는 이름의 구조체를 정의한다. 그러나 실제로 구조체 coord형의 변수(instances)를 생성하지는 않는다. 즉, 구조체형에 대한 메모리 영역을 보존하지 않는다. 구조체형의 변수를 실제로 생성하는 두 가지 방법이 있는데, 하나는 다음과 같이 구조체 정의문 다음에 하나 이상의 변수 이름을 포함시키는 것이다.
struct coord {
int x;
int y;
} first, second;
앞의 문장은 구조체 coord를 정의하고 coord형 변수인 first와 second라는 이름의 두 구조 체를 선언한다. first와 second는 coord형으로 선언된 구조체형 변수이다. first는 x와 y라는 이름의 두 가지 정수형 멤버를 가지고, second도 first와 같은 멤버를 가짐. 구조체를 선언하는 첫 번째 방법은 구조체의 선언문과 정의를 결합시킨 형태이다. 두 번째 방법은 소스 코드의 서로 다른 부분에서 구조체를 정의하고 구조체형 변수를 선언하는 것이다. 그래서 다음과 같은 문장도 coord형의 두 변수를 선언한다.
struct coord {
int x;
int y;
};
/* 그 밖의 프로그램 문장 */
struct coord first, second;
1.2 구조체 멤버를 사용하는 방법
: 개별적인 구조체 멤버는 동일한 형태의 일반적인 변수와 마찬가지 방법으로 사용될 수 있다. 구조체 멤버는 구조체 멤버 연산자(structure member operator)나 멤버 연산자(dot operator)라고 하는 마침표(.)를 사용하여 참조할 수 있다. 그래서 first라는 이름의 구조체가 좌표 x=50, y=100의 값으로 화면 위치를 표현하도록 하기 위해서는 다음과 같은 문장을 사용할 수 있을 것이다.
first.x = 50;
first.y = 100;
구조체 second에 저장된 화면의 좌표값을 출력하기 위해서는 다음과 같은 문장을 사용할 수 있다.
printf("%d, %d", second.x, second.y);
여기서, 개별적인 변수 대신에 구조체를 사용하는 것이 어떤 장점을 제공하는지 의문을 가질 것이다. 한 가지 중요한 장점은 간단한 할당문으로 동일한 형태의 구조체 간에 모든 값을 복사할 수 있다는 것이다. 앞의 예에서 다음 문장은
first = second;
다음과 같은 뜻을 가진다.
first.x = second.x;
first.y = second.y;
프로그램에서 많은 멤버를 가지는 복잡한 구조체를 사용할 때 이렇게 한 번의 할당문으로 값을 복사할 수 있다는 사실은 매우 효율적인 시간 절약 방법이다. 구조체의 다른 한 가지 장점은 약간 더 고급 기능을 배울 때 이해할 수 있을 것이다. 일반적으로, 여러 가지 형태의 변수를 동시에 다룰 필요가 있을 때에는 항상 구조체를 유용하게 사용할 수 있을 것이다. 예를 들어, 우편용 주소록 데이터베이스를 처리할 때 각각의 자료를 구조체로 만들어 하나의 자료에 포함되는 이름, 주소, 전화번호 등을 구조체 멤버로 취급할 수 있을 것이다.
2. 더욱 복잡한 구조체
지금까지 간단한 형태의 구조체에 대해서 알아보았으므로, 이제 더욱 흥미롭고 복잡한 형태의 구조체에 대해서 알아보자. 복잡한 형태의 구조체란 다른 어떤 구조체를 멤버로 가지거나 또는 배열을 멤버로 가지는 구조체를 말한다.
2.1 구조체를 가지는 구조체
: 앞에서도 설명했듯이, C의 구조체는 C에서 사용되는 모든 데이터형을 포함할 수 있다. 예를 들어, 구조체는 다른 어떤 구조체를 가질 수 있다. 앞에서 사용했던 예를 다시 보자. 그래픽 프로그램이 좌표뿐 아니라 사각형을 다루어야 한다고 가정해 보자. 사각형(rectangle)은 축을 중심으로 상반되는 위치에 있는 좌표로 표현할 수 있다. 앞에서는 이미 하나의 점을 표현하기 위해서 두 좌표값을 가지는 구조체를 정의하는 방법에 대해서 설명했다. 사각형을 표현하는 구조체를 정의하기 위해서는 두 개의 점을 표현하는 구조체가 필요할 것이다. coord형의 구조체가 이미 정의되어 있다고 가정하면 다음과 같은 구조체를 정의할 수 있을 것이다.
struct rectangle {
struct coord topleft;
struct coord bottomrt;
};
이 문장은 두 개의 coord형 구조체 변수를 가지는 rectangle이라는 구조체를 정의한다. 두 개의 coord형 구조체 변수는 topleft와 bottomrt라는 이름을 가진다. 앞의 에는 단지 rectangle형의 구조체를 정의한다. 실제로 구조체형 변수를 선언하기 위해서는 다음과 같은 문장을 사용해야 한다.
struct rectangle mybox;
또한, 앞에서 coord형 구조체를 선언할 때와 마찬가지로 구조체 정의와 선언을 결합시킬 수 있을 것이다.
struct rectangle{
struct coord topleft;
struct coord bottomrt;
} mybox;
여기서 int형의 데이터를 참조하기 위해서는 멤버 연산자(.)를 두 번 사용해야 할 것이다. 그래서 다음 수식은
mybox.topleft.x
mybox라는 이름의 rectangle형 구조체 변수에서 topleft라는 멤버 내의 x라는 멤버를 나타내는 것이다. 좌표가 (0, 10), (100, 200)인 사각형을 정의하기 위해서는 다음과 같은 문장을 작성할 것이다.
mybox.topleft.x = 0;
mybox.topleft.y = 10;
mybox.bottomrt.x = 100;
mybox.bottomrt.y = 200;
이 문장은 다소 혼란스러울 것이다. rectangle형 구조체와 여기에 포함된 두 개의 coord형 구조체, 각각의 coord형 구조체가 가지는 두 개의 int형 변수의 관계를 보여주는 <그림 11.1> 을 살펴보면 지금까지 설명한 내용을 쉽게 이해할 수 있을 것이다.
구조체는 앞의 예에서와 같은 이름을 가진다.
<그림 11.1> 구조체, 구조체 내의 구조체, 구조체 멤버 간의 관계를 보여주는 그림
이제, 구조체를 포함하는 구조체의 사용 예를 살펴보도록 하자. <리스트 11.1>에 있는 프로그램은 사각형의 좌표값을 읽어들이고, 사각형의 면적을 계산하여 출력한다. 프로그램의 앞 부분에 있는 주석문에서 설명되는 프로그램의 조건을 주의해서 살펴보자.
<리스트 11.1> 다른 구조체를 가지는 구조체의 사용 예
/* 다른 구조체를 가지는 구조체의 사용 예 */
/* 사각형의 구석 좌표를 읽어서 면적을 구한다.
오른쪽 하단 구석의 y좌표가 왼쪽 상단 구석의 y좌표보다 크고,
오른쪽 하단 구석의 x좌표가 왼쪽 상단 구석의 y좌표보다 크며,
모든 좌표가 양수라고 가정한다. */
#include <stdio.h>
int length, width;
long area;
struct coord{
int x;
int y;
};
struct rectangle{
struct coord topleft;
struct coord bottomrt;
} mybox;
main()
{
/* 좌표 입력 */
printf("\nEnter the top left x coordinate: ");
scanf("%d", &mybox.topleft.x);
printf("\nEnter the top left y coordinate: ");
scanf("%d", &mybox.topleft.y);
printf("\nEnter the bottom right x coordinate: ");
scanf("%d", &mybox.bottomrt.x);
printf("\nEnter the bottom right y coordinate: ");
scanf("%d", &mybox.bottomrt.y);
/* 길이와 높이 계산 */
width = mybox.bottomrt.x - mybox.topleft.x;
length = mybox.bottomrt.y - mybox.topleft.y;
/* 면적 계산과 출력 */
area = width * length;
printf("\nThe area is %ld units.\n", area);
return 0;
}
C는 구조체의 종속 단계에 제한을 두지 않는다. 메모리가 허용하는 범위 내에서는 구조체를 가지는 구조체, 이런 구조체를 가지는 또다른 구조체등 몇 단계로 구성되는 구조체를 사용할 수 있다. 그러나 여러 단계의 종속된 구조체가 효율적이지 않은 경우도 있다. 대부분의 C 프로그램에서는 3단계 이상 종속된 구조체를 사용하지 않는다.
2.2 배열을 가지는 구조체
: 또한, 하나 이상의 배열을 멤버로 포함하는 구조체를 정의할 수 있다. 이때 사용되는 배열은 int, char등 C의 어떤 데이터형이든지 될 수 있다. 예를 들어, 다음 문장은
struct data{
int x[4];
char y[10];
};
4개의 요소를 가지는 x라는 이름의 정수형 배열과 10개의 요소를 가지는 y라는 이름의 문자형 배열을 구조체 멤버로 포함하는 data형 구조체를 정의한다. 그리고 나서 다음과 같이 data형 구조체 변수 record를 선언할 수 있다.
struct data record;
이 구조체의 상태가 <그림 11.2>에 나타나 있다. 그림에서 배열 x의 각 요소는 배열 y의 각 요소보다 2배나 많은 공간을 차지한다는 것을 주목하기 바란다. 이것은 int형이 대개 2바이트의 저장 영역을 요구하는 반면에 char형이 1바이트를 차지하기 때문이다.
<그림 11.2> 배열을 구조체 멤버로 가지는 구조체
구조체 멤버인 배열의 개별적인 요소를 사용하기 의해서는 멤버 연산자와 배열의 첨자를 함께 사용해야 한다.
record.x[2] = 100;
record.y[1] = 'x';
아마도 문자 배열은 문자열을 저장하기 위해서 가장 많이 사용된다는 것을 기억할 것이다. 또한 "포인터에 대해서"에서 설명했듯이 괄호를 포함함지 않는 배열의 이름은 배열에 대한 포인터를 뜻한다는 사실을 기억할 것이다. 이런 사실은 구조체 멤버로 사용되는 배열에도 적용되므로 다음 수식은
record.y
구조체 record의 멤버인 배열 y[]의 첫 번째 요소에 대한 포인터이다. 그래서 다음과 같은 문장을 사용하여 y[]의 내용을 화면 상에 출력할 수 있을 것이다.
puts(record.y);
이제 다른 예제를 살펴보자. <리스트 11.2>에 있는 프로그램은 float형 변수와 두 개의 char형 배열을 가지는 구조체를 사용하고 있다.
<리스트 11.2> 배열을 멤버로 가지는 구조체의 사용 예
/* 배열을 멤버로 가지는 구조체 */
#include <stdio.h>
/* 데이터를 저장할 구조체 정의와 선언 구조체는 하나의 부동 소수형 변수와
두 개의 문자형 배열을 가진다. */
struct data{
float amount;
char fname[30];
char lname[30];
} rec;
main(}
{
/* 키보드에서 데이터 입력 */
printf("Enter the donor's first and last names,\n");
printf("separated by a space: ");
scanf("%s %s", rec.fname, rec.lname);
printf("\nEnter the donation amount: ");
scanf("%f", &rec.amount);
/* 정보 출력 */
/* 참고 : %2f는 부동 소수형 값을 소수점 이하 두 자리까지 출력하도록 지정한다. */
/* 화면상에 데이터 출력 */
printf("\nDonor %s %s gave $%.2f.\n", rec.fname, rec.lname, rec.amount);
return 0;
}
3. 구조체 배열
: 배열을 멤버로 가지는 구조체를 사용할 수 있다면 구조체의 배열을 사용하는 것도 가능할까? 가능하다고 생각할 것이다. 실제로 구조체의 배열은 아주 강력한 프로그래밍 도구다. 구조체의 배열에 대해서 알아보도록 하자.
앞에서는 프로그램에서 필요한 데이터형을 정의하기 위해서 구조체를 사용할 수 있다고 설명했다. 프로그램은 대부분의 경우 여러 개의 데이터를 사용한다. 예를 들어, 전화번호부를 관리하는 프로그램에서는 사람의 이름과 전화번호를 저장하기 위해서 구조체를 정의할 수 있다.
struct entry{
char fname[10];
char lname[12];
char phone[8];
};
그러나 전화번호부는 여러 사람의 데이터를 포함하는 것이므로 entry 구조체형 변수를 한 번만 선언하는 것은 유용하지 않을 것이다. 여기서 필요한 것은 entry형 구조체의 배열이다. 그래서 구조체를 정의하고 나면 다음과 같이 구조체의 배열을 선언할 수 있다.
struct entry list[1000];
이 문장은 1,000개의 요소를 가지는 list라는 이름의 배열을 선언한다. 모든 요소는 entry형 구조체 변수이고, 각 요소는 일반적인 배열 요소와 마찬가지로 첨자에 의해서 구분된다. 배열 요소인 각각의 구조체는 세 개의 char형 배열을 구조체 멤버로 포함하고 있다. 구조체의 배열을 선언할 때에는 다양한 방법으로 데이터를 다룰 수 있다. 예를 들어, 한 배열 요소의 데이터를 다른 배열 요소에 할당하기 위해서 다음과 같이 할 수 있을 것이다.
list[1] = list[5];
이 문장은 list[5]의 각 멤버에 저장된 값을 구조체 list[1]에서 대응하는 멤버에 할당한다. 또한, 개별적인 구조체의 멤버들 간에 데이터를 이동시킬 수도 있다. 다음 문장은
strcpy(list[1].phone, list[5].phone);
list[5].phone에 저장된 문자열을 list[1].phone에 복사하는 것이다. 라이브러리 함수 strcpy()는 한 문자열을 다른 문자열에 복사한다. 자세한 내용은 "문자열 다루기"에서 상세히 설명하겠다. 또한, 필요하다면 구조체 멤버의 하나인 배열의 개별적인 요소들 간에도 데이터를 이동시킬 수 있다.
list[5].phone[1] = list[2].phone[3];
이 문장은 list[5]에서 전화번호 값을 저장하는 배열의 두 번째 위치에 list[2]에서 전화번호값을 저장하는 배열의 네 번째 문자를 할당한다. 첨자는 항상 0에서부터 시작한다는 것을 잊지 않도록 하자.
<리스트 11.3>에 있는 프로그램은 구조체 배열의 사용 예를 보여준다. 또한, 이 프로그램은 배열을 멤버로 가지는 구조체의 배열을 사용하고 있다.
<리스트 11.3>
/* 구조체의 배열 사용 예 */
#include <stdio.h>
/* 항목을 저장할 구조체 정의 */
struct entry{
char fname[20];
char lname[20];
char phone[10];
};
/* 구조체의 배열 선언 */
struct entry list[4];
int i;
main()
{
/* 네 명의 데이터 입력 */
for(i = 0; i < 4; i++)
{
printf:\nEnter first name: ");
scanf("%s", list[i].fname);
printf("Enter last name: ")"
scanf("%s", list[i].lname);
printf("Enter phone is 123-4567 format: ");
scanf("%s", list[i].phone);
}
/* 두 개의 빈줄 출력 */
printf("\n\n");
/* 데이터 출력 */
for(i = 0; i < 4; i++)
{
printf("Name: %s %s", list[i].fname, list[i].lname);
printf("\t\tPhone: %s\n", list[i].phone);
}
return 0;
}
<리스트 11.3>에서 사용된 프로그래밍 방식에 익숙해지도록 하자. 많은 프로그래밍 작업에서는 배열을 멤버로 가지는 구조체의 배열을 사용할 필요가 있을 것이다.
4. 구조체의 초기화
: C의 다른 변수형과 마찬가지로 구조체를 선언하는 동시에 초기화할 수 있다. 수행 과정은 배열을 초기화하는 것과 비슷하다. 구조체 선언문에는 등호와 함께 쉼표에 의해서 구분되고, 중괄호에 포함되는 초기화값의 목록이 나타난다. 예를 들어, 다음 예제를 살펴보자.
struct sale {
char customer[20];
char item[20];
float amount;
} mysale = {"Acme Industries",
"Left-handed widget",
1000.00
};
이 문장이 실행되면 다음과 같은 동작이 수행된다.
① 1번째 줄부터 5번째 줄까지는 sale이라는 이름의 구조체형을 정의한다.
② 5번째 줄에서는 mysale이라는 이름의 sale형 구조체 변수가 선언된다.
③ 5번째 줄에서는 구조체 멤버 mysale.customer를 문자열 'Acme Industries'로 초기화한다.
④ 6번째 줄에서는 구조체 멤버 mysale.item을 문자열 'Left-handed widget'으로 초기화한다.
⑤ 7번째 줄에서는 구조체 멤버 mysale.amount를 1000.00의 값으로 초기화한다.
구조체를 멤버로 가지는 구조체의 경우에는 순서대로 초기화값을 나열하면 된다. 초기화값은 구조체를 정의하는 문장에서 멤버가 나타나 있는 순서대로 구조체 멤버에 할당된다. 다음은 앞의 예제를 약간 보충한 다른 하나의 예제이다.
struct customer {
char firm[20];
char contact[25];
}
struct sale {
struct customer buyer;
char item[20];
float amount;
} mysale = {{"Acme Industries", "George Adams"),
"Left-handed widget",
1000.00
};
이 예제는 다음과 같은 초기화를 수행한다.
① 10번째 줄에서 구조체 멤버 mysale.buyer.firm은 문자열 'Acme Industries'로 초기화된다.
② 10번째 줄에서 구조체 멤버 mysale.buyer.contact는 문자열 'George Adams'로 초기화된다.
③ 11번째 줄에서 구조체 멤버 mysale.item은 문자열 'Left-handed widget'으로 초기화된다.
④ 12번째 줄에서 구조체 멤버 mysale.amount는 1000.00의 값으로 초기화된다.
또한, 구조체의 배열을 초기화할 수 있다. 입력되는 초기화값은 배열에 포함되어 있는 구조체에 순서대로 할당된다. 예를 들어, sale형 구조체의 배열을 선언하고 나서 처음 두 배열 요소인 두 개의 구조체를 초기화하기 위해서는 다음 예제와 같이 할 수 있을 것이다.
struct customer {
char firm[20];
char contact[25];
};
struct sale {
struct customer buyer;
char item[20];
float amount;
};
struct sale y1990[100] = {
{{"Acme Industries", "George Adams"},
"Left-handed widget",
1000.00
}
{{"Wilson & Co.", "Ed Wilson"},
"Type 12 gizmo",
290.00
}
};
이 코드에서는 다음과 같은 동작이 수행된다.
① 14번째 줄에서 구조체 멤버 y1990[0].buyer.firm은 문자열 'Acme Industries'로 초기화된다.
② 14번째 줄에서 구조체 멤버 y1990[0].buyer.contact는 문자열 'George Adams'로 초기화됨.
③ 15번째 줄에서 구조체 멤버 y1990[0].item은 문자열 'Left-handed widget'으로 초기화된다.
④ 16번째 줄에서 구조체 멤버 y1990[0].amount는 1000.00의 값으로 초기화된다.
⑤ 18번째 줄에서 구조체 멤버 y1990[1].buyer.firm은 문자열 'wilson & Co.'로 초기화된다.
⑥ 18번째 줄에서 구조체 멤버 y1990[1].buyer.contact는 문자열 'Ed Wilson'으로 초기화된다.
⑦ 19번째 줄에서 구조체 멤버 y1990[1].item은 문자열 'Type 12 gizmo'로 초기화된다.
⑧ 20번째 줄에서 구조체 멤버 y1990[1].amount는 290.00의 값으로 초기화된다.
5. 구조체와 포인터
: 포인터가 C에서 아주 중요한 부분을 차지한다는 사실을 인식한다면 구조체와 함께 사용할 수 있다는 사실도 쉽게 짐작할 수 있을 것이다. 포인터를 구조체의 멤버로 사용할 수 있으며, 구조체에 대한 포인터를 선언할 수도 있다. 이런 내용들을 하나씩 살펴보도록 하자.
5.1 구조체 멤버로 사용되는 포인터
: 포인터를 구조체 멤버로 사용하면 더욱 융통성 있는 프로그램을 작성할 수 있다. 포인터는 구조체 멤버로 사용될 때 일반적인 경우와 동일한 방법으로 선언된다. 즉, 간접 연산자(*)를 사용하여 선언된다. 다음은 예이다.
struct data {
int *value;
int *rate;
} first;
이 문장은 int형에 대한 두 개의 포인터를 멤버로 가지는 구조체를 정의하고 선언한다. 다른 모든 포인터에서와 마찬가지로 포인터를 선언하는 것만으로 사용할 수는 없다. 포인터에는 변수의 주소를 할당하여 포인터가 어떤 영역을 지적하도록 초기화해야 한다. 만약 cost와 interest가 int형 변수로 선언되어 있다면 다음과 같은 문장을 작성할 수 있을 것이다.
first.value = &cost;
first.rate = &interest;
이제 포인터가 초기화되었으므로 간접 연산자(*)를 사용할 수 있다. 수식 *first.value는 cost에 저장된 값을 뜻하고, 수식 *first.rate는 interest에 저장된 값을 뜻한다. 구조체 멤버로 가장 많이 사용되는 포인터는 아마도 char형에 대한 포인터일 것이다. 앞 강의 "문자와 문자열"에서는 '첫 번째 문자를 지적하는 포인터와 마지막을 표시하는 널 문자 사이의 일련의 문자로 문자열이 구성된다'는 것을 설명했었다. 메모리를 효율적으로 사용하기 위해서는 다음과 같이 char형에 대한 포인터를 선언하고 문자열을 지적하도록 초기화해야 한다.
char *p_message;
p_message = "Teach Yourself C in 21Days";
또한, char형에 대한 포인터를 멤버로 가지는 구조체를 사용하여 동일한 결과를 얻을 수 있다.
struct msg {
char *p1;
char *p2;
} myptrs;
myptrs.p1 = "Teach yourself C in 21Days";
myptrs.p2 = "By SAMS Publishing";
구조체 멤버로 포함되어 있는 포인터는 일반적인 포인터를 사용할 수 잇는 어떤 곳에서든지 사용할 수 있다. 예를 들어, 포인터가 지적하는 문자열을 출력하기 위해서는 다음과 같은 문장을 사용할 수 있을 것이다.
printf("%s %s", myptrs.p1, myptrs.p2);
구조체 멤버로 char형의 배열을 사용하는 것과 char형에 대한 포인터를 사용하는 것의 차이점은 무엇일가? 두 가지 모두 구조체에 문자열을 '저장'하기 위한 방법이다. 다음은 두 가지 방법을 모두 사용하는 구조체 msg이다.
struct msg(
char p1[30];
char *p2;
} myptrs;
괄호를 포함하지 않는 배열의 이름은 배열의 처 번째 요소에 대한 포인터라는 것을 기억하자 그래서 앞의 구조체 멤버는 다음과 같이 사용할 수 있다.
strcpy(myptrs.p1, "Teach Yourself C In 21 Days");
strcpy(myptrs.p2, "By SAMS Publishing");
/* 그 밖의 프로그램 문장 */
puts(myptrs.p1);
puts(myptrs.p2);
두 가지 방법의 실제 차이점은 무엇일까? 만약 구조체 멤버로 char형 배열을 가지는 구조체를 정의하면, 이 구조체형으로 선언되는 모든 변수는 일정한 크기의 배열만을 저장할 수 있는 저장 영역을 가지게 된다. 또한, 저장 영역의 크기는 제한되고 구조체에는 지정된 것보다 긴 문자열을 저장할 수 없다. 다음은 사용 예이다.
struct msg {
char p1[10];
char p2[10];
} myptrs;
…
strcpy(p1, "Minneapolis"); /* 문자열이 배열보다 길다. */
strcpy(p2, "MN"); /* 문자열이 배열보다 잛지만 저장 영역이 낭비되고 있다. */
반면에, char형에 대한 포인터를 가지는 구조체를 정의하면 이런 제한은 적용되지 않는다. 각각의 구조체 변수는 포인터를 저장하기 위한 저장 영역만을 가지게 된다. 실제 문자열은 메모리 내의 다른 어떤 곳에 저장되지만 메모리의 위치에 대해서는 신경 쓸 필요가 없다. 이렇게 포인터를 사용하면 문자열의 길이에 제한이 없고 저장 영역이 낭비되지 않는다. 실제 문자열은 구조체의 일부분이 아닌 상태로 메모리의 특정 부분에 저장된다. 구조체에 포함되는 각 포인터는 어떤 길이의 문자열도 지적할 수 있다. 실제 문자열이 구조체에 저장되는 것은 아니지만 구조체의 일부분과 관련된다.
5.2 구조체에 대한 포인터
: C 프로그램에서는 다른 어떤 데이터형에 대한 포인터와 마찬가지로 구조체에 대한 포인터 를 선언하고 사용할 수 있다. 이 장에서 나중에 설명할 것처럼 구조체에 대한 포인터는 함수 의 인수로 구조체를 전달할 때 가끔 사용된다. 또한, 구조체에 대한 포인터는 링크드 리스트 (linked lists)라고 알려져 있는 매우 강력한 데이터 저장 방법에서 사용된다. 링크드 리스트는 나중에 "포인터 : 고급 기능들"에서 설명할 것이다.
여기서는 프로그램에서 구조체에 대한 포인터를 생성하고 사용하는 방법에 대해서 알아보도록 하자. 우선, 구조체를 정의하겠다.
struct part {
int number;
char name[10];
};
이제 part형 구조체에 대한 포인터를 선언한다.
struct part *p_part;
선언문에서 사용되는 간접 연산자(*)는 p_part가 part형 구조체 변수가 아니라 part형 구조체 에 대한 포인터라는 사실을 알려준다는 것을 기억하기 바란다. 이제 포인터를 초기화할 수 있을까? 그렇지 않다. 구조체 part는 정의되었지만 어떤 구조체 변수도 선언되지 않았다. 앞의 문장은 구조체 변수 선언문이 아니라 구조체에 대한 포인터 선언문이라는 것을 기억하자. 이제 데이터를 저장하기 위해서 메모리 내의 저장 영역을 보존 하는 변수 선언문이 필요하다. 포인터가 어떤 주소를 지적하도록 하기 위해서는 우선 part형 구조체 변수를 선언해야 한다. 다음은 선언문이다.
struct part gizmo;
이제 포인터를 초기화할 수 있다.
p_part = &gizmo;
이 문장은 gizmo의 주소값을 p_part에 저장한다.
<그림 11.5>에는 구조체와 구조체에 대한 포인터의 관계가 나타나 있다.
<그림 11.5> 구조체에 대한 포인터는 구조체의 첫 번째 바이트를 지적한다.
이제, 구조체 gizmo에 대한 포인터를 생성했다. 이 포인터를 어떻게 사용할 것인가? 한 가지 방법은 간접 연산자(*)를 이용하는 것이다. 이런 사실을 여기에 적용하면 p_part는 구조체 gizmo에 대한 포인터이고, *p_part는 gizmo 자체를 뜻한다는 것을 알 수 있다. 이제 gizmo의 각 멤버를 사용하기 위해서는 구조체 멤버 연산자(.)를 사용하자. gizmo.number에 100의 값을 저장하기 위해서는 다음과 같은 문장을 사용할 수 있을 것이다.
(*p_part).number = 100;
여기서 멤버 연산자(.)는 간접 연산자(*)보다 높은 우선 순위를 가지고 있으므로 *p_part는 괄호 내에 포함되어야 한다. 구조체에 대한 포인터를 사용하여 구조체 멤버를 참조하는 두 번째 방법은 하이픈(-)과 부등호(>)로 구성되는 간접 멤버 참조 연산자(indirection membership operator)를 사용하는 것이다. 하이픈과 부등호를 함께 사용하면 C에서 하나의 새로운 연산자로 간주된다는 것을 기억하기 바란다. 이 연산자는 포인터 이름과 멤버 이름 사이에 위치된다. p_part 포인터를 사용하여 gizmo의 멤버인 number를 참조하기 위해서는 다음과 같은 문장을 사용할 수 있다.
p_part -> number
다른 하나의 예를 살펴보자. 만약 str이 구조체이고, p_str이 str에 대한 포인터이며, memb가 str의 멤버라면 다음과 같은 문장을 통해서 str.memb를 사용할 수 있을 것이다.
p_str->memb
그래서 전체적으로 구조체 멤버를 참조하는 세 가지 방법이 있음을 알 수 있다.
·구조체의 이름을 사용한다.
·간접 연산자(*)와 구조체에 대한 포인터를 함께 사용한다.
·간접 멤버 참조 연산자(->)와 구조체에 대한 포인터를 함께 사용한다.
p_str이 구조체 str에 대한 포인터라면 다음의 세 가지 수식은 모두 동일한 것이다.
str.memb
(*p-str).memb
p_str->memb
5.3 포인터와 구조체의 배열
: 앞에서는 구조체의 배열과 구조체에 대한 포인터가 매우 강력한 프로그래밍 도구라는 것을 설명했다. 또한, 구조체를 참조하는 포인터를 배열 요소로 사용해서 두 가지를 결합시킬 수도 있다. 앞에서 예제로 사용했던 구조체를 다시 한 번 살펴보도록 하자.
struct part{
int number;
char name[10];
};
구조체 part가 정의되면 part형 배열을 선언할 수 있다.
struct part data[100];
다음으로 part형에 대한 포인터를 선언하고, 포인터가 배열 data의 첫 번째 구조체를 지적하도록 초기화할 수 있다.
struct part *p_part;
p_part = &data[0];
괄호를 포함하지 않는 배열의 이름이 배열의 첫 번째 요소에 대한 포인터라는 것을 기억하자 그래서 두 번째 문장은 다음과 같이 작성할 수도 있을 것이다.
p_part = data;
이제 part형 구조체의 배열과 배열의 첫 번째 요소, 즉 배열의 첫 번째 구조체에 대한 포인터를 생성했다. 다음과 같은 문장을 사용하여 첫 번째 요소의 내용을 출력할 수 있을 것이다.
printf("%d %s", p_part->number, p_part->name);
만약 배열의 모든 요소를 출력하기 원한다면 어떻게 해야 할까? 아마도 순환문이 한 번 반복될 때마다 배열의 요소를 하나씩 출력하는 for문을 사용할 것이다. 포인터를 사용하여 구조체 멤버를 참조하기 위해서는 순환문이 반복될 때마다 포인터 p_part가 배열의 다음 요소, 즉 배열의 다음 구조체를 지적하도록 변경해 주어야 한다. 어떻게 해야 여기서 의도 하는 대로할 수 있을까? 여기서는 C의 포인터 연산을 사용할 필요가 있다. 단항 증가 연산자(++)는 포인터와 함께 사용될 때 특별한 의미를 가지게 된다. 즉, '포인터가 지적하는 데이터형의 크기를 반영하여 포인터의 값을 증가시켜라'는 것을 뜻한다. 예를 들어, obj형의 변수를 지적하는 포인터 ptr이 있다면 다음 문장은
ptr++;
다음과 같은 뜻을 가진다.
ptr += sizeof(obj);
포인터 연산의 이런 특징은 배열에서 특히 유용하게 사용된다. 배열의 각 요소는 메모리에 순서대로 저장된다. 포인터가 배열의 요소를 지적하고 있을 때 증가(++) 연산자를 사용하면 포인터는 n + 1번째의 요소를 지적하게 된다. <그림 11.6>에서는 4바이트 크기의 요소로 구성되는 x[]라는 이름의 배열을 예로 들어 이런 사실을 설명한다. 그림에서는 2바이트 길이의 두 int형 변수를 멤버로 가지는 구조체가 사용되고 있다. 포인터 ptr은 x[0]을 지적하도록 초기화되어 있고 ptr의 값이 증가될 때마다 배열의 다음 요소를 지적한다.
<그림 11.6> 포인터의 값이 증가될 때 포인터는 배열의 다음 요소를 '지적'한다.
결국, 프로그램에서 포인터를 증가시켜 구조체의 배열이나 다른 어떤 데이터형의 배열을 순서대로 사용할 수 있다는 사실을 알 수 있다. 이 방법은 같은 작업을 수행하기 위해서 배열의 첨자를 사용하는 것보다 쉽고 간단하다. <리스트 11.4>에 있는 프로그램은 지금까지 설명한 내용을 예제로 보여준다.
<리스트 11.4> 포인터를 증가시켜 배열의 요소를 순서대로 사용하는 프로그램
/* 포인터 표기를 사용하여 구조체의 배열을 차례대로 사용하는 예 */
#include <stdio.h>
#define MAX 4
/* 구조체를 정의 후 4구조체의 배열을 선언하고 초기화한다. */
struct part {
int number;
char name[10];
} data[MAX] = {1, "Smith", 2, "Jones", 3, "Adams", 4, "Wilson"};
/* part형에 대한 포인터와 카운터 변수 선언 */
struct part *p_part;
int count;
main()
{
/* 첫 배열 요소에 대한 포인터 초기화 */
p_part = data;
/* 순환할 때마다 포인터를 증가시키며 배열을 사용한다. */
for (count = 0; count < MAX; count++)
{
printf("\nAt address %d: %d %s", p_part, p_part->number, p_part->name);
p_part++;
}
return 0;
}
출력되는 주소값을 자세히 살펴보자. 실제 값은 시스템마다 달라질 수 있지만 간격은 일정하게 증가할 것이다. 이 간격은 구조체 part의 크기로 대부분의 시스템에서는 12가 될 것이다. 이 프로그램은 포인터를 증가시킬 때 포인터가 지적하는 변수의 크기를 반영하여 주소값이 증가된다는 사실을 분명히 보여준다.
5.4 함수의 인수로 구조체를 전달하는
: 다른 데이터형과 마찬가지로 함수의 인수로 구조체를 전달할 수도 있다. <리스트 11.5>에 있는 프로그램은 이런 사실을 증명한다. 이 프로그램은 <리스트 11.2>에 있는 프로그램을 수정한 것이다. 여기에서는 화면 상에 데이터를 출력하는 함수를 사용하는 반면에, <리스트 11.2>는 main()의 일부분에 해당하는 문장에서 데이터를 출력한다.
<리스트 11.5> 함수의 인수로 구조체를 전달하는 프로그램
/* 함수에 구조체를 전달하는 예 */
#include <stdio.h>
/* 데이터를 저장할 구조체 선언과 정의 */
struct data {
float amount;
char fname[30];
char lname[30];
} rec;
/* 함수 원형. 함수는 복귀값이 없고 하나의 인수로 data형의 구조체를 받아들인다. */
void print_rec(struct data x);
main()
{
/* 키보드에서 데이터 입력 */
printf("Enter the donor's first and last name,\n");
printf("separated by a space: ");
scanf("%s %s", rec.fname, rec.lname);
printf("\nEnter the donation amount: ");
scanf("%f", &rec.amount);
/* 출력 함수 호출 */
print_rec( rec );
return 0;
}
void print_rec(struct data x)
{
printf("\nDonor %s %s gave $%.2f.\n", x.fname, x.lname, x.amount);
}
구조체의 주소, 즉 구조체에 대한 포인터를 인수로 사용하여 함수에 구조체를 전달할 수도 있다. 이전에는 C에서 구조체를 인수로 사용하는 것이 유일한 방법이었다. 이제는 이렇게 구조체를 직접 전달하지 않지만, 아직까지도 가끔 구조체를 전달하는 경우를 볼 수 있다. 구조체에 대한 포인터를 인수로 전달하면 함수에서는 구조체 멤버를 참조하기 위해서 간접 멤버 참조 연산자(->)를 사용해야 한다는 것을 기억하기 바란다.
6. 공용체
: 공용체(unions)는 구조체와 비슷하다. 공용체는 구조체와 같은 방법으로 선언되고 사용된다. 공용체는 한 번에 하나의 멤버만이 사용될 수 있다는 점에서 구조체와 다르다. 그 이유는 간단하다. 공용체의 모든 멤버는 메모리에서 같은 영역을 차지하고 있다. 모든 멤버는 겹쳐져 있는 셈이다.
6.1 공용체 정의와 선언, 그리고 초기화
: 공용체는 구조체와 같은 방법으로 정의되고 선언된다. 선언문에서 유일한 차이점은 키워드 struct 대신에 union이 사용된다는 점이다. char 변수와 정수형 변수의 간단한 공용체를 정의하기 위해 다음과 같이 작성할 수 있다.
union shared {
char c;
int i;
};
이 공용체 shared는 문자값 c나 정수값 i의 하나를 가질 수 있는 공용체 변수(instance)를 생성하는 데 사용될 수 있다. 이것은 OR 조건문과도 같다. 두 값을 모두 가지게 되는 구조체와 달리 공용체는 한 번에 하나의 값만을 가질 수 있다.
<그림 11.7>은 shared 공용체가 메모리에 저장되는 방법을 보여준다.
<그림 11.7> 공용체는 한 번에 하나의 값을 가질 수 있다.
공용체를 선언하는 동시에 초기화할 수도 있다. 그러나 한 번에 하나의 멤버만이 사용될 수 있으므로 하나만 초기화될 수 있다. 일반적으로, 혼란을 막기 위해서 공용체의 첫 멤버만이 초기화될 수 있다 .다음 코드는 shared 공용체형 변수가 선언되고 초기화되는 것을 보여준다.
union shared generic_variable = ('@'};
6.2 공용체 멤버 사용하기
: 개별적인 공용체 멤버는 구조체 멤버와 마찬가지로 멤버 연산자인 마침표(.)를 이용해서 사용할 수 있다 .그러나 공용체 멤버를 사용할 때에는 중요한 한 가지 차이점이 있다. 한 번에 하나의 공용체 멤버만이 사용되어야 한다는 것이다. 공용체는 서로 겹쳐져 있으므로 한 번에 하나의 멤버를 사용하는 것은 매운 중요하다. <리스트 11.6>은 예제를 보여준다.
<리스트 11.6> 공용체의 잘못된 사용 예
/* 한번에 하나 이상의 공용체 멤버 사용 */
#include <stdio.h>
main()
{
union shared_tag {
char c;
int i;
long l;
float f;
double d;
} shared;
shared.c = '$';
printf("\nchar c = %c", shared.c);
printf("\nint i = %d", shared.i);
printf("\nlong l = %ld", shared.l);
printf("\nfloat f = %f", shared.f);
printf("\ndouble d = %f", shared.d);
shared.d = 123456789.8765;
printf("\nchar c = %c", shared.c);
printf("\nint i = %d", shared.i);
printf("\nlong l = %ld", shared.l);
printf("\nfloat f = %f", shared.f);
printf("\ndouble d = %f", shared.d);
return 0;
}
7. typedef와 구조체
: 구조체를 정의할 때에는 동의어인 typedef 키워드를 사용할 수 있다. 예를 들어, 다음 문장은 구조체에 대한 동의어로 coord를 사용할 수 있도록 정의해준다.
typedef struct {
int x;
int y;
} coord;
이제 coord를 사용하여 구조체형의 변수를 선언할 수 있다.
coord topleft, bottomright;
typedef는 이 장의 앞부분에서 설명한 구조체 태그와는 다르다는 것을 주의하자. 만약 다음과 같은 구조체를 정의하면
struct coord {
int x;
int y;
};
coord는 구조체의 태그로 사용된 것이다. 구조체 변수를 선언하기 위해서 이런 태그를 사용할 수 있지만 typedef와 달리 반드시 struct 키워드를 포함시켜야 한다
struct coord topleft, bottomright;
구조체를 선언할 때 typedef를 사용해야 하는지 구조체 태그를 사용해야 하는지의 여부는 결정하기 어려운 문제이다. typedef를 사용하는 경우에는 struct 키워드가 필요하지 않으므로 더 간결하다고 볼 수 있다. 반면에, 태그를 사용하고 struct 키워드를 포함시키면 구조체라는 사실을 분명히 알 수 있어 편리하다.
'pc관련 > C언어' 카테고리의 다른 글
고급 프로그램제어문 (0) | 2019.04.03 |
---|---|
C언어_변수에 범위 (0) | 2019.03.28 |
C언어_문자,문자열 (0) | 2019.03.24 |
C언어_포인터에 대하여... (0) | 2019.03.24 |
C언어_숫자배열사용 (0) | 2019.03.24 |