220902 C언어#11

김혜진·2022년 9월 2일
0

C언어

목록 보기
11/13

C언어 #11

함수와 포인터

배열을 함수의 인자로 전달하기

  • 기본적인 함수의 인자 전달 형태
    기본적인 함수의 인자 전달 형태는 복사 형태이다.
    A에서 B로 복사 시 A에도 값이 존재하고 B에도 값이 존재한다.

    ❗ A와 B는 별도의 메모리이며 값만 복사가 된다.

배열형의 인자는 포인터형으로 받는다

  • 1-2개, 또는 5-6개까지 정도는 전달인자로 사용할 수 있다. 하지만 그 이상 사용하게 되면 난잡해보인다.
  • 배열을 전달인자로 사용하면 여러 개의 값을 넘길 수 있다.
  • 어떻게 배열을 전달하고 받을 것인가? 바로 포인터이다.
  • "배열의 이름은 포인터이다" 배열과 포인터는 형태만 다를 뿐 참조할 수 있다.

배열의 이름은 첫 번째 주소값을 가리키고 있으므로 포인터라고 할 수 있다.

#include<stdio.h>

void func(int* pArr);

int main(void)
{
	int arr[] = { 1,2,3,4,5 };
	int i;

	func(arr);
	for (i = 0; i < 5; i++)
	{
		printf("배열의 요소 출력 : %d\n", arr[i]);
	}
	return 0;
}

void func(int* pArr)
{
	int i;
	for (i = 0; i < 5; i++)
	{
		printf("함수 안에서 배열의 요소 출력 : %d\n", *(pArr + i));
	}
	printf("\n");
}

출력결과
함수 안에서 배열의 요소 출력 : 1
함수 안에서 배열의 요소 출력 : 2
함수 안에서 배열의 요소 출력 : 3
함수 안에서 배열의 요소 출력 : 4
함수 안에서 배열의 요소 출력 : 5

배열의 요소 출력 : 1
배열의 요소 출력 : 2
배열의 요소 출력 : 3
배열의 요소 출력 : 4
배열의 요소 출력 : 5

  • 함수의 전달인자 arr은 주소값을 넘기고 있다.

  • 형식 인수의 형태가 int *pArr임을 볼 수 있다. 주소값을 넘겨받기 위해서는 포인터 형태로 받아야 하기 때문이다.

  • func로 배열의 모든 요소의 합을 리턴

#include<stdio.h>

int func(int* pArr, int size);

int main(void)
{
	int arr[] = { 1,2,3,4,5 };
	int sumArr, sizeArr;

	sizeArr = sizeof(arr) / sizeof(int);
	sumArr = func(arr, sizeArr);
	printf("배열의 총 합은 :%d\n", sumArr);

	return 0;
}
int func(int* pArr, int size)
{
	int i, sum = 0;
	for (i = 0; i < size; i++)
	{
		sum += *(pArr + i);
	}
	return sum;
}

주소를 복사했다 = 같은 메모리를 가지고 있다.


값 호출 방식과 참조 호출 방식

값 호출 방식(call-by-value)

  • 값 호출 방식이란 실인수의 값이 형식 인수로 전달되는 방식
  • 실인수의 메모리와 형식 인수의 메모리가 별도로 관리
  • 실인수의 값이 형식인수로 복사되는 형태
#include<stdio.h>

void callValue(int b);

int main(void)
{
	int a = 1;
	callValue(a);
	printf("실인수 a의 출력 : %d\n", a);

	return 0;
}

void callValue(int b)
{
	b = b + 3;
	printf("형식인수 b의 출력 : %d\n", b);
}

출력결과
형식인수 b의 출력 : 4
실인수 a의 출력 : 1

실인수 a의 값을 형식인수 b로 넘겨받으면서 별도의 메모리상에 복사가 일어난다.

두 개의 값을 바꾸는 예제

#include<stdio.h>

void Swap(int a, int b);

