13. Strings(1)

하모씨·2021년 10월 29일
0

KNKsummary

목록 보기
12/23

우리가 char 변수들과 char 배열을 사용해왔지만, 문자들의 모음(C 용어로는 string)을 처리하기에는 어떠한 편리한 방법이 없었다. 이 chapter에서는 string constant(C표준으로는 literal)와 string variable을 이용하여 이러한 결핍을 치료(remedy)할 것이다.

1. String Literals

문자열 리터럴(string literal)은 큰따옴표로 둘러싸인 연속적인 문자들이다.

"When you come to a fork in the road, take it."

우리는 printfscanf의 호출 안에서 서식 문자열로써 나타나는 문자열 리터럴을 보았을 것이다.

Escape Sequences in String Literals

문자열 리터럴은 문자 상수로써 escape sequence를 포함할 수 있다.

"Candy\nIs dandy\nBut liquor\nIs quicker.\n --Ogden Nash\n"

이는 커서가 다음 행으로 나아가도록 한다.

Candy
Is dandy
But liquor
Is quicker.
  --Ogden Nash

비록 8진 또는 16진 escape 또한 문자열 리터럴에서 규칙에 맞지만, 일반적으로 문자 escape만큼 일반적이지는 않다.

8진과 16진 escape를 문자열 리터럴에서 사용할 때에는 주의해야 한다. 8진 escape는 3자리 이후에 끝이 나거나 8진 문자가 오면 끝이난다. 예를 들어 문자열 "\1234"는 2개의 문자 (\1234)를 가지고, 문자열 "\189"는 3개의 문자(\1, 8, 9)를 포함한다. 반대로 16진 escape는 3자리에 한정되지 않는다. 16진수가 아닌 글자가 나올 때까지 끝나지 않는다. 만약 문자열이 ASCII의 일반적인 확장인 Latin1 문자 집합에서 ü를 표현하는 escape \xfc를 포함하고 있다고 생각해보자. 문자열 "Z\xfcrich"("Zürich")는 6개의 문자(Z, \xfc, r, i, c, h)를 가지지만, 문자열 "\xfcber"("über"의 실패한 시도)는 오직 2개의 문자(\xfcbe, r)를 가진다. 대부분의 컴파일러는 후자의 문자열에 반대하는데(obect to), 왜냐하면 16진 escape는 보통 \x0 - \xff로 범위가 한정되어있기 때문이다.

Continuing a String Literal

만약 문자열 리터럴이 너무 길어서 한 행에 딱 맞춰 들어가지 않았을 때, C언어는 문자열 리터럴을 다음줄에 계속 이어지게 하도록 한 행의 끝부분에 붙이는 backslash 문자(\)를 을 허용한다. (보이지않는)개행 문자가 끝에 오는 것을 제외하고는 \뒤에 어떠한 문자도 같은 행에 잇따를 수 없다.

printf("When you come to a fork in the road, take it.   \
--Yogi Berra");

일반적으로, \ 문자는 두개 또는 그 이상의 행을 한 행에 넣어야 할 때에 사용된다(C표준에서 이를 "splicing"이라고 함). splicing의 예시는 Section 14.3에서 더 자세하게 볼 것이다.
\ 기술은 약점을 하나 가지고 있따. 문자열이 반드시 다음행의 처음에 와야한다는 점인데, 그래서 프로그램의 들여쓰기 구조를 부순다. 다행히 어떤 규칙 덕분에 긴 문자열 리터럴을 처리하는 더 좋은 방법이 있다. 2개 또는 그 이상의 문자열 리터럴이 인접했을 때(공백으로만 분리되었을 때), 컴파일러는 이를 한 문자열로 만든다. 이 규칙은 문자열 리터럴을 2개 또는 그 이상의 행으로 분리할 수 있도록 허용한다.

printf("When you come to a fork in the road, take it. "
       "--Yogi Berra");

How String Literals Are Stored

