c++고급 기능2

김성진·2024년 1월 31일

1월 31일 수업 정리

함수 포인터

함수도 메모리상에 저장되기 때문에 포인터를 통해 가리킬 수 있다.
형태는 데이터형 (포인터이름) (매개변수 ) 로 만든다.
반드시 (
포인터이름) 형태를 해야 하며 소괄호가 없다면 함수의 꼴이 된다.

bool (*fp)(int, int);

fp라는 이름을 가진 함수 포인터를 선언했다.
단순히 함수 하나만을 대응하는게 아닌, 함수 이름과 관계없이 데이터형과 매개변수가 동일한 함수들은 전부 같은 함수 포인터로 참조할 수 있다.
형태만 같다면 fp로 여러 함수를 변경해가며 대입할 수 있는것이다.
매개변수 두개가 같은지를 판단하는 함수 Equals 를 만들고, 함수 포인터로 그 함수를 가리켜서 함수 대신 사용해보자.

#include <iostream>

bool Equals(int num1, int num2)
{
	return num1 == num2;
}

bool NotEquals(int num1, int num2)
{
	return num1 != num2;
}

int main()
{
	bool (*fp)(int, int) = NotEquals;
	fp = Equals;

	std::cout << Equals << std::endl;
	std::cout << Equals(1, 2) << std::endl;
	std::cout << fp(1, 2) << std::endl;

	return 0;
}

fp를 마치 Equals 함수처럼 사용해도 동일하게 동작한다. 둘이 같은 주솟값을 가지고 있으니 당연하다. 마치 배열 포인터를 배열처럼 사용하는것과 비슷한 원리다.

이런것은 어디에 사용할 수 있을까?
동일한 함수를 여러번 호출하는것은 반복문으로 만들 수 있지만, 여러 함수를 호출하는것은 직접 코딩해야만 했다.
이럴 때, 함수 포인터를 사용하면 간편하게 해결할 수 있다.
최솟값을 찾는 함수를 만들고, 어떤 함수가 들어와도 최솟값을 찾아준다거나,
배열의 각 요소의 합을 전부 더하는 함수에서, 배열의 값에 변화를 주는 함수가 뭐가 들어오는 결과를 출력할 수 있다. 그럼 배열의 각 요소를 더하는 예제 코드를 작성해보자.

int Square(int n)
{
	return n * n;
}

int MyFunc(int n)
{
	return n * 3 - 2;
}

int MyFunc2(int n)
{
	return n;
}

int Sum(int* arr, int size, int (*fp2)(int))
{
	int sum = 0;
	for (int i = 0; i < size; ++i)
	{
		sum += fp2(arr[i]);
	}
	return sum;
}

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

	std::cout << Sum(arr, 5, MyFunc2) << std::endl;
	std::cout << Sum(arr, 5, Square) << std::endl;
	std::cout << Sum(arr, 5, MyFunc) << std::endl;

	return 0;
}

함수 객체

함수는 함수호출 연산자 () 를 통해 호출된다. 따라서 이것을 재정의한 클래스가 있다면 그 객체가 함수처럼 동작할 수도 있는것이다. 이것을 함수 객체라고 한다. (펑터 라고도 한다)
()연산자 오버로딩은 이전에 사용한 operator 함수를 통해 이루어진다.
반환형 operator()(매개변수) 의 형태이다.

그럼 매개변수를 받으면, 초기화된 멤버 변수와 비교해서 값을 리턴하는 펑터 클래스 Equals 를 만들어보자.

class Equals
{
private:
	int number;
public:
	Equals(int n) : number(n)
	{
	}

	bool operator()(int x)
	{
		return number == x;
	}
};
int main()
{
	Equals eq(123);

	std::cout << eq(1) << std::endl;
	std::cout << eq(123) << std::endl;

	return 0;
}

결과값은 0 1 이 나온다. 1과 123을 매개변수로 받아서 멤버변수의 값 123과 비교한것이다.

스마트 포인터

