C 언어 포인터 심층 분석

포인터 기초

메모리 이해

메모리는 데이터를 저장하는 장치입니다. 컴퓨터 시스템에는 두 가지 주요 유형의 메모리가 있습니다:

외부 저장 장치

외부 저장 장치는 장기간 데이터를 보관하며, 전원이 꺼져도 데이터가 유지됩니다(비휘발성).

일반적인 외부 저장 장치: 하드 디스크, USB 플래시 드라이브, 광 디스크 등

내부 메모리

내부 메모리(램)는 전원이 공급되는 동안만 데이터를 유지합니다(휘발성).

일반적인 장치: RAM(랜덤 액세스 메모리), DDR3, DDR4 등

물리 메모리: 실제로 존재하는 저장 장치

가상 메모리: 운영체제가 가상으로 만들어낸 메모리 공간으로, 코드에서 보이는 주소는 모두 가상 주소입니다.

32비트 운영체제에서:

  • 32비트 → 32개의 주선(하나의 주소에 한 바이트 저장) → 2^32개의 주소 → 2^32바이트의 메모리

포인터의 개념

시스템은 가상 메모리의 각 메모리 셀에 고유한 번호를 할당하며, 이 번호가 바로 포인터(주소)입니다.

포인터 변수: 주소 번호를 저장하는 변수

32비트 플랫폼에서 주선은 32비트이므로 주소 번호도 32비트이며, 포인터 변수는 32비트(4바이트)입니다.

주의사항

  1. 어떤 유형의 주소든 메모리 셀의 번호이며, 32비트 시스템에서는 모두 4바이트입니다.
  2. 어떤 유형의 포인터든 해당 유형의 변수만 저장할 수 있습니다.

끝단 바이트 순서(Endianness)

끝단 바이트 순서는 메모리에 데이터가 저장되는 방식을 결정합니다:

  • 끝단 순서(Big-endian): 상위 주소가 메모리의 낮은 주소에 저장됩니다.
  • 작은 끝단 순서(Little-endian): 상위 주소가 메모리의 높은 주소에 저장됩니다.

컴퓨터가 끝단 순서인지 작은 끝단 순서인지는 컴퓨터 아키텍처에 따라 다릅니다.

#include 
int main() {
    int num = 0x12345678;
    char *ptr = (char *)#
    if (*ptr == 0x78) {
        printf("작은 끝단 순서\n");
    } else {
        printf("끝단 순서\n");
    }
    return 0;
}

포인터 정의 방법

1. 기본 포인터 변수

데이터형 *포인터변수

int *ptr; // 정수형 포인터 변수 ptr 정의

포인터 변수를 정의할 때 *는 수식자로, 포인터 변수를 수식합니다. 변수명은 ptr입니다.

2. 포인터 연산자

  • &: 주소 연산자 (변수의 메모리 주소를 얻음)
  • *: 역참조 연산자 (포인터가 가리키는 값에 접근)
int value = 0x12345678;
int result;
int *ptr;
ptr = &value; // value의 주소를 ptr에 할당, &는 주소 연산자
result = *ptr; // ptr이 가리키는 value의 값을 result에 할당. *는 역참조

호출 시 *는 값을 가져옵니다.

한 줄에 여러 포인터 변수를 정의할 때는 각 포인터에 수식자를 추가해야 합니다.

int *ptr1, *ptr2; // 두 개의 정수형 포인터 변수 ptr1과 ptr2 정의
int *ptr1, var; // 정수형 포인터 변수 ptr1과 정수형 변수 var 정의

일반적으로 포인터 변수를 처음 정의할 때 값을 할당하지 않으면 NULL을 가리키도록 설정해야 합니다. 그렇지 않으면 야 포인터(wild pointer)가 발생할 수 있습니다.

3. 포인터의 크기

  • 32비트 시스템에서 어떤 유형의 포인터든 4바이트입니다.
  • 64비트 시스템에서 어떤 유형의 포인터든 8바이트입니다.

4. 포인터의 유형

가리키는 데이터 유형에 따라 분류됩니다:

문자 포인터

문자형 데이터의 주소를 저장합니다.

char *ptr; // 문자형 데이터만 저장할 수 있음
char ch;

함수 포인터

int (*func)(int); // func는 int형을 반환하고 int형 인수를 받는 함수를 가리킵니다
int calculate(int a) {
    return 0;
}
func = calculate;

구조체 포인터

struct student *stuPtr;
struct student student1;
stuPtr = &student1;

배열 포인터

int (*arrPtr)[2][2]; // arrPtr은 2x2 정수형 배열을 가리킵니다
int matrix[2][2];
arrPtr = matrix;

포인터의 포인터(이중 포인터)

int **doublePtr;
int *singlePtr;
doublePtr = &singlePtr;

void 포인터(범용 포인터)

#include 
void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
// 함수 반환값: 매핑된 주소 → 반환값이 void* 형식인 이유
// 주소 → int *ptr = mmap(); *ptr = 0x00ff0000;
//      char *ptr = mmap(); *ptr = 0x00;

