요약
정수는 프로세서에게 비트의 나열이며, C++는 그 나열에 열 단위로 작용하는 연산자를 제공한다. &, |, ^, ~ 연산자와 시프트 연산자 <<, >> 등이다. 이 수업은 그것들을 손으로 계산하는 법, 마스크 기법으로 정수 하나에 여러 플래그를 저장하는 법, 표준이 정의하지 않는 시프트를 알아보는 법을 가르친다. 이어서 2 ^ 3 식의 값이 왜 8이 아니라 1인지, 왜 비트 연산자에는 괄호가 필요한지를 설명한다. 후반부는 수학 라이브러리를 소개한다. <cmath> 헤더의 함수, <numbers> 헤더의 상수, <numeric> 헤더의 최대공약수와 최소공배수, <bit> 헤더의 비트 함수이며, 어느 언어에서나 통하는 원칙을 함께 다룬다. 라이브러리가 올바르고 검증된 형태로 제공하는 것은 다시 쓰지 않는다는 원칙이다.
학습 목표
- 작은 정수에 대해
&,|,^,~연산과 시프트의 결과를 손으로 계산한다. - 정의된 시프트(C++20부터는 음수의 시프트도 포함)와 정의되지 않은 동작인 시프트를 구별하고, 앞의 것의 결과를 예측한다.
- 마스크를 써서 비트를 확인하고, 켜고, 끄고, 반전한다.
2 ^ 3식의 값이 왜 8이 아니라 1인지 설명하고, 경우에 따라std::pow함수와 시프트 가운데 하나를 고른다.<cmath>헤더의 함수(std::sqrt,std::pow,std::abs,std::hypot, 삼각함수)와<numbers>헤더의 상수, 예컨대std::numbers::pi상수를 식 안에서 쓴다.
비트 연산자
수업 정수와 그 표현에서 int 타입의 값은 비트의 나열이고(이 강좌의 시스템에서는 32비트), 그 값은 1이 있는 위치의 가중치를 더한 것임을 보았다. 그러나 지금까지 프로그램은 정수를 수로만 다루었다. 더하고, 곱하고, 나누었을 뿐이다. 그런데 같은 나열을 달리 읽을 수도 있다. 32개의 “예” 또는 “아니요” 대답의 모음으로 읽는 것이며, 이렇게 읽기 위해 C++에는 비트를 하나씩, 위치 사이의 올림 없이 다루는 연산자가 네 개 있다.
비트는 오른쪽에서부터 0으로 시작하여 번호를 매긴다. 비트 의 가중치는 의 값이다. 예컨대 의 경우(이진법으로 1100), 비트 2와 3은 켜져 있고 비트 0과 1은 꺼져 있다.
연산자는 이렇다. 비트 AND 연산 a & b 식은 a 변수와 b 변수가 모두 1인 각 위치에서 1이다. 비트 OR 연산 a | b 식은 둘 중 적어도 하나가 1인 위치에서 1이다. 비트 XOR 연산 a ^ b 식은 둘 중 정확히 하나가 1인 위치, 곧 비트가 서로 다른 위치에서 1이다. 비트 NOT 연산 ~a 식은 피연산자가 하나뿐이며 모든 비트를 반전한다. 비트 한 쌍 , 에 대한 앞의 세 연산의 표는 연언, 선언, 배타적 선언의 표이다.
강좌 “수리논리학 I”의 내용을 공부한 사람은 여기서 명제 연결사를 알아볼 것이다. 1은 참, 0은 거짓을 뜻한다. 비트 연산자는 모든 열에 동시에 적용되는 연결사에 지나지 않는다. 이로부터 손으로 계산하는 방법이 나온다. 두 수를 이진법으로 위아래로 쓰고 오른쪽 끝을 맞춘 뒤, 각 열에 표를 적용한다. 이웃한 열은 신경 쓰지 않는다.
두 수 1100, 1010 값에는 네 열이 두 비트의 네 가지 조합을 모두 거친다는 성질이 있다. 그래서 세 표를 한꺼번에 보는 데 알맞다.
증명
자료:
1100값은 12이고1010값은 10이다. 열들은 왼쪽에서 오른쪽으로 쌍 , , , 의 값을 가진다.12 & 10식의 첫째 열: 두 비트가 모두 1이므로 AND 연산은 1을 낸다.둘째 열:
1값과0값. 0이 하나만 있어도 AND 연산은 0을 낸다.셋째 열:
0값과1값. AND 연산은 다시 0을 낸다.넷째 열:
0값과0값은 0을 낸다. 결과의 줄1000값은 8이다. 곧12 & 10식의 값은 8이다.OR 연산은 1이 적어도 하나 있는 열을 모두 켠다.
1110값, 곧12 | 10식의 값은 14이다.XOR 연산은 비트가 서로 다른 열만 켠다.
0110값이며,12 ^ 10식의 값은 6이다.
아래 프로그램으로 그림을 확인한다. 정수 수업에서 소개한 서식 필드 {:04b} 지정자는 값을 이진법 네 자리로, 모자라는 자리를 0으로 채워 쓴다.
#include <print>int main(){ const int a{0b1100}; const int b{0b1010}; std::println("a = {:04b} = {}", a, a); std::println("b = {:04b} = {}", b, b); std::println("a & b = {:04b} = {}", a & b, a & b); std::println("a | b = {:04b} = {}", a | b, a | b); std::println("a ^ b = {:04b} = {}", a ^ b, a ^ b); std::println("~a = {}", ~a);}a = 1100 = 12b = 1010 = 10a & b = 1000 = 8a | b = 1110 = 14a ^ b = 0110 = 6~a = -13마지막 줄에는 설명이 필요하다. NOT 연산은 쓰인 네 비트만이 아니라 a 변수의 32비트 전부를 반전한다. 왼쪽의 0 스물여덟 개가 1이 되고, 1로 시작하는 int 타입의 값은 2의 보수에서 음수이다. 그런데 2의 보수에서 어떤 수의 반수는 비트를 반전하고 1을 더하면 얻어진다. 곧 의 관계이다. 따라서 의 관계가 성립하고, ~12 식의 값은 의 값이다. 그러므로 부호 있는 정수에 대한 비트 NOT 연산은 수로서는 좀처럼 쓸모없는 수를 낸다. 쓸모는 비트 패턴으로서 있으며, 그래서 비트는 되도록 부호 없는 자료형에서 다룬다. 마스크의 절에서 보게 될 것이다.
작은 자료형에 대한 주의. 연산하기 전에 C++는 std::uint8_t 타입처럼 더 좁은 정수 자료형의 피연산자를 모두 int 타입으로 변환한다. 이것은 수업 산술 연산에서 산술 연산에 대해 설명한 승격이며, 여기서도 똑같이 적용된다. 따라서 flags 변수가 std::uint8_t 타입이고 값이 5일 때, ~flags 식의 값은 바이트 11111010 값이 아니라 int 타입의 의 값이며, 그 32비트에는 왼쪽에 덧붙은 1 스물네 개가 들어 있다. std::uint8_t 타입에 다시 저장할 때에야 8비트로 잘려 기대한 바이트 250을 되찾는다.
비트 연산자를 수업 관계와 논리의 논리 연산자와 혼동해서는 안 된다. 6 && 3 식의 값은 true 값이다. 두 피연산자가 모두 0이 아니며, 결과는 bool 타입이다. 반면 6 & 3 식의 값은 정수이며, 두 수 모두 1을 가진 열로 이루어진다. 게다가 && 연산자는 단락 평가되지만, & 연산자는 언제나 두 피연산자를 모두 계산한다.
콘텐츠를 불러오는 중…