본문 바로가기

컴퓨터공학/C, C++

C,C++> 조건문 제어문 연산자

자잘한 건 생략하고 핵심 중점으로 고고. 

 

제어문 비교 

for문 : 제어 변수를 사용하는 특징. for문은 이미 반복 횟수가 정해져 있다. 루프 중간에 탈출하는 경우는 별로 없다. 변수 변화를 쉽게 파악하고 변경할 수 있다. 따라서 미리 정해진 횟수만큼 반복할 때 사용하면 편하다. 

 

while문 : 제어변수 개념이 없어서 루프 내부에서 진위 여부를 변경해야 한다. 그래서 반복 횟수가 가변. 네트워크 변화, 특정 신호 입력 등 언제 발생할 지 모르는 조건에서 반복할 때 쓰는 것이 적합하다. 

 

do~while문 : while문과 비슷하지만 조건 점검 시기가 다르다. while문은 로프로 들어가기 전에 조건을 점검하지만 do while문은 일단 실행한 후 루프 계속 여부를 점검한다. 따라서 do while문은 최소 한 번은 실행하는 차이점이 있다. while문은 선평가 후 실행문, do while문은 선실행 후 평가문이다. 

 

switch 문 :

case 다음엔 반드시 정수 상수 사용. 문자형과 열거형은 정수형과 호환되므로 사용 가능.

실수, 사용자 정의형, 변수, 범위 지정 불가능.

변수값끼리 비교하거나 일정 범위에 있는 지 보려면 if문을 사용한다.

 

break 

여러 개 루프가 중첩되어 있는 다중 루프에서 break문이 사용되면 현재 루프 하나만 탈출한다. 

 

산술연술자

피연산자 타입에 따라서 연산 결과가 달라진다.

피연산자 모두 정수형이면 결과도 정수형.

피연산자 중 실수형이 있으면 결과도 실수형.

쉬운 내용이지만 놓칠 수 있는 내용이다. 

또 까먹기 쉬운 내용은 %는 나머지, /는 몫이다.

 

대입연산자

대입연산자 우변에는 상수, 변수와 연산자들로 구성된 계산할 수 있는 표현식, 함수 호출문이 올 수 있다.

즉 하나의 결과를 낼 수만 있다면 어떤 표현식이 와도 상관없다. 

대입연산자 좌변에는 좌변값만 올 수 있다. 

 

좌변값

정확한 정의는 실제 메모리를 점유하고 있고 그 값을 변경할 수 있는 대상이라는 뜻이다. 

변수는 좌변값이지만 변수로 구성된 수식은 좌변값이 아니다.

1 = 2+3 

여기서 상수 1은 실제 메모리를 점유하고 있지 않으며 값이 고정되어 변경할 수 없으므로 좌변값이 아니다. 

char str[12];

str="STRING";

문자 배열은 문자열을 저장할 수 있지만 이런 식으로 대입 불가능

왜냐하면 배열 이름 str은 좌변값이 아니기 때문이다. 

배열 요소는 좌변값. 배열은 좌변값이 아니다. 

 

연산자 리턴값

모든 연산자는 연산 후 결과를 돌려주는데 리턴값이라고 한다.

원래 함수 계산 결과를 리턴값이라고 하는데 연산자도 일종의 함수라서 연산 결과를 리턴값이라 한다. 

 

증감 연산자

전위형(Prefix)과 후위형(Postfix)이 있다. 

전위형은 일단 값을 증감시키고 결과를 리턴한다.

후위형은 값을 먼저 리턴하고 증감시킨다. 

 

증감 연산자는 피연산자 값을 변경하는 점에서 대입 연산자와 비슷하다. 

a++이 a=a+1과 같으므로 증감 연산자도 대입 연산자와 마찬가지로 좌변값만 피연산자로 쓸 수 있다. 

즉 증가할 수 있는 대상만 제대로 작동한다. 

1++처럼 상수를 증가하는 건 안되며 (a+1)++도 사용할 수 없다. 

왜냐하면 1과 (a+1)은 좌변값이 아니기 때문이다. 

또 다른 예는  a++++ 이다.. 

a는 변수이고 좌변값이지만 a를 1 증가한 결과는 정수 상수이지 더 이상 좌변값이 아니다. 