요약: 다바이트 변수는 여러 메모리 셀을 차지하며, 각 셀에는 주소 번호가 있습니다. C 언어에서는 가장 작은 주소 번호가 해당 다바이트 변수의 주소로 규정됩니다.

5. 포인터와 변수의 관계
6. 포인터와 배열 요소의 관계
  • 변수는 메모리에 저장되며 주소 번호가 있습니다. 정의된 배열은 동일한 유형의 변수들의 집합이며, 각 변수는 메모리 공간을 차지하고 주소 번호를 가집니다. 포인터 변수는 배열 요소의 주소를 저장할 수 있습니다.
int arr[5];
int *ptr = NULL;
ptr = &arr[0]; // 또는 ptr = arr;
  • 배열 요소를 참조하는 방법:
  1. 배열에 인덱스 사용: arr[2]
  2. 포인터에 인덱스 사용: *(ptr+2)
int arr[5];
arr[2] = 100;
int *ptr = NULL;
ptr = arr;
*(ptr+2) = 100; // ptr은 arr[0]의 주소이므로 ptr+2는 arr[2]의 주소입니다. arr[2]는 *(ptr+2)와 동일합니다.
ptr와 arr는 다릅니다. ptr은 포인터 변수이고 arr은 상수입니다. ptr에 값을 할당할 수 있지만 arr에는 할당할 수 없습니다.
ptr = &arr[3]; // 올바름
arr = &arr[3]; // 오류

포인터의 연산

  • 포인터에 정수를 더하면 결과는 여전히 주소입니다.
  • 동일한 배열을 가리키는 포인터끼리 비교할 때만 의미가 있으며, 앞에 있는 포인터가 뒤에 있는 포인터보다 작습니다.
  • 동일한 배열을 가리키는 포인터의 뺄셈은 두 포인터 사이의 요소 개수입니다.
  • 동일한 유형의 포인터는 서로 할당할 수 있습니다(void* 유형 제외).
  • 다른 유형의 포인터를 서로 할당하려면 형 변환이 필요합니다.

포인터 배열

개념: 포인터 배열은 배열 자체이며, 동일한 유형의 포인터 변수들의 집합입니다.

포인터와 배열의 관계

  1. 포인터는 배열 요소의 주소를 저장할 수 있습니다.
  2. 배열은 포인터 유형의 변수를 저장할 수 있습니다.
int *ptrArr[5]; // 포인터 배열
int (*arrPtr)[5]; // 배열 포인터
#include 
int main() {
    int data[] = {10, 20, 30, 40, 50};
    int *ptr = &data[1];
    printf("ptr의 주소: %p\n", ptr);
    int value = *ptr++;
    printf("%d\n", value);
    printf("*ptr++ 후 ptr이 가리키는 값: %d\n", *ptr);
    printf("*ptr++ 후 ptr의 주소: %p\n", ptr);
    return 0;
}

포인터의 포인터(이중 포인터)

(특별히 언급하지 않은 경우 32비트 시스템을 기준으로 합니다)

포인터의 포인터는 포인터 변수의 주소입니다.

int value = 0x12345678;  // value의 주소를 0x00002000이라 가정
int *ptr = NULL; // 포인터 변수 ptr의 주소를 0x00003000이라 가정
ptr = &value;   // 포인터 변수 ptr에는 value의 주소 0x00002000이 저장됩니다
int **doublePtr = NULL;
doublePtr = &ptr;  // 포인터 변수 doublePtr에는 포인터 변수 ptr의 주소 0x00003000이 저장됩니다. 즉, doublePtr는 ptr을 가리킵니다

ptr과 doublePtr는 모두 포인터 변수로 4바이트를 차지하며 주소 번호를 저장합니다.

문자열과 포인터

문자열의 개념

문자열은 '\0'로 끝나는 여러 문자의 조합입니다. 예: "hello"

문자열의 주소: 첫 번째 문자의 주소입니다. "hello" 문자열에서 'h'의 주소입니다.

char *str = "hello";

문자열의 저장 위치

  1. 배열에 저장된 문자열: 메모리(스택, 정적 전역 영역)에 공간이 할당됩니다.
  2. 문자열 상수 영역에 저장된 문자열: 문자열 상수 영역에 공간이 할당되며 문자열의 첫 주소가 포인터 변수에 할당됩니다.
  3. 힙 영역에 저장된 문자열: malloc 등의 함수를 사용하여 힙 영역에 공간을 수동으로 할당하고 문자열을 복사합니다.

배열에 저장된 문자열

char str[100] = "hello";

문자열 상수 영역에 저장된 문자열

char *str = "hello";
// 문자 포인터 변수 str을 정의하고, 문자열 상수 영역의 "hello"의 첫 주소를 str에 할당

힙 영역에 저장된 문자열

헤더 파일: #include

함수 원형: void *malloc(size_t size)

