본문 바로가기

C & C++

[C89] 포인터

목차

1. 포인터

2. 주소 연산자

3. 역참조 연산자

4. 널 포인터

5. 포인터 연산

6. 포인터와 const

7. 포인터 배열

 

포인터

포인터메모리 주소값을 저장하기 위한 특별한 변수

구분이 없다면 어떤게 값이고 주소인지 헷갈림

 

주소 연산자

주소 연산자 &

num이란 변수가 있으면 &num은 그 변수가 위치한 메모리 주소

보통 주소 16진수

주소에 저장된 자료형은?

 

int num = 10;
int* num_address = &num

 

위의 예시처럼 선언하여 num_address에 담긴 값에 가면 int형이 저장되어있다는것을 알려줌

포인터는 오른쪽에서 왼쪽으로 읽음

위의 코드를 예로 들면, num_address는 포인터이다. int로 향하는(pointer to an int)

 

역 참조 연산자 *

주소로 직접 가서 거기 저장되어 있는 값에 접근

 

int score = 100;
int* pointer = &score; /* 포인터 변수 선언 */
*pointer = 50; /* 역 참조 */

 

아래의 코드에서는 스택 메모리에 값을 복사하기 때문에 바뀌지 않는다.

 

void swap(int num1, int num2)
{
    int tmp;
    
    tmp = num1;
    num1 = num2;
    num2 = tmp;
}

int main(void)
{
    int num1 = 20;
    int num2 = 10;
    
    swap(num1, num2);
    
    return 0;
}

 

이럴 때 쓰는게 포인터

 

void swap(int* arg1, int* arg2)
{
    int tmp;
    
    tmp = *arg1;
    *arg1 = *arg2;
    *arg2 = tmp;
}

 

이렇게 하면 주소값을 함수의 매개변수로 받아서 그 주소값에 있는 원본을 바꾸므로 값이 바뀌어있다!

 

값에 의한 전달? vs 참조에 의한 전달

 

원본이 바뀌니 -> 참조에 의한 전달

메모리 주소를 복사했으니 -> 값에 의한 전달

 

의견 분분

 

중요한건 원본이 바뀐다.

 

주의 사항

포인터도 변수이므로 반환값으로 사용가능

but 지역 변수의 주소를 반환하면 위험

지역변수는 스택에 저장되고, 함수 호출이 끝나면 지역 변수도 사라지기 때문(= 잘못된 주소값)

잘못된 주소값 = 지역 변수가 사용한 '주소' 자체가 사라진 건 아님

유효하지 않은 주소를 가리키는 포인터를 댕글링 포인터(dangling pointer)라고 함

 

포인터를 반환해도 되는 경우

- 전역 변수

- 파일 속 static 전역 변수

- 함수 내 static 변수

- 힙 메모리에 생성한 데이터

널 포인터

아무것도 가리키지 않는 포인터

어떤 주소를 가리킨다는 것은 아무 주소도 가리키고 싶지 않을 때도 있다는 얘기

1) 값이 '0'인 정수 상수 표현식

2) void*로 캐스팅된 표현식

 

보통 다음의 전용 매크로를 사용

#define NULL ((void*)0)

 

포인터 변수와 NULL은 비교(==, !=) 가능

 

int* ptr;

if (ptr == NULL) {    /* 만약 ptr이 널 포인터면 */
/* 코드 생략 */
}

if (ptr != NULL) {    /* 만약 ptr이 널 포인터가 아니라면 */
/* 코드 생략 */
}

 

주의 사항

함수 매개변수로 포인터가 들어올 떄는 기본적으로 NULL이 안 들어온다고 가정

NULL이 들어올 수 있는 함수는 다음과 같이 매개변수명에서 분명히 밝힐 것

 

int get_score(const char* student_id_or_null)
{
    /* 코드 생략 */
}

 

NULL이 안 들어온다고 가정한 경우 assert()를 사용해 검증

 

#define PRICE (2)

