포인터 공부, Pointer

C++ 포인터

오늘은 포인터 공부를 한 내용을 정리 해볼려고 한다. 이번 포인터 공부는 The Cherno라는 Youtuber의 채널을 통해 하였다. 이분은 호주에서 개발자로 살고 계신 분 같은데 유익한 비디오를 많이 포스팅 하셨다.

 

YouTube 원문:  www.youtube.com/watch?v=DTxHyVn0ODg

 

프로그램을 작성하는데 있어서 메모리를 이해 하는 것은 매우 중요하다. 만약 우리가 게임 어플리케이션을 작성하고 실행을 하게 되면 이 앱은 컴퓨터 메모리의 로딩이 된다.

 

우리가 작성한 모든 코드들은 메모리의 로딩이 되며, 컴퓨터의 CPU가 메모리상에 로딩된 커맨드들을 액세스 하고 실행을 하게 된다. 우리가 작성한 프로그램을 하드 디스크에 저장하고 실행을 하게 되면,

 

첫번째로 프로그램이 메모리로 로딩이 되고, CPU가 메모리상에 로딩된 커맨드들을 액세스를 하며 실행을 하는 구조이다. 

 

이러한 메모리를 관리 하고 액세스를 할 수 있게 해주는 녀석이 바론 포인터이다.  포인터는 쉽게 integer 이다. 포인터는 컴퓨터 메모리의 주소를 저장하고 있는 integer 이다. 데이터 타입 (Data type) 과 상관없이 포인터는  메모리의 주소값을 가지고 있는 integer 이다. 데이터 타입은 프로그램을 작성할 때 엔지니어들에게 이 포인터는 몇 바이트인지만을 나타내 줄 뿐 포인터의 성질?을 바꿀 수 없다.. 포인터는 단지 메모리의 주소값을 가지고 있는 integer 이다. 

 

심플한 포인터 하나를 정의 하였다. nullptr은 c++11 에서 새로 소개된 것으로 NULL 값을 가질 수 있는 곳에 사용 할 수 있다. NULL은 실질적으로 #define NULL 0 이다. 메모리 주소 0 는 유효하지 않은 값으로 프로그램을 실행 할 경우 에러가 난다. 아래 예제를 실행하게 되면 에러가 나지만 여전히 ptr은 포인터 이다. 

#include <iostream>

int main()
{
	void* ptr = nullptr;
	std::cin.get();
}

이번엔 실질적으로 int 타입의 변수를 생성하고 메모리 주소값을 반환하는 예제를 보겠다.

#include <iostream>

int main()
{
	int var = 8;

	void* ptr = &var;
	std::cout << ptr << std::endl;
	std::cin.get();
}

위 예제를 실행 하게 되면 var 변수의 메모리 주소 값 0x0077FBEC 출력이 되는것을 알 수 있다. 보는것과 같이 16진수 형태의 숫자가 반환 되었으며 이것은 integer 이다.  여기서 & 앰퍼센드는 변수의 주소값을 반환할때 사용되는 키워드 이다. 0x0077FBEC  var 변수의 메모리의 주소 값이며, 이 메모리 주소 안에 var 변수의 값이 저장되어져 있다. 

 

Visual Studio 에서 Debug 모드로 코드를 실행 후 실질적으로 변수 var의 메모리 값을 확인 할 수 있다.

Breake point를 std::cout << ptr << std::endl; line 에 만든 후 디버깅 모드로 실행하자.

 

위 메뉴에서 Debug -> Windows -> Memory -> Memory 1을 실행 

Visual Studio 메모리 뷰 활성화 시키기

 

var 변수 메모리 주소 확인

위에 보는것과 같이 var변수의 메모리 주소 값은 0x00cffe4c 이다. 메모리 창을 열고 메모리 주소 값을 입력을 하게 되면 실제 0x00cffe4c 메모리 주소를 확인 할 수 있다. 위에 보는 것과 같이 이 메모리 주소상에는 숫자 8이 저장되어져 있는 것을 확인 할 수 있다. ( int 타입은 4 bytes 이기 때문에 총 4개의 byte가 할당 되어진 것을 볼 수 있다. )

 