함수의 역할: 메모리에 연속된 공간을 요청하고 메모리의 첫 주소를 반환합니다.

char *buffer = (char *)malloc(100);
// 동적으로 100*1바이트의 공간을 할당
strcpy(buffer, "hello");

문자열의 수정 가능성

문자열 내용이 수정 가능한지는 문자열이 어디에 저장되었는지에 따라 다릅니다.

  1. 배열에 저장된 문자열은 수정할 수 있습니다.
  2. char str[100] = "hello";
    str[0] = 'y'; // 올름, 수정 가능
  3. 문자열 상수 영역의 내용은 수정할 수 없습니다.
  4. char *str = "hello";
    str[0] = 'y'; // 오류
    // str은 포인터 변수로 문자열 상수 영역을 가리키며, 가리키는 내용은 수정할 수 없지만 다른 곳을 가리키도록 수정할 수 있습니다
    str = "yyyy";
  5. 힙 영역의 내용은 수정할 수 있습니다.
  6. char *str = (char *)malloc(100);
    strcpy(str, "hello");
    *str = 'y'; // 올름, 수정 가능
    // str은 포인터 변수로 힙 영역을 가리키며, 가리키는 내용은 수정할 수 있습니다

요약: 문자열 상수 영역에 저장된 문자열은 수정할 수 없으며, 배열이나 힙 영역에 저장된 문자열은 수정할 수 있습니다.

문자 배열의 초기화

  1. 문자 배열의 초기화
  2. char buffer[10] = "hello";
  3. 문자열 상수 영역을 가리키는 포인터 초기화
  4. char *buffer = "hello";
  5. 힙 영역에 문자열 저장
  6. 초기화로 바로 할당할 수 없습니다. 먼저 포인터에 값을 할당하여 포인터가 힙 영역을 가리키도록 하고, strcpy, scanf 등을 사용하여 문자열을 힙 영역에 입력합니다.

    char *buffer = NULL;
    buffer = (char *)malloc(100);
    strcpy(buffer, "hello"); // 또는 scanf("%s", buffer);

할당

  1. 문자 배열: scanf 또는 strcpy 사용
  2. char buffer[20] = "hello";
    buffer = "kitty";  // 오류, 배열 이름은 상수이므로 상수에 값을 할당할 수 없음
    strcpy(buffer, "kitty"); // 올름
    scanf("%s", buffer); // 올름
  3. 문자열 상수 영역을 가리키는 포인터
  4. char *buffer = "hello";
    buffer = "kitty"; // 올름, 포인터 변수 buffer의 가리키는 대상을 수정
  5. 힙 영역을 가리키는 포인터
  6. char *buffer = (char *)malloc(100);
    strcpy(buffer, "kitty"); // 올름
    scanf("%s", buffer); // 오류

배열 포인터

1. 2차원 배열

2차원 배열은 행과 열을 가집니다. 2차원 배열은 여러 1차원 배열로 구성된 것으로 볼 수 있으며, 2차원 배열의 각 요소는 1차원 배열입니다.

int matrix[3][5];

고려사항: 배열 이름은 첫 번째 요소의 주소이며 상수입니다. 배열 이름에 1을 더하면 다음 요소를 가리킵니다.

2차원 배열 matrix에서 matrix+1은 다음 요소, 즉 다음 1차원 배열(다음 행)을 가리킵니다.

2. 배열 포인터의 개념

배열 포인터 자체는 포인터이며 배열을 가리키며, 1을 더하면 다음 배열(다음 행)으로 이동합니다.

3. 배열 포인터 정의 방법

배열을 가리키는 유형 (*포인터변자)[요소개수]

int (*matrixPtr)[5]; // 5개의 요소를 가진 배열을 가리키는 배열 포인터 변수 matrixPtr 정의
matrixPtr+1은 5개 요소의 배열 하나를 건너뛰며, matrixPtr과 matrixPtr+1은 20바이트 차이
#include 
int main() {
    int matrix[3][5];
    int (*ptr)[5];
    printf("matrix=%p\n", matrix); // 0번 행의 첫 주소
    printf("matrix+1=%p\n", matrix+1); // 1번 행의 첫 주소, matrix과 matrix+1은 20바이트 차이
    ptr = matrix;
    printf("&matrix=%p\n", &matrix); // 전체 배열 matrix의 첫 주소
    printf("&matrix+1=%p\n", &matrix+1); // 전체 배열을 건너뛴 주소, &matrix과 &matrix+1은 60바이트 차이
    printf("&matrix[0]=%p\n", &matrix[0]); // 0번 행의 첫 주소
    printf("&matrix[0]+1=%p\n", &matrix[0]+1); // 0번 행의 첫 주소, &matrix[0]과 &matrix[0]+1은 20바이트 차이
    printf("&matrix[0][0]=%p\n", &matrix[0][0]); // 0번 행 0번 열의 주소
    printf("&matrix[0][0]+1=%p\n", &matrix[0][0]+1); // 0번 행 0번 열의 다음 주소, &matrix[0][0]과 &matrix[0][0]+1은 4바이트 차이
    printf("ptr=%p\n", ptr); // 0번 행의 첫 주소
    printf("ptr+1=%p\n", ptr+1); // 1번 행의 첫 주소, ptr과 ptr+1은 20바이트 차이
    
    ptr[0][0] = 100; // matrix[0][0] = 100과 동일
    return 0;
}

