Introduction

Why do we use '#include <stdio.h>'?

stdio.h 안에 미리 정의되어 있는 함수를 사용하기 위해 사용합니다.

#include를 이용하면 그 다음에 나오는 파일의 텍스트를 내가 다음에 작성할 파일의 일부분처럼 넣을 수 있습니다.
여기에서는 #include가 사용자의 OS에 어딘가에 존재하는 stdio.h를 복사해 #include가 있던 자리를 대체합니다.


How are C strings represented?

C string은 메모리에 문자의 형태로 나타납니다. string의 끝은 NULL(0) byte를 포함하고 있습니다. 그러므로 "ABC"라는 문자열은 ['A', 'B', 'C', '\0'] 4바이트를 필요로 합니다. C string의 길이를 알아내는 유일한 방법은 NULL byte를 만날 때까지 메모리를 계속 읽는 것입니다. C character는 항상 각각 1바이트를 차지합니다.
만약 char pointer인 char* 로 "ABC"라는 문자열을 쓴다면 char*는 그 문자열의 첫번째 바이트(문자)를 가리킵니다. 아래의 예에서 ptr에는 문자열의 첫번째 문자의 주소값을 가지고 있습니다.

char *ptr = "ABC"

아래의 예는 문자열을 초기화하는 다른 일반적인 방법들입니다.


char *str = "ABC";
char str[] = "ABC";
char str[]={'A','B','C','\0'};



How do you declare a pointer?


Pointer는 메모리의 주소를 의미합니다. 포인터 타입은 컴파일러에게 얼마나 많은 바이트를 읽거나 쓸 필요가 알려줄 수 있어 유용합니다. 포인터는 아래와 같은 방식으로 선언할 수 있습니다.


int *ptr1;
char *ptr2;


C의 문법에 따라 int*나 어떤 포인터는 실제 그 타입이 아닙니다. 각각의 포인터 변수 앞에 asterisk(*)을 붙일 필요가 있습니다. 일반적으로 다음과 같은 실수를 저지르기 쉽습니다.


int* ptr3, ptr4;


위와 같은 선언은 ptr3만 포인터로 선언되고, ptr4는 int형이 됩니다.

int *ptr3, *ptr4;

이와 같이 선언해야 ptr3 와 ptr4 모두 포인터형으로 선언됩니다.


How do you use a pointer to read/write some memory?

int* ptr 포인터를 선언하고 설명을 위해 이 ptr의 메모리 주소는 0x1000이라고 가정해 봅시다. 만약 이 포인터에 쓰고 싶다면 *ptr에 할당하여 역참조가 가능합니다. 

*ptr = 0; // Writes some memory.


이러한 선언으로 C가 할일은 pointer의 타입을 알고(여기서는 int) sizeof(int) 바이트만큼 0을 쓰는 것입니다. 즉, ptr이 0x1000이라고 했을 때, 0x1000, 0x1001, 0x1002, 0x1003 네 바이트는 모두 0이 됩니다.이렇게 쓰여지는 바이트의 수는 포인터 변수의 타입에 의존합니다. 이와 같은 동작은 구조체를 제외한 모든 기본 타입에 유효합니다.


What is pointer arithmetic?

pointer에 정수를 더하는 것도 가능합니다. 하지만 포인터의 타입은 그 연산을 통해 얼마만큼의 바이트가 증가할지 결정하는데 사용됩니다. 
만약 char pointer 변수라면 char형이 1 바이트이므로 이러한 차이점은 큰 의미가 없습니다

char *ptr = "Hello"; // ptr holds the memory location of 'H'
ptr += 2; //ptr now points to the first'l'


하지만 만약 4바이트인 int라면 ptr에 1을 더함으로서 4바이트 뒤의 주소값은 가리킵니다.(int가 4바이트 자료형이기 때문에)


char *ptr = "ABCDEFGH";
int *bna = (int *) ptr;
bna +=1; // Would cause iterate by one integer space (i.e 4 bytes on some systems)
ptr = (char *) bna;
printf("%s", ptr);
return 0;


