13.5 Using the C String Library


몇몇 프로그래밍 언어들은 문자열을 복사, 비교, 연결, 부분집합 선택하는 것 등이 가능하다. 그러나 C의 연산자들은 근본적으로 문자열에 대해서는 쓸모가 없다. 문자열이 기본적으로 배열이기 때문에, 배열이 가지고 있는 제한과 마찬가지로 문자열도 제한되어 있다. 특히, 연산자를 이용해 복사하거나 비교할 수 없다.


char str1[10], str2[10];

str1 = "abc";    /*** WRONG ***/

str2 = str1;    /*** WRONG 배열의 이름은 lvalue가 될 수 없음 ***/


char str1[10] = "abc";    // legal

if (str1 == str2)...    /*** WRONG 포인터 끼리의 비교임***/


다행히 C에서는 string.h 헤더를 통해 문자열에 관한 다양한 함수를 제공한다.

#include <string.h>


string.h에 정의되어 있는 함수들은 대부분 최소한 하나의 문자열 argument가 있어야 한다. String parameter는 type char * 로 선언되며, 문자 배열, char * type의 변수, 스트링 리터럴이 argument가 될 수 있다.


이제부터 나오는 예제에서 str1str2는 문자 배열(string)으로 사용된다.



The strcpy(String Copy) Function


char *strcpy(char *s1, const char *s2);


문자열 s2를 문자열 s1으로 복사한다. (엄밀히 표현하면, s2가 가리키는 문자들 - 첫번째로 만나는 null character까지 - 을 s1이 가리키는 배열로 복사한다.


strcpy(str2, "abcd");    /* str2 now contains "abcd" */

strcpy(str1, str2);    /* str1 now contains "abcd" */


 strcpy의 리턴 값은 버리는 경우가 대부분이다. 다음과 같은 경우엔 리턴 값을 사용하기도 한다.


strcpy(str1, strcpy(str2, "abcd"));    /* both str1 and str2 now contain "abcd" */


이 함수는 각 문자열 변수의 길이를 체크해 주지 않는다. str1이 가리키는 배열의 길이가 n이라면, str2가 가리키는 문자열의 길이가 n-1을 넘어서는 안 된다(마지막의 null character를 감안). 만약 str1 배열의 길이가 str2가 가리키는 문자열의 길이보다 적다면, undefined behavior가 발생한다.


strncpy 함수는 더 느리지만 더 안전하다. strcpy와 비슷하지만 세번째 인자를 통해 복사될 문자의 갯수를 제한한다.


strncpy(str1, str2, sizeof(str1));    // ①


strncpy(str1, str2, sizeof(str1) - 1);    // ②

str1[sizeof(str1) - 1] = '\0';

① str1이 str2에 저장된 문자열을 저장하기에 충분하다면, 복사는 정상적으로 이뤄진다. 만약 str1에 자리가 부족하다면, 자리만큼만 복사가 이뤄지는데 한가지 문제는 str1은 null character로 끝나지 못한다. 

②는 str1이 항상 null character로 끝나는 것을 보장해 주는 더 안전한 방법이다.



The strlen(String Length) Function


size_t strlen(const char *s);


size_t는 C library에 정의되어 있으며 a typedef name that represents one of C's unsigned integer types.


아주 매우 긴 문자열을 다루지 않는다면, 이것이 기술적으로 문제되지는 않는다. 이 값을 integer로 저장해도 무방하다.

strlen 함수는 문자열 s의 길이를 리턴한다 - 첫번째 null character가 나오기 직전까지의 문자의 갯수 - 


int len;

len = strlen("abc");    /* len is now 3 */

len = strlen("");    /* len is now 0 */

strcpy(str1, "abc");

len = strlen(str1);    /* len is now 3 */



The strcat(String Concatenation) Function


char *strcat(char *s1, const char *s2);


s2의 내용을 s1의 끝에 더해 주고, s1을 반환한다.


strcpy(str1, "abc");

strcat(str1, "def");    /* str1 now contains "abcdef" */

strcpy(str1, "abc");

strcpy(str2, "def");

strcat(str1, str2);    /* str1 now contains "abcdef" */

 

strcpy와 마찬가지로 strcat의 리턴 값은 보통 버려지지만, 다음과 같이 사용될 수도 있다.


strcpy(str1, "abc");

strcpy(str2, "def");

strcat(str1, strcat(str2, "ghi"));

/* str1 now contains "abcdefghi", str2 contains "defghi" */

strncat 함수는 strcat의 더 안전하지만 느린 버전이다. 


strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);



The strcmp(String Comparison) Function


int strcmp(const char *s1, const char *s2);


strcmp 함수는 string s1과 s2를 비교해서 0보다 작거나, 같거나, 혹은 더 큰 값을 리턴한다.

값을 리턴하는 기준은 lexicographic ordering이다. 


다음과 같은 경우 s1<s2라고 판단한다.

