본문 바로가기

Programming/C/C++

C/Pointer/C포인터의 이해와 활용 - 3

반응형

※ 본 자료는 위 책을 읽고 개인적으로 정리한 포스트임을 알려드립니다.


# 포인터와 함수
- 포인터는 함수로 데이터를 전달하거나 함수에 의해 데이터를 수정할 수 있게함.
  • 복잡한 데이터 역시 구조체의 포인터 형태로 함수에 전달되거나 반환될 수 있음.
  • 포인터가 데이터 타입이 아닌 함수의 주소를 가리키는 경우
  • 프로그램의 실행 흐름을 동적으로 제어하는 데 사용될 수 있음.

- 프로그램 스택 이해
  • 현대의 블록 구조 언어에서 함수의 실행을 지원하기 위해 사용
  • 함수가 호출되면 함수의 스택 프레임이 생성되고 프로그램 스택에 추가(push)

- 함수가 반환될 때
  • 프로그램 스택은 C언어와 같은 현대의 블록 구조 언어에서 함수의 실행을 지원하기 위해 사용
  • 함수는 함수에 의해 참조되는 데이터를 수정할 수 있음.
  • 덩치가 큰 데이터를 효과적으로 전달

# 프로그램 스택과 힙
- 프로그램 스택과 힙은 런타임을 구성하는 중요한 요소들

- 프로그램 스택
  • 프로그램 스택은 함수의 실행을 지원하기 위한 메모리 영역