우리는 printfscanf의 호출 안에서 종종 문자열 리터럴을 사용해왔다. 그러나 우리가 printf를 호출하고 argument로써 문자열 리터럴을 제공하였을 때, 우리가 실제로 전달한 것은 무엇일까?
이 질문에 대답하기 위해서는, 우리는 문자열 리터럴이 어떻게 저장되는지 알 필요가 있다.
본질적으로, C언어는 문자열 리터럴을 문자 배열로써 처리한다. C 컴파일러가 프로그램 안에서 n 길이의 문자열 리터럴을 마주했을 때, 문자열을 위해 n + 1 bytes의 메모리 공간을 설정할 것이다. 이 메모리의 영역은 문자열의 문자들을 포함할 것이고, 거기에 문자열의 끝을 표시하기 위해 하나의 추가적인 문자를 더 포함한다. 바로 null character이다.
null 문자는 bit가 모두 0인 바이트이고, 그래서 escape sequence \0으로 표현된다.

null 문자('\0')과 0 문자('0')를 혼동하면 안된다. null문자는 0의 코드를 가지지만, 0 문자는 다른 코드를 가진다(ASCII에선 48).

예를 들어 문자열 리터럴 "abc"는 4개의 문자의 배열로써 저장된다(a, b, c, \0).

문자열 리터럴은 비어있을 수 있다. 문자열 ""는 단일 null 문자로써 저장된다.

문자열 리터럴은 배열로써 저장되기 때문에, 컴파일러는 문자열 리터럴을 char *자료형의 포인터로써 처리한다. 예를 들어 printfscanf 둘 다 첫번째 argument로써 char * 자료형의 값을 요구한다. 아래의 예시를 보자.

pritnf("abc");

printf가 호출되었을 때, printf"abc"의 주소가 전달된다(메모리 안에 단어 a가 저장된 곳을 가리키는 포인터).

Operations on String Literals

일반적으로, C언어가 char * 포인터를 허용하는 어디든 문자열 리터럴을 사용할 수 있다. 예를 들어, 문자열 리터럴은 대입의 오른쪽 부분에서도 나타날 수 있다.

char *p;

p = "abc";

이 대입은 "abc"를 복사하는 것이 아니다. 단순히 p가 문자열의 첫번째 문자를 가리키도록 만든다.
C언어는 포인터가 subscripted되는 것을 허용하고, 그래서 우리는 문자열을 subscript할 수 있다.

char ch;

ch = "abc"[i];

ch의 새로운 값은 단어 b가 될 것이다. 다른 가능한 subscript는 0(단어 a), 2(단어 c), 3(null 문자)이다. 문자열 리터럴의 이러한 특성은 자주 사용되지 않는데, 때때로 편리하다. 0과 15사이의 문자를 동일한 16진수로 바꾸는 아래의 함수를 보자.

char digit_to_hex_char(int digit)
{
    return "0123456789ABCDEF"[digit];
}

문자열 리터럴을 수정하려고 시도하는 것은 undefined behavior이다.

char *p = "abc";

*p = 'd';    /*** WRONG ***/

문자열 리터럴을 변경하려는 프로그램은 충돌하거나 예상지못한 동작을 일으킬 수 있다.

String Literals versus Character Constants

단일 문자를 포함하는 문자열 리터럴은 문자 상수와 동일하지 않다. 문자열 리터럴 "a"는 문자 a를 포함하고 있는 메모리 장소를 가리키는 포인터로 표현된다. 문자 상수 'a'는 정수에 의해 표현된다(문자의 숫자 코드).

문자열을 필요로 할 때 문자를 사용하면 안된다(그 반대도 마찬가지다).

printf("\n");

위의 구문은 규칙에 맞는데, 왜냐하면 printf가 첫번째 argument로써 포인터를 요구하기 때문이다. 그러나 아래의 호출은 규칙에 어긋난다.

printf('\n');    /*** WRONG ***/

2. String Variables

