프로그램은 데이터와 코드로 구성되는데 데이터는 프로그램이 처리하는 재료이고 코드는 데이터를 가공하는 수단이다. 데이터(Data)를 우리말로 번역하면 자료이며 이 데이터를 처리하여 인간에게 유용한 형태로 가공하면 이것이 정보(Information)가 된다. 자료와 정보는 비슷한 것 같지만 많이 다르다. 자료는 불규칙하게 널려 있는 여러 가지 형태의 수치나 문자열이며 정보는 인간이 곧바로 사용할 수 있도록 정리된 유용한 것이다.
예를 들어 성적 처리 프로그램의 경우 각 학생의 과목별 성적은 데이터인데 이 자체로는 별로 유용한 정보를 얻을 수 없다. 이 자료를 가공하여 총점, 평균, 석차를 내면 비로소 학력 평가를 알 수 있는 유용한 정보가 된다. 또한 인터넷에 널려있는 웹 문서들은 그 자체로서는 가치가 없으며 검색 엔진 등에 의해 필요한 것을 추출, 정리해야 비로소 가치있는 정보가 된다. 프로그램이란 사람을 대신해서 이런 일을 하는 소프트웨어이며 프로그램이 자료를 가공하여 정보를 만드는 주된 수단이 바로 이 장의 주제인 연산자다.
연산자(Operator)가 무엇인가는 굳이 문장화하여 정의를 내리지 않더라도 이미 이해하고 있을 것이다. 더하고 빼고 곱하고 나누는 동작을 하는 것들이 바로 연산자이며 실생활에도 늘상 사용하고 있는 것들이다. 물론 소프트웨어 공학적인 측면에서 정의를 내린다면 좀 더 복잡해지겠지만 일단은 여러분들이 상식적으로 이해하고 있는 연산자와 같다고 생각하면 된다.
C는 아주 많은 종류의 연산자를 제공하는데 이런 풍부한 연산자 지원이 C의 큰 장점이기도 하다. 제공하는 연산자가 많다는 것은 그만큼 데이터를 가공할 수 있는 능력이 탁월하다는 뜻이며 이런 연산자들을 자유 자재로 사용할 수 있으면 복잡한 연산을 간단하게 처리할 수 있다. 수가 많으므로 연산자를 종류별로 분류해 보도록 하자. 분류 방법에는 여러 가지가 있지만 일단 연산자의 기능별로 분류하는 것이 가장 일반적이다.
기능별 종류 |
연산자 |
산술 연산자 |
+ - * / % |
부호 연산자 |
+ - |
대입 연산자 |
= 복합 대입 연산자 |
관계 연산자 |
== != <= < >= > |
증감 연산자 |
++ -- |
포인터 연산자 |
* & [] |
구조체 연산자 |
. -> |
논리 연산자 |
|| && ! |
비트 연산자 |
| & ~ >> << |
삼항 조건 연산자 |
? : |
쉼표 연산자 |
, |
sizeof 연산자 |
sizeof |
캐스트 연산자 |
(type) type() |
괄호 연산자 |
() |
C++ 연산자 |
new delete :: .* ->* |
도표를 보면 이 많은 연산자들을 언제 다 익히나 싶을 정도로 양이 많다. 양만 많을 뿐이지 어렵지는 않으므로 이 장을 통해 하나씩 익혀 나가도록 하자. 솔직히 연산자 자체는 결코 어렵다고 할 수 없으나 적재 적소에 상황에 맞는 최적의 연산자를 선정하여 요긴하게 사용하는 응용력을 키우는 것이 어렵다. 연산자 하나를 배울 때마다 데이터를 다룰 수 있는 능력이 향상된다고 생각하면 즐거운 마음으로 공부할 수 있을 것이다. 프로그래머를 무사에 비유한다면 연산자는 칼, 활 같은 무기라고 할 수 있다.
이 도표는 연산자의 기능별로 분류한 것이고 이 외에 피연산자의 개수에 따라 분류하기도 한다. 피연산자란 연산 대상을 말하는데 몇 개의 피연산자를 요구하는가에 따라 단항 연산자(Unary Operator), 이항 연산자(Binary Operator)로 분류한다. a=1+2; 라는 문장에서 +연산자는 1과 2의 합을 계산한다. 양쪽에 두 개의 피 연산자가 있으므로 + 연산자는 이항 연산자이다. = 대입 연산자도 a와 1+2의 결과인 3을 피연산자로 요구하므로 이항 연산자이다. 반면 -3 같은 부호 연산자나 a++같은 연산자는 피연산자가 하나밖에 없으므로 단항 연산자이다. 앞에서 포인터를 소개할 때 잠시 구경해 본 *, & 연산자도 단항 연산자이다.
1. 산술 연산자
산술(Arithmetic) 연산자는 더하고 곱하고 빼고 나누는 가장 기본적이고 또한 가장 많이 사용되는 연산자이다. 초등학교때부터 배웠고 일상 생활에서도 늘상 사용하는 연산자이므로 더 이상의 상세한 설명이 필요치는 않을 것 같다. 연산자는 기호로 표시하는데 더하기와 빼기는 수학에서와 마찬가지로 +, - 기호를 사용한다. 곱하기는 보통 ´ 기호를 사용하지만 알파벳 X와 모양이 동일해서 *를 대신 사용한다. 그리고 나누기는 보통 ¸기호를 사용하는데 이 문자가 키보드에 없기 때문에 / 기호를 사용하여 분수 형태로 표현한다.
기호가 조금 다른 것 빼고는 학교에서 배운 것과 동일하되 다만 나눗셈을 하는 / 연산자에 대해서만 약간의 주의를 하면 된다. 이 연산자는 피연산자의 타입에 따라 연산의 결과가 달라지는 특징이 있다. 피연산자가 모두 정수형이면 결과도 정수형이 되고 피연산자중에 실수형이 있으면 결과도 실수형이 된다.
6/3 // 결과는 2
3.0/2.0 // 결과는 1.5
출력 문자열 앞쪽에 공백이 하나 더 있음을 유의하도록 하자. 이 공백은 이전의 달팽이 꼬리를 지우는 역할을 하는데 없을 경우 잔상이 남게 된다.
산술 연산자에 대해서는 다 알아보았고 산술 연산자와 모양이 똑같은 부호 연산자에 대해 알아보자. 부호 연산자는 피연산자의 부호를 바꾼다. score 변수에 23이라는 값이 들어 있다면 -score는 부호를 바꾸어 -23이 된다. 산술 연산자의 -와 부호 연산자의 -는 모양은 같지만 기능은 다르다. 뺄셈 연산자 -는 피연산자를 두 개 취하는 이항 연산자이고 부호 연산자 -는 피연산자 하나만 취하는 단항 연산자이다. 다음 식을 보자.
a*-b+c-d;
b앞의 -는 부호 연산자이고 d앞의 -는 산술 연산자이다. 어렵지 않게 구분될 것이다. 그럼 다음 대입문에서 -는 어떤 연산자일까?
a=-1;
여기서 사용된 -기호는 언뜻 보기에 부호 연산자인 것 같지만 사실은 연산자가 아니다. -1이라는 상수의 한 부분일 뿐이며 부호를 바꾸는 동작을 하지는 않는다. 부호 연산자 -를 사용하면 변수에 저장되어 있는 값의 음수값을 얻을 수 있다. 다음은 두 개의 정수형 변수 a와 b에 대한 연산예이다.
a+b // a와 b를 더한다.
a+-b; // a와 b의 음수값을 더한다.
a-b; // a에서 b를 뺀다.
a--b; // a에서 b의 음수값을 뺀다.
이 연산문중 앞쪽 세 개는 모두 합법적이다. 그러나 4번째 연산문은 에러로 처리되는데 왜냐하면 --는 감소 연산자라는 단항 연산자로 별도로 정의되어 있기 때문이다. 수학에서 --는 +와 같아지므로 a--b는 사실 a+b와 같지만 C에서는 그렇지 않다. 만약 정 이런 연산을 할 필요가 있다면 a- -b와 같이 공백을 하나 넣어 주든가 아니면 a-(-b)같이 괄호를 싸야 한다.
대입 연산자
대입(Assignment)이란 변수에 어떤 값을 집어 넣는 동작이며 대입 연산자는 변수의 값을 변경할 때 사용한다. = 기호를 사용하며 다음이 가장 간단한 대입문의 예이다.
i=1;
i라는 변수에 1이라는 값을 대입한다. = 연산자는 우변에 있는 값을 좌측으로 대입하는데 위와같이 우변이 상수일 경우는 계산할 것도 없이 상수를 바로 좌변에 대입한다. 우변이 다음과 같은 수식일 경우는 이 수식을 계산한 결과가 좌변으로 대입된다.
i=2+3;
i=j+k*m;
i에 2+3을 더한 5가 대입될 것이다. 이런 상수 수식은 계산 결과가 상수이기 때문에 실행시에 계산되지 않으며 컴파일할 때 컴파일러에 의해 미리 계산된다. 즉 i=2+3;이라고 대입하면 컴파일러는 이 명령을 i=5;로 고쳐서 써 넣으므로 2+3이라고 쓰나 5라고 쓰나 전혀 차이가 없다. 변수가 들어가 있는 수식은 변수의 값에 따라 결과가 달라지므로 실행할 때 계산되어 i로 대입될 것이다.
복합 대입 연산자
복합 대입 연산자는 대입 연산자와 다른 연산자가 결합된 연산자이다. 간단한 사용예를 보자.
a=a+3;
이 수식은 원래 a값에 3을 더해 a에 다시 대입하라는 뜻이며 a를 3만큼 증가시킨다. 변수의 현재값을 기준으로 상대적으로 값을 증감시킬 때 아주 많이 사용하는 식이다. 이처럼 좌변의 변수가 우변의 수식에 포함되어 있을 때는 이것을 복합 대입 연산자로 바꿀 수 있다.
a+=3;
여기서 사용된 +=이라는 연산자가 바로 복합 대입 연산자이며 좌변의 값을 우변만큼 증가시킨다. a=a+3과 a+=3은 완전히 같은 식이되 변수명이 다음과 같이 아주 길다면 이 변수를 두 번 쓰는 것보다는 복합 대입 연산자로 한 번만 쓰는 것이 편리하다.
+= -= *= /= %= <<= >>= &= |= ^=
그러나 아무 연산자나 다 복합 대입 연산자로 만들 수는 없으며 위에 보인 10개가 전부이다. 이런 복합 대입 연산자를 만나면 원래 수식을 떠올려 보면 된다. 복합 대입 연산자를 쓰면 식이 좀 더 간단해지는 반면 몇 가지 부작용이 있기 때문에 복잡한 수식에서는 사용하지 않는 것이 좋다. a+=n 정도의 간단한 수식에만 사용하는 것이 바람직하다.
증감 연산자
증감 연산자는 피연산자를 1씩 증가시키거나 감소시킨다. 루프의 제어 변수처럼 순서대로 어떤 작업을 할 때는 변수값을 하나씩 증감시키는 경우가 많기 때문에 별도의 연산자가 마련되어 있다. 증가 연산자는 ++로 쓰며 감소 연산자는 --를 쓴다. 둘 다 사용 방법은 비슷하므로 ++연산자에 대해서만 집중적으로 연구해 보자. 다음 세 식은 모두 동일하며 a를 1 증가시킨다.
■ 전위형(Prefix) : 증감연산자가 피연산자 앞에 위치한다. ++a, --a
■ 후위형(Postfix) : 증감연산자가 피연산자 뒤에 위치한다. a++, a--
2. 논리 연산자
관계 연산자
관계 연사잔는 피연산자 두개를 취하는 이항연산자이며 좌변과 우변을 비교할 때 쓰이는 연산자이다.
==, !=, >, <, >=, <=
논리 연산자
논리 연산자는 주로 관계 연산자와 함께 사용되며 두 개 이상의 조건식을 결합하여 하나의 진리값을 만들어 낸다. 다음 세 가지 종류가 있다.
연산자 |
뜻 |
설명 |
! |
논리 부정(Not) |
논리식의 진위를 반대로 만든다. |
&& |
논리곱(And) |
두 논리식이 모두 참이어야 참이다. |
|| |
논리합(Or) |
두 논리식 중 하나만 참이면 참이다. |
if ((a > 5 && a < 10) || (b >= 20 && b <= 100) && c != 7) 명령; 연산자 설명 ~ 비트를 반전시킨다. & 대응되는 비트가 모두 1일 때 1이다. | 대응되는 비트가 모두 0일 때 0이다. ^ 두 개의 비트가 달라야 1이다. << 지정한 수만큼 왼쪽으로 비트들을 이동시킨다. >> 지정한 수만큼 오른쪽으로 비트들을 이동시킨다.
비트 연산자
비트 연산자는 논리 연산자와 비슷하지만 비트를 연산 대상으로 한다는 점이 조금 다르다.
쉬프트 연산자
쉬프트(Shift) 연산자는 비트들을 지정한 수만큼 좌우로 이동시킨다. >> 연산자는 오른쪽으로 비트를 이동시키며 << 연산자는 왼쪽으로 비트를 이동시킨다. 연산자의 모양이 이동 방향의 화살표와 비슷하게 되어 있어 직관적이다. a << 1은 a를 1비트 왼쪽으로 이동시키며 a << 3은 3비트 왼쪽으로 이동시킨다. 두 개의 피연산자를 취하는 이항 연산자인데 좌변은 보통 정수형 변수가 오고 우변은 정수 상수가 온다. 물론 양변이 모두 변수(a << b)일 수도 있고 양변이 모두 상수(1 << 3)일 수도 있다.
if (((a & 0x7c00) >> 10) == 2)
a를 0x7c00과 & 연산하여 마스크 오프시키고 10번 오른쪽으로 민 후 이 값이 ㄱ의 코드인 2인지 비교했다. 반대로 초성만 특정한 값으로 바꿀 때는 어떻게 할지 생각해 보기 바란다. 쉬프트 연산은 고속의 그래픽 처리가 필요할 때 비디오 램을 직접 액세스하기 위해서도 많이 사용된다. 비디오 램에 들어 있는 이미지를 쉬프트하면 스크롤될 것이다.
쉬프트 연산의 피연산자는 주로 부호없는 정수형이다. 실수형은 당연히 안된다. 부호있는 정수형은 가능은 하지만 이 경우 동작이 조금 달라진다. 최상위에 있는 부호 비트는 쉬프트 대상에서 제외되는데 부호는 값이 아니기 때문에 유지하는 것이 옳다. 부호있는 정수에 대한 쉬프트 연산은 권장되지 않으며 실제로 의미를 가지는 경우도 드물다.
쉬프트 연산이 곱셈에 비해 불리한 점은 2의 거듭승에 대해서만 곱셈이 가능하다는 점이다. 2배, 4배, 8배, 16배 등만 할 수 있으며 3배, 17배 이런 연산은 할 수 없다. 그러나 쉬프트 연산과 덧셈, 뺄셈을 잘 조합하면 이런 연산이 가능해지기도 한다.
3배 : a << 1 + a;
9배 : a << 3 + a;
15배 : a << 4 - a;
60배 : a << 6 - a << 2;
특히 제일 마지막의 쉬프트 연산으로 60배를 하는 코드는 아주 기발한 응용예이며 감탄을 금하기 어려운 예술 코드라고 할 수 있다. 정밀하게 측정해 보면 이런 연산들이 곱셈보다 수배~수십배 더 빠르다. 보통의 경우라면 일반적인 곱셈을 하는 것이 소스를 유지하기에 편리하지만 속도가 지극히 중요하다면 곱셈보다는 가급적이면 쉬프트 연산을 사용하는 것이 좋다.
회전 연산자
회전(Rotate) 연산은 쉬프트 연산과 유사한 비트 조작 명령이다. 쉬프트는 비트를 선형으로 이동시키는데 비해 회전 연산은 원형으로 이동시킨다. 비트 이동에 의해 밀려나는 비트는 버려지지 않고 반대쪽으로 다시 이동된다는 것이 특징이다. 회전 연산은 쉬프트 연산에 비해 많이 사용되지 않기 때문에 연산자 형태로는 제공되지 않으며 _rotl, _rotr 함수로 제공된다. C 수준에서는 사용할 일이 그리 많지 않다. 과거 도트 프린터나 잉크젯 프린터의 경우 헤더가 수직으로 배열되어 있으므로 한 비트씩 추출하여 한 줄을 만들었는데 이럴 때 회전 연산이 사용되었다.
3. 기타 연산자
삼항조건 연산자
삼항 조건 연산자는 특이하게도 피연산자를 세 개나 가지는데 다른 언어에는 없는 C언어의 독특한 연산자이다. 기본 형식은 다음과 같다.
(조건식) ? 값1:값2
? 앞에 조건식이 있고 ? 뒤에 :을 사이에 두고 두 개의 값이 온다. ?와 :은 한 연산자를 구성하는 짝이기 때문에 반드시 같이 와야 하며 단독으로 사용할 수는 없다. if else, do while처럼 짝을 이루어 사용되는 연산자이다. 이 연산자는 조건식을 평가해 보고 참이면 값1을 리턴하고 거짓이면 값2를 리턴한다. 조건식 자리에는 보통 변수의 값을 비교하는 관계 연산문이 오지만 조건식으로 사용될 수 있는 식이면 어떤 것이든지 가능하다. 변수나 상수, 함수호출문 등 논리값을 리턴하는 모든 식이 올 수 있다. 조건식을 감싸는 괄호는 반드시 필요한 것은 아니나 괄호가 있는 것이 보기에 좋고 안정감이 있어 보인다.
printf("%d는 %s수입니다.\n",i,i%2==0 ? "짝":"홀");
쉼표 연산자
쉼표 연산자는 쉼표 기호(,)를 사용하는데 모양만으로 보면 구두점같이 생겨서 연산자가 아닌 것처럼 보이기도 한다. 하지만 분명히 연산자이다. 피연산자로 양쪽에 두 개의 표현식을 취하며 좌변을 먼저 평가하고 우변을 평가한 후 우변의 연산 결과를 리턴한다. 쉼표 연산자는 어떤 연산을 한다기보다는 두 연산식을 하나로 묶는 역할만 한다. 이 연산자를 사용하면 두 개의 표현식을 하나로 합칠 수 있다.
j=i=3,i+2;
for ({ i=1;j=2; };i<5;{ i++;j+=2; }) {
sizeof 연산자
다른 연산자들은 모두 +, -, && 같은 기호로 표현하는데 sizeof 연산자는 단어로 되어 있어 조금 특이해 보인다. 이 연산자는 피연산자로 주어진 타입 또는 변수의 크기를 계산한다. 기본 형식은 다음과 같다.
sizeof(타입 또는 변수)
피연산자로 int, double같은 타입을 쓸 수도 있고 변수를 쓸 수도 있으며 상수를 사용할 수도 있다. 아뭏든 괄호안에 있는 대상이 메모리를 얼마나 차지하고 있는지 계산한다.
캐스트 연산자
캐스트 연산자는 수식내에서 변수의 타입을 강제로 다른 타입으로 바꾼다. 별다른 지정이 없으면 변수의 고유한 타입대로 연산이 수행되는데 가끔 타입을 바꿔서 연산해야 할 경우가 있다. 캐스트 연산자의 형식은 다음 두 가지가 있다.
(타입)변수
타입(변수)
전자는 C언어의 캐스트 연산자 형식이며 후자는 C++언어에서 새로 추가된 캐스트 연산자 형식이다. C++ 컴파일러에서는 두 형식 모두 사용할 수 있는데 C++ 형식이 함수 호출문과 유사해서 더 명시적이고 가독성이 좋다. 하지만 C 형식도 기능상 특별한 문제가 없고 오랫동안 사용해 왔기 때문에 아직까지도 C++형식보다는 C형식이 더 많이 사용된다. 둘 중 어떤 형식을 사용할 것인가는 쓰는 사람의 자유이다.
r=(double)i/j;
4. 연산 규칙
연산 순서
순위 |
연산자 |
결합순서 |
1 |
( ) [ ] -> . |
왼쪽 우선 |
2 |
! ~ ++ -- + -(부호) *(포인터) & sizeof 캐스트 |
오른쪽 우선 |
3 |
*(곱셈) / % |
왼쪽 우선 |
4 |
+ -(덧셈, 뺄셈) |
왼쪽 우선 |
5 |
<< >> |
왼쪽 우선 |
6 |
< <= > >= |
왼쪽 우선 |
7 |
== != |
왼쪽 우선 |
8 |
& |
왼쪽 우선 |
9 |
^ |
왼쪽 우선 |
10 |
| |
왼쪽 우선 |
11 |
&& |
왼쪽 우선 |
12 |
|| |
왼쪽 우선 |
13 |
? : |
오른쪽 우선 |
14 |
= 복합대입 |
오른쪽 우선 |
15 |
, |
왼쪽 우선 |
결합 순서
결합 순서는 수식내에 같은 종류의 연산자가 있을 때 어떤 방향의 연산을 먼저 수행할 것인가를 지정한다. 연산 순위는 다른 종류의 연산자에 대한 실행 순서인 반면 결합 순서는 같은 연산자(또는 같은 순위내의 다른 연산자)의 실행 순서를 지정한다.
대부분의 이항 연산자들은 왼쪽 우선 순위를 가지기 때문에 수식에 등장하는 순서대로 실행된다. a=b+c+d; 연산문은 b와 c를 먼저 더하고 그 결과와 d를 다시 더하는데 덧셈은 교환 법칙이 성립하므로 사실 결합 순서가 큰 의미가 없다. 앞서 실습한 바 있는 다음 대입문을 보자.
a=b=c=3;
이 대입문은 a, b, c 모두 3을 대입하는데 대입 연산자는 오른쪽 우선이다. 즉 제일 오른쪽에 있는 c=3이 가장 먼저 실행되고 차례대로 b=c, a=b가 대입된다. 만약 대입 연산자가 왼쪽 우선 순위를 가지게 되면 a=b, b=c, c=3 순서대로 실행되어 a는 b의 쓰레기값을 가질 것이고 b는 c의 쓰레기값을 가지며 결국 3이 되는 것은 c밖에 없을 것이다.
대입 연산자가 오른쪽 우선의 결합 순서를 가지므로 복합 대입 연산자들도 모두 오른쪽 우선으로 되어 있다. 그외 단항 연산자들은 모두 오른쪽 우선 순위를 가진다. 다음 캐스트 연산문을 보자.
(double)(unsigned)i;
(unsigned)가 먼저 실행되어 i의 부호를 없앤 후 (double)이 실행되어 실수 타입으로 바꾼다.
출처: http://winapi.co.kr/