: sequnce of charaters enclosed within double quotes
: "abc"
printf("abc\
def");
이런 식으로 하면 문제점이, 다음 줄에서 문자열이 바로 시작해야됨.
그래서 들여쓰기를 할 수 없어서
printf("abc"
"def");
두개 이상의 문자열이 인접하면(white space로만 구분된 경우) complier가 하나로 이어붙인다.
이렇게 많이 쓰임.
C에서 String Literal은 character의 배열로 구현된다.
배열의 마지막에 null character를 저장하여 String Literal의 끝임을 표시한다.
(null character : a byte whose bits are all zero)
char * type을 가짐.
subscripting 허용
char digit_to_hex_char(int digit)
{
return "0123456789ABCDEF"[digit];
}
modifying 불가
char *p = "abc";
*p = 'd'; -----> WRONG
수정이 불가하므로, string literal을 parameter로 받을 때는 const를 사용해주는게 좋음.
왜 수정이 안되는지는 최하단 Q&A 참고
string literal은 static storage duration
https://stackoverflow.com/questions/56667780/c-do-all-string-literals-have-static-storage-duration
string type을 따로 제공하는 언어와 다르게,
char의 일차원 배열이기만 하면 string을 저장할 수 있음.
null character와 length에 대해서만 신경쓰면 됨.
char date1[8] = "June 14";
char date1[] = "June 14";
이런식으로 initialize하면 자동으로 제일 끝에 null character가 저장됨.
배열 크기는 빼먹어도 OK (null 저장할 공간까지 알아서 배정됨. 왜냐하면 결국 오른쪽 initializer는 아래 형태의 축약형이니까)
여기서 "June 14"는 String literal 처럼 보이지만 그렇지 않음.
아래 array initializer의 abbreviation임.
char date1[8] = {'J', 'u', 'n', 'e', ' ', '1', '4', '\0'};
배열크기가 문자열 보다 길면 compiler는 남은 뒷 공간에 null character를 저장한다.
배열크기가 문자열보다 작거나 같으면 남은 공간이 없어서 null character는 그냥 빼먹는다.
array :
char date[] = "June 14";
pointer :
char *date = "June 14";
함수에 넘겨줄때는 둘이 구분이 안되지만, 둘은 엄연히 다르다.
printf("%s", str);
puts(str);
(puts는 항상 마지막에 new-line character를 추가함)
scanf("%s", str);
시작할때 white space를 skip하고, 읽기 시작한 뒤, white space를 만나면 저장한다. 마지막에 null character도 자동으로 저장한다.
따라서 얘를 이용하면 white space를 저장할 수 없음
대안으로 %ns를 사용하면 좀 더 안전하게 사용할 수 있다.
gets(str);
얘는 scanf와 다르게 white space를 skip하지 않고, 쭉 읽다가 new-line character 가 나오면 멈춘다. 그리고 new-line character는 버리고 그 자리에 null chatacter를 저장한다.
scanf와 달리 안전하게 사용할 수 없다, fgets(chapter 22.5에 나옴)가 더 나은 대안이다.
scanf나 gets는 위험이 있고 유연하지 않아 프로그래머들은 주로 스스로 목적에 맞는 input function을 만들어서 사용한다.
자신만의 input function을 만들기 위해선 아래 사항들을 확인해야 한다.
1. string을 저장하기 전에 white space를 skip할 것인가?
2. 어떤 character로 읽기를 중지할 것인가?(white space, new-line 등등) 그리고 그 chatacter를 버릴 것인가 저장할 것인가?
3. input string이 너무 길면 어떻게 할 것인가? 남은 부분을 버릴 것인지 다음 input 연산을 위해 그냥 놔둘 것인지
예시:
white space를 skip하지 않고,
new-line character를 만났을 때 중지하며,
나머지 character는 버리는 함수
int read_line(char str[], int n)
{
int ch, i = 0;
while ((ch = getchar()) != '\n' && ch != EOF) //short circuit
if (i < n)
str[i++] = ch;
str[i] = '\0'; //마지막에 null character 넣어주는건 우리 몫
return i; //number of characters stored
}
ch를 int로 선언한 이유는 getchar() 함수가 int를 return 하기 때문이다. (EOF 때문에 int를 return)
string은 결국 배열 형태로 저장되기 때문에 각 character를 subscript해서 접근할 수 있다. 혹은 포인터 증감 연산으로도 접근 가능하다.
그냥 배열은 함수로 넘겨줄때 길이도 같이 넘겨줘야 했지만, string은 어차피 null character가 마지막을 표시하고 있으므로 딱히 필요없으면 안받아도 상관없다.
string은 결국 array로 취급되기 때문에 연산을 하는데 array처럼 제한이 있다.
assignment operator를 용도와 type에 맞게 잘 사용해야 하고, relational operator나 equality operator는 사용해도 원하는 결과를 얻긴 힘들다.
C Library에 <string.h> 헤더 파일은 string에 대한 다양한 연산 함수를 제공한다.
#include <string.h>
char *strcpy(char *s1, const *s2);
s2의 characters를 s1으로 복사한다. 첫번째 null character가 나올때 까지 모두 복사한다.
(s1보다 s2가 크다면 Undefined Behavior)
(strncpy가 안전한 방법, 자세한 내용은 chapter 23.6에 나옴)
그러고 s1을 return 한다.(대부분의 경우는 return 값을 그냥 버림)
string도 배열로 저장되므로, 배열이 복사를 위해 assignment를 해도 안되듯이 string도 마찬가지다. 그래서 strcpy를 사용한다.
strncpy 는 세번째 parameter로 복사될 문자의 개수를 입력받아 strcpy 보다 더 안전하다.
strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1)-1] = '\0';
(여기 쓰인 str1과 str2는 string인 character 배열)
이렇게 str2가 str1보다 크거나 같을 경우를 대비해서 마지막에 null chatacter를 직접 넣어줘야 한다.
size_t strlen(const char *s);
string의 첫번째 null character까지의 길이(길이에 null character는 포함하지 않음)를 반환하는 함수
엄청나게 긴 string이 아니라면, return 값을 그냥 int로 생각하고 다뤄도 OK
char *strcat(char *s1, const char *s2);
s2를 s1의 끝에 이어붙이고, s1을 반환한다.(대부분의 경우는 return 값을 그냥 버림)
s1이 s2까지 포함하기 충분하지 않다면 undefined.('\0'이 들어갈 공간도 생각해야함)
strncat가 느리지만 안전한 방법, 세번째 parameter로 복사될 문자의 개수를 입력받는다.
strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);
str1 배열에서 문자열 길이를 빼고 남은 부분을 계산한다.(추가로 1을 빼서 null character가 들어올 공간을 확보해준다.)
int strcmp(const char *s1, const char *s2);
두 문자열 중 누가 더 큰지 판별하기 위해 사용한다.
s1이 더 크다면 양수, 같다면 0, s2가 더 크다면 음수를 반환한다.
문자열 크기 비교?
ASCII character set properties
string을 다루는 여러 idiom에 대해 알아본다. 이 스타일을 마스터 해두는 편이 좋다.
본인만의 string 함수를 작성할 때는 standard library의 함수와 이름을 달리해야한다. 그 header를 include하지 않더라도, standard library의 함수와 같은 이름을 적는건 금지됐다.(setction 21.1 restriction 참고)
애초에 소문자 str로 시작하는 함수 이름은 이미 reserve돼있다.(section 21.1 restriction 참고)
size_t strlen(const char *s)
{
size_t n;
//s가 const인 것은 s가 가리키는 것이 수정이 안되는거지, s가 다른것을 가리키는건 상관없음
for (n = 0; *s != '\0'; s++)
n++;
return n;
}
여기서 n의 initialization을 합치고,
'\0'
이 0
과 값이 같은 점, 그리고 postfix increment 연산이 indirection 연산보다 precedence가 높은 점을 고려하여 함수를 압축 시킬 수 있다. ('\0'
과 0
의 값은 같지만, '0'
과 0
의 값은 다르다.) (연산 우선순위가 높다고 오해하면 안되는게, 후위연산이므로 indirection 연산이 진행돼서 값을 확인하고 s의 값이 증가한다. indirection이 우선순위가 높았으면, s가 가리키는 값 자체(*s)를 증가시켰을 것이다.)
그러면 for문을 사용하는 의미도 없어지므로(가운데 조건문만 남게 됨), while로 바꾸면
size_t strlen(const char *s)
{
size_t n;
while(*s++)
n++;
return n;
}
이걸 좀 더 빠르게 작동 시키고 싶으면,
size_t strlen(const char *s)
{
const char *p = s; //s도 const이므로 p를 const로 선언하지 않으면 compiler가 risk로 판단할 수 잇음
while (*s)
s++;
return s - p;
}
idiom (Search for the null character at the end of a string)
1. while (*s) s++;
2. while (*s++) ;
2번은 위보단 좀 간단하지만, 반복문이 끝났을 때, null character가 아니라 그 다음 주소를 가리키고 있다.
idiom
while (*p++ = *s2++) ;
null character가 나오면 assgin하고 loop는 끝남
char planets[][8] = {"Mercury", "Venus", "Earth",
"Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto"}; //pluto는 이제 행성이 아니긴 함
대부분 문자열이 최대치인 8에 미치지 못하므로, 이런 식의 선언은 공간을 낭비한다.
따라서 ragged array type이 있으면 좋지만, C에서 이는 지원하지 않는다.
하지만 배열의 원소를 배열이 아닌(배열의 원소가 배열이면 2차원 배열) 포인터로 함으로써 이를 해결할 수 있다.
char *planets[8] = {"Mercury", "Venus", "Earth",
"Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto"};
command-line information은 OS 커맨드 뿐만 아니라 다른 모든 프로그램에서도 접근할 수 있다. 우선 main 함수를 아래와 같이 만들어야 한다.
int main(int argc, char *argv[])
{
~~
}
argc (argument count) : the number of command-line arguments
argv (argument vector) : an array of pointers to the command line argument (얘의 크기는 argc+1)
argv[0] : name of the program
argv[1] ~ argv[argc-1] : remaining command-line arguments
argv[argc] : null pointer (null pointer는 chapter 17.1에 나옴)
예시)
command line에
ls -1 remind.c
이라고 입력하면,
argc
는 3,
argv[0]
는 문자열 "program name"을 가리키는 포인터 (해당 문자열을 사용할 수 없으면 empty string을 가리킴),
argv[1]
은 문자열 "-1"을 가리키는 포인터,
argv[2]
는 문자열 "remind.c"를 가리키는 포인터,
argv[3]
은 null pointer
for (int i = 1; i < argc; i++)
printf("%s\n", argv[i]);
char **p;
for (p = &argv[1]; *p != NULL; p++)
printf("%s\n", *p);
//argv가 char* array이므로 p를 char**로 선언해야됨. int array에 접근하려면 int* 이 필요하듯.
//char *로 해서 argv[1] 입력받은 다음 포인터 한번 더 먹여서 다음 element로 접근못하나?
//당연히 못하지 ㅋㅋ char*로 받으면 그냥 배열의 값 자체를 "copy" 받는건데 거기다가 pointer 해봤자 다른 주소지..
string literals 이 아니라 그냥 string constants 라고 이름 붙이면 안되나?
string literals은 constant와 다르게 pointer를 통해 접근하기 때문에, 프로그램이 이를 변경하려는 시도를 막을 수 없다.
string literal을 수정하면 왜 UB인가?
1. 몇몇 compiler들은 공간 절약을 위해 같은 string literal은 한번만 저장한다.
char *p = "abc", *q = "abc";
라고 하면 p와 q가 같은 곳을 가리키게 되는거다. 이 경우 bug 발생할 수 있다.
2. string literal이 Read Only Memory(ROM)에 저장되므로 여기 데이터를 수정하면 crash 발생할 수 있다.
모든 character array는 null character로 끝나야 하나?
꼭 그렇진 않다. null character로 끝나는걸 확인하는 함수에 넘겨준다거나 하는 해당 목적으로 사용할 경우에는 그렇게 해야하지만, 본인이 다른 목적으로 알아서 쓰겠다면 굳이 null로 끝낼 필요는 없다.
K&R에 쓰인 strcmp 함수
int strcmp(char *s, char *t)
{
int i;
for (i = 0; s[i] == t[i]; i++)
if (s[i] == '\0')
return 0;
return s[i] - t[i];
}
strcmp가 이렇게 쓰였는지 보장은 못하지만, 아마 비슷하게 이런 매커니즘으로 쓰였을 것
그래도 return 값이 특정 의미를 가지고 있다고 해석하진 말자, 그냥 같은 문자열인지 보거나 크기 비교 하는 정도로만 사용하면 되지 싶다.
while (*p++ = *s2++) ; warning 발생
대부분 컴파일러에선 =를 ==와 헷갈렸다고 생각하고 warning이 발생하는 것
그래서
while ((*p++ = *s2++) != 0) ;
이렇게 적던지
while ((*p++ = *s2++)) ;
이렇게 적으면 됨
strlen 함수와 strcat 함수가 이 책에 나온 예시처럼 쓰였나?
그랬을 수도 있지만, 컴파일러 만들때 그런 함수는 효율성과 속도를 위해 주로 assembly로 작성함
왜 command-line arguments를 C standard에선 program parameters라고 부르나?
프로그램이 꼭 command-line으로 실행되는건 아니기 때문이다. 마우스 클릭으로 실행할 수도 있고, 프로그램에 정보를 넘겨주는 방법은 command-line 말고도 있다.
꼭 argc와 argv라는 이름을 써야되나?
No. 그건 그냥 convention, not a language requirement
*argv[] 대신에 **argv 이라고 적어도 되나?
당연. parameter에서는 둘이 구분안됨. declare 할때는 당연히 구분해야됨.
여기서 주의할게있는데 parameter에서 **argv나 *argv[]나 둘 다 이중포인터이므로 뭘 써도 상관없지만,
이차원 배열을 입력받는 경우엔 함부로 바꾸면 안된다.
만약 int a[10][20];로 선언한 a를 인자로 받고 싶다면 int ** type의 parameter을 만들어선 안될 것이다. 왜냐하면 a의 type은 int * [20]이기 때문이다.
그렇기때문에 이런 a를 함수 인자로 받으려면 func(int a[][20]){~} 이라고 정의하거나 func(int (*a)[20]){~} 이라고 정의해야한다.
이 경우엔 "함수 인자에서 배열이나 포인터나 그게 그거였지!" 하면서 func(int **a){~} 라고 선언하면 안된다.