4. 다양한 배열 포인터 정의

  1. 1차원 배열 포인터, 1을 더하면 다음 1차원 배열을 가리킵니다.
  2. int (*ptr)[5];
    // 5개 int 요소의 행을 가진 2차원 배열과 함께 사용
    int matrix[3][5];
    int data[4][5];
    // ...
    ptr = matrix;
    ptr = data;
  3. 2차원 배열 포인터, 1을 더하면 다음 2차원 배열을 가리킵니다.
  4. int (*ptr)[4][5];
    // 4행 5열 int 요소의 3차원 배열과 함께 사용
    int matrix[3][4][5];
    int data[4][4][5];
    // ...
    ptr = matrix;
    ptr = data;

5> 혼동하기 쉬운 개념

  1. 포인터 배열: 배열이며 동일한 유형의 포인터들의 집합입니다.
  2. int *ptrArr[10]; // 배열 ptrArr에는 10개의 int* 유형 포인터 변수 ptrArr[0]~ptrArr[9]가 있습니다
  3. 배열 포인터: 배열을 가리키는 포인터이며, 1을 더하면 다음 배열로 이동합니다.
  4. int (*ptr)[10]; // ptr은 포인터이며 배열 포인터입니다. ptr+1은 다음 배열을 가리키며, 여기서는 10개의 int 변수를 건너뜁니다
  5. 포인터의 포인터: 이중 포인터
  6. int **ptr;
    int *singlePtr;
    ptr = &singlePtr;
  7. 배열 이름에 주소 연산자를 사용하면 배열 포인터가 됩니다.
  8. int arr[10];
    // arr+1은 한 int 요소를 건너뛰며, 즉 arr[1] 주소입니다
    // arr과 arr+1은 요소 하나, 즉 4바이트 차이
    // arr은 int* 유형입니다
    // &arr은 1차원 배열 포인터, int(*ptr)[10]가 됩니다
    // (&arr)+1과 &arr은 배열 하나, 즉 10개 요소, 40바이트 차이
  9. &는 업그레이드 역할을 하고 *는 다운그레이드 역할을 하며, &*는 서로 상쇄됩니다.
  10. int matrix[4][5];
    matrix의 데이터 유형 → int (*ptr)[5];
    &matrix의 데이터 유형 → int (*ptr)[4][5];
    
    *(&matrix[0]) <=> matrix[0] <=> &matrix[0][0]

6. 배열 이름과 포인터 변수의 차이점

int arr[10];
int *ptr = arr;

공통점

  1. arr과 ptr은 모두 arr[0]의 주소를 가리킵니다.
  2. 배열 요소를 참조할 때는 동일합니다.
#include 
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    printf("%d\n", arr[3]);
    printf("%d\n", ptr[3]);
    printf("%d\n", *arr+3);
    printf("%d\n", *ptr+3);
    printf("%d\n", *(arr+3));
    printf("%d\n", *(ptr+3));
    return 0;
}
// 결과는 모두 arr[3]인 4를 가져옵니다.

차이점

  1. arr은 상수이고 ptr은 변수입니다. ptr에 값을 할당할 수 있지만 arr에는 할당할 수 없습니다.
  2. arr에 주소 연산자를 사용하는 것과 ptr에 주소 연산자를 사용하는 것은 다릅니다.

포인터를 함수 매개변수로 사용

1단계 포인터를 함수 매개변수로 사용

#include 
void swap(int *c, int *d) {
    int *temp;
    temp = c;
    c = d;
    d = temp;
}

int main() {
    int a = 3, b = 4;
    int *x = &a, *y = &b;
    printf("swap 전 x, y 주소: %p %p\n", x, y);
    printf("swap 전 a, b 값: %d %d\n", a, b);
    swap(x, y);
    printf("swap 후 x, y 주소: %p %p\n", x, y);
    printf("swap 후 x, y 값: %d %d\n", *x, *y);
    return 0;
}

해석:

swap 함수 분석:

  • swap 함수는 두 포인터 c와 d를 받습니다.
  • 함수 내에서 새 포인터 temp를 생성합니다.
  • 먼저 c의 값을 temp에 할당한 다음 d의 값을 c에 할당하고 마지막으로 temp의 값을 d에 할당합니다.
  • 문제는 swap 함수 내에서 c와 d의 값만 교환한 것으로, main 함수에서 가리키는 주소에는 영향을 주지 않는다는 것입니다.

