C에서의 함수(function)는 수학에서의 함수와 항상 비슷하지는 않다. C언어에서 함수는 argument를 항상 필요로 하지도 않고, 항상 값을 계산하는 것도 아니다(어떤 프로그래밍 언어에
서는 "함수"는 값을 반환하지만 "프로시저(procedure)"는 그렇지 않다. C언어는 이러한 구별이 없다.).
함수는 C 프로그램을 구성하는 블록이다. 함수를 사용하는 것은 프로그램을 작은 조각으로 쪼개기 쉽게 하여 이해하기 쉽고 수정하기 쉽게 만든다. 또 함수는 코드를 복제하는 것을 피할 수 있다. 함수는 재사용이 가능하다.
두 개의 double
값의 평균을 자주 계산해야할 필요가 생겼다고 가정해보자. C 라이브러리는 "average" 함수를 가지고 있지 않지만, 우리는 쉽게 이를 정의할 수 있다.
double average(double a, double b)
{
return (a + b) / 2;
}
맨 처음에 시작할 때 나오는 단어 double
은 average
의 return type
이다. 함수가 호출될 때마다 함수가 반환하는 데이터의 자료형을 나타낸다. 지시자(identifier) a
와 b
(함수의 parameter
)는 average
가 호출될 때 전달되는 두 개의 숫자를 나타낸다. 각각의 parameter는 자료형을 반드시 가져야 한다(모든 변수가 자료형을 가지는 것처럼). 함수의 parameter는 함수가 호출되었을 때 초기값이 나중에 제공되는 변수이다.
모든 함수는 중괄호로 둘러싸인 body
라고 부르는 실행 부분을 가지고 있다. average
의 body는 단일 return
구문으로 이루어져있다. 이 구문을 실행하는 것은 함수가 호출된 것으로부터 함수를 반환시키도록 한다. (a + b) / 2
가 함수에 의해 반환된 값이 될 것이다.
이 함수를 호출하기 위해서는 argument
를 가진 함수의 이름을 써야한다. 예를 들어 average(x, y)
가 average
의 호출이다. argument는 함수에게 정보를 제공하기 위해서 사용된다. 이 경우에는 average
는 평균을 구하기 위해 두 개의 숫자를 알 필요가 있다. average(x, y)
를 호출하는 것의 효과는 x
와 y
의 값을 복사하여 parameter a
와 b
에 넣고, 그 후 average
의 body를 실행한다. argument는 변수일 필요가 없다. 적합한 자료형의 어떠한 표현식이든 가능하기 때문에 average(5.1, 8.9)
와 같이 쓸 수도 있고, average(x/2, y/3)
과 같이도 쓸 수 있다.
우리는 average
함수의 호출을 반환값이 필요한 어디에든지 넣을 수 있다.
printf("Average: %g\n", average(x, y));
average
함수가 argument로써의 x
, y
와 함께 호출된다.x
와 y
는 a
와 b
에 복사된다.average
는 a
와 b
의 평균을 반환하는 return
구문을 실행한다.printf
가 average
의 반환값을 출력한다. average
의 반환값이 printf
의 argument 중 하나가 되었다.average
의 반환값이 어디에서나 저장되지 않음을 유의해야 한다. 이 프로그램은 average
를 출력한 후 그 값을 바로 폐기할 것이다. 만약 나중에도 이 반환값이 필요하다면 우리는 변수 내부에 반환값을 저장해야 한다.
avg = average(x, y);
반환형이나 parameter를 가지지 않은 함수도 만들 수 있다.
#include <stdio.h>
void print_pun(void)
{
printf("To C, or not to C: that is the question. \n");
}
int main(void)
{
print_pun();
return 0;
}
아래는 함수 정의(funciton definition)의 일반적인 형태이다.
return-type function-name ( parameter )
{
declarations
statements
}
함수의 return type은 함수가 반환하는 값의 자료형이다. return type은 아래의 규칙을 따른다.
void
라고 명시하는 것은 함수가 값을 반환하지 않는다는 것을 나타낸다.int
자료형의 값을 반환한다고 가정한다. C99에서는 함수의 return type을 생략하는 것이 규칙에 어긋난다(illegal).어떠한 프로그래머들은 return type을 함수 이름의 위에 붙이기도 한다.
double
average(double a, double b)
{
return (a + b) / 2;
}
다른 줄에 return type을 넣는 것은 return type이 unsigned long int
처럼 길 때 특히 유용하다.
parameter의 목록이 함수의 이름 뒤에 온다. 각각의 parameter는 자료형의 명시가 앞에 붙어있어야 한다. parameter들은 콤마(,)로 분리된다. 만약 함수가 어떠한 parameter도 가지지 않는다면, 단어 void
가 괄호 사이에 있어야 한다. 자료형이 반드시 각각의 parameter에 나타내야 한다. parameter가 같은 자료형을 가지고 있더라도 말이다.
double average(double a, b) /*** WRONG ***/
{
return (a + b) / 2;
}
함수의 body는 선언과 구문을 포함할 수 있다.
double average(double a, double b)
{
double sum;
sum = a + b;
return sum / 2;
}
body 내부에 선언된 변수는 그 함수에서만 독점적으로 사용할 수 있다. 다른 함수에서 검사되거나 수정될 수 없다. C89에서는 다른 모든 구문보다 먼저 변수 선언이 맨 앞에 와야하지만, C99에서는 변수선언과 구문이 섞여도 된다. 단 변수가 선언되기 전에 그 변수를 구문에서 사용하면 안된다.
return type이 void
인 함수의 body는 비어있어도 된다.(void function
)
void print_pun(void)
{
}
함수 호출(function call)은 괄호 안에 있는 argument의 목록과, 함수 이름으로 구성되어 있다.
average(x, y)
print_count(i)
print_pun()
만약 괄호가 없으면 함수가 호출되지 않는다.
print_pun; /*** WRONG ***/
결과는 규칙에 맞는 표현식 표현식 구문처럼 보인다. 하지만 아무 효과가 없다. 어떤 컴파일러는 "statement with no effect."과 같은 오류를 발생시킬 것이다.
void
함수의 호출의 뒤에는 항상 세미콜론(;)이 따라와 구문이 된다.
printf_count(i);
printf_pun();
void
가 아닌 함수의 호출은 변수에 저장되거나, 검사되거나, 출력되거나, 또는 어떠한 방식으로든 사용될 수 있는 값을 생성한다.
avg = average(x, y);
if (average(x, y) > 0)
printf("Average is positive\n");
printf("The average is %g\n", average(x, y));
void
가 아닌 함수에 의해 반환된 값은 항상 필요한 것이 아니라면 폐기할 수 있다.
average(x, y); /* discards return value */
이 average
의 호출은 표현식 구문의 예시이다. 이 구문은 표현식을 평가하는 구문이지만 결과값을 폐기한다.
average
의 반환값을 무시하는 것은 정말 이상한 행동인데, 어떠한 함수들에겐 말이 되는 행동이다. printf
함수를 예로 들자면 printf
는 출력하는 문자의 숫자를 반환한다. 아래의 예시를 보자. num_chars
는 9의 값을 가질것이다.
num_chars = printf("Hi, Mom!\n");
우리는 출력된 문자가 몇 글자인지는 별로 흥미가 없을 수 있기 때문에, 우리는 일반적으로 printf
의 반환값을 버린다.
printf("Hi, Mom!\n"); /* discards return value */
의도적으로 함수의 반환값을 버린다는 것을 확실하게 하기 위해서, C언어는 호출 이전에 (void)
를 붙이는 것을 허용한다.
(void)printf("Hi, Mom!\n");
printf
의 반환값을 void
로 casting한 것이다(C언어에서 "void
로 casting 하는것"은 값을 "던져버리는" 정중한 방법이다). (void)
를 사용하는 것은 잊어버리고 반환값을 안쓴것이 아니고, 의도적으로 폐기했다는 것을 다른 사람들에게 명확하게 표현할 수 있게 해준다.
불행하게도 C 라이브러리의 아주 많은 함수들의 반환값들이 일상적으로 버려진다.
함수는 하나 이상의 return
구문을 가져도 된다. 하지만 함수의 호출 도중에는 이 return
구문들 중 하나만 실행될 수 있다. 왜냐하면 return
구문에 도달하는 것은 함수가 호출된 곳으로부터 반환되도록 하기 때문이다.
Section 9.1의 프로그램에서는 각각 함수의 정의(definition)가 항상 호출되는 지점보다 위에 위치해있었다. 사실, C언어는 함수의 정의가 호출보다 위에 있도록 요구하지 않는다. 아래의 average.c
프로그램을 보자.
#include <stdio.h>
int main(void)
{
double x, y ,z;
printf("Enter three numbers: ");
scanf("%lf%lf%lf", &x, &y, &z);
printf("Average of %g and %g: %g\n", x, y, average(x, y));
printf("Average of %g and %g: %g\n", y, z, average(y, z));
printf("Average of %g and %g: %g\n", x, z, average(x, z));
return 0;
}
double average(double a, double b)
{
return (a + b) / 2;
}
컴파일러가 main
내부의 average
의 첫번째 호출을 발견했을 때, average
에 대한 정보가 없다. average
가 가진 parameter가 몇 개인지 알 수 없고, parameter가 어떤 자료형인지도 알 수 없으며, average
가 어떤 값을 반환하는지 알 수 없다. 그러나 컴파일러는 에러 메시지를 발생시키는 대신에, average
가 int
값을 반환한다고 가정한다(Section 91에서 함수의 return type이 기본적으로 int
로 설정되어있다는 것을 회상해보라.). 이를 컴파일러가 함수의 암시적인 선언(implicit declaration of function)을 생성했다고 말한다. 컴파일러는 우리가 average
에 argument로써 옳은 수를 전달했는지, 그리고 argument가 적절한 자료형을 가지고 있는지 확인할 수 없다. 프로그램 내부에서 average
의 정의를 나중에 발견했을 때, 컴파일러는 함수의 return type이 int
가 아니라 실제로는 double
이라는 것을 인식할 것이고, 그래서 우리는 에러 메시지를 얻게된다.
정의 이전에 호출되는 문제를 피하기 위한 방법 중 하나는 호출보다 앞에 정의가 오도록 프로그램을 정렬하는 것이다. 하지만 이러한 정렬은 항상 가능한 것이 아니고, 만약 가능하더라도 자연스럽지 않은 순서로 함수의 정의를 넣는 것은 프로그램을 알아보기 더 어렵게 만든다.
다행히도, C언어는 더 좋은 해결책을 제공하는데, 그것을 호출하기 전에 각각의 함수를 선언(declaration)하는 것이다. 함수 선언(function declaration)은 뒤에 있는 완전히 정의된 함수를 컴파일러가 간결하게 알아 볼 수 있도록 한다. 함수 선언은 함수 정의의 첫번째 줄에 세미콜론(;)을 추가한 것과 비슷하다.
return-type function-name ( parameters ) ;
말할 필요도 없이, 함수의 선언은 반드시 함수 정의와 일관되어야 한다.
위의 프로그램을 average
의 선언이 추가되도록 만들어보자.
#include <stdio.h>
double average(double a, double b); /* DECLARATION */
int main(void)
{
double x, y ,z;
printf("Enter three numbers: ");
scanf("%lf%lf%lf", &x, &y, &z);
printf("Average of %g and %g: %g\n", x, y, average(x, y));
printf("Average of %g and %g: %g\n", y, z, average(y, z));
printf("Average of %g and %g: %g\n", x, z, average(x, z));
return 0;
}
double average(double a, double b)
{
return (a + b) / 2;
}
이러한 종류의 함수 선언(functoin declaration)은 function prototypes
라고도 알려져있다. prototype함수가 어떻게 호출되는지에 대한 완벽한 설명을 제공한다. 얼마나 많은 argument가 제공되어야 하는지, argument가 무슨 자료형이여야 하는지, 결과값이 어떠한 자료형을 반환할 것인지에 대한 것들이다.
function prototype은 함수의 parameter의 이름을 명시하지 않아도 되는데, 자료형은 표시해야한다.
double average(double, double);
parameter의 이름을 생략하지 않는 것이 일반적으로 가장 좋은데, 왜냐하면 parameter의 이름이 각각의 parameter의 의도를 알게 해주고 함수가 호출되었을 때 argument가 나타나야 하는 순서를 프로그래머에게 다시 떠오르게 하기 때문이다. 그러나 parameter 이름을 생략하는 합당한 이유가 있기도 하고, 어떠한 프로그래머들은 생략하는 것을 선호하기도 한다.
C99는 어떠한 함수의 호출보다 먼저 함수의 선언이나 함수의 정의가 존재해야 한다는 규칙을 채택했다. 컴파일러가 함수의 정의나 선언을 아직 발견하지 못한 함수를 호출하는 것은 에러를 발생시킨다.
parameter와 argument 사이의 차이를 생각해보자. parameter는 함수의 "정의"에서 나타나는 것이다. 그들은 함수가 호출되었을 때 전달될 값을 나타내는 가짜 이름이다. arguments는 함수의 "호출"에서 나타나는 표현식이다. argument와 parameter 사이의 구분이 중요하지 않을 때에는 두 쪽모두 argument라고 부르기도 한다(물론 엄밀히 따지면 다르지만).
C언어에서 argument는 값에 의해 전달된다passed by value
. 함수가 호출되었을 때, 각각의 argument가 계산되고 그 값이 대응하는 parameter로 할당된다. parameter가 argument가 가진 값의 복사본을 포함하고 있기 때문에, 함수의 실행중에 parameter에 어떠한 수정을 가하더라도 argument에는 영향을 미치지 않는다. 각각의 parameter는 일치하는 argument의 값으로 초기화된 변수처럼 행동하게 된다.
argument가 값에 의해 전달된다는 사실은 장점도 있고 단점도 있다. parameter가 대응되는 argument에는 영향을 미치지 않고 parameter를 수정할 수 있기 때문에, 우리는 parameter을 함수 내부의 변수처럼 사용할 수 있어서 필요한 실제 변수의 개수를 줄일 수 있다.
숫자 x
에 n
을 제곱하는 아래의 함수를 보자.
int power(int x, int n)
{
int i, result = 1;
for(i = 1; i <= n; i++)
result = result * x;
return result;
}
n
이 원래의 exponent의 복사본이기 때문에 함수 내부에서 n
을 수정할 수 있고, 그래서 i
가 필요하지 않다.
int power(int x, int n)
{
int result = 1;
while (n-- > 0)
result = result * x;
return result;
}
불행하게도, argument가 값으로 전달된다는 C언어의 요구사항은 특정 종류의 함수를 작성하기 어렵게 만든다. 예를 들어, double
값을 정수 부분와 분수 부분으로 분해하는 함수가 필요하다고 생각해보자. 함수가 2개의 숫자를 반환할 수 없기 때문에, 우리는 이 한 쌍의 변수를 함수에게 전달하도록 시도할 것이고, 수정하도록 할 것이다.
void decompose(double x, long int_part, double frac_part)
{
int_part = (long)x; /* drops the fractional part of x */
frac_part = x - int_part;
}
아래의 방식으로 함수를 호출했다고 가정해보자.
decompose(3.14159, i, d);
호출이 시작하면, 3.14159는 x
로 복사될 것이고 i
의 값이 int_part
로, d
의 값이 frac_part
로 복사될 것이다. decompose
내부의 구문은 int_part
에 3을 대입할 것이고, .14159를 frac_part
에 대입할 것이다. 그 후 함수는 반환된다. 불행하게도, i
와 d
는 int_part
와 frac_part
에 행한 대입의 영향을 받지 않는다. 그래서 i
와 d
는 함수의 호출 전과 동일한 값을 가진다. 조금의 노력과 함께라면 decompose
는 잘 작동할 수 있게 되지만, 이는 Section 11.4에서 알아볼 것이다.
C언어는 parameter의 자료형과 일치하지 않은 arguments의 자료형을 가진 함수 호출을 허용한다. argument가 변환되는 규칙은 컴파일러가 함수 호출 이전에 함수에 대한 prototype을 본 적이 있는지 없는지에 따른다.
double
을 기대하는 함수로 int
argument가 전달된다면, argument는 자동적으로 double
로 변환된다.default argument promotions
에 따른다. (1) float
argument는 double
로 변환된다. (2) char
과 short
argument가 int
로 바뀌는 integral promotion이 수행된다.(C99에서는 integer promotion이 수행된다)default argument promotion에 의존하는 것은 위험하다. 아래의 예시를 보자.
#include <stdio.h>
int main(void)
{
double x = 3.0;
printf("Square: %d\n", square(x));
return 0;
}
int square(int n)
{
return n * n;
}
square
가 호출되는 순간, 컴파일러는 prototype을 아직 확인하지 못했을 것이고 컴파일러는 그렇기에 square
가 int
자료형의 argument를 기대한다는 것을 알지 못한다. 대신에 컴파일러는 아무 효과없는 default argument promotion을 x
에 수행할 것이다. int
자료형의 argument를 기대했지만 double
값이 대신 주어졌기 때문에 square
를 호출하는 것의 효과는 undefined
이다. square
의 argument를 casting 하는것으로 프로그램을 올바르게 수정할 수 있다.
printf("Square: %d\n", square((int)x));
당연하게도, 함수 호출 전에 square
에 대한 prototype을 제공하는 것이 더 좋은 해결책이다. C99에서는 함수의 선언이나 정의를 처음에 제공하지 않고 square
를 호출하는 것은 에러이다.
배열은 argumenet로 자주 사용된다. 함수 parameter가 one-dimensional array일 때, 배열의 길이는 명시되지 않을 수 있다(일반적으로 명시하지 않음).
int f(int a[]) /* no length specified */
{
...
}
하나의 문제가 생긴다. f
는 함수가 얼마나 길지 어떻게 아는가? 불행하게도 C언어는 함수가 전달된 array의 길이를 결정하는 쉬운 방법을 제공하지 않는다. 대신에, 우리는 함수가 필요로 한다면 추가적인 argument로 길이를 제공해야할 것이다.
sizeof
연산자를 사용하여 배열 변수(variable)의 길이를 결정하는 것에 도움을 줄 수 있다고 하더라도, 배열 parameter에 대해서는 올바른 답을 줄 수 없다.
int f(int a[])
{
int len = sizeof(a) / sizeof(a[0]);
/*** WRONG: not the number of elements in a ***/
...
}
Section 12.3에서 이 상황에 대해 설명한다.
아래의 함수는 one-dimensional array argument의 사용에 대해 설명한다.
int
값의 a
배열이 주어졌을 때, sum_array
는 a
에 있는 인자들의 합을 반환한다. sum_array
가 a
의 길이를 알 필요가 있기 때문에 두 번째 argument로 길이를 제공해야 한다.
int sum_array(int a[], int n)
{
int i, sum = 0;
for (i = 0; i < n; i++)
sum += a[i];
return sum;
}
sum_array
에 대한 prototype은 아래와 같은 모습이다.
int sum_array(int a[], int n);
원한다면 parameter의 이름도 생략할 수 있다.
int sum_array(int, int);
sum_array
가 호출되었을 때, 첫 번째 argument는 배열의 이름이 되어야 하고, 두 번째 argument는 길이가 될 것이다.
#define LEN 100
int main(void)
{
int b[LEN], total;
...
total = sum_array(b, LEN);
...
}
함수에게 배열을 전달할 때 배열의 이름 뒤에 대괄호를 붙이면 안된다.
total = sum_array(b[], LEN); /*** WRONG ***/
배열 argument에 대해 중요한 점은 우리가 전달한 배열의 길이가 올바른지 함수가 확인할 방법이 없다는 것이다. 우리는 배열이 원래보다 더 작다고 함수에게 말하는 것으로 이 사실을 악용할 수 있다. b
배열이 100개의 숫자를 저장할 수 있지만, b
배열 안에 오직 50개의 숫자를 저장할 수 있다고 가정해보자. 우리는 아래와 같이 작성할 수 있다.
total = sum_array(b, 50); /* sums first 50 elements */
sum_array
는 나머지 50개의 요소를 무시할 것이다.
함수에게 배열의 길이를 원래 길이보다 더 길다고 전달하지 않도록 조심해야 한다.
total = sum_array(b, 150); /*** WRONG ***/
이 예제에서 sum_array
는 배열의 끝을 넘어갈 것이고, undefined behavior를 초래한다.
또다른 중요한 것은 함수에게 배열 parameter의 요소들을 바꿀 수 있도록 허용되어 있고, 이 변화는 대응되는 argument에 반영된다는 사실이다. 예를 들어 아래의 함수는 배열 내부의 각각의 요소들을 0으로 수정하는 것이다.
void store_zeros(int a[], int n)
{
int i;
for (i = 0; i < n; i++)
a[i] = 0;
}
store_zeros(b, 100)
의 호출은 배열 b
에 있는 첫 100개의 요소들을 0으로 바꿀 것이다. 배열 argument의 요소를 수정할 수 있는 이 능력은 C가 값으로 argument를 전달한다는 사실과 모순된다. 사실 이것은 모순이 아닌데, 이는 Section 12.3에서 설명한다.
만약 parameter가 multidimensional array라면 parameter가 선언될 때 오직 첫번째 dimensional만 생략이 가능하다. 예를 들어 우리가 a
가 two-dimensional array가 되도록 sum_array
함수를 수정한다고 한다면, 우리는 row의 개수를 표시하지 않더라도 반드시 a
의 column의 개수를 명시해야 한다.
#define LEN 10
int sum_two_dimensional_array(int a[][LEN], int n)
{
int i, j, sum = 0;
for (i = 0; i < n; i++)
for (j = 0; j < LEN; j++)
sum += a[i][j];
return sum;
}
multidimensional array의 column의 개수를 자의적(arbitrary)으로 전달하지 못하는 것은 성가신 일이다. 다행히도, 포인터의 배열을 사용하는 것으로 이 어려움을 해결할 수 있다. C99의 variable-length array parameter는 문제를 해결하는 것에 더 좋은 해결책을 제공한다.
C99는 배열 arguments에 몇 가지 새로운 변형을 추가했다. 첫번째는 C99의 특징인 상수가 아닌 표현식을 사용하여 배열의 길이를 특정하는 variable-length array(VLA)와 관련이 있다. variable-length array는 parameter로도 사용될 수 있다.
아래에 body가 생략된 sum_array
의 정의가 있다.
int sum_array(int a[], int n)
{
...
}
n
과 배열 a
의 길이와는 직접적인 연결이 없다. 비록 함수 body가 n
을 a
의 길이로써 처리하지만 실제 배열의 길이는 n
보다 더 클 수도 있고, 더 작을 수도 있다.
variable-length array parameter를 사용하는 것으로 우리는 a
의 길이가 n
이라는 것을 명시적으로 설명할 수 있다.
int sum_array(int n, int a[n])
{
...
}
첫번째 parameter n
의 값은 두번째 parameter a
의 값을 특정한다. parameter의 순서가 바뀌었음을 주목해야 한다. variable-length array parameter가 사용될 때에는 순서가 중요하기 때문이다.
아래의 sum_array
는 규칙에 어긋난다.
int sum_array(int a[n], int n) /*** WRONG ***/
{
...
}
컴파일러가 int a[n]
에서 에러 메시지를 발생시키는데, 컴파일러가 아직 n
을 발견하지 못했기 때문이다.
sum_array
에 대한 새로운 버전의 prototype을 작성하는 여러 방법이 있다. 하나의 가능성은 함수 정의처럼 보이게 만드는 것이다.
int sum_array(int n, int a[n]); /* Version 1 */
또다른 가능성은 배열의 길이를 별표(*
)로 대체하는 것이다.
int sum_array(int n, int a[*]); /* Version 2a */
*
표시를 사용하는 것에 대한 이유는 parameter 이름은 함수 선언에서 선택적이라는 것이다. 만약 첫번째 parameter의 이름이 생략되었다면 배열의 길이가 n
이라는 것을 특정할 수 없지만, *
은 배열의 길이가 앞에 사용된 parameter와 관계가 있다는 단서를 제공한다.
int sum_array(int, int [*]); /* Version 2b */
우리가 일반적으로 배열 parameter을 선언할 때 하는 것 처럼 대괄호를 빈칸으로 남기는 것도 규칙에 맞다.
int sum_array(int n, int a[]); /* Version 3a */
int sum_array(int, int []); /* Version 3b */
대괄호를 빈칸으로 남기는 것은 좋은 선택은 아닌데, 왜냐하면 n
과 a
의 관계성을 드러낼 수 없기 때문이다.
일반적으로 variable-length array의 길이를 나타내는 parameter는 어떠한 표현식이라도 될 수 있다. 예를 들어 두 개의 배열 a
와 b
의 요소를 복사하여 배열 c
에 사슬처럼 이어붙이는 함수를 작성한다고 가정해보자.
int concatenate(int m, int n, int a[m], int b[n], int c[m+n])
{
...
}
c
의 길이는 a
와 b
의 길이의 합이다. c
의 길이를 특정하기 위해 사용된 표현식은 두 개의 다른 parameter를 포함하는데, 일반적으로 외부 함수의 변수를 참조하거나 또다른 함수를 호출하기도 한다.
variable-length array parameter가 사용된 single dimension에서는 한정된 활용법을 가진다. 배열 argument의 길이가 얼마나 요구되는지에 대해 설명하는 더 서술적인 정의나 함수 선언에 사용된다. 그러나 추가적인 에러확인이 수행되지는 않는데, 아직도 배열 argument가 더 길 수도 있고 더 짧을 수도 있다.
variable-length array parameter는 multidimensional array에서 가장 유용하다. 앞에서, 우리는 two-dimensional array의 요소를 더하는 함수를 작성하려고 했었다. 원래의 함수는 고정된 column의 개수를 가진 배열에 한정되어 있었다. 만약 우리가 variable-length array parameter를 사용한다면, 우리는 함수가 column 개수에 상관없이 일반화하도록 할 수 있다.
int sum_two_dimensional_array(int n, int m, int a[n][m])
{
int i, j, sum = 0;
for (i = 0; i < n; i++)
for (j = 0; j < m; j++)
sum += a[i][j];
return sum;
}
이 함수에 대한 prototype은 아래와 같다.
int sum_two_dimensional_array(int n, int m, int a[n][m]);
int sum_two_dimensional_array(int n, int m, int a[*][*]);
int sum_two_dimensional_array(int n, int m, int a[][m]);
int sum_two_dimensional_array(int n, int m, int a[][*]);
static
in Array Parameter DeclarationsC99는 배열 parameter의 선언에서 키워드 static
의 사용하는 것을 허용한다.
아래의 예제에서 static
을 3
앞에 넣은 것은 a
의 길이가 최소 3이라는 것을 보장한다.
int sum_array(int a[static 3], int n)
{
...
}
static
을 이 방법으로 사용하는 것은 프로그램에 어떠한 효과도 미치지 않는다. static
의 존재는 단순히 C 컴파일러가 배열에 접근하는 것에 있어서 더 빠르게 지침(instruction)을 생성하도록 하는 "hint"를 제공할 뿐이다. (만약 컴파일러가 배열이 특정한 최소 길이를 항상 가진다는 것을 알고 있다면, 컴파일러는 함수 내부의 구문에서 요소들이 실제로 필요해지기 전에, 함수가 호출되었을 때 이 요소들을 "prefetch"하여 정렬할 수 있다.)
만약 배열 parameter가 one dimension보다 더 많은 dimension을 가지게 된다면 static
은 오직 첫번째 dimension에서만 사용될 수 있다.(예를 들어 two-dimensional array의 row의 수를 명시할때)
원래의 sum_array
함수로 돌아가보자. sum_array
가 호출되었을 때, 첫번째 argument는 보통 배열의 이름이다. 예를 들어, 우리가 sum_array
를 아래와 같이 호출할 수 있다.
int b[] = {3, 0, 3, 4, 1};
total = sum_array(b, 5);
이것의 문제는 b
가 변수로써 선언되어야 한다는 점이고, 호출보다 더 앞서서 초기화가 되어야 한다는 점이다. b
가 다른 의도를 필요로 하지 않는다면, sum_array
를 호출하기 위한 의도로만 b
를 생성한다면 조금 성가신 일일 수도 있다.
C99에서는 복합 리터럴(compound literal
)을 사용하는 것으로 이러한 성가심을 피할 수 있다. 복합 리터럴은 포함하고 있는 요소(element)를 간단하게 명시하는 것으로 "즉석으로" 이름없는 배열을 생성한다. 아래의 sum_array
의 호출은 첫 번째 argument로 복합 리터럴을 가지고 있다.
total = sum_array((int []){3, 0, 3, 4, 1}, 5);
이 예제에서는 복합 리터럴이 5개의 정수인 3, 0, 3, 4, 1을 포함하고 있는 배열을 생성했다. 배열의 길이를 명시하지 않았는데, 리터럴 내부의 요소의 수에 따라 결정되기 때문이다. 길이를 명시적으로 특정하는 것은 선택적이다. (int [4]){1, 9, 2, 1}
은 (int []){1, 9, 2, 1}
과 동일하다.
일반적으로 괄호 내부에 자료형의 이름이 나타나고, 중괄호에 의해 둘러싸인 값의 집합이 이어서 나타나는 형태로 복합 리터럴이 구성된다. 사실, 복합 리터럴과 initializer는 같은 규칙을 따른다. 복합 리터럴 또한 designated initializer처럼 designator를 포함할 수 있다. 복합 리터럴은 다른 초기화 되지 않은 요소의 기본값을 0으로 하는 전체 초기화(full initialization)를 제공한다. 예를 들어 리터럴 (int [10]){8, 6}
이 10개의 요소를 가지는데, 처음 두 개의 요소는 8과 6의 값을 가지고 나머지 요소들은 0의 값을 가진다.
함수 내부에 생성된 복합 리터럴은 상수가 아닌 자의적(arbitrary) 표현식을 포함할 수 있다. 아래의 예시를 보자.
total = sum_array((int []){2 * i, i + j, j * k}, 3);
복합 리터럴의 이러한 측면은 편리함을 대폭 증가시켜준다.
복합 리터럴은 lvalue인데, 그래서 요소들의 값이 변경될 수 있다. 만약 필요하다면, (const int []){5, 4}
처럼 복합 리터럴은 자료형 앞에 const
를 붙이는 것으로 "read-only"로 만들어질 수 있다.