깨달은 게 있다.
뭔가 뇌가 꼬이는 거 같고, 이해가 너무 안 될 때는 일단 생각하기를 멈추고 바람좀 쐬고 와야한다.
그렇게 이해가 안 가서 징징거리던 ch6 Out of class definition과 Operator Overload 부분이,, 나중에 보니까 그래도 좀 이해가 슬슬 되기 시작했다.
그리고 세상에 감사하게도 Chat GPT를 쓰니까 넘 든든했다.
어디 뭐 물어볼 곳도 딱히 없었는데 웬만한 교수님급 지식을 갖고 있는 녀석이 함께해주니 이렇게 좋을 수 없다^^
각설하고, 공부는 지속된다..
Overview --> Special members ( Copy and move semantics & Static members ) , Debugging
Copy Semantics
#include <iostream>
using namespace std;
class SimpleVector{
public:
int* array; //points to elements
int size; // # of elements
int capacity; // capa of array
SimpleVector(int initialCapacity):
size(0),capacity(initialCapacity){
array = new int[capacity]; // array dynamic 할당
}
~SimpleVector(){
delete[] array;
}
void addElement(int element){
if(size == capacity)
resize();
array[size] = element;
size++;
}
void resize(){
capacity = capacity * 2;
int* newArray = new int[capacity];
for(int i=0;i<size;i++)
newArray[i] = array[i];
delete[] array;
array = newArray;
}
int getSize(){
return size;
}
};
- 처음에 vec1에 new로 dynamic memory를 할당한 뒤 size 2, capa2의 0x6080 주소를 갖는 array를 만들고 {1,2}로 초기화 한다.
- 그리고 vec2에 vec1를 덮어 씌운다.
(위 코드참고해서 함수동작 이해필요)
- vec1에 addElement(3)을 하면서 size 늘리고(size == capa라 resize필요) capa는 2배로는다. 이 과정에서 기존에 있던 memory를 delete하고 capa가 커진 memory로 이사를 간다.
- 그리고 vec2에 addElement(4)를 할 때 error 발생.. 역시 resize를 하려고 하니, 이미 해당 memory는 free가 되었는데 왜 또 delete를 하려고 하냐는 Error가 뜬다. ----> Deep Copy가 必
기존의 Copy case
- 이렇게 일시적인 copy는 error를 초래하기 때문에 non temporary object를 통해 deep copy를 진행하는 것이 필요하다.
Copy Construct
template <typename T>
SimpleVector<T>::SimleVector(const SimpleVector<T>& other):
size(0),capacity(other.capacity),array(new T[other.capacity()]){
//copy the elements
for(int i=0;i<other.size;++i){
addElement(other.array[i]);
}
}
- const reference를 받는 constructor를 만들어 준다. for문에서 element copy가 발생함.
- 새롭게 memory공간할당을 하고, copy하려는 놈을 one by one으로 옮겨준다.
Copy Assignment Operator
- 이미 initialize 되어 있는 object를 다른 non-temporary object로 DEEP COPY 하는 operator를 생각해보자
template <typename T>
SimpleVector<T>& SimpleVector<T>::operator=(const SimpleVector<T>& other){
if(this != &other) //this는 현재 array의 주소(T* array)이다.
{delete[] array;
capacity = other.capacity;
array = new T[capacity];
size = 0;
for(int i=0;i<other.size;i++){
addElement(other.array[i]);
}
}
return *this;
}
- 기본적으로 지금 하고자 하는 건, 현재 SimpleVector의 객체에 other라는 SimpeVector<T>&의 내용을 copy하려는 것이다.
- 빼껴먹으려는 other의 내용이 변하면 안 되기에 input단에 const를 넣는다. 그리고, 누누히 다뤘듯이, other라는 객체의 메모리 복제없이 내용만을 참조하기 위해서 <T>& 꼴로 Reference를 부른 것이다.
- this는 무엇이냐? this는 이제 copy할 내용이 들어갈 새로운 SimpleVector class의 객체를 포인팅하는 variable이다.
- 엥? this가 뭔데 pointer냐
this 포인터가 SimpleVector 인스턴스의 포인터라는 것은 C++ 언어의 규칙과 클래스 멤버 함수의 작동 방식에 기반합니다. 클래스의 멤버 함수 내에서 this는 자동으로 현재 인스턴스(즉, 메서드가 호출된 객체)를 가리키는 포인터로 제공됩니다. 이는 C++ 언어의 일부이며, 컴파일러에 의해 자동으로 처리됩니다. SimpleVector<T>::operator=와 같은 멤버 함수에서 this는 다음과 같은 특징을 가집니다:
|
- 답변은 위와 같다고 한다. by GPT. C++의 약속이라고 한다. Class의 Member Method 안에서 this는 자동으로 현재 Class의 instance를 가르키는 포인터로 제공된다고 한다. 이 때문에 this가 처음보는 녀석이라고 생각이 들어도 Class 내부에서 정의했던 array, size, capacity 요런 것에 접근이 가능한 것이다.
- 참고로 우리가 지금 vec1 = vec2 를 써서 copy를 진행하고 싶다면 여기서 this는 vec1을 가르키는 pointer이고, other는 vec2를 의미한다.(정확히 말하면 vec2를 Reference/참조 한 것)
for(int i=0;i<other.size;i++){
addElement(other.array[i]);
}
}
- 이건 for문에 대한 개인적인 궁금증인데, 왜 other[i]를 쓰면 안 되는 지 궁금했다.
A1 . other.array[i]를 other[i]로 바꾸는 것이 가능한지 여부는 SimpleVector<T> 클래스의 구현에 달려 있습니다. 만약 SimpleVector<T> 클래스에 operator[]가 정의되어 있어서 T& operator[](int index) 형태로 외부에서 접근 가능하다면, other[i]로 사용하는 것이 가능합니다. +Q2 : array의 경우 class public에 T* array;인 포인터로 정의 되어 있는데 그냥 other.array[i]로 other에 있는 array값에 접근이 가능해? dereference해줘야 하는 거 아니야? A2 : other.array[i]를 사용하는 경우, array는 T* 타입의 포인터로, T 타입의 동적 배열을 가리킵니다. other.array[i] 표현식에서, other.array는 배열을 가리키는 포인터이며, other.array[i]는 이 배열의 i번째 요소에 접근하는 것입니다. 여기서 i번째 요소에 접근하는 것은 배열의 포인터에 대한 인덱싱(indexing) 연산을 수행하는 것이며, 이는 자동으로 해당 요소를 참조합니다 (즉, 해당 요소를 디레퍼런싱(dereferencing)하는 것과 동일합니다). 따라서, other.array[i]는 *(other.array + i)와 동일한 동작을 합니다. 여기서 other.array + i는 배열의 시작부터 i 요소만큼 떨어진 위치를 가리키는 포인터이며, * 연산자는 이 포인터가 가리키는 위치의 값을 가져옵니다. 하지만 C++에서 배열을 사용할 때는 일반적으로 array[i] 표기법을 사용하여 간단하고 읽기 쉽게 만듭니다. 이는 내부적으로 포인터 연산과 디레퍼런싱을 처리하지만, 개발자가 명시적으로 이러한 세부 사항을 작성할 필요는 없습니다. 만약 array가 private 또는 protected 멤버라면, 클래스 외부에서 직접 접근할 수 없습니다. 이 경우, 클래스 내에서 public으로 정의된 메서드나 friend 함수를 통해서만 접근이 가능합니다. 하지만 public으로 선언된 경우에는 클래스 외부에서도 직접 접근할 수 있습니다. |
- Pointer에 대한 기본적인 이해가 부족했다. int * ptr = new int[3] { 1, 2 , 3} 이렇게 array를 가르키고 있는 포인터 ptr이 있다고 하면, ptr[1] -> 2 가 출력되고 이는 *(ptr+1) 과 같다는 것이다. array를 포인팅하고 있을 때 대괄호를 쓰는 것 자체가 이미 dereference를 하겠다는 뜻인 것!
- Operator의 input과 output type에 관련한 추가 궁금사항
SimpleVector<T> SimpleVector<T>::operator=에 return this;를 사용하는 것은 문법적으로 올바르지 않으며, 메서드 체이닝을 사용할 수 없습니다. 여기에는 몇 가지 중요한 이유가 있습니다:
|
- Output은 Reference을 반환해야 그거 가지고 Method Chaining을 할 수 있고, this는 기본적으로 포인터이다.
- int *ptr = &a 처럼 정의하듯이, 기본적으로 참조(Reference) 타입과 *this를 같이 쓰는 것이 맞다.
- 흠.. this는 SimpleVector<T> vec = {1,2,3} 객체를 가르키는 포인터이면 *this는 1을 가르키게 된다. 근데 그 1을 그 값 자체로 반환하려면 SimpleVector<T> SimpleVector<T>::operator~ 이렇게 해도 되는데, 그렇게 하면 그 1을 받는 객체를 추가로 하나 더 복제해서 Method의 Output으로 내는 것이다.
- 그러면 Memory 소요가 되니까, 그 1값의 reference를 반환해준다는 것 --> SimpleVector<T>& SimpleVector<T>::operator~
- 실컷 객체(this)에 other에 있던 element 다 복사해서 *this를 만들었는데, 그걸 그대로 반환하지 않고 또 Memory를 써서 거기다가 옮겨적은다음 반환하는 꼴
Move Semantics
- 지금껏 살펴본 Deep copy는 사실 활용하기엔 비싼 녀석이다. 데이터를 옮기고 필요 없어진 녀석은 그냥 메모리 삭제해주는 게 괜찮지 않나..? 라는 관점에서 나오게 된 Move Semantics! 즉, transfer 하는 기능이다.
- 처음에 temporary object 하나를 initialize 해준다.
- createVectorMove function은 tempoary object를 받거나[or] local object를 by value로 return 한다... 이게 무슨 소리지???
- 아.. 위 같은 Case에 Move를 사용한다는 뜻
template <typename T>
SimpleVector<T>::SimpleVector(SimpleVector<T>&& other) noexcept
: array(other.array), size(other.size),capacity(other.capacity){
other.array = nullptr;
other.size = 0;
other.capacity = 0;
// Source 초기화 시키는 과정
}
- &&는 rvalue reference를 뜻한다.
<T>&&는 C++에서 r-value reference를 나타냅니다. 이는 C++11 표준에서 도입된 개념으로, 오른쪽 값(r-value)에 대한 참조를 의미합니다. r-value reference는 주로 임시 객체나 이동될 수 있는 객체를 참조하는 데 사용됩니다.R-Value Reference란?
이동 생성자와 R-Value Reference제시된 SimpleVector<T>::SimpleVector(SimpleVector<T>&& other)는 이동 생성자의 예입니다. 이동 생성자는 다른 객체의 리소스를 현재 객체로 '이동'하는 데 사용됩니다. 이동 생성자는 다음과 같은 특징을 가집니다:
|
R-Value Reference (<T>&&)
L-Value Reference (<T>&)
<T>&&와 <T>&의 차이점
|
- <T> && Rvalue -> 객체이동 , <T>& Lvalue -> 객체 참조 --> 이동X 복사O
- noexcept specifier는 말 그대로 constructor가 exception을 던지지 않는다는 것인데.. 걍 Compiler가 필요로하는 기능 정도로 알아두면 될 거 같다.
- Transfer를 통해 기종 source object는 접근가능하게 두되, unspecified state로 둔다.
(딱히 가이드가 있는 건 아니다)
template <typename T> // move assignment operator
SimpleVector<T>& SimpleVector<T>::operator=(SimpleVector<T>&& other) noexcept{
if(this != &other) // this 할당 받을 것, other 내용을 this로 transfer하고 other는 free시킬 것
{ delete array;
array = other.array;
size = other.size; // 사실 이 모든 것은 this->size 요런식인데 this 생략한 것
capacity = other.capacity;
other.array = nullptr;
other.size = 0;
other.capacity = 0;
}
return *this
}
- array/size/capacity 를 other에서 따 오는 것까진 --> Swallow Copy
- 그 이후에 기존 Source(other)를 초기화 해주는 것 --> Deep Copy
Compiler Optimization
- Compiler에 따라 다르지만 Compiler는 보통 Return Value Optimization(RVO)라는 동작을 지원한다.
- Output을 보면 &vec이 function decalre 안과 Main 함수 안에서 불렸을 때 각각 같은 값을 갖는다.
- 이는, Compiler는 어차피 함수가 Main에서 쓰이면 그냥 Memory 주소를 Main에다가 만든다는 뜻이다.
Copy and Move
- Copy Semantics
- Deep copying the resources of one object ( often non-temporary ) to another
SimpleVector<int> vec{1,2,3};
SimpleVector<int> copiedVec = vec;
- Move Semantics
- Transferring the resources of one object ( often temporary ) to another
SimpleVector<int> vec{1,2,3};
vec = SimpleVector<int> {4,5,6};
- 상황에 맞게 적절히 이것들을 사용해서 memory leak 등을 막아보즈아..!
The Five Rules
- Memory를 다룬다면.. 아래 5가지 special member function은 재정의 해줄 것을 권. 고. 함 !
1) Destructor 2) Copy Constructor 3) Copy Assignment Operator
4) Move Constructor 5) Move Assignment Operator
Static Members
- Static Member는 Class에 종속되어 있다. 다른 Instance들한텐 종속 ㄴㄴ
- 그치만 class의 모든 instance들에게 share가 된다.
- Class의 instance를 만들지 않아도 access가 가능하다.
- 그럼 이걸 언제 쓰냐? : Shared data/methods, class-wide information, utility functions.
template <typename T>
class SimpleVector{
// class 구문 안에서 선언
static std::map<T,int> elementcounts;
};
template <typename T>
std::map<T,int> SimpleVector<T>::elementCount = {}; //class 구문 밖에서 초기화
template <typename T>
void SimpleVector<T>::addElement(T element){
if (size == capacity)
resize();
array[size] = element;
size++;
elementCount[element]; // element 하나씩 집어넣을때마다 +1 -> element가 몇 번씩 나왔는지 count 가능
}
- static member에 대한 접근권한은, 이 놈들이 pulic, private 등 어떤 specifier에서 선언되었는 지에 따라 다르다.
- 요렇게만 해줘도 그냥 printElementCount를 call만 하면 vec1, vec2 값들의 구성요소 count가 반환이 되는구나..
//static 사용 예상시나리오(for 위 case)
template <typename T>
class SimpleVector {
private:
// ...
public:
static std::map<T, int> elementCount; // 각 원소의 카운트를 저장하는 static 맵
// ...
static void printElementCount();
};
// elementCount를 정의합니다.
template <typename T>
std::map<T, int> SimpleVector<T>::elementCount = {};
// 생성자에서 원소들을 elementCount에 추가하는 코드를 구현해야 합니다.
template <typename T>
SimpleVector<T>::SimpleVector(std::initializer_list<T> elements) {
for (const T& element : elements) {
++elementCount[element];
}
// ...
}
template <typename T>
void SimpleVector<T>::printElementCount() {
for (const auto& pair : elementCount) {
cout << pair.first << ":" << pair.second << endl;
}
}
SimpleVector<T>::SimpleVector(std::initializer_list<T> elements) 생성자에서 elementCount에 원소들이 어떻게 추가되는지 구체적으로 설명해드리겠습니다. 먼저 elementCount는 SimpleVector<T> 클래스의 static 멤버 변수입니다. 이는 클래스의 모든 인스턴스 간에 공유되며, 특정 타입 T의 각 원소가 SimpleVector 인스턴스들에 몇 번 나타나는지를 카운트합니다. 이 변수는 일반적으로 std::map<T, int> 타입으로 선언됩니다. 여기서 키(T)는 원소의 값, 값(int)은 그 원소의 출현 횟수입니다. elementCount에 원소 추가생성자에서는 std::initializer_list<T> elements를 통해 초기화할 원소들을 받습니다. 이 리스트를 순회하면서 각 원소에 대해 다음과 같이 처리합니다:
예시
이 코드를 실행할 때 생성자는 다음을 수행합니다:
이 경우에는:
|
끄읕,,,!
'SW 만학도 > C++ & Algorithm' 카테고리의 다른 글
9. Inheritance - Advanced and Applications (0) | 2024.04.13 |
---|---|
8. Inheritance - Basics (0) | 2024.04.12 |
6. Out_of_class Definition & Operator Overloading (0) | 2024.04.10 |
5. Classes (0) | 2024.04.09 |
4. Functions and Memory Management (1) | 2024.03.29 |