[42seoul] 4 circle - CPP Module 02

하이초·2022년 10월 19일
0

42seoul

목록 보기
3/11
post-thumbnail

CPP02 github 😋

1. ex00

👀 CPP02 ex00부터는 Orthodox Canonical Form에 따라 객체를 생성해야 한다!! 그게 뭐냐???

  • Default constructor 기본 생성자

  • Copy constructor 복사 생성자

  • Copy assignment operator 복사 대입 연산자

  • Destructor 소멸자

    위 네가지를 기본으로 객체를 만들어야 하는 것이다.

🥨 기본 생성자

  • 입력 인수가 없는 말그대로 기본 생성자를 말한다. 사용자가 생성자를 하나도 만들어놓지 않으면 컴파일러가 자동으로 이 기본 생성자를 만들어준다!

🥨 복사 생성자

  • 이 놈은 정말 처음 만들어봤다. 앞선 과제에서 계속 나왔던 '임시 객체'를 만들 때도 사실은 계속 복사 생성자가 사용되어 왔다. 근데 왜 몰랐냐?
  • 이것도 사용자가 복사 생성자를 따로 지정해놓지 않으면 컴파일러가 알아서 하나를 만들어둔다.
  • 복사 생성자의 기본 형태는 ClassName(const ClassName &obj); 이다.
    - 이때 const를 쓰는 이유는 뭐 당연하고, 참조형으로 인수를 받는 이유는 생성 무한루프를 막기 위해서다. 당연함. 참조형으로 받지 않는 이상임시 객체를 무한으로 만들게 될테니까...
  • 아니 근데 컴파일러가 알아서 잘 만들어주는데 왜 또 내가 만들어야 하냐???
    - 그게 또 포인터 때문이다 🤦🏻‍♀️ 이 놈의 포인터 !!! 이 놈의 메모리!!!!
  • 여기서 얕은 복사깊은 복사의 개념을 알아야 한다

🥨 객체의 얕은 복사, 깊은 복사

  • 얕은 복사: 얕게 복사한다. 그냥 복사 객체에다가 원본 객체 변수 주소를 때려 박는다. 따라서 값을 수정하지 않는 한 일반 상수와 같은 값을 복사하는 데 있어서는 그다지 큰 문제가 되지 않을 수 있다. 포인터에 비해서는.... 근데 문제는 또 그 놈의 포인터다. 얘는 포인터 변수에 대해 공간을 새로 만들지 않고 원본 포인터 변수 주소를 그냥 그대로 넣어논다. 그래서 얕은 복사라 한다.

    SampleClass (const &SampleClass sample) {
    	char_pointer = sample.char_pointer;
       idx = sampe.idx;
    }

    기본으로 주어지는 복사 생성자가 바로 위와 같은 얕은 복사로 이루어져있다. 만약 어떤 객체가 char*와 int를 멤버 변수로 가지고 있다고 해보자. 이때 char 포인터 변수는 복사대상의 주소를 그대로 가지게 된다.

  • 그래서 모가 문제냐고요?
    - 저 복사 생성자가 어떤 함수 내나, 스코프 안 지역변수에서 활용된다고 해보자. 그 범위를 벗어나면 복사 생성자로 생성된 객체는 소멸될 것이다.

    • 이때!!! 문제가 발생한다. 객체가 소멸되며 해당 객체가 가지고 있던 char* 변수도 해제 된다. 근데 아까 모라고 했지요??? 원본 객체와 주소가 같다고 했지요???? 네 당첨입니다. 원본 객체도 주소를 잃었습니다...
  • 깊은 복사: 그래서 포인터 변수가 있다면 깊은 복사를 활용하여 복사 생성자를 필수로 사용자화 해야한다. 깊은 복사는 단순히 주소를 대입하는 것이 아니라 새로운 포인터 변수를 만들어 그 안에 값을 넣어주는 식으로 진행한다. 원본 객체와 값은 같지만 주소는 서로 다른 포인터 변수를 갖게 되기 때문에 문제 해결!!

  • 결국 얕은 복사원본 값이 훼손될 여지가 매우 크다는 점에서 주의가 필요하다. 깊은 복사를 통해 복사 객체를 수정해도 원본 객체에는 영향을 미치지 않도록 해야한다.

