[Modern C++] 스마트 포인터 개론

 

이번에는 스마트 포인터에 대해 간략하게 알아보자.

 

우선 포인터에 대해 다시 알아보자면 포인터는 양날의 검과 같다.

장점은 메모리에 저장되어 있는 원본 데이터에 접근해서 수정 등이 가능하기 때문에 메모리 절약 및 효율성이

올라가지만, 단점은 메모리 오염이 일어나기 쉽다는 것이다. 

 

A와 B 클래스를 만들어서 동적 할당을 하고 A 클래스에서 B 클래스를 받아서 B 클래스의 데이터를 일부 수정한다고

가정하자. 그러던 도중 B 클래스가 어떤 사유에 의해 동적 할당이 해제되고 그 이후에 A 클래스가 B 클래스의 데이터를

수정하기라도 하면 바로 메모리 오염이 발생한다. 메모리 오염은 매우 치명적이기 때문에 절대로 발생해서는 안 되는 

실수이다.

 

이러한 단점을 보완한 것이 스마트 포인터이다. 좀 더 정확히 설명하자면 스마트 포인터는 

포인터를 알맞은 정책에 따라 관리하는 객체를 말한다. 

 

스마트 포인터는 크게 세 가지 개념이 존재한다.

shared_ptr, weak_ptr, unique_ptr

하나하나 알아보도록 하자.

 


 

shared_ptr

 

스마트 포인터에서 가장 대표 격인 친구이다.

shared_ptr은 레퍼런스 카운트를 관리한다. 말 그대로 참조 카운트, 즉 원본 포인터 객체를 몇 명이나 참조하고 있는지

카운트를 하고 있다가 아무도 해당 포인터 객체를 참조하고 있지 않을 때 비로소 delete를 진행한다.

 

또 한가지 스마트 포인터의 특징이 있는데, 사용자가 명시적으로 delete를 하지 않아도 자동으로 참조를 파악해서

delete를 진행하기 때문에 좀 더 수월한 코딩을 할 수 있다.

 

이제 shared_ptr의 단점을 얘기해 보자면, shared_ptr을 이용한 객체 A, B가 존재한다고 가정하자.

A는 B를 참조하고 B는 A를 참조하는 상황이다. 즉 서로가 서로를 참조하고 있다. shared_ptr의 특징은 참조하고 있는

객체가 있을 경우 메모리에서 할당이 해제되지 않는다는 점인데 이렇게 서로를 참조하고 있다면 두 객체는 메모리에서

절대 해제되지 않고 계속 남아있게 되는 문제점이 있다. 그렇기 때문에 메모리 할당을 해제하기 전에 포인터 객체를

받아주는 부분을 nullptr로 밀어주어서 할당을 해제해야 한다.

 


 

weak_ptr

 

weak_ptr의 경우 위에서 얘기한 shared_ptr의 단점을 보완하기 위한 ptr이다.

shared_ptr의 경우 내부적으로 참조 카운트를 관리하고 있는데 weak_ptr의 경우 참조 카운트와 더불어

weak 카운트를 관리한다. weak 카운트는 참조하고 있는 객체의 숫자를 나타낸다.

 

이러한 weak_ptr은 객체의 생명주기에 직접적으로 관여하는 친구는 아니지만 해당 객체가 메모리에서 

해제되었는지 아닌지를 체크해주는 기능을 한다.

참고로 expired()로 할당이 해제됐는지 아닌지 체크할 수 있다.

 

해당 객체가 메모리에 아직 존재한다면 lock() 기능을 이용해서 다시 shared_ptr로 변환을 해주고 원래

사용하던 것처럼 포인터를 이용하면 된다.

 

정리하자면 weak_ptr를 이용해서 포인터 객체를 받아주고 해당 포인터 객체가 아직 메모리에 존재한다면

shared_ptr로 변환해서 사용하는 방식이다. 즉 shared_ptr를 사용하기 전에 미리 체크한다는 개념으로 생각하면

될 것 같다. 

 


 

unique_ptr

 

unique_ptr의 경우 매우 간단하다.

말 그대로 나만 포인터 객체를 가지고 있는 가리키고 있는 형태라고 생각하면 된다.

기본적으로 다른 포인터에게 넘겨주지 못하지만 만약에 넘겨주려고 하면 오른값을 이용해서 넘겨줄 수 있다.