++연산자를 전위형으로 사용하여 ++++a는 가능하다.

 

어떤 값에 1을 더해 다시 대입하는 a=a+1 보다 값을 1 증가시키는 a++이 속도가 더 빠르고 메모리를 적게 차지한다.

그러나 C언어에서는 별 차이가 없는데 왜냐하면 컴파일러가 a=a+1을 a++로 바꿔서 컴파일하기 때문이다. 

 

인수 전달 순서

증감연산자는 편리하지만 부작용이 생길 수 있다. 

주로 복잡한 수식 내 증감연산자를 사용할 때 문제가 생긴다. 

int i = 3;

printf("%d, %d\n", i, i++);

이런 예시가 있다면 3, 4 출력을 예상하겠지만 실제 결과는 4, 4로 출력된다. 

 왜냐하면 c는 함수 인수를 뒤에서부터 전달하기 때문이다. 

i++가 먼저 실행되어 그 결과가 전달하고 한 번 더 전달한다. 

C 표준에 인수 전달 순서는 정해져 있지 않다. (Unspecified)

대부분 컴파일러는 뒤 부터 평가하는데 왜냐하면 가변 인수를 다루기 편하기 때문이다. 

이 문제는 호출 규약(Calling Convention)과 관련된 문제이다. 

컴파일러마다 평가 순서도 달라질 수 있어서 이런 수식에서 증감 연산자를 사용하면 이식성에 불리하다.

이런 문제를 신경쓰고 싶지 않다면 증감 연산자를 쓰지 않으면 된다. 

 

논리 연산자

관계연산자

주의점은 좌변과 우변의 데이터 타입이 일치해야 한다는 점만 주의.

관계 연산자는 수식을 평가하고 결과가 참이면 1(true)를 반환하고 거짓이면 0(false)를 반환한다. 

C에서 숫자 0은 거짓을 표현하고 0이 아닌 모든 값은 참으로 표현한다. 

if문 조건문은 참일 때 실행한다고 했는데 좀 더 정확하게 동작을 정의하자면 0이 아닐 때 명령을 실행한다고 할 수 있다. 

==는 교환법칙이 성립한다. 

 

논리 연산자

if(a == 1) 명령;

if(a != 1) 명령;

두 명령문은 같은 의미이다. 같은 의미이지만  

논리식이 여러 개 연결되어 있거나 수식이 복잡하면 논리식 역을 구하기 어렵기 때문에 원래 조건문을 모두 괄호로 치고 앞에 ! 연산자를 쓰는게 더 쉽다. 

 

Short Circuit

논리 연산자는 속도 향상과 안전을 위해 불필요한 연산은 하지 않는다. 

예를 들어 a > 5 && a < 10  조건식이 있을 경우 a가 2라고 한다면 이 값은 5보다 크지 않으므로 거짓이 되고 좌변 조건식이 거짓이니 우변 진위와 상관없이 전체는 거짓으로 결정난다. 따라서 우변을 평가할 필요가 없다. 

이렇게 컴파일러는 이미 결정난 값에 불필요한 연산을 하지 않아 실행 속도를 높인다. 

컴파일러의 이런 기능을 Short Circuit라고 한다. 

지원하는 컴파일러도 있고 그렇기 않은 컴파일러도 있다. 

조건문 평가 하나에 무슨 차이가 있을까 싶지만 함수 호출문일 경우 차이가 생길 수 있다. 

게다가 코드 안정성을 높일 수 있는데 다음 예를 보자.

if (a != 0 && b/a == 3) 명령;

a가 0이라면 첫 번째 조건식이 벌써 거짓이 되므로 쇼트 서키트에 의해 우변 조건식은 평가하지 않는다. 

만약 쇼트 서키트가 없으면 a가 0인 상태에서 b/a 연산을 할 것이고 이는 에러를 발생시킨다. 

왜냐하면 0으로 나눌 수 없기 때문이다.

쇼트 서키트가 없으면 다음과 같은 조건문으로 고쳐 쓸 수 밖에 없다.

if(a != 0){

if(b/a == 3) 명령; }

이 기능은 컴파일러마다 다르므로 신경쓸 필요는 없지만 알고 있으면 유리한 경우가 있으므로 기억하자. 

