Programming 기초/C Language

C언어 기초#11 이중포인터, 함수포인터, 배열포인터, void포인터

코딩상륙작전 2023. 4. 18. 21:57

* 이중 포인터(double pointer) = 포인터의 포인터(pointer to pointer)

**q는 *q가 가리키는 위치의 내용이다. *q는 q가 가리키는 위치의 내용이다.

#include<stdio.h>

int main(void)
{
	int i = 100;	//정수 변수 선언
	int* p = &i;	// 포인터 p는 i를 가리킨다.
	int** q = &p;	// 이중포인터 q는 p를 가리킨다.

	*p = 200;	// p를 통하여 i에 200 저장
	printf("i=%d\n", i);
	
	**q = 300;	// q를 통하여 i에 300 저장
	printf("i=%d\n", i);
	
	return 0;
}
-------------------------------------------------
i=200
i=300

이중 포인터가 가장 많이 사용되는 상황은 외부에서 정의된 포인터 값을 함수의 인수로 받아서 변경하려고 하는 경우이다.

#include<stdio.h>

void set_pointer(char** q);
char* proverb = "All that glisters is not gold.";

int main(void)
{
	char* p = "zzz";
	set_pointer(&p);	// 포인터 p의 주소를 복사전달
	printf("%s \n", p);
	return 0;
}

void set_pointer(char** q)	// 이중 포인터 q를 통하여 외부의 포인터 p를 변경한다.
{
	*q = proverb; // 간접 참조 연산자 * 
}
#include<stdio.h>

void set_pointer(char** q);
char* proverb = "All that glisters is not gold.";

int main(void)
{
	char* p = "zzz";
	set_pointer(p);	// p의 값을 복사 전달
	printf("%s \n", p);
	return 0;
}

void set_pointer(char* q)	// 함수 내에서의 변화가 main 함수에 적용되지 않는다.
{
	q = proverb;
}

 

* 포인터 배열(an array of pointers)

- 배열의 원소가 포인터인 배열이다.

- int* ap[10]; 에서 [ ] 연산자가 *연산자보다 우선순위가 높으므로 ap는 먼저 배열이 되고 int*(포인터)들의 배열이 된다.

char* fname[4][10] = {
	"apple",
    "blueberry",
    "orange",
    "melon"
};

위의 문장은 메모리를 낭비한다. 아래와 같이 행들의 길이가 가변적으로 변할 수 있는 래그드 배열(ragged array) 방법으로 작성한다.

char* fname[4] = {
	"apple",
    "blueberry",
    "orange",
    "melon"
};

* 배열 포인터(a pointer to an array)

- 배열을 가리키는 포인터.

- int (* pa) [10] 의 형식이다. int [10]을 가리키는 포인터이다.

#include<stdio.h>

int main(void)
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int(*pa)[5];	// int[5] 배열에 대한 포인터 선언
	int i;

	pa = &a;	// 배열 포인터에 배열의 주소를 계산하여 대입한다.
	for (i = 0; i < 5; i++)
		printf("%d \n", (*pa)[i]);	// 배열 포인터를 이용하여서 배열의 각 원소에 접근한다. *pa가 배열이 된다.

	return 0;
}

포인터 배열 vs 배열 포인터

  • 포인터 배열 : int* ap[10] 포인터들의 배열이다.
  • 배열 포인터 : int (*ap)[10] 배열int[10]을 가리키는 포인터이다.

 

*함수 포인터(function pointer)

함수 포인터는 함수가 시작되는 주소를 가리킨다.

 

반환형 (*함수포인터이름)(매개변수1, 매개변수 2, ...);
함수를 가리키는 포인터를 선언한다. 반드시 괄호가 필요하다. 왜냐하면 괄호에 의하여 pf가 먼저 포인터가 되어야 한다. int *pf(int, int)는 정수형 포인터를 반환하는 pf라는 함수가 된다.
예 : int (*pf) (int, int);
void (*pf) (double);

함수 포인터는 반환형, 매개 변수 등이 정확히 일치하는 함수만을 가리킬 수 있다.

int sub(int, int);	//함수 원형 정의
int (*pf)(int, int);	// 함수 포인터 정의
...
pf = sub;	// 함수의 이름을 함수 포인터에 대입. 함수포인터는 &를 쓰지 않는다.

함수 포인터를 이용하여 함수를 호출할 수 있다. 만약 매개 변수만 일치하면 이름이 다르더라도 함수를 바꿔가며 가리킬 수 있다.

result = (*pf) (10, 20);

대부분의 컴파일러는 (*pf)대신 pf를 함수 이름처럼 사용해서 호출하는 것을 허용한다.

result = pf(10, 20);

 

* 함수 포인터의 배열

반환형 (*배열 이름[배열의 _크기]) (매개변수 목록);
예 : int (*pf[5]) (int, int);
void (*pf[5]) (double);