즉 복사는 허용해주지 않고 이동만 허용해주는 포인터라고 생각하면 된다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 람다 표현식  (0) 2022.07.19
[Modern C++] 전달 참조  (0) 2022.07.18
[Modern C++] 오른값(rvalue) 참조  (0) 2022.07.16
[Modern C++] override, final  (0) 2022.07.15
[Modern C++] nullptr  (0) 2022.07.15

 

[Modern C++] 람다 표현식

 

이번에는 람다를 알아보자.

람다란? 함수 객체를 빠르게 만드는 문법이다.

 

[ 이전에 공부했던 함수 객체를 다시 한번 간단하게 정리하자면 객체를 함수처럼 사용할 수 있는 문법이다.

즉 괄호()를 오버 로딩해서 객체가 함수처럼 동작하도록 만들어서 사용하는 것을 함수 객체라고 한다. ]

 

다시 돌아와서 람다는 이러한 함수 객체를 빠르게 만드는 문법이다. 잘 활용하면 10분 걸릴 코드를 1분 만에

작성할 수도 있다.

 


실습을 진행해보자

1-1. 실습 세팅

본격적으로 람다를 실습하기 전에 기본적인 세팅을 해주었다.

이전에 배운 enum class 문법을 이용해서 아이템 타입과 아이템의 레어도를 만들어주었고,

Item 클래스를 아이템 아이디, 레어도, 타입을 받아주는 형태로 만들었다.

 


 

1-2. 기본 함수 객체 사용

메인 함수에서 Item class를 가지는 vector를 선언해주고 push_back을 이용해서 Item class의 임시 객체를

넣어주는 모습이다. 임시 객체를 넣어주다 보니 이전에 공부했던 오른값 참조로 받아주는 모습이 보인다.

 


 

1-3. 기본 함수 객체 사용

어떤 아이템을 특정 조건에 맞춰서 찾고 싶은 상황이라고 가정하고 코드를 작성했다.

이전에 배웠던 벡터에서 조건에 맞는 데이터를 찾기 위해 find_if를 사용하고 벡터의 시작 지점부터 끝 지점까지

스캔하면서 조건에 맞는 데이터를 찾는 것이다. 마지막 조건 부분에 함수 객체를 만들어서 넣어주었고 해당 함수 객체는

오버 로딩을 이용해서 벡터에 저장되어 있는 아이템의 레어도가 Unique라면 true를 반환하도록 되어 있다.

결괏값으로 아이템 id가 정상 출력되는 것을 확인할 수 있다.

 

지금은 코드의 양이 적기 때문에 이미지 1-3과 같이 함수 객체를 struct나 class를 이용해서 만들고 사용해도 

큰 문제는 없지만 상황에 따라 코드의 양이나 아이템 등이 많아지면 위와 같이 매번 함수 객체를 만들어서 사용하기가

불편하다. 그래서 사용하는 것이 람다이다. 이제부터 람다를 실습해보자.

 


 

2-1. 람다 사용
2-2. 람다 타입 지정

람다는 기본적으로 '[]() {};' 이러한 형태로 사용한다. 이 형태는 이름이 정해지지 않은 익명 상태이다.

괄호 부분에 매개 값을 받아주고 중괄호 부분에 구현 부분을 넣어주면 된다. 따로 bool 타입으로 명시하지 않아도

자동으로 return 타입을 추측해서 넣어주게 되고 명시적으로도 타입을 지정할 수 있다. (이미지 2-2, int 타입 지정)

 

 

2-3. 람다 사용

만들었던 람다 표현식에 이름을 지정해주고 해당 이름을 기존의 함수 객체 자리에 넣어서 정상 작동

하는 것을 확인하였다. 그리고 62줄 코드 자체를 클로저(closure)라고 한다. 뜻은 람다에 의해 만들어진

실행 시점 객체라는 뜻을 가지고 있다.

 

 

람다 표현식에 이름을 지정해줘서 계속 사용할 수도 있지만 일회성으로 사용하고 싶다면 아래와 같이 할 수도 있다.

2-4. 람다 사용

find_if의 조건 부분에 람다 표현식을 넣어서 일회성으로 사용하는 방법이다.

이외에도 람다는 특정 데이터를 받아서 해당 데이터가 존재하는지 조건을 만들어주는 것도 가능하다.

 


 

3-1. 데이터를 받아주는 람다

 

[ ] 대괄호를 캡처라고 하며 변수 값을 복사하는 방식과 참조하는 방식이 존재한다.

3-1 이미지에서는 복사 방식을 이용했고 복사한 데이터(아이템 아이디)가 벡터 내에 존재하는지 확인한다.

 

사용자가 명시적으로 캡처를 지정하지 않으면 암시적으로 전부 복사 방식으로 동작하게 된다.

