이번에는 C언어에서 사용되는 포인터에 대해 정리하겠습니다. 포인터는 주소 값을 이용하는 변수로서 애스터리스크(*)를 이용하여 가리키는 곳의 주소를 활용할 수 있습니다.
이번 정리에서는 아래와 같은 주제로 정리되어 있습니다.
포인터 | 단일 포인터 다중 포인터 메모리 동적 할당 메모리를 특정 값으로 초기화 |
배열 | 1차원 배열 2차원 배열 배열과 포인터의 응용 |
포인터의 사용
값을 변수에 저장하면 메모리 공간에 할당하는 방식으로 저장이 됩니다. 변수는 메모리의 특정 장소에 위치함으로 메모리 주소로도 표현할 수 있습니다.
변수의 메모리 주소를 구할 때 변수 앞에 주소 연산자(&)를 붙이고 서식 지정자를 pointer를 뜻하는 %p로 지정하거나 16진수 형태로 출력(%x)하면 메모리 주소를 확인할 수 있습니다. 이때, 메모리 주소는 고정되어 있지 않습니다.
포인터 변수를 선언하는 방법은 포인터 변수를 애스터 리스크(*)를 사용하여 해당 자료형의 변수를 만들고 메모리 주소를 포인터 변수에 저장하면 됩니다.
#include <stdio.h>
int main()
{
int a = 10;
int *b;
b = &a;
//포인터 변수에 a변수 주소를 저장하여 생성
//애스터 리스크(*)의 위치는 자료형 뒤와 변수 앞이면 상관없음
}
(자료형 *)는 (Pointer to 자료형)이라 읽으며 자료형을 가리키는 포인터라는 뜻으로 자료형 포인터라 합니다.
예로 (int *)은 int형 공간을 가리킵니다. 여기서 32bit의 포인터 크기는 4바이트로 16진수가 8자리이고, 64bit에서는 8바이트로 16진수의 자리가 16자리가 나옵니다.
포인터 변수를 사용하여 메모리 주소 값이 아닌 주소에 저장되어있는 값을 가져와 표현하고자 하면 역참조를 사용하여 가져오면 됩니다.
역참조를 하는 방법은 아래와 같으며 포인터는 변수의 메모리 주소만 가리키지만, 역참조는 주소에 접근하여 값을 가져오거나 저장할 수 있습니다. 역참조 연산자는 Pointer to 자료형에서 Pointer to를 제외하여 자료형만 남기는 방법입니다.
#include <stdio.h>
int main()
{
int a = 10;
int *b = &a;
printf("%d", *b);
*b = a;
// int *b는 포인터 변수를 선언
// printf함수의 *b는 역참조로 값을 반환하여 10을 출력
// 포인터를 역참조하여 값을 저장시킬 수 있음
}
역참조는 위와 같으며 쉽게 보기 위해 디버거를 활용하여 역참조를 확인해 보겠습니다. 디버거를 실행 후 디버그 메뉴에 아래 그림과 같이 메모리를 찾아 클릭하면 아래 창에 메모리와 관련된 창이 활성화될 것입니다.
메모리를 보기 쉽게 활성화된 창을 보면 16진수로 값들이 표현되어 있을 것입니다. 여기서 로컬 탭으로 돌아와 f11을 누르며 코드를 한 줄씩 실행시켜보면 포인터를 통해 주소 값이 저장되는 것을 확인할 수 있습니다.
아래 주소 값을 복사하여 메모리 탭에 붙여 넣으면 해당 메모리의 값이 어떻게 저장되어 있는지 확인할 수 있습니다.
int의 크기는 4byte이기에 16진수 8자리를 차지하며 0a 00 00 00으로 나타나 있는데 리틀 엔디안으로 표시되어 있기에 앞에 있는 0a는 뒤로 가 00 00 00 0a로 b에 저장되어 있는 주소 값의 값이 10인 것을 확인할 수 있습니다.
그렇기에 b에는 a의 16진수 주소 값이 저장되어 있는 것이고 주소 값에 해당되는 값은 10인 것입니다.
여기서 값들이 변화는 것을 확인하기 위해서는 아래와 같이 조사식을 확인하여 코드를 실행할 때 변화하는 값을 확인할 수 있습니다.
자료형이 정해져 있지 않은 void포인터를 선언할 수 있으며 자료형이 정해져 있지 않기에 자료형이 다른 포인터끼리 메모리 주소를 저장해도 컴파일 에러가 발생하지 않고 어떤 자료형이든 모두 저장할 수 있습니다. 반대로 다양한 자료형으로 된 포인터에도 void 포인터를 저장할 수 있습니다.
#include <stdio.h>
int main()
{
int a = 10;
float b = 10.0f;
void *c;
c = &a;
c = &b;
// void포인터에 int 자료형인 a주소 값이 들어갈 수 있습니다.
// void포인터에 float 자료형인 b주소 값이 들어갈 수 있습니다.
}
void포인터는 범용 포인터라고도 하며, 자료형이 정해져 있지 않아 값을 가져오거나 저장할 크기가 정해져 있지 않기에 역참조를 할 수 없습니다.
포인터는 단일 포인터만 있는 것이 아니고 다중 포인터도 존재합니다. 다중 포인터 중 이중 포인터가 주로 사용되며 이중 포인터는 애스터리스크(*)를 두 번 사용하여 포인터에 포인터를 참조하게 됩니다.
포인터도 변수이기에 메모리 주소를 구할 수 있습니다. 포인터의 메모리 주소는 일반적인 포인터에 저장할 수 없고 다중 포인터를 이용하여 저장하여야 합니다. 이중 포인터는 Pointer to Pointer to 자료형입니다.
#include <stdio.h>
int main()
{
int a = 10;
int *b = &a;
int **c = &b;
printf("%x", *c);
printf("%d". **c);
//이중 포인터 c는 포인터 b의 주소를 가지고 있습니다.
//처음 printf함수는 b의 값에 접근하여 a의 주소를 출력합니다.
//두 번째 printf함수는 b의 값에 접근하여 a의 주소 값을 확인하고 a의 주소 값에 접근하여 a의 값을 출력합니다.
}
포인터를 잘못된 방법으로 사용하는 경우가 있는데 메모리 주소를 직접 지정하여 포인터에 저장하는 경우가 대표적입니다. 직접 주소 값을 지정할 때 잘못된 주소 값을 지정하면 운영체제에서 프로그램이 잘못된 메모리 주소에 접근하면서 에러를 발생시킬 수 있습니다.
실제 존재하는 메모리 주소라면 포인터에 직접 저장이 가능하지만, 실제 주소를 사용하는 경우는 임베디드 시스템이나 마이크로 프로세서에서 제공하는 메모리 주소를 사용할 때 직접 저장할 수 있습니다.
지금까지 포인터에 변수의 메모리 주소를 저장하여 사용하였는데 <stdlib.h> 헤더 파일을 이용하여 malloc함수로 원하는 만큼의 메모리를 할당하여 사용할 수 있습니다.
malloc함수는 할당하여 사용 후 free함수로 할당 해제를 하는 방식으로 사용되며 메모리를 사용하기 위해 사용할 메모리 공간을 확보합니다. 메모리의 크기는 byte단위로 지정되며 memory allocation의 약자로 사용됩니다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n;
int* all;
scanf("%d", &n);
all = malloc(sizeof(int));
*all = n;
printf("%d", *all);
free(all);
//원하는 크기만큼 malloc함수를 이용하여 할당
//포인터 = malloc(크기);
//void *malloc(size_t _size);
//size_t는 typedef로 정의되어 만들어진 자료형이며 원하는 반환 자료형을 void부분에 형변환하면 됩니다.
//마지막에 free함수로 메모리를 회수해줍니다.
//free(포인터);
//void free(void *ptr);
}
동적 메모리 할당(Dynamic Memory Allocation)이라 하며, 동적과 정적 메모리 할당방식에 따라 메모리 주소의 차이가 존재합니다.
정적 메모리 할당인 변수를 생성하면 stack으로 생성되고 malloc함수를 이용하면 heap부분으로 메모리를 사용하게 됩니다.
스택, 힙의 위치와 커지는 방향은 운영체제 및 플랫폼에 따라 달라질 수 있으며, 위 그림은 windows 32bit 기준입니다.
malloc함수로 heap에서 할당한 메모리는 반드시 해제가 필요하며, 해제하지 않을 시 메모리 사용량이 계속 증가되는 메모리 누수가 발생됩니다.
위의 코드처럼 malloc함수로 정수 크기인 int크기만큼 메모리를 할당 후 malloc함수가 할당된 변수를 역참조하여 값을 할당하는 방식으로 값을 저장할 수 있습니다.
해당 자료형의 크기만큼 하나씩 할당하는 것이 아닌 메모리의 내용을 원하는 크기만큼 특정 값으로 설정하는 방법이 존재하는데 memset함수를 사용하면 됩니다.
memset함수는 <string.h>또는 <memory.h> 헤더 파일에 선언되어 있으며 메모리를 설정한다는 memory set을 의미합니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
long long *all;
all = malloc(sizeof(long long));
memset(all, 0x27, 8);
printf("%llx ", *all);
free(all);
//long long 크기 8바이트만큼 동적 메모리 할당
//all이 가르키는 메모리를 8바이트만큼 27으로 설정
//memset(포인터, 설정할 값, 크기);
//void *memset(void *_Dst, int _Val, size_t _size);
}
memset함수는 설정할 값을 0으로 지정하여 메모리의 내용을 모두 0으로 만들 때 주로 사용됩니다.
여태 정리한 것처럼 메모리가 할당된 포인터도 존재하지만 할당되지 않은 포인터도 존재합니다. NULL이 값으로 들어있으며 NULL포인터라 합니다.
#include <stdio.h>
int main()
{
int *po = NULL;
//NULL포인터 선언형식
//자료형 *포인터 이름 = NULL;
}
NULL포인터는 아무것도 가리키지 않은 상태를 뜻하고 그렇기에 역참조가 불가능합니다. 주로 포인터가 NULL인지 확인 후 NULL일 시 메모리를 할당하는 방식으로 사용됩니다.
배열의 사용
여러 데이터를 이용하여 값을 구할 때 간단하게 코딩하기 위해 배열을 사용합니다. 배열은 같은 자료형의 변수를 순서대로 저장한 형태입니다.
#include <stdio.h>
int main()
{
int arr1[3];
int arr2[3] = { 1, 2, 3 };
//배열의 선언과 초기화입니다.
//자료형 배열이름[크기];
//자료형 배열이름[크기] = { 값, 값, 값 };
}
배열의 이름과 크기만 지정한 것은 배열의 선언이고, 값까지 지정한 것은 초기화입니다. 이미 선언된 배열에는 중괄호를 이용하여 값을 초기화할 수 없으며 배열 초기화 시 값의 개수는 배열의 크기보다 크면 안 됩니다.
배열의 값이 저장된 공간을 요소라 하며 요소에 접근하기 위해서는 배열[인덱스] 형식으로 지정해주면 됩니다.
C언어에서는 배열의 인덱스가 0부터 시작하는데 메모리 주소가 0부터 시작하기에 배열의 첫 번째 요소의 인덱스는 0이 됩니다.
배열의 대괄호([])는 선언과 접근에 사용되는데 선언의 대괄호는 선언한 변수가 배열이고 크기는 얼마인지 알려주는 것이고, 사용할 때의 대괄호는 배열의 요소에 접근할 때 어느 인덱스에 접근하는지 알려줄 때 사용합니다.
배열의 모든 요소를 0으로 초기화하기 위해서는 초기화 시 아래와 같이 설정하면 모든 값이 0으로 되며 선언한 크기보다 작게 값을 초기화하여도 나머지 뒤의 값들은 0으로 초기화됩니다.
#include <stdio.h>
int main()
{
int arr1[3] = { 0, };
int arr2[3] = { 0 };
//0으로 초기화
//자료형 배열이름[크기] = { 0, };
//자료형 배열이름[크기] = { 0 };
}
배열의 요소에 값을 할당하는 방법으로 하나의 인덱스를 지정하여 값을 할당하는 방법이 있습니다. 배열의 크기에 벗어나는 요소에 접근하면 쓰레기 값인 전혀 다른 값이 출력되며 다른 메모리 공간에 값을 할당하거나 출력되기에 주의해야 합니다.
#include <stdio.h>
int main()
{
int arr1[3];
arr1[0] = 1;
printf("%d", arr1[0]);
//0번 인덱스의 값에 1을 할당
//배열[인덱스] = 값;
}
배열의 크기를 확인하는 방법으로는 sizeof연산을 이용하면 확인할 수 있습니다. 배열이 차지하는 전체 공간에서 자료형의 크기만큼 나누어 배열의 크기를 구할 수 있는데 sizeof(배열)이면 배열이 차지하는 전체 공간이 되고, sizeof(배열) / sizeof(자료형)이면 배열의 크기(요소의 개수)가 됩니다.
배열은 첫 번째 요소의 주소 값만을 가지고 있으므로 크기를 제외한 배열 이름만 sizeof연산에 넣어도 됩니다. 요소가 여러 개라도 첫 번째 요소의 주소 값을 가리키며 그렇기에 배열을 포인터에 넣을 수 있습니다.
배열을 포인터에 바로 할당할 수 있는데 자료형이 서로 같아야 하며 1차원 배열이면 애스터리스크(*)가 하나인 단일 포인터야 하고 다중이면 다중만큼 있어야 합니다.
#include <stdio.h>
int main()
{
int arr1[3];
int* po = arr1;
arr1[0] = 1;
printf("%d\n", *arr1);
printf("%d\n", arr1[0]);
printf("%d\n", *po);
printf("%d", po[0]);
//0번 인덱스의 값에 1을 할당
//배열의 주소 값인 첫 번째 인덱스를 포인터가 가지고 있기에 포인터를 역참조하면 0번 인덱스의 값을 확인할 수 있습니다.
}
배열과 배열을 할당한 포인터를 역참조하였을 때 둘 다 첫 번째 요소의 값이 나오는 것을 확인할 수 있고 인덱스를 지정하면 인덱스에 해당되는 값이 출력되는 것을 알 수 있습니다.
이것을 보면 배열도 포인터라 할 수 있으나 포인터와 다른 점은 sizeof연산으로 크기를 확인하면 다르게 나오는 것을 알 수 있습니다. 배열의 크기는 배열이 차지하는 전체 공간이 출력되지만, 포인터의 크기는 포인터의 크기만 나옵니다.
2차원 배열은 가로(행)(row)와 세로(열)(column) 크기를 가집니다. 2차원 배열의 선언은 세로 크기를 먼저 선언하고 그다음 가로 크기를 선언하는 방식으로 이루어집니다.
#include <stdio.h>
int main()
{
int arr1[3][3];
int arr2[3][3] = {{1, 2, 3}, {2, 3, 4}, {3, 4, 5}}
arr1[0][0] = 1;
printf("%d\n", arr1[0][0]);
//세로 0번, 가로 0번 인덱스의 값에 1을 할당
//자료형 배열 이름[세로 크기][가로 크기];
//자료형 배열 이름[세로 크기][가로 크기] = {{가로 크기}세로 크기}
//배열 이름[세로 인덱스][가로 인덱스] = 값;
}
선언은 배열의 이름과 크기만 지정하면 되고, 초기화할 시에는 중괄호 안에 중괄호를 넣어 값을 할당하는 방식입니다.
중괄호 안의 값은 가로 크기의 요소들로 이루어지고, 중괄호의 개수가 세로의 크기가 될 것입니다. 이외에는 1차원 배열과 비슷하여 따라 설명하지 않겠습니다.
2차원 배열을 포인터에 넣는 방법은 아래와 같이 지정합니다. 2차원 배열에서의 세로의 값은 주소 값을 가지고 있기에 포인터에 사용되지만, 가로는 값만 있기에 대괄호로 묶어줍니다.
#include <stdio.h>
int main()
{
int arr1[3][3] = { 0 };
int (*po)[3] = arr1;
printf("%p\n", *arr1);
printf("%p\n", *po);
printf("%d\n", arr1[0][0]);
printf("%d", po[0][0]);
//값을 0으로 초기화
//세로 첫 번째의 주소출력
//포인터에 2차원 배열 할당
//자료형 (* 포인터 이름)[가로 크기];
}
자료형과 가로 크기가 일치할 때 포인터에 할당이 가능합니다.
메모리 동적 할당을 활용하여 크기가 고정되지 않은 배열을 만들어 원하는 크기만큼 생성하는 기능을 사용해보겠습니다. 이 기능은 가변 길이 배열(Variable-Length Array, VLA)인데 기능을 지원하지 않아 C언어에서 동적으로 지정합니다.
malloc함수를 이용하여 1차원 배열, 2차원 배열을 동적으로 아래와 같이 동적으로 할당합니다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n;
int* po;
scanf("%d", &n);
po = malloc(sizeof(int) * n);
for (int i = 0; i < n; i++)
{
*po = i;
printf("%d ", *po);
}
free(po);
//동적 1차원 배열 할당
//입력받은 크기만큼 할당
//자료형 *포인터 이름 = malloc(sizeof(자료형) * 크기);
}
-------------------------------------------------------------------------
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int h, w;
int** po;
scanf("%d %d", &h, &w);
po = malloc(sizeof(int *) * h);
for (int i = 0; i < h; i++)
{
po[i] = malloc(sizeof(int) * w);
for (int j = 0; j < w; j++)
{
po[i][j] = i + j;
printf("%d ", po[i][j]);
}
printf("\n");
}
for (int i = 0; i < w; i++)
{
free(po[i]);
}
free(po);
//동적 2차원 배열 할당
//입력받은 크기만큼 할당 w는 행, h는 열
//자료형 **포인터 이름 = malloc(sizeof(자료형 *) * 크기);
//세로크기 할당
//반복문으로 입력받은 세로 크기만큼 반복하며 가로 크기 메모리 할당
//포인터 이름[반복되는 변수] = malloc(sizeof(자료형) * 크기);
//가로크기 할당
//해제 시 가로 크기만큼 반복
//free(포인터[반복 변수]);
//세로 크기 할당 해제
//free(포인터 이름);
}
이상으로 포인터에 대한 정리를 마치겠습니다. 다음에는 포인터를 활용한 문제들을 확인하며 정리해보겠습니다. 감사합니다!
'TeamH4C_ProjectH4C > 프로그래밍 언어' 카테고리의 다른 글
[TeamH4C_C언어] 문자열과 구조체 (0) | 2021.03.14 |
---|---|
[TeamH4C_C언어] 포인터를 활용한 문제 (0) | 2021.03.06 |
[TeamH4C_C언어] 다시 정리하는 CodeUp (0) | 2021.02.26 |
[TeamH4C_C언어] 반복의 loop (2) | 2021.02.21 |
[TeamH4C_C언어] 언어의 계산 (0) | 2021.02.21 |