지금까지 char 변수와 char의 배열을 이용해서 문자열(series of characters, a string in C terminology)을 표현했는데, 더 편리한 방법을 배운다.
String constant - C 용어로 literal. 한글로도 그냥 문자 리터럴이라고 표현함.
string variables - 프로그램 중간에 바뀔 수 있다.
13.1 - string literal에 관한 규칙
13.2 - how to declare string variables
13.3 - ways to read and write strings
13.4 - how to write functions that process strings
13.5 - string-handling functions in C library
13.6 - idioms related to strings
13.7 - how to set up arrays whose elements are pointers to strings of different lengths
13.1 String Literals
String literal은 따옴표(") 안에 문자열이 배치된 것이다.
"When you come to a fork in the road, take it."
string literal은 printf와 scanf함수를 다룰 때 이미 나온바가 있다.
string literal을 수정하려고 하면 undefined behavior가 발생한다.
char *p = "abc";
*p = "def"; /* WRONG */
Escape Sequences in String Literals
string literal은 ecape sequence(\n, \t, \a 등)을 포함할 수 있다.
"Candy\nIs dandy\n"
octal 또는 hexadecimal escape를 사용할 때 주의할 점
octal escape는 3자리 숫자까지 escape로 인식하고, 그 전에 non-octal character가 오면 끝낸다.
"/1234" -> /123과 4
"/189" -> /1, 8, 9
반면 hexadecimal escape는 몇자리까지에 대한 제한이 없다. non-hex character가 와야만 끝난다.
"/xfcber" -> /xfcbe, r
만약 über를 입력하고 싶다면 "/xfc" "ber"라고 쓴다.
Continuing a String Literal
string literal이 너무 길 때는, 마지막에 \를 쓰고 다음 줄로 넘어간다. \ 문자는 두개 이상의 줄을 하나의 줄로 인식하게 만들어 준다.
\ 문자 뒤에는 보이지 않는 개행문자 외에는 다른 문자가 와서는 안 된다.
그러나 \ 다음 줄에 반드시 문자가 와야 한다는 단점이 있다(그리고 탭 문자를 쓴 것처럼 띄어져서 출력된다)
printf("When you come to a fork in the road, take it. "
"--Yogi Berra");
위처럼 닫힌 따옴표 뒤에 열린 따옴표가 다시 나오고 그 사이에 whitespace 문자만 있다면, 컴파일러는 두 따옴표 안에 묶인 내용을 하나로 합쳐서 하나의 string으로 만든다.
How String Literals Are Stored
C는 string literal을 문자(char)의 배열인 것처럼 다룬다. C 컴파일러가 n의 길이를 가진 string literal을 만나면, 그 string을 위해서 n+1 바이트의 메모리를 할당한다. 문자들의 가장 뒤에 하나의 문자 - null character - 가 추가된다. null character는 모든 bit가 0인 byte로, escape sequence로 \0으로 표현된다.
"abc"라는 string literal은 a, b, c, \0 이렇게 네 개의 문자로 저장된다.
비어있는 string literal은 single null character로 저장된다.
string literal이 배열로 저장되므로, pointer of type char*로 다뤄진다. printf, scanf함수는 모두 첫번째 인자로 char* type을 받는다.
Operations on String Literals
일반적으로 C가 char * pointer를 허용하는 곳에서는 항상 string literal을 사용 가능하다. 예를 들어, string literal은 assignment 오른쪽에 올 수 있다.
char *p;
p = "abc";
이 assignment는 "abc"의 문자들을 복사하는 것이 아니며 p가 문자열의 첫 글자를 가리키게 할 뿐이다.
포인터에 대한 subscription이 가능하기 때문에, string literal에도 가능하다.
char ch;
ch = "abc"[1];
ch의 새로운 값은 'b'가 된다.
이 성질을 이용한 유용한 함수
char digit_to_hex_char(int digit)
{
return "0123456789ABCDEF"[digit];
}
String Literals versus Character Constants
하나의 문자가 담겨진 string literal은 문자 상수와는 다르다.
string literal "a"는 a와 null character가 담겨진 메모리를 가리키는 포인터로 표현되고,
character constant 'a'는 정수(그 문자에 대한 숫자 코드)로 표현된다.
13.2 String Variables
어떤 언어에서 string을 위한 special type이 존재하는 것과 다르게, C에서는 character로 이루어진 일차원 배열은 string을 저장하는 데 사용할 수 있다. 이로 인해 발생하는 어려움이 있다. 문자의 배열이 string으로 쓰였는지 아닌지 알기가 어렵다. string-handling 함수를 쓸 때 null character를 다루는 것에 대해 조심해야 한다. 문자열의 길이를 알려면 처음부터 시작해서 null character를 찾는것 보다 더 빠른 방법이 없다.
#define STR_LEN 80
...
char str[STR_LEN+1];
위 표현은 common practice. str에 80개 이상의 문자가 들어갈 수 없다는 것을 강조한다. 마지막 글자는 null character가 되기 때문에 배열의 길이를 81로 해야 80개의 문자가 들어갈 수 있다.
Initializing a String Variable
선언과 동시에 initialize될 수 있다.
char date1[8] = "June 14";
char date1[8] = {'J', 'u', 'n', 'e', ' ', '1', '4', '\0'};
// equivalent statements
만약 배열의 길이보다 더 적은 문자가 입력되면, array initializing의 일반적인 규칙에 따라 나머지 원소는 0으로 채워진다(0 = null character)
char date2[9] = "June 14";
char date3[7] = "June 14";
배열의 길이보다 더 길게 입력되는 것은 illegal하지만, 딱 한 자리만 부족해서 null character를 넣지 못하는 경우는 허용한다.
(dev c++에서 실험한 결과 배열의 길이가 더 길게 입력되면 warning이 뜬다. char 배열에서 길이를 글자수와 같게 지정하면 경고가 뜨지 않는다.)
char date4[] = "June 14";
선언시 배열의 길이를 생략한 경우에는 컴파일러가 길이를 계산해 준다. 이 경우에는 8
Character Arrays versus Character Pointers
char date[] = "June 14"; // date를 배열로 선언
char *date = "June 14"; // date를 포인터로 선언
배열과 포인터 간의 밀접한 관계 덕에 date를 string으로 사용하고자 할 때 두 버전 모두 사용 가능하다.
또한, function이 받는 type이 둘 중 어떤 것이더라도 둘을 바꿔서 쓸 수 있다.
그렇지만 두 표현 간에는 중요한 차이가 있다.
- 배열 버전에서는, date에 저장되어 있는 문자들이 수정 가능하다(다른 모든 배열과 마찬가지로). 포인터 버전에선, date는 string literal의 포인터이고 문자를 개별적으로 수정할 수 없다.
- 배열 버전에서는, date는 배열의 이름이다. 포인터 버전에서는, date가 변수의 이름이고 다른 문자열을 가리키도록 수정될 수 있다.
따라서 문자가 수정 가능하도록 하려면 포인터를 사용하는것 만으로는 불충분하며 배열을 사용해야 한다.
char *p;
위 선언은 포인터 변수를 위해 필요한 메모리만을 설정하고 문자열을 위한 메모리를 설정하지 못한다(길이가 얼마나 되는지도 모른다). 만약 p를 string으로 사용하고 싶다면, 문자 배열을 가리켜야 한다.
char str[STR_LEN+1], *p;
p = str;
다른 방법으로는 p가 dynamically allocated string을 가리키게 하는 것이 있다.
13.3 Reading and Writing Strings
printf에서 string을 출력하기 위한 conversion string: %s
char str[] = "Are we having fun yet?";
printf("%s\n", str);
==> Are we having fun yet?
printf 함수는 문자열에 들어있는 문자들을 출력하다가 null character를 만나면 멈춘다. 만약 null character가 없다면, string의 끝을 지나서 메모리 어딘가에 들어있는 null character를 발견할 때까지 계속 출력한다.
문자열의 일부만 출력하고 싶을 때는
%.ps
p : number of characters to be displayed
printf("%.6s\n", str);
==> Are we
%ms
m: m만큼의 칸에 문자열을 표시. 만약 문자열이 m보다 길다면, 짤리지 않고 전체가 표시된다. 문자열이 m보다 짧다면,
우측 정렬된다. m 대신 -m을 사용하면 좌측 정렬된다.
puts 함수로도 string을 출력할 수 있다. puts 함수는 하나의 인자(출력될 문자열)만 받으며, string이 출력되면 항상 추가로 new line character까지 출력해서 다음 줄로 진행한다.
puts(str);
Reading Strings Using scanf and gets
scanf 함수에서 %s conversion specification은 문자열을 읽어 문자 배열에 넣어 준다.
scanf("%s", str);
배열의 이름이 포인터로 쓰일 수 있으므로, str 앞에 & operator가 오지 않아도 된다.
scanf가 호출되면 whitespace를 스킵하고 문자를 읽는다 - white-space character를 만날 때까지. 따라서 scanf 함수로 저장된 문자열은 절대로 white-space character를 포함하지 않고, 보통 입력된 전체 라인을 읽어들이지 못한다.
scanf는 항상 문자열의 끝에 null character를 저장한다.
전체 라인을 읽지 못하는 경우가 많은 scanf의 대안으로 gets가 된다. get도 scanf처럼 문자를 배열에 저장하고 null character를 덧붙이지만 scanf와 다른 점이 있다.
- gets는 scanf와 다르게 문자열을 읽어들이기 전에 white space를 스킵하지 않는다.
- gets는 new-line character를 찾기 전까지는 계속 문자를 읽는다(scanf는 white-space 문자를 찾으면 멈춘다). 자연스럽게 gets는 new-line character를 저장하지 않고, 대신 null character를 마지막에 저장한다.
scanf의 경우 conversion specification %ns를 사용해서 (n: maximum number of characters to be stored) 문자의 최대 개수를 지정할 수 있으나, gets는 그런 안전장치가 없으며, 만약 배열의 크기보다 더 많은 문자가 입력되었을 경우 undefined behavior를 일으킨다. 따라서 gets는 위험성을 내포하고 있으며, fgets는 더 나은 대안이다.
Reading Strings Character by Character
scanf와 gets 모두 위험성이 있고 충분히 유연하지 못하기 때문에, 프로그래머들은 자신만의 input 함수를 쓰는 경우가 많다. 문자열을 한번에 하나의 문자로만 읽으면, standard input function보다 좀더 넓은 범위의 컨트롤이 가능하다.
만약 자신만의 input function을 짜기로 결정했다면, 다음 이슈를 고려해야 한다.
- 함수가 문자열 앞에 오는 white space를 스킵할 것인가?
- 어떤 문자가 왔을때 함수가 그만 읽게 할 것인가? new-line character? any white-space character? or some other character? 그 문자를 버릴 것인가 또는 포함시킬 것인가?
- 입력된 문자가 너무 길 경우 어떻게 처리할 것인가? 초과된 문자를 버릴 것인가 아니면 다음 input operation을 위해 남길 것인가?
문자열 앞의 white-space를 스킵하지 않고, new-line character를 처음으로 만났을 때 읽기를 중단(new-line character는 저장하지 않음)하고, 초과된 문자들은 버리는 함수
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
ch 가 char type이 아닌 int type임에 유의하자. getchar 함수는 문자를 int 값으로 반환하기 때문이다.
13.4 Accessing the Characters in a String
문자열은 배열로 저장되므로, subscripting을 통해 각 문자에 접근할 수 있다. 모든 문자에 접근하기 위해서는 루프문을 사용한다. 이때 포인터를 사용하면 편리하다.
using 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;
}
using pointer arithmetic
int count_spaces(const char *s)
{
int count = 0;
for(; *s != '\0'; s++)
if (*s == ' ')
count ++;
return count;
}
const는 함수가 s를 수정하는 것을 막지 않는다. 다만 s가 가리키는 것은 바꾸지 못하게 한다. s가 함수에 전달된 포인터의 복사본이므로, 함수 안에서 이뤄지는 s의 increment는 원래 포인터에게 영향을 주지 못한다.
이 함수에서 제기할 수 있는 의문들
- 문자열에 있는 문자들에 접근할 때, array operation과 pointer operation 중 어느 것이 더 나은가?
더 편리한 것을 쓰면 되고, 섞어서 써도 된다. 포인터 사용시 index를 위한 int 변수를 쓰지 않아도 되므로 좀더 단순하게 표현 가능하다. 전통적으로 프로그래머들은 문자를 다룰 때 포인터 연산을 사용했다.
- 문자열 파라미터가 어떤 것으로 선언되어야 하는가? 배열 or 포인터?
둘 간에 차이가 없다. 12.3에서 살펴봤듯이 컴파일러는 배열 파라미터를 포인터로 선언된 것과 마찬가지로 취급한다.
- 파라미터의 형태(s[] or *s)가 함수에 넣을 수 있는 argument에 영향을 끼치는가?
NO. 위 count_spaces 함수에서 argument는 배열의 이름, 포인터 변수, 또는 문자 리터럴이 될 수 있다. 위 함수는 그 차이를 구분하지 못한다.