혼자 공부하는 C언어
카테고리: C Language
Chapter 01 프로그램 만들기
01-1 프로그램과 C언어
- C언어는 유닉스 시스템에 사용하기 위해 만들었다
- C언어의 장점으로는 시스템 프로그래밍이 가능하다는 점이 있다
01-2 컴파일과 컴파일러 사용법
- 비주얼 스튜디오가 엄격한 검사 기능을 사용하지 않도록 제한하는 템플릿을 만드는게 좋다
- 컴파일 과정 3단계는 전처리 - 컴파일 - 링크
Chapter 02 상수와 데이터 출력
02-1 C 프로그램의 구조와 데이터 출력 방법
- 함수는 일정한 기능을 수행하는 코드 단위
- 함수 head는 함수 원형이라고 하며 함수의 이름과 필요한 데이터 등을 표시
- 제어 문자란 출력 방식에 영향을 주는 문자
02-2 상수와 데이터 표현 방법
- 코드에서 8진수는 숫자앞에 0, 16진수는 0x를 붙여 표현
- e는 밑수 10을 의미하며 대문자로 쓸 수도 있다 ex) 3.14e-5
- 정규화 표기법 이란 소수점 앞에 0이 아닌 유효숫자 한 자리를 사용하여 지수 형태로 바꾼것
- 소수를 지수 형태로 출력하라면 %le 변환문자 사용
- 상수의 크기는 컴파일러에 따라 다르므로 sizeof 연산자를 통해 확인하자
- 음수를 2의 보수로 처리하는 이유는 특별한 과정없이 바로 양수와 음수를 더할 수 있기 때문
- 실수는 제한된 데이터 크기에 수를 표현하기 위해 IEEE 754 표준을 따른다
Chapter 03 변수와 데이터 입력
03-1 변수
- short형은 int형보다 크기가 작아 메모리를 적게 사용하지만, 연산 과정에서 int형으로 변환되므로 실행 속도가 느려질 수 있다
- unsigned 자료형은 양수만 저장하고 %u를 통해 출력하기를 권고
- float형은 유효숫자 7자리, double형은 15자리 라는것을 참고
- 배열명은 주소 상수 이므로 변수만 가능한 대입 연산자의 왼쪽에 쓸 수 없다
- char 배열에 초기화 이외에 문자열을 저장할 때는 strcpy함수 이용
03-2 데이터 입력
- VS++ 2019 컴파일러는 시스템 보안 문제 때문에 메모리에 직접 접근하는 함수를 사용하면 에러 메시지를 띄우니, 시스템 보안 검사 기능을 제한하는 속성을 적용해야 한다
- 만약 변환문자와 다른 예상치 못한 데이터가 scanf 함수를 통해 입력되면 변환을 포기하고 실행을 중단하는데 이경우 변수값이 입력되지 않으므로 이전에 변수에 있던 값이 계속 사용됨
- scanf_s 함수는 배열의 크기 까지만 문자열을 입력하도록 제한한다
Chapter 04 연산자
04-1 산술 연산자, 관계 연산자, 논리 연산자
- 일반적으로 연산자는 컴파일되면 명령어로 바뀌므로 연산자를 배우는 것은 결국 명령어를 익히는 것이다
- 하나의 수식에서 같은 변수를 두 번 이상 사용할 때는 그 변수에 증감 연산자를 사용하면 안된다
ex) (++a) + a + (++a)
- 숏 서킷 룰이란? 좌항만으로 AND와 OR 연산 결과를 판별하는 기능
- 연산을 하려면 메모리에 있는 a와 b의 값을 CPU의 저장 공간인 레지스터에 복사해야 하며 이과정을 로드라 한다
- 데이터가 레지스터에 저장되면 연산장치인 ALU에 의해 연산이 수행된다
04-2 그외 유용한 연산자
- 자료형의 크기는 컴파일러나 CPU에 따라 다를 수 있다
- 대입 연산자와 복합대입 연산자의 특징은 왼쪽 피연산자는 반드시 변수가 와야 한다는점
- 복합대입 연산자는 저장되는 공간과 연산되는 공간이 다르다는 개념을 이해하자
- 비트연산자를 사용해보면 값을 왼쪽으로 한 비트씩 이동할 때마다 2가 곱해짐을 알 수 있다
Chapter 05 선택문
05-1 if문
- 2개의 실행문 중에 하나를 선택하는 경우에는 주저없이 if-else 문을 사용하자
05-2 if문 활용과 switch ~ case문
- if문은 선행조건이 참인 경우만 다음 조건을 따진다는 점을 활용해 불필요한 연산을 줄일 수 있다
- 분할 정복 기법등을 구현할때 유용하게 사용할 수 있다
- switch문의 case에 사용하는 상수식은 정수만 가능
Chapter 06 반복문
06-1 while문, for문, do ~ while문
- while문은 반복 문장을 실행하기 전에 반복 조건을 먼저 검사한다
- for문은 반복 횟수가 정해진 경우 사용하면 편리하다
06-2 반복문 활용
- continue를 사용하면 조건에 따라 반복문의 일부를 제외하고 반복할 수 있다 ex) 3의 배수를 빼고 1부터 100까지의 합을 구할 때
Chapter 07 함수
07-1 함수의 작성과 사용
- 함수를 만들기 전에 고려해야할 사항은 함수 기능에 맞는 이름, 함수가 기능을 수행하는데 필요한 데이터, 함수가 수행된 후 결과
- 컴파일러는 변수명의 사용범위를 선언한 블록 내부로 제한한다
- 실행 순서는 함수호출 -> 연산자 순이다
- 컴파일러는 컴파일할 때 함수를 호출한 자리에 반환값과 같은 형태의 저장 공간을 준비한다
07-2 여러 가지 함수 유형
- 반환형이 void인 함수는 컴파일러가 반환값이 없다고 가정하여 호출한 위치에 반환값을 저장할 공간을 준비하지 않는다
- 함수가 무한으로 호출되면 프로그램 하나가 쓸 수 있는 메모리( 해당 프로세스에 할당된 스택 메모리 )를 모두 사용하여 강제 종료된다
- 재귀호출 함수는 최초 호출한 곳이 아니라 이전에 호출했던 곳으로 돌아간다
- 재귀호출은 하나의 함수에서 코드를 반복 실행하는 듯하지만 실제로는 새로운 함수를 실행하는 것과 같다
Chapter 08 배열
08-1 배열의 선언과 사용
- 배열 요소의 개수는 다음과 같이 구한다 sizeof(배열명) / sizeof(배열 요소)
08-2 문자를 저장하는 배열
- 널 문자를 저장하기 위해 저장할 문자열의 길이보다 최소한 하나 이상 크게 배열을 선언하자
- strcpy 함수는 char형 배열에 새로운 문자열을 저장하는 함수로, 저장할 문자열의 길이를 파악하여 딱 그 길이만큼만 char형 배열에 복사한다
- 대입연산자 왼쪽에 배열명이 올 수 없음을 기억하자
- gets 함수는 빈칸을 포함하여 한 줄 전체를 문자열로 입력한다
- gets 함수는 입력할 배열의 크기를 검사하지 않는다 즉 메모리 영역을 침범할 가능성이 있다
- char형 배열에 문자를 하나씩 대엽하여 직접 문자열을 만드는 경우에는 문자열의 맨 끝에 널 문자를 저장하는 것을 잊지말자
Chapter 09 포인터
09-1 포인터의 기본 개념
- 포인터는 코드블록 벗어난 경우에도 데이터를 공유할 수 있도록 해준다
- 메모리의 위치를 식별하는 주소값은 바이트 단위로 구분된다
- 2바이트 이상 크기를 갖는 변수는 여러 개의 주소 값에 걸쳐 할당된다
- 시작 주소는 연산자 &를 사용해서 구한다
- 주소는 보통 16진수로 표기한다 따라서 주소를 출력할 때는 전용 문자인 %p를 사용하는 것이 좋다
- 주소를 저장할 포인터도 변수처럼 선언하고 사용한다 다만 선언할 때 변수 앞에 *만 붙여주면 된다
- x -> y의 의미는 x는 포인터이며 변수 y의 주소를 저장하고 있다는 뜻이다
*pa 는 a
와 같으므로&a는 &*pa
와 같다const int *pa = &a
에서 사용된 const의 의미는 pa가 가르키는 변수 a는 pa를 간접 참조하여 바꿀 수 없다는 것이다- 포인터에 const를 사용하는 대표적인 예는 문자열 상수를 인수로 받는 함수
09-2 포인터 완전 정복을 위한 포인터 이해하기
- 주소는 상수이고 포인터는 변수이다
- 포인터의 크기는 컴파일러에 따라 다를 수 있으나 모든 주소와 포인터는 가리키는 자료형과 상관없이 그 크기가 같다
- 포인터는 가리키는 변수의 형태가 같을 때만 대입해야 한다
- 형 변환을 사용한 포인터의 대입은 언제나 가능하나, 정상적으로 할당받은 메모리 공간의 주소를 저장해서 사용하자
- 포인터를 초기화 하지 않는 행위는 매우 위험하다
- 함수는 오직 하나의 값만을 반환할 수 있으므로 한 번의 함수 호출을 통해 두 변수의 값을 바꾸는 것은 불가능하다
- 포인터의 주요 기능 중 하나는 함수 간에 효과적으로 데이터를 공유하는 것이다
Chapter 10 배열과 포인터
10-1 배열과 포인터의 관계
- 컴파일러는 첫 번째 배열 요소의 주소를 쉽게 사용하도록 배열명을 컴파일 과정에서 첫 번째 배열 요소의 주소로 변경한다
- 배열명은 첫 번째 배열 요소의 주소 값 이라는 것을 명심하자
- 주소 + 정수 연산은 주소 + ( 정수 * 주소를 구한 변수의 크기 )로 수행된다
- 배열
ary+2
는&ary[2]
와 같다 - 대괄호[]는 포인터 연산의 간접참조, 괄호, 더하기 기능을 갖는다 ( 배열 요소에 사용하는 대괄호는 연산식이다 )
- 배열 요소 표현식
ary[1]
은 포인터 연산식*(ary + 1)
과 같다 - 배열과 포인터의 차이는 첫째 sizeof 연산의 결과가 다르다 둘째 변수와 상수의 차이가 있다
*(pa++)
에서 컴파일러는 pa가 증가되기 이전 값을 임시공간에 저장해두었다가 간접 참조 연산에 사용한다- 배열명은 주소 상수로 그 값이 바뀌지 않으므로 언제든지 배열의 시작 위치를 찾아갈 때 사용할 수 있다
++(*pa)
의 경우 pa의 값 자체는 바뀌지 않으며 pa가 가르키는 배열 요소의 값이 증가한다- 포인터 - 포인터 -> 값의 차 / 가리키는 자료형의 크기
10-2 배열을 처리하는 함수
- 배열명을 받을 함수의 매개변수 자리에 포인터가 필요하다
- 첫 번째 배열 요소의 주소만 알면 나머지 배열 요소는 포인터 연산으로 모두 사용할 수 있다
- 포인터를 활용하면 대량의 데이터를 다른 함수로 복사하지 않고 접근하므로 더 효율적이다
void print_ary(int *pa, size)
함수에서 배열 요소의 개수를 size에 저장해서, 그 수만큼 반복을 통해 배열에 접근할 수 있다- 함수를 호출할 때 주는 배열 요소의 개수는 sizeof 연산자로 구할 수 있다
// 배열에 값을 입력하는 함수 예제 코드 void input_ary(double *pa, int size) { int i; printf("%d개의 실수값 입력 : ", size) for(i=0; i<size; i++) { scanf("%lf",pa + i); // &pa[i]도 가능 } }
- 함수의 매개변수 자리에 배열을 선언하면 배열의 저장 공간이 할당되지 않으며, 배열명은 컴파일 과정에서 첫 번째 배열 요소를 가리키는 포인터로 바뀐다
ex) void func(int pa[5]){...} -> void func(int *pa){...}
Chapter 11 문자
11-1 아스키 코드 값과 문자 입출력 함수
- 컴파일러는 a를 변수명으로 해석하는 반면, ‘a’는 문자 상수로 해석한다
- 제어 문자는 백슬래시와 함께 표시하며 출력할 때 그 기능을 수행한다
- 컴파일러는 문자에서 아스키 코드 값을 갖는 오른쪽 1바이트만 변수에 저장하고 남는 바이트는 버린다
- scanf함수는 제어 문자도 입력하므로 주의해야 한다 ( 데이터 구분을 위해 칸을 띄우거나 Tab을 사용하면 해당 문자도 데이터로 입력되며 이를 화이트 스페이스 라고 한다 )
- 만약 화이트 스페이스를 제외한 문자들만 입력하고 싶다면
scanf(" %c %c",&ch1, &ch2);
이런식으로 %c 앞에 화이트 스페이스 중에 아무거나 하나를 추가한다 이경우 scanf 함수는 입력 문자 중에서 화이트 스페이스는 무시한다 - 문자를 입력하는 경우에는 가능한 char형 변수를 사용하는 것이 좋다
- 문자만 입출력하는 경우는 문자전용 함수를 쓰는것이 효율적이다
ex) getchar(), putchar()
- getchar() 반환형이 int형인 이유는 데이터를 입력하는 경로가 키보드가 아닌 파일로 바뀌었을때도 대응하기 위해
- 문자를 반복 입력하는 경우 반환값을 일단 int형 변수에 저장하고 -1과 비교한 후에 -1이 아니면 별도의 char형 변수에 옮겨 사용하는 것이 좋다
11-2 버퍼를 사용하는 입력 함수
- 버퍼는 프로그램에서 직접 할당하는 것이 아니고 프로그램을 실행하는 중에 운영체제가 자동으로 할당하는 메모리의 저장 공간이다
- scanf 함수는 키보드와 직접 연결되지 않고 정해진 크기와 형태를 가진 버퍼에서 입력을 받는다
- scanf 함수가 -1 반환하기 전까지 반복 입력하면 개행 문자를 포함한 모든 문자를 데이터로 사용할 수 있다
- scanf 함수는 기본적으로 입력한 값의 개수를 반환한다
- 키보드로 숫자를 입력하는 경우에도 일단 문자열의 형태로 버퍼에 저장된다 그후 문자열이 실제 연산이 가능한 값으로 변환되어 변수에 저정된다
- 변환 문자는 코드화된 문자열을 숫자로 변환하는 방법을 scanf 함수에 알려주는 역할을 한다
- scanf 함수의 반환값과 비교하는 값으로 -1대신 EOF를 쓸 수 있다
- 버퍼에 남아있는 개행문자는 getchat() 함수를 호출하거나 fflush(stdin)으로 제거할 수 있다
- fgetc와 fputc함수는 추가적인 인수가 필요한 것 외에는 getchar, putchar 함수와 기능은 같다
Chapter 12 문자열
12-1 문자열과 포인터
- 컴파일 과정에서 문자열은 첫 번째 문자의 주소로 바뀐다
- 문자열은 컴파일 과정에서 char 변수의 주소로 바뀌나, 주소로 접근하여 문자열을 바꿔서는 안된다
- 컴파일러는 같은 문자열을 여러 번 사용한 경우 하나의 문자열만 메모리에 저장하고 그 주소를 공유하도록 번역한다
- scanf 함수는 공백 문자 전까지의 문자열을 배열에 저장하고 널 문자를 붙인다
- scanf 함수로 문자열을 입력할 때는 [배열의크기 -1]까지 입력이 가능하다
- 공백이나 탭 문자를 입력할 때는 gets 함수를 사용한다
- gets 함수는 버퍼에서 개행 문자를 가져오지만 배열에는 널 문자로 바꿔 저장한다
- 안전하게 문자열을 입력하려면 배열 크기를 확인하는 fgets 함수를 사용하는 것이 좋다
- fgets 함수는 입력 버퍼를 선택할 수 있는 함수이므로 키보드로 입력할 때는 항상 stdin을 넣어야 한다
- fgets 함수는 버퍼에 있는 개행 문자도 배열에 저장하고 널 문자를 붙여 문자열을 완성한다
- fgets 함수에서 개행문자는 다음 공식을 이용하여 제거한다
str[strlen(str)] - 1] = '\0'
- 버퍼에 남아있는 개행문자를 지우는 방법은 간단하다, 문자 입력 함수를 호출하면 된다
ex) getchar(); scanf("%*c"); fgetc(stdin);
- puts와 fputs 함수는 문자열의 시작 위치부터 널 문자가 나올 때 까지 모든 문자를 출력한다
12-2 문자열 연산 함수
strcpy(dst, src);
에서 복사할 문자열의 시작 위치를 알 수 있다면 어떤 것이든 두 번째 인수로 사용할 수 있다- dst 에는 char 배열이나 배열명을 저장한 포인터만 입력될 수 있다
- strcat 함수는 메모리를 침범할 수 있다는 점을 유의하자
- strcat 함수는 특정 문자열로 초기화 하거나 최소한 첫 번째 문자가 널 문자가 되도록 초기화 해야한다 ( 쓰레기 값의 중간부터 붙여넣을 가능성이 있어서 )
- strcmp 함수의 반환값으로 사전 순서를 판단할 때는 반드시 대소문자를 일치시켜야 한다
Chapter 13 변수의 영역과 데이터 공유
13-1 변수 사용 영역
- 지역 변수 사용의 장점중 하나는 디버깅에 유리하다는 점
- 지역 변수는 자동 초기화가 되지 않으나 전역 변수는 0으로 자동 초기화된다
- 정적 지역 변수는 사용범위가 블록 내부로 제한되나 저장 공간이 메모리에 존재하는 기간은 전역 변수와 같다
- 정적 지역 변수는 프로그램이 끝날 때 까지 저장 공간을 유지하면서 특정 함수에서만 쓰는 경우 유용하다
- 반복문에 쓰는 변수와 같이 사용 횟수가 많은 경우 레지스터에 할당하면 실행 시간을 줄일 수 있다
13-2 함수의 데이터 공유 방법
- 값을 복사해서 전달하는 방식과 주소를 전달하는 방식중 꼭 필요한 경우가 아니면 값을 복사해서 전달하는 방식을 기본으로 한다
- C 언어에는 call by reference를 구현하는 문법 형식이 없다, 다만 주소를 함수의 인수로 주고 포인터로 받아 간접 참조 연산을 통해 call by reference와 비슷한 효과를 낼 수 있다 결국 이 방법도 주소 값을 주고 받으므로 call by value 이다
- 주소를 반환하여 쓸 수 있는 경우는 정적 지역 변수나 전역 변수의 주소를 반환하거나 호줄하는 함수에 있는 변수의 주소를 인수로 받을 경우 그 주소를 다시 반환할 수 있다
Chapter 14 다차원 배열과 포인터 배열
14-1 다차원 배열
int score[3][4]
에서 물리적으로 7번째에 있는 요소가 score 배열의 몇 번 첨자인가를 구하는 공식은 우선 행 첨자의 경우 1차원 배열로 계산했을 때의 위치 / 열의 수 즉 6 / 4 = 1 이며 열 첨자의 경우 1차원 배열로 계산했을 때의 위치 % 열의 수 즉 6 % 4 = 2 이다void main(void) { int num[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; // 좋지 않은 2차원 배열 초기화 코드 형식 int num[3][4] = { // 좋은 2차원 배열 초기화 코드 형식 {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; }
- 2차원 배열 선언시 일부 초기값을 생략할 수 있으며, 행의 갯수를 생략할 수 있다
- 2차원 배열에서 부분배열명은 1차원 배열의 배열명이며 각 행의 단위를 독립적으로 처리할 때 배열명의 역활을 수행한다
ex) scanf("%s", animal[0]);
여기서 animal[0]은 배열명으로 부분배열의 시작 위치 값이다 따라서 앞에 주소 연산자 &를 붙일 필요가 없다 - 2차원 배열의 행크기를 구하는 방법은 count = sizeof(score) / sizeof(score[0]);
14-2 포인터 배열
- 처리할 데이터가 여기저기 흩어져 있더라도 그 주소만 따로 모아 놓으면 모든 데이터를 쉽게 처리할 수 있다
char *pary[5];
에서 각 배열 요소의 자료형이 포인터형이므로 배열명 앞에 별(*)을 붙인다- char 포인터 배열의 초기화 방법은 2차원 char 배열의 초기화 방법과 같다
- 포인터 배열의 초기화는 문자열의 시작 주소만 배열 요소에 저장되며, 2차원 char 배열의 초기화는 문자열 자체를 배열의 공간에 저장한다
- 1차원 배열의 포인터를 배열로 연결하면 2차원 배열처럼 쓸 수 있다
ex) int *pary[3] = { arr1, arr2, arr3 };
의 각 요소는pary[i][j];
이렇게 접근 및 사용할 수 있다 - 즉 포인터 배열에서 각 요소 pary[0], pary[1], pary[2]는 부분배열명 기능을 한다
- C언어를 배우다 보면 다양한 궁금증이 생길건데 그때는 두려워하지 말고 코드를 직접 수정해보며 결과를 확인해보는 습관을 들이자
- 포인터 배열을 2차원 배열처럼 사용할 수 있는 이유는 배열 표현이
pary[2][2]
인 경우 포인터 표현*(pary[2] + 2);
과 같기 때문이다
Chapter 15 응용 포인터
15-1 이중 포인터와 배열 포인터
- 포인터의 주소는 이중 포인터에 저장되며 포인터를 가르킨다
int **ppi
에서int *
는 가르키는 자료형을 뜻하며*ppi
는 ppi자신이 포인터임을 뜻한다- 주소는 상수이기 때문에 주소 연산자를 쓸 수 없다
void swap_ptr(char **ppa, char **ppb);
함수를 그림으로 표현하면 아래와 같다
- 포인터가 배열명을 저장하면 배열명 처럼 쓸 수 있으므로 함수 안에서는 매개변수를 배열명처럼 사용하여 문자열을 출력한다
int ary[5];
에서 ary와 &ary의 값은 모두 배열의 시작위치 이나 가르키는 대상이 다르므로 두 주소에 1을 더한 결과는 다르다- 배열의 크기와 배열 요소의 개수는 다른 의미이다
- 배열은 전체가 하나의 논리적인 변수이다
- 배열은 논리적인 변수이므로 일반 변수처럼 대입 연산자 왼쪽에 사용하는 것은 불가능하다
- 배열의 주소에 정수를 더하면 배열이 할당된 메모리 영역을 벗어나므로 특별한 경우가 아니면 사용하지 않는다
- 2차원 배열은 1차원 배열로 만든 배열이므로 논리적인 배열 요소가 1차원 배열이다
- 2차원 배열의 이름은 1차원 배열의 주소이다
- 배열 포인터는 배열을 가르키는 포인터로 2차원 배열의 이름을 저장할 수 있다
void print_ary(int (*pa)[4])
{
// ...
}
void main(void)
{
int ary[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
int (*pa)[4]; // int형 변수 4개의 배열을 가르키는 배열 포인터, 괄호가 없으면 포인터 배열이 되므로 주의하자
pa = ary; // 가능
print_ary(ary); // 2차원 배열명을 인수로 주면 함수에는 첫 번째 부분배열의 주소가 전달된다
}
- 배열 포인터를 매개변수에 쓰면 함수 안에서 2차원 배열처럼 쓸 수 있다
- 2차원 배열 ary의 논리적 배열 요소의 개수는? 3개
- 2차원 배열 ary의 물리적 배열 요소의 개수는? 12개
ary + 1
-> 100 + (1 * sizeof(ary[0])) -> 100 + (1 * 16) -> 116*(ary + 1)
-> ary[1]*(ary + 1) + 2
-> *(ary + 1) + (2 * sizeof(ary[1][0])) -> 116 + ( 2 * 4 ) -> 124*(*(ary + 1) + 2)
-> ary[1][2]- 결국 첫 번째 부분배열의 주소인 ary를 사용하면 배열의 모든 공간과 값을 사용할 수 있다, 같은 원리로 ary를 배열 포인터에 저장하면 배열 포인터를 배열처럼 쓰는 것이 가능하다
15-2 함수 포인터와 void 포인터
- 기능은 다르지만 형태가 같은 함수를 선택적으로 호출할 때 함수 포인터가 필요하다
- 명령어들의 집합인 함수의 주소를 이해하고 포인터를 통해 함수의 기능을 사용해보자
- 함수명은 함수 정의가 있는 메모리의 시작 위치이다
- 컴파일 후 함수명이 함수가 올려진 메모리 주소로 바뀌므로 함수를 호출할 때 함수명을 사용한다
- 함수의 주소도 포인터에 저장하면 포인터로 함수를 호출할 수 있다
- 함수 형태는 매개변수의 개수와 자료형 그리고 반환값의 자료형으로 정의한다
- 함수 포인터
int (*fp)(int, int);
에서 괄호가 없으면 주소를 반환하는 함수의 선언이 되므로 주의하자 - 함수 포인터도
res = fp(10, 20);
이런식으로 간접 참조 연산자 없이 바로 함수 호출이 가능하다 - 만약 func 함수만 따로 만드는 경우 만드는 시점에서 연산 방법을 결정할 수 없다면 일단 함수 포인터를 쓰고 나중에 func 함수를 호출하는 곳에서 연산 방법을 함수로 구현할 수 있다
- void 포인터는 가리키는 자료형이 정해지지 않은 포인터이다
- void 포인터는 가리키는 자료형이 정해져 있지 않으므로 어떤 주소든 저장할 수 있다 또한 같은 이유로 간접 참조 연산이나 정수를 더하는 포인터 연산이 불가능 하다
- void 포인터를 사용할 때는 원하는 형태로 변환하여 사용한다
*(int *)vp
와 같이 형 변환 후에는 각각 가리키는 변수를 출력하기 위해 간접 참조 연산을 수행한다- 대입 연산을 할 때는 형 변환 없이 void 포인터를 다른 포인터에 대입할 수 있으나 명시적 형 변환을 해야 형 변환 실수를 했을때 컴파일러가 경고 메시지를 날려준다
Chapter 16 메모리 동적 할당
16-1 동적 할당 함수
- 처리할 데이터 종류나 수를 장담할 수 없다면 필요한 변수나 배열의 공간을 실행 도중에 동적으로 확보하는 것이 좋다
- 프로그램 실행 중에 저장 공간을 할당하는 것을 동적 할당 이라고 한다
- 동적 할당 할때 직접 바이트 수를 인수로 주는 것보다 sizeof 연산자로 각 자료형에 대한 크기를 계산해 주는것이 좋다
ex) pd = (double*)malloc(sizeof(double));
- malloc 함수의 반환값이 널 포인터인지 반드시 확인하고 사용해야 한다 ( 널 포인터는 보통 NULL로 표기하는데 전처리 단계에서 0으로 바뀌므로 정수 0과 같다고 생각하면 된다 )
- 예외상황이 발생하여 프로그램을 바로 종료하는 경우 인수로 1을 주고 호출한다
ex) exit(1);
- 동적으로 할당한 저장 공간은 해당 프로그램이 종료될 때 운영체제에 의해서 자동으로 회수되지만 메모리 누수를 피하기 위해 메모리 해제를 잊지말자
- calloc 함수는 메모리를 동적으로 할당하여 0으로 초기화된 메모리 공간을 얻고자 할 때 사용한다
- realloc 함수로 저장 공간의 크기를 조절할 수 있다
- 사용하던 저장 공간의 위치를 포인터가 기억하고 있더라도 재할당 과정에서 메모리의 위치가 바뀔 수 있으므로 항상 realloc 함수가 반환하는 주소를 다시 포인터에 저장해 사용하는 것이 좋다
- realloc 함수는 첫 번째 인수가 널 포인터인 경우 molloc과 같은 기능을 수행하여 두 번째 인수의 크기만큼 동적 할당하고 그 주소를 반환한다
16-2 동적 할당 저장 공간의 활용
- 문자열을 저장하기 위해 malloc함수를 활용할 경우 널 문자도 포함할 수 있도록 인수에 + 1을 하자
ex) str[i] = (char*)malloc(strlen(temp) + 1);
- 동적 할당 영역에 저장한 문자열을 함수로 처리하는 예
ex) void print_str(char **ps);
- 포인터 배열은 선언과 동시에 널 포인터로 초기화하여 참조할 때 널 포인터인지를 검사하면 더 안정적인 프로그래밍이 가능하다
- 명령행에서 프로그램을 실행시킬 때는 프로그램의 이름 외에도 프로그램에 필요한 정보를 함께 줄 수 있는데 이들을 모두 명령행 인수 라고 한다
int main(int argc, char **argv)
Chapter 17 메모리 동적 할당
17-1 구조체
- 구조체는 배열이 갖고있는 단점인 모든 데이터의 형태가 같아야 하는 불편함을 해소해준다
- 구조체 선언이 main함수 앞에 있으면 프로그램 전체에서 사용할 수 있고, 함수 안에 선언하면 그 함수 안에서만 쓸 수 있다
- 컴파일러는 구조체 멤버의 크기가 들쑥날쑥한 경우 멤버 사이에 패딩 바이트를 넣어 멤버들을 가지런하게 정렬하는데 이를 바이트 얼라이언트 라고 한다
- 구조체 변수의 크기는 바이트 얼라이먼트로 인해 시스템 마다 다를 수 있다
- 패딩 바이트가 가장 작도록 구조체를 선언하면 메모리를 아낄 수 있다
pragma pack(1);
이런식으로 컴파일러에 패딩 바이트를 넣지 않도록 지시할 수 있다, 이 구문은 보통 구조체 선언 전에 적어주며, include 다음에 넣어준다
17-2 구조체 활용, 공용체, 열거형
- 구조체 변수의 주소는 구조체 포인터에 저장하며, 구조체 변수 전체를 가르킨다
(*ps).kor
은ps->kor
과 같은 의미이다struct address list[5] = { <- 배열 초기화 괄호 { <- 구조체 초기화 괄호 "홍길동", 23, "111-1111", "울릉도 독도" }, ... }
- 구조체 배열의 이름을 인수로 받는 함수는 구조체 포인터를 매개변수로 선언한다
struct address
{
char name[20];
int age;
char tel[20];
char addr[80];
}
void print_list(struct address *lp)
void main(void)
{
struct address list[5] = { ... };
print_list(list);
}
- 다음 세 가지 표현은 모두 같은 결과값을 같는다
ex) lp[i].name
(*(lp+i)).name
(lp+i)->name
- 연결리스트 에서 첫 번째 변수의 위치만 알면 나머지 변수는 포인터를 따라가 모두 사용할 수 있으므로 대부분 첫 번째 변수의 위치를 head 포인터에 저장해 사용한다
- 공용체 변수의 크기는 멤버 중에서 크기가 가장 큰 멤버로 결정된다
- 공용체 변수의 초기화는 중괄호를 사용하여 첫 번째 멤버만 초기화 된다 ( 만약 첫 번째 멤버가 아닌 멤버를 초기화할 때는 멤버 접근 연산자 .로 멤버를 직접 지정해야 한다 )
- 공용체 멤버는 언제든지 다른 멤버에 의해 값이 변할 수 있으므로 항상 각 멤버의 값을 확인해야 하는 단점이 있다
- typedef 문으로 필요에 따라 기본 자료형도 재정의해서 사용할 수 있다
ex) typedef unsigned int nbyte;
Chapter 18 파일 입출력
18-1 파일 개방과 입출력
- 데이터를 입출력하기 전에 준비하는 과정을 파일개방 이라고 부른다
- fopen 함수가 개방할 파일을 찾는 기본 위치는 [현재 작업 디렉터리] 이다, 이 기준은 프로그램 개발이 끝나고 실행 파일을 직접 실행할 때 적용된다, 비쥬얼 스튜디오 같은 통합 개발 환경에서는 프로젝트 폴더가 현재 작업 디렉토리가 되므로 개방할 파일이 프로젝트 폴더에 저장된다
- fopen 함수는 실제 파일이 있는 장치와 연결되는 스트림 파일을 메모리에 만든다, 이 포인터를 가지면 입출력 함수를 통해 원하는 입출력 작업을 수행할 수 있다
- 스트림 파일은 프로그램과 입출력 장치 사이의 다리 역할을 하는 논리적인 파일이다
- 프로그램은 일단 메모리에 있는 스트림 파일로 입출력을 수행하고 그파일이 다시 물리적인 장치와 연결되어 실제적인 입출력을 수행한다
- 스트림 파일 구조체(FILE)는 입출력에 필요한 정보가 저장되어 있다
- 스트림 파일에는 문자를 입력할 버퍼의 위치를 알려주는 지시자가 있다
- 만약 버퍼의 데이터를 즉시 장치로 출력해야 한다면 fflush() 함수를 사용하면 된다
- 운영체제는 프로그램을 실행할 때 기본적으로 3개의 스트림 파일을 만든다
stdin
stdout
stderr
- getchar() 함수가 처음 호출되면 키보드에서 입력하는 데이터는 개행 문자와 함께 stdin 스트림 파일의 버퍼에 한꺼번에 저장된다
- fgetc() 함수와 fputc() 함수는 stdin, stdout, stderr와 같은 파일 포인터를 직접 인수로 사용할 수 있다
ex) ch = fgetc(stdin);
이를 통해 별도의 파일을 개방하지 않고 운영체제가 개방한 스트림 파일을 사용할 수 있다 - fgetc() 함수는 리턴 문자(\r)을 읽으면 버리고 다음의 개행 문자(\n) 하나만 입력한다
- fgetc() 함수는 ctrl + z에 대한 아스키 문자를 읽으면 파일의 끝으로 인식한다 주의할 점은 이러한 동작은 텍스트 모드로 개방된 파일에서만 수행되며 바이너릴 모드로 개방된 파일은 파일의 내용을 있는 그대로 읽거나 쓴다
- 입출력 순서가 꼬이지 않게 하려면 버퍼의 데이터를 하드디스크로 옮기고 버퍼을 읽기 위한 공간으로 설정한 후에 하드디스크의 데이터를 처음부터 다시 읽도록 해야한다
- rewind() 함수는 위치 지시자를 맨 처음으로 설정한다
- feof() 함수는 스트림 파일의 데이터를 모두 읽었는지 확인할 때 유용하다
18-2 다양한 파일 입출력 함수
- 파일에서 데이터를 한 줄씩 입력할 때는 fgets() 함수를 사용한다, 이 함수는 데이터의 크기가 큰 경우 저장 공간의 크기까지만 입력할 수 있으므로 할당하지 않은 메모리를 침벙할 가능성을 차단한다
- 문자열 입출력은 안전하고 정확하게 수행되는 fgets()와 fputs() 함수를 사용하는 것이 좋다, 또한 이 함수들에 stdin과 stdout을 파일 포인터로 주면 키보드와 모니터로 데이터의 입출력이 가능하다
- fscanf(), fprintf() 함수는 scanf(), printf() 함수와 같은 기능을 수행하지만 파일을 지정해 줄 수 있다
- fscanf() 함수는 파일의 데이터를 입력할 각 변수의 형태에 맞게 자동 변환 한다
- fprintf() 함수는 각 변수의 데이터를 모두 문자열로 변환하여 파일에 출력한다
void my_fflush(FILE *fp) // 스트림 버퍼에서 개행 문자까지 데이터를 읽어 제거
{
while(fgetc(fp) != '\n') { }
}
- fread()와 fwrite() 함수는 구조체나 배열과 같이 데이터양이 많은 경우도 파일에 쉽게 입출력할 수 있다 주의할 점은 항상 바이너리 모드로 개방해야 한다는 것
Chapter 19 전처리와 분할 컴파일
19-1 전처리 지시자
#include
는 지정한 파일의 내용을 읽어와 지시자가 있는 위치에 붙여 놓는다- 꺽쇠괄호를 사용하면 복사할 파일을 컴파일러가 설정한
#include
디렉터리에서 찾고 큰따옴표를 사용하면 소스 파일이 저장된 디렉터리에서 먼저 찾는다 - 전처리 지시자는 컴파일러가 처리하는 것이 아니므로 백슬레시를 제어 문자로 사용하지 않는다
- 매크로명을 정의하면 복잡한 상수나 문장을 의미 있는 단어로 쓸 수 있다
- 컴파일러는 전처리가 끝난 후에 치환된 소스 코드로 컴파일하고 사용자는 매크로 명으로 작성된 소스코드를 보게 되므로 컴파일러가 표시하는 에러 메시지를 소스 코드에서 즉시 확인하기 힘들다 따라서 매크로명은 필요한 경우에만 제한적으로 쓰는 것이 좋다
- 매크로 함수는 치환된 후의 부작용을 줄이기 위해 치환될 부분에 괄호를 써서 정의한다 이때 치환될 부분을 구성하는 인수에 모두 괄호를 붙이는 것이 좋다
- 매크로 함수는 호출한 함수로 이동할 때 필요한 준비작업이 없으므로 함수 호출보다 상대적으로 실행 속도가 빠르다 따라서 크기가 작은 함수를 자주 호출한다면 매크로 함수가 도움이 될 수 있다
- 이미 정의된 매크로는
__FILE__
__FUNCTION__
__LINE__
__DATE__
__TIME__
가 있다 #pragama
지시자는 컴파일 방법을 세부적으로 제어할 때 사용한다
19-2 분할 컴파일
- C언어는 분할 컴파일을 통해 여러개의 소스 코드를 각각 독립적으로 작성하고 컴파일 할 수 있으며 컴파일된 개체 파일을 링크하여 하나의 큰 프로그램으로 만들 수 있다
- 컴파일을 하면 obj 파일이 생기고 솔루션 빌드를 하면 링크를 수행해 실행 파일이 생성된다
- 컴파일을 위해 파일을 나눌때 주의할 점은 각 파일을 독립적으로 컴파일할 수 있도록 필요한 선언을 포함해야 한다는 것이다
- 다른 파일에 선언된 전역 변수를 사용할 때는 extern 선언을 한다 반면에 다른 파일에서 전역 변수를 공유하지 못하게 할 때는 static을 쓴다
- 다른 파일과 데이터를 공유할 필요가 없는 전역 변수라면 다른 파일과의 중복을 차단하는 것이 좋다
- 다른 파일의 전역 변수를 공유하는 경우라면 항상 명시적으로 extern 선언을 하는 것이 좋다
- extern 선언을 한 경우는 초기화하지 않도록 주의하자
- 함수에 static을 사용하지 않으면 함수 선언은 기본적으로 extern 선언으로 간주된다 따라서 extern 없이 원형 선언만으로 다른 파일의 함수를 호출할 수 있다 단 다른 파일에 있는 함수임을 명시적으로 표현하기 위해 extern을 붙이기도 한다
- 조건부 컴파일 전처리 명령이 있으면 중복 포함 문제를 해결할 수 있다
- 매크로명은 헤더 파일명과 비슷하게 만들어 헤더 파일이 다르면 인클루드하더라도 매크로명이 중복되지 않도록 하자
댓글남기기