본문 바로가기
Language

[Programming Language] 4. 표현식과 배정문

by 삼준 2023. 7. 15.
반응형

직전글

2023.07.14 - [Language] - [Programming Language] 3. 자료형 (3)

● 산술식

산술식의 자동적인 계산은 고급 프로그래밍 언어의 주요 목적 중 하나임.

- 산술식 구성 요소

연산자, 피연산자, 괄호, 함수 호출로 구성됨.

 

ㄴ 연산자 종류 (피연산자 개수에 따라 구분)

1. 단항(Unary) - 피연산자가 한 개

2. 이항(Binary) - 두 개

3. 삼항(Ternary) - 세 개

 

ㄴ 이항 연산자 표기법

대부분의 명령형 프로그래밍 언어에서 중위 표기법(Infix)을 사용함.

예외적으로 Perl과 같은 언어에서는 전위 표기법(Prefix)을 사용함.

 

ㄴ 연산자 평가 순서

연산자 우선순위와 결합규칙에 따라 평가 순서가 결정됨.

 

(1) 우선순위

연산자 우선순위 규칙은 서로 다른 우선순위 수준을 갖는 연산자들이 평가되는 순서를 부분적으로 정의함.

+ 덧셈과 뺄셈의 단항 버전

많은 언어들에서 지원함.

단항 덧셈을 항등 연산자라 부름. 어떤 영향도 주지 않기 때문임 (ex. +3 == 3)

단항 뺄셈은 그 연산자의 부호를 변경하는 연산자임. 단, 괄호 사용이 필요함. (ex. a + (-b) * c)

+ 우선순위의 중요성

- x ** y 와 같은 식은 연산자 간의 상대적 우선순위에 따라 결과가 달라지기 때문에 중요함.

따라서 언어들마다 산술 연산자의 우선순위를 정의하고 있음.

 

(2) 결합 규칙

식의 연산자들이 동일한 수준의 우선순위를 가지면서, 인접한 두 개의 연산자를 포함할 때, 어느 연산자가 먼저 평가될 것인가에 대한 규칙을 의미함.

+ 공통된 언어들의 결합 규칙

왼쪽부터 오른쪽 순서. 지수 연산자는 예외적으로 오른쪽에서 왼쪽 순서로 진행됨.

ex. Python 에서, 10 ** 2 ** 3 == 10 ** ( 2 ** 3 )

 

(3) 괄호

프로그래머가 식에 괄호를 포함시켜서 우선순위 규칙과 결합 규칙을 변경할 수 있음.

 

(4) 조건식

if-then-else 문이 조건식 배정문을 수행하는데 사용될 수 있음.

if (count == 0) average = 0;
else average = sum / count;

C, Java 에서는 삼항 연산자 "?"로 동일한 조건식을 표현 가능함.

average = (count == 0) ? 0 : sum / count;

Python의 경우 다음과 같이 조건식을 사용 가능함.

average = 0 if (count == 0) else sum / count

 

ㄴ 피연산자 평가 순서

어떤 피연산자도 부작용을 갖지 않으면, 피연산자의 평가 순서는 무관함.

따라서, 피연산자의 평가가 부작용을 가지는 경우에 대한 고려가 필요함.

 

(1) 부작용(Side Effects)

함수의 부작용 : 함수의 부작용은 함수가 호출되었을 때 함수 외부에 영향을 주거나 예상치 못한 동작이 발생하는 것으로,함수가 자신의 매개변수들 중의 하나의 값이나 전역 변수의 값을 변경할 때 발생함.
+ 예제 식 : a + fun(a)
fun()이 a를 변경하는 부작용을 갖지 않으면, 두 피연산자 a와 fun(a)의 평가의 순서는 식의 값에 영향을 미치지 않으나, fun()이 매개변수 a의 값을 20으로 변경하고 10을 반환하는 경우,

a = 10;
b = a + fun(a);

위와 같은 식에서 a의 값이 먼저 인출되면, b의 값 계산에서 a의 값은 10이 되고, 결국 b는 20이 됨.
그러나, fun(a)가 먼저 평가되면, b의 값 계산에서 a의 값은 20이 되고, 결국 b는 30이 됨.