void increase_price(int* current_price)
{
    assert(current_price != NULL);
    
    *current_price += PRICE
}

NULL을 반환할 때도 마찬가지

기본적으로 안함

반환을 해야 한다면 함수 이름에 NULL을 반환하는 것을 명시 할 것

 

const char* get_name_or_null(const int id)
{
    /* 코드 생략 */
    return NULL;
}

 

널 포인터는 언제 사용?

1. 포인터 변수를 초기화하고 싶을 때(아직 참조할 주소가 없을 때)

 

void do_something(void)
{
    int* ptr = NULL; /* 당장 사용하지 않으므로 널 포인터로 초기화 */
    
    /* 코드 생략 */
    ptr = &g_monster_count; /* 전역 변수의 주소 저장 */
    /* 코드 생략 */
}

 

2. 포인터 변수가 유효한 주소를 참조하고 있는지 확인하고 싶을 때

 

void do_something(void)
{
    int* ptr = #
    
    ptr = NULL;
    
    if(ptr != NULL){
        *ptr = 100;
    }
}

 

3. 댕글링 포인터를 막기 위해

동적 메모리 할당된 메모리를 더 이상 필요 없어서 해제했는데, 이를 여전히 가리키는 포인터가 있다면?

더 이상 사용할 수 없는 데이터니, 포인터 변수에 저장되어 있는 그 주소를 초기화 해야함

이 때 널 포인터를 이용해 리셋

 

/* 동적 메모리 할당 */
int* ptr = (int*)malloc(sizeof(int));

/* 코드 100줄 */

/* 더 이상 ptr을 사용하지 않음 */
free(ptr);
ptr = NULL;

 

포인터와 두가지 const

 

const int* p = # /* 주소에 저장된 값을 바꿀 수 없음 */

int* const p = # /* p가 가리키는 주소를 바꿀 수 없음 */

const int* const p = # /* 둘 다 바꿀 수 없음 */

헷갈릴 때는 오른쪽에서 왼쪽으로 읽기

- p는 포인터, 무엇을 가리키냐면 int const (const int)

- p는 const 포인터, 무엇을 가리키냐면 int

- p는 const 포인터, 무엇을 가리키냐면 int const

 

포인터의 용도

1. 큰 데이터를 매개변수로

- 자료가 커질수록 데이터를 복사하느라 시간을 낭비함

- 그래서 배열이 매개변수로 전달될 경우, 첫 번째 요소의 주소를 전달

2. 반환 값이 둘 이상일 때

- C에서 return문으로 불가능(하나만 가능)

- 포인터를 사용하면 함수 안에서 원본 직접 변경가능(원본값 변경하는 거는 반환이나 마찬가지)

3. 동적 메모리 할당

4. 그 외...

 

포인터 산술 연산

포인터 크기

모든 포인터는 동일한 크기를 가짐

포인터의 크기는 코드를 컴파일하는 시스템 아키텍쳐에 따라 결정

- 보통 CPU가 한 번에 처리할 수 있는 데이터의 크기(word)와 동일함

- 예: 32비트 아키텍쳐에서 포인트 크기는 4 바이트

        64비트 아키텍쳐에서 포인트 크기는 8바이트

함수의 매개변수로 전달한 배열의 크기도 4!!

배열은 연속된 메모리 -> 그걸 다 스택에 넣을 수 없음

따라서 시작 위치(메모리 주소)만 전달

 

배열 == 포인터 ?!

대입 가능

 

int* ptr1 = nums;
int* ptr2 = &nums[0]

 

nums[1] == ptr[1] == *(ptr + 1)

모두 같은 의미

- 지금 이 연속되는 메모리에서 시작에서 한 칸 건너뛰어서 두 번째를 보여줘
- 그게 어디에 있다고?

- 지금 데이터는 int, int는 4바이트

- 그러면 4바이트 한 번 건너뛰면 되지

 

C에서 주소를 얻을 수 있는 방법

- 주소 연산자(&)

- 배열의 이름

 