포인터를 사용한 동적 할당을 사용하게 되면서 c와 c++ 프로그래머들은 메모리 누수가 굉장한 골칫거리이다. 이를 완화하기 위해 고안된 것이 스마트 포인터이며, 이들은 delete를 자동으로 해주기 떄문에 복잡한 코딩에서 메모리 해제에 대한 부담을 크게 줄여준다. 세가지 종류가 있다.
스마트 포인터들은 동적 할당에만 사용한다. 정적 할당은 원시 포인터를 사용하면 된다.

유니크 포인터

제약이 제일 많은 스마트 포인터이다. 프로그래밍 언어에서는 제약이 많은것을 선호하고 그것부터 사용해야 한다. 안전하기 때문이다.
std::unique_ptr a(new int(5)); 이런 형태로 사용된다.
유니크 포인터가 가진 제약은 얕은 복사를 허용하지 않는 것이다.
즉, 하나의 주소값을 한 포인터만 가리킬 수 있는것이다. 유일한 이라는 뜻의 유니크라는 이름이 붙은 이유이다.

	std::unique_ptr<int> a(new int(5));
	std::unique_ptr<int> b;
	b = a;

이러한 코드를 짜면, 컴파일 에러가 난다. b는 a의 값을 복사할 수 없다.
유니크 포인터가 가진 값을 넘겨주려면 특정한 함수를 사용해야 한다.

	std::unique_ptr<int> b(a.release());

release 함수를 쓰면 a에 있는 값을 삭제하고, b에 넘긴다. 결국 하나의 값은 하나만 가지는것은 변하지 않는다.

	b.reset();
	b.reset(new int(10));

reset은 유니크 포인터 안에 들어있는 값을 없애고 널 값을 대신 집어넣는것이다.
이렇게 되면 delete를 따로 하지 않아도 자동으로 원래 들어있던 객체의 메모리를 해제한다.
reset과 동시에 다른 값을 가리키게 할 수도 있으며 이때도 해제는 자동으로 실행된다.

셰어드 포인터

셰어드 포인터는 유니크 포인터보다는 제약이 덜하다. 이름처럼 여러 포인터가 한 객체를 공유해서 가리킬 수 있으며, 한 포인터라도 그 객체를 가리키는것이 남아있다면 객체를 소멸시키지 않는다. 어느 포인터가 소멸을 담당할지 , 그리고 그 포인터가 마지막에 남도록 하는 등의 작업이 불필요한 것이다.
std::shared_ptr a(new int(5));
의 형태로 사용할 수 있다.
셰어드 포인터 또한 릴리즈와 리셋 등의 함수를 유니크와 동일하게 사용할 수 있다.

위크 포인터

위크 포인터는 그 객체를 가리키기는 하지만 소유하지는 않는, 그래서 그 값을 바꾸거나 소멸시키지 않는 포인터이다.
std::shared_ptr a(new int(5));
std::weak_ptr b = a;

하지만 위크 포인터는 소유권이 없기에 가리키는 대상이 소멸되었을 가능성이 있다.
따라서 소멸되었는지 아닌지 확인하는 expired 메소드가 필요하다.
expired 메소드는 weak 포인터가 가리키는 대상이 소멸되었는지 아닌지를 true false로 리턴하며 true면 소멸되었다는 뜻이다.
b.expired() 처럼 사용한다.

또한 위크 포인터가 가리키는 대상을 역참조하고 싶으면 바로 대입을 하는것이 아니라
lock 이라는 메소드를 사용해야 한다. lock 은 대상 객체가 존재하면 그 객체에 대한 셰어드 포인터를, 소멸되었으면 NULL을 가리키는 셰어드 포인터를 리턴한다.
*b.lock() 으로 사용한다.
약한 포인터라는 이름처럼 그 객체에 관여하지 않아야 할때 사용한다.
보통 그 객체가 위크 포인터로 역참조 하는것보다 늦게 사라지는게 보장될때 사용해야 좋다.

profile
듀얼리스트

0개의 댓글