int main(void)
{
	int x = 10, y = 20;
	printf("초기값 x = %d, y = %d\n", x, y);
	Swap(x, y);
	printf("함수 밖에서 변경 후 x = %d, y = %d\n", x, y);
	return 0;
}

void Swap(int a, int b)
{
	int temp;
	temp = a;
	a = b;
	b = temp;
	printf("함수 안에서 변경 후 a = %d, b = %d\n", a, b);
}

출력결과
초기값 x = 10, y = 20
함수 안에서 변경 후 a = 20, b = 10
함수 밖에서 변경 후 x = 10, y = 20

실인수 x,y는 각각 형식인수 a,b에 값이 복사된다.
메모리 구조를 통해 이해해보자.

참조 호출 방식(call-by-reference)

함수 호출 시 전달인자로 메모리 접근에 사용되는 주소값을 전달

#include<stdio.h>

void callReference(int* b);

int main(void)
{
	int a = 1;
	callReference(&a);
	printf("실인수 a의 출력 : %d\n", a);

	return 0;
}

void callReference(int* b)
{
	*b = *b + 3;
	printf("형식인수 b의 출력 : %d\n", *b);
}

출력결과
형식인수 b의 출력 : 4
실인수 a의 출력 : 4

참조 호출 메모리 구조

b ⇒ 0x100
*b ⇒ 1

차이점
값에 의한 복사 : 각각 다른 메모리를 가짐
참조에 의한 복사 : 주소값을 복사한 것이기 때문에 메모리를 공유함

두 개의 값을 바꾸는 예제

#include<stdio.h>

void Swap(int *a, int *b);

int main(void)
{
	int x = 10, y = 20;
	printf("초기값 x = %d, y = %d\n", x, y);
	Swap(&x, &y);
	printf("함수 밖에서 변경 후 x = %d, y = %d\n", x, y);
	return 0;
}

void Swap(int *a, int *b)
{
	int temp;
	temp = *a;
	*a = *b;
	*b = temp;
	printf("함수 안에서 변경 후 a = %d, b = %d\n", *a, *b);
}

출력결과
초기값 x = 10, y = 20
함수 안에서 변경 후 a = 20, b = 10
함수 밖에서 변경 후 x = 20, y = 10

주소값 앞에 타입이 붙는다면 포인터 선언을 하는 것!

실인수 x,y는 값을 a와 b에 넘길 때 주소값 &x, &y를 넘기기 때문에 포인터 변수 a와 b에는 주소값이 대입된다.
메모리 구조를 통해 이해해보자.


포인터의 포인터

이중 포인터란

  • *이 한 개이든 **처럼 두개이든 무조건 *이 붙어있으면 포인터 변수이다.
int **pp

단일 포인터의 개념부터 알아보자.

int a = 5;
int *p;
p = &a;

포인터 p가 변수 a의 주소를 가리키고 있다.

  • 이중 포인터의 구조

a는 p가 p는 pp가 가리키고 있다.
포인터 변수 p와 이중 포인터 변수 pp를 통해서 a에 접근할 수 있다.
포인터의 주소값을 가지고 있는 것이 이중 포인터.


void형 포인터

void형 포인터의 기본 개념

함수의 리턴값이 없는 경우 void형으로 선언하는 정도

  • void형도 자료형인가?
#include<stdio.h>

int main(void)
{
	void a;
    return 0;
}

error C2182: 'a' : 'void' 형식을 잘못 사용했습니다.

void형은 메모리를 얼마나 할당할 지 알 수 없기 때문이다.

  • void형 포인터로 선언해보자.
int main(void)
{
	void* a;
    return 0;
}

void a 문장의 오류에 막연히 오류가 날 것이라고 예상했지만 오류가 뜨지 않는다.
void형 포인터는 메모리 주소값을 저장하기 위한 포인터형이다.
포인터 변수는 무조건 4바이트 메모리가 할당되므로, 4바이트 메모리가 할당된다.

  • void* 형에 임의의 대상체를 대입하고 출력해보자.
#include<stdio.h>

int main(void)
{
	void* a;
	int b = 1234;

	a = &b;
	printf("%d\n", *a);

	return 0;
}

