Unit 34. 포인터 사용하기

값을 저장할 때 사용하는 변수는 컴퓨터의 메모리에 생성된다. 메모리에 일정한 공간을 확보해 두고 원하는 값을 저장하거나 가져오는 방식이다.

변수는 num과 같이 지정된 이름으로 사용되지만, 메모리의 특정 장소에 위치하므로 메모리 주로로도 표현할 수 있다.

메모리 주소는 다음과 같이 출력할 때변수 앞에 &을 붙이면 된다.

1
2
3
4
5
6
7
8
#include <stdio.h>
 
int main()
{
    int num1 = 10;
    printf("%p\n", &num1);
    return 0;
}
cs

메모리주소는 위와 같이 16진수 형태이며 서식지정자 %p를 사용하여 출력한다. 16진수를 의미하는 %x나 %X를 이용해도 된다. 메모리주소는 고정된 값이 아니기 때문에 컴퓨터마다, 실행할 때 마다 달라진다.

시스템이 32비트인지, 64비트인지에 따라 메모리 주소의 범위도 달라진다. 각각의 범위는 다음과 같다.

  • 32비트: 16진수 8자리

  • 0x00000000 ~ 0xFFFFFFFF

  • 예) 0x008AF7FC

  • 64비트: 16진수 16자리

  • 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF

  • 예) 0x00000000008AF7FC

  • 64비트 메모리 주소는 0x00000000`00000000처럼 8자리씩 끊어서 `를 붙이기도 한다

리눅스나 맥의 osX에서는 서식지정자 %p를 사용하며 주소 앞에 0x를 붙이고, a~f는 소문자로 출력되며 높은 자리수의 0은 생략된다.

 

34.1 포인터 변수 선언하기

C에서 메모리 주소는 포인터 변수에 저장한다.

포인터 변수는 다음과 같이 *을 이용하여 선언한다.

자료형 *포인터이름;
포인터 = &변수;
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main()
{
    int *numPtr;      
    int num1 = 10;   
    numPtr = &num1;  
 
    printf("%p\n", numPtr);   
    printf("%p\n"&num1);     
    return 0;
}
cs

포인터 변수를 선언할 때는 자료형 뒤에 *(Asterisk, 애스터리스크)를 붙인다. *위치는 자료형과 변수 사이에 있다면 어디에 있던 상관 없다.

&로 변수의 주소를 구해서 포인터 변수에 저장할 수도 있다.

printf로 포인터 변수 numPtr을 출력해보면 num1의 메모리 주소와 포인터 변수는 같은 값이 나온다. 포인터와 메모리 주소는 같은 의미이다.

포인터 변수를 선언할 때는 자료형을 알려주고 *을 붙여야 한다. 만약 변수가 int형이면 이 변수를 저장하는 포인터는 int *로 지정해야 한다.

포인터는 메모리의 특정 위치를 가리킬 때 사용한다. 포인터가 메모리를 가리키는것을 그림으로 확인하면 다음과 같다.

 

34.2 역참조 연산자 사용하기

포인터 변수에는 메모리 주소가 저장되어 있는데, 이 포인터 주소를 이용하여 값을 가져오고 싶으면 역참조 연산자 *을 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    int *numPtr;      
    int num1 = 10;  
    numPtr = &num1; 
    
    printf("%d\n"*numPtr);    // 역참조 연산자 사용
    return 0;
}
cs

역참조 연산자 *는 포인터 앞에 붙인다. 위와 같이 포인터 변수 앞에 *을 붙이면 그 변수에 저장된 메모리 주소로 가서 값을 가져온다.
다음은 포인터 변수에 역참조 연산자를 사용하여 값을 할당한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
int main()
{
    int *numPtr;      
    int num1 = 10;   
    numPtr = &num1;  
    *numPtr = 20;    
 
    printf("%d\n"*numPtr);    
    printf("%d\n", num1);       
    return 0;
}
cs