캡처를 참조 방식으로 하게 되면 변수의 주소 값을 받아주게 되는 차이만 생긴다.

 

 

3-2. 여러 데이터를 받아주는 람다

마찬가지로 하나의 데이터가 아닌 여러 데이터를 받아주는 람다를 만들었다.

 

 

3-3. 캡처 모드 각자 지정

이번에는 각 데이터마다 복사 방식으로 할 것인지 참조 방식으로 할 것인지 지정해주는 

방법이다. 대괄호 안에 해당 데이터마다 하나씩 지정해주면 된다.

 

일반적으로 한 번에 복사인지 참조인지 지정하는 방식보다는 데이터마다 복사, 참조를 지정해주는

방법을 좀 더 권장한다. 그 이유는 캡처 안에 데이터를 넣기 때문에 어떤 데이터가 있는지 알 수 있고

데이터마다 복사 방식인지 참조 방식인지 한눈에 알 수 있어서 가독성이 높아진다.

 

그리고 중요한 개념 하나는 특정 class 안에 함수를 만들고 그 안에 람다를 만들게 되면 캡처 안에는 

기본적으로 this 포인터가 존재하게 된다. 즉 해당 클래스의 주소 값을 가지고 있게 되므로 웬만하면

캡처 안에 복사(=)나 참조(&) 같이 한 번에 처리하는 방법 말고 데이터를 하나하나 지정해주는 방법이

권장된다. 

 

물론 람다를 외부로 넘길 일이 전혀 없다면 한 번에 처리해도 크게 상관은 없다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 스마트 포인터 개론  (0) 2022.07.19
[Modern C++] 전달 참조  (0) 2022.07.18
[Modern C++] 오른값(rvalue) 참조  (0) 2022.07.16
[Modern C++] override, final  (0) 2022.07.15
[Modern C++] nullptr  (0) 2022.07.15

 

[Modern C++] 전달 참조

 

오늘은 전달 참조에 대해 알아보자.

C++17 이전에는 전달 참조가 보편 참조로 불렸다.

보편 참조 == 전달 참조, 같은 의미이니 잘 숙지해두자.

 

 

일단 전달 참조 같은 경우 오른값 참조 문법과 거의 같다고 보면 된다.

1-1. 오른값 참조 복습겸 실습

Hoon 클래스를 생성하고 기본, 복사, 이동 생성자를 각각 만들어주었다.

그리고 별도의 함수를 생성해주고 오른값 참조를 받도록 선언하였다.

메인 함수에서 클래스 객체를 생성하고 별도로 만들었던 함수를 호출하며 전달해주는 값으로 클래스 객체를

넣어주는데 일반적으로는 넣을 수 없으니 std::move를 통해 오른값으로 캐스팅 후 넣어주었다.

 

하지만 오늘의 주제는 전달 참조이며 전달 참조는 오른값 참조와 눈으로 보기엔 다름이 없다.

즉 '&&' 처럼 코드가 만들어져 있어도 무조건 오른값 참조는 아니라는 소리이다. 

 

 

1-2. 전달 참조, 오른쪽 참조

28~32번 코드를 보면 템플릿을 통해 새로운 함수를 만들어주었고 전달받는 값으로는 '&&'로 선언해주었다.

그리고 메인 함수로 넘어가서 해당 함수를 호출해주었다.

39번 코드를 보면 37번 코드처럼 std::move를 통해 오른값 참조로 변환하여 넣어주어서 정상 작동한다.

근데 40번 코드를 보면 왼값 참조를 그냥 넣어주었는데 에러가 발생하지 않고 심지어 일반 참조 타입으로 

정상 호출되는 것을 알 수 있다.

 

해당 문제는 템플릿에만 해당되는 것이 아니라 auto에도 적용된다. 

정리하자면 컴파일러가 자동으로 타입을 추측해서 변환해주는 '형식 연역'이 일어날 때 전달 참조가 발생한다.

즉 29번 코드와 같이 템플릿 혹은 auto를 사용하는 것을 전달 참조라고 한다.

 

하지만 해당 코드를 변경하게 되면 전달 참조로서 동작하지 않는다. 29번 코드와 같이 'T&&'로만 만들어져

있어야 전달 참조이고 앞에 const 같은 것이 붙게 되면 오른값 참조로서 동작한다.

 

그렇다면 전달 참조는 왜 존재하는 것일까?

그냥 오른값 참조로서만 사용하면 안 되는 것일까?

 

전달 참조가 만들어진 이유는 효율성 때문이다.