+ C 언어 프로그램 예 (전역변수 변경)

int a = 5;
int fun1() {
	a = 17;
	return 3;
}
void main() {
	a = a + fun1();
}

main()에서 계산된 a의 값은 피연산자들의 평가 순서에 의존적임.
a가 먼저 평가되면 a의 결과 값은 8이지만, fun1()이 먼저 평가되면 a의 결과 값은 20임.

 

(2) 부작용 해결책 2가지

+ 함수적 부작용 불허 방법

수학에서의 함수는 부작용을 가지지 않음. (수학에는 변수의 개념이 없기 때문)
따라서, 함수형 프로그래밍 언어에서는 함수적 부작용이 발생하지 않음.
그러나, 명령형 언어에서 함수적 부작용을 불허하는 것은 어려우며 (변수가 존재하기 때문), 단지 하나의 값만을 반환하는 C 언어와 같은 경우에는 프로그래머의 유연성을 없애는 요인이 됨. 함수의 결과로 여러 개의 값을 반환 (값을 바꾸기 위하여)하는 경우, 구조체를 사용하는 것 외에는 방법이 없어짐. 그래서 Go 언어에서는 함수가 여러 개의 값을 반환하는 것을 지원함.

 

+ 엄격한 평가 순서 정의 방법

컴파일러에 사용되는 어떤 코드 최적화 기법들은 피연산자 평가 순서를 재순서화함.
엄격한 평가 순서 정의는 최적화 기법의 사용을 불허해야 함.
따라서, 실제 언어 설계에서 사용하기는 어려움. (Java는 피연산자들이 왼쪽에서 오른쪽 순서로 평가되는 것을 확정함.)

 

(3) 참조 투명성과 부작용(Referential Transparency and Side Effects)

+ 참조 투명성

프로그램에서 동일한 값을 갖는 임의의 두 개의 식이, 프로그램의 행동에 영향을 미치지 않으면서(== 투명하게) 프로그램의 임의의 위치에서 서로 다른 식으로 대체될 수 있다면 그 프로그램은 참조 투명성의 특징을 가짐.

 

+ 참조 투명과 함수적 부작용 간의 관계 예시

// 참조 투명 X
result1 = (fun(a) + b) / (fun(a) – c);

// 참조 투명 O
temp = fun(a);
result2 = (temp + b) / (temp – c);

함수 fun()이 부작용을 갖지 않으면 result1과 result2의 값은 같지만, fun()이 b나 c의 값을 변화시키는 부작용을 가지고 있는 경우 result1과 result2의 값은 달라질 수 있음.

이런 부작용은 그 코드가 나타내는 프로그램이 참조 투명성을 가지지 못하게 함.
※ 순수 함수형 언어들은 변수를 가지지 않기 때문에, 작성된 프로그램들을 참조 투명성을 가짐

● 중복 연산자(Overloaded Operations)

산술 연산자는 한 가지 이상의 목적으로 사용됨. 예를 들어, ‘+’ 연산자는 정수 덧셈과 부동-소수점 덧셈에 사용됨. 또한, Java와 같은 언어에서, ‘+’ 연산자는 스트링 접합에도 사용됨.

한 연산자의 이러한 다중 사용을 연산자 중복(Operator Overloading)이라 부름.

- 연산자 중복이 갖는 위험 예 (C, C++언어)

이항 연산자로서 ‘&’는 비트 단위 논리 AND 연산자이고, 단항 연산자로서 ‘&’는 피연산자의 주소를 얻어내는 연산자임. (a.k.a. 주소 연산자)


ㄴ 두 가지 문제
1. 완전히 관련이 없는 두 개의 연산에 대해 동일한 기호를 사용하는 것은 판독성에 유해함.
2. 비트 단위 AND 연산에서 첫 번째 피연산자를 빠뜨리는 단순한 입력 오류가 컴파일러에 의해 탐지되지 않아 진단하기 어려움.