- s1, s2의 첫번째 i개의 문자가 동일(match)하나, i+1번째 문자를 비교했을 때 s1의 것이 s2의 것보다 작다. "abc"는 "bcd"보다 작고, "abd"는 "abe"보다 작다.

- s1의 모든 문자가 s2에 match되지만, s1이 s2보다 짧은 경우. "abc"는 "abcd"보다 작다.


문자 비교시 문자의 numerical code가 기준이다. 아스키 코드가 가지는 몇가지 특징은 다음과 같다.

- A-Z, a-z, 0-9의 연속된 문자들은 코드도 연속적이다.

- 대문자는 소문자보다 작다(ASCII에서 대문자는 65~90이고, 소문자는 97~122이다).

- 숫자는 문자보다 작다(48~57)

- 공백은 다른 모든 'printing characters'보다 작다(공백은 32).

  

Remind.c

  

  

13.6 String Idioms


string을 조작하는 방법 중 가장 유명한 idiom들을 이용해서 strlen과 strcat 함수를 직접 써 본다. 물론 이 함수들은 표준 라이브러리에 들어 있으므로 실무에서 함수를 직접 쓸 일은 없다. 하지만 간결한 스타일을 배우기 위해 여기서는 써 보는 것.


함수를 새로 쓸 때는 라이브러리 안에 있는 함수의 이름과 같은 함수를 쓸 수 없으므로, 이름을 좀 바꿔야 한다(ex. strlen -> my_strlen).


Searching for the End of a String


size_t strlen(const chr *s)    

{

size_t n;

for (n = 0; *s != '\0'; s++)

n++;

return n;

}


포인터 s가 왼쪽에서 오른쪽으로 움직임에 따라, n은 문자가 몇개나 있었는지를 저장한다. s가 null character를 가리키게 되면, n의 값은 전체 문자열의 길이가 된다. 


size_t strlen(const char *s)

{

size_t n = 0;

for(; *s != '\0'; s++)

n++;

return n;

}


*s!='\0' 과 *s!=0 은 동일한데, null character의 정수 값이 0이기 때문이다. 또한 *s!=0인지 확인하는 것은 *s를 테스팅하는 것과 같다.


size_t strlen (const char *s)

{

size_t n = 0;

for (; *s; s++)

n++;

return n;

}


12.2에서 보았듯이 하나의 expression으로 s를 증가시키면서 *s를 test할 수 있다.


size_t strlen (const char *s)

{

size_t n = 0;

for(; *s++;)

n++

return n;

}

  

for 문에 들어가는 세개의 expr 중에서 가운데 있는 탈출조건만 남아있기 때문에, while문으로 대체할 수 있다.


size_t strlen(const char *s)

{

size_t n = 0;

while (*s++)

n++;

return n;

}

지금까지의 변형은 속도와는 관계가 없으나, 아래 버전은 (어떤 컴파일러에서는) 더 빠르다.


size_t strlen(const char *s)

{

const char *p = s;

while (*s)

s++;

return s - p;

}


이 방법은 null character가 있는 위치에서 string의 첫번째 문자의 위치를 빼서 길이를 계산한다. while loop 안에서 길이를 1씩 증가시키지 않기 때문에 더 빠르다. p를 정의할 때 const가 있어야 한다. Without it, the compiler would notice that assigning s to p places the string that s points to at risk. VS2015에서는 C4090 경고를 해 준다.

  

while (*s)

s++;


while (*s++)

;

 

첫번째 idiom은 string의 끝에 있는 null character를 찾는다. s는 null character를 가리키게 된다.

두번째 idiom은 string 끝의 null character 바로 다음을 가리킨다.



Copying a String

 

strcat 함수의 두가지 버전


직관적이지만, 좀 긴 버전


char *strcat(char *s1, const char *s2)

{

char *p = s1;

while (*p != '\0')

p++;

while (*s2 != '\0')    {

*p = *s2;

p++;

s2++;

}

*p = '\0';

return s1;

}


이 버전은 두 단계의 알고리즘을 사용한다

(1) s1 끝의 null character를 찾아서 p가 그 위치를 가리키게 한다.

(2) s2가 가리키는 문자들을 p로 하나씩(one by one) 복사한다.


strlen을 간결하게 만들었듯이, 이 함수도 간결하게 만들어 보자.


char *strcat(char *s1, const char *s2)

{

char *p = s1;

while (*p)

p++;

while (*p++ = *s2++)

;

return s1;

}


위 코드의 핵심은 "string copy" idiom이다.


while (*p++ = *s2++)

;


두 개의 postfix increment operator를 제외하면, 

*p = *s2

이 표현식은 s2가 가리키고 있는 문자를 복사해서 p가 가리키는 곳에 넣는다. 이 할당이 끝나면, p와 s2는 increment된다.

이 것을 반복해서 s2가 가리키고 있는 문자들을 p가 가리키는 곳으로 계속 복사한다.