왼값 참조와 오른값 참조를 동시에 가지고 있기 때문에 각각 만들어주지 않고 템플릿과 auto를

활용해 효율적으로 만들 수 있기 때문에 전달 참조가 사용된다.

(그렇다고 자주 사용되는 것은 아니다.)

 

 

다음은 자주 헷갈리는 부분을 알아보자

2-1. 헷갈리는 부분

참조 타입의 h2를 선언하고 미리 선언했었던 Hoon 클래스 타입의 h1을 받아주었다.

그리고 오른값 참조 타입으로 선언된 h3에 std::move를 이용하여 h2를 넣어주었다.

(참고로 Test1이라는 함수는 오른값을 받아주는 형태의 함수이다.)

오른값 참조 타입의 h3를 Test1 함수에 넣어주려고 하는데 이상하게 에러가 발생한다.

오른값을 받아주는 함수에 오른값 참조 타입의 변수를 넣었는데 왜 에러가 발생하는 것일까

 

그 이유는 h3가 오른값 참조 타입이지만 h3 자체는 왼값이기 때문이다.

오른값이라는 정의 자체가 단일식에서 벗어나면 사용할 수 없다고 했다. 즉 해당 라인을 벗어나면

다른 곳에서는 사용할 수가 없다. 하지만 h3의 경우 해당 라인을 벗어나서도 사용이 가능하다.

즉 타입만 오른값 참조 타입일 뿐 h3 자체는 왼값이다. 그래서 한 번 더 std::move를 이용해서 

함수에 넣어주어야 정상 작동한다.

 

 

진짜 문제는 지금부터이다.

위의 헷갈리는 문제에서 파생되는 문제인데 바로 전달 참조로 받아주었을 때 조건에 따라 

받아준 값을 왼값으로 처리해서 복사를 할지 아니면 오른값으로 처리해서 이동을 할지 선택해야 한다.

전달 참조 같은 경우 왼값과 오른값 모두 사용할 수 있기 때문에 이러한 문제가 생길 수 있다.

 

왼값인지 오른값인지 조건에 따라 나눠주는 기능을 따로 지원하는데 

std::forward가 해당 기능을 지원한다.

2-2. std::forward()

std::forward(value) 이렇게 사용하면 되고 value의 값이 왼값이라면 복사를 진행하고

value의 값이 오른값이라면 value는 오른값 참조를 가지고 있지만 value 자체가 왼값이기 때문에

한 번 더 std::move를 이용해서 오른값 참조로서 사용하게 된다.

 

즉 39번 코드가 실행되면 이동 생성자가 실행되고,

40번 코드가 실행되면 복사 생성자가 실행된다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 스마트 포인터 개론  (0) 2022.07.19
[Modern C++] 람다 표현식  (0) 2022.07.19
[Modern C++] 오른값(rvalue) 참조  (0) 2022.07.16
[Modern C++] override, final  (0) 2022.07.15
[Modern C++] nullptr  (0) 2022.07.15

 

[Modern C++] 오른값(rvalue) 참조

 

우선 개념적으로 왼값(lvalue)와 오른값(rvalue)가 있다.

 

lvalue(left value) : 단일식을 넘어서 계속 지속되는 개체

rvalue(right value) : lvalue가 아닌 나머지 값(임시 값, 열거형, 람다, i++ 등)

 

이렇게만 보면 이해가 안될 텐데 구체적으로 왼값과 오른값이 뭘까?

 

1-1. 왼값, 오른값 설명

'='을 기준으로 나누게 값을 나누게 되는데 왼값의 설명을 다시 한번 쓰자면 

'단일식을 넘어서 계속 지속되는 개체'이다. 이게 무슨 뜻이냐면 15번 라인에 변수 a를 선언하고 이 라인을 넘어서

계속해서 값을 변경하거나 사용할 수 있는 개체라는 것이다. 이것이 왼값이다. 반대로 = 오른쪽에 있는 정수 3은

해당 라인 이후로는 더이상 지속되지 않는다. 일시적인 임시 값이라는 뜻이다.

 

 

2-1. 클래스 예시

클래스를 이용해 실습을 진행해보자.

Hoon 클래스를 만들고 따로 함수를 만들어서 해당 클래스를 받아주는 형식으로 코드를 만들었다.

27번 라인에서 함수에 lvalue를 넘겨주었고 당연히 정상 작동한다. 

28번 라인은 Hoon 클래스의 기본 생성자를 바로 넘겨주는 형태이며 rvalue에 해당한다. 에러가 발생했다.

