포인터란?
- 포인터란 기억공간의 주소를 가리키는(pointing) 자료형이다.
- C언어에서 변수가 선언되면 기억장소에 변수의 값을 저장하고 주소로 각각의 위치를 구별한다.
- 포인터 변수는 실질적인 데이터가 아니라 데이터가 저장된 기억장소의 주소를 저장한다.
- 어떤 변수든 차지하고 있는 기억공간의 영역이 존재하고, 그 영역은 반드시 주소(번지)가 부여되어 있으며 이에 대한 자료형이 포인터인 것이다.
- 포인터가 가리키는 변수의 값을 가져오는 것을 역참조(dereference)라고 한다.
- C언어에서 포인터 변수는 메모리의 주소를 지정하는 값을 가지며, 이를 사용하여 메모리의 데이터에 액세스한다.
- 변수의 메모리 주소는 CPU 설계 또는 운영체제에 의해 결정된다.
- 포인터 변수는 임의의 위치를 바꿀 수 있다는 유연성을 가지지만, 아직 정의되지 않은 메모리 영역을 액세스할 수 있다는 단점도 있다.
포인터 변수의 선언
자료형* 변수명;
또는 자료형 *변수명;
- C언어의 모든 자료형에 대해 포인터 변수를 만들 수 있다.
- C언어에서 포인터 변수를 선언하려면 변수명 앞에
*
를 붙인다는 것만 제외하면, 일반 변수의 선언 방식과 동일하다.
- 포인터 변수를 선언하고 사용하기 위해서
&
(주소 연산자), *
(내용 연산자)가 필요하다.
&
는 변수가 위치한 메모리 주소 값을 구하는 연산자 이다.
포인터 변수 = &포인터가 가리키는 변수명;
형식으로 주로 초기화한다.
- 즉, 일반 변수의 변수명 앞에
&
를 붙이면 그 변수의 주소를 의미한다.
- 반면에 포인터 변수는 그 자체가 주소이므로, 포인터 변수에 저장된 주소를 이용할 때는
&
를 붙이지 않는다.
- 포인터 변수에
&
를 붙이면 포인터 변수가 가리키는 객체의 주소가 아닌 포인터 변수 자체가 위치한 주소를 의미한다.
*
는 포인터 변수가 가리키는 객체의 내용을 가져오는 연산자 이다. 즉 역참조를 위한 연산자 이다.
*
는 또한 곱셈을 하는 이항연산자이기도 하며, 포인터 변수를 선언하거나 함수의 매개변수로 포인터를 지정할 때도 사용되므로 혼동하지 않도록 주의해야 한다.
- 한 변수의 자료형과 그 변수를 가리키는 포인터의 자료형이 일치하지 않으면 여러 문제가 발생하기 때문에 변수와 포인터의 자료형은 반드시 일치해야 한다.
- C언어의 일반 자료형들은 그 종류에 따라 크기가 다양하지만 포인터의 크기는 자료형의 종류에 따라 다르지 않고 모두 같다.
- 보통 단독주택 보다 아파트가 차지하는 땅이 더 넓지만 주소의 길이는 큰 차이가 없다는 것과 비슷하다.
- 포인터의 크기는 어떻게 컴파일 하는 지에 따라 결정된다.
- 일반적으로 32비트로 컴파일하면 포인터의 크기는 4바이트, 64비트로 컴파일하면 8바이트 이다.
#include <stdio.h>
int main()
{
int a, b;
a = 123;
int *a_ptr;
a_ptr = &a;
int *ptr = (int *)0xFF000000;
printf("%d %d %p\n", a, *a_ptr, a_ptr);
*a_ptr = 456;
printf("%d %d %p\n", a, *a_ptr, a_ptr);
b = *a_ptr;
printf("%d\n", b);
*a_ptr = 789;
printf("%d %p\n", b, &b);
printf("%d %d %p\n", a, *a_ptr, a_ptr);
b = 12;
printf("%d %p\n", b, &b);
printf("%d %d %p\n", a, *a_ptr, a_ptr);
a = 1004;
printf("%d %d %p\n", a, *a_ptr, a_ptr);
return 0;
}
포인터의 코딩 스타일
- 포인터 변수 선언 시 필요한
*
의 위치에 따라 코딩 스타일이 달라진다.
*
의 위치에 따라 실제로 코드가 동작하는 방식도 바뀔 수 있기 때문에 주의해야 한다.
- 주로 사용되는 방식은 다음의 세 가지가 있다.
- 자료형 뒤에 붙이는 방식:
int* ptr;
- 변수명 앞에 붙이는 방식:
int *ptr;
- 변수명 앞에 빈칸을 넣는 방식:
int * ptr
- 빈칸을 넣는 방식은 많이 쓰이지 않는다.
- 일관성 있게 한 가지만 사용하여 코딩하는 게 좋다.
int *a, b;
a만 포인터이고, b는 그냥 int 변수이다.
int* a, b;
와 int *a, *b;
는 변수 a, b 모두 포인터로 선언한 것이다.
void 포인터와 NULL 포인터의 사용
- 컴파일 타임에 자료형을 결정하지 못하고 런타임에 결정하는 경우가 있다.
- 이를 위해
void
형 포인터가 사용된다.
void* vptr;
다른 자료형의 포인터와 선언 방법은 같다.
void
포인터는 어떤 형태의 포인터라도 모두 저장할 수 있다.
- 하지만
void
포인터에 어떤 변수의 주소를 저장하기 전에 반드시 명시적 형변환을 해줘야 한다.
NULL
포인터란 NULL
매크로를 활용하여 포인터를 초기화하는 것을 말한다.
- NULL 포인터는 아무것도 가리키지 않는 상태를 의미한다. 그러므로 역참조를 할 수 없다.
- NULL 포인터를 역참조하면 여러 문제가 발생하므로 이를 방지하는 코드가 필요하다.
- 실무에서도 포인터가 NULL이면 메모리를 할당하거나 NULL이 아닌 경우에만 역참조하는 패턴을 자주 사용한다.
#include <stdio.h>
int main()
{
int a = 100;
char b = 'b';
void* void_ptr = NULL;
void_ptr = (int*)&a;
printf("*void_ptr = %d\n", *(int*)void_ptr);
int *safer_ptr = NULL;
int num = 123;
int c;
scanf("%d", &c);
if (c % 2 == 0)
safer_ptr = #
if (safer_ptr != NULL) {
printf("%p\n", safer_ptr);
printf("%d\n", *safer_ptr);
}
return 0;
}
포인터 매개변수와 참조에 의한 호출
- 포인터는 함수의 매개변수로도 사용될 수 있다.
- 매개변수로 일반 변수를 사용하면
값에 의한 호출(call by value)
로만 사용할 수 있다.
- 값에 의한 호출을 하면 함수의 인수로 넣은 변수 자체가 변경되지 않는다.
- 왜냐하면 인수로 넣은 변수의 범위가 호출된 함수의 안쪽으로 한정되기 때문이다.
- 즉, 인수의 값만 복사되어 함수 안에서 활용된다.
- 함수의 인수로 넣은 변수 자체를 변경하려면
참조에 의한 호출(call by reference)
을 해야 한다.
- 그러나 C언어에서 모든 함수는
값에 의한 호출만
가능하다.
- C언어에서 참조에 의한 호출과 비슷한 효과를 보려면 포인터 매개변수가 필요하다.
- C언어에서 함수에 포인터 매개변수를 쓰면 변수의 값 자체가 아닌 변수가 위치한 주소가 함수에 입력된다.
- 포인터 매개변수를 쓴다고 참조에 의한 호출을 하는 것은 아니며 함수 호출의 결과가 참조에 의한 호출을 하는 것과 동일할 뿐이다.
- 따라서 C언어에서 포인터 매개변수를 쓰는 방식을
주소에 의한 호출(call by address)
이라 부르기도 한다.
#include <stdio.h>
void swap_value(int a, int b) {
printf("swap by value\n");
printf("argument address: ");
printf("%p %p\n", &a, &b);
int temp = a;
a = b;
b = temp;
}
void swap_address(int *, int *);
void swap_address(int *a, int *b) {
printf("swap by address\n");
printf("argument address: ");
printf("%p %p\n", &a, &b);
printf("argument value: ");
printf("%p %p\n", a, b);
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 123;
int b = 456;
printf("main() var adress\n");
printf("%p %p\n", &a, &b);
printf("\n");
swap_value(a, b);
printf("%d %d\n", a, b);
printf("\n");
swap_address(&a, &b);
printf("%d %d\n", a, b);
return 0;
}
- C언어에선 참조에 의한 호출을 할 수 없지만,
C++
에선 참조형(reference, &
)을 지원하며, 함수를 정의할 때 매개변수에 &
를 사용하면 참조에 의한 호출을 할 수 있다.
#include <iostream>
void swap_reference(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 123;
int y = 456;
int &xref = x;
int &yref = y;
std::cout << "Before swap: x = " << x << ", y = " << y << "\n";
swap_reference(xref, yref);
std::cout << "After swap: x = " << x << ", y = " << y << "\n";
return 0;
}
배열과 포인터
- C언어의 다른 자료형들과는 다르게 배열의 이름은 포인터같이 쓸 수 있다.
- 배열은 특정 자료형의 집합이며 배열의 각 요소들은 메모리에 연속적으로 저장된다.
- 배열의 이름만 쓰면 배열이 시작하는 첫 번째 요소의 주소로 암시적 변환이 된다.
- 포인터는 곧 주소이므로, 결국 배열명을 포인터 같이 쓸 수 있게 된다.
- 배열과 포인터가 서로 호환적이긴 하지만, 완전히 같은 것은 아니다.
- 포인터 변수는 가리키는 주소를 변경할 수 있지만, 배열은 불가능하다.
- 배열은 메모리 레이아웃의 데이터 영역을 고정적으로 확보하는 데 비해 포인터는 유동적으로 확보한다.
- 즉, 포인터는 필요할 때만 기억공간을 확보하고 필요가 없으면 확보하지 않는다.
- 따라서 자료의 개수가 가변적인 경우에는 배열보단 포인터를 사용하는 것이 효율적이다.
- 이는 메모리 동적할당(
malloc()
등)과 관련이 있다.
char *cp = "COMPUTER";
문자열에 대한 포인터는 이런식으로도 만들 수 있다.
- 문자열은 곧 문자 배열이므로 이러한 할당이 가능하다.
- 배열의 특정 위치에 대한 포인터를 만드려면
배열명[인덱스]
에 주소 연산자 &
를 붙여야 한다.
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
int* p1 = &arr[0];
int* p2 = &arr[1];
- 다차원배열에 대한 포인터는 배열명으로 선언하거나 다차원배열의 첫번째 요소를 참조하는 방식으로 선언할 수 있다.
- 다차원배열도 물리적으로는 1차원적으로 기억공간에 저장되기 때문이다.
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int* parr1 = arr;
int* parr2 = arr[0];
int* parr3 = &arr[0][0];
printf("%d %d %d\n", *parr1, *parr2, *parr3);
포인터의 연산
- 포인터의 연산은 C언어의 숫자 자료형(int, float, double 등)과는 다른 방식이 적용된다.
- 포인터의 실제 값(주소)은 포인터가 가리키는 자료형의 크기 단위로 증가 또는 감소할 수 있다.
- 예를 들면
int* p
라는 int 자료형의 포인터 p
가 있다.
- int의 크기는 일반적으로 4바이트 이므로
p + 1
은 포인터 p가 가리키는 주소에서 4를 더한 주소를 의미한다.
- 따라서 포인터 변수에 1을 증감하는 것은 포인터가 가리키는 번지를 중심으로 한 단위 이전 또는 이후의 주소를 가리키는 것이다.
- 이러한 포인터의 특성을 이용하여 배열의 각 요소를 접근하기 위해 포인터를 활용할 수 있다.
- 포인터 변수에는 + 또는 -부호를 붙일 수 없다.
- 포인터의 연산은 덧셈과 뺄셈만 가능하다.
- 포인터와 포인터 간의 덧셈과 뺄셈은 불가능하지만, 두 포인터가 같은 배열을 가리키는 경우에는 뺄셈이 가능하다.
- 이 때 뺄셈의 결과는 두 포인터 사이의 거리를 나타낸다.
void
포인터는 산술 연산을 할 수 없다.
- 역참조 연산자
*
와 함께 포인터 연산을 할 때는 괄호에 유의해야 한다.
*(p + 2)
는 포인터 p의 주소로부터 2단위 뒤에 있는 주소의 내용을 의미하는 것이다.
*p + 2
는 p번지의 내용에 2를 더한 값을 의미하는 것이다.
#include <stdio.h>
int main() {
int arr[10];
int num = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < num; ++i)
arr[i] = (i + 1) * 100;
int *ptr = arr;
printf("%p %p %p\n", ptr, arr, &arr[0]);
ptr += 2;
printf("%p %p %p\n", ptr, arr + 2, &arr[2]);
printf("%d %d %d\n", *(ptr + 1), *(arr + 3), arr[3]);
printf("%d %d %d\n", *ptr + 1, *arr + 3, arr[3]);
for (int i = 0, *ptr = arr; i < num; ++i)
printf("%d %d\n", *ptr++, arr[i]);
for (int i = 0, *ptr = arr; i < num; ++i)
printf("%d %d\n", *(ptr + i), arr[i]);
for (int i = 0, *ptr = arr; i < num; ++i)
printf("%d %d\n", *ptr + i, arr[i]);
return 0;
}
*
연산자와 함께 증감 연산자(++
또는 --
)를 사용할 경우 연산자 우선순위에 유의해야 한다.
*p++
:
*p
는 포인터 p
가 가리키는 주소의 값을 가져온다.
p++
는 포인터 p
를 다음 위치로 증가 시킨다.
- 이 연산은 후위 증가 연산자로, 먼저
p
위치의 값을 가져와서 사용하고, 그 다음에 p
를 1 증가 시키는 것이다.
*++p
:
++p
는 포인터 p
를 다음 위치로 증가 시킨다.
*++p
는 증가 시킨 위치를 가리키는 포인터 p
의 값을 가져온다.
- 이 연산은 전위 증가 연산자로, 먼저 포인터
p
를 1 증가 시키고, 증가 된 p
위치의 값을 가져오는 것이다.
++*p
:
*p
는 포인터 p
가 가리키는 주소의 값을 가져온다.
++*p
는 해당 주소의 값을 1 증가 시킨다.
- 이 연산은 전위 증가 연산자로, 먼저
p
를 1 증가 시키고, 증가 된 p
위치의 값을 가져오는 것이다.
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
printf("%d\n", *p++);
printf("%d\n", *p);
printf("%d\n", *++p);
printf("%d\n", *p);
printf("%d\n", ++*p);
printf("%d\n", *p);
return 0;
}
#include <stdio.h>
int main() {
void* vptr = NULL;
int* ptr = 0;
printf("%p %lld\n", ptr, (long long)ptr);
ptr += 1;
printf("%p %lld\n", ptr, (long long)ptr);
int arr[10];
int *ptr1 = &arr[3], *ptr2 = &arr[5];
int i = ptr2 - ptr1;
printf("%p %p %d\n", ptr1, ptr2, i);
return 0;
}
#include <stdio.h>
int main() {
int a = 0;
int arr[5] = {100, 200, 300, 400, 500};
int *ptr1, *ptr2, *ptr3;
int i;
ptr1 = arr;
printf("%p\n", arr);
printf("%p %d %p\n", ptr1, *ptr1, &ptr1);
ptr2 = &arr[2];
printf("%p %d %p\n", ptr2, *ptr2, &ptr2);
ptr3 = ptr1 + 4;
printf("%p %d %p\n", ptr3, *ptr3, &ptr3);
printf("%td\n", ptr3 - ptr1);
ptr3 = ptr3 - 4;
printf("%p %d %p\n", ptr3, *ptr3, &ptr3);
if (ptr1 == ptr3)
printf("Same\n");
else
printf("Different");
double d = 3.14;
double *ptr_d = &d;
if (ptr1 == (int*)ptr_d)
printf("Same\n");
else
printf("Different\n");
return 0;
}
포인터 배열
- 포인터 배열이란 말 그대로 포인터로 이루어진 배열을 말한다.
- 일반 배열처럼 동일한 속성의 포인터를 여러 개 담을 수 있는 배열이다.
char *names[3];
일반 배열처럼 선언하고 *
만 붙여주면 포인터 배열을 선언할 수 있다.
- 포인터 배열은 2차원 배열과 비슷하게 동작하지만, 기억공간을 더 효율적으로 관리할 수 있다.
- 예를 들어 배열
char carr[4][20];
은 char 자료형의 크기가 1바이트 이므로 20바이트 짜리 문자열 4 개가 있는 배열이다.
- 따라서 배열
carr
은 요소의 내용과 관계없이 고정된 80바이트의 기억공간을 차지 한다.
- 그러나 포인터 배열
char *parr[4];
은 포인터 4개의 크기만큼만 차지 한다.
- 따라서 배열과 포인터 배열의 내용이 서로 같아도 두 배열의 크기는 다를 수 있다.
int (*ptr)[3]
와 int *ptr[3]
는 서로 다른 종류의 포인터 변수이다. 괄호에 의해 의미가 완전히 달라진다.
int (*ptr)[3]
: 1차원 배열을 가르키는 포인터이고 각 요소는 크기가 3인 정수형 배열을 가리키는 것이다. 이런 표현은 주로 다차원 배열에 대한 포인터를 만들 때 자주 쓰인다. int arr[2][3]; int (*ptr)[3] = arr;
int *ptr[3]
: int형을 가리키는 포인터들의 배열을 의미한다.
#include <stdio.h>
int main() {
char carr[4][20] = {"ASDF", "QWERTYUIOP", "ABC", "FOOBAR"};
char *parr[4];
parr[0] = carr[0];
parr[1] = carr[1];
parr[2] = carr[2];
parr[3] = carr[3];
printf("size of carr: %d\n", (int)sizeof(carr));
printf("size of parr: %d\n", (int)sizeof(parr));
return 0;
}
#include <stdio.h>
int main() {
int arr0[3] = {1, 2, 3};
int arr1[3] = {4, 5, 6};
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int *parr0 = arr[0];
int *parr1 = arr[1];
for (int i = 0; i < 3; ++i)
printf("%d ", parr0[i]);
printf("\n");
for (int i = 0; i < 3; ++i)
printf("%d ", parr1[i]);
printf("\n");
int *parr[2];
parr[0] = arr[0];
parr[1] = arr[1];
printf("\n");
for (int j = 0; j < 2; ++j) {
for (int i = 0; i < 3; ++i)
printf("%d %d %d %d\n", arr[j][i], parr[j][i], *(parr[j] + i), *(*(parr + j) + i));
printf("\n");
}
printf("%p\n", &parr[0]);
printf("%p\n", parr[0]);
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%p\n", &arr[0]);
printf("%p\n", arr[0]);
printf("%p\n", &arr[0][0]);
printf("\n");
printf("%zd\n", sizeof(char *));
char *name[] = {"Aladdin", "Jasmine", "Magic Carpet", "Genie"};
const int n = sizeof(name) / sizeof(char*);
for (int i = 0; i < n; ++i)
printf("%s at %u\n", name[i], (unsigned)name[i]);
printf("\n");
char aname[][15] = {"Aladdin", "Jasmine", "Magic Carpet", "Genie"};
const int an = sizeof(aname) / sizeof(char[15]);
for (int i = 0; i < an; ++i)
printf("%s at %u\n", aname[i], (unsigned)&aname[i]);
printf("\n");
return 0;
}
#include <stdio.h>
int main() {
float arr2d[2][4] = {{1.0f, 2.0f, 3.0f, 4.0f}, {5.0f, 6.0f, 7.0f, 8.0f}};
float (*pa)[4];
float* ap[2];
printf("%zu\n", sizeof(pa));
printf("%zu\n", sizeof(ap));
printf("\n");
pa = arr2d;
ap[0] = arr2d[0];
ap[1] = arr2d[1];
printf("%u %u\n", (unsigned)pa, (unsigned)(pa + 1));
printf("%u %u\n", (unsigned)arr2d[0], (unsigned)arr2d[1]);
printf("%u %u\n", (unsigned)pa[0], (unsigned)(pa[0] + 1));
printf("%f\n", pa[0][0]);
printf("%f\n", *pa[0]);
printf("%f\n", **pa);
printf("%f\n", pa[1][3]);
printf("%f\n", *(*(pa + 1) + 3));
printf("\n");
printf("%u %u\n", (unsigned)ap, (unsigned)(ap + 1));
printf("%u %u\n", (unsigned)arr2d[0], (unsigned)arr2d[1]);
printf("%u %u\n", (unsigned)ap[0], (unsigned)(ap[0] + 1));
printf("%f\n", ap[0][0]);
printf("%f\n", *ap[0]);
printf("%f\n", **ap);
printf("%f\n", ap[1][3]);
printf("%f\n", *(*(ap + 1) + 3));
return 0;
}
포인터에 대한 포인터(이중 포인터)
- 포인터 변수를 가리키는 포인터를 만들 수 있다.
- 이를 이중 포인터라고 한다.
- 즉 이중 포인터는 자신이 가리키는 포인터 변수의 주소 값을 저장한다.
- 이중 포인터를 가리키는 삼중 포인터도 만들 수 있고 그 이상의 다중 포인터도 만들 수 있다.
*
를 연달아 써서 선언한다.
- 이중 포인터가 가리키는 포인터가 가리키는 값을 참조하려면
**
를 써야 한다.
#include <stdio.h>
int main() {
char a = 'A';
char *p = &a;
char **pp = &p;
printf("%p\n%p\n%p\n", &a, p, *pp);
printf("\n");
printf("%p\n%p\n", &p, pp);
printf("\n");
printf("%c %c %c\n", a, *p, **pp);
return 0;
}
함수 포인터
- 함수 포인터는 함수를 가리키는 포인터 이다.
- 함수 포인터를 선언할 때는 함수를 정의할 때와 마찬가지로 매개변수와 반환 값의 자료형을 명시해야 한다.
- 함수 포인터 선언 시 배열명과 비슷하게 함수명 앞에 주소 연산자
&
를 붙이지 않아도 된다.
- 함수의 이름 자체가 포인터로 변환되기 때문이다.
- 프로그래머는 함수의 이름을 이용하여 프로그래밍 하지만, 컴파일러에선 함수명을 메모리 주소로 인식한다.
- 즉, 함수를 실행하는 것은 함수의 주소에 있는 명령어들을 순차적으로 실행하는 것과 같다.
#include <stdio.h>
void f1() {
printf("It is f1().\n");
return;
}
int f2(char i) {
return i + 1;
}
int main()
{
void (*pf1)() = f1;
int (*pf2)(char) = f2;
(*pf1)();
pf1();
int a = pf2('A');
printf("%d\n", a);
return 0;
}
#include <ctype.h>
#include <stdio.h>
void ToUpper(char *str)
{
while (*str) {
*str = toupper(*str);
str++;
}
}
void ToLower(char *str)
{
while (*str) {
*str = tolower(*str);
str++;
}
}
void UpdateString(char *str, int (*pf)(int))
{
while (*str) {
*str = (*pf)(*str);
str++;
}
}
int main()
{
char str[] = "Hello, World!";
void (*pf)(char *);
pf = ToUpper;
printf("String literal %lld\n", (long long)("Hello, World!"));
printf("Function pointer %lld\n", (long long)ToUpper);
printf("Variable %lld\n", (long long)str);
(*pf)(str);
printf("ToUpper %s\n", str);
pf = ToLower;
pf(str);
printf("ToLower %s\n", str);
UpdateString(str, toupper);
printf("ToUpper %s\n", str);
UpdateString(str, tolower);
printf("ToLower %s\n", str);
return 0;
}
구조체, 공용체와 포인터
- C언어의 구조체는 배열과는 다르게 여러 가지의 자료형을 조합해서 사용자 정의하여 만들 수 있는 자료형이다.
- 구조체 안에 속한 변수를 멤버 변수라고 한다.
- 공용체는 구조체와 비슷하게 여러 자료형의 멤버 변수를 정의하여 사용할 수 있는 자료형이다.
- 구조체의 모든 멤버 변수들은 독립적이고, 구조체의 크기는 멤버 변수들의 자료형과 패딩에 따라 결정된다.
- 반면에 공용체의 모든 멤버 변수들은 같은 메모리 공간을 공유한다.
- 따라서 공용체의 멤버 변수 중 가장 큰 것의 크기가 곧 공용체의 크기가 된다.
- 구조체는 동시에 여러 개의 멤버 변수에 접근할 수 있지만, 공용체는 한 번에 하나의 멤버 변수에만 접근할 수 있다.
- 구조체는
struct
, 공용체는 union
키워드로 선언한다.
- 구조체 또는 공용체에 대한 대한 포인터를 만들 수 있다.
- 구조체와 공용체에 대한 포인터 선언과 초기화는 일반적인 포인터와 동일하게 할 수 있다.
- 구조체와 공용체의 멤버 변수에 접근하려면 연산자
.
를 사용해야 한다.
- 구조체와 공용체의 포인터로 멤버 변수에 접근하려면
.
이 아닌 ->
를 사용해야 한다.
.
은 *
보다 연산자 우선순위가 높기 때문에 그런 것이며, 포인터 변수에 괄호를 사용하면 .
를 쓸 수 있다. (*sptr).member
- 구조체와 공용체에 대해
typedef
키워드로 별칭을 만들어서 사용하면 편리하다.
- typedef 선언과 구조체 또는 공용체를 동시에 정의하면 이름을 생략할 수 있다.
#include <stdio.h>
struct MyStruct {
int number;
};
typedef struct {
char name[20];
int age;
} Student;
typedef union {
int intValue;
float floatValue;
} MyUnion;
int main() {
struct MyStruct s;
s.number = 123;
Student s1 = {"John", 20};
Student *sptr = &s1;
printf("%s %s\n", s1.name, sptr->name);
printf("%d %d\n", s1.age, sptr->age);
MyUnion u1;
u1.intValue = 123;
MyUnion *uptr = &u1;
printf("%d %d\n", u1.intValue, uptr->intValue);
u1.floatValue = 3.14f;
printf("%f %f\n", u1.floatValue, uptr->floatValue);
printf("%d %d\n", u1.intValue, uptr->intValue);
return 0;
}