11. 선택 연산
11.2 힙 영역 메모리
11.2.1 메모리 관리
힙 저장 공간의 주요 문제점은 다음과 같다.
메모리 누수(leaked object) : new 를 통해 힙 영역에 데이터를 생성하고 나서 delete 를 사용하지 않을 때 메모리 누수가 발생한다. -> 메모리 누수는 프로그램의 저장 공간을 줄이기 때문에, 심각한 문제가 될 가능성이 있다.
빠른 소멸(premature deletion) : 개체를 delete 하고 나중에 그 포인터를 사용하고자 할 때
중복 소멸(double deletion) : 개체를 두번 소멸할 때 -> 한번 개체를 소멸하면, 해당 메모리 공간은 운영체제에 의해 다른 변수에게 할당될 수 있다. -> 두번 개체를 소멸하는 것은 다른 변수의 메모리 공간을 소멸한다는 것! (정말 잘못된 상황을 초래할 수 있다.. )
위와 같은 문제점이 있기에, 'new', 'delete' 를 사용하는 방식에 두 가지 권장 사항이 있다.
꼭 필요하지 않다면, 동적 할당을 하지 않는다. -> 스택에 변수를 우선적으로 저장한다.
힙 영역에 개체를 생성할 때, 그것들을 관리해주는 표준 라이브러리 or 컨테이너를 사용해라. -> 예를 들어, STL 컨테이너는 기본적으로 동적으로 메모리 공간을 관리해주고, 내부적으로 개체의 생성과 소멸을 담당한다. -> string, vector, unique_ptr, shared_ptr 등 ..
11.2.2 배열
개체의 배열 역시 new 로 생성할 수 있다.
개체의 배열을 삭제할 때 delete[] 가 사용된다. -> [] 를 사용하지 않는다면 배열의 첫번째 메모리만 해제되고, 나머지 해제되지 않는다.
배열과 컨테이너는 new 를 통해 힙영역에 생성할 수 있지만, 지역 개체에는 new 를 사용하지 말아야 한다. -> 아래 코드는 비효율적이다.. -> return 이나 delete 이전에 발생한 예외는 메모리 누수를 일으킬 것이다. (지역 변수를 사용하자.. )
11.2.3 메모리 공간 확보
new 키워드를 통해 힙 영역의 메모리 공간을 확보할 수 있다.
new 가 할당할 수 있는 공간을 찾지 못하면 기본 설정 상 std::bad_alloc 이라는 예외를 던진다. -> 아래 코드는 엄청난 메모리 공간을 소모하고, 결국 bad_alloc 예외를 호출할 수 밖에 없다.
11.3 리스트 {}
11.3.1. 리스트 {} 구현 모델
{} 는 아래와 같이 다양한 용도로 사용된다.
-> 이는 {} 가 범용 모델이며 컴파일러에 의해 효율적인 최적화가 가능하다.
{} 가 생성자로 사용되는 경우 ->
A a{1, 2};
의 형태로 사용되는데, 해당 형태는 데이터 손실이 있는(Narrowing) 변환을 불어한다. -> 또한 initializer_list 를 매개변수로 받는 생성자가 오버로딩 되어 있다면 컴파일러는 가장 먼저 해당 생성자를 사용하기 때문에, 사용에 주의가 필요하다.{} 가 어떤 집합체(배열, 컨테이너, 클래스 등.. ) 의 원소를 초기화하는 경우
{} 가 initializer_list 개체를 생성하는데 사용되는 경우
{} 타입은 auto 와 함께 사용시 모든 원소가 동일한 타입일 경우에만 사용이 가능하다.
11.3.2 qualified list
특정 클래스나 구조체의 멤버를 초기화 할 때 {} 사용하면 qualified list 라고 부른다.
11.3.3 unqualified list
특정 클래스나 구조체의 멤버를 초기화하는 용도가 아닌 경우 unqualified list 라고 부른다.
일반적인 템플릿 인자에 대해 unqualified list 타입을 추론할 수 없다.
auto 타입은 {} 를 initializer_list<> 타입으로 추론해 주었지만, 템플릿에서는 명시적으로 타입을 지정해주지 않는다면 컴파일 에러가 발생한다. -> 이는 언어상의 제약이다!
마찬가지로 아래의 경우에도 컴파일러는 타입을 추론하지 못해 컴파일 에러를 발생시킨다. -> f2 함수 사용 어디에도 vector 를 사용한다는 걸 알 수 없다.. (추론 불가) ->
너무 모호한 정보만을 제공하고 추론해달라고 하는 것은 약간 무책임? 하다 생각된다..
11.4 람다 표현식
람다는 이름이 없는 함수 개체를 정의하고, 사용하기 위한 목적의 단순화된 표기법이다.
이런 사용 방식은 일반적인 함수 사용 방식보다는, 특히 연산을 인자로 알고리즘에 전달하고자 할 때 유용하다!
람다 표현식은 몇가지 부분으로 나뉜다.
캡처 리스트 : 캡부 변수들을 복사 or 참조할 것인지를 표현
매개변수 리스트 : 람다 표현식에 어떤 인자가 필요한지 표현
mutable 지정자 : 값에 의해 갭처된 변수의 대한 람다의 복제본을 변경 하도록 표현
-> 타입 : 반환타입을 지정
람다 본체 {} : 실행될 코드를 지정
11.4.1 구현 모델
실제로 람다식은 컴파일러가 해당 람다식에 대한 익명 클래스를 생성하고, 해당 클래스의 개체를 만들어서 사용한다!
람다로 구현한 다음 코드를 살펴보자
위 코드의 의미를 살피기 위해서 컴파일러가 만들어낸, 동일한 역할을 하는 함수가 있는 개체를 정의해보자.
캡처 리스트는 두 개의 멤버 변수와 그것을 초기화하는 하나의 생성자가 된다. (복사생성자)
람다의 본체는 단순히 operator() 의 본체가 된다.
위 람다식은 값을 반환하지 않기 떄문에, 리턴타입은 void 이다.
기본 설정상 operator() 는 const 이므로, 람다 본체는 캡처된 변수를 변경하지 않는다. -> 람다 본체에서 캡처된 변수를 변경하고 싶다면 mutable 로 선언해야 한다.
아래와 같이 람다로부터 생성되는 임시 클래스 개체를 closure object 라고 부른다.
클로저 개체(closure object)
람다 표현식이 사용될 때 컴파일러는 클로저 객체라는 특별한 타입의 객체를 생성합니다.
클로저 개체는 다음과 같은 특징을 가진다.
캡처된 변수들
람다 표현식의 캡처 목록에 있는 변수들을 멤버 변수로 저장합니다.
캡처 방법에 따라 값으로 캡처하거나 참조로 캡처할 수 있습니다.
연산자 오버로딩
람다 표현식의 함수 본문은 클로저 객체의
operator()
로 구현됩니다.아래의 람다식을 컴파일러가 클로저 개체로 만들어 준다면 대략적으로 다음과 같을 것이다.
의미를 파악했으니 실제 코드를 사용할 때 아래와 같이 사용할 수 있다. -> 재사용 할 것 같지 않는 코드인데, 내부적으로 클래스를 정의하는 것은 생각보다 번잡한 일이다..
11.4.2 람다의 대안
람다를 사용하기 이전에 케이스부터 람다를 사용하고 또 개선하는 케이스를 살펴보자.
step1. 람다식 대신 단 한번만 사용되는 직전 클래스 생성
step2. 람다식 사용
step3. for_each 대신 for 문사용(람다식 미사용)
-> 더욱 직관적이다.
step4. 좀 더 많은 컨테이너를 사용하기 위한 템플릿 사용
11.4.3 캡처
11.4.3.1 람다와 수명
람다는 참조를 캡처할 때, 호출자보다 오래 살아남을 수 있다. -> 이 때문에, 문제가 생기게 된다.
아래 코드가 문제가 생기는 과정은 다음과 같다.
setup() 이 종료되고 차후에(아마 몇분 뒤) 사용자가 "draw triangle" 버튼을 누르면 람다는 오래 전에 사라진 지역 변수에 접근하려 할 것이고,
이는 지역변수가 이미 사라진 후이기 때문에, 문제가 발생한다.
때문에, 람다가 호출자보다 오래 살아 남을것이라고 생각한다면 값들이 return 매커니즘이나, 적절한 인자를 통해서 반환 되도록 만들어야 한다. -> = 캡처 리스트는 람다가 만들어지는 순간 값을 복사해놓기 때문에, 이후에 사용하더라도 문제가 없다.
11.4.3.3 람다와 this
멤버 함수에 쓰인 람다에서 클래스 개체의 멤버에 접근하기 위해서 캡처 리스트에 this 를 추가하면 된다. -> [this] 은 값에 의한 캡처가 아닌, 참조에 의한 캡처이다. -> 때문에, 주의가 필요하다..
11.4.3.4 mutable 람다
대부분의 함수 개체(closure Object) 의 상태를 변경할 필요가 없기 때문에, 기본 설정상 값에 의해 캡처된 값에 대한 변경은 불가능하게 되어있다. -> operator() 는 const 이다.
상태를 변경해야 하는 이벤트가 만약에 생긴다면 mutable 로 선언할 수 있다.
Last updated