어떤 프로그래밍 언어는 문자열 변수를 선언하는 것에 특별한 string 자료형을 제공한다. C언어는 다른 정책을 가지고 있다. 문자의 어떤 one-dimensional 배열은 문자열을 저장하는 것에 사용될 수 있는데, 문자열은 null 문자로 끝나는 것으로 이해된다. 이러한 접근은 간단한데, 큰 어려움을 가지고 있다. 문자의 배열이 문자열로써 사용되었는지 전달하는 것이 어렵다. 만약 우리가 문자열 조작 함수를 작성했다면, 우리는 null 문자를 적절하게 처리하는 것에 주의를 기울여야 한다. 또한, 문자열의 길이를 결정하는 방법으로 null 문자를 한글자씩 찾는 것보다 빠른 방법은 없다.
80문자의 문자열까지 저장할 수 있는 변수를 필요로 한다고 생각해보자. 문자열이 끝에 null 문자를 필요로 하기 때문에, 우리는 81문자의 배열이 되도록 변수를 선언해야한다.

#define STR_LEN 80
...
char str[STR_LEN+1];

우리는 STR_LEN을 81이 아닌 오히려 80으로 정의했는데, 그래서 str이 80문자보다 더 많이 저장할 수 없다는 것을 강조했다. 그리고 str의 선언에서 STR_LEN에 1을 추가했다. 이것은 C 프로그래머들 사이에서 일반적인 행동이다.

문자열을 저장하는 것에 사용되는 문자의 배열을 선언할 때에는, 항상 문자열보다 한 글자 더 긴 배열을 만들어야 하는데, 이는 모든 문자열은 null 문자로 종료되어야 한다는 C언어의 관습때문이다. null 문자가 들어갈 공간을 남기지 않는 것은 프로그램이 실행되었을 때 예측할 수 없는 결과를 야기하는데, C 라이브러리의 함수는 문자열들이 null로 종료된다고 가정하기 때문이다.
STR_LEN + 1만큼의 길이를 가지는 문자 배열을 선언하는 것은 문자 배열이 항상 STR_LEN 길이의 문자를 포함하는 것은 아니다. 문자열의 길이는 null 문자로 끝나는 위치에 의존하고 문자열이 저장될 수 있는 배열의 길이에 영향을 받지는 않는다. STR_LEN + 1 문자의 배열은 빈 문자열부터 STR_LEN의 길이의 문자열까지 다양한 길이의 문자열을 저장할 수 있다.

Initializing a String Variable

문자열 변수는 선언되는 동시에 초기화될 수 있다.

char date1[8] = "June 14";

컴파일러는 "June 14"로부터 문자를 date1 배열 안에 넣을 것이다. 그 후 date1이 문자열처럼 사용될 수 있도록 null 문자를 추가한다. date1은 아래처럼 보일 것이다.

비록 "June 14"가 문자열 리터럴처럼 보이겠지만, 이는 문자열 리터럴이 아니다. 대신 C는 배열 initializer에 대한 축약으로 이를 본다. 사실 우리는 아래와 같이도 쓸 수 있다.

char date1[8] = {'J', 'u', 'n', 'e', ' ', '1', '4', '\0'};

"June 14"라고 사용하는 것이 배열 initializer를 그대로 사용하는 것보다 읽기 쉽게 보인다는 것에 충분히 동의할 것이라고 생각한다.
만약 initializer가 문자열 변수를 채우기에 너무 짧다면 어떻게 될까? 이 경우에는 컴파일러가 null 문자들을 추가하게 된다.

char date2[9] = "June 14";

그래서 위의 선언에서 date2는 아래와 같은 모습을 보인다.

이 동작은 C언어가 일반적으로 array initializer를 다루는 것과 일관된다. array initializer가 배열 자체보다 짧다면, 남은 요소들은 0으로 초기화된다. 나머지 문자 변수의 요소를 \0으로 초기화하는 것으로, 컴파일러는 같은 규칙을 따른다.
만약 initializer가 문자열 변수보다 길면 어떻게 될까? 이는 문자열에 대한 규칙에 어긋난다. 그러나 C언어는 initializer가(null 문자를 세지 않고) 변수와 같은 길이를 가지게 하는 것을 허용한다.

char date3[7] = "June 14";

null 문자가 들어갈 공간이 없는데, 그래서 컴파일러는 null 문자를 저장하는 시도를 하지 않는다.