아래 예제는 double 타입의 포인터를 생성하고 var의 메모리 주소값을 저장 한다. 위에서 언급한것과 같이 포인터를 공부하는데 있어서 데이터 타입은 중요하지 않다. 데이터 타입은 몇바이트의 메모리 값을 할당 해야 하는지 CPU에게 알려줄 뿐이다. 

 

#include <iostream>

int main()
{
	int var = 8;

	double* ptr = (double*)&var;
	std::cout << ptr << std::endl;
	std::cin.get();
}

위에 예제를 위와 같은 방법으로 실행을 하고 메모리 뷰에서 확인을 하면, 총 4 bytes의 메모리가 할당되어져 있고 value 8이 저장되어져 있는것을 확인 할 수 있다.

 

람다 펑션 - Lambdas function

람다 펑션은 실무에서 많이 사용되는 것중 하나로 아주 중요 합니다.

아래 예제는 std::stringfind_if()를 통해 string array 에서 "nut"을 포함 하는 string을 찾는 예제 입니다. 

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
 
bool containsNut(std::string str) 
{
	// 매게변수로 받은 str값에서 nut을 발견 하면 True 를 리턴한다.
	return (str.find("nut") != std::string::npos);
}
 
int main()
{
  const std::array<std::string, 4> arr{ "apple", "banana", "walnut", "lemon" };
 
  // std::find_if 의 세번째 인자는 포인터 펑션을 받는다.
  const auto found = std::find_if(arr.begin(), arr.end(), containsNut) ;
 
  if (found == arr.end())
  {
    std::cout << "No nuts\n";
  }
  else
  {
    std::cout << "Found " << *found << '\n';
  }
 
  return 0;
}

여기서 문제는  std::find_if()함수 포인터 (function pointer) 를 전달 해야 합니다.  그러기 위해서 펑션을 무조건 정의해야 합니다. 한번의 사용을 위해 펑션을 정의하는것은 유지보수 측면에서 힘들다. 


이러한 문제를 해결 하기 위해 나타난 람다펑션 - Lambdas to the rescue

lambda expression  (람다 또는 클로저라고도 부름)을 사용하면 다른 함수 내에 익명 함수 (anonymous functions)를 정의 할 수 있다. 

람다 구문은 c++ 의 다른 구문과 비교해 봤을때 조금 낯설어서 익숙해 지는데 다소 시간이 걸리지만, 계속적으로 사용을 하다 보면 금방 익숙해 질 수 있다. Lambda는 다음과 같은 형식을 취합니다.

[ captureClause ] ( parameters ) -> returnType
{
    statements;
}

여기서 capture clause parameters 는 생략 될 수 있습니다. 또한 리턴타입 (return type)optional 이며 만약 생략될 경우 auto 타입이 사용됩니다. 마지막으로 람다펑션의 이름 또한 생략 될 수 있습니다. 종합해 보면 람다 펑션은 아래와 같이 구현 할 수 있습니다.

#include <iostream>
 
int main()
{
  []() {}; // 캡쳐, 파라미터와 리턴 타입이 생략 되었다.
 
  return 0;
}

위에 첫번째 예제를 람다 펑션을 이용하여 재 구현 해보자.

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
 
int main()
{
  constexpr std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
 
  const auto found{ std::find_if(arr.begin(), arr.end(),
                           [](std::string_view str) // 람다펑션,  no capture clause
                           {
                             return (str.find("nut") != std::string_view::npos);
                           }) };
 
  if (found == arr.end())
  {
    std::cout << "No nuts\n";
  }
  else
  {
    std::cout << "Found " << *found << '\n';
  }
 
  return 0;
}

위 예제를 실행하게 되면 첫번째 펑션포인터를 사용한 결과와 동일하다. 


람다 펑션 유형 - Type of a lambda

위 예제에서 람다 펑션을 find_if 함수 안에 정의 하였다. (람다펑션의 시작은 항상 capture clause 으로 시작 되기 때문에[] 보이면, 아!! 이것은 람다펑션이구나!! 생각을 해주면 된다.)  이러한 사용 방식을 함수 리터럴 (function literal) 이라고 한다. 위와 같이 함수 안에 람다를 정의를 하게 되면 코드의 가독성이 떨어질 수 있다. 이러한 문제를 해결 하기 위해 우리는 람다 변수를 정의 한 후 람다 펑션을 사용 할 수 있다.

 