- 사용자 정의 중복(User-defined Overloading)

추상 데이터 타입(Abstract Data Types - 이후 나올 예정)을 지원하는 언어들 중에 C++, C# 등은 프로그래머가
연산자 기호를 중복하는 것을 허용함. 예를 들어, 행렬의 곱을 ‘*’ 연산자로, 행렬의 합을 ‘+로 정의할 수 있음.

하지만 이런 사용자-정의 중복은 판독성에 해로울 수 있음. 예를 들어, 사용자가 ‘+’를 곱셈을 의미하는 것으로 정의하는 것을 막을 수 없음.

연산자 중복은 Java에서는 도입되지 않았으나, 그 후에 나온 C#에서는 다시 도입됨.

● 형 변환(Type Conversions)

형 변환은 축소적이거나 확장적임. 또한 명시적 (explicit)이거나 묵시적 (implicit)일 수 있음

- 축소 변환 (narrowing conversion)

값을 그 원래 타입의 모든 값들의 근사치 값으로조차 저장할 수 없는 타입으로 변환하는 것.
Ex. Java에서 double을 float로 변환하는 것 (double은 8바이트, float은 4바이트임)
축소 변환은 항상 안전하지 않음.

- 확장 변환 (widening conversion)

값을 적어도 그 원래 타입의 모든 값들의 근사치를 포함할 수 있는 타입으로 변환하는 것.
Ex. Java에서 int을 float로 변환하는 것
확장 변환은 거의 안전함.

- 강제 형 변환

ㄴ 혼합형 식

하나의 연산자가 서로 다른 타입의 피연산자를 가지는 식. 묵시적 피연산자 타입 변환을 필요로 함.

묵시적 타입 변환은 컴파일러에 의해 수행됨.

대부분의 언어에서 혼합형 산술식에 대한 제약 없음.

 

ㄴ 강제 변환으로 초래될 수 있는 신뢰성 문제

//Java
int a;
float b, c, d;
...
d = b * c;

/*
타이핑 오류로 c를 a로 입력한 경우
*/

int a;
float b, c, d;
...
d = b * a;

Java에서 혼합형 식을 허용하기 때문에, 컴파일러는 이를 오류로 탐지하지 않음.
컴파일러는 단순히 int 피연산자 a의 값을 float로 강제 변환하는 코드를 삽입할 것임.
만약, 혼합형 식을 허용하지 않으면, 이런 입력 오류는 컴파일러에서 타입 오류로 탐지 가능해짐.

 

+ C, Java에서 int 보다 더 크기가 작은 타입들의 연산 예

byte a, b, c;
...
a = b + c;

변수 b와 c의 값은 int 타입으로 강제 변환된 뒤, int 덧셈이 수행됨.
그 다음, 그 합이 byte 타입으로 변환되어 a에 저장됨.

위와 같은 코드는 메모리 절약이 가능함.
오늘날은 컴퓨터의 메모리 용량이 크므로, 이와 같은 프로그램의 이점이 거의 없음. (임베디드 시스템에 사용되는 C 프로그램의 경우는 예외)

- 명시적 형 변환

대부분의 언어에서 확장 변환과 축소 변환 모두에 대하여 명시적 타입 변환 기능을 제공함. 일부 경우, 명시적 축소 변환에 대하여 경고 메시지 생성 가능.
C 언어, Java 등에서는 명시적 타입 변환을 캐스트(Cast)라고 부름.

i = (int) angle;
ptr = (char *)malloc(100);

● 관계식과 불리안 식(Relational and Boolean Expressions)

프로그래밍 언어는 산술식 말고도 관계식과 불리안(or 부울) 식을 지원함.

- 관계식

관계식은 두 개의 피연산자와 한 개의 관계 연산자로 구성됨. 관계 연산자는 두 개의 피연산자의 값을 비교하는 연산자를 의미함.
관계식의 (결과) 값은 불리안 값임 (예외 : 불리안 타입이 존재하지 않는 C 언어 등에서는 수치 값)