main 함수 분석:

  • main 함수에서 두 정수 a와 b를 정의하고 포인터 x와 y를 각각 가리키게 합니다.
  • swap(x, y)를 호출할 때 실제로는 x와 y의 값(즉 a와 b의 주소)이 swap 함수의 형식 매개변수 c와 d에 복사됩니다.
  • swap 함수 내의 교환 작업은 형식 매개변수 c와 d에만 영향을 미치며, 이들은 x와 y의 복사본입니다. 따라서 x와 y의 값(저장된 주소)과 가리키는 a와 b의 값은 변경되지 않습니다.

2단계 포인터를 함수 매개변수로 사용

#include 
void swap(int **c, int **d) {
    int *temp;
    temp = *c;
    *c = *d;
    *d = temp;
}

int main() {
    int a = 3, b = 4;
    int *x = &a, *y = &b;
    printf("swap 전 x, y 주소: %p %p\n", x, y);
    printf("swap 전 a, b 값: %d %d\n", a, b);
    swap(&x, &y);
    printf("swap 후 x, y 주소: %p %p\n", x, y);
    printf("swap 후 x, y 값: %d %d\n", *x, *y);
    return 0;
}

해석:

swap 함수 설명:

  • swap 함수는 두 이중 포인터 int **c와 int **d를 받습니다.
  • 함수 내에서 먼저 일단 포인터 temp를 선언합니다.
  • temp = *c;: *c(즉 x의 값, 즉 &a)를 temp에 저장합니다.
  • *c = *d;: *d(즉 y의 값, 즉 &b)를 *c에 저장합니다. 이로 인해 x의 값이 &a에서 &b로 변경됩니다.
  • *d = temp;: 이전에 temp에 저장된 값(즉 &a)을 *d에 저장합니다. 이로 인해 y의 값이 &b에서 &a로 변경됩니다.

main 함수 설명:

  • 먼저 두 정수 a와 b를 정의하고 각각 3과 4로 초기화합니다.
  • 두 포인터 x와 y를 정의하며 각각 a와 b를 가리킵니다. 즉, x에는 a의 주소가 저장되고 y에는 b의 주소가 저장됩니다.
  • swap(&x, &y);를 호출하기 전에 x와 y의 주소와 가리키는 값(즉 a와 b의 값)을 출력합니다.
  • swap(&x, &y);를 호출할 때 x와 y의 주소를 swap 함수에 매개변수로 전달합니다.
  • swap 함수에서 x와 y에 저장된 주소를 교환하므로 x는 y의 원래 주소를 가리키고 y는 x의 원래 주소를 가리킵니다.
  • swap 함수 호출 후 x와 y의 주소와 가리키는 값을 출력합니다. x와 y에 저장된 주소가 교환되었으므로 x는 이제 b를 가리키고 y는 a를 가리킵니다.

배열 포인터를 함수 매개변수로 사용

배열 포인터를 함수 매개변수로 사용하는 목적

  1. 다차원 배열 처리: 다차원 배열을 처리할 때 배열 포인터를 사용하면 전체 배열이 아닌 배열의 일부를 쉽게 전달할 수 있어 코드의 유연성과 유지보수성이 향상됩니다.
  2. 성능 향상: 포인터를 통해 배열을 전달하면 배열 복사를 피할 수 있어 성능이 향상되며, 특히 대형 배열의 경우 그 효과가 더욱 큽니다.
#include 

// 함수 선언, 4개 int 요소의 배열을 가리키는 포인터를 매개변수로 받음
void printArray(int (*arrPtr)[4], int numRows) {
    for (int i = 0; i < numRows; ++i) {
        for (int j = 0; j < 4; ++j) {
            // 포인터 역참조를 사용하여 배열 요소에 접근
            printf("%d ", (*(arrPtr + i))[j]);
        }
        printf("\n");
    }
}

int main() {
    int matrix[3][4] = {{1, 2, 3, 4},
                   {5, 6, 7, 8},
                   {9, 10, 11, 12}};
    // 배열 포인터를 함수에 전달
    printArray(matrix, 3);
    return 0;
}

코드 설명:

  • printArray 함수에서 int (*arrPtr)[4]는 배열 포인터이며, 4개 int 요소의 배열을 가리킵니다.
  • (*(arrPtr + i))[j]는 배열 요소에 접근하는 데 사용됩니다. 여기서 arrPtr + i는 포인터가 i번째 행의 시작 위치를 가리키도록 하고, *(arrPtr + i)는 i번째 행의 배열을 역참조한 다음 [j]는 해당 배열의 j번째 요소에 접근합니다.
  • main 함수에서 printArray를 호출할 때 arr을 직접 전달합니다. 2차원 배열 이름 arr은 함수에 전달될 때 첫 번째 행의 배열 포인터로 암시적으로 변환됩니다.

다양한 크기의 2차원 배열 처리를 위한 배열 포인터 사용

다양한 크기의 2차원 배열을 처리하려면 배열의 열 수를 함수에 추가 매개변수로 전달해야 합니다.

#include 