30번 라인은 동일하게 기본 생성자를 넘겨주는 형태이지만 const 참조 타입으로 선언하니 에러가 발생하지 않았다.

const 하나가 붙었다고 오른값을 받아주는 걸 확인할 수 있었다. 그러면 const가 없는 28번 코드는 왜 오른값을

받아줄 수 없을까?

 

오른값이라는 것 자체가 곧 사라질 임시적인 값이라는 뜻이기 때문에 해당 값을 받아주어서 다른 여러 동작들을

하게 되면 위험 부담이 크다. 그렇기 때문에 const라는 안전장치를 붙여줘야지만 받아줄 수 있는 것이다.

 

하지만 const로 선언하게 되면 여러 제약이 생긴다.

이미 공부했듯이 const는 읽기 전용으로 만드는 것이다. 수정이 불가능하다. 21번 라인에 함수는 const 참조로 

만들었기 때문에 클래스를 받아주고 난 후에 클래스 내부에 있는 값을 수정한다던가 클래스 내부 함수 등을

수정할 수 없게 된다. 이런 문제를 해결할 수 있는 것이 C++11의 오른값 참조이다. 

 

 

3-1. 오른값 참조

23번 라인에 오른값 참조 코드를 넣어주었다. 오른값 참조를 하겠다고 선언하는 방법은 && 참조 두 개를

붙여서 선언하는 것이다.

 

34번 코드를 보면 기존에 왼값이었던 hoon 객체를 더 이상 넣어줄 수 없는 것을 확인할 수 있고

반대로 35번 코드를 보면 기존에 오른값이었던 Hoon 클래스의 생성자를 정상적으로 넣어줄 수 있는 것을 볼 수 있다.

 

오른값 참조는 다른 일반 참조 타입과 마찬가지로 주소를 넘겨준다. 

그렇기 때문에 오른값 참조로 받아주고 데이터를 수정하면 원본 데이터가 수정된다.

 

 

그렇다면 이 오른값 참조를 굳이 사용하는 이유는 뭘까?

가장 큰 이유는 사용자에게 전달하는 의미가 일반 참조 타입과 다르기 때문이다.

 

이게 무슨 말이냐 하면

일반 참조 타입의 경우 원본 데이터의 주소를 넘겨주고 해당 원본 데이터를 마음대로 수정하고 사용할 수 있을뿐더러

원본 데이터를 계속 사용한다는 암묵적인 힌트를 주는 것이다.

 

하지만 오른값 참조의 경우 원본 데이터의 주소를 넘겨주고 원본 데이터를 마음대로 수정하고 사용할 수 있는 것은

동일하나 해당 원본 데이터를 사용하고 난 후에는 더 이상 사용하지 않겠다는 암묵적인 힌트를 사용자에게 주는 것이다.

 

원본 데이터를 이후에 사용하지 않겠다는 것은 해당 원본 데이터를 받아들인 함수 등에서, 즉 외부에서 마음대로

원본 데이터를 수정하고 사용할 수 있다는 것이기 때문에 여러 이점들이 생긴다.

 

원본 데이터에 피해를 주지 않기 위해 깊은 복사를 통해 원본 데이터를 안전하게 두고 깊은 복사를 진행한 데이터를

수정하는 방식으로 사용했다면 오른값 참조 같은 경우 원본 데이터를 마음대로 이용해도 되기 때문에 깊은 복사가

아닌 얕은 복사를 진행해서 원본 데이터를 수정하면 된다. 왜냐하면 해당 원본 데이터는 이후에는 사용하지 않을 것

이기 때문이다.

 

 

이러한 오른값 참조는 다음과 같이 std::move를 통해 간단하게 사용할 수 있다.

4-1. std::move()

std::move를 사용하게 되면 44번 라인의 코드와 같이 동작하게 된다.

즉 왼값인 hoon1을 오른값 참조로 캐스팅하는 것이다. 오른값 참조는 위에서도 얘기했듯이 원본 데이터를

마음대로 사용해도 되고 이후에는 원본 데이터를 사용하지 않을 것이기 때문에 원본 데이터를 복사했다기보다

이동했다는 개념이라고 생각하면 된다. 그래서 이름이 move이다.

 

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 람다 표현식  (0) 2022.07.19
[Modern C++] 전달 참조  (0) 2022.07.18
[Modern C++] override, final  (0) 2022.07.15
[Modern C++] nullptr  (0) 2022.07.15
[Modern C++] 중괄호 초기화 { }  (0) 2022.07.14

 

[Modern C++] override, final

 