예를 들어,  std::all_of 함수를 사용하여 배열의 모든 요소가 짝수인지 확인합니다. 아래 예제는 std::all_of 펑션 안에 람다를 정의 하였다. 

// 코드의 가독성이 떨어지는 좋지 않은 example
return std::all_of(array.begin(), array.end(), [](int i){ return ((i % 2) == 0); });

위 코드는 아래와 같이 바꿀 수 있습니다.

// isEven 변수에 람다를 저장 한다.
auto isEven {
	[](int i) 
    {
    	return ((i % 2) == 0);
    }
}

return std::all_of(array.begin(), array.end(), isEven);

여기서 람다의 타입은 무엇일까? 람다의 타입은 컴파일시 컴파일러가 유니크한 타입을 만들어 준다. 

람다의 타입은 컴파일 타임까지 알 수는 없지만,  람다를 저장하기 위한 방법은 아래와 같이 여러 가지가 있다.

 

Capture clause가 생략되었을 때 함수펑션 (function pointer)를 사용할 수 있다.

// A regular function pointer. Only works with an empty capture clause.
    double (*addNumbers1)(double, double) {
        [](double a, double b) {
            return (a + b);
        }
    };

    std::cout << addNumbers1(1, 2) << std::endl;

Capture clause가 사용될 경우 std::function을 이용하여 람다를 정의 할 수 있다.

// Using std::function.The lambda could have a non - empty capture clause(Next lesson).
int aa = 10;
std::function<int(double, double)> addNumber2;
addNumber2 = [aa](double a, double b) {
	return (aa + a + b);
};

addNumber2(3, 4);

위 예제를 실행시 17이 출력 된다.

 

마지막으로 auto를 사용하여 람다를 정의 할 수 있다.

 // Using auto. Stores the lambda with its real type.
  auto addNumbers3{
    [](double a, double b) {
      return (a + b);
    }
  };
 
  addNumbers3(5, 6);

람다의 실제 유형을 사용하는 유일한 방법은 auto keyword을 사용하는 것입니다. autostd::function에 비해 오버 헤드가 없다는 이점도 있습니다.

 

하지만 람다를 정의할때 항상 auto 을 사용 할 수 있는 것은 아니다. 예를 들어 람다를 매개변수로 함수에 전달하고 호출자가 전달하는 람다의 타입을 결정 하기 때문에 auto를 사용 할 수 없다. 이러한 경우엔 std::function을 사용하여 정의 할 수 있다. 

#include <functional>
#include <iostream>
 
// We don't know what fn will be. std::function works with regular functions and lambdas.
void repeat(int repetitions, const std::function<void(int)>& fn)
{
  for (int i = 0; i < repetitions; ++i)
  {
    fn(i);
  }
}
 
int main()
{
  repeat(3, [](int i) {
    std::cout << i << '\n';
  });
 
  return 0;
}

위 예제는 다시 아래와 같이 바꿀 수 있다. using 키워드와 std::function 의 예는 melbourne-engineer.tistory.com/3?category=807213 


using printI = const std::function<void(int)>;
void repeat(int repetitions, printI fn) {
    for (int i = 0; i < repetitions; i++) {
        fn(i);
    }
}

int main()
{
  repeat(3, [](int i) {
    std::cout << i << '\n';
  });
 
  return 0;
}

 

 

이포스트의 원문은 www.learncpp.com/cpp-tutorial/introduction-to-lambdas-anonymous-functions/

'C++ 프로그래밍 > 함수 Functions' 카테고리의 다른 글

함수 포인터 Function Pointers - 1  (0) 2020.09.24

함수 포인터 - Function Pointer

함수 포인터 ( Function Pointer ) 에서 가장 헷갈리는 부분은 아마도 Syntax 일 것입니다. 함수 포인터의 Syntax 는 아래와 같습니다.

data-type (*pointerName)(parameters);