// 함수 선언, int 요소 배열의 포인터와 열 수를 매개변수로 받음
void printArrayGeneric(int (*arrPtr)[], int numRows, int numCols) {
    for (int i = 0; i < numRows; ++i) {
        for (int j = 0; j < numCols; ++j) {
            // 포인터 역참조를 사용하여 배열 요소에 접근
            printf("%d ", (*(arrPtr + i))[j]);
        }
        printf("\n");
    }
}

int main() {
    int matrix[3][4] = {{1, 2, 3, 4},
                   {5, 6, 7, 8},
                   {9, 10, 11, 12}};
    int matrix2[2][5] = {{1, 2, 3, 4, 5},
                   {6, 7, 8, 9, 10}};
    // 배열 포인터와 열 수를 함수에 전달
    printArrayGeneric(matrix, 3, 4);
    printArrayGeneric(matrix2, 2, 5);
    return 0;
}

코드 설명:

  • printArrayGeneric 함수는 배열 포인터 int (*arrPtr)[]를 받으며, 여기서 []가 비어 있으면 임의 크기의 1차원 배열 포인터를 받을 수 있습니다.
  • numRows와 numCols 두 매개변수를 함께 전달하여 함수가 배열의 크기를 알고 올바르게 순회하고 배열 요소를 출력할 수 있도록 합니다.

포인터 배열을 함수 매개변수로 사용

포인터 배열을 함수 매개변수로 사용할 때는 함수 선언과 정의에서 매개변수가 포인터 배열의 포인터여야 함에 주의해야 합니다. 배열이 함수에 전달될 때 배열은 포인터로 축소됩니다.

#include 

// 함수 선언
void printStrings(char *arr[], int size);

int main() {
    char *strings[] = {"Hello", "World", "C Programming"};
    int numStrings = sizeof(strings) / sizeof(strings[0]);
    printStrings(strings, numStrings);
    return 0;
}

// 함수 정의
void printStrings(char *arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%s\n", arr[i]);
    }
}
#include 

// 함수 선언
void printStrings(char **arr, int size);

int main() {
    char *strings[] = {"Hello", "World", "C Programming"};
    int numStrings = sizeof(strings) / sizeof(strings[0]);
    printStrings(strings, numStrings);
    return 0;
}

// 함수 정의
void printStrings(char **arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%s\n", arr[i]);
    }
}

위의 코드에서 printStrings 함수는 포인터 배열을 매개변수로 받을 수 있습니다. 여기에는 두 가지 동등한 함수 선언 및 정의 방식이 있습니다:

  • void printStrings(char *arr[], int size);: char *arr[] 형식을 사용하여 명확하게 포인터 배열임을 나타냅니다.
  • void printStrings(char **arr, int size);: char **arr 형식을 사용합니다. 배열이 매개변수로 전달될 때 포인터로 축소되므로 char *arr[]는 char **arr와 동일합니다.

설명

  • main 함수에서 문자열 리터럴의 주소를 저장하는 포인터 배열 strings를 선언합니다.
  • strings 배열의 요소 개수를 계산하기 위해 sizeof(strings) / sizeof(strings[0])를 사용합니다.
  • printStrings 함수를 호출하고 strings 배열과 요소 개수를 전달합니다.
  • printStrings 함수에서 char **arr 또는 char *arr[]를 사용하여 포인터 배열을 받습니다. 여기서 arr은 포인터의 포인터입니다. 왜냐하면 strings 배열에 저장된 포인터 요소를 가리키기 때문입니다.
  • for 루프를 사용하여 포인터 배열을 순회하고 printf를 사용하여 각 문자열을 출력합니다. %s 형식 지정자는 문자열을 출력하는 데 사용되며, arr[i]는 해당 문자열을 가리킵니다.

주의사항

  • 포인터 배열을 사용할 때 포인터가 가리키는 데이터가 유효한지 확인하고 초기화되지 않거나 떠다니는 포인터를 사용하지 않도록 해야 합니다.
  • 함수에서 포인터 배열 요소가 가리키는 데이터를 수정할 경우 원래 배열의 데이터에 영향을 미칠 수 있습니다. 왜냐하면 포인터가 전달되기 때문입니다.

포인터를 함수 반환값으로 사용

1. 로컬 배열 주소 반환 사례

#include 
char *function();

int main() {
    char *ptr = function();
    printf("%s", ptr);
    return 0;
}

char *function() {
    char buffer[] = "hello";
    return buffer;
}

주소를 반환할 때 가리키는 내용이 이미 해제된 것이면 반환할 의미가 없습니다.

2. 정적 로컬 배열 주소 반환

(전역 변수와 같습니다)

#include 
char *function();

int main() {
    char *ptr = function();
    printf("%s", ptr);
    return 0;
}

char *function() {
    static char buffer[] = "hello";
    return buffer;
}

3. 문자열 상수 영역 주소 반환

#include 
char *function();

int main() {
    char *ptr = function();
    printf("%s", ptr);
    return 0;
}

char *function() {
    char *buffer = "hello";
    return buffer;
}

