본문 바로가기

컴퓨터공학/C, C++

C,C++> 기타 내용

타입

음수 표현법

일정수 감소법 : 부호 없는 값에서 일정수를 빼서 음수를 표현한다. 빼주는 값을 바이어스라고 한다.  전체 범위 절반값 정도를 뺀다. 0~7 값에서 바이어스는 4이고 -4~3 값이 된다. 이 방법은 직관성이 부족하다.

부호 비트와 절대값 : 최상위 1비트를 부호 비트로 사용한다. 나머지는 절대값을 표현한다. 단점은 +0과 -0이 존재하여 모호하다는 특징이 있다.

1의 보수법 : 양수 비트를 모두 반전시켜 음수를 만든다. 이것도 +0과 -0이 존재하게 된다.

2의 보수법 : 1의 보수에서 1을 더해 음수를 표현한다. -0은 1000이 되어서 오버플로우 되어 사라지므로 한 가지 0만 있다.

 

현대 컴퓨터는 모두 2의 보수법으로 음수를 표현한다. 

반전과 증가는 간단한 동작이라서 음수 만드는 속도가 빠르며 2의 보수를 이용하여 덧셈으로 뺄셈을 대신할 수 있다.

 

예를 들어 8비트 부호없는 char형 변수 c가 100을 갖고 있을 때 여기에 256을 더한다고 가정하자. 

결과는 356이지만 오버플로우되어 100 그대로 갖게 된다. 100부터 1씩 256번 반복하면 결국 제자리로 온다. 

그래서 8비트에서 256은 0이다. 

이번엔 255을 더해보면 100은 한 바퀴 돌고 99가 된다. 결국 255을 더한다는 건 -1을 더한다는 것과 같다.

255는 -1을 2의 보수로 표현한 값이다. 같은 원리로 254는 -2이고 253은 -3이다. 

즉 여기서 알 수 있는 건 컴퓨터가 -n 연산할 때 n을 2의 보수로 만든 후 값을 더하는 방식으로 뺄셈을 수행한다.

그래서 컴퓨터는 덧셈을 하는 가산기만 갖고 있다. 

 

보수(Complement)란 어떤 수(기수)가 되기 위해 보충되어야 하는 수를 의미한다. 

예를 들어 10에 대한 3의 보수는 7이다.

a+b = 기수 이면 a와 b는 보수 관계라고 표현한다. 

2의 보수란 n비트에 대해 2^n을 기수로한 보수이다. 

n=8이면 2^8 = 256이고 결국 8비트에서 2의 보수는 256이 되기 위해 더 필요한 수로 정의할 수 있다. 

8비트에서 a의 2의 보수 b가 있으면 a+b=256이고 256은 0이므로  a+b = 0 이며 a=-b, b=-a 관계이다.

1의 보수는 2^n-1을 기수로 한 보수이며 n이 8이면 255가 기수이다. 1의 보수끼리는 비트 반전 관계가 성립한다.

1의 보수는 2의 보수보다 1이 작기 때문에 2의 보수는 반전 후 1을 더하면 된다. 

 

바이트 순서 

메모리 저장 단위는 8비트인데 실제 저장해야 할 값은 32나 64비트이다. 

그래서 값을 나누어서 저장한다. 예를 들어 0x12345678이 있을 경우 0x12, 0x34, 0x56, 0x78 4개의 8비트로 구성한다. 

저장하는 방식은 두 가지 방식이 있는데 빅 엔디안과 리틀 엔디안이다. 

빅 엔디안 : 높은 자리 수를 먼저 저장한다. 0x12가 가장 앞 쪽 바이트에 저장하고 0x34가 그 다음이다. 

리틀 엔디안 : 낮은 자리수를 먼저 저장한다. 0x79이 가장 앞 쪽 바이트에 저장하고 그 다음이 0x56이다.

 

빅 엔디안이 읽기 편하지만 리틀 엔디안이 연산에서 유리하다. 

0x1234라는 32비트 정수값이 메모리에 저장되어 있을 때 하위 2바이트만 받는다고 가정해보자. 

리틀 엔디안은 34120000으로 저장되고 빅 엔디안은 00001234로 저장된다. 

포인터를 통해 값을 읽을 때 리틀 엔디안은 시작점에서 2바이트만 읽지만 

리틀 엔디안은 포인터 자리를  뒤로 옮기고 2바이트를 읽는다. 

타입 확장시 리틀 엔디안은 뒤에 0만 추가하면 되지만 빅 엔디안은 값을 이동하고 앞에 00을 추가하기 때문에 번거롭다.

값의 축소 확장 시 리틀 엔디안이 편리하고 효율적이지만 값을 구성하는 바이트를 배열처럼 다루고 싶을 땐 빅 엔디안이 편리하다. 

빅 엔디안은 순서대로 출력하면 되지만 리틀 엔디안은 뒤부터 값을 읽어야 하므로 거꾸로 읽는 것과 같아서 헷갈릴 수 있다. 

 

큰 값을 작은 단위에 나누어 저장하는 순서가 다른 것뿐이다. 어떤 방식이 우수하다고 할 수 없다. 

값 자체가 바뀌는 것이 아니기 때문에 고급 언어 사용자는 신경 쓸 필요가 없다. 