🥨 복사 대입 연산자 오버로딩

  • C++에서는 오버로딩과 오버라이딩의 개념이 있는데, 오버로딩은 같은 이름의 함수가 서로 다른 매개변수를 받아 서로 다른 반환값을 가지는 경우를 말하고, 오버라이딩은 함수의 이름, 매개변수, 반환값은 모두 같지만 서로 다른 행동을 하는 것을 뜻한다.
  • 여기서 연산자 오버로딩이란! 연산자를 내 입맛대로 구현할 수 있단 뜻이겠지!
  • ClassName& operator?(const ClassName& obj); 가 기본형이다.
    - 😲 근데 왜 참조형으로 반환할까? a = b = c = d와 같은 연속 대입을 가능하게 하기 위해서라는 데 이 말이 이해가 안돼서 걍 일반형으로 해봤다. 무한루프 참사가 일어났다. 위의 복사생성자와 같은 이유다. 말그대로 '대입' 연산자이다보니 return값이 필요한데, 이때 참조형으로 return하지 않으면 복사생성자의 무한 굴레를 벗어날 수 없게 되는 것이다. wow!
  • 아 단항 연산자 오버로딩은 멤버함수로만 가능하다고 한다. 전역으로 설정 불가능!

2. ex01

👀 ex01은 이제 본격적으로 고정소수점에 대해 알아가는 과제였다. ex00도 고정소수점 형태를 만들기는 하지만, 그건 그냥 과제 말처럼 0.0에 지나지 않으니까!

  • 일단 고정소수점은 ex02에서 나올 부동소수점과 함께 10진수의 실수를 2진수로 변환하여 저장할 때 어떻게 저장할 것인가?에 대한 아이디어이다

🥨 고정소수점

  • 고정소수점은 2진수로 변환하는 수를 지정한 소수점 위치를 기준으로 정수와 소수를 나누어 그대로 박아넣는 것이다

  • 기본적으로 32bit int 자료형에는 위와 같이 357이라는 10진수가 2진수로 변환되어 맨 앞 부호비트 1bit를 포함하여 32bit로 2,147,483,647 ~ -2,147,483,648까지의 정수가 표현될 수 있다.

  • 이 357을 고정소수점으로 표현하면 어떻게 될까?

    - 바로 요렇게 된다. 이때 소수점은 과제 기준으로 8bit를 산정했지만, 소수점은 정의하기 나름이다. 이처럼 고정소수점은 말 그대로 고정되어있는 임의의 소수점 비트를 기준으로 왼쪽은 정수, 오른쪽은 실수를 표현하며 빈값은 다 0으로 채워진다.

  • 임의의 정수를 고정소수점으로 변환할 때는 2가지 방법이 있다.

    1. 비트 쉬프트: num << _bits
    • 정수 num을 소수점 bit만큼 왼쪽으로 옮겨서 저장해놓는다.
    1. 2^_bits 곱하기: num * 2^_bits
    • 소수점 bit만큼 비트를 옮겼다는 건 결국 그 정수가 2^_bits만큼 커졌다는 걸 의미한다. 따라서 그만큼 곱해주면 된다.
      - 과제에서는 소수점 bit로 8이 주어졌으니 8비트만큼 왼쪽으로 밀거나, 2^8 = 256만큼 곱해서 저장하면 된다
  • 그렇다면 실수는 어떨까? 실수의 소수부를 2진수로 변환하는 방법과 부동 소수점은 다른 문서를 참고하시기 바란다.. 나도 이번에 처음 알았다.. ㅎ

    - 42.625라는 실수를 2진수로 변환하면 101010.101이 되고 이를 고정소수점으로 표현하면 위와 같이 표현할 수 있다.
    😟 근데 실수는 bit연산이 불가능하잖아 어떻게 해?
    - num * (1 << _bits) 이렇게 꼼수를 쓰면 돼. 2^_bits를 걍 냅다 해버리는 거지. 결국은 num2^8과 같은 말!
    - 근데 이때 roundf(num
    (1 << _bits))를 해주면 원래 실수와 더 가까운 값을 얻을 수 있어. 42.42f 같은 경우에 roundf를 하지 않으면 42.418이라는 값을 얻을 수 있지만, roundf를 해주면 42.4219라는 값을 얻을 수 있지. 소수점 버리지말고 반올림해주자구 👍

  • 그래서 저걸 뭐 어디에 어떻게 쓰는건데?
    1. 실수의 정수를 얻고 싶다!: num >> _bits, (int)num / 2^_bits

    • 아주 간단하다 num을 다시 소수점 비트만큼 밀어주거나, 2^bits으로 나누어 준 몫을 취하면 된다. 그러면 소수점이 사라지는 거 아닌가요? 맞음. 그게 정수이니까(끄덕)
  1. 저 고정소수점으로 표현된 실수를 얻고 싶어!
    - 역시 실수는 비트연산이 안 되니까 (float)(num / (1 << _bits)) 같은 꼼수를 쓰거나 (float)2^_bits로 나눠주면 돼~!
    - 이때 실수는 부동소수점으로 저장되어있는 값을 그대로 가져다 쓰는 것이다