여기서 문자열은 문자열 상수 영역에 저장되며 내용은 항상 존재합니다.

4. malloc으로 할당된 공간 주소 반환

#include 
#include 
#include 
char *function();

int main() {
    char *ptr = function();
    printf("%s", ptr);
    free(ptr);
    return 0;
}

char *function() {
    char *buffer = (char *)malloc(100);
    strcpy(buffer, "hello world");
    return buffer;
}

문자열은 힙 영역에 저장되며 해제하지 않는 한 내용은 항상 존재합니다.

함수 주소를 저장하는 포인터(함수 포인터)

1. 함수 포인터의 개념

우리가 정의한 함수는 프로그램을 실행할 때 함수의 지시문이 메모리의 코드 영역에 로드됩니다. 따라서 모든 함수에는 시작 주소가 있습니다.

C 언어에서 함수 이름은 함수의 시작 주소, 즉 함수의 진입점 주소입니다.

포인터 변수를 사용하여 함수의 주소를 저장하는 것이 함수 포인터 변수입니다.

2. 함수 포인터의 용도

  1. 함수 포인터는 함수의 진입 주소를 저장하는 데 사용됩니다.
  2. 프로젝트 개발에서 함수 포인터 매개변수를 가진 함수를 작성하거나 호출하는 경우가 자주 발생합니다.
  3. 리눅스 시스템에서 다중 스레드를 생성하는 함수에는 스레드 함수의 진입 주소를 받는 함수 포인터 매개변수가 있습니다. 즉, 스레드 생성이 성공하면 새로운 작업이 스레드 함수를 실행합니다.
#include 
스레드 생성 함수:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

매개변수 1: 스레드 ID
매개변수 2: 스레드 속성 설정 -> 기본적으로 NULL 작성
매개변수 3: 실행할 스레드 함수의 주소
매개변수 4: 스레드 함수에 전달할 매개변수
컴파일 시 스레드 라이브러리와 링크
Compile and link with -pthread.

3. 함수 포인터 정의

반환값형 (*함수포인터변수명)(매개변수목록)

1. void * (*func)(void *):

이것은 함수 포인터의 선언입니다.

  • func는 함수 포인터의 이름입니다.
  • (*func)는 func가 함수를 가리키는 포인터임을 나타냅니다.
  • void *는 해당 함수의 반환 유형으로, 해당 함수는 void * 유형의 포인터를 반환합니다. void *는 모든 유형의 데이터를 가리킬 수 있는 범용 포인터 유형이지만, 사용할 때 명시적 형 변환이 필요합니다.
  • (void *)는 해당 함수의 매개변수 목록으로, 해당 함수는 void * 유형의 매개변수를 하나 받습니다. 마찬가지로 void * 유형의 매개변수는 모든 유형의 포인터를 받을 수 있지만, 함수 내에서 사용할 때 적절한 형 변환이 필요합니다.

2. int (*p)(int, int):

이것도 함수 포인터의 선언입니다.

  • p는 함수 포인터의 이름입니다.
  • (*p)는 p가 함수를 가리키는 포인터임을 나타냅니다.
  • int는 해당 함수의 반환 유형으로, 해당 함수는 정수를 반환합니다.
  • (int, int)는 해당 함수의 매개변수 목록으로, 해당 함수는 두 개의 int 유형 매개변수를 받습니다.
#include 
int max(int a, int b);
int main() {
    int a = 4, b = 5;
    int (*funcPtr)(int, int);
    funcPtr = max;
    printf("%d", funcPtr(a, b));
    return 0;
}

int max(int a, int b) {
    return a > b ? a : b;
}

함수 포인터 호출은 직접 함수를 호출하는 형식과 동일합니다.

4. 함수 포인터 배열

개념

함수 포인터 배열은 배열이며, 해당 요소는 함수를 가리키는 포인터입니다. 이 포인터들은 동일한 매개변수 목록과 반환 유형을 가진 함수 그룹을 저장하고 호출하는 데 사용할 수 있습니다.

정의

형명 (*배열명[요소개수]) (매개변수목록)

형명 (*배열명[요소개수]) (매개변수목록)
int (*funcArr[5])(int, int);
// 5개 요소의 함수 포인터 배열 정의, funcArr[0]~funcArr[4], 각 요소는 함수 포인터 변수
// 각 함수 포인터는 int를 반환하고 두 개의 int 매개변수를 받는 함수를 가리킵니다

예: 함수 포인터 배열을 사용하여 덧셈, 뺄셈, 곱셈, 나눗셈 함수 호출 구현

#include 

int add(int, int);
int subtract(int, int);
int multiply(int, int);
int divide(int, int);

