[42Seoul] CPP Module 02 / Orthodox canonical form, 연산자 오버로딩

tpwhzla·2023년 9월 5일

42Seoul

목록 보기
9/16

CPP Module 02

고정소수점

고정소수점 수 (Fixed-Point Numbers)는 소수점이나 쉼표의 위치가 고정된 위치에 있는 수를 나타낸다.
이러한 숫자 표현 방식은 정수 부분과 소수 부분을 특정 비트 수로 나누어 표현한다.
예를 들어, 16비트 고정소수점 수에서 8비트는 정수 부분, 나머지 8비트는 소수 부분을 표현할 수 있다.

사용 이유

  • 정밀도: 고정소수점 연산은 소수점 아래로 정밀도를 제한함으로써 오차가 누적되는 것을 방지할 수 있다.
  • 성능: 고정소수점 연산은 부동소수점 연산에 비해 일반적으로 더 빠른 계산 속도를 제공한다.
  • 메모리 사용량: 고정소수점 수는 메모리 공간을 더 효율적으로 사용할 수 있다.
  • 예측 가능성: 고정소수점 수를 사용하면 연산 결과가 일정하고 예측 가능하게 된다.
  • 고정소수점 수와 부동소수점 수 사이의 차이점

표현 방식

고정소수점 수: 소수점 위치가 고정되어 있으며, 정수 부분과 소수 부분이 일정한 비트 수로 나누어진다.
부동소수점 수: 소수점의 위치가 고정되어 있지 않으며, 수의 절대적 크기에 따라 변경된다. 이를 위해 지수와 가수 부분으로 나누어 표현한다.

정밀도

고정소수점 수: 정밀도는 소수점 아래 비트 수에 의해 결정된다.
부동소수점 수: 정밀도는 가수 부분의 비트 수에 의해 결정되며, 상대적인 정밀도가 제공된다.

성능

고정소수점 수: 일반적으로 더 빠른 연산 속도를 제공한다.
부동소수점 수: 복잡한 연산이 더 많은 시간을 소모할 수 있다.

응용 분야

고정소수점 수: 음향 처리, 그래픽, 임베디드 시스템 등에서 널리 사용된다.
부동소수점 수: 과학 계산, 금융 분석 등에서 주로 사용된다.

고정소수점 수를 사용하면 얻을 수 있는 성능과 정밀도의 이점
높은 계산 속도: 고정소수점 연산은 부동소수점 연산보다 일반적으로 더 빠른 연산 속도를 제공할 수 있다. 따라서 고성능이 필요한 응용 프로그램에서 유리하다.

일정한 정밀도: 고정소수점 수는 소수점 아래에 일정한 비트 수를 할당하므로, 연산 결과의 정밀도가 일정하다. 이는 예측 가능한 결과를 제공하며, 오류 누적을 방지할 수 있다.

리소스 최적화: 고정소수점 연산은 메모리와 계산 리소스를 더 효율적으로 사용할 수 있다. 특히 임베디드 시스템과 같이 리소스가 제한된 시스템에서 유용하다.

이러한 특징들은 고정소수점 수가 과학적 계산, 컴퓨터 그래픽스, 음향 처리 등 다양한 분야에서 매우 유용하게 사용될 수 있음을 의미한다.

Ex01

int main(void)
{
	Fixed a;
	Fixed b(a);
	Fixed c;

	c = b;

	std::cout << a.getRawBits() << std::endl;
	std::cout << b.getRawBits() << std::endl;
	std::cout << c.getRawBits() << std::endl;

	return (0);
}

기본적으로 main 함수는 제공된다.

출력 형태는 다음과 같다.

Default constructor called
Copy constructor called
Copy assignment operator called
getRawBits member function called
Default constructor called
Copy assignment operator called
getRawBits member function called
getRawBits member function called
0
getRawBits member function called
0
getRawBits member function called
0
Destructor called
Destructor called
Destructor called

Fixed a 를 선언할 때 기본 생성자가 출력된다.
Default constructor called

Fixed b(a) 를 선언할 때 복사 생성자가 호출된다.
Copy constructor called

이 때, Copy assignment operator called 와
getRawBits member function called 도 함께 출력되는데, 이는 사용자의 구현에 따라 다르다.

Fixed::Fixed(const Fixed &copy) : fixedPoint(copy.fixedPoint)
{
	std::cout << "Copy constructor called" << std::endl;
	*this = copy;
}

Fixed &Fixed::operator=(const Fixed &rhs)
{
	std::cout << "Copy assignment operator called" << std::endl;
	if (this != &rhs)
		this->fixedPoint = rhs.getRawBits();
	return *this;
}

나의 경우에는, 복사 생성자에서 *this = copy를 통해 ' = ' 연산자를 불러내었고, 이를 불러내었기 때문에 내부 함수 getRawBits 함수도 불려온다.

Fixed c 를 선언했을 때 기본 생성자가 출력된다.

c = b 를 선언했을 때 Copy assignment operator called와 getRawBits member function called 가 출력된다.

이후 3번의 getRawBits 함수가 출력되고, 소멸자가 3번 호출된다.

Ex01

본격적으로 고정 소수점을 표현해보는 과제이다.

정수형 인자를 받아 고정소수점 형식으로 변환하는 함수, 실수(float)인자를 받아 고정소수점 형식으로 변환하는 함수 2개가 있고, 이 둘이 핵심이다.

Fixed::Fixed(const int intValue)
{
	std::cout << "Int constructor called" << std::endl;
	fixedPoint = intValue << fractionalBits;
}