🥨 고정 소수점 vs 부동 소수점

  • 🌟정확도: 부동 소수점 > 고정 소수점, 연산 속도: 고정 소수점 > 부동 소수점

  • 요 정도의 차이를 갖는다 할 수 있겠다. 고정 소수점의 경우 32bit를 정수부와 실수부로 쪼개서 사용하다보니 표현할 수 있는 수에 한계가 있을 수밖에 없다

  • 당장 우리의 과제만 봐도 그렇다. 8bit를 왼쪽으로 밀게 되는데, 이때 왼쪽으로 짤리게 된 bit들이 생길 수 있다. 그 경우는 원래의 값을 잃을 수밖에 없다.

  • 고정 소수점이 부동 소수점보다 표현할 수 있는 수의 범위가 훨씬 적다 이 말! 따라서 고정 소수점이 실제로 쓰이는 곳은 별로 없다고 한다 🤔 한정된 값만 필요한 곳이나 소규모에서 빠른 연산이 필요할 때 쯤?



    하 힘들었다.. 고정소수점..

🥨 '<<' 연산자 오버로딩

  • <<연산자는 출력을 위한 연산자로 ostream 클래스에 여러 함수로 오버로딩되어 구현되어있다.
  • 그리고 우리도 그 연산자를 오버로딩 할 수 있다!
  • 과제에 보면 객체를 넣었더니 그 객체의 float_value값이 출력된다. 따라서 해당 float_value값을 출력할 수 있도록 잘 오버로딩 해야 한다.

3. ex02

👀 ex02는 여러 연산자들을 오버로딩하는 과제였다.

🥨 비교 연산자 : >, <, >=, <=, ==, !=

  • 비교 연산자들은 bool 형태로 반환해주었고, 인수로는 const Fixed 참조자형으로 받았다
  • 단순히 값 비교를 해주면 되는 것이라 딱히 신경 쓸 것이 없었음

🥨 산술 연산자 : +, 0, *, /

  • this와 인수로 받은 Fixed& 객체와의 연산 결과를 반환해야 하기 때문에 새로운 Fixed 객체를 반환해주었다

