기타 연산자
기타 연산자 (Other operators)
앞서 다룬 분류(산술, 비교, 논리, 멤버 접근 등)에 딱 들어맞지 않는 연산자들을 모아둔 페이지예요. 함수 호출, 콤마, 캐스트, 조건, sizeof, _Alignof, typeof가 여기 속해요. 각각이 언제, 어떻게 쓰이는지 살펴볼게요.
출처: cppreference
본문
다른 주요 분류 중 어느 것에도 맞지 않는 연산자들의 모음이에요.
| 연산자 | 연산자 이름 | 예시 | 설명 |
|---|---|---|---|
(...) |
함수 호출 | f(...) |
인자를 0개 이상 사용해 함수 f()를 호출 |
, |
콤마 연산자 | a, b |
표현식 a를 평가하고 그 반환 값을 무시한 채 모든 부수 효과를 끝낸 다음, 표현식 b를 평가해 타입과 평가 결과를 반환 |
(type) |
타입 캐스트 | (type)a |
a의 타입을 type으로 변환 |
?: |
조건 연산자 | a ? b : c |
a가 논리적으로 참(0이 아닌 값)이면 b를, 아니면 c를 평가 |
sizeof |
sizeof 연산자 | sizeof a |
a의 크기(바이트) |
_Alignof (since C11) |
_Alignof 연산자 | _Alignof(type) |
type에 요구되는 정렬 |
typeof |
typeof 연산자 | typeof(a) |
a의 타입 |
함수 호출 (Function call)
함수 호출 표현식은 다음 형태예요.
expression ( argument-list (optional) )
여기서 각 항목은 이렇게 돼요.
expression— (lvalue 변환 후) 포인터-함수 타입의 아무 표현식argument-list— 아무 완전한 객체 타입의 표현식들(콤마 연산자는 될 수 없어요)을 콤마로 구분한 목록. 인자를 받지 않는 함수를 호출할 때는 생략할 수 있어요.
함수 호출 표현식의 동작은, 호출 시점에 호출되는 함수의 프로토타입(prototype)이 스코프 안에 있는지에 따라 달라져요.
프로토타입이 있는 함수 호출 (Call to a function with a prototype)
-
매개변수의 개수는 인자의 개수와 같아야 해요 (줄임표(ellipsis) 매개변수를 쓰는 경우 제외).
-
각 매개변수의 타입은, 대응 인자의 한정되지 않은 타입을 매개변수 타입으로 변환하는 할당-하듯 암시적 변환이 존재하는 타입이어야 해요.
추가로, [와 ] 사이에 static 키워드를 사용하는 배열 타입의 각 매개변수에 대해, 인자 표현식은 매개변수의 크기 표현식에 명시된 만큼 이상의 원소를 가진 배열의 원소를 가리키는 포인터를 지정해야 해요. (since C99)
-
인자들은 지정되지 않은 순서로, 시퀀싱 없이(without sequencing) 평가돼요.
-
각 인자의 값을 대응 함수 매개변수에 복사하는 할당이 수행돼요. 매개변수 타입의 한정자와 그 매개변수의 재귀적 원소·멤버(있을 경우)는 무시돼요. (주의: 함수는 자기 매개변수를 수정할 수 있고, 그 변경은 인자에 영향을 주지 않아요. C 함수 호출은 오직 call-by-value예요.)
뒤에 줄임표 매개변수가 있다면, 나머지 인자들에 기본 인자 승격(Default argument promotions)이 수행되고, 이것들은 va_list로 이용 가능해져요.
- 함수가 실행되고, 반환한 값이 함수 호출 표현식의 값이 돼요 (함수가
void를 반환하면 함수 호출 표현식은 void 표현식이에요).
void f(char* p, int x) {}
int main(void)
{
f("abc", 3.14); // array to pointer and float to int conversions
}
프로토타입이 없는 함수 호출 (Call to a function without a prototype)
-
인자들은 지정되지 않은 순서로, 시퀀싱 없이 평가돼요.
-
모든 인자 표현식에 기본 인자 승격이 수행돼요.
-
각 인자의 값을 대응 함수 매개변수에 복사하는 할당이 수행돼요. 매개변수 타입의 한정자와 그 매개변수의 재귀적 원소·멤버(있을 경우)는 무시돼요.
-
함수가 실행되고, 반환한 값이 함수 호출 표현식의 값이 돼요 (함수가
void를 반환하면 함수 호출 표현식은 void 표현식이에요).
void f(); // no prototype
int main(void)
{
f(1, 1.0f); // UB unless f is defined to take an int and a double
}
void f(int a, double c) {}
프로토타입이 없는 함수에 대한 함수 호출의 동작은 다음과 같으면 정의되지 않아요.
- 인자의 개수가 매개변수의 개수와 일치하지 않는 경우
- 인자의 승격된 타입이 매개변수의 승격된 타입과 호환되지 않는 경우. 단, 다음은 예외예요.
- 같은 정수 타입의
signed·unsigned버전은, 인자의 값이 두 타입 모두로 표현 가능하면 호환되는 것으로 간주돼요. void포인터와 (cvr-한정된) 문자 타입 포인터는 호환되는 것으로 간주돼요. (until C23)
- 같은 정수 타입의
참고 사항 (Notes)
호출할 함수를 지정하는 expression의 평가와 모든 인자의 평가는 서로에 대해 unsequenced예요 (하지만 함수 본문이 실행되기 전에 시퀀스 포인트가 하나 있어요).
(*pf[f1()]) (f2(), f3() + f4()); // f1, f2, f3, f4 may be called in any order
함수 호출은 함수 포인터에 대해서만 정의되지만, 함수-포인터 암시적 변환 덕분에 함수 지정자에서도 동작해요.
int f(void) { return 1; }
int (*pf)(void) = f;
int main(void)
{
f(); // convert f to pointer, then call
(&f)(); // create a pointer to function, then call
pf(); // call the function
(*pf)(); // obtain the function designator, convert to pointer, then calls
(****f)(); // convert to pointer, obtain the function, repeat 4x, then call
(****pf)(); // also OK
}
printf처럼 사용하지 않는 인자를 무시하는 함수는, 정의되지 않은 동작을 피하려면 프로토타입이 스코프 안에 있는 상태로 호출되어야 해요 (그런 함수의 프로토타입은 필연적으로 뒤에 줄임표 매개변수를 사용해요).
함수 매개변수를 준비하는 의미론에 대한 현재 표준 문구는 결함이 있어요. 그것은 호출하는 동안 매개변수가 인자로부터 '할당'된다고 명시하는데, 이는 const-한정 매개변수 또는 멤버 타입을 잘못 거부하고, 많은 플랫폼에서 함수 매개변수에 대해 구현 불가능한 volatile의 의미론을 부적절하게 적용해요. C11 이후의 결함 보고서 DR427은 그러한 의미론을 할당에서 초기화로 바꾸는 변경을 제안했지만, not-a-defect로 종결됐어요.
expression이 전적으로 식별자로 구성되고 그 식별자가 선언되지 않은 함수 호출 표현식은, 그 식별자가 다음과 같이 선언된 것처럼 동작해요.
extern int identifier(); // returns int and has no prototype
그래서 다음 완전한 프로그램은 유효한 C89예요.
main()
{
int n = atoi("123"); // implicitly declares atoi as int atoi()
}
(until C99)
콤마 연산자 (Comma operator)
콤마 연산자 표현식은 다음 형태예요.
lhs , rhs
여기서 각 항목은 이렇게 돼요.
lhs— 아무 표현식rhs— 다른 콤마 연산자가 아닌 아무 표현식 (즉, 콤마 연산자의 결합은 왼쪽→오른쪽이에요)
먼저 왼쪽 피연산자 lhs가 평가되고 결과 값은 버려져요.
그 다음 시퀀스 포인트가 일어나 lhs의 모든 부수 효과가 완료돼요.
그 다음 오른쪽 피연산자 rhs가 평가되고, 그 결과가 콤마 연산자에 의해 non-lvalue로 반환돼요.
참고 사항 (Notes)
lhs의 타입은 void일 수 있어요 (즉 void를 반환하는 함수의 호출이거나, void로 캐스트된 표현식일 수 있어요).
콤마 연산자는 C++에서는 lvalue일 수 있지만, C에서는 결코 lvalue가 아니에요.
콤마 연산자는 구조체를 반환할 수 있어요 (구조체를 반환하는 다른 유일한 표현식들은 복합 리터럴, 함수 호출, 할당, 조건 연산자예요).
다음 맥락에서는 콤마가 다른 의미를 가지므로, 콤마 연산자가 표현식의 최상위에 나타날 수 없어요.
- 함수 호출의 인자 목록
- 초기화 표현식 또는 초기화자 목록
- 제네릭 선택
그런 맥락에서 콤마 연산자를 써야 한다면 괄호로 감싸야 해요:
// int n = 2,3; // error, comma assumed to begin the next declarator
// int a[2] = {1,2,3}; // error: more initializers than elements
int n = (2,3), a[2] = {(1,2),3}; // OK
f(a, (t=3, t+2), c); // OK, first, stores 3 in t, then calls f with three arguments
최상위 콤마 연산자는 배열 경계에서도 금지돼요.
// int a[2,3]; // error
int a[(2,3)]; // OK, VLA array of size 3 (VLA because (2,3) is not a constant expression)
콤마 연산자는 최상위든 아니든 상수 표현식에서는 허용되지 않아요.
// static int n = (1,2); // Error: constant expression cannot call the comma operator
캐스트 연산자 (Cast operator)
캐스트 연산자 문서를 봐요.
조건 연산자 (Conditional operator)
조건 연산자 표현식은 다음 형태예요.
condition ? expression-true : expression-false
여기서 각 항목은 이렇게 돼요.
condition— 스칼라 타입의 표현식expression-true—condition이 0과 같지 않다고 비교되면 평가될 표현식expression-false—condition이 0과 같다고 비교되면 평가될 표현식
expression-true와 expression-false로는 다음 표현식들만 허용돼요.
- 아무 산술 타입의 두 표현식
- 같은 구조체나 공용체 타입의 두 표현식
void타입의 두 표현식- 호환되는(cvr-한정자 무시) 타입을 가리키는 포인터 타입의 두 표현식
nullptr_t타입의 두 표현식 (since C23)- 하나는 포인터이고 다른 하나는 null 포인터 상수(
NULL등) 또는nullptr_t값인 경우 (since C23) - 하나는 객체를 가리키는 포인터이고 다른 하나는 (한정될 수 있는)
void포인터인 경우
- 먼저
condition을 평가해요. 이 평가 후에 시퀀스 포인트가 있어요. condition의 결과가 0과 같지 않다고 비교되면expression-true를 실행하고, 그렇지 않으면expression-false를 실행해요.- 평가 결과에서 공통 타입(common type)으로 변환을 수행해요. 공통 타입은 다음과 같이 정의돼요.
- 표현식들이 산술 타입이면, 공통 타입은 일반 산술 변환(usual arithmetic conversions) 후의 타입이에요.
- 표현식들이 구조체/공용체 타입이면, 공통 타입은 그 구조체/공용체 타입이에요.
- 둘 다
void이면, 전체 조건 연산자 표현식은 void 표현식이에요. - 하나가 포인터이고 다른 하나가 null 포인터 상수 또는
nullptr_t값(since C23)이면, 타입은 그 포인터의 타입이에요. - 둘 다 포인터이면, 결과는 두 가리키는 타입의 cvr-한정자를 합친 타입을 가리키는 포인터예요 (즉, 하나가
const int*이고 다른 하나가volatile int*라면 결과는const volatile int*). 그리고 타입이 달랐다면 가리키는 타입은 복합 타입(composite type)이 돼요. - 하나가
void포인터이면, 결과는 결합된 cvr-한정자를 가진void포인터예요. - 둘 다
nullptr_t타입이면, 공통 타입도nullptr_t예요. (since C23)
#define ICE(x) (sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
// if x is an Integer Constant Expression then macro expands to
sizeof(*(1 ? NULL : (int *) 1)) // (void *)((x)*0l)) -> NULL
// according to point (4) this further converts into
sizeof(int)
// if x is not an Integer Constant Expression then macro expands to
// according to point (6)
(sizeof(*(void *)(x)) // Error due incomplete type
참고 사항 (Notes)
조건 연산자는 결코 lvalue 표현식이 아니지만, 구조체/공용체 타입의 객체를 반환할 수는 있어요. 구조체를 반환할 수 있는 다른 유일한 표현식들은 할당, 콤마, 함수 호출, 복합 리터럴이에요.
C++에서는 lvalue 표현식일 수 있다는 점을 주의하세요.
이 연산자와 할당의 상대적 우선순위에 대한 자세한 내용은 연산자 우선순위를 봐요.
조건 연산자는 오른쪽→왼쪽 결합이어서 체이닝이 가능해요.
#include <assert.h>
enum vehicle { bus, airplane, train, car, horse, feet };
enum vehicle choose(char arg)
{
return arg == 'B' ? bus :
arg == 'A' ? airplane :
arg == 'T' ? train :
arg == 'C' ? car :
arg == 'H' ? horse :
feet ;
}
int main(void)
{
assert(choose('H') == horse && choose('F') == feet);
}
sizeof 연산자 (sizeof operator)
sizeof 연산자 문서를 봐요.
_Alignof 연산자 (_Alignof operator)
_Alignof 연산자 문서를 봐요.
typeof 연산자 (typeof operators)
typeof 연산자 문서를 봐요.