쇼트 서키트 기능으로 두 조건을 논리 연산자로 연결할 때 가급적 쉽게 조사할 수 있는 조건은 앞쪽에 두는 것이 유리하다. 

또 반드니 실행해야할 조건도 앞 쪽에 배치해야 한다. 

 

비트 연산자

비트 연산자는 논리 연산자와 비슷하지만 비트를 연산 대상으로 한다는 점이 조금 다르다. 

과거 프로그램은 비디오 메모리를 직접 접근할 때 비트 연산이 굉장히 중요했고 섬세한 처리할 때 꼭 사용한 중요한 연산이었다. 비트를 조작하여 반전, 스크롤, 투명 처리 등이 가능했고 일반적 산술 연산보다 훨씬 더 빠른 속도로 복잡한 연산을 할 수 있었다. 그러나 윈도우 환경에서 비디오 메모리를 직접 접근하는 것이 금지되었고 그럴 필요도 없기 때문에 요즘은 비트 연산자를 많이 사용하지 않는다.

하지만 게임이나 시스템 소프트웨어에서는 아직 비트 연산이 필요하며 활용 범위가 넓다. 

 

비트 연산자에서 &은 마스크 오프 연산할 때 사용하고 | 은 마스크 온 연산할 때 사용한다. 

마스크 오프는 특정 비트만 남기고 나머지는 0으로 만들어 버리는 연산을 말한다.

마스크 온은 특정 비트를 강제로 1로 만드는 연산을 말한다. 

& 과 | 연산자는 주로 일부 비트만 제한적으로 읽거나 변경할 때 흔히 사용한다. 

기억 공간 절약하기 위해 하나의 정수값을 비트 별로 잘라 여러 가지 값을 같이 기억시키는 방법이 많이 사용된다. 

 

예를 들어 한글 조합형 코드가 16비트 길이를 가지는 이유는 이 때문이다. 

한글 16비트에서 최상위 비트는 항상 1인데 이 코드는 한글임을 표시한다. 

영문 알파벳은 128보다 작기 때문에 이 비트가 0으로 되어 있어 한글과 구분된다. 

16비트 정수값을 5비트씩 잘라서 초성, 중성, 종성 코드를 기억한다. 

한글 낱글자 총 갯수는 32개가 안 되기 때문에 5비트면 낱글자 하나를 기억할 수 있고, 세 글자가 모이면 한글 1음절을 표현할 수 있다. 초성, 중성, 종성 코드를 각각 정수에 기억하는 방법에 비해 훨씬 기억 공간이 절약된다. 

조합 값에서 일부만 추출하거나 변경하려면 &, | 비트 연산이 필요하다. 

 

쉬프트 연산자 

쉬프트 연산 x만큼 한다는 것은 2의 x승을 한다는 뜻이다. 음수일 때는 2를 나누는 것과 같다. 

그래서 곱셈 대신 쉬프트 연산을 할 수 있는데 차이점은 속도 차이가 있다.

비트를 이동하는 것과 일정 횟수 반복하는 것은 다른 작업이기 때문이다. 

쉬프트 연산은 논리적이지 않고 기계적이며 기계가 하기에 아주 쉬운 연산이다.

속도차는 무려 10배나 차이가 난다. 

따라서 곱셈을 할 지 쉬프트 연산을 할 지에 따라서 프로그램 성능에 차이가 난다. 

단점은 쉬프트 연산은 오직 2의 거듭승 곱셈만 가능하다는 점이다. 

따라서 쉬프트 연산과 덧셈, 뺄셈을 잘 조합해서 사용하면 된다.

 

회전 연산

쉬프트 연산과 유사한데 쉬프트는 비트를 선형으로 이동하지만 회전 연산은 원형으로 이동한다. 

비트 이동에 밀려나는 비트는 버려지지 않고 반대쪽으로 다시 이동한다. 

연산자 형태가 아니라 함수로 제공된다. 

 

 

쉼표 연산자는 { } 도움 없이 두 개 이상 문장을 하나로 묶어야 할 때와 for문에서 제어 변수 두 개를 쓸 때 사용한다. 

 

sizeof 연산자

배열 전체 크기를 배열 요소 크기로 나누면 배열 요소 개수가 된다. 