- 공유된 메모리 영역에서 프로그램 스택은 메모리의 낮은 부분을 사용하는 경향
- 힙은 메모리의 높은 부분을 사용
- 프로그램 스택은 스택 프레임을 포함
  • 활성화 레코드(Activation Records) or 활성화 프레임(Activation Frames)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void function2() {
    Object *var1 = ...;
    int var2;
`
 
void function1(){
    Object *var3 = ...;
    function2();
}
 
int main(){
    int var4;
    function1();
}
  • 입력된 스택은 위쪽으로 자람
  • 함수의 스택 프레임이 프로그램 스택에서 제거(pop)
  • 스택 프레임에 의해 사용된 메모리는 초기화되지 않음
- 힙은 할당과 해제의 반복으로 단편화 이슈 발생

# 스택 프레임의 구성
- 구성 단계
  • 반환 주소 : 함수 종료 후 돌아가야 할 프로그램 내의 주소
  • 로컬 변수 저장소 : 로컬 변수를 위해 할당된 메모리
  • 매개변수 저장소 : 함수의 매개변수를 위해 할당된 메모리
  • 스택 포인터와 기반 포인터 (base pointer) : 스택 관리 목적으로 런타임 시스템에 의해 사용되는 포인터
- 발생 문제
  • 스택 프레임이 프로그램 스택에 입력될 때, 메모리 부족 상태가 발생할 수 있다.
  • 이러한 상태를 스택 오버플로(stack overflow)라고 하며 일반적으로 프로그램의 비정상 종료를 초래
  • 메모리의 같은 개체에 접근하는 경우 잠재적인 충돌로 이어질 수 있음.

# 포인터에 의한 전달과 반환
- 포인터를 함수로 전달하면 해당 개체를 전역으로 만들지 않고도 다양한 함수에서 참조가 가능해짐.
- 단지 해당 메모리 개체에 접근이 필요한 함수들만 접근이 가능해지며 해당 개체를 복사하지 않아도됨
  • 데이터를 포인터로 전달하면서 상수 포인터를 전달하면 함수내에서 데이터가 수정되는 것을 방지할 수 있음.
  • "상수 포인터 전달하기"절
  • 함수 내에서 수정해야 할 데이터가 포인터인 경우, 인자로 포인터의 포인터를 전달
- by value 전달 문제
  • 아주 큰 구조체를 통째로 함수로 전달하면 해당 구조체의 모든 바이트를 복사해야 하며, 이 때문에 프로그램은 느려지고 더 많은 스택 프레임 메모리를 사용

- 포인터로 전달
  • 데이터를 포인터로 전달하는 이유는 함수 내에서 전달된 데이터를 수정하기 위해서
  • 매개변수에 의해 참조되는 값을 상호 교환하는 스왑(swap) 기능 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
void swapWithPointers(int* pnum1, int* pnum2){
    int tmp;
    tmp = *pnum1;
    *pnum1 = *pnum2;
    *pnum2 = tmp;
}
 
int main(){
    int n1 = 5;
    int n2 = 10;
    swapWithPointers(&n1, &n2);
    return 0;
}
  • pnum1과 pnum2이 스왑 동작이 처리되는 동안 역참조됨
  • 결과
    • n1과 n2값이 수정이됨

# 값에 의한 전달
- 복사된 값이 num1과 num2에 저장될 뿐이며 num1을 수정해도 인자 n1은 변경되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
void swap(int num1, int num2){
    int tmp;
    tmp = num1;
    num1 = num2;
    num2 = tmp;
}
 
int main(){
    int n1 = 5;
    int n2 = 10;
    swap(n1, n2);
    return 0;
}

# 상수 포인터 전달하기
- 구조체와 같은 큰데이터를 함수로 전달할 때 많은 메모리의 복사를 피하고 데이터의 주소만 전달하는 효과적인 방법
  • 단순히 포인터만 전달하면 전달된 데이터가 수정될 수 있으므로, 데이터의 수정이 불가하게 상수 포인터로 전달 한다.
1
2
3
4
5
6
7
8
9
void passingAddressOfConstants(const int* num1, int* num2){
    *num2 = *num1;
}
 
int main(){
    const int limit = 100;
    int result = 5;
    passingAddressOfConstants(&limit, &result);
}

# 포인터 반환하기
- 적절한 데이터 타입의 포인터를 반환하도록 선언
  • 함수로부터 메모리 개체를 반환할 필요가 있는 경우
    • 함수 내에서 malloc으로 메모리를 할당한 후 함수 종료 시 반환, 이 함수를 호출한 호출자(caller)는 반환된 메모리를 해제할 책임 있다.
    • 수정할 메모리 개체를 함수의 인자로 전달, 이 방법은 메모리 개체의 할당과 해제에 대해서 호출에게 책임이 있다.

# malloc 함수를 사용하여 반환될 메모리를 할당
- 그리고 메모리를 반환
1
2
3
4
5
6
7
8
9
10
11
12
int* allocateArray(int sizeint value){
    int* arr = (int*)malloc(size * sizeof(int));
    for(int i=0; i<size; i++){
        arr[i] = value;
    }
    return arr;
}
 
int* vector = allocateArray(545);
for(int i=0; i<5; i++){
    printf("%d\n"vector[i]);
}


- 포인터를 반환 받는 코드
1
2
3
4
int* vector = allocateArray(545);
for(int i=0; i<5; i++){
    printf("%d\n"vector[i]);
}
  • 초기화되지 않은 포인터의 반환
  • 잘못된 주소를 가리키는 포인터의 반환
  • 로컬 변수를 가리키는 포인터의 반환
  • 반환된 포인터의 메모리 해제 실패

# 로컬 데이터 포인터
- 프로그램 스택의 동작 방식을 이해하지 못할 경우
  • 로컬 데이터에 대한 포인터를 반환하는 실수를 하기 쉽다.
  • 로컬 데이터의 함수는 종료 즉시 스택 프레임이 스택에서 제거됨.
- arr을 static으로 선언
  • 변수의 범위 함수로 제한

# Null 포인터 전달
1
2
3
4
5
6
7
8
9
10
11
int* allocateArray(int *arr, int sizeint value){
    if(arr != NUULL){
        for(int i=0; i<size; i++){
        arr[i] = value;
        }
    }
    return arr;
}
 
int *vector = (int*)malloc(5 * sizeof(int));
allocateArray(vector545);

#포인터의 포인터 전달
- 포인터 자체의 수정을 원하면 포인터의 포인터를 전달해야 한다.
  • 해당 포인터는 함수 내에서 메모리가 할당되고 초기화함

# 사용자 정의 free 함수 작성
- free함수는 전달된 포인터가 NULL인지 검사하지 않으며, 할당 해제 후 반환 시에 포인터를 NULL로 설정하지도 않는다.
- 메모리 해제 후 포인터를 NULL로 설정하는 것은 매우 좋은 습관
- 예시
1
2
3
4
5
6
void safeFree(void **pp){
    if (pp != NULL && *pp != NULL) {
        free(*pp);
        *pp = NULL;
    }
}

# 함수 포인터
- 함수 포인터는 함수의 주소를 가리키는 포인터
  • 함수 포인터는 어떠한 조건 문장도 사용하지 않고서 컴파일 시간에 미리 결정된 순서가 아닌 함수의 실행을 제어하는 방법
- 파이프라이닝(Pipelining) : 프로세서의 성능을 향상하기 위해 일반적으로 사용되는 하드웨어 기술이며 명령을 중첩 실행
- 분기 예측(prediction) : 프로세서가 다음에 실행될 것으로 판단되는 분기 처리를 시작하게 되며, 프로세서가 성공적으로 정확한 분기를 예측한다면 현재 파이프라인에 있는 명령을 폐기할 필요가 없어 성능이 향상됨

- 선언
  • void (*foo)(); 
  • int (*f1)(double);
  • void (*f2)(char*);
  • double* (*f3)(int, int);
- 사용
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int (*fptr1)(int);
     
    int square(int num) {
        return num*num;
    }
     
    int n = 5;
    fptr1 = square;
    printf("%d squared is %d\n", n, fptr1(n));
    fptr1 = &square; // 주소 연산자의 사용은 중복된 연산으로 사용할 필요가 없다.
     
     

- 타입 정의 사용
1
2
3
4
5
6
typedef int (*funcptr)(int);
 
...
funcptr fptr2;
fptr2 = square;
printf("%d squared is %d\n", n, fptr(n));

- 함수 포인터 전달
  • 함수 포인터의 선언 > 함수의 매개변수로 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int add(int num1, int num2){
    return num1 + num2;
 
}
 
int sub(int num1, int num2){
    return num1 - num2;
}
 
typedef int (*fptrOperation)(intint);
 
int compute(fptrOperation operation, int num1, int num2){
    return operation(num1, num2);
 
}
 
printf("%d\n", compute(add, 56));
printf("%d\n", compute(sub, 56));
 

- 함수 포인터 반환
  • 함수 선언 시 함수의 포인터를 반환하도록 선언
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int add(int num1, int num2){
    return num1 + num2;
 
}
 
int sub(int num1, int num2){
    return num1 - num2;
}
 
typedef int (*fptrOperation)(intint);
 
fptrOperation select(char opcode) {
    switch(opcode){
        case '+' : return add;
        case '-' : return subtract;
    }
}
 
int evaluate(char opcode, int num1, int num2){
    fptrOperation operation = select(opcode);
    return operation(num1, num2);
}
 
printf("%d\n", evaluate('+'56));
printf("%d\n", evaluate('-'56));
 

- 함수 포인터의 배열 이용
  • 어떤 조건에 근거하여 실행할 함수를 선택하는 기능을 구현하는데 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef int (*operation)(intint);
operation operations[128= {NULL};
 
// 또는
int (*operations[128])(intint= {NULL};
 
void initializeOperationArray() {
    operation['+'= add;
    operation['-'= sub;
}
 
int evaluateArray(char opcode, int num1, int num2){
    fptrOperation operation;
    operation = operations[opcode];
    return operation(num1, num2);
}
 
 
initializeOperationsArray();
printf("%d\n", evaluatedArray('+'56);
printf("%d\n", evaluatedArray('-'56);
 

- 함수 포인터 캐스팅
  • 하나의 함수를 가리키는 포인터는 다른 타입으로 캐스팅이 가능
  • 런타임 시스템은 함수 포인터의 매개변수가 올바른지 확인하지 않기 때문에 함수 포인터의 캐스팅은 신중해야함
  • 함수 포인터를 다른 함수 포인터로 캐스팅한 후 다시 원래의 함수 포인터로 되돌리는 것이 가능
  • 캐스팅 후에도 포인터는 원래의 포인터와 완전히 같음.
  • 사용 코드
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      typedef int (*fptrToSingleInt)(int);
      typedef int (*fptrToTwoInts)(intint);
      int add(intint);
       
      fptrTwoInts fptrFirst = add;
      fptrToSingleInt fptrSecond = (fptrToSingleInt)fptrFirst;
      fptrFirst = (fptrToTwoInts)fptrSecond;
      pritnf("%d\n", fptrFirst(5,6));
       
      • 반환 타입이 다른 함수 포인터로 캐스팅할 수 없다.


반응형