1. int (*fcnPtr)(int, std::string);

2. void (*fcnPtr)();

3. bool (*fcnPtr)(std::string);

첫번째로 데이터 타입이 나오며, 그 뒤로 함수 포인터의 이름, 그리고 포인터를 가르키는 * 이 함수 포인터 이름 앞에 붙습니다. 그리고 함수 포인터가 가르키는 함수의 parameter 타입을 정의 합니다. 

 

예를들면 

1. 함수의 리턴형은 int 여야 하고, int 값과 std:string 2개의 파라미터를 받는 함수여야 한다

2. 함수의 리턴형은 void, 파라미터는 받지 않는 함수여야 한다.

3. 함수의 리턴형은 bool 이며, std::string 을 파라미터로 받는 함수여야 한다.

 

함수 포인터에 가장 빠르게 익숙해 지는 방밥은 계속 사용하는 것입니다. 간단한 void 함수를 만든 후 void 함수를 함수 포인터를 할당 후, 함수 포인터를 사용하여 할당된 void 함수를 호출해 보도록 하겠습니다.

#include <iostream>

void firstFunction() {
	std::cout << "first function without any paramter" << std::endl;
}

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

bool thirdFunction(std::string name) {
	return !name.empty();
}


int main() {
	void (*firstPtr)() = firstFunction; 
    // 리턴타입이 void이며, parameter를 받지 않는 포인터 함수 선언; 선언과 동시에 firstFunction의 주소값을 포인터함수에 할당
	firstPtr();

	int(*secondPtr)(int, int) = secondFunction; 
    // 리턴타입이 int이며, 두개의 int 값을 파라미터로 받는 포인터 함수 선언; 선언과 동시에 secondFunction의 주소값을 포인터 함수에 할당
	std::cout << secondPtr(10, 20) << std::endl;

	bool (*thirdPtr)(std::string) = thirdFunction;
	if (thirdFunction("name")) {
		std::cout << "return value is true" << std::endl;
	}
}

 

함수를 다른 함수의 파라미터로 전달. (Callback Function)Passing functions as arguments to other functions

 

함수 포인터는 다른 함수의 파라미터로 전달 될 수 있으며, 흔히 콜백펑션이라고 부른다. 여기서 중요한 포인트는 함수를 전달 할때 정의되어져 있는 함수포인터의 리턴타입, 파라미터의 값일 정확하게 일치 하여야 한다. 

#include <utility> // for std::swap
#include <iostream>

// 세번째 인자는 포인터 함수로, bool 값을 리턴 하며, 두개의 int 값을 받는 comparisonFcn
void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int))
{
    for (int i = 0; i < (size - 1); ++i)
    {
        int bestIndex = i;

        for (int currentIndex = i + 1; currentIndex < size; ++currentIndex)
        {
            // 세번째 인자로 받은 함수 펑션을 호출한다. comparisonFcn은 두개의 int 값을 비교하여 True or False를 리턴한다.
            if (comparisonFcn(array[bestIndex], array[currentIndex]))
            {
                bestIndex = currentIndex;
            }
        }

        std::swap(array[i], array[bestIndex]);
    }
}

// 오름차순 정렬 함수. X 와 Y 의 값을 비교하여 X 가 Y 보다 클 경우 True를 작을 경우 False를 반환 한다.
bool ascending(int x, int y)
{
    return x > y;
}

// 내림차순 정렬 함수. X 와 Y 의 값을 비교하여 X 가 Y 보다 작을 경우 True를 클 경우 False를 반환 한다.
bool descending(int x, int y)
{
    return x < y; // swap if the second element is greater than the first
}

// 결과 출력 하는 함수
void printArray(int* array, int size)
{
    for (int index{ 0 }; index < size; ++index)
    {
        std::cout << array[index] << ' ';
    }

    std::cout << '\n';
}