위 코드는 어떤 값을 반환할까요?

맨 처음 ptr은 "ABCDEFGH" 문자열의 시작하는 주소값, 즉, 'A'가 있는 주소값을 가리키고 있었습니다.

하지만 bna라는 int pointer 변수에 이 주소값을 int pointer로 강제 형변환을 시켜 대입했습니다.

그 다음 이 bna라는 포인터에 1을 더하면, 'A'로부터 4바이트 떨어진 'E'를 가리키게 됩니다. 

여기서부터 문자열의 끝까지 printf를 통해 출력하면 "EFGH"라는 문자열이 출력될 것입니다.

하지만 ptr 포인터에 1을 더해 포인터연산을 수행하면 1바이트 뒤인 'B'를 가리키므로 출력하면 "BCDEFGH"가 될 것입니다.


C포인터 연산은 항상 자동적으로 포인터가 가리키는 타입의 사이즈만큼 조절되어 연산되므로 void pointer에 대한 포인터 연산은 불가능합니다.


int *ptr1 = ...;
int *offset = ptr1 + 4;


위와 같은 수행은 



int *ptr1 = ...;
char *temp_ptr1 = (char*) ptr1;
int *offset = (int*)(temp_ptr1 + sizeof(int)*4);


이와 같은 과정을 통해 값을 얻는다고 생각해야 합니다.

위의 코드에서 offset은 ptr에 4를 더해 4*4(sizeof(int))인 16바이트가 증가했습니다.

이를 아래에 코드에서는 char*로 변환해 16을 증가시키는 것을 보여주고 있습니다.


포인터 연산을 할 때는 잘 생각한 다음에 얼마나 바이트를 이동시켜야 하는지 잘 확인해야 합니다.


What is a void pointer?

void pointer는 타입이 정해지지 않은 포인터입니다. void pointer는 알려지지 않은 데이터 타입을 처리하거나 다른 프로그래밍 언어와 C code를 인터페이싱할 때 사용합니다. 이 포인터를 단순히 메모리 주소를 가리키는 포인터라고 생각해도 됩니다. 이러한 void pointer는 size가 없기 때문에 직접 읽거나 쓰는 것이 불가능합니다.


void *give_me_space = malloc(10);
char *string = give_me_space;


위와 같은 예에서는 따로 형 변환이 필요 없습니다. C에서 void*를 적절한 자료형으로 변환시켜주기 때문입니다.


NOTE

gcc와 clang은 전부 ISO-C호환이 아닙니다. 그래서 void pointer에 연산을 허용할 수 도 있습니다. 이러한 연산은 char pointer처럼 취급되어 연산되지만, 모든 컴파일러와 호환되는 것이 아니므로 이러한 연산을 하지 않도록 합시다.




Does printf call write or does write call printf?

printf가 write를 호출합니다. performance를 위해 printf에는 내부 버퍼가 있어서 printf가 호출될 때마다 wirte를 호출하지는 않습니다. printf는 library function이고 write는 system call이기 때문입니다. system call은 library call보다 더 많은 작업이 필요합니다. printf는 내부 버퍼에 저장해두었다 적절한 시기에 write를 수행합니다.


How do you print out pointer values? integers? string?


형식 지정자를 사용해야 합니다. int는 %d, string은 %s, 포인터는 %p를 사용해 출력할 수 있습니다. 
실제적인 사용 예는 아래와 같습니다.
int의 경우

int num1 = 10;
printf("%d", num1); //prints num1


pointer의 경우


int *ptr = (int *) malloc(sizeof(int));
*ptr = 10;
printf("%p\n", ptr); //prints the address pointed to by the pointer
printf("%p\n", &ptr); /*prints the address of pointer -- extremely useful
when dealing with double pointers*/
printf("%d", *ptr); //prints the integer content of ptr
return 0;



string의 경우


char *str = (char *) malloc(256 * sizeof(char));
strcpy(str, "Hello there!");
printf("%p\n", str); // print the address in the heap
printf("%s", str);
return 0;