Fixed::Fixed(const float floatValue)
{
	std::cout << "Float constructor called" << std::endl;
	fixedPoint = roundf(floatValue * (1 << fractionalBits));
}

정수를 고정 소수점 형식으로 바꾸기 위해 왼쪽으로 8비트만큼 시프트 하고

실수를 고정 소수점으로 변환하기 위해 인자를 2^8(256) 만큼 곱한 후 반올림 시킨다.

이 부분이 이해가 잘 가지 않을 수 있는데

서브젝트에서 요구하는 고정소수점은 정수부와 소수부를 표현하는데에 정확하게 8비트씩을 사용한다(서브젝트에서 fractionalBits 8을 요구)

이 곳에서 intValue를 고정 소수점으로 변환할 때, 왼쪽으로 8비트를 시프트 한다는 뜻은

intValue의 원래 비트를 왼쪽으로 8칸 옮기고, 뒤에 소수부에도 8비트가 존재한다는 뜻이다.

즉, intValue에 3이 들어온다면

00000011 00000000 이 된다.

고정 소수점 표현에서는 3.0을 가리킨다.

마찬가지로, 실수 부분을 고정소수점으로 변환해보자.

floatValue를 고정소수점 형식으로 변환하기 위해서는, 기존 floatValue에 256을 곱해주면 된다.

floatValue에 256을 곱해주는 이유는, 실수 값을 고정소수점 형식으로 변환하는 과정에서 소수점 아래 8비트를 정수 부분으로 옮기는 역할을 수행한다.
실수 값에 256을 곱해주면, 소수점 아래 비트들이 8비트만큼 이동하여 정수 부분으로 오게 된다.

기존 floatValue에 256을 곱해주면 원래의 소수점 아래 8비트가 정수 부분으로 올라오게 되며, roundf 함수를 이용하여 이를 반올림하고, 반올림 한 결과를 fixedPoint에 저장한다.

예를 들어, floatValue에 3.5가 들어왔다면 이곳에 256을 곱하여 896.0이 만들어지고, 반올림을 하여 896을 만들어 fixedPoint에 저장한다.

이 값은 이후 toFloat, toInt 함수에 의하여 변형되어 사용된다.

그렇다면, toInt와 toFloat 함수는 어떻게 만들까?

float	Fixed::toFloat(void) const
{
	return (float)fixedPoint / (1 << fractionalBits);
}

int Fixed::toInt(void) const
{
    if(fixedPoint >= 0) {
        return (fixedPoint + (1 << (fractionalBits - 1))) >> fractionalBits;
    } else {
        return -((-fixedPoint + (1 << (fractionalBits - 1))) >> fractionalBits);
    }
}

앞서 말했듯, fractionalBits로 8만큼 이동시키어 각각의 Value에 256을 곱한 값을 fixedPoint에 집어 넣었다.

float 값은 256이 이미 곱해진 값인 fixedPoint를 다시 한 번 256으로 나누어줌으로서 float 형태를 얻을 수 있다.

subject의 출력 예제에도 나와있듯 넣은 값에 비해 정확하지 않은데, 고정 소수점은 특정 비트 수 (subject에서 8비트) 만을 사용하여 소수 부분을 나타내므로, 소수 부분에 대한 정밀도가 제한적으로 출력된다.

toInt 함수는 소수점의 반올림까지 표현해야 하므로 구현이 조금 복잡하다.

먼저, fixedPoint가 0보다 클 때 (양수일 때)

비트시프트 연산 fixedPoint + (1 << 7) >> 8 을 수행하게 된다.
fixedPoint 값에 128(또는 0.5)를 더하게 되는데, 이렇게 하면 소수 부분이 0.5 이상일 때 1이 추가된다.

그 다음, 오른쪽으로 8칸 비트 시프트 연산을 수행하게 되면 소수 부분이 삭제되고 정수 부분만 남게 된다. 이 과정이 소수부를 버리는 반올림을 완성한다.

마지막으로, 클래스를 어떻게 바로 출력할 수 있을까?


std::ostream &operator<<(std::ostream &os, const Fixed &fixed) {
	os << fixed.toFloat();
	return os;
}

연산자 오버로딩을 이용한다, << 연산자를 오버로딩 하지 않으면 클래스를 바로 출력할 수 없다.

해당 함수는 operator<< 라는 함수로, std::ostream이라는 클래스의 &os와 Fixed 클래스의 참조를 매개변수로 받는다.
여기서 os는, 출력 스트림(std::cout)를 참조하고, fixed는 출력할 fixed라는 객체를 참조한다.

2번째 줄의 os << fixed.toFloat()을 보면, fixed 객체의 toFloat 함수를 호출하여, Fixed 객체를 부동소수점 형태로 변환하고, 그 결과를 os 에 쓰는 작업을 수행한다.

여기서 각 '<<' 연산은 수정된 출력 스트림을 반환하여 다음 연산에 사용되도록 한다.

해당 함수는 Fixed 클래스를 출력할 때 사용되며, 이러한 방식으로 std::cout << Fixed_A << std::endl; 이 출력이 가능하게 된다.

이 코드가 실행될 때 operator<< 함수가 실행되며, Fixed 객체의 toFloat 메서드가 호출되어 객체의 고정 소수점 값이 부동 소수점 값으로 변환되어 스트림에 출력된다.

profile
DevOps / Infrastructure / Cloud Native / Platform Engineering

0개의 댓글