만약 문자 배열이 문자열을 가지도록 초기화할 예정이라면, initializer의 길이보다 배열의 길이가 더 길도록 해야한다. 그렇지 않는다면 컴파일러는 조용하게 null 문자를 생략할 것이고, 배열은 문자열로써 사용될 수 없다.

문자열 변수의 선언은 길이를 생략할 수 있는데, 이 경우에는 컴파일러가 길이를 계산한다.

char date4[] = "June 14";

컴파일러는 "June 14"에 null 문자를 더한 문자들을 저장하기 충분한 공간인 8개의 문자의 공간을 date4에 설정한다.(명시되지 않은 date4의 길이가 배열의 길이가 나중에 바뀔 수 있다는 것을 의미하지는 않는다. 프로그램이 컴파일되고 나면, date4의 길이는 8로 고정된다) 문자열의 길이를 생략하는 것은 특히 initializer가 길 때 유용한데, 손으로 길이를 하나씩 계산하는 것은 에러를 만들어내기 쉽기 때문이다.

Character Arrays versus Character Pointers

char date[] = "June 14";

char *date = "June 14";

위의 선언과 아래의 선언을 비교해보자.
위의 선언은 date를 배열로써 선언했고, 아래의 선언은 date를 포인터로써 선언하였다. 배열과 포인터 사이의 밀접한 관계성 덕분에, 우리는 위와 아래의 date 모두 문자열로써 사용이 가능하다. 특히, 문자 배열이나 문자 포인터를 전달받기를 요구하는 어떤 함수든지 위와 아래의 date를 argument로써 사용할 수 있다.
그러나, 우리는 date의 두 가지 버전(version)이 상호교환적이라고 생각하지 않도록 조심해야한다. 아주 큰 차이점이 존재한다.

  • 배열의 version은, 어떤 배열의 요소처럼 date 안에 저장된 문자들이 수정가능하다. 포인터의 version은, date가 문자열 리터럴을 가리키는데, Section 13.1에서 보았듯 문자열 리터럴은 수정이 불가능하다.
  • 배열의 version은, date가 배열 이름이다. 포인터 version은, date가 프로그램 실행동안에 다른 문자열을 가리키도록 할 수 있는 변수이다.

만약 수정이 가능한 문자열을 원한다면, 문자열을 저장하는 문자의 배열을 설정해야할 책임이 있고, 포인터 변수를 선언하는 것은 이에 부합하지 않다.

char *p;

위의 선언은 컴파일러가 포인터 변수에 대한 충분한 메모리를 설정하도록 한다. 불행하게도 이것은 문자열에 대한 공간을 할당하는 것이 아니다(이게 어떻게 가능한 것인가? 우리는 문자열이 얼마나 긴지 나타내지 않았다). p가 문자열로써 사용할 수 있기 이전에, p는 반드시 문자의 배열을 가리켜야 한다. p가 문자열 변수를 가리키도록 만드는 하나의 방법은 아래와 같다.

char str[STR_LEN+1], *p;

p = str;

p는 이제 str의 첫 번째 문자를 가리키게 되고, 그래서 p를 문자열로써 사용할 수 있는 것이다. 또다른 방법은 p가 동적으로 할당된(dynamically allocated) 문자열을 가리키도록 하는 것이다.

초기화되지 않은 포인터 변수를 문자열로써 사용하는 것은 심각한 에러이다. 문자열 "abc"를 만드는 시도인 아래의 예제를 보자.

char *p;
p[0] = 'a';    /*** WRONG ***/
p[1] = 'b';    /*** WRONG ***/
p[2] = 'c';    /*** WRONG ***/
p[3] = '\0';   /*** WRONG ***/

p가 초기화되지 않았기 때문에, 우리는 p가 어디를 가리키고 있는지 모른다. a, b, c, \0 문자들을 p가 가리키는 메모리에 넣으려고 하는 것은 undefined behavior를 일으킨다.

3. Reading and Writing Strings