위와 같습니다.



How would you make standard out be saved to a file?

가장 쉬운 방법은 프로그램을 실행하고 shell redirection(<)을 사용하는 것입니다.
./program > output.txt

#To read the contents of the file,
cat output.txt

위와 같이 실행하면 program이 실행되면서 출력된 output stream이 output.txt에 저장되게 됩니다.


조금 더 복잡한 방법은 close(1)을 이용해 standard output이 file descriptor를 닫고 output.txt를 열어 사용하면 가능합니다.

(0 : standard in, 1 : standard out, 2: standard error)



What's the difference between a pointer and an array?

Give an example of something you can do with on but not the other


char ary[] = "Hello";
char *ptr = "Hello";


array의 이름은 그 array이 첫 바이트를 가리킵니다.

array와 ptr은 모두 아래와 같은 방법으로 출력이 가능합니다.


char ary[] = "Hello";
char *ptr = "Hello";
// Print out address and contents
printf("%p : %s\n", ary, ary);
printf("%p : %s\n", ptr, ptr);


하지만 array는 변형이 가능해 그 안에 내용을 바꿀 수 있습니다. 하지만 정해진 array의 길이를 벗어나지 않도록 주의해야 합니다.

아래에서 사용할 World는 Hello보다 길지 않기 때문에 별다른 문제가 발생하지는 않습니다.


위와 같은 보기에서 ptr은 오직 읽기만 가능한 메모리를 가리키고 있기 때문에(string literal이 static하게 저장되어 있음), 그 내용을 바꿀 수는 없습니다.


strcpy(ary, "World"); // OK
strcpy(ptr, "World"); // NOT OK - Segmentation fault (crashes)



따라서 위와 같이 strcpy를 통해 ptr의 주소의 값을 바꾸려고 하면 segmentation falut가 발생하게 됩니다.


ptr이 가리키는 곳의 내용을 직접 바꾸는 것은 불가능하지만 ptr이 다른 곳을 가리키게 하는 것은 가능합니다.



ptr = "World"; // OK!
ptr = ary; // OK!
ary = (..anything..) ; // WONT COMPILE
// ary is doomed to always refer to the original array.
printf("%p : %s\n", ptr, ptr);
strcpy(ptr, "World"); // OK because now ptr is pointing to mutable memory (the array)



맨 첫번째 줄은 Hello의 시작 주소값을 가리키던 ptr을 World문자열이 시작하는 주소값으로 가리키는 곳을 바꾸는 것이기 때문에 가능합니다.

두번째 줄에서는 ptr이 가리키는 주소값이 array인 ary의 시작 주소값이 됩니다. 이때의 ptr은 array의 주소값을 가키는 것이므로 변형이 가능합니다.

즉 strcpy를 통해 메모리에 접근해 그 내용을 바꾸는 것이 가능합니다.



array의 포인터는 오직 stack의 메모리값만 가리킬 수 있는 것에 반해 char* 포인터는 메모리의 어느 부분이든 가리킬 수 있습니다.  대부분의 일반적인 경우에서 포인터는 대부분 힙 영역의 메모리를 가리킬 것입니다. 이 때 heap 영역의 메모리는 수정될 수 있는 부분입니다.



sizeof() returns the number of bytes. So using above code, what is sizeof(ary) and sizeof(ptr)?


sizeof(ary) : ary는 array입니다. 이 경우 array의 총 필요한 바이트 수를 반환해줍니다. 즉 array가 "Hello"였다면 '\0'을 포함한 6바이트를 반환하게 됩니다.
반면 sizeof(ptr)은 포인터 주소형의 크기를 반환합니다. 즉 32비트컴퓨터라면 4, 64비트 컴퓨터라면 8 바이트를 반환하게 됩니다.
즉, sizeof(char*)는 머신의 주소값을 가리키는 데 필요한 크기를 반환합니다(32비트컴퓨터라면 4, 64비트 컴퓨터라면 8)
반면 sizeof(char[])를 사용한다면 컴파일러는 char array의 총 크기의 바이트수로 치환해줍니다.(array의 총 크기는 컴파일 타임에 알려지기 때문에)

