# 1학년 과목을 재수강 하고 있다.

새내기 시절 ‘정말 저렇게 살아도 될까?’ 싶을 정도로 학교생활을 즐겨서
전공 과목 점수가 영 순탄하지 못했다. (후회는 없다! 잘 놀았어! 🤤)
그 중에서도 가장 기본이 되는 C프로그래밍 과목 점수들이 모두 C+
+ 하나만 더 있으면 C++이 되겠는데?

다행히 복학 후 학업에 관심을 갖게 되면서 학점을 야금야금 끌어올렸다.
마지막 학기에 6학점이 남았는데, 원래도 교양수업보다 전공수업을 좋아했어서
학과 전공과목만 몰아서 듣다보니 더 이상 들을 과목이 없었다.
(AI, 러닝 관련 과목이 남아있으나, 내가 그쪽에 관심이 없다.)

어째야하나 고민하다가, “새내기 시절 그르쳐놓은 전공과목 점수나 올려보자!” 라는 생각으로 재수강 중이다.
교재는 모두의 바이블인 열혈C프로그래밍인데, 중간고사 공부 겸 오랜만에 다시 들춰보다보니 여전히 많은 배움을 얻을 수 있었다.
1학년 기준인만큼 쉬운 내용도 많았지만, 새로 깨닫거나 배운 내용이 생각보다 많아서 기록해두기로 했다.

1. scanf 함수는 공백이 포함된 문자열을 저장 못한다.

#include <stdio.h>

int main(int argc, char *argv[]){
    char str[20];
    scanf("%s", str);   // "My name is 9!" 입력
    printf("%s", str);  // "My" 출력
}

scanf 함수는 공백을 기준으로 동작한다.
때문에 문자열에 공백이 포함되면 공백 뒤 문자열은 저장하지 못한다.
공백을 포함한 문자열을 저장하고 싶다면 정규표현식을 이용하면 된다.

#include <stdio.h>

int main(int argc, char *argv[]){
    char str[20];
    scanf("%[^\n]s", str);   // "My name is 9!" 입력
    printf("%s", str);       // "My name is 9!" 출력
}

정규표현식에서 []는 문자선택을 의미하고, [] 내부의 ^는 제외를 의미한다.
즉, 개행(\n)을 제외한 모든 문자열을 받겠다 는 의미로 해석되어서
엔터키 입력 전까지의 모든 문자열을 받아 저장한다.

2. 포인터 변수의 값을 +-1 하면, 자료형의 크기만큼 주소값이 변환된다.

#include <stdio.h>

int main(int argc, char *argv[]){
    int arr[5] = {1,2,3,4,5};
    int *ptr = arr;

    printf("%d\n", *(ptr+1)) // 2 출력
}

정수(int)를 4Byte로 해석하는 컴파일 환경 기준으로
포인터의 값을 증가시키거나 감소시키면, 주소값이 +-4 변한다.

3. 포인터 변수의 const는 앞 뒤 의미가 다르다.

포인터 변수 앞에 붙는 const는 값 변경을 불가능하게 한다.
단, 다른 변수를 통해 접근 후 변경하는 것은 충분히 가능하다.

#include <stdio.h>

int main(int argc, char *argv[]){
    int num = 9;
    const int *ptr = &num;  // 값 변경 불가!

    *ptr = 30;  // 컴파일 에러
    num = 40;   // 컴파일 성공
}

포인터 변수 뒤에 붙는 const는 주소 변경을 불가능하게 한다.

#include <stdio.h>

int main(int argc, char *argv[]){
    int num = 9;
    int num2 = 10;
    int * const ptr = &num;  // 주소 변경 불가!

    *ptr = 30;  // 컴파일 성공
    ptr = &num2 // 컴파일 에러
}

앞, 뒤 동시에 선언도 가능하다!

#include <stdio.h>