printfputs 함수를 사용하여 문자열을 쓰는 것은 쉽다. 문자열을 읽는 것은 조금 어려운데, 주로 문자열 변수가 저장할 수 있는 것보다 더 긴 문자열 입력을 받을 가능성때문이다. 첫번째 단계에서 문자열을 읽기 위해, 우리는 scanfgets를 사용할 수 있다. 여기에 대한 한 대안으로써, 문자열을 한 문자씩 읽을 수도 있다.

Writing Strings Using printf and puts

%s 변환 명시자는 printf가 문자열을 쓸 수 있도록 해준다. 아래의 예시를 보자.

char str[] = "Are we having fun yet?";

printf("%s\n", str);

결과는 아래와 같다.

Are we having fun yet?

printf는 null 문자를 만날 때까지 문자열 안에서 한 글자씩 문자를 쓴다. (만약 null 문자가 없다면, printf는 문자열의 끝부분을 넘어가서 메모리의 어딘가에 있는 null 문자를 찾을 때 까지 계속된다.)
문자열의 일부를 출력하기 위해서, 우리는 변환명시자 &.ps를 사용할 수 있는데, p는 출력될 문자의 숫자이다.

printf("%.6s\n", str);

위의 구문은 아래를 출력한다.

Are we

숫자와 같이, 문자열은 영역 안에서 출력될 수 있다. %ms 변환은 m 크기의 영역 안에서 문자열을 출력할 것이다. (m 문자보다 더 긴 문자열은 잘리지 않고 전부 출력된다.) 만약 문자열이 m 문자보다 짧다면 영역 내에서 오른쪽 정렬된다. 대신 왼쪽 정렬을 강제하기 위해서, 우리는 m앞에 - 기호를 붙이면 된다. mp는 조합되어 사용될 수 있는데, %m.ps의 형태의 변환 명시자는 크기 m의 영역 안에서 p만큼의 문자가 문자열에서 출력되게 한다.
printf만이 문자열을 쓸 수 있는 함수는 아니다. C 라이브러리는 puts 또한 제공하는데, 이는 아래와 같은 방식으로 사용한다.

puts(str);

puts는 오직 하나의 argument만 가진다(출력될 문자열). 문자열을 쓴 뒤, puts는 개행 문자를 추가적으로 쓰며, 그래서 다음 행의 시작부분부터 다음 결과가 출력된다.

Reading Strings Using scanf and gets

%s 변환 명시자는 scanf가 문자 배열로 문자열을 읽도록 한다.

scanf("%s", str);

scanf의 호출에서, str의 앞에 & 연산자를 앞에 넣을 필요가 없다. 어떤 배열이름처럼, str도 함수로 전달될 때 포인터로써 다루어진다.
scanf가 호출되었을 때, 공백 문자를 스킵할 것이고, 그 후 str 안에서 공백 문자를 만날때 까지 문자를 읽는다. scanf는 항상 문자열의 끝에 null 문자를 저장한다.
scanf를 사용하여 읽은 문자열은 공백 문자를 절대로 포함할 수 없다. 결과적으로 scanf는 일반적으로 입력한 모든 행을 전부 읽지 않는다. 개행문자는 scanf가 읽는 것을 막도록 하며, 스페이스(space) 문자나 탭(tab) 문자 또한 scanf가 읽는 것을 막는다. 한번에 입력한 모든 행을 읽기 위해서, 우리는 gets를 사용할 수 있다. scanf처럼 gets함수도 입력 문자들을 배열에 읽고 null 문자를 저장한다. 그러나 다른 측면에서, getsscanf와 다른 점들이 있다.

  • gets는 문자를 읽기 시작했을 때 공백 문자를 스킵하지 않는다(scanf는 스킵한다).
  • gets는 개행문자를 찾을 때까지 읽는다(scanf는 어떠한 공백 문자가 나오든 멈춘다). 우연히 gets가 개행문자를 배열에 저장하는 대신에 폐기했다면, null 문자가 그 자리를 대체하게 된다.

scanfgets 사이의 차이를 보기 위해서 프로그램의 일부인 아래의 예시를 보자.

char sentence[SENT_LEN+1];

printf("Enter a sentence:\n");
scanf("%s", sentence);
Enter a sentence:

위의 prmopt 이후에 사용자는 아래의 행을 입력했다고 생각해보자.

    To C, or not to C: that is the question.