// 함수 포인터 배열 계산기
#define _CRT_SECURE_NO_WARNINGS	// scanf() 경고 무시
#include <stdio.h>
//함수 원형 정의
void menu(void);
int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);
void menu(void)
{
	printf("=======================\n");
	printf("0. 덧셈\n");
	printf("1. 뺄셈\n");
	printf("2. 곱셈\n");
	printf("3. 나눗셈(몫)\n");
	printf("4. 종료\n");
	printf("=======================\n");
}
int main(void)
{
	int choice, result, x, y;
	// 함수 포인터 배열을 선언하고 초기화한다.
	int (*pf[4]) (int, int) = { add, sub, mul, div }; // 함수포인터배열

	while (1)
	{
		menu();

		printf("메뉴를 선택하시오:");
		scanf("%d", &choice);

		if (choice < 0 || choice >= 4)	// 4번메뉴 또는 1~4선택 외 입력시 프로그램종료.
			break;

		printf("2개의 정수를 입력하시오: ");
		scanf("%d %d", &x, &y);

		result = pf[choice](x, y);	//함수 포인터를 이용한 함수 호출
		printf("연산 결과 = %d\n", result);
	}
	return 0;
}
int add(int x, int y)
{
	return x + y;
}

int sub(int x, int y)
{
	return x - y;
}

int mul(int x, int y)
{
	return x * y;
}
int div(int x, int y)
{
	return x/ y;
}

 

* 다차원 배열과 포인터

c에서는 행우선 방법(row-major)으로 다차원 배열을 메모리에 저장한다.

다차원 배열의 포인터 연산은 아래 그림과 같이 해석된다.

#include <stdio.h>
#define ROWS 4
#define COLS 3

int m[ROWS][COLS] = { {10, 20, 30}, {10, 20, 30}, {10, 20 ,30}, {10, 20, 30} };
double get_total_avg(int m[][COLS])
{
	int* p, *endp;
	double sum = 0.0;

	p = &m[0][0];
	endp = &m[ROWS - 1][COLS - 1];
	
	while (p <= endp)
		sum += *p++;
	sum /= ROWS * COLS;
	return sum;
}

void main(void)
{
	printf("m배열의 평균 : %f\n", get_total_avg(m));
	return 0;
}

 

* const 키워드

const char *p 
const 키워드가 *연산자 앞에 있으면 포인터가 가리키는 대상이 변경되지 않는다는 것을 의미한다.
char *const p;
const 키워드가 * 연산자 다음에 있으면 포인터 자체가 변경되지 않는다는 것을 의미한다. 하지만 포인터가 가리키는 값은 변경할 수 있다.

 

* volatile 키워드

주로 동일한 메모리를 여러 개의 프로세스나 스레드가 사용할 때 필요. volatile은 다른 프로세스나 스레드가 값을 항상 변경할 수 있으니 값을 사용할 때마다 다시 메모리에서 읽으라는 것을 의미한다. 따라서 이것은 컴파일러의 최적화를 방해한다. 하지만 불시에 변경되는 값을 처리하는 경우에는 불가피하다.

volatile char *p;
p가 가리키는 내용이 수시로 변경되니 사용할 때마다 다시 로드하라는 의미.

 

*void 포인터

  • 포인터를 선언할 당시에는 아직 구체적으로 대상물이 정해지지 않은 경우에 유용.
  • void형 포인터는 순수하게 메모리의 주소만을 가지고 있는 변수이다.

 

- '포인터의 형변환' 복습 -
double *pd = &f; //변수 f의 주소를 포인터 pd에 대입.
int *pi;

pi = (int*)pd; // double형 포인터를 int형 포인터로 변환
  • *연산자를 사용하려면 반드시 명시적인 대상을 가리키는 포인터 타입으로 형변환을 하여야 한다.
  • 증감 연산자도 사용이 불가하다.
void *vp;
*vp // 오류
*(int*)vp; // void형 포인터를 int형 포인터로 변환한 후에 간접참조한다.

vp++; // 오류.

 

*main 함수의 인수

main()도 함수이므로 변수와 반화값을 가질 수 있다. 

int main(int argc, char *argv[])
{
	...
}
  • argc는 명령어가 가지는 인수들의 개수
  • argv는 명령어가 가지는 인수들을 문자열 형태로 전달한다.
  • 만들어진 프로그램은 최종적으로 확장자가 exe인 실행 파일이 되어서 하드 디스크에 저장된다. 
  • argv[]배열의 원소들은 명령어 행에 있는 인수들의 주소를 가지게 된다.
#include <stdio.h>

int main(int argc, char* argv[])
{
	int i = 0;

	for (i = 0; i < argc; i++)
		printf("명령어 행에서 %d번째 문자열 = %s\n", i, argv[i]);
	return 0;
}

이 프로그램을 실행시킬 때는 이전과는 다르게 해야 한다. 두 가지 방법이 있다.

  1. 첫 번째는 DOS 창을 이용하는 것.
  2. 두 번째는 visual Studio에서 명령 인수를 입력하는 방법이다. [프로젝트] -> [프로젝트_이름.exe속성]을 선택, 디버깅 탭에서 명령 인수 칸에 원하는 문자열을 입력한다.