이번에는 오버라이드와 파이널에 대해 알아보자. 

중요한 개념이니 잘 숙지해두자.

 

override는 재정의라는 뜻으로 이미 존재하는 함수를 다르게 동작시키기 위해 재정의하는 것을 말한다.

이와 비슷한 개념으로 오버 로딩이 존재하는데 이미 공부했듯이 함수의 이름을 재사용하는 것을 말한다.

(즉 같은 함수 이름을 사용하지만 함수의 형태가 달라서 공존할 수 있다는 것이다.)

 

다시 본론으로 돌아와서 재정의 실습을 진행해보겠다.

 

 

1-1. 실습

두 개의 클래스를 정의하고 한쪽에서 다른 한쪽의 클래스를 상속받는 형태로 만들었다.

그리고 간단하게 문자열을 출력하는 함수를 만들었으며 자식 클래스에서 해당 함수를 재정의하는 형태이다.

메인 함수에서 포인터 클래스 타입으로 지정하고 동적할당을 이용해서 메모리 할당을 진행했으며 자식 클래스의

함수를 호출하는 모습이다. 당연하게도 출력은 자식 클래스의 함수가 호출된다.

 

하지만 여러 프로그래밍을 하다보면 한 개의 부모 클래스의 여러 자식 클래스가 존재하는 경우가 많다. 

그래서 부모 클래스의 포인터 타입으로 지정해서 코딩을 좀 더 효율적으로 하고 싶은 상황이 발생한다. 

그래서 다음과 같은 문제가 발생할 수 있다.

 

 

1-2. 실습

 효율적인 코딩을 위해 부모 클래스 포인터 타입으로 선언을 해주었지만 호출되는 함수는 재정의한 자식 클래스

함수가 아닌 부모 클래스의 함수가 호출되는 것을 알 수 있다. 이 문제를 해결하는 방법은 이전에도 공부한 적이 있는

가상 함수를 이용하는 방법이 있다.

 

 

1-3. 실습

15번 라인을 보면 부모 클래스의 함수를 가상 함수로 선언해주었다.

이렇게 가상 함수를 이용하게 되면 메인 함수에서 어떤 포인터 클래스 타입으로 선언하든 동적 할당된 원본의

함수가 호출된다. (이와 비슷한 느낌으로 최상위 부모 클래스 혹은 부모 클래스의 소멸자에는 virtual을 붙이는 것이

필수적이라는 걸 잊지말자!)

 

 

위의 코드에 아쉬운 점이 있다면 가독성적인 측면이 떨어진다는 점이다.

어느 부분에서 가독성이 떨어진다는 것일까?

 

1-3 이미지와 같은 코드에서 다른 사용자가 부모 클래스를 보지 않은 상황에서 자식 클래스만 확인했다고 가정해보자

그러면 Hellow라는 함수가 해당 자식 클래스에서 처음 시작된 함수인지 부모 클래스에도 같은 함수가 존재하는지 

알 수 있는 방법이 없다.(직접 부모 클래스의 내부를 확인하지 않는 이상) 이러한 부분에서 가독성이 떨어지며 또한

해당 함수가 어디에서 처음 시작됐는지에 대한 부분은 상당히 중요하다.

 

그러면 어느 부분부터 처음으로 함수가 시작됐는지가 왜 중요할까?

 

 

2-1. 실습
2-2. 실습

1-3 이미지의 코드에서 새로운 최상위 부모 클랙스가 추가되었다. 

Mother 클래스 또한 최상위 God 클래스를 상속받는다. 그리고 God 클래스의 함수는 가상 함수로

선언되어 있지 않다.

 

이러한 상태에서 메인 함수를 보면 최상위 클래스의 포인터 타입으로 동적 할당을 진행했다.

예전처럼 코드를 실행했지만 원본의 함수가 호출되는 것이 아닌 최상위 클래스의 함수가 호출되었다. 

최상위 클래스의 함수가 가상 함수가 아니기 때문에 이런 일이 발생하였으며, 중간의 Mother 클래스부터

가상 함수가 적용되었기 때문에 함수가 처음에 어디에서 시작되는지 아는 것이 상당히 중요하다.

 

이 문제를 해결하기 위해선 자식 클래스에서 처음 함수가 시작되는 것인지 부모 클래스에서 받아와서

사용하는 것인지 정보를 알려줄 필요성이 있다. 그래서 사용하는 것이 override이다. 이미지를 통해 알아보자.

 

 

3-1. 실습

36번 라인을 보면 함수 맨 끝에 override가 붙은 걸 확인할 수 있다. 