실행결과: error C2100: 간접참조가 잘못되었습니다.

void형 포인터인 a는 정수형 b의 주소값을 받고 있지만 주소값만 넘겨받았을 뿐이지 a입장에서는 몇 바이트만큼의 메모리를 읽어야 할 지 알 수 없다.

(예제 수정 후)

#include<stdio.h>

int main(void)
{
	void* a;
	int b = 1234;

	a = &b;
	printf("%d\n", *(int)a);

	return 0;
}

출력결과
1234

*a 출력 시 *(int*)a를 사용
참조하고 있는 변수 b가 정수이므로 int형, 포인터형이므로 형변환 자료형은 int*, a가 가리키고있는 변수의 값을 참조하므로 변수명 앞에 * 연산자 붙이기
*((int*)a)와 같은 표현

#include<stdio.h>

int main(void)
{
	void* a;
	double b = 3.14;

	a = &b;
	printf("%f\n", *(double*)a);

	return 0;
}

출력결과
3.140000


함수 포인터

함수 포인터의 선언

  • 함수 포인터의 선언 방법은 다음과 같다.
    자료형(*함수 포인터 이름)(인자 목록)

  • 함수명 앞에 *만 붙여주면 함수 포인터가 선언된다.
    int(*func)(int a);

  • 함수 포인터도 포인터이므로 주소값을 저장한다.

포인터: 무조건 주소값 저장

#include<stdio.h>

int Add(int a, int b);
int Min(int a, int b);

int main(void)
{
	int a, b, sel, result;
	int (*fPtr)(int a, int b);

	while (1)
	{
		printf("다음 중 선택하시오 (1. 덧셈 2. 뺄셈 3. 종료 ) : ");
		scanf_s("%d", &sel);

		switch (sel)
		{
		case 1:
			fPtr = Add;
			break;
		case 2:
			fPtr = Min;
			break;
		case 3:
			return 0;
		default:
			break;
		}


		printf("두 정수를 입력하시오 : ");
		scanf_s("%d%d", &a, &b);
		result = fPtr(a, b);
		printf("결과 : %d\n", result);
	}
	return 0;
}

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

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

출력결과
다음 중 선택하시오 (1. 덧셈 2. 뺄셈 3. 종료 ) : 1
두 정수를 입력하시오 : 10 20
결과 : 30
다음 중 선택하시오 (1. 덧셈 2. 뺄셈 3. 종료 ) : 2
두 정수를 입력하시오 : 30 10
결과 : 20
다음 중 선택하시오 (1. 덧셈 2. 뺄셈 3. 종료 ) : 3

코드를 하나 만들어놓고 런타임에 결정을 하게 해주면 효율적인 프로그램을 만들 수 있다.

함수 포인터 사용 이유

  • 직접 함수 호출하면 되지 왜 복잡하게 포인터를 사용하나?
    컴파일 타임 또는 런타임에 메모리 크기 및 위치가 결정된다.
    이를 각각 정적 바인딩, 동적 바인딩이라고 한다.
    이클립스와 같은 플러그인 사용으로 예를 들어보면 이클립스 실행 시 플러그인은 매번 수행된다.
    함수 포인터를 사용하지 않았다면, 플러그인에 새로운 함수가 추가 될 때마다 매번 다시 컴파일해야하는 비효율적인 상황이 발생된다.
    함수 포인터의 사용은 프로그램의 확정성과 유용성을 위함이다.

NULL 포인터

NULL 포인터란 아무것도 가리키지 않는 포인터를 말한다.
아무것도 가리키지 않는다는 의미는 포인터 변수에 아무런 주소값도 저장되어 있지 않는다는 의미이다.
NULL은 아무것도 가리키지 않는다는 의미를 가지고 있지만, 실전에서는 함수의 동작 에러 체크 용도로 사용한다.

char *p = Func();
if(p == NULL)
{
	// 에러 처리
}
else
{
	// 수행 처리
}
profile
알고 쓰자!

0개의 댓글