arsize = sizeof(array) / sizeof(array[0])

배열은 암산이 되어서 크기를 구하기 쉽지만 구조체는 각 멤버 크기 총합을 구해야 해서 직접 크기를 계산하는 건 귀찮고 힘든 일이다. 

게다가 구조체는 정렬방식이라는 컴파일러 옵션에 따라서 달라질 수 있기 때문에 직접 계산하면 틀릴 수 있다.

따라서 이럴 경우 sizeof 연산자를 이용한다. 

 

캐스트 연산자

캐스트 연산자는 수식 내 변수 타입을 임시적으로 바꾸는 것이지 변수 타입 자체를 바꾸는 건 아니다. 

 

연산 순위

C 연산자 일부는 실생활에서 사용하는 것도 아니고 익숙하지 않아서 가끔 실수를 할 때가 있다.

짧은 코드는 어디가 잘못되었는지 바로 알 수 있지만 크기가 큰 프로그램은 틀린 연산 효과가 곧바로 나타는 것도 아니라서 찾기가 힘들다. 연산 우선 순위는 항상 주의해야 한다. 우선 순위표를 보거나 의심이 가는 식은 괄호를 싸면 된다. 

 

결합 순서

결합 순서는 수식 내 같은 종류 연산자가 있을 때 어떤 방향 연산을 먼저 수행할 것인가를 지정한다.

즉, 연산 순위는 다른 종류 연산자에 대한 실행 순서이고, 결합순서는 같은 연산자 또는 같은 순위 내 다른 연산자의 실행 순서를 지정한다. 

대부분 이항 연산자는 왼쪽 우선 순위를 가진다. 예를 들면 a = b+ c+ d;

대입문은 오른쪽 우선이다. a = b = c = 3;

왜냐하면 만약 왼쪽 우선 순위라면 a=b, b=c, c=3 순서대로 실행되는데 a는 b의 쓰레기 값을 가질 것이고 b는 c의 쓰레기값을 가지게 되어서 결국 3이 되는 건 c 밖에 없다.

복합 대입 연산자들도 모두 오른쪽 우선이다.

 

산술 변환

1. 이항 연산 시 양변의 타입이 다르면 큰 쪽으로 상승 변환한다. 

2. 대입 연산 시 좌변의 타입에 따른다. 값 대입을 받는 변수 능력치를초과할 수 없어서 대입하는 값이 변수보다 크면 잘라낸다. 하강 변환.

3. 함수 호출 시 실인수와 형식인수 타입이 다르면 형식인수 타입을 따라간다. 

4. 수식 내 사용될 경우 char, unsigned char,enum 형은 int형으로 자동 확장되며 float 형은 double형으로 확장된다. 

예를 들어 short a = 20000, b = 30000일 경우, int c = a + b 결과는 50000이 되지 않는다.

왜냐하면 short끼리 더한 결과가 short가 되면 연산 중에 오버플로우가 발생하여 c가 아무리 int라도 50000결과를 받을 수 없다. 

따라서 수식 내에서 타입 확장을 먼저하고 연산해야 한다. 

 

부호 확장은 부호 있는 작은 타입이 큰 타입으로 확장할 때 발생한다. 

양수일 때 0x01이 0x0001 된다. 음수일 땐, 0xff가 0xffff가 된다.  

 

 


 

문법적으로 아무 문제 없고, 따져 보면 결과 예측할 수 있을 지 모른다.

하지만 수식이 불안해 보이고,

어떤 의도로 작성한 코드인지 한 눈에 알아보기 어렵고,

컴파일마다 평가 방법이 달라서 이식성에 좋지 않다면  지양해야 하는 코드이다. 

현란하고 복잡한 코드가 많다고 실력 좋은 프로그래머라고 할 수 없다. 

오히려 호기심이 많은 철부지 초보일수록 현란한 코드를 써서 부족한 실력을 감추려고 한다. 

경험 있는 개발자는 결코 어려운 코드를 쓰지 않는다. 

 

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

C,C++> 배열  (0) 2022.06.09
C,C++> 기억 부류  (0) 2022.06.08
C,C++> 함수  (0) 2022.06.06
C,C++> 변수 이야기  (0) 2022.05.25
C, C++ 공부 프롤로그  (0) 2022.05.08