int main()
{
    int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };

    // descending 함수를 3번재 파라미터로 전달. 
    // selectionSort 함수는 파라미터로 int 타입의 Array, int 값과 , bool (*comparisonFcn)(int, int) 함수 포인터를 받는다.
    selectionSort(array, 9, descending);
    printArray(array, 9);

    // ascending 함수를 3번재 파라미터로 전달. 
    // selectionSort 함수는 파라미터로 int 타입의 Array, int 값과 , bool (*comparisonFcn)(int, int) 함수 포인터를 받는다.
    selectionSort(array, 9, ascending);
    printArray(array, 9);

    return 0;
}

예제에서 보는것과 같이 ascending 함수와 descending 함수를 selectionSort 펑션의 파라미터 값으로 전달을 하여 selectionSort 함수 안에서 ascending과 descending를 호출 하여 사용하는 것을 볼 수 있다.  

 

그렇다면 왜? 이렇게 사용을 하는것일까? 함수 포인터를 사용하게 되면 우리는 다양한 함수를 selectionSort 함수 안에서 콜백펑션으로 사용을 할 수 있다. But 함수의 리턴 타입과 정의되어져 있는 파라미터 값이 일치 해야 한다. 

 

위에서 언급한것과 같이 함수 포인터의 문법은 data-type (*fcnName)(parameters) 이다. 위에 예제에서 보는것과 같이 콜백펑션을 사용 하기 위에 bool (*comparisonFcn)(int, int) 를 파라미터로 정의 하였다. 만약 bool (*comparisonFcn)(int, int) 함수포인터를 전반적인 프로젝트 내에서 사용을 한다고 가정을 하면 코드의 가독성이 떨어진다.

 

이 문제를 해결 하기 위해서 우리는 type aliases를 사용 할 수 있다.

 

bool (*comparisonFcn)(int, int); 함수 포인터는 using comparisonFcn = bool(*)(int, int); 로 바꿀 수 있다.

comparisionFcn은 리턴 타입이 bool 이면서, 두개의 int 파라미터를 가지고 있는 함수를 포인팅 할 수 있다. 

 

Type aliase를 사용하여 우리는 위 예제를 아래와 같이 바꿀 수 있다. 

using comparisonFcn = bool(*)(int, int); 
void selectionSort(int* array, int size, comparisonFcn compare)

 // 이 부분도 comparisonFcn 이 아닌 compare로 바꿔 주어야 한다
 // 세번째 인자로 받은 함수 펑션을 호출한다. comparisonFcn은 두개의 int 값을 비교하여 True or False를 리턴한다.
 if (compare(array[bestIndex], array[currentIndex]))
 {
 	bestIndex = currentIndex;
 }

C++ 11부터 <functional> standard library 에 정의되어져 있는 std::function 을 사용하여 함수 포인터를 정의 할 수 있다.

#include <functional>
bool validate(int x, int y, std::function<bool(int, int)> fcn); 
// std::function 펑션, 리턴 타입은 bool 이며, 두개의 int값을 파라미터로 받고 있다

위에 예문과 같이 < 리턴타입 ( 파라미터1, 파라미터2) > 정의되어져 있다. 만약 파라미터가 없고 bool 값만 리턴 한다면 std::function<bool () > 와같이 정의를 하면 된다. 

std::function<void(const std::string&)> mErrorHandler;
// 리턴 값은 void 이며, const타입의 string 파라미터를 받는 함수 포인터 정의

Type aliasestd::function 을 함께 사용하기.

using comparisonFcn = std::function<bool(int, int)>; 
void selectionSort(int* array, int size, comparisonFcn compare)

 // 이 부분도 comparisonFcn 이 아닌 compare로 바꿔 주어야 한다
 // 세번째 인자로 받은 함수 펑션을 호출한다. comparisonFcn은 두개의 int 값을 비교하여 True or False를 리턴한다.
 if (compare(array[bestIndex], array[currentIndex]))
 {
 	bestIndex = currentIndex;
 }

 

이 포스트의 원문은 www.learncpp.com/cpp-tutorial/78-function-pointers/ 

 

7.8 — Function Pointers | Learn C++

7.8 — Function Pointers By Alex on August 8th, 2007 | last modified by nascardriver on August 8th, 2020 In lesson 6.7 -- Introduction to pointers, you learned that a pointer is a variable that holds the address of another variable. Function pointers are

www.learncpp.com

 

+ Recent posts