그러나 이 사실을 알아야 하는 경우가 있는데 디버깅 중 변수가 저장된 메모리 영역을 본다거나 변수 값을 바이트 단위로 직접 조립해야 하는 경우이다. 이 외에도 바이트 저장 방식이 다른 이기종 컴퓨터간 네트워크 통신할 때이다. 

소켓은 기본적으로 빅 엔디안으로 통일되어 있다. 

 

 

부동 소수점

가수는 실수 실제값, 지수는 크기를 표현한다. 지수 값에 따라 소수점이 이동한다. 

32비트 float형은 부호 1비트 지수 8비트 가수 23비트로 구성.

부호 : 0이 양수이고 1이 음수

지수 : 지수가 n이면 가수부에 2^n이 곱해진다. 지수 길이는 8비트이므로 0~255까지 범위를 가진다. 바이어스 127을 적용하면 지수 표현 범위는 -127~128이다. 최소 지수 -127과 최대 지수 128은 0과 무한대를 표현하는 용도로 예약되어 있다. 그래서 최대표현 범위는 2^127이며 대략 10^38 정도 값을 갖는다.

가수 : 각 자리수는 2의 음수 거듭승으로 부여되어 있다. 가수 제일 왼쪽 비트부터 1/2, 1/4, 1/8, 1/16 가중치를 가지는 셈이다. 정규화 규칙으로 가수는 1~2 사이의 수이기 때문에 제일 왼쪽 비트는(2^0자리)는 항상 1이라고 가정한다. 

 

부동 소수점은 10진수를 정확하게 표현하지 못한다. 근본적으로 2진수와 10진수 수체계는 다르기 때문이다.

2진 가수부 각 자리수는 이전 자리수의 절반이다. 정확한 10진수를 표현할 수 있을 때까지 절반씩 더해간다.

그래도 원하는 10진수에 꼭 맞는 수가 안 만들어진다. 그래서 아주 낮은 자리까지 더하기를 계속 반복해야 한다. 

그래서 0.1을 소수점 이하 10자리까지 출력하면 0.1000000015 값이 나온다. 

0.1을 1000번 더하면 99.999046이 나오고 1000을 곱하면 100.0이 나온다. 

항상 오차가 나올 수 있다는 점 주의하자. 예를 들어 실수 끼리 상등 비교 연산을 하면 안된다. 

아무리 비슷한 값이라도 비트열은 다르기 때문이다. 상등 비교 연산대신 부등 비교하는 방식으로 처리해야 한다. 

 

다른 주의 사항은 범위와 정밀도가 다르다. float형이 10^38까지 표현할 수 있어도 정확하게 기억할 수 있다는 건 아니다. 지수 범위가 10진수로 38자리가 된다는 뜻이고 가수 정밀도는 10진수로 7자리 정도밖에 되지 않는다. 

 

수치 연산 보조 프로세서가 실수 연산을 하고 출력, 변환 등 해주는 함수들이 있어서 우리가 직접 실수 비트를 해석하는 일이 거의 없다. 

실수 비트 구조와 정수가 달라서 코드가 잘못되었다는 건 알 수 있다. 

 

구조체 정렬

구조체 첫 번째 멤버가 오프셋 0에 오고 두 번째 멤버가 첫 번째 멤버 길이만큼 뒤쪽 오프셋에 자리 잡는다. 

그래서 구조체 총 크기는 구조체 멤버 총 크기와 같다. 하지만 실제로는 다른데 기계 성능을 최대한 끌어올리기 위해 컴파일러가 구조체를 메모리에 배치할 때 두 가지 사항을 고려한다. 

첫 번째는 구조체가 시작될 번지를 고를 때 가급적이면 16바이트 경계에서 시작한다. 

왜냐하면 캐시 단위가 16바이트로 되어 있기 때문이다. 캐시 크기 배수 위치에 구조체를 배치하면 구조체를 액세스할 때 캐시 용량을 덜 차지하면서 빠르게 액세스할 수 있다. 경계 양쪽을 걸치면 캐시를 많이 차지하기 때문이다. 

두 번째는 구조체 멤버를 배치할 때 멤버 오프셋도 액세스하기 유리한 위치로 조정한다. 별다른 지정이 없으면 멤버 크기에 따라 자연스러운 경계 위치에 맞추도록 되어 있다. 예를 들어 int는 4바이트, double은 8바이트 경계에 맞춘다. 

c가 1바이트를 차지하고 난 뒤 d가 8바이트 경계에 배치되면 c와 d 사이 7바이트는 버려진다. 

이렇게 사용되지 않고 버려지는 공간을 패딩(Padding)이라고 한다. 

 

컴파일러는 CPU가 메모리를 취대한 빠른 속도로 액세스할 수 있도록 구조체 베이스와 멤버 오프셋을 조정해서 배치한다. 이를 구조체 정렬(alignment)라고 한다. 개발자는 구조체 정렬 방식에 대해 몰라도 문제가 없다. 왜냐하면 변수가 어떤 메모리에 배치되는가는 컴파일러 마음이다. 개발자는 변수명으로 그 번지 내용을 읽고 쓰기 때문이다. 

 

 

 

 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 생성자  (0) 2022.08.08
C,C++> 클래스  (0) 2022.08.04
C,C++> 파일 입출력  (0) 2022.07.28
C,C++> 함수 고급  (0) 2022.07.27
C,C++> 포인터 고급  (0) 2022.07.25