scanf"To"sentence에 저장할 것이다. 다음 scanf의 호출 때 To 이후의 공간을 읽을 것이다.
이제 scanfgets가 대신했다고 생각해보자.

gets(sentence);

사용자가 전과 동일한 입력값을 넣었다면 gets은 입력한 문자열인 아래의 문자열을 전부 sentence에 넣을 것이다.

"    To C, or not to C: that is the question."

scanfgets는 꽉 찼는지 확인할 방법이 없다. 결과적으로, 배열을 넘어서 문자를 저장할 수도 있는데, 이는 undefined behavior이다. scanf는 변환명시자 %s대신에 %ns를 사용했을 때 더 안전해지는데, n은 저장될 문자의 최대 개수를 나타낸다. 불행하게도 gets는 본질적으로 안전하지 않다. fgets가 더 좋은 대안이 될 것이다.

Reading Strings Character by Character

scanfgets 둘다 위험요소가 있고, 다양한 적용에 유연하지 못하기 때문에 C 프로그래머들은 종종 자기들이 직접 입력 함수를 만들었다. 문자열에서 한 문자씩 읽는 것으로, 표준 입력 함수보다 더 좋은 정도의 제어를 얻을 수 있게 함수를 만들었다.
만약 우리가 우리만의 입력 함수를 설계한다고 결정했으면, 우리는 아래의 항목들을 고려해야 한다.

  • 문자열 저장을 시작하기 전에 공백 문자르 스킵해야하는가?
  • 함수가 읽는 것을 멈추기 위해 어떤 문자를 사용해야 하는가? 개행문자, 어떤 공백 문자, 아니면 어떤 문자를 사용할까? 그리고 이 문자는 문자열에 저장할까 아니면 폐기할까?
  • 입력 문자열이 저장하기에 너무 길다면 함수가 어떤 동작을 취해야할까? 나머지 문자들을 모두 버리거나, 아니면 이 문자들을 다음 입력 작동을 위해 남길것인가?

우리가 공백 문자를 스킵하지 않고, 첫번째 개행 문자에서 읽는 것을 멈추며, 이는 문자열에 저장되지 않고, 나머지 문자들을 모두 버리는 함수를 필요로 한다고 가정해보자. 그 함수는 아래의 prototype을 가질 수 있다.

int read_line(char str[], int n);

str은 입력값을 저장할 배열을 나타내고, n은 읽을 수 있는 문자의 최대 개수를 나타낸다. 만약 입력 행이 n 문자보다 더 많이 포함하고 있다면, read_line은 추가적인 문자들을 폐기한다. str이 실제로 저장한 문자의 개수를 read_line이 반환하도록 할 것이다(이 숫자는 0에서 n까지의 범위). read_line의 반환값을 항상 필요로 하지 않지만 필요할 때 이용가능하도록 만들 것이다.
주로 단일 문자를 읽어 공간이 있다면 str안에 문자를 저장할 수 있도록 getchar를 호출하는 루프로 read_line을 구성할 것이다. 이 루프는 개행문자가 읽혔을 때 종료된다(엄밀히 말하자면, getchar가 문자를 읽는 것에 실패했을 때도 루프가 종료되도록 해야할 것이다). 아래에 read_line의 완전한 정의가 있다.

int read_line(char str[], int n)
{
    int ch, i = 0;
    
    while ((ch = getchar()) != '\n')
        if (i < n)
            str[i++] = ch;
    str[i] = '\0';           /* terminates string */
    return i;                /* number of characters stored */
}

chchar 자료형이 아닌 int 자료형을 가졌다는 점을 주목해야 하는데, getcharint 값으로써 읽히는 문자를 반환하기 때문이다.
반환하기 전에, read_line은 문자열의 끝에 null 문자를 넣는다. scanfgets와 같은 표준 함수들은 자동적으로 입력 문자열의 끝에 null 문자를 넣는다. 우리가 우리만의 입력 함수를 작성하고 있다면 반드시 여기에 대한 책임이 있을 것이다.

4. Accessing the Characters in a String