역참조 연산자로 메모리에 접근하여 값을 저장할 수도 있다. 위 num1 변수에는 10이 들어있었는데 num1을 가리키는 포인터변수 numPtr에 역참조 연산자로 20을 저장했기 때문에 num1 과 numPtr을 출력하면 둘 다 20이 나온다.
만약 포인터 변수 numPtr에 num1을 할당하면 간접 참조 수준이 다르다는 경고가 발생한다. numPtr과 num1이 각각 포인터와 int로 자료형이 다르기 때문이다. 컴파일 경고가 발생하지 않도록 하려면 numPtr앞에 * 을 붙여 값을 가져오게 하여 int와 동일한 자료형으로 만들어야 한다. 값을 가져오는것은 자료형을 동일하게 한다는 것이다. 주소연산자 &도 마찬가지로 자료형을 맞춰주는 역할을 한다.
변수, 주소 연산자, 역참조 연산자, 포인터의 차이는 다음과 같다.

 

34.3 디버거에서 포인터 확인하기

디버거를 사용하면 변수의 메모리 주소, 포인터, 역참조를 쉽게 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    int *numPtr;     
    int num1 = 10;    
 
    numPtr = &num1;   
    *numPtr = 20;    
 
    printf("%d\n"*numPtr);    
    printf("%d\n", num1);      
    return 0;
}
cs

9번째 줄에 중단점을 설정하고 실행하면 다음과 같다.

위 코드에서 8번째 줄까지 실행한 상태이다. 포인터 변수 numPtr에 num1의 주소가 저장되있다. Locals 창에서 > 을 클릭하면 현재 메모리 주소에 저장된 값을 확인할 수 있다.

맥 OS에서 메모리 주소의 내용을 확인하려면 Xcode에서 디버깅 하면 된다. 

Xcode에서 9번째 줄과 10번째 줄에 중단점을 지정하고 한번 디버깅 버튼을 클릭하면 8번째 줄까지 실행되어 numPtr은 num1의 주소를 가리킨다.

이때 메모리 주소의 내용을 확인하려면 numPtr을 클릭하고 우클릭을 눌러 view memory of *numPtr을 클릭하면 된다.

num1은 int 자료형이기 때문에 0A 00 00 00 처럼 숫자 4개를 차지한다. 리틀 엔디언 방식으로 저장되기 때문에 값이 거꾸로 저장되어 원래는 00 00 00 0A 이며 0A는 10진수로 10이다. 

다음줄을 실행시키면 메모리 주소의 내용이 다음과 같이 바뀐다.

0A가 14로 바뀌었다. 16진수 14는 10진수로 20이다. 역참조후 20을 할당하여 메모리의 내용이 바뀐 것을 확인할 수 있다.

 

 

34.4 다양한 자료형의 포인터 선언하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
 
int main()
{
    long long *numPtr1;    
    float *numPtr2;        
    char *cPtr1;  
 
    long long num1 = 10;
    float num2 = 3.5f;
    char c1 = 'a';
 
    numPtr1 = &num1;    
    numPtr2 = &num2;    
    cPtr1 = &c1;       
 
    printf("%lld\n"*numPtr1);    
    printf("%f\n"*numPtr2);    
    printf("%c\n"*cPtr1);       
    return 0;
}
cs

C에서 사용할 수 있는 모든 자료형은 포인터로 만들 수 있다.

포인터에서 저장되는 메모리 주솟값은 정수형으로 동일하지만 선언하는 자료형에 따라 메모리에 접근하는 방법이 달라진다. 다음과 같이 포인터를 역참조 할 때 자료형의 크기에 맞게 값을 가져오거나 저장한다.

상수와 포인터

포인터에도 const 키워드를 붙일 수 있는데 const의 위치에 따라 특성이 달라진다.

1
2
3
4
5
const int num1 = 10;    
const int *numPtr;      
 
numPtr = &num1;
*numPtr = 20; //컴파일에러
cs

num1 이 const int 이므로 num1의 주소를 넣을 포인터도 const int로 선언해야 한다. 역참조 연산자로 값을 변경하려 해도 num1은 상수이기 때문에 컴파일 에러가 발생한다.

다음은 포인터 자체가 상수인 상황이다.

1
2
3
4
5
int num1 = 10;  
int num2 = 20;    
int * const numPtr = &num1;    
 
numPtr = &num2;    // 컴파일 에러
cs

numPtr은 포인터 자체가 상수이기 때문에 상수 포인터에는 다른 변수의 메모리 주소를 할당할 수 없다.

다음은 상수 포인터가 상수를 가리키는 상황이다.