포인터에 정수를 더하면 주소 이동

포인터에 정수 n을 더하거나 빼면 언제나 "sizeof(자료형) x n"한 만큼 메모리 주소 이동

 

딱 한바이트만 옮기고 싶다면?

한 바이트짜리 포인터로 캐스팅 -> 주소 바뀜 X, 그 주소로 갔을 때 몇 바이트를 읽어 와서 어떤 데이터형으로 실제 값을 읽어 와야 되냐 그 의미만 바뀜

int_ptr = (char*)int_ptr + 1;

 

주의 사항

한 바이트만큼 이동 후 거기서 4바이트를 읽음

 

int int_array[] = { 27, 65 };
int* int_ptr = int_array;

int_ptr = (char*)int_ptr + 1;

 

두 주소 간의 사칙연산

- 뺄셈만 지원

- 뺄셈의 경우 두 주소 사이에 들어갈 수 있는 데이터 수 반환 -> 따라서 포인터가 아니라 정수를 반환

 

int* ptr = &nums[5] - &nums[1]; /* 컴파일 오류 */
int sub = &nums[5] - &nums[1]; /* 컴파일 됨 */

 

C#이나 Java에서는 모든게 포인터

그런데 왜 *가없냐? 안전하지 않아서 주소 이동을 허용하지 않기 때문

 

포인터와 배열

대부분 경우 동일

 

포인터와 배열의 차이

 

1. sizeof 연산자

배열을 함수의 매개변수로 받은 경우가 아니라면

sizeof(배열): 배열의 총크기를 반환

sizeof(포인터): 포인터의 크기를 반환

 

int nums[3] = { 34, 135, 49 };

int* ptr = nums;

size_t size1 = sizeof(nums); /* 12 = 3 X 4 */
size_t size2 = sizeof(ptr); /* 4 */

 

2. 문자열 초기화

C는 문자열(string) 자료형이 없음 -> char 배열 

- 문자열이 끝나는 지점을 알려주기 위해 마지막값은 '\0'

 

char day1[] = "Monday";
char* day2 = "Monday";

day1[0] = 'p'; /* OK */
day2[0] = 'p'; /* 결과가 정의되지 않음 */

 

day1

배열에 차례대로 들어간 후 마지막에 '\0'가 들어감

함수 안에서 사용하면 스택 메모리에 저장됨

 

day2

포인터 변수는 스택에 저장

실제 문자열은 데이터 섹션에 저장

 

주의 사항

스택에 저장된 문자열은 수정해도 괜찮지만 데이터 섹션에 저장된 문자열은 수정할 경우 '결과가 정의되지 않음'

후자의 경우 문자열이 읽기 전용

 

3. 대입

포인터 변수에 값을 대입할 수 있으나 배열 변수에는 할 수 없음

 

4. 포인터 산술 연산

포인터는 산술 연산 가능, 배열은 불가능

배열의 주소를 증감하고 싶으면 포인터에 배열의 주소를 대입 후 그 포인터 변수를 증감

 

 

연산자 우선순위 및 결합 법칙

 

 

 

int nums[] = { 134, 68, 47965 };
int* p = nums; /* 변수 nums의 주소가 0x104라 가정 */

int num = *p++;
int num = *++p;
int num = ++*p;
int num = (*p)++;

첫번째 경우

int num = *(p++);

(p++) -> p로 평가

아직 p = p + 1 실행 안함

num = 134,

p = p + 1

p = 108

 

*p++ 이렇게 접근하는 게 배열보다 쬐금 빠름

- 배열은 언제나 첫 주소 + 요소의 위치까지의 오프셋

- 포인터는 이미 다음 주소에 가 있기에 그대로 참조

 

포인터 배열

 

int* num_pointers[3];

 

'C & C++' 카테고리의 다른 글

[C++] 재귀함수를 이용한 조합  (0) 2024.07.17
[C++] 효율적인 문자열 분할(split) 함수 구현  (0) 2024.07.09