루프가 끝나는 것은? while statement는 assignment가 끝난 후의 값을 테스트 한다. 즉 복사되는 문자의 값이 test된다. null character를 제외한 모든 문자들은 true를 반환하므로, 루프는 null character가 한 번 복사된 이후에 끝나게 된다. 따라서 마지막에 따로 null character를 집어넣을 필요가 없다.



13.7 Array of Strings


문자열의 배열을 생각할 때, 문자의 2차원 배열 - remind.c에서 했던 것처럼 - 로 저장하는 것이 바로 떠오르는 해결책이지만 이 방법은 메모리 낭비를 초래함. 문자열들의 길이가 제각각인데 가장 긴 문자열을 기준으로 배열의 크기를 지정해야 하므로, 나머지 문자열을 저장할 때 '\0'을 불필요하게 많이 저장하게 된다.

char planets[][8] = { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto" };


 이 비효율성을 해결하기 위해서 필요한 것은,

ragged array: 각각의 행이 서로 다른 길이를 가지고 있는 이차원 배열


C에서 ragged array type을 제공하지는 않으나, 만들어 낼 수 있다.

char *planets[] = { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto" };


planets을 2차원 배열 대신 포인터로 만들었을 뿐이지만 그 차이는 엄청나게 크다. planets의 각 원소는 null-character로 끝나는 문자열이며, 더 이상 낭비되는 문자 공간이 없다. 

각각의 행성 이름에 접근할 때는 plantets에 subscript하기만 하면 된다. 포인터와 배열의 관계 덕분에, 행성 이름에 접근하는 것은 2차원 배열의 원소에 접근하는 것과 동일한 방법으로 하면 된다.



Command-Line Arguments


프로그램을 실행할 때 추가적인 정보를 써넣기도 한다. 파일명이나 추가적인 실행 지침 등

UNIX의 ls command를 예로 들면

ls를 command line에 입력하면 현 디렉토리 내의 파일명을 보여준다.

ls -l을 입력하면 파일명과 자세한 정보(파일 크기, 소유자, 수정 시간 등)을 보여준다.

ls -l remind.c 와 같이 단 하나의 파일에 대한 자세한 정보를 출력할 수도 있다.


위와 같이 command-line arguments (C표준으로는 program parameters)를 사용하기 위해서는 다음과 같이 main 함수를 정의한다.


int main(int argc, char *argv[])

{ ... }


argc("argument counter"): number of command-line arguments

argv("argument vector"): array of pointers to the command-line arguments, which are stored in string form.


argv[0]은 프로그램의 이름을 가리킨다.

argv[1]부터 argv[argc-1]은 나머지 command-line argument를 가리킨다.


argv[argc]는 언제나 null pointer - 아무것도 가리키지 않는 포인터 -다. 이에 대한 자세한 내용은 17.1에서 다룬다. 일단은 macro NULL을 null pointer로 쓴다.


사용자가 ls -l remind.c 라고 입력하면

argc는 3, argv[0]는 프로그램의 이름, argv[1]은 문자열 "-l"의 포인터, argv[2]는 문자열 "remind.c"의 포인터, argv[3]은 null pointer가 된다.


parameter로 문자열(들)을 받아서 행성의 이름인지 확인하는 프로그램




Q&A


string literal의 길이 제한은 C89에선 최소 509글자, C99 에선 최소 4095글자이다.

string literal의 포인터이고 컴파일러는 그에 대한 수정 시도를 막지 못하기 때문에 string constants라고 불리지 않는다.

string literal을 수정하는 것이 위험해 보이지 않은데 어째서 undefined behavior를 발생시키는가? 어떤 컴파일러는 메모리를 저장하기 위해 동일한 string literal을 하나만 저장하려고 한다. char *p = "abc", char *q = "abc"; 에서 "abc"를 한 번만 저장하고 p와 q가 모두 그 곳을 가리키게 한다. 또한, string literal은 메모리의 읽기 전용 구역에 저장될 수 있고 이에 대해 수정 시도하면 크래시를 일으킨다.

문자열이 아닌 문자들의 배열은 꼭 null character를 포함하지 않아도 된다.

printf와 scanf의 첫번째 인자는 string literal이 아닌 string variable도 올 수 있다.

printf의 인자에 문자열 변수를 넣는 것은 위험할 수 있는데, 변수 안에 %가 들어가면 함수는 그것을 conversion specification의 시작으로 간주한다.

만약 에러나 파일의 끝이어서 문자를 읽지 못하는 경우 getchar는 int type의 EOF(매크로)를 반환한다.

strlen이나 strcat같은 함수는 실제로는 최고의 효율성을 위해 어셈블리로 작성되는 경우가 많다.

command-line argument 대신 program parameter란 용어를 사용하는 것은 프로그램이 이제 마우스로 실행되는 경우가 많고 기타 정보가 직접 타이핑해서가 아닌 다른 방식으로 전달되는 경우가 많기 때문이다.

main 함수의 파라미터로 argc와 argv라는 이름을 사용하는 것은 convention이며 꼭 그렇게 하지 않아도 된다.

*arvg[] 대신 **arvg를 사용해도 된다.





+ Recent posts