지금의 에러가 그대들을 현명한 프로그래머로 이끌어 줄 것이다.
널(Null) 문자에 대한 이해
문자열을 표현할 때에는 문자열의 끝을 의미하는 '\0'을 문자열 끝에 삽입한다.
이 문자를 가리켜 널(Null) 문자라 하며, 아스키 코드 값은 0이다.
문자열 입력받기
scanf 함수는 공백(스페이스, tab, enter)을 기준으로 데이터 수를 결정짓는다.
따라서 My Sweet Home이라고 입력되면 My, Sweet, Home 세 개의 문자열을 입력한 샘이다.
포인터
메모리 공간의 주소 값을 저장하는데 사용하는 변수를 포인터라 한다.
포인터가 변수라는 것을 강조하기 위해서 '포인터 변수'라는 표현을 쓴다.
- 포인터는 const 키워드에 의해서 상수화되기도 한다. (뒤에서 자세히 설명)
- 포인터 변수의 크기는 타입에 상관없이 4byte로 표현된다. (컴퓨터 주소 체계가 4byte로 표현된다는 것을 의미)
- 포인터에도 타입(type: 형)이 존재한다. 가리키고자 하는 변수의 자료형에 따라 적절한 타입의 포인터를 선언해야 한다.
주소 값을 참조할 때 사용하는 연산자는 & 연산자이다.
예를 들어, &a 는 변수 a의 주소 값을 반환하라는 의미다.
포인터 변수 앞에 * 연산자를 붙이게 되면, 포인터가 가리키는 메모리 공간에 존재하는 값을 참조하겠다는 뜻이 된다.
포인터에 다양한 타입이 있는 이유
포인터에 타입이 존재하지 않는다면, 포인터를 이용해서 변수를 참조하는 경우 몇 바이트를 읽어들여야 할 지 알 수 없게 된다.
포인터의 타입은 메모리를 참조하는 방법을 알려주는 역할을 한다.
포인터와 배열
배열의 이름도 포인터다.
- 단 그 값을 바꿀 수 없는 상수라는 점이 일반적인 포인터와의 유일한 차이점이다.
- 배열 이름은 첫 번째 요소의 주소 값을 나타낸다.
- 배열 이름을 가리켜 "상수 포인터"라 한다.
포인터를 배열의 이름처럼 사용할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h> int main(void) { int arr[3] = {0, 1, 2}; int *ptr; ptr = arr; printf("%d, %d, %d \n", ptr[0], ptr[1], ptr[2]); return 0; } | cs |
포인터는 제한된 형태의 연산이 가능하다.
포인터 연산 - 포인터 값을 증가 혹은 감소시키는 것이다. (곱셈, 나눗셈은 불가능)
1 2 3 4 | // 아무것도 가리키지 않는 형태 = 널(Null) 포인터 int* ptr1 = 0; char* ptr2 = 0; double* ptr3 = 0; | cs |
포인터 연산에 따른 실질적인 값의 변화는 포인터 타입에 따라 다르다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 배열을 다룰 때 범위를 넘어선 접근을 하지 않도록 주의해야 한다. #include <stdio.h> int main(void) { int arr[5] = {1, 2, 3, 4, 5}; int* pArr = arr; printf("%d\n", *pArr); printf("%d\n", *(++pArr)); printf("%d\n", *(++pArr)); printf("%d\n", *(pArr + 1)); printf("%d\n", *(pArr + 2)); return 0; } | cs |
arr[i] = *(arr+i)
배열의 이름을 포인터처럼 사용할 수 있는 방법
arr이 포인터이거나 배열 이름인 경우 arr[i] == *(arr+i)
1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h> int main(void) { int arr[2] = { 1,2 }; int* pArr = arr; // 배열 이름을 통한 출력(배열 이름을 포인터처럼 사용) printf("%d %d \n", arr[0], *(arr + 1)); // 포인터 변수를 통한 출력(포인터를 배열처럼 사용) printf("%d %d \n", pArr[0], *(pArr + 1)); return 0; } | cs |
문자열 상수를 가리키는 포인터
배열 str1은 문자열 전체를 저장
포인터 str2는 메모리상에 저장되어 있는 문자열 상수 "ABCD"를 단순히 가리킴
1 2 | char str1[5] = "abcd"; char *str2 = "ABCD"; | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h> int main(void) { char str1[5] = "abcd"; // 문자열 변수 선언 char *str2 = "ABCD"; // 문자열 상수 선언 printf("%s \n", str1); printf("%s \n", str2); str1[0] = 'x'; // 문자열 변수 변경, 따라서 OK //str2[0] = 'x'; // 문자열 상수 변경, Error 발생 printf("%s \n", str1); printf("%s \n", str2); return 0; } | cs |
문자열 상수는 메모리 공간에 할당되면 주소를 반환한다.
똑같은 문자열을 선언하면 한번만 메모리 공간에 할당된다.
- 좋은 컴파일러는 똑같은 코드라 할지라도 메모리를 효율적으로 사용하기 위해서 최적화(Optimization)라는 과정을 거친다.
- 문자열이 상수면 내용 변경이 불가능하다. 이러한 상황에서 따로 만들면 메모리만 차지하기 때문에, 이는 메모리 공간을 효율적으로 사용하기 위해 컴파일러가 진행한 최적화의 결과이다. (단, 일부 컴파일러에만 해당)
- Turbo C/C++ 컴파일러는 문자열 상수 조작도 허용하고 같은 문자열 상수라 할지라도 두 번 메모리 공간에 할당한다. (최적화 수행 X)
1 2 3 4 5 6 7 8 | #include <stdio.h> int main(void) { char *str1 = "Good!"; char *str2 = "Good!"; printf("%d, %d \n", str1, str2); return 0; } | cs |
포인터 배열
배열 요소로 포인터를 지니는 포인터 배열
1 2 3 4 5 6 7 8 9 10 | #include <stdio.h> int main(void) { int a = 10, b = 20, c = 30; int* arr[3] = {&a, &b, &c}; printf("%d \n", *arr[0]); printf("%d \n", *arr[1]); printf("%d \n", *arr[2]); return 0; } | cs |
문자열 배열 (char형 포인터 배열)
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h> int main(void) { char* arr[3] = {"Fervent-lecture", "TCP/IP", "Socket Programming" }; printf("%s \n", arr[0]); printf("%s \n", arr[1]); printf("%s \n", arr[2]); return 0; } | cs |
포인터와 함수
함수의 인자로 배열 전달하기
- 함수의 인자 전달 방식
인자 전달의 기본 방식은 복사다. 즉, 함수 호출 시 전달되는 값을 매개 변수라는 것을 통해서 전달 받는데, 이때 값의 복사가 일어난다.
함수 호출 시 배열을 통째로 복사하여 넘겨주는 방법은 존재하지 않는다. 이에 대안은 포인터를 이용하는 것이다. (구조체를 정의하면 배열을 통째로 복사해서 넘겨줄 수 있다) - 배열을 함수의 인자로 전달하는 방식
배열을 통째로 전달하는 것이 불가능하다면, 배열의 주소 값을 인자로 전달해서 이를 통해서 접근하도록 유도하는 방법을 생각해 볼 수 있다.1234567891011121314151617#include <stdio.h>void fct(int *arr2);int main(void){int arr1[2] = {1, 2};fct(arr1);printf("%d \n", arr1[0]);return 0;}void fct(int *arr2){printf("%d \n", arr2[0]);arr2[0]=3;}cs - 배열을 인자로 전달받는 함수의 또 다른 선언
ArrAdder 호출 시 첫 번째 인자(배열의 주소)만 전달할 경우 함수 내에서 배열의 길이를 알 방법이 없다. 그래서 반드시 배열의 길이를 두 번째 인자로 전달해야 한다.
둘 다 int형 포인터 변수이다.
다만 함수의 매개 변수를 선언하는데 있어서 인자로 배열이 전달된다는 것을 좀 더 명확히 할 수 있도록 "int pArr[]"이라는 선언을 허용하는 것 뿐이다.공부하는 동안에는 배열을 인자로 전달받는 매개 변수 선언 시 "int *arr" 와 같은 선언을 주로 사용하기 바란다. "int arr[]" 와 같은 선언은 매개 변수 선언 시 예외적으로 허용되는 방법에 지나지 않는다. (다른 사람이 어떠한 방식으로 구현하더라도 이해할 수 있도록 설명해준 것이다)
배열의 길이를 얻는 방법(공식) : sizeof(arr)/sizeof(int)
ArrAdder 호출 시 첫 번째 인자(배열의 주소)만 전달할 경우 함수 내에서 배열의 길이를 알 방법이 없다. 그래서 반드시 배열의 길이를 두 번째 인자로 전달해야 한다.
Call-By-Value 와 Call-By-Reference
함수의 호출 방식
- Call-By-Value (값에 의한 호출)
가장 일반적인 함수 호출의 형태 "값의 복사" - Call-By-Reference (참조에 의한 호출)
함수 호출 시 변수의 주소를 전달해서, 인자로 전달된 주소가 가리키는 변수의 조작을 함수 내에서 가능하게 하는 것.
이제는 알 수 있다! scanf 함수 호출 시 &를 붙이는 이유
scanf 함수는 내부적으로 사용자로부터 정수를 입력받은 다음, 변수 val에 접근해서 값을 대입해야 한다. 결국 Call-By-Reference에 해당한다.
포인터와 const 키워드
상수를 만들 때 사용하는 const 키워드
포인터를 상수화
- 포인터 자체를 상수화시킨다
p가 가리키는 변수의 값을 못 바꾸게 하겠다는 의미
즉, 포인터 p를 통해서 변수 a의 값을 변경하는 것만 막겠다는 의미
따라서 변수 a의 조작은 문제되지 않는다.1234int a = 10;const int* p = &a;*p = 30; // Errora = 30; // OK!cs - 포인터가 가리키는 변수를 상수화시킨다
p가 지니는 주소 값을 변경할 수 없다는 뜻 - const를 사용하는 이유?
const 키워드를 많이 사용하게 되면 프로그램의 구성은 안정적이 된다.
const 사용전
컴파일하고 실행하는 과정에서 오류메시지를 보여주지 않음
다만 결과 값이 이상할 뿐....123456789101112#include <stdio.h>float PI = 3.14;int main(void){float rad;PI = 3.07; // 분명히 실수!scanf("%f", &rad);printf("원의 넓이는 %f \n", rad*rad*PI);return 0;}cs
const 사용 후
자신의 실수를 컴파일러에 의해서 발견
이렇게 실수를 컴파일러가 발견할 수 있도록 프로그램을 작성하는 것은 아주 중요하다.
훌륭한 프로그래밍 습관은 아주 사소한 것에서부터 시작된다.123456789101112#include <stdio.h>const float PI = 3.14; // PI값 변경 시 오류 발생int main(void){float rad;PI = 3.07; // 분명히 실수!scanf("%f", &rad);printf("원의 넓이는 %f \n", rad*rad*PI);return 0;}cs
|
'Books > 열혈강의 C 프로그래밍' 카테고리의 다른 글
[열혈C] 06. Part3 Chapter20 도전! 프로그래밍 THREE (0) | 2018.04.27 |
---|---|
[열혈C] 05. Part3 Chapter19 함수 포인터와 void 포인터 (0) | 2018.04.26 |
[열혈C] 04. Part3 Chapter18 다차원 배열과 포인터의 관계 (0) | 2018.04.25 |
[열혈C] 03. Part3 Chapter16-17 내용 정리 (다차원 배열 / 포인터의 포인터) (0) | 2018.04.24 |
[열혈C] 01. Part1 내용 정리 (C 언어의 기본) (0) | 2018.04.19 |