char str1[] = "will be 11";
char* str2 = "will be 8";
sizeof(str1) //11 because it is an array
sizeof(str2) //8 because it is a pointer


위에서 sizeof(str2)는 null character를 포함해 10이 되어야 할 것 같지만 char*의 크기인 8이 됩니다.


string의 길이를 알 때 sizeof를 사용하는 것은 주의해야 합니다!



Which of the following code is incorrect or correct and why?

int* f1(int *p) {
    *p = 42;
    return p;
} // This code is correct;
char* f2() {
    char p[] = "Hello";
    return p;
} // Incorrect!

여기서 위의 코드는 올바른 코드지만 아래의 코드는 잘못된 코드입니다. 
아래의 코드에서 char p는 f2의 stack에 선언된 후 Hello를 저장합니다. 하지만 이 stack은 f2가 return되면서 stack pointer가 되돌아가 invalid한 값이 됩니다.

이러한 일을 방지하려면 아래와 같이 작성해야 합니다.


char* f3() {
    char *p = "Hello";
    return p;
} // OK


이처럼 선언하면  pointer p는 "Hello"라는 string constant의 주소값을 가리킵니다. 이 주소값을 return해주므로 f3가 리턴된 이후도 p의 값은 변하지 않는다.

char* f4() {
    static char p[] = "Hello";
    return p;
} // OK


또는 위와 같이 선언한다면 static 변수는 프로세스의 lifetime동안 존재하므로 유효하다(static variable은 heap이나 stack에 존재하지 않는다)


How do you look up information C library calls and system calls?

man page를 이용합시다. man page는 섹션으로 구분되어 있습니다. section2는 system call입니다. section 3는 c library입니다.
Web에서라면 google에 man7 open으로,
shell에서는 man -S2 open이나 man -S3 printf를 사용합니다.


How do you allocate memory on the heap?

heap에 메모리를 할당하기 위해서는 malloc을 사용합니다. 또는 calloc이나 realloc을 사용해도 됩니다. 다음은 10개의 정수를 할당하기 위한 메모리를 heap에 할당하는 예시입니다.


int *space = malloc(sizeof(int) * 10);


What's wrong with this string copy code?

void mystrcpy(char*dest, char* src) { 
  // void means no return value   
  while( *src ) { dest = src; src ++; dest++; }  
}

위 코드에는 두 가지 문제점이 있습니다. 첫 번째는 주소의 값을 복사하는 것이 아니라 dest가 가리키는 값을 src를 가리키게 바꾸고 있습니다. 

또한 string의 마지막이 항상 null byte인데 이를 복사하지 않고 있습니다.


while( *src ) { *dest = *src; src ++; dest++; } 
  *dest = *src;

이와 같은 코드를 짜면 주소를 바꾸는 게 아닌 그 주소가 가리키는 값을 복사해올 수 있고 마지막에 null byte까지 복사해올수 있습니다.

마지막으로 아래와 같은 코드는 대입 후 while비교가 일어나므로  while문 안에서 null byte를 복사하는 것까지 완료할 수 있습니다.


  while( (*dest++ = *src++ )) {};



How do you write a strdup replacement?


// Use strlen+1 to find the zero byte... 
char* mystrdup(char*source) {
   char *p = (char *) malloc ( strlen(source)+1 );
   strcpy(p,source);
   return p;
}



위와 같은 코드를 이용하면 문자열을 복사하는 함수를 만들 수 있습니다.



How do you unallocate memory on the heap?

free함수를 호출하면 됩니다!
int *n = (int *) malloc(sizeof(int));
*n = 10;
//Do some work
free(n);



What is double free error? How can you avoid?
What is a dangling pointer? How do you avoid?

double free error은 같은 allocation을 두번 free시켜주는 것을 의미합니다.
Dangling pointer란 free하여 더이상 할당되지 않은 메모리에 접근하는 것을 의미합니다.


int *p = malloc(sizeof(int));
free(p);

