C++

[C++] 42SEOUL CPP02 개념 공부하기

madylin 2023. 7. 28. 15:06
반응형

(42Seoul의 CPP02 과제를 진행하며 공부한 내용을 작성한 TIL을 정리한 글 입니다.)


개념 공부📓

Orthodox Canonical Form

  • OCCF 란, 클래스의 기본적인 형식 또는 일반적인 형식을 따르는 규칙 집합
  • 이 규칙들은 클래스의 일관성과 호환성을 유지하고, 예기치 않은 동작을 방지하는데 도움을 줌
  • C++ 98 버전의 OCCF 는 아래 규칙을 포함함
    • 기본 생성자(Default Constructor)
    • 복사 생성자(Copy Constructor)
    • 소멸자(Destructor)
    • 대입 연산자(Assignment Operator)
    • 동등 연산자(Equality Operator)
    • 복사 할당 연산자(Copy Assignment Operator)
    • ostream 연산자(’<<’) 오버로딩
    • istream 연산자(’>>’) 오버로딩
  • 42 과제에서는 위 규칙들 중 기본 생성자, 복사 생성자, 복사 할당 연산자, 소멸자만을 필수로 요구함

오버로딩(Overloading)

  • 같은 이름을 가진 함수나 연산자가 다양한 매개변수로 다른 동작을 수행할 수 있도록 하는 기능
  • 동일한 이름의 함수나 연산자를 여러번 정의할 수 있고, 함수나 연산자 호출 시에 전달되는 인자의 유형이나 개수에 따라 적절한 함수나 연산자가 선택됨
  • 오버로딩은 함수 오버로딩과 연산자 오버로딩으로 나뉨
    • 함수 오버로딩
      • 동일한 이름을 가진 함수가 서로 다른 매개변수 목록을 가지도록 허용하는 기능
      • 함수의 이름을 재사용하며, 비슷한 동작을 하는 여러 함수를 구현하는데 유용함
    • int add(int a, int b) { return a + b; } int add(double a, double b) { return a + b; } int main() { add(1,2); //int add(int a, int b) 함수가 호출됨 add(1.1, 2.2); //int add(double a, double b) 함수가 호출됨 return 0; }
    • 연산자 오버로딩
      • C++에서 기본적으로 제공되는 연산자(+,-,* 등)에 대해 사용자가 동작을 정의할 수 있게 하는 기능
      • 클래스나 구조체에 대해 기존 연산자를 재정의하여 원하는 동작을 수행할 수 있음
    • class Vector { private: double x, y; public: Vector(double x, double y) : x(x), y(y) {} Vector operator+(const Vector& other) const { return Vector(x + other.x, y + other.y); } }

복사 생성자(Copy Constructor)

  • 다른 객체의 값을 복사하여 새로운 객체를 생성하며, 일반적으로 참조로 전달
  • 복사생성자는 기본적으로 c++ 컴파일러에서 제공되며, 사용자가 별도로 정의하지 않아도 자동으로 생성됨. 디폴트 복사 생성자는 얕은 복사를 함.
  • MyClass obj1; //MyClass 의 객체 obj1을 생성 MyClass obj2(obj1); //obj1을 사용하여 obj2를 복사 생성자 호출
  • 복사 생성자는 기존 값을 복사해 전달하는 개념이기 때문에, 값이 변경되지 않도록 const 로 선언한다.
  • class MyClass { publick: MyClass(const MyClass& other){ //객체 복사 } }
  • 얕은 복사(Shallow Copy)란?
    • 단순히 원본 객체의 주소를 복사 대상 객체에 할당하는 것
    • 원본 객체와 복사된 객체는 동일한 메모리를 공유함
  • class MyClass { public: int* data; MyClass(const MyClass& other) { // 주소만 복사되고 데이터는 공유됨 data = other.data; } };
  • 깊은 복사(Deep Copy)란?
    • 복사 대상 객체에 새로 메모리를 할당하여 데이터를 복사하는 것
    • 원본 객체와 복사된 객체가 다른 메모리를 갖기 때문에, 변경이 발생해도 서로 영향을 주지 않음
    • 복사 생성자나 대입 연산자 오버로딩을 통해 수행될 수 있음
  • class MyClass { public: int* data; MyClass(const MyClass& other) { // 새로운 메모리를 할당하고 데이터를 복사함 data = new int(*other.data); } ~MyClass() { delete data; // 메모리 해제 } };

대입 연산자(Assignment Operator)

class MyClass {
public:
    // 대입 연산자 오버로딩
    MyClass& operator=(const MyClass& other) {
        // 대입 연산자의 본문에는 객체를 복사하는 로직이 들어감
        // 다른 멤버 변수들을 다른 객체에서 복사하여 할당하는 등의 작업을 수행할 수 있음
        if (this != &other) { // 자기 자신에게 대입하는지 확인
            // 멤버 변수 복사
        }
        return *this; // 대입 후 자기 자신을 반환
    }
};
  • 클래스의 객체에 다른 객체의 값을 할당하는 연산자(’=’)
  • 클래스의 멤버 변수를 다른 객체의 멤버 변수로 복사하여, 객체 간의 데이터를 교환하거나 복사할 때 주로 사용됨
  • 복사 개념이기 때문에 얕은 복사와 깊은 복사의 개념이 적용됨

동등 연산자(Equality Operator)

  • 두 개의 값을 비교하고, 동일한 값이면 true를, 다른 값이면 false 를 반환하는 연산자(’==’)
  • 기본 데이터 유형(정수, 실수 등) 뿐만 아니라 사용자가 정의한 클래스나 구조체에 대해서도 사용할 수 있음. 이 경우에는 동등 연산자 오버로딩을 통해 사용자가 직접 판단하는 방식을 정의해야 함
class Person
{
private:
	std::string name;
	int age;

public:
	Person(const std::string& name, int age) : name(name), age(age) {}

	bool operator==(const Person& other) const
	{
		return (name == other.name && age == other.age);
	}
}

복사 할당 연산자(Copy Assignment Operator)

class MyString
{
private:
	char *buffer;

public:
	MyString(const char *str)
	{
		int len = 0;
		while (str[len] != '\\0')
			len++;
		buffer = new char[len + 1];
		
		for (int i = 0; i < len; i++)
			buffer[i] = str[i];
		buffer[len] = '\\0';
	}

	MyString& operator=(const MyStyle& other) //복사 할당 연산자
	{
		if (this == &other) // 자기 자신인지 확인
			return *this;

		delete[] buffer; // 기존 할당된 메모리 해제

		int len = 0;
		while (str[len] != '\\0')
			len++;
		buffer = new char[len + 1]; // 새 메모리 할당
		
		for (int i = 0; i < len; i++)
			buffer[i] = other.buffer[i]; // 값 복사
		buffer[len] = '\\0';

		return *this;
	}
}

  • 이미 생성된 객체에 대해 다른 객체의 값을 할당하거나, 이미 할당된 메모리 자원을 해제하고 다른 값으로 다시 할당할 때 사용하는 연산자(’=’)
  • 일반적으로 반환 형식은 ‘클래스명&’ 이며, 매개변수는 ‘const 클래스명&’
  • 복사 생성자와의 차이점은 호출되는 시점과 사용되는 상황에 차이가 있음.
    • 복사 생성자 : 객체가 생성될 때, 복사 생성자를 호출하는 경우. 새로운 객체에 대해 기존 객체의 값으로 초기화 하기 위해 사용
    • 복사 할당 연산자 : 객체가 이미 생성된 후. 다른 객체의 값으로 할당하여 복사 하기 위해 사용

ostream 연산자(’<<’) 오버로딩

class Person
{
private:
	std::string name;
	int age;

public:
	Person(const std::string& name, int age) : name(name), age(age) {}

	std::ostream& print(std::ostream& os) const
	{
		os << name << age;
		return os;
	}
}

// ostream 연산자 오버로딩
std::ostream& poerator<<(std::ostream& os, const Person& person)
{
	return person.print(os);
}
  • ‘<<’ 연산자는 std::ostream 객체에 대한 출력을 수행하는 연산자로, ‘<<’ 연산자를 오버로딩하여 사용자가 정의한 클래스나 구조체에 대한 출력 동작을 지정할 수 있음
  • 반환 형식은 출력 스트림 객체를 참조 형식(std::ostream&)으로 반환하고, 매개변수를 출력 스트림 객체(std::ostream&)와 출력하고자 하는 사용자 정의 클래스(또는 구조체) 객체를 받음

istream 연산자(’>>’) 오버로딩

class Person
{
private:
	std::string name;
	int age;

public:
	Person() {}

	std::istream& read(std::istream& is)
	{
		is >> name >> age;
		return is;
	}
}

std::istream& operator>>(std::istream& is, Person& person)
{
	return person.read(is);
}
  • ‘>>’ 연산자는 std::istream 객체에서 입력을 수행하는 연산자로, ‘>>’ 연산자를 오버로딩하여 사용자가 정의한 클래스나 구조체에 대한 입력 동작을 지정할 수 있음
  • 반환 형식은 입력 스트림 객체를 참조 형식(std::istream&)으로 반환하고, 매개변수를 입력 스트림 객체(std::istream&)와 입력받고자 하는 사용자 정의 클래스(또는 구조체) 객체를 받음

고정소수점

  • 정수를 표현하는 비트 수와 소수를 표현하는 비트 수를 미리 정해두고, 해당 비트만 사용하여 숫자를 표현하는 방식
  • 한정된 비트에 정수와 소수를 표현해야하기 때문에, 나타낼 수 있는 범위가 한정적인 문제
  • 아래 처럼 32비트를 쪼개 실수를 표현
    • [   1  ][         15        ][           16         ] 
    • [sign][integer part][fractional part]
    • 처음 1 비트는 sign(부호) 이며, 양수는 0 음수는 1
    • 다음 15 비트를 integer(정수부)
    • 다음 15 비트를 fractional(소수부)
    • 정수부에 정수값을 2진수로 바꾸어 넣고, 소수부에 소수값을 2진수로 바꾸어 넣은 후, 남은 자리는 0으로 채움

부동소수점

  • 소수점의 위치를 고정하지 않고, 그 위치를 나타내는 수를 따로 적는 방식으로, 유효숫자를 나타내는 가수와 소수점의 위치를 나타내는 지수로 나누어 표현함
  • 부동소수점은 다양한 방식이 있고, 일반적으로는 IEEE754 를 표준으로 함
  • 실수 연산이 부정확할 수 있다는 단점이 있음

ex02 구현

  • subject 에서 요구하는 내용은 단순히 소수부를 8 비트로 사용하여, int 변수에 fixed-point 값을 저장하라고 한다.
  • 그래서 아래처럼 단순하게 비트를 구성하는 것으로 이해하고 진행했다.
    • [    24   ][     8     ]
    • [ 정수부 ][ 소수부 ]
  • 기본적인 개념으로는, 만약 10.01 이라는 2진수를 비트에 넣어야 한다면, 비트연산을 통해 8 만큼을 왼쪽으로 밀어줘야 한다. (그래야 정수부에 10이, 소수부에 01이 들어가기 때문)
    • [000000000000000000000010][01000000]
  • 값을 받아 저장하는 프로세스는 다음과 같다.
    • 받아온 10진수 값에 8비트를 왼쪽으로 밀어준다( = 256을 곱한다 )
    • 만약 소수값이라면 int에 저장할 수 없기 때문에, roundf 를 사용하여 반올림한 후 저장한다.
    • ex
      • 값이 10인 경우
        • 10 에 256 을 곱한 2560이 int 변수에 저장됨
      • 값이 10.01인 경우
        • 10.01 에 256 을 곱한 2562.56 을 roundf 로 소숫점을 반올림하여, 2563이 int 변수에 저장됨
  • 저장한 값을 출력하는 프로세스는 다음과 같다.
    • int 변수에 저장된 값에 8비트를 오른쪽으로 밀어준다( = 256을 나눈다 )
      • ex
        • 값이 10인 경우
          • 저장된 2560 에서 256을 나눈 후 10을 출력함
        • 값이 10.01인 경우
          • 저장된 2563 에서 256을 나눈 후 10.0117을 출력함 (정확한 원래 값으로의 출력은 할 수 없다)

cout 소수값 출력 범위

  • cout 은 소수값 출력 시, 기본적으로 6자리까지만 출력됨
  •  ex
    • 1.234567 → 1.23456
    • 12.34567 → 12.3456
  • 앞의 값이 0인 경우에는 카운팅에 포함되지 않는 것 같다. '0.00390625' 이렇게도 출력됨.
  • 만약 소수값이 더 출력되길 원한다면, 아래 함수를 사용하여 설정을 바꿔주면 됨
cout.precision(10);
cout << 10 / 3;
//3.333333333 출력

전위 연산자, 후위 연산자 오버로딩

  • 전위 연산자 오버로딩
MyClass& operator++()
{
	a += 1;
	b += 1;
	return *this;
}
  • 후위 연산자 오버로딩 
    • 후위연산자 오버로딩에서는 int 를 매개변수로 받고 있는데, 이 것은 전위연산자와의 구분을 위한 값일 뿐임. 실제로 인자를 받는 것은 아님
    • 후위연산자도 전위연산자와 마찬가지로 증감이 즉시 이루어진다. 다만 전위연산자는 증감된 값을 참조형으로 반환하고, 후위연산자는 증감 전의 값을 가진 임시 객체를 만들어 반환하기 때문에, 증감된 값은 이후에 사용할 수 있는 구조
MyClass operator++(int)
{
	MyClass tmp(a, b);

	a += 1;
	b += 1;
	return tmp;
}

신발끈 공식

  • 삼각형의 넓이를 구하는 공식 중 하나 (다각형도 가능)
  • 삼각형의 반시계 방향으로 점 P1, P2, P3 이 있을 때, 붉은 화살표로 이어진 것끼리 곱한 뒤 모두 더하고, 초록 화살표로 이어진 것끼리 곱한 뒤 뺀 후, 절댓값을 구하면 그것이 넓이가 됨

  • 위 내용으로 정리한 수식

 

 

 

 

제가 공부한 내용을 기록하고 있습니다.

혹시 수정이 필요한 부분이 있다면, 댓글로 지적 부탁드립니다!

선한 관심과 도움 감사드립니다😊

반응형

'C++' 카테고리의 다른 글

[C++] 42SEOUL CPP04 개념 공부하기  (0) 2023.07.28
[C++] 42SEOUL CPP03 개념 공부하기  (0) 2023.07.28
[C++] 42SEOUL CPP01 개념, 함수 공부하기  (0) 2023.07.28