🥨 증감 연산자(전위 연산 + 후위 연산): ++a, a++, --a, a--

  • 이 과제를 통해 새롭게 개념을 인식하게 된 전위 연산과 후위 연산

  • 이 연산자들은 this의 값이 즉시 바뀌는 할당 연산자로 어디서 연산이 이루어지냐에 따라 반환값 자체는 나 자신일수도, 나 자신이 아닐수도 있다
    - 이게 무슨 개 풀 뜯어먹는 소리야 싶을 수 있다. 당연함. 나도 그랬음.

  • 먼저 전위 연산은 연산이 끝나기 전에 증감이 완료되어야 한다. 따라서 나의 값을 증가시키고 반환도 증가된 나를 참조형으로 반환하면 된다.

  • 후위 연산은 연산이 종료된 후에 그 값이 결정되어야 한다. 근데 위에서 this의 값이 '즉시' 바뀐다고 하였다. 근데 또 종료 뒤에 값이 결정되어야 한다니? 싶다.
    - 정리하자면 그렇다 이전까지 우리는, 혹은 나는 후위 연산자가 연산 다음행에서 값을 증감시킨다고 생각했다. 근데 다시 생각해보면 이미 코드는 흘러갔는데 대체 어디서 증감을 한단 말인가? 다음행에 눈에 보이지 않는 코드를 컴파일러가 넣어놓는다?

    • 사실 우리는 이미 답을 알고 있는 것과 다름없다. 전위 연산자는 this의 값을 증가시키고 *this로 참조형을 반환했다. 그렇다면 후위 연산자는 어떻게 해야할까? 그렇다. this의 값을 증가시키는 것은 동일하다. 대신에 반환하는 것을 this가 아니라, this가 증감되기 전 변화하지 않은 값을 가지고 있는 임시 객체를 만들어 반환해야 한다.
  • 정리하자면

int main(void) {
	int a = 3;
    
    ++a = 6;
    std::cout << "a = " << a << std::endl;
    
    // a = 6 출력
 }

요건 가능해도

int main(void) {
	int a = 3;
    
    a++ = 6; // 컴파일 에러
 }

요건 불가능하다는 말이다.

  • 왜? 전위 연산이나 후위 연산이나 일단 현재 a의 값은 4라고 볼 수 있다. 전위 연산의 경우 ++a는 a를 뱉었기 때문에 그냥 4였던 a가 다시 6으로 재할당 된 것이라고 보면 된다. 그런데 후위 연산의 경우 ++a는 a가 아니라 a의 원래값인 3을 가지고 있는 다른 임시 객체가 반환되었기 때문에 3 = 6 이라는 이상한 식이 되어버리는 것이다. 그러니 컴파일 에러가 생기는 것이고!
  • 🚨 그런데 이러한 컴파일 에러를 내가 오버로딩한 함수에도 똑같이 정의해주려면 후위 연산의 경우 함수에 const를 달아 객체를 상수화 시켜주어야 한다. 그렇지 않으면 걍 컴파일이 되고, 이루어지면 안되는 식이 되게끔 되어버린다. (a++)++; 과 같은 것도 불가능해야 한다는 말!
  • 위와 같은 차이 때문에 사용자 정의 객체 연산시에는 전위 연산이 속도가 더 빠르다. 후위 연산의 경우 임시 객체를 만드는 과정이 추가로 들어가기 때문이다
  • 전위 연산과 후위 연산은 인수로 void, int를 넣어 서로 다르게 오버로딩 하여 컴파일러에게 알려주는 것이 관례라고 한다

🥨 전역 멤버 함수, min - max

  • 두 Fixed 객체의 대소를 비교하는 함수를 static으로 만드는 것이 ex02의 마지막 과제! min, max 각각 const 버젼과 const가 없는 버젼으로 총 2개씩 4개를 만들어야 했다.
  • 🚨 static 함수에는 const를 달 수 없다.
    - const 버젼을 만들면서 처음에는 static Fixed& min(Fixed& obj1, Fixed& obj2) const; 와 같이 만들려고 했었다. 멍청..
    - 하지만 그건 불가능하다. 왜냐?