ㄴ 동등과 비동등에 대한 관계 연산자
프로그래밍 언어에 따라 다름.
비동등에 대한 예 : C → "!="   /   Ada → "/="   /   Lua → "~=" 등


ㄴ 새로운 관계 연산자 추가 예
JavaScript와 PHP는 "==="와 "!=="를 추가로 지원함.
[ "7" == 7 ] == 참, 왜냐하면 스트링을 수치로 강제 변환한 뒤 비교하기 때문
[ "7" === 7 ] == 거짓, 강제 변환을 허용하지 않기 때문

 

ㄴ 관계 연산자는 항상 산술 연산자보다 더 낮은 우선 순위를 가짐.
[ a + 1 > 2 * b ] 산술식이 먼저 평가됨 (괄호를 사용하는 습관도 좋음)

- 불리안 식

불리안 식은 (1) 불리안 변수, (2) 불리안 상수, (3) 관계식, (4) 불리안 연산자들로 구성됨

ㄴ 불리안 연산자

AND, OR, NOT, XOR, 동등 연산의 역할을 하는 연산자

불리안 대수의 수학에서, OR와 AND 연산자는 동일한 우선순위를 가져야 함. Ada에서는 AND와 OR가 동일한 우선 순위를 가지만, C, Java 등에서는 OR보다 AND가 높은 우선 순위를 가짐. (AND를 곱셈, OR를 덧셈으로 생각)

C99 이전의 C 언어에서는 불리안 타입을 포함하지 않았음. 이 경우, 0은 거짓이고, 0이 아닌 모든 값은 참으로 고려함.

따라서, 다음과 같은 특이한 식이 적법해 짐.

a > b > c

C 언어의 관계 연산자들이 좌결합이기 때문에, "a > b"가 먼저 평가되어 0 또는 1로 결과 값이 생성됨.

다음 단계로 "0 > c" 또는 "1 > c"가 평가됨. (b와 c의 비교는 실행되지 않음.)

● 단락회로 평가(Short-circuit Evaluation)

식의 결과가 모든 피연산자와 연산자를 평가하지 않고서 결정되는 평가.

예시1 : (13 * a) * (b / 13 – 1)  → a == 0이면 b의 값과 관계없이 0으로 결정됨.

예시2 : (a >= 0) && (b < 10)  → a < 0 이면 두 번째 관계식과 무관하게 불리안 식의 결과가 정해짐.

- 불리안 식에서 비 단락회로 평가가 갖는 문제점

// Java
index = 0;
while ((index < listlen) && (list[index] != key))
	index = index + 1;

평가가 단락회로가 아니면, while 문의 불리안 식에 포함된 두 개의 관계식이 첫 번째 관계식의 값에 관계없이 모두 평가됨.
따라서, key가 list에 속하지 않으면, 프로그램은 첨자 범위 이탈 예외로 종료됨.

- 단락회로 평가에 의해 발생 가능한 문제점

(a > b) || ((b++) / 3)

 

위의 식에서, b는 a <= b인 경우에만 변경됨.

만일, 프로그래머가 실행 중에 이 식이 평가될 때마다 b의 값이 변경된다고 가정했다면, 심각한 오류를 초래할 수 있음.

C와 Java에서 AND와 OR 연산자인 "&&"와 "||"는 각각 단락회로임. (비트 AND '&' 와 비트 OR '|' 을 미포함)

Python에서 모든 논리적 연산자는 단락회로로 평가됨.

● 배정문(Assignment Statements)

배정문은 사용자가 변수에 대한 값의 바인딩을 동적으로 변경시킬 수 있는 메커니즘을 제공함.

- 단순 배정문

대부분의 언어에서 배정 연산자로 동등 기호 ("=")를 사용함.
따라서, 그러한 언어들에서는 동등 관계 연산자로 "=="를 주로 사용하고, ALGOL 60, Ada에서는 배정 연산자로 “:=“ 기호를 사용하기도 함.

- 복합 배정 연산자

배정문에서 공통적으로 필요한 형식을 명세하는 축약을 가능하게 함