문자열들이 배열로써 저장되기 때문에, 우리는 문자열 내부의 문자들에 접근하기 위해 subscripting을 사용할 수 있다. 예를 들어, 문자열 s안에 있는 모든 문자를 처리하기 위해 우리는 카운터 i를 증가시키고, 표현식 s[i]를 통해 문자를 선택할 수 있는 루프를 설정해야 할 것이다.
문자열 안에 공백의 개수를 세는 함수가 필요하다고 가정해보자. array subscripting을 사용하여 우리는 아래와 같은 방법으로 함수를 작성할 수 있다.

int count_spaces(const char s[])
{
    int count = 0, i;
    
    for (i = 0; s[i] != '\0'; i++)
        if (s[i] == ' ')
            count++;
    return count;
}

s의 선언 앞에 const를 붙여 count_spacess가 나타내는 배열을 바꾸지 않음을 나타냈다. 만약 s가 문자열이 아니라면, 함수는 배열의 길이를 명시하는 두번째 argument가 필요할 것이다. 그러나 s가 문자열이기 때문에, count_spaces는 null 문자를 검사하는 것으로 문자열이 어디서 끝나는지 결정할 수 있다.
많은 C 프로그래머들은 위의 예시처럼 count_spaces를 작성하지 않는다. 대신에, 배열 내부의 현재 위치를 추적하는 포인터를 사용한다. Section 12.2에서 보았던 것처럼, 이 기술은 배열을 처리하는 것에 항상 가능하고, 문자열에서 작업하는 것에 특히 편리하다는게 입증되어 있다.
array subscripting이 아닌 포인터 연산을 사용하여 count_spaces 함수를 다시 작성해보자. 변수 i를 제거하고 함수 내부를 추적할 수 있도록 s 자체를 사용할 것이다. s를 반복적으로 증가시키는 것으로, count_spaces는 문자열 안의 각각의 문자들을 단계적으로 통과할 것이다. 아래의 함수가 새로운 version의 함수이다.

int count_spaces(const char *s)
{
    int count = 0;
    
    for (; *s != '\0'; s++)
        if(*s == ' ')
            count++;
    return count;
}

constcount_spacess를 수정하는 것을 막지 않다는 점을 주목해야 한다. 이 const는 함수가 s가 가리키는 것을 수정하는 것으로부터 보호한다. 그리고 scount_spaces에 전달된 포인터의 복사본이기 때문에, s를 증가시키는 것은 원래의 포인터에 어떠한 영향도 미치지 않는다.
count_spaces 예시는 어떻게 문자열 함수를 작성해야 하는지에 대한 여러 질문이 떠오르게 한다.

  • 문자열 내부의 문자들에 접근하기 위해 배열(array operation)과 포인터(pointer operation) 중 어떤 것을 사용하는 것이 더 좋은가? 더 편리한 것을 선택하면 된다. 우리는 심지어 이 두 개를 섞어서 쓸 수도 있다. count_spaces의 두번째 version에서, s를 포인터로써 다루는 것은 변수 i의 필요성을 제거하는 것으로 저금더 함수가 간단해지게 만들었다. 전통적으로, C 프로그래머들은 문자열을 처리하는 것에 포인터의 작용을 이용하는 경향이 있다.

  • 문자열 parameter를 배열 또는 포인터로 선언해야 하는가? count_spaces의 두가지 version은 선택지를 보여준다. 첫번째 version은 s를 배열로 선언했다. 두번째 version은 s를 포인터로 선언했다. 실질적으로 두개의 선언 사이에는 차이점이 없다. Section 12.3에서 컴파일러는 array parameter를 처리할 때 포인터로 선언된 것처럼 처리한다고 했었던 것을 떠올려보면 된다.

  • parameter (s[] 또는 *s)의 형태는 argument로써 전달되는 것에 영향을 주는가? 영향을 주지 않는다. count_spaces가 호출되었을 때, argument는 배열의 이름, 포인터 변수, 또는 문자열 리터럴일 것이다. count_spaces는 이것들에 대한 차이점을 구분할 수 없다.

profile
저장용

0개의 댓글