저번 시간에 배운 연산자는 복습하셨나요? 이제 더 난이도를 올려보겠습니다.
비트 연산자
우리는 10진법을 기본으로 사용하지만, 컴퓨터는 이진법이 기본인 거 아시죠? 0과 1을 사용하죠.
_ _ _ _ _ _ _ _ 이렇게 8칸이 있으면 각 칸에서 1 또는 0을 표현할 수 있습니다. 각 칸을 비트(Bit)라고 하고 8비트가 모인 걸 1바이트(1byte = 1B)라고 합니다. 1바이트, 2바이트, 4바이트 변수가 이런 형식입니다.
각 비트의 자리수는 각각 128/64/32/16 / 8/4/2/1을 나타냅니다.
즉 이진수 0000 0011은 2와 1 자리에 비트가 있으니 2+1 = 3입니다.
이진수 0010 0010은 32와 2 자리에 비트가 있으니 32+2 = 34입니다.
unsigned char은 1바이트이고, 0부터 255까지의 값을 저장할 수 있습니다.
0 0 0 0 0 0 0 0 이렇게 모든 비트가 0이면 0이고,
0 0 0 0 0 0 0 1 오른쪽부터 채워서 이건 1,
0 0 0 0 0 0 1 0 1을 더 더하면 받아올림이 되어 이진수 10 = 십진수2가 됩니다.
이렇게 계속 채우다 보면
1 1 1 1 1 1 1 1 이건 모든 자리에 비트가 존재하므로 128+64+32+16+8+4+2+1=255입니다. 1바이트의 최대 표현 값이 255인 이유가 여기 있습니다.
2바이트인 short는 어떻게 표현할까요?
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 이렇게 2배로 늘리면 됩니다. 1바이트가 2개니 2바이트입니다.
비트 연산자는 정수를 비트 단위로 바꾸어서 연산을 합니다 (물론 사람이 보기엔 10진수를 2진수로 바꾸는 거지만, 컴퓨터는 원래 2진수로 저장한다는 걸 생각하면..)
종류는 &, |, ~, ^, <<, >> 가 있습니다.
익숙한 게 보이시죠? &과 |는 논리 연산자에서 &&, ||으로 썼고 그 의미도 비슷합니다.
<<와 >>는 std::cout, std::cin에서 사용했지만 그 의미는 전혀 다릅니다.
우선 예제부터 보여드리겠습니다.
#include <iostream>
#include <bitset> // std::bitset<>
int main()
{
using _8bit = std::bitset<8>;
int su1 = 0b0100'1011;
int su2 = 0b0101'0110;
std::cout << "su1 = " << su1 << std::endl;
std::cout << "su2 = " << su2 << std::endl;
std::cout << "su1 (binary) = " << _8bit(su1) << std::endl;
std::cout << "su2 (binary) = " << _8bit(su2) << std::endl;
return 0;
}
2진수로 출력하기 위해 헤더가 필요합니다. include해 주세요. using _8bit() 이건 std::bitset<8>을 다른 이름으로 줄여서 편리하게 사용하고자 하는 것입니다. 여기는 일단 넘어갑시다.
std::cout 는 10진수를 기본으로 출력합니다. 이때 아까 만든 _8bit()를 이용해서 2진수로 바꿀 수 있습니다. 그냥 함수라고 생각해도 괜찮습니다. (뭐 생성자도 함수이니...)
참고로 2진수를 변수에 대입하려면
// 4바이트 모두 표현. 일부만 해도 무방
int binary = 0B0000'0000'0000'0000'0000'0000'0000'0000;
숫자 맨 앞에 0b 또는 0B를 붙이면 2진수 표현이 가능합니다. 이때 작은따옴표 ' 를 이용해서 4자리마다 끊어주면 가독성이 올라갑니다.
그럼 이제 비트 연산자를 사용해 보겠습니다.
#include <iostream>
#include <bitset>
int main()
{
using _16bit = std::bitset<16>;
short su1 = 0b0100'1011;
short su2 = 0b0101'0110;
std::cout << "===10진수 su1, su2===" << std::endl;
std::cout << "su1 = " << su1 << std::endl;
std::cout << "su2 = " << su2 << std::endl;
std::cout << "su1 & su2 = " << (su1 & su2) << std::endl;
std::cout << "su1 | su2 = " << (su1 | su2) << std::endl;
std::cout << "su1 ^ su2 = " << (su1 ^ su2) << std::endl;
std::cout << "~su1 = " << ~su1 << std::endl;
std::cout << "~su2 = " << ~su2 << std::endl << std::endl;
std::cout << "===2진수 su1, su2===" << std::endl;
std::cout << "su1 = " << _16bit(su1) << std::endl;
std::cout << "su2 = " << _16bit(su2) << std::endl;
std::cout << "su1 & su2 = " << _16bit(su1 & su2) << std::endl;
std::cout << "su1 | su2 = " << _16bit(su1 | su2) << std::endl;
std::cout << "su1 ^ su2 = " << _16bit(su1 ^ su2) << std::endl;
std::cout << "~su1 = " << _16bit(~su1) << std::endl;
std::cout << "~su2 = " << _16bit(~su2) << std::endl;
return 0;
}
===10진수 su1, su2===
su1 = 75
su2 = 86
su1 & su2 = 66
su1 | su2 = 95
su1 ^ su2 = 29
~su1 = -76
~su2 = -87
===2진수 su1, su2===
su1 = 0000000001001011
su2 = 0000000001010110
su1 & su2 = 0000000001000010
su1 | su2 = 0000000001011111
su1 ^ su2 = 0000000000011101
~su1 = 1111111110110100
~su2 = 1111111110101001
2바이트의 short 자료형을 사용했습니다. 10진수 결과만 보면 전혀 이해가 되지 않으시겠지만, 2진수 결과를 보면 이해가 쉽습니다. 일단 ~su1의 10진수 표현이 음수가 나오는 건, signed 자료형에선 맨 앞의 비트를 +,- 부호를 판별하는 데 쓰기 때문입니다. 0이면 양수, 1이면 음수입니다. 자 일단 하나씩 살펴봅시다.
su1 = 0000000001001011 // 75
su2 = 0000000001010110 // 86
su1 & su2 = 0000000001000010 // 66
비트 연산자는 비트별로 계산을 하기 때문에 세로줄로 보시면 됩니다. &연산자(AND)는 같은 위치의 비트를 계산하여
1 & 1 일때만 결과값을 1로 내고, 나머지 경우엔 0을 냅니다. 즉, 둘 다 1일때만 1입니다.
보시다시피 su1 & su2 도 둘 다 1인 비트만 1로 계산이 된 것을 볼 수 있습니다. 이 수는 십진수로 66입니다.
기호 & 대신 bitand 라고 써도 작동합니다 (ex. su1 bitand su2)
su1 = 0000000001001011 // 75
su2 = 0000000001010110 // 86
su1 | su2 = 0000000001011111 // 95
| 연산자(OR)는 둘 중 하나라도 1이면 1이되고, 둘 다 0일때만 0이 나옵니다. su1 | su2도 | 연산에 의해서 하나라도 1인 비트는 1의 값을 가지게 됩니다. 이 수는 십진수로 95입니다.
기호 | 대신 bitor 라고 써도 작동합니다 (ex. su1 bitor su2)
su1 = 0000000001001011 // 75
su2 = 0000000001010110 // 86
su1 ^ su2 = 0000000000011101 // 29
자, 이건 헷갈리실 수도 있는데, ^연산자(XOR)는 둘 중 하나만 1이어야 1이됩니다. 둘 다 1이거나 둘 다 0이면 0입니다.
즉, 같으면 0 다르면 1입니다. 0 ^ 1 = 1, 1 ^ 1 = 0
기호 ^ 대신 xor 라고 써도 작동합니다 (ex. su1 xor su2)
su1 = 0000000001001011 // 75
~su1 = 1111111110110100 // -76
su2 = 0000000001010110 // 86
~su2 = 1111111110101001 // -87
~연산자(NOT)는 비트를 반전시킵니다. 이때 signed 변수의 경우 부호까지 바뀌게 됩니다. 맨 앞 비트는 부호를 표시하는 비트라고 말씀드렸습니다.
기호 ~ 대신 compl 이라고 써도 작동합니다 (ex. compl su1)
#include <iostream>
#include <bitset>
int main()
{
using _16bit = std::bitset<16>;
short su1 = 1;
short su2 = 0b0001'0111;
std::cout << "===10진수 su1, su2===" << std::endl;
std::cout << "su1 = " << su1 << std::endl;
std::cout << "su1 << 1 = " << (su1 << 1) << std::endl;
std::cout << "su1 << 2 = " << (su1 << 2) << std::endl;
std::cout << "su1 << 3 = " << (su1 << 3) << std::endl << std::endl;
std::cout << "su2 = " << su2 << std::endl;
std::cout << "su2 << 1 = " << (su2 << 1) << std::endl;
std::cout << "su2 << 2 = " << (su2 << 2) << std::endl;
std::cout << "su2 << 3 = " << (su2 << 3) << std::endl << std::endl;
std::cout << "===2진수 su1, su2===" << std::endl;
std::cout << "su1 = " << _16bit(su1) << std::endl;
std::cout << "su1 << 1 = " << _16bit(su1 << 1) << std::endl;
std::cout << "su1 << 2 = " << _16bit(su1 << 2) << std::endl;
std::cout << "su1 << 3 = " << _16bit(su1 << 3) << std::endl << std::endl;
std::cout << "su2 = " << _16bit(su2) << std::endl;
std::cout << "su2 << 1 = " << _16bit(su2 << 1) << std::endl;
std::cout << "su2 << 2 = " << _16bit(su2 << 2) << std::endl;
std::cout << "su2 << 3 = " << _16bit(su2 << 3) << std::endl;
return 0;
}
===10진수 su1, su2===
su1 = 1
su1 << 1 = 2
su1 << 2 = 4
su1 << 3 = 8
su2 = 23
su2 << 1 = 46
su2 << 2 = 92
su2 << 3 = 184
===2진수 su1, su2===
su1 = 0000000000000001
su1 << 1 = 0000000000000010
su1 << 2 = 0000000000000100
su1 << 3 = 0000000000001000
su2 = 0000000000010111
su2 << 1 = 0000000000101110
su2 << 2 = 0000000001011100
su2 << 3 = 0000000010111000
std::cout의 << 연산자와 su1, su2의 << 연산자랑 모양이 같아 헷갈리시죠? 괄호만 잘 씌워 주시면 됩니다.
<<, >>는 비트 쉬프트 연산자입니다. 비트 값을 n만큼 각각 왼쪽, 오른쪽으로 쉬프트 하는 연산자입니다.
말 그대로 비트를 옮기는 것이기 때문에 <<연산자의 경우 1을 계속 연산하면
$1,\ 2,\ 2^2,\ 2^3,\ 2^4,\ ...
이렇게 증가하는 것을 확인할 수 있습니다. 2진수 출력 결과를 보면 1인 비트가 계속 왼쪽으로 이동하는 걸 볼 수 있습니다.
나머지 수들에 대해서도 비트 전체를 왼쪽으로 쉬프트 하기 때문에 비트가 옮겨지는 모습을 볼 수 있습니다.
>> 연산자는 <<의 반대 연산자입니다.
참고로 비트연산자도 복합대입연사자로 사용할 수 있습니다. &=, |=, ^=, <<=, >>=로 쓰면 됩니다.
&=는 and_eq, |=는 or_eq, ^=는 xor_eq 로 써도 작동합니다.
형 변환 연산자
아래처럼 하면 오류는 아니지만 경고가 발생합니다.
long long ll{4537534546734557676LL};
int n = ll;
C4244: '초기화 중': 'long long'에서 'int'로 변환하면서 데이터가 손실될 수 있습니다.
long long은 8바이트고, int는 4바이트입니다. 값이 잘리는 상황이 발생하기도 하죠.
그런데 개발자가 의도한 동작일 수도 있습니다. 이때는 명시적으로 바꾼다고 컴파일러에게 알려야 합니다.
그럴 때 사용하는 연산자가 형 변환 연산자, static_cast<> 입니다.
static_cast<변환할 타입>(변환할 값)
즉, 아래처럼 형 변환을 해 주면 경고가 뜨지 않습니다.
long long ll{4537534546734557676LL};
int n = static_cast<int>(ll);
다만 이 형변환은 '안전한' 경우에만 가능합니다. 형변환이 안 되는 타입은 변환할 수 없습니다.
int n = 30;
std::string s = static_cast<std::string>(n);
정수형 변수는 문자열로 '강제' 변환할 수 없습니다. 만약 정수값을 문자열로 변환시키고 싶다면 다른 함수를 사용해야 합니다.
std::string s = std::to_string(n); // #include <string>을 해 주세요.
sizeof 연산자
이 연산자는 자료형의 크기 또는 변수의 크기를 재어 알려줍니다.
sizeof(자료형) // 괄호가 반드시 필요함
sizeof 변수 // 괄호 필요 없음
예제 코드를 봅시다.
std::cout << "char형의 크기: " << sizeof(char) << std::endl;
std::cout << "short형의 크기: " << sizeof(short) << std::endl;
int n;
std::cout << "int형 변수 n의 크기: " << sizeof n << std::endl;
std::cout << "3.14의 크기: " << sizeof 3.14 << std::endl;
std::cout << "std::string형의 크기: " << sizeof(std::string) << std::endl;
char형의 크기: 1
short형의 크기: 2
int형 변수 n의 크기: 4
3.14의 크기: 8
std::string형의 크기: 24
std::string은 좀 유난히 크죠? 아무튼, 자료형의 크기를 측정할 때는 괄호가 필수이고, 변수나 상수는 괄호가 선택입니다. 참고로 이 연산자는 sizeof...(param) 의 형태로도 쓰이는데, 이건 좀 나중에, 9-5에서 배울겁니다.
typeid
그러면 안 되겠지만, 선언해놓고 '이게 무슨 변수더라?'하는 경우가 생길 수 있습니다. 그럴 때 변수의 자료형을 출력해주는 연산자가 이 typeid입니다. 변수 뿐만 아니라 상수, 함수, 그리고 나중에 배울 클래스, 열거형, 구조체, 람다식, 펑터, 템플릿 등... 하여튼 모든 것을 알고 있는 연산자입니다.
int num;
std::cout << typeid(num).name() << std::endl;
std::cout << typeid(main).name() << std::endl;
int
int __cdecl(void)
단항 연산자
1 + 2, 2 - 3 이렇게 쓰면 +와 - 는 각각 덧셈, 뺄셈 연산자입니다.
그런데, +1, -3 이렇게 쓸 수도 있습니다. 이건 단항 덧셈, 단항 뺄셈이라고 부릅니다.
이게 변수로 가면 골치가 약간 아픕니다.
단항 + 연산자 (프로모션 연산자)
char ch; short s;
auto ach = +ch;
auto as = +s;
std::cout << typeid(ach).name() << std::endl; // int 가 출력
std::cout << typeid(as).name() << std::endl; // int 가 출력
정수 계열 변수에 + 연산자를 붙이면 프로모션이 되어 int보다 작은 변수들은 int로 변환한 값을 반환합니다.
auto 변수들에 int형이 담기는 이유입니다.
단항 - 연산자 (부호 반전 연산자)
int n{5};
std::cout << -n << std::endl; // -5가 출력
부호를 반전시키는 연산자입니다. 참고로 unsigned int 자료형에 이 연산자를 적용할 경우 의도치 않은 값이 나올 수 있으니 주의하세요.
함수 호출 연산자
std::printf("%d", 10);
여기서 소괄호 ()도 연산자입니다. 함수를 호출하는 역할을 합니다.
멤버 접근, 네임스페이스
. -> ::
이들은 선언된 멤버에 접근하거나 네임스페이스에 접근할 때 사용합니다.
네임스페이스에 관한 자세한 내용은 4단원에서 하겠습니다.
기타
5단원에서 배울 동적 할당을 위한 new와 delete 연산자, 구조체를 위한 alignof 연산자 및 noexcept도 연산자입니다.
쉼표도 연산자입니다. 쉼표는 앞의 식과 뒤의 식을 묶어주며, 뒤의 식을 결과로 반환합니다.
즉, 1,2는 2입니다. 그냥 무조건 뒤에 있는 게 결과값입니다. 함수를 호출하거나 매개변수 리스트를 작성할 때도 사용합니다. std::printf() 기억하시죠?
연산자 우선순위
수학에서 곱셈을 먼저 계산하고 덧셈을 계산하듯, C++에서도 다양한 연산자가 만나면 우선순위가 있습니다.
아래는 연산자들의 우선순위를 정리한 표입니다.
우선순위 |
연산자 |
사용 |
결합 순서 |
1 |
:: |
범위 지정 연산자 |
왼쪽에서 오른쪽 |
2 |
a++ a−− |
후위 증감 연산자 |
|
type() type{} |
Functional Cast |
||
a() |
함수 호출 |
||
a[] |
배열 |
||
. -> |
멤버 접근 |
||
3 |
++a −−a |
전위 증감 연산자 |
오른쪽에서 왼쪽 |
+a −a |
단항 덧셈, 단항 뺄셈 |
||
! ~ |
논리 NOT, 비트 NOT |
||
(type) |
C 스타일 캐스팅 |
||
*a |
역참조 |
||
&a |
주소 연산자 |
||
sizeof |
sizeof 연산자 |
||
co_await |
C++20 await 표현식 |
||
new new[] |
동적 메모리 할당 |
||
delete delete[] |
동적 메모리 반환 |
||
4 |
.* ->* |
멤버 포인터 |
왼쪽에서 오른쪽 |
5 |
a*b a/b a%b |
곱셈, 나눗셈, 나머지 |
|
6 |
a+b a−b |
덧셈, 뺄셈 |
|
7 |
<< >> |
비트 쉬프트 연산자 |
|
8 |
<=> |
Three-way 비교 연산자 (C++20) |
|
9 |
< <= > >= |
관계연산자 <, ≤, >, ≥ |
|
10 |
== != |
관계연산자 =, ≠ |
|
11 |
& |
비트 AND |
|
12 |
^ |
비트 XOR |
|
13 |
| |
비트 OR |
|
14 |
&& |
논리곱(AND) |
|
15 |
|| |
논리합(OR) |
|
16 |
a ? b : c |
삼항 조건 연산자 |
오른쪽에서 왼쪽 |
throw |
throw 연산자 |
||
co_yield |
yield 표현식 (C++20) |
||
= |
대입연산자 |
||
+= −= *= /= %= |
복합 대입 연산자 (산술) |
||
<<= >>= &= ^= |= |
복합 대입 연산자(비트) |
||
17 |
, |
쉼표 연산자 |
왼쪽에서 오른쪽 |
참고로, sizeof (int) * p는 (sizeof(int)) * p로 인식됩니다.
삼항 연산자 a ? b : c;에서 b는 연산자 우선순위에 관계없이 괄호 안에 있는 것과 동일한 효과를 받습니다.
즉, a ? (b) : c; 로 인식됩니다.
2-3. 도전 과제
1. 다음 코드에서 출력되는 값은 무엇입니까?
int n = 5;
std::cout << n++ << std::endl;
2. 다음 코드는 경고가 발생합니다. 이를 해결하기 위한 연산자는 무엇입니까?
double d = 6.2458;
int su{d};
3. 다음 코드는 오류가 발생합니다. 이유가 무엇인가요? (HINT: 연산자 우선순위)
std::cout << true || false << std::endl;
4. [고난이도] 비트 연산자를 사용하여 홀수와 짝수를 판별하시오.
int n;
std::cin >> n;
std::cout << (/* 이 부분에 알맞은 식은? */ ? "홀수" : "짝수");
5. [고난이도] 비트 연산자를 사용하여 2의 제곱수를 판별하시오.
int n;
std::cin >> n;
std::cout << ( /* 이 부분에 알맞은 식은? */ ? "2의 제곱수입니다" : "2의 제곱수가 아닙니다");
비트연산자가 상당히 어렵죠? 사실 이 연산자들은 산술만큼 자주 쓰이진 않아요. 안심해도 됩니다.
연산자까지 열심히 학습하시느라 정말 수고 많으셨습니다. 다음 시간에는 조금 쉬어가는 느낌으로 변수의 다양한 초기화 방법에 대해 알아봅시다.
'C++ > 카루의 C++ 강좌' 카테고리의 다른 글
[카루의 C++ 강좌] 2-5. 참조형 변수 I (0) | 2020.03.22 |
---|---|
[카루의 C++ 강좌] 2-4. 변수의 초기화 I (0) | 2020.03.21 |
[C++ 강좌] 2-2. 연산자 I (5) | 2020.03.21 |
[C++ 강좌] 2-1. 변수와 상수, 배열 I (0) | 2020.03.21 |
[카루의 C++ 강좌] 1-6. 전처리기 - include, define (0) | 2020.03.21 |