1.Auto 키워드
Auto 키워드는 네 가지 전혀 다른 의미를 가지고있다.
1. 어떤 변수의 타입을 컴파일 타입에 자동적으로 연역해내는 것이다.
- EX) auto x = 123; // x는 int 타입이 된다.
위 코드는 int 대신 auto를 사용하였고, 특수한 효용은 없다. 하지만 복잡한 타입의 경우에는 효용이 생기는데
예를들어 함수의 리턴 타입이 매우 복잡하다고 하면, 이때 리턴값을 변수에 대입하려면 타입을 일일일 타이핑 해야
한다 하지만, Auto 키워드를 사용함으로써 컴파일러에게 타입 결정을 맡길수있다. auto result = 함수이름()
2. 새로운 함수 정의 문법
3. 자동 함수 리턴 타입
- 리턴 타입을 Auto로 해두면 return문의 표현식이 어떤 타입을 가지냐에 따라 리턴 타입이 자동으로 결정된다. 함 수 안 return 문이 여럿 존재할 경우 모든 return 문의 표현식이 항상 같은 타입으로 결정될 수 있어야 한다. Auto 리 턴 타입은 재귀함수에도 사용할 수 있으나 첫번째 return문은 반드시 재귀 호출이 아닌 타입이 결정될 수 있는 일반
표현식이어야 한다.
2. Decltype 키워드
decltype 키워드는 표현식을 인자로 받아서 그 표현식의 결과 타입이 무엇인지 알아낸다.
int x = 123;
decltype(x) y = 456;
컴파일러는 x가 int 타입이라는 사실로부터 y의 타입을 int로 연역하고, 새로운 함수 문법에서의 auto 키워드와 마찬가지 로 decltype도 처음 볼 때는 어떤 효용이 있는지 잘 보이지 않는다. 하지만 템플릿 코드를 작성할 때는 auto와 decltype 키워드가 큰 역할을 한다.
3. 포인터와 동적 메모리
동적 메모리는 컴파일 타임에 크기를 정할 수 없는 데이터를 이용할 수 있게 해준다.
1. 스택과 힙
C++에서 사용하는 메모리는 스택과 힙 두가지로 나누어진다. 스택 메모리는 쌓여있는 카드로 비유하며, 가장 위에 있 는 카드는 프로그램의 상태를 나타내는데 현재 실행중인 함수와 관련되어져 있다. 즉 현재 실행 중인 함수의 로컬 변 수는 모두 가장 위에 놓인 카드로 제일 위 스택에 위치한다. 현재 실행중인 함수 a() 가 또 다른 함수 b()를 호출하면, 새로운 스택 프레임이 생성되고, b() 함수를 호출할 때 넘겨진 파라미터가 a()의 스택 프레임에서 복제되어, b()의 스택
프레임에 옮겨진다.
main() 함수가 두 개의 int 타입 파라미터를 인자로 갖는 a()함수호출 상황이다.
스택 프레임은 각 함수 간 메모리 공간을 격리시키는 중요한 역할을 한다. 예를 들어 a() 함수의 로컬 변수는 b() 함수가 호출되는 동안 바뀌지 않는다.(단, 명시적으로는 b() 함수에 해단 변수의 메모리 위치를 넘겨주는 것은 예외)
b() 함수의 실행이 끝나고 함수가 리턴되면 b()함수의 로컬 변수들은 스택프레임과 함께 사라지기 때문에
더이상 메모리를 차지하지 않는다. 즉 스택에 할당된 메모리를 프로그래머가 직접 해제할 필요가 없이 자동으로 해제
힙 메모리는 현재 실행 중인 함수나 스택프레임과는 완전히 독립적인 메모리 영역이다. 함수의 호출과 리턴에 관계없이
항상 존재해야 하는 변수라면 힙에 위치시키면 된다. 힙 메모리는 창고에 비유할 수 있다. 언제든 필요하다면 창고에 보 보관하거나 보관되어져 있는 수를 정할 수 있다. 힙에 할당된 메모리는 반드시 프로그래머가 직접 해제해야 한다. 힙 메
모리에 대한 해제는 자동으로 일어나지 않는다. 단 스마트 포인터를 사용할 경우에는 더 이상 사용하는 곳이 없을 경우
자동으로 해제되도록 할 수 있다.
4. 포인터
힙에 명시적으로 메모리를 할당하며 무엇이든 저장할 수 있다. 힙에 정수값을 저장하고 싶다면 메모리를 할당해야 한다.
이를 위해서는 포인터 변수를 선언해야 한다. Ex() - int* A;
int 타입 키워드 뒤에 있는 * 기호는 변수가 정수값이 저장된 메모리 영역을 가리키기 위한 변수이다.
포인터를 동적으로 할당된 힙 메모리를 가리키는 화살표로 생각해도 된다.
예시와 같이 선언된 상태에서는 아직 아무것도 가리키는 것이 없다. 즉 초기화되지 않은 변수이다.
초기화되지 않은 변수는 최대한 빨리 값을 지정해주는 것이 바람직하다. 특히 초기화되지 않은 포인터 변수라면 임의 메모리 영역을 가리키고 있을 것이기 때문에 더욱 위험하다. 초기화되지 않은 포인터 변수를 이용하다 보면 프로그램이
크래시될 가능성이 높다. 이 때문에 포인터 변수는 선언하자마자 초기화해야 한다.
int* A = nullptr; 이런식으로 초기회할 수 있다.
메모리를 할당할 경우에는 new 연산자를 사용한다 A = new int;
여기는 포인터는 정수 변수 단 하나가 위치한 주소를 가리킨다. 변수값에 접근하기 위해서는 포인터를 역참조 해야한다
역참조는 힙에 위치한 값을 가리키는것이다. 힙 메모리에 새로운 값을 집어넣을 때는 *A = 8; 과 같이 한다.
이때 A에 값 8을 대입하는 것이 아니라 포인터가 가리키는 메모리의 값이 바뀌는 것이다. 포인터 자체에 값을 대입하면
주솟값 8에 위치한 임의의 값을 가리키게 되어 프로그램이 오작동할 수 있다.
동적으로 할당된 메모리의 사용이 끝났다면 delete 연산자를 이용하여 할당을 해제 해야한다.
할당 해제된 메모리에 접근하는 것을 막기 위해 해제한 후 nullptr로 초기화를 해준다.
delete A;
A = nullptr;
포인터가 항상 힙 메모리만 가리키는 것은 아니다. 스택을 가리킬수도 있고 심지어 또 다른 포인터를 가리킬 수도 있다.
변수로부터 포인터를 얻을 떄는 주소 참조 연산자 &를 이용한다.
int i = 8;
int* A = &i
포인터로 구조체를 참조할 경우에는 특별한 문법을 사용한다. 값에 접근하기 위해 추가적으로 역참조 연산자*을 포인터에
붙이고, 이렇게 역참조된 변수에 . 을 붙여서 구조체 내 각 필드를 선택하다.
A * anA = B();
cout << (*anA).c << endl; 구조체 포인터를 리턴하는 가상의 함수 B()를 호출하여 리턴받은
구조체 A의 필드 c의 값을 출력
하지만 위의 방식은 조금 성가시다. 그렇기에 -> 구조체 역참조 연산자를 이용하면 참조와 필드 접근을 한 번에 할 수있다
A* anA = B();
cout << anA -> c <<endl;
보통 함수를 호출하면서 파라미터 전달을 위해 변수를 인자로 이용할 때 해당 변수의 주소가 아니라. 그 값을 넘기게 된다.
이러한 방법을 값에 의한 전달(Passing by value)이라고 한다, 예를 들어 어떤 함수가 정수값을 파라미터로 받고, 함수 호출
시 정수 변수를 인자로 넘겼다면 해당 변수의 값을 복사하여 함수 스택 프레임 안의 파라미터 영역에 위치시킨다. 그런데 변수 대신 변수의 주소를 가리키는 포인터를 인자로 전달할 수도 있다. C에서는 함수가 변수값을 바꿀 수 있도록 하기 위해
그런 방법을 사용한다. 포인터를 인자로 전달하면 스택 프레임 안에 복사되어 저장되는 것이 변수의 값이 아닌 변수의 주소가 되기 때문에 그 주소를 역참조함으로써 스택 프레임 밖에 있는 변수의 값을 변경하는 것이 가능해진다. 이러한 파라미터 전달 방법을 참조에 의한 전달(Passing by reference)라고 한다. C++ 에서는 참조형 변수라는 더 좋은 방식을 지원하기에 포인터를 통한 우회적인 방법을 사용하지않아도 된다.
5. 동적 할당 배열
힙은 동적으로 배열을 할당하는 데 쓰일 수도 있다. new[] 연산자를 이용하여 아래와 같이 배열을 위한 메모리 공간을
할당할 수 있다.
위 명령은 int 변수를 arraySize개만큼 저장할 수 있는 힙메모리를 확보하여 그 주솟값을 포인터 변수에 대입해준다.
이 코드가 실행된 이후 스택 메모리와 힙 메모리 상태가 어떻게 될지 보여준다.
위의 사진을 보면 포인터 변수 자체는 스택에 위치하고 있지만 동적할당되는 배열 데이터는 힙에 위치해있다.
이제 메모리가 할당되었기 때문에 다음과 같이 스택 배열인 것처럼 이용할 수 있다.
동적할당된 배열을 더 사용하지 않으면 명시적으로 힙에서 해제해야 한다.
메모리 릭을 막기 위해 new와 delete는 각각 짝을 맞춰 이용되어야 하며, 배열을 대상으로 new를 했다면 반드시 배열에 대한 delete를 수행하야 막을 수 있다.
여기서 메모리 릭이란 개발자가 의도하지 않은 메모리를 점유하고 있는 현상으로 서비스 장애를 유발한다.
6. null 포인터 상수
func(NULL) 인자로 NULL을 넘기게 될 경우 포인터가 아니라 정수 0과 같기 때문에 컴파일러는 func(int i)를 호출한다.
그렇기에 nullptr이라는 새로운 살수를 이용하면 간결하게 대응할수있다.
func(nullptr);
7. 스마트 포인터(이중 포인터)
여러가지의 메모리 문제를 피하기 위해서는 기존에 쓰는 일반 포인터가 아닌 스마트 포인터를 사용해야 한다.
스마트 포인터는 객체에 유효한 스코프가 더 이상 없을 때 자동으로 메모리를 해제한다.
C++ 스마트 포인터에는 새 종류가 있다. std::unique_ptr, std::shred_ptr, std::weak_ptr로 모두 <memory> 헤더 파일에 정의되어 있다. unique_ptr은 보통의 포인터와 비슷하나, unique_ptr 변수가 스코프를 벗어날 떄 자동으로, 또는 명시적으로
delete가 수행될때 메모리가 해제하는 것이 다르다. unique_ptr은 포인팅하고 있는 객체에 대해 단독으로 오너십을 가진다.
unique_ptr을 이용하면 예외 상황이 발생했을 때 메모리 해제를 단순하게 할 수 있다. unique_ptr에는 객체뿐만 아니라 C 스타일 배열을 넣을 수도 있다. unique_ptr을 만들 때는 std::make_unique<>()를 이용한다.
이런식으로 사용 가능하다.
변수 anEmpoyee는 스마트 포인터가 되고, 사용법은 일반포인터와 같다.
unique_ptr은 범용 스마트 포인터로, 어떤 종류의 메모리든 참조할 수있다.
템플릿으로 만들어진 이유인데, 템플릿 <> 로 템틀릿 파라미터를 받는다. <>사이에 unique_ptr이 참조할 메모리 타입을 지정한다,
shared_ptr은 데이터에 대한 오너십이 분산될 수 있게 해준다. 변수가 다른 변수에 대입될 때마다 레퍼런스 카운트가 증가되어 데이터의 오너가 하나 더 늘었다는 것을 표시하고 shared의 변수가 모든 스코프를 벗어날 경우에는 레퍼런스 카운트가 0이 되며 이는 오너가 더 이상 없다는 뜻으로 포인터에 의해 참조되고 있는 객체의 메모리가 해제된다.
unique와 달리 shared는 배열을 지원하지 않는다, shared는 생성할 때 std::make_shared<>()를 이용한다.
weak_ptr은 shared_ptr에 대입된 객체를 참조하되 레퍼런스 카운트에 영향을 주고 싶지 않을 경우에 사용한다.
'C++ Practice' 카테고리의 다른 글
C++ 부동 소수점 (0) | 2024.03.15 |
---|---|
전문가 C++공부 (0) | 2024.03.01 |
전문가 C++ 공부 (0) | 2024.02.22 |
01. C++ 공부 (0) | 2024.01.12 |
비트 연산 활용 (0) | 2023.12.19 |