🥨 함수 뒤에 붙는 const

  • ClassName memberFunction() const; 가 뜻하는 바는 함수 내 멤버 변수들을 상수화 하겠다는 뜻이다. 고로 이 때의 const는 해당 객체의 this 포인터에 영향을 미친다. 즉 멤버 함수 내에서 this 포인터의 타입은 const ClassName*이 되는 것이다.

  • 그런데 다들 아는 바와 같이 static 멤버 함수는 this 포인터를 가지고 있지 않다. 따라서 컴파일 에러가 발생할 수밖에 없다.

🥨 함수 앞에 붙는 const

  • 그래서 static const ClassName memberFunction() 와 같이 만들어야 한다. 이는 함수의 반환값을 상수화하겠다는 뜻이다.

🤔 근데 왜 굳이 const 버젼과 아닌 버젼을 만들라고 했는 지 모르겠다. 그냥 const의 차이에 대해 생각해보라는 건가?

❓ 그리고 hpp에서 static 멤버 함수로 선언한 함수는 cpp파일에서 다시 static const ~ 이런 식으로 쓸 수 없었다. 선언 시에만 static 키워드를 쓸 수 있다고 컴파일 에러가 뜨던데,, 뭐 바인딩 어쩌고 저쩌고 하는 글들을 찾긴 찾았는데 정확한 이유는 모르겠다. 그냥 static은 한 번만 선언될 수 있다하고 마음으로 이해하기로 했다.

4. ex03

👀 ex03은 주어진 point가 삼각형 내부에 있는 점인지 판단하는 과제였다.

  • 아주 옛날에 cpp의 c도 몰랐을 시절 평가를 가서 벡터의 외적, 내적에 대해 얘기를 들으면서 나도 다음에 이 과제를 하게되면 요걸 써먹어야지 했었다. 하지만 나는 수학 바보니까 설명 글을 읽는 것도 힘들더라고,,,, 그래서 다른 걸 찾아보다 점에서 오른쪽으로 선을 그어서 딱 한 번만 삼각형의 선분(오른쪽 선분이겠지!)을 만나면 삼각형 내부에 있는 것이고, 아예 만나지 않거나(삼각형 오른쪽, 위, 아래에 있을 경우), 두 번 만나면(삼각형 왼쪽에 있을 경우) 삼각형 밖에 있는 것을 알 수 있는 식을 써주신 포스팅을 발견하였다.
  • 바로 요 포스팅을 참고하였으며, 이 자리를 빌어 정말 무한한 감사의 말씀을 드린다!!!!

🥨 초기화 리스트

  • cpp01에서도 나왔던 초기화 리스트. const 변수 혹은 참조자 변수는 초기화 리스트를 사용해야 한다.
  • 이유는 cpp01 참고!
  • 과제에서 Fixed x, Fixed y를 const로 만들어야 하니 이는 초기화 리스트를 사용하여 초기화 하여야 한다.

🥨 const

  • 이놈의 const 변수 때문에 계속 컴파일 오류가 나가지고 진짜 엄청 헤맸었다.ㅠㅠ
  • 결국 fixed 클래스에 만들어뒀던 연산자 오버로딩 함수들을 멤버 함수에서 외부함수로 일부 변경하였다. 그래서 매개변수를 둘 다 const 변수로 받도록 함..
  • 그리고 복사 대입 연산자에서도 const로 지정한 변수에 어케 값을 대입해주라는거여... 장난하나... 하다가 갓jseo님 두들에서 너무 잘 정리된 자료를 보고 광명 찾았다. 단순한 설명 뿐만 아니라 여러 사례들도 같이 보여주셔서 훨씬 훨씬 이해가 잘 됐다.
    - 진심 얼굴도 모르고 뭐하는 분인지도 모르지만 그저 빛,, 그저 갓,, 감사합니당.. 존경해 마지않음..
  • 근데 그래도 좀 실 사용에 의문이 들기는 한다. 기껏 const로 만들어놓고 값을 수정하다니?! const의 마음은 갈대같아서,,

이제 겨우 3개 끝난거 실화냐 ㅠㅠ!

🚴 CPP 가보자고!

profile
개발국대가 되는 그 날까지. 지금은 개발 응애.

0개의 댓글