sum += value;       // 1
sum = sum + value;  // 2

C, Java, Python 등의 언어에서 위의 두 문장은 동등함.

- 단항 배정 연산자

C, Java, JavaScript 등이 언어에서는 두 개의 특정 단항 산술 연산자("++"와 "--")를 지원함. (Python은 지원하지 않음)
이들은 실제로는 배정문의 축약이며, 전위 연산자 또는 후위 연산자로 나타날 수 있음.

// 전위 연산자
sum = ++count;
// 위 아래 두 식은 동일한 결과를 가져옴
count = count + 1;
sum = count;
// 후위 연산자
sum = count++;
// 위 아래 두 식은 동일한 결과를 가져옴
sum = count;
count = count + 1;
count++;

++count;

count = count + 1;
// 세 식은 모두 동일한 결과를 가져옴

ㄴ 두 개의 단항 연산자가 동일한 피연산자에 적용될 때의 예
결합 순서는 오른쪽부터 왼쪽의 방향임에 유의해야 함.

-count++;
// 위 아래 두 식은 동일한 결과를 가져옴
-(count++);

ㄴ 컴파일 에러 예시

(-count)++

 

"++"는 lvalue와 rvalue 모두 가능한 변수에만 사용이 가능한데, (-count)는 임시변수라 lvalue가 될 수 없기 때문에 컴파일 에러가 발생함.

- 식으로서의 배정문

C, Java 등에서 배정문은 결과를 생성하는데, 그 결과는 목적지에 할당되는 값과 동일함.
따라서, 배정문은 식으로서, 그리고 다른 식에 포함된 피연산자로서 사용될 수 있음.
배정 연산자는 다른 이항 연산자와 매우 유사함. 단, 배정 연산자는 왼쪽 피연산자를 변경하는 부작용을 가짐.
ex. a = b에서 왼쪽 피연산자 a의 값을 변경함.

 

ㄴ C 예시

while ((ch = getchar()) != EOF) { … }

위의 문장에서, 키보드 입력 결과를 변수 ch에 배정한 후, 배정한 값과 EOF를 비교함
주의할 점은 위의 배정문( ch = getchar() )이 괄호 안에 표현되어야 함. (관계 연산자(“!=“)가 배정 연산자 (“=“)보다 우선 순위가 높기 때문)
괄호가 없으면, 키보드 입력 문자가 EOF와 비교된 후, 변수 ch에는 0 또는 1이 배정됨.

 

ㄴ 단점

이런 유형의 부작용은 읽고 이해하기 어려운 식을 초래할 수 있음

// C, Java
a = b + (c = d / b) – 1;

위의 문장을 해석해 보면, (1) d / b의 결과를 c에 배정한 뒤, (2) b + c를 temp에 임시 저장하고, (3) temp – 1의 결과를 a에 배정함.

 

ㄴ다중 목적지 배정 (Multiple-target Assignments)의 효과

sum = count = 0;

이러한 형태는 Python에서도 가능함. 이것 이외에 앞서 설명한 코드 형태는 허용하지 않음.

 

ㄴ 빈번하게 프로그램 오류를 초래하는 배정 연산의 예
C 언어에서 자주 발생

if (x = y) // 원래는 if (x == y)

관계식을 검사하기 보다 x에 할당된 값이 검사됨. 그 결과, y가 0이 아니면 if 문이 참, y가 0이면 if 문이 거짓으로 처리됨.
Java와 C#에서는 if 문 안에 불리안 식만 허용하기 때문에, 컴파일러 또는 인터프리터가 오류를 발견하여 Error를 발생시킴.

 

- 혼합형 배정문

앞서 혼합형 식을 다루었었음.

혼합형 배정문 또한 빈번히 사용됨.

C, C++와 같은 언어에서는 타입 강제 변환 규칙을 사용하여 확장이든 축소든 관계없이 사용 가능함.

float f = 10.3;
int i = f;

Java에서는 확장인 경우에만 타입 강제 변환을 허용함.

int i = 1;
float f = i;
// 반대는 불가능
반응형

댓글