포인터
어떠한 값을 저장하기 위해서는 값의 형태에 맞는 자료형의 변수를 선언해야 한다. 변수를 선언하면, 해당 변수는 컴퓨터의 메모리에 만들어진다. 자료형의 크기만큼 메모리에 공간을 할당한 후에, 그 곳에 값을 저장하는 식이다. 우리가 사는 집들 각각에 주소가 다른 것처럼 각각의 메모리 공간에는 서로 다른 주소가 존재한다. 우리는 변수에 접근하기 위해 변수의 이름을 지정하고 이름을 이용해 사용하지만, 메모리 상의 주소를 이용해서 직접적으로 접근할 수 있다. 접근 방법을 설명하기에 앞서, 주소에 관련된 연산자에 대해 알아야 한다. 주소 관련 연산자로는 &(주소 연산자)가 있다. 값과 값 사이에 존재하는 &는 비트 AND연산자이지만, 어떠한 변수 앞에 붙어있는 &는 해당 변수의 주소값을 출력하는 주소 연산자가 된다. 주소를 출력하고자 할땐 %p라는 서식 지정자를 사용해야 한다. p는 포인터(pointer)의 약자이고, 실제 출력 결과는 16진수 형태의 숫자가 출력된다. 결과물이 16진수이기 때문에 %x, %X로 출력해주어도 무방하다. 변수가 선언되고 메모리에 할당 될 때, 공간이 정해져있는 것이 아니기 때문에 출력할 때 마다 결과가 달라진다.
값을 저장할 땐 변수를 이용해 저장했지만, 만약 메모리 주소를 저장하고 싶다면 어디에 어떻게 저장해야 할까?
이에 대한 답이 바로 포인터이다. 주소를 저장할 수 있는 변수를 포인터 변수라고 하며, 선언시에 자료형 뒤에 *(Asterisk, 애스터리스크)을 붙여 선언한다. 포인터 변수를 선언하였다면, 변수에 주소를 저장해야 한다. 이 때, num1이 아닌 &num1로 작성해서 주소 값이 넘어가도록 해주어야 한다.
#include <stdio.h>
int main()
{
int *numPtr; // 포인터 변수 선언
int num1 = 10; // int형 변수를 선언하고 10 저장
numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장
printf("%p\n", numPtr);
printf("%p\n", &num1);
return 0;
}
포인터 변수의 자료형은 변수의 자료형에 맞춰야 한다. 위의 코드는 int형 변수 num1을 저장하기 때문에 int* numPtr로 선언해주었다.
역참조 연산자
애스터리스크(*)를 역참조 연산자라고도 한다. 주소 값이 저장된 포인터 변수에서 해당 주소에 저장된 값을 가져오기 위해 변수 앞에 역참조 연산자를 붙여준다.
printf("%d\n", *numPtr);
위의 코드를 실행시켜보면 num1의 주소가 아닌 주소에 저장되어있는 값을 출력한다. 출력 뿐만 아니라 값을 저장하는 등의 동작도 가능하다. *numPtr = 20 과 같이 역참조를 통해 포인터가 가리키는 위치에 20이라는 값을 저장할 수 있다. 이 때, num1을 출력해보면 10이 아닌 20이 출력되는 것을 볼 수 있다. numPtr 포인터에 num1의 주소가 저장되어 있기 때문에 numPtr 포인터를 통해 값을 바꾸면 실제 num1의 값이 바뀌게 된다.
numPtr은 &num1이 저장되어 있고, *numPtr은 *(&num1)이 된다. 이 때, *numPtr의 출력 값이 num1의 출력 값이 같기 때문에 *(&num1) = num1이라고 볼 수 있다. 그렇기에, 주소 연산자 앞에 역참조 연산자가 붙어있으면 이 둘이 무마된다고 이해하면 보다 쉽게 이해할 수 있다.
변수, 주소 연산자, 역참조 연산자, 포인터의 차이는 아래 그림과 같다.
자료형의 종류 중엔 정해지지 않은 자료형으로 void 자료형이 존재한다. C언어는 자료형이 다른 포인터를 사용하게 되면 컴파일에서 경고가 발생하는데, 자료형이 정해지지 않은 특성 때문에 void형 포인터를 선언한 후에 어떠한 자료형의 주소든 저장하고 실행해도 경고가 발생하지 않고 사용할 수 있다. 이러한 특성 때문에 void 포인터를 범용 포인터라고 한다. 하지만 값을 가져오거나 저장할 크기 또한 정해지지 않았기 때문에 void 포인터를 통한 역참조는 불가능하다.
이중 포인터
변수의 주소를 저장하는 변수를 포인터 변수라고 한다. 그렇다면 그러한 포인터 변수의 메모리 주소를 저장하는 포인터 변수를 사용하려면 어떻게 해야 할까. 그에 대한 답이 이중 포인터 변수이다.
기본적인 사용법은 포인터 변수와 똑같지만, 이중 포인터를 선언할 때에는 int **numPtr과 같이 *을 두 번 사용하여 선언해야 한다.
#include <stdio.h>
int main()
{
int *numPtr1; // 단일 포인터 선언
int **numPtr2; // 이중 포인터 선언
int num1 = 10;
numPtr1 = &num1; // num1의 메모리 주소 저장
numPtr2 = &numPtr1; // numPtr1의 메모리 주소 저장
printf("%d\n", **numPtr2); // 20: 포인터를 두 번 역참조하여 num1의 메모리 주소에 접근
return 0;
}
포인터도 변수이기 때문에 메모리 주소가 존재한다. 하지만 포인터 변수의 주소는 일반 포인터에는 저장할 수 없고 이중 포인터 변수를 선언해서 저장해주어야 한다. 위의 코드를 보면 num1의 주소가 numPtr1에, numPtr1의 주소가 numPtr2에 저장되어 있다. numPtr1에서 num1의 값을 가져오기 위해서는 역참조 연산자를 한 번 사용하여 *numPtr1과 같이 가져오지만, numPtr2에서 num1의 값을 가져오기 위해서는 역참조 연산자를 두 번 사용하여 **numPtr2로 사용해야 한다. 각 변수들의 관계를 보면 numPtr2->numPtr1->num1과 같은 모양이다.
이러한 포인터들의 특징을 이용해서, 변수 선언 없이 사용자가 원하는 위치에 직접 값을 저장할 수도 있다. 포인터 선언시에 int *num = 0x00CCFC2C와 같이 직접 메모리 주소를 입력하면 변수를 선언해서 할당 받지 않고도 직접 값을 저장해놓고 사용할 수 있다. 하지만 이 때, 주소 값이 실존하지 않는 값이라면 컴파일 에러가 발생하므로 주의해야 한다.
메모리 할당
위의 내용들은 먼저 선언한 변수의 주소를 포인터에 저장하는 방식으로 사용하였다. 하지만 변수를 선언하지 않고도 포인터에 원하는 만큼 공간을 할당받아서 사용하는 방법이 있다. 메모리를 사용하는 패턴은 malloc -> 사용 -> free이다.
malloc 함수
malloc(memory allocation)은 메모리의 공간을 확보하기 위한 C언어에서 제공하는 함수이다. 해당 함수는 stdlib.h 헤더 파일에 선언되어 있으며 사용법은 다음과 같다.
포인터 변수 = malloc(할당받을 크기);
void *malloc(size_t_Size); => 성공 시 메모리 주소를, 실패 시 NULL을 반환.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
int main()
{
int num1 = 20; // int형 변수 선언
int *numPtr1; // int형 포인터 선언
numPtr1 = &num1; // num1의 메모리 주소를 구하여 numPtr에 할당
int *numPtr2; // int형 포인터 선언
numPtr2 = malloc(sizeof(int)); // int의 크기 4바이트만큼 동적 메모리 할당
printf("%p\n", numPtr1);
printf("%p\n", numPtr2);
free(numPtr2); // 동적으로 할당한 메모리 해제
return 0;
}
위의 코드를 실행하면, numPtr1과 numPtr2의 주소가 각각 다르게 나오는 것을 확인할 수 있다.
변수를 선언하는 것과 malloc 함수를 통해 메모리를 할당하는 것에는 차이가 있다. 기본적으로 변수를 선언하면 스택(stack)에 생성되고, malloc 함수는 힙(heap)에 생성된다. 스택에 생성되는 변수는 사용한 뒤에 따로 처리 해주지 않아도 저절로 사라지지만, 힙에 할당한 메모리는 할당을 해제해주지 않고 종료하게 되면 공간을 차지한 채 종료되게 된다. 프로그램을 여러 번 실행할수록 공간을 많이 차지하게 되고 프로그램이 무거워지는 현상이 발생하는데, 이러한 현상을 메모리 누수(memory leak)이라고 부른다. 그렇기 때문에 malloc 함수를 통해 할당한 메모리는 프로그램 종료 전에 꼭 free함수로 해제해야 한다.
free(포인터);
memset 함수
할당 받은 메모리에 값을 저장하거나 바꾸는 방법은 포인터를 통해 변수의 값을 바꾸는 방법과 동일하게 역참조를 통해 할 수 있다. 이 때, 원하는 크기만큼 특정 값으로 설정할 수 있게끔 해주는 함수가 memset함수이다. memset함수는 string.h 또는 memory.h 헤더파일에 선언되어 있기 때문에 둘 중에 하나를 선언해야 사용할 수 있다. 사용법은 다음과 같다.
memset(포인터, 설정할 값, 크기);
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
#include <string.h> // memset 함수가 선언된 헤더 파일
int main()
{
long long *numPtr = malloc(sizeof(long long)); // long long의 크기 8바이트만큼 동적 메모리 할당
memset(numPtr, 0x27, 8); // numPtr이 가리키는 메모리를 8바이트만큼 0x27로 설정
printf("0x%llx\n", *numPtr); // 0x2727272727272727: 27이 8개 들어가 있음
free(numPtr); // 동적으로 할당한 메모리 해제
return 0;
}
먼저, long long타입의 포인터 변수 numPtr을 long long의 크기만큼 할당했다. 이 때 할당되는 메모리는 8바이트이다.
그 후에 memset을 통해 numPtr 변수가 가리키는 메모리에 8바이트 만큼 27을 채워넣는다. 그러면 numPtr의 메모리에 저장된 값은 0x2727272727272727이 된다.
#34.8 퀴즈
포인터의 선언은 자료형과 변수이름 사이에 *이 들어가면 되고, 띄어쓰기에 상관없이 int* numPtr, int * numPtr, int *numPtr 세 개 모두 같은 기능을 한다. 답은 c.
메모리 주소는 주소 연산자(&)를 변수 앞에 붙여서 구할 수 있다. 답은 d.
역참조 연산자(*)를 포인터 변수 앞에 붙여주면 역참조가 가능하다. 답은 a.
부호 없는 long 타입의 변수는 unsigned long이다. 이를 선언하는 법은 unsigned long *변수 이다. 답은 e.
삼중 포인터는 변수 선언시에 *을 3개 사용하면 된다. 답은 d.
#34.9 연습문제: 포인터와 주소 연산자 사용하기
#include <stdio.h>
int main()
{
int *numPtr;
int num1 = 10;
int num2 = 20;
① ________________
printf("%d\n", *numPtr);
②_________________
printf("%d\n", *numPtr);
return 0;
}
실행 결과
10
20
numPtr을 이용해 각각 num1, num2를 참조해서 해당 값들을 출력해야 한다. 참조 방법은 numPtr에 참조할 변수의 주소를 저장하면 된다.
답은 1. numPtr = &num1; 2. numPtr = &num2;
#34.10 심사문제: 포인터와 주소 연산자 사용하기
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int *numPtr1;
int **numPtr2;
int num1;
scanf("%d", &num1);
___________________
___________________
printf("%d\n", **numPtr2);
return 0;
}
입력 예시
10
출력 예시
10
입력 값을 num1에 저장한 후, 이중 포인터 numPtr2를 이용해 num1을 역참조해서 출력해야 한다. 먼저 포인터 변수 numPtr1에 num1의 주소를 저장한 후, numPtr2에 numPtr1의 주소를 저장하면 된다. 답은 아래와 같다.
numPtr1 = &num1;
numPtr2 = &numPtr1;
#35.5 퀴즈
포인터형 변수를 선언하고 malloc함수를 사용하면 된다. 답은 d.
malloc을 통해 메모리 할당 -> 메모리 사용 -> free를 통해 할당 해제. 답은 c.
포인터를 통해 값에 접근하는 법은 역참조 연산자(*)를 이용하는 것이다. 답은 b.
memset 함수는 메모리를 1바이트 단위로 지정한 값으로 초기화 시킨다. 답은 a.
memset 함수는 string.h과 memory.h 헤더파일에 선언되어 있다. 답은 d.
아무것도 가리키지 않는 포인터를 널(NULL) 포인터라고 한다.
#35.6 연습문제: 메모리 할당하기
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
int *numPtr1 = ①_________________;
long long *numPtr2 = ②____________________;
*numPtr1 = INT_MAX;
*numPtr2 = LLONG_MAX;
printf("%d %lld\n", *numPtr1, *numPtr2);
free(numPtr1);
free(numPtr2);
return 0;
}
동적 할당에 관련된 문제이다. 각각 int의 최대값과 long long int의 최대값을 넣어줄 수 있어야 하기 때문에, 각각 int, long long int의 크기만큼 할당해주어야 한다.
1. malloc(sizeof(int));
2. malloc(sizeof(long long));
#35.7 심사문제: 두 정수의 합 구하기
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num1;
int num2;
_________________________________
_________________________________
scanf("%d %d", &num1, &num2);
*numPtr1 = num1;
*numPtr2 = num2;
printf("%d\n", *numPtr1 + *numPtr2);
free(numPtr1);
free(numPtr2);
return 0;
입력 예시
10 20
출력 예시
30
num1, num2을 각각 참조하는 포인터를 선언해야 한다. 이 때, num1과 num2는 모두 int 형이므로 포인터의 타입도 int여야한다. 메모리 크기 또한 int형에 맞게 sizeof(int)로 해주면 된다.
int *numPtr1 = malloc(sizeof(int));
int *numPtr2 = malloc(sizeof(int));
int *numPtr1 = malloc(sizeof(int));
int *numPtr2 = malloc(sizeof(int));
'Programming > C' 카테고리의 다른 글
[ProjectH4C] 코딩도장 Unit 39~40 Write-Up (0) | 2020.07.23 |
---|---|
[ProjectH4C] 코딩도장 Unit 36~38 Write-Up (0) | 2020.07.23 |
[ProjectH4C] 코딩도장 Unit 33 Write-Up (0) | 2020.07.16 |
[ProjectH4C] 코딩도장 Unit 32 Write-Up (0) | 2020.07.16 |
[ProjectH4C] 코딩도장 Unit 27~31 Write-Up (0) | 2020.07.16 |