int main() {
    int (*operations[4])(int, int) = {add, subtract, multiply, divide};
    int i, num1, num2;
    char op;
    while (1) {
        printf("두 수를 입력하세요:\n");
        scanf("%d%d", &num1, &num2);
        getchar();
        printf("연산자를 입력하세요:\n");
        scanf("%c", &op);
        switch (op) {
        case '+':
            printf("%d+%d=%d\n", num1, num2, operations[0](num1, num2));
            break;
        case '-':
            printf("%d-%d=%d\n", num1, num2, operations[1](num1, num2));
            break;
        case '*':
            printf("%d*%d=%d\n", num1, num2, operations[2](num1, num2));
            break;
        case '/':
            printf("%d/%d=%d\n", num1, num2, operations[3](num1, num2));
            break;
        default:
            printf("\n연산자 입력 오류\n");
            break;
        }
    }
    return 0;
}

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

int divide(int a, int b) {
    if (b != 0)
        return a / b;
    else {
        printf("\n나눗셈의 분모는 0이 될 수 없습니다\n");
        return 0;
    }
}

혼동하기 쉬운 것:

int *func(void); // 함수 선언; 함수가 int* 유형을 반환함을 선언; 포인터 함수
int (*func)(void); // 괄호로 묶여 있으므로

5. 특수 포인터

  1. void 포인터

void *는 범용 포인터로, 모든 유형의 포인터를 void * 유형의 포인터 변수에 할당할 수 있습니다.

int *ptr;
void *voidPtr;
voidPtr = ptr; // 직접 할당할 수 있으며 형 변환이 필요하지 않음

void * 포인터 변수는 32비트 시스템에서 4바이트이고 64비트 시스템에서 8바이트입니다.

  1. NULL(널 포인터)
char *ptr = NULL; // 주소 번호가 0인 것으로 가정할 수 있습니다.

C 언어에서 NULL은 일반적으로 (void *)0으로 정의된 미리 정의된 매크로입니다.

#include 
int main() {
    int *ptr = NULL;
    printf("%p", ptr);
    return 0;
}

main 함수 매개변수

int main(int argc, char *argv[]) // argc는 반드시 정수 변수여야 하고 argv는 char* 유형의 포인터 변수여야 하며 문자열을 가리켜야 합니다

매개변수 의미:

  • argc(argument count): 정수로, 명령줄 매개변수의 개수를 나타냅니다. 이 매개변수의 값은 최소 1입니다. 왜냐하면 프로그램 이름 자체도 매개변수로 취급되기 때문입니다. 예를 들어 명령줄에서 ./test를 실행할 때 argc 값은 1입니다. ./test arg1 arg2를 실행하면 argc 값은 3입니다.
  • argv(argument vector): 포인터 배열로, 각 요소는 문자열을 가리키는 포인터입니다. argv[0]는 프로그램 이름을 가리킵니다(일부 운영체제에서는 전체 경로를 포함할 수 있음), argv[1]은 첫 번째 명령줄 매개변수를 가리키고, argv[2]는 두 번째 명령줄 매개변수를 가리키는 식입니다.
#include 
int main(int argc, char *argv[]) {
    int i;
    printf("argc = %d\n", argc);
    for (i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

const

1. 일반 변수를 수식하면 읽기 전용을 의미합니다.

2. 포인터 수식

  1. const char *ptr;char const *ptr;과 동일합니다.

의미: 포인터 변수가 가리키는 내용은 ptr을 통해 수정할 수 없으며, ptr이 가리키는 내용을 보호하기 위함입니다.

open(const char *path, int oflag, ...);
/* 
이것은 파일을 열기 위한 함수입니다:
매개변수 1: 열 파일의 경로 주소
const를 수식하는 이유는 파일의 주소를 가져와서 파일 내용을 수정하지 않기 위함입니다.
*/
#include 
#include 
#include 
int main(int argc, char *argv[]) {
    char buffer[20] = "hello";
    const char *ptr = buffer;
    strcpy(ptr, "world"); // ptr이 가리키는 내용을 수정할 수 없음
    ptr = "world"; // ptr의 가리키는 대상을 수정할 수 있음
    return 0;
}
  1. char *const ptr;

의미: ptr은 읽기 전용 변수이며, ptr은 다른 곳을 가리키도록 수정할 수 없습니다.

#include 
#include 
#include 
int main(int argc, char *argv[]) {
    char buffer[20] = "hello";
    char *const ptr = buffer;
    strcpy(ptr, "world"); // ptr이 가리키는 내용을 수정할 수 있음
    // ptr = "world"; // ptr의 가리키는 대상을 수정할 수 없음
    printf("%s\n", ptr);
    return 0;
}
  1. const char *const ptr;

의미: ptr의 가리키는 대상을 수정할 수도 없고 ptr이 가리키는 내용도 수정할 수 없습니다.

#include 
#include 
#include 
int main(int argc, char *argv[]) {
    char buffer[20] = "hello";
    const char *const ptr = buffer;
    // strcpy(ptr, "world"); // ptr이 가리키는 내용을 수정할 수 없음
    // ptr = "world"; // ptr의 가리키는 대상을 수정할 수 없음
    return 0;
}

태그: C언어 포인터 메모리 관리 배열 함수 포인터

7월 10일 21:16에 게시됨