override가 붙게 되면 자식 클래스에서 처음 선언한 함수가 아니라 부모 클래스에서 해당 함수를

상속받아서 재정의해서 사용하고 있다는 뜻이 된다.

 

이렇게 되면 자식 클래스에서만 함수를 봐도 해당 함수가 자식 클래스에서 처음 선언한 함수가 아닌

부모 클래스에서 상속받은 함수라는 정보가 추가되는 것이다. 가독성이 좋아지며 코드가 오동작하는 것도

방지할 수 있는 안전장치인셈이다.

 

자식 클래스에서 override를 확인하고 부모 클래스로 이동했더니 똑같이 override가 있을 수 있다. 그렇다는 소리는

해당 부모 클래스 또한 상위 부모 클래스에게 함수를 상속받아서 재정의하고 있다는 뜻이다. 

 


 

이제 간단하게 final에 대해 알아보자.

final은 override와 동일하게 함수 뒤쪽에 붙여주면 되고, final을 붙이게 되면

해당 함수가 override 하고 있다는 힌트와 더불어서 해당 함수에서 마지막으로 사용할 것이고 

자식 클래스들에게 더 이상 재정의를 하게 만들어주지 않겠다는 의미이다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 전달 참조  (0) 2022.07.18
[Modern C++] 오른값(rvalue) 참조  (0) 2022.07.16
[Modern C++] nullptr  (0) 2022.07.15
[Modern C++] 중괄호 초기화 { }  (0) 2022.07.14
[Modern C++] auto  (0) 2022.07.14

 

[Modern C++] nullptr

 

nullptr은 누구나 사용하고 있을 정도로 널리 퍼져있고 유명하다.

Modern C++에서 nullptr이 등장하기 전에는 0 혹은 null이 사용되었는데 오늘은 예전에 쓰이던 방식과

nullptr의 차이를 알아보도록 하자.

 

 

1-1. 예전 방식

우선 예전 방식의 경우 이미지 1-1과 같은 문제가 있다.

0 or NULL값을 넣어주어도 결국엔 정수를 넣어주는 것이나 다름없기 때문에 포인터 형식의 함수가

호출되는 것이 아닌 int형 함수가 호출되는 것을 확인할 수 있다. 이러한 문제 때문에 사용자가 포인터 형식의

함수를 호출하고 싶은데 전혀 다른 정수형 함수가 호출되어서 문제가 될 수도 있다.

 

 

1-2. nullptr 사용

이번에는 nullptr을 넣어주었다. 결과는 예상대로 포인터를 받아주는 형식의 함수가 호출되는 것을

확인할 수 있다. 1-1 이미지처럼 nullptr를 사용하지 않으면 의도치 않게 잘못된 함수를 호출하거나 오동작이

일어날 수 있으므로 nullptr 사용은 필수라고 생각하면 된다.

 

 

nullptr은 가독성적인 측면에서도 도움이 된다. nullptr을 사용했다는 것은 포인터를 사용하고 있다는 것을

간접적으로 알려주는 것이나 다름 없기 때문에 사용하는 것이 여러모로 이득이 많다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] 오른값(rvalue) 참조  (0) 2022.07.16
[Modern C++] override, final  (0) 2022.07.15
[Modern C++] 중괄호 초기화 { }  (0) 2022.07.14
[Modern C++] auto  (0) 2022.07.14
algorithm  (0) 2022.07.13

 

[Modern C++] 중괄호 초기화 { }

 

기본적으로 중괄호 초기화는 그리 어렵지 않다.

기존에 있던 문법에서 하나가 추가되었을 뿐이다.

 

1-1. 예시

1-1 이미지와 같이 C++11에서는 중괄호로 초기화하는 것이 추가되었다.

기존에는 vector 같은 컨테이너를 초기화할 때 중괄호를 사용할 수 없었지만 C++11에서 추가된

중괄호 초기화 덕분에 vector 또한 배열처럼 중괄호를 이용해서 한 번에 초기화가 가능해졌다.

 

 

2-1. vector 중괄호 초기화 사용

보는 바와 같이 기존에는 push_back이나 일반 괄호를 통해 초기화를 해주었지만 

중괄호 초기화 기능을 통해 일반 배열처럼 초기화가 가능해졌다.

 

 

중괄호 초기화 장점 정리

1. vector 등 컨테이너와 잘 어울린다.

2. 축소 변환을 방지한다.

    // 데이터 타입이 변환되는 과정에서 데이터 손실 등 무의식적으로 실행되던 코드들을 방지해준다.

 

 