1
2
3
4
5
6
const int num1 = 10;    
const int num2 = 20;   
const int * const numPtr = &num1;    
 
*numPtr = 30;      // 컴파일 에러
numPtr = &num2;    // 컴파일 에러
cs

포인터를 역참조한 값을 변경하려해도 해당 주소가 가리키는 것은 상수이기 때문에 컴파일 에러가 불가능하며 포인터도 상수 이므로 메모리 주소도 변경할 수 없다.

 

34.5 void포인터 선언하기

void 포인터는 자료형이 정해져있지 않은 포인터이다. 다음과 같이 사용한다.

void *포인터이름;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
 
int main()
{
    int num1 = 10;
    char c1 = 'a';
    int *numPtr1 = &num1;
    char *cPtr1 = &c1;
 
    void *ptr;        
 
    
    ptr = numPtr1;    
    ptr = cPtr1;     
 
   
    numPtr1 = ptr;   
    cPtr1 = ptr;      
 
    return 0;
}
cs

기본적으로 C는 자료형이 다른 포인터끼리 메모리 주소를 저장하면 컴파일 경고를 발생하지만 void 포인터는 어떤 자료형의 포인터든 모두 저장할 수 있다. 이런 특성 때문에 void 포인터를 범용 포인터라고 부르기도 한다.

void 포인터는 자료형이 정해지지 않았으므로 값을 가져오거나 저장할 크기도 정해지지 않았기 때문에 역참조를 하게 되면 컴파일 에러가 발생한다.

또한 void키워드로 변수를 선언할 수도 없다.

void포인터는 함수에서 다양한 자료형을 받아들일 때, 함수의 반환 포인터를 다양한 자료형으로 된 포인터를 저장할 때, 자료형을 숨기고 싶을 때 사용한다.

 

34.6 이중 포인터 사용하기

포인터의 메모리주소를 저장하는 포인터의 포인터도 선언할 수 있다. 이것을 이중 포인터 라고 하며 *을 두 번 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    int *numPtr1;    
    int **numPtr2;    
    int num1 = 10;
 
    numPtr1 = &num1;  
    numPtr2 = &numPtr1; 
 
    printf("%d\n"**numPtr2);  
    return 0;
}
cs

포인터도 변수이기 때문에 포인터를 저장하고 있는 메모리 주소도 구할 수 있다. 포인터의 메모리 주소는 일반 포인터에는 저장할 수 없고 **을 사용한 이중 포인터에 저장해야 한다.

이중포인터를 끝까지 따라가서 실제 값을 가져오려면 변수 앞에 역참조 연산자를 두 번 사용하면 된다.

포인터를 선언할 때 *의 개수에 따라서 삼중포인터, 사중포인터 등도 만들 수 있다.

 

34.7 잘못된 포인터 사용

포인터는 메모리 주소를 저장하는 용도이므로 다음과 같이 값을 직접 저장하면 안된다.

 int *numPtr = 0x100; 

메모리에서 0x100은 잘못된 주솟값이기 때문이다.

위와 같이 할당할 때 까지는 에러는 발생하지 않지만 역참조로 메모리 주소를 접근할 때 에러가 발생한다. 운영체제는 프로그램이 잘못된 메모리 주소에 접근했을 때 에러를 발생시킨다.

만약 실제 존재하는 메모리 주소라면 포인터에 직접 저장할 수 있다.


34.8 퀴즈

정답은 c이다.

정답은 d이다.

정답은 a이다.

정답은 e이다.

정답은 d이다.

 

34.9 연습문제 : 포인터와 주소 연산자 사용하기

정답은 다음과 같다.

1. numPtr = &num1;

2. numPtr = &num2;

 

34.10 심사문제 : 포인터와 주소 연산자 사용하기

정답은 다음 코드와 같다.

1
2
numPtr1 = &num1;
numPtr2 = &numPtr1;
cs

 

'Language > C, C++' 카테고리의 다른 글

[C++] Reference(참조자)  (0) 2021.08.21
[C++] 입출력  (0) 2021.08.21
[C] 문자 단위 입출력 함수  (0) 2021.01.15
[C] 스트림  (0) 2021.01.15
[C] main 함수로의 인자 전달  (0) 2021.01.09

+ Recent posts