*p = 123; // Oops! - Dangling pointer! Writing to memory we don't own anymore

free(p); // Oops! - Double free!



위에 예에서 p는 malloc후 바로 free되었는데, 아래에서 p에 접근해 123이라는 값을 썼습니다. 하지만 이 메모리는 더이상 allocate된 곳이 아니기 때문에 다른 곳에 쓰일 수 있으므로 엉뚱한 값이 나올 수 있습니다. 그 이후에 이미 free했던 p를 다시 한번 free함으로써 error가 발생하게 됩니다.

이러한 일을 방지하기 위해선 우선 올바른 프로그램을 짜야합니다.

또한 free된 메모리를 reset하여 다시 접근할 수 없도록 해야 합니다.


p = NULL; // Now you can't use this pointer by mistake


위와 같이 free된 메모리를 NULL로 초기화시켜 준다면 p에 접근해서 사용하려고 할 때 프로그램 충돌이 일어나게 할 것입니다.



What is an example of buffer overflow?

유명한 예로 heart bleed가 있습니다.(충분하지 않은 크기의 버터에 memcpy를 수행)
즉, strcpy를 호출할 때 복사되는 메모리의 길이가 strlen에 1을 더하지 않고 strlen 길이만큼의 메모리에 복사.(null byte 빠짐)

What is 'typedef' and how do you use it?

typedef는 형식의 별칭을 선언하는 것입니다. 구조체를 선언할 때 주로 사용하여 구조체 타입을 사용할 때 struct까지 써야하는 혼란을 줄여줄 수 있습니다.


typedef float real; 
real gravity = 10;
// Also typedef gives us an abstraction over the underlying type used. 
// In the future, we only need to change this typedef if we
// wanted our physics library to use doubles instead of floats.

typedef struct link link_t; 
//With structs, include the keyword 'struct' as part of the original types



위 코드에서 맨 처음에 real 이라는 이름의 자료형을 float로 정의했습니다.

그래서 그 다음 라인에서 real gravity는 float 자료형이 될 것입니다. 또한 이 한 줄만을 바꿔줌으로써 그 다음에 사용되는 real이라는 자료형을 double로도, int로도 한번에 바꿀 수 있습니다.


아래에서 구조체 자료형인인 struct link를 link_t라는 이름으로 정의하고 있습니다.


typedef function 또한 사용할 수 있습니다. 다음은 typedef function의 예입니다.


typedef int (*comparator)(void*,void*);

int greater_than(void* a, void* b){
    return a > b;
}
comparator gt = greater_than;

위에서는 function pointer를 사용하고 있습니다.

function pointer는 일반적으로

int (*gt)(void *, void*)와 같은 형식으로 함수 포인터 변수 gt를 선언할 수 있습니다.

gt = greater_than과 같이 사용해

선언된 함수 포인터 gt에 greater_than을 지정하면, gt(a,b)와 같은 형태로 greater_than(a,b)를 호출할 수 있습니다. 이러한 함수 포인터를 좀 더 편하게 사용하기 위해 typedef를 사용하고 있습니다.


typedef int (*comparator)(void*,void*);

 위에서는 comparator라는 타입이 (void*, void*)를 인자로 받아 int를 리턴해주는 함수 포인터라는 것을 선언합니다.


이렇게 선언된 후에는 comparator gt와 같은 형식으로 함수포인터를 쉽게 선언할 수 있습니다. 이 함수 포인터가 가리킬 함수를 지정해주고 쉽게 사용이 가능합니다. 이러한 함수 포인터는 분기에 따라 다른 함수를 호출해야 하는 경우가 많이 생길 경우 유용합니다.

'Angrave System Programming > Learning C' 카테고리의 다른 글

Common Gotchas(3)  (0) 2019.01.09
Common Gotchas(2)  (0) 2019.01.09
Common Gotchas(1)  (0) 2019.01.09
Text Input And Output(2)  (0) 2019.01.08
Text Input And Output(1)  (0) 2019.01.08
Posted by 몰랑&봉봉
,