물론 이러한 중괄호 초기화도 장점만 있는 것은 아니다. 단점 또한 존재하는데 조금 

문법적으로 알아야 할 것이 있다.

 

클래스나 컨테이너 등에서 중괄호 초기화를 사용해서 초기화를 해주면

initializer_list 타입의 생성자가 호출된다. 문제는 initializer_list 타입의 생성자의 우선순위가 매우 높아서

발생하는데 좀 더 쉽게 예시를 들어보겠다.

 

 

3-1. 단점 예시

우선 3-1 이미지와 같이 a 클래스를 명시적으로 만들어주었다.

그리고 기본 생성자와 정수 두 개를 받아주는 생성자, 마지막으로 initializer_list 타입의 생성자를 만들었다.

 

 

3-2. 단점 예시

그리고 메인 함수내에서 중괄호 초기화를 이용해 정수 데이터 두 개를 초기화하는 코드를 넣어주었다.

 

문제는 지금부터이다. 

사용자는 정수 두 개를 받아주는 생성자를 호출하기 위해 중괄호 초기화 기능을 이용하여 정수 두 개를

넣어주었지만 실행 되는 생성자는 initializer타입의 생성자가 호출된다. 그 이유는 위에서도 설명했듯이 

initializer의 우선순위가 매우 높기 때문에 다른 생성자를 무시하고 initializer 생성자가 호출되는 것이다.

 

이러한 문제들 때문에 중괄호 초기화 또한 만능이 아니며 무조건적으로 중괄호 초기화만을 사용하는 것도

옳은 방법이 아니다. 기존에 있던 문법들을 무시하며 의도치 않게 다르게 동작할 수도 있으니 사용 시 

주의가 필요하다.

 

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] override, final  (0) 2022.07.15
[Modern C++] nullptr  (0) 2022.07.15
[Modern C++] auto  (0) 2022.07.14
algorithm  (0) 2022.07.13
STL - deque  (0) 2022.07.11

 

[Modern C++] auto

 

modern C++은 C++11 버전 이후부터를 modern C++이라고 한다.

C++11 이전과 이후는 엄청난 차이가 있기 때문에 C++11 문법을 잘 숙지하고 있어야 한다.

오늘은 그중에서 가장 자주 활용되고 중요한 auto에 대해 알아보자.

 

auto는 지금까지 몇 번 사용했듯이 컴파일러가 인자 타입을 추론해서 자동으로 넣어주는 기능을 지원한다.

추론의 기준은 넣어주는 데이터 타입에 따라서 하게 된다.

 

auto a = 3;

auto b = 3.14f;

auto c = "Hellow";

 

물론 추론 규칙이 복잡해지면 형식 타입을 추론하지 못해 에러가 발생하기도 한다.

auto* test1 = a; // a는 일반 정수 타입인데 auto 옆에 포인터 때문에 형식을 추론할 수 없다.

그렇기 때문에 auto만 붙여준다고 항상 100퍼센트 타입을 추론해주는 것이 아니다.

 

그리고  auto에서 가장 큰 특징이자 주의할 점이 있는데

기본 auto는 const와 &(참조)를 무시한다는 점이다.

 

예를 들면

1-1. test

7번 라인 같은 경우 데이터가 정수이기 때문에 자동으로 int형으로 지정될 것이며 

8번과 9번 라인에 각각 & 참조와 const int형을 선언해주었다.

 

그리고 11번과 12번 라인에서 auto를 이용해서 & 참조와 const int형 데이터를 받아주도록 했다.

결과적으로 test1과 test2에는 참조와 const int형이 지정되어야 하지만 일반 int형이 지정되었다.

 

이렇듯 auto는 const와 & 참조를 무시한다. 이 부분을 꼭 기억하자.

 

 

2-1. 해결법

const와 참조를 무시하는 특성을 해결하는 방법이다.

사용자가 명시적으로 참조와 const가 있어야 한다는 것을 알려줘서 해결할 수 있다.

 

 

일반적으로 auto를 자주 사용할 텐데

타이핑이 길어지는 타입 형태의 경우 auto를 사용하는 것이 추천되고 그 외에는 

일반 타입을 직접 타이핑하는 것이 가독성 측면에서 유리하다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

[Modern C++] nullptr  (0) 2022.07.15
[Modern C++] 중괄호 초기화 { }  (0) 2022.07.14
algorithm  (0) 2022.07.13
STL - deque  (0) 2022.07.11
STL - list  (0) 2022.07.08

+ Recent posts