int main(int argc, char *argv[]){
    int num = 9;
    const int * const ptr = &num;  // 값, 주소 변경 불가!
}

4. 2차원 이상 배열 선언시, ‘열’ 크기는 정해줘야 컴파일이 된다.

배열의 크기(길이)를 지정하지 않고 선언하곤 하는데,
2차원 이상의 배열에서는 열의 크기를 지정해줘야
나머지 면, 행을 컴파일러가 자동으로 해석한다.
지정해주지 않으면 여러 방법으로 해석이 가능해지기 때문에, 에러가 발생된다.

#include <stdio.h>

int main(int argc, char *argv[]){
    int arr[][] = {1,2,3,4,5,6};  // 2*3? 3*2? 1*6? ...
    
    int arr[][3] = {1,2,3,4,5,6}; // 2*3이 되겠군!
}

5. 2차원 배열의 포인터형은 행과 열에 따라 달라진다.

1차원 배열의 경우 배열이름이 가리키는 대상을 기준으로
#2번과 같이 메모리의 접근법과 포인터 연산시의 주소 증감크기가 지정되었으나,
2차원 배열에서는 #4번과 같이 열의 정보가 없으면 연산이 불가능하다.
때문에 2차원 배열의 포인터형은 아래와 같은 모양을 띈다.

#include <stdio.h>

int main(int argc, char *argv[]){
    int arr2d[2][4]
    int (*ptr)[4];
    // int자료형만큼, 4개 열 씩 건너뛰는 포인터!
}

6. 2차원 배열을 매개변수로 사용하는 방법

배열의 Call-by-Value를 허용하지 않고 Call-by-Reference를 활용해야하는 C언어에서
2차원 배열을 매개변수로 활용하려면 #5번의 특성과 맞물리는 형태의 모습을 갖춘다.

void newFunction(int (*ptr)[4], double (*ptr2)[5]);
void newFunction(int ptr[][4], double ptr2[][5]);

개인적으로는 두 번째 방법이 더 직관적이고 좋은듯!

7. getchar(), fgetc의 반환형이 int인 이유

gets, fgets 함수는 반환형이 문자열인데,
getchar, fgetc 함수는 반환형이 정수다. 왜그럴까?

대부분 char 자료형은 signed char를 의미한다.
char 자료형을 통해서도 정수 연산을 진행하는 경우가 존재하는 것 같은데(?),
unsigned char는 0~255까지, signed char는 -128 ~ 127까지 표현이 가능하다.
그런데! unsigned char를 처리하지 못하는 컴파일러가 존재하기 때문에
EOF -1을 처리하기 위해서 반환값이 int형 이라고 한다.

… 솔직히 읽기도 이해가 잘 안됐다.
#7번은 나중에 조금 더 공부해봐야 할 것 같다.
어쨌거나 결론은 아래와 같다!

1. EOF(End-Of-File)이 -1 이므로.
2. unsigned char를 처리 못하는 컴파일러가 존재.

8. string.h 함수들의 NULL 자동포함 여부

문자열 처리 함수로 유명한 strcpy, strcat에 NULL 자동포함 여부 차이가 있다.

strcpy, strncpy

  • strcpy, strncpy는 문자 단위로 복제하는 함수다.
  • 이 때문에 NULL을 자동 포함해주지 않는다.
  • NULL이 사라지면 문자열로 해석하지 못하기 때문에, NULL 복제까지 고려해야한다.

strcat, strncat

  • strcat, strncat은 문자열 단위로 합체하는 함수다.
  • 이 때문에 마지막에 NULL을 자동으로 붙여준다.
  • NULL까지 고려하지 않아도 된다.

앞으로 공부하면서 기록할게 더 늘아날 수도 있겠지만,
당장은 8가지 정도로 압축되는 것 같다.

‘이래서 전공서적은 버리는게 아니구나’ 생각하게 된다.
지식이 늘었다~ 👏👏👏

댓글남기기