1. 포인터의 타입
- 포인터 변수를 선언할 때는 가리키고자 하는 대상체(object)의 타입을 반드시 명시해야 한다.
- 포인터가 저장하는 번지값이라는 것은 4바이트 크기로 고정되어 있고, 이 변수에 저장될 값은 항상 부호없는 정수형이다.
32비트 호나경에서 주소값은 항상 32비트이며 번지는 0을 포함한 양수값이다. 포인터 변수가 정수를 가리키든 실수를 가리키트 도는 구조체나 배열 같은 큰 데이터를 가리키든 대상체의 타입과는 상관없이 포인터는 항상 4바이트의 부호없는 정수값인 것이다.
- 포인터 변수 크기와 형태가 이미 고정되어 있음에도 대상체의 데이터형을 기술하는 이유는
- 첫 번째 이유는 '*'연산자로 포인터의 대상체를 읽거나 쓸 대 대상체의 바이트 수와 비트 해석 방법을 알아야 하기 때문이다.
- 두 번째 이유는 인접한 다른 대상체로 이동할 때 이동 거리를 알기 위해서다.
- 예를 들면 배열의 인덱스가 아니라 포인터를 이용하여 배열을 순회할 수 있다. 이때 포인터 변수의 증감 연산자를 이용하면 포인터에 대한 실체의 타입 크기 만큼 증감 된다.
2. 포인터의 연산
- 포인터 변수의 연산은 일반적인 산술 연산과는 다른 규칙이 적용된다.
- 포인터끼리 더할 수 없다.
- 포인터끼리 뺄 수는 있다. 결과값은 단순한 정수값이다.(즉, 결과값이 포인터가 아니다)
- 두 포인터 뺄셈의 결과는 메모리 상에서 두 주소값 차이(의 크기)에 포인터 변수의 타입의 크기로 나눈 값이다.
- 일단 이 경우는 배열과 같이 동일한 타입이 연속적으로 나열 되어 있을 때 적용되는 듯.
- 일반적으로 선언된 변수나 구조체 타입에서는 다른 룰이 적용되어 계산되는 것 같다.
- 포인터에 정수를 더하거나 뺄 수 있다. (결과는 포인터다)
- 정수값에 의해서 더해지거나 감소되는 실제 포인터값의 이동 거리는 '정수값*sizeof(타입)' 만큼이다.
- 포인터끼리 대입할 수 있다. 단, 대입식의 좌변과 우변의 포인터 타입이 일치해야 한다.
- 캐스팅 또한 포인터 구두점을 함께 기술해서 캐스팅해야함.
- 포인터와 실수와의 연산은 허용되지 않는다.
- 포인터에 곱셈이나 나눗셈을 할 수 없다.
- 포인터끼리 비교는 가능하다. (물론, 좌변과 우변의 포인터 타입이 일치해야 한다)
* if (ptr==NULL)
- 대소 비교 연산자는 메모리상 위치를 비교하는 의미로 사용됨.
*ptr++
* 연산자와 ++연산자는 연산 우선순위가 값으며 우측 우선의 결합순서를 가진다.
- 따라서
++가 먼저 연산되어야하지만 이 경우 후위형으로 기술되어 있기 때문에 *가 먼저 연산된다.
*(ptr++)로 했다 하더라도 후위형 증감 연산자는 연산 순위와 상관없이 평가된 후에 증가하기 때문에 결과는 동일하다.
- 참고 :
(*ptr)++는 ptr 포인터 변수가 가리키는 대상체에 증감연산을 실행한다.
3. void형 포인터
- 선언할 때 대상체의 타입을 명시하지 않는 포인터 다.
- 특징
- 임의의 대상체를 가리킬 수 있다.
- void형 포인터는 임의의 대상체를 모두 가리킬 수 있기 때문에 대입받을 대 어떠한 캐스팅도 할 필요가 없다.
- 좌변이 void형 포인터일 때 우변에 임의의 포인터형이 모두 올 수 있다.
- 반대로 임의의 포이터에 void형 포인터를 대입할 때는 반드시 캐스팅을 해야한다.
* 연산자를 쓸 수 없다.
- 사용하기 위해서는
*(int *)vp 처럼 캐스팅을 해야 한다.
- 포인터 연산자와 캐스트 연산의 우선 순위는 같으며 결합 순서는 우측 우선이므로 캐스트 연산자가 먼저 수행되어야 한다.
- 증감 연산자를 쓸 수 없다.
- 참고 :
(int *)vp++는 가능할 것 같지만 증감 연산자의 우선순위가 높아서 에러가 발생.
(gcc에서는 에러 없이 의도치 않은 값이 출력되었다.)
- 참고 :
((int *)vp)++도 가능할 것 같지만 캐스팅을 먼저 시도함으로써 형변환된 번지 값(상수)에 증감 연산을 적용하기 때문에 컴파일 에러가 발생한다.
- void형 포인터의 활용
void *memset(void *s, int c, size_t n);
- s 번지에서 n바이트 만큼 c값으로 채우는 함수, 주로 배열 전체를 0으로 초기화할 때 사용.
int ari[10], char arc[20], double ard[30] 과 같은 배열에 모두 적용할 수 있다.
- 즉, 타입이 다른 배열을 위해 3가지 버전의 함수를 만들 필요 없다.
- NULL 포인터
- 0으로 정의되어 있는 포인터 상수값이다.
- (아주 특수한 시스템에서는 0이 아닐 수도 있다)
- 어떤 포인터 변수가 NULL값을 가지고 있다면 이 포인터는 0번지를 가리키고 있는 것이다.
0번지라면 메모리 공가의 제일 처음에 해당하는 첫 번째 바이트인데 이 위치도 분명히 실존하는 메모리 공간이므로 0번지를 가리킬 수도 있다.
그러나 대부분의 플랫폼에서 0번지는 ROM이거나 시스템 예약 영역에 해당되므로 응용 프로그램이 이 번지에 어떤 값을 저장하거나 읽을 수 없도록 보호되어 있다.
시스템 영역에 응용 프로그램이 고유의 데이터를 저장할 수는 없으므로 포인터 변수가 0번지를 가리키는 상황은 발생할 수 없다.
그래서 이런 상황은 일종의 에러로 간주되며 그렇게 하기로 약속되어 있다.
포인터를 리턴하는 거의 대부분의 함수는 에러가 발생했을 때 NULL값을 리턴한다.
- 포인터 변수에 주소값을 상수로 대입한다거나 포인터를 정수 상수와 비교하는 것은 허락되지 않지만 유일하게 NULL 포인터만 포인터 변수에 직접 대입할 수 있다. 또한
==이나 != 연산자를 이용하여 비교할 수 있다.
4. 동적 메모리 할당
- 동적 할당이란 프로그램을 작성할 때 메모리 필요량을 지정하는 정적할당과는 달리
실행 중에 필요한 만큼 메모리를 할당하는 기법이다.
- 메모리 필요량을 프로그램 작성 중에 결정할 수 없을 때는 정적 할당을 할 수 없으며 동적할당을 사용해야 한다.
- 또한 임시적인 메모리가 필요할 때 사용한다.
- 동적 할당된 메모리는 이름이 없는 변수라고 할 수 있다.
독점적인 메모리 영역을 차지하고 있으므로 일단 값을 기억할 수 있지만 이름이 없으므로 오로지 포인터로만 접근할 수 있다.
그래서 malloc 함수가 리턴하는 포인터는 반드시 적절한 타입의 포인터 변수로 대입받아야 한다.
(시작번지를 잃어버리면 할당된 메모리를 쓸 수 없음은 물론, 다 사용하고 난 후에 해제하지도 못한다.)
- 함수내 변수의 임시공간인 스택은 용량이 그다지 크지 않아서 지역변수로 큰 크기의 배열이나 구조체를 만드는 것은 적합하지 않다.
- 메모리 관리 원칙으로 응용 프로그램들은 자신이 꼭 필요한 만큼만 할당해서 사용하고 다 쓴 후에는 반드시 반납해서 다른 목적에 사용될 수 있도록 해야 한다.
- 메모리를 동적으로 할당 및 해제할 때는 다음 두 함수를 사용한다.
void *malloc(size_t size);
- 인수로 필요한 메모리 양을 바이트 단위로 전달한다.
- 참고 :
_t로 끝나는 사용자 정의 타입은 표준에 의해 반드시 정의하도록 되어 있으므로 기본 타입과 거의 대등한 자격을 가진다.
- 메모리를 스택이 아닌 힙 영역에 할당한다.
- 그리고 할당된 메모리의 시작 번지를 void 포인터 로 반환하므로 원하는 타입으로 캐스팅해야 한다.
- 할당에 실패하면 에러의 표시로 NULL을 리턴한다.
- 따라서 이 함수를 호출할 때는 malloc이 리턴한 번지를 반드시 점검해야 한다.
void free(void *memblock);
void *calloc(size_t num, size_t size);
- 첫 번째 인수 num은 할당할 요소의 개수이고, size는 요소의 크기다.
- malloc이 "몇 바이트 할당해 주세요"라고 요청하는 것에 비해 calloc은 "몇 바이트짜리 몇 개 할당해 주세요"라고 요청하는 것이다.
- 구조체 같은 큰 데이터의 배열을 할당할 때는 calloc으로 할당하는 것이 더 보기에 좋고 코드를 읽기에도 좋다.
- 또한 malloc과 다른 점은 메모리를 할당 후 전부 0으로 초기화한다는 점이다.
- mallco을 이용하여 동적 할당후 memset을 활용하여 0으로 초기화할 수 있지만 calloc을 쓰는 것이 더 편리하다.
int *arScore;
int stNum=50, sum=0;
arScore=(int*)malloc(stNum*sizeof(int));
if(arScore==NULL){
printf("메모리 부족!!\n");
exit(0);
}
for (int i=0; i<stNum;i++){
arScore[i]=i;
}
for (int i=0; i<stNum;i++){
sum+=arScore[i];
}
printf("%d\n", sum);
free(arScore);
- 재할당
void *realloc(void *memblock, size_t size);
- 이미 할당된 메모리의 크기를 바꾸어 재할당한다.
- 최초 할당한 크기보다 더 큰 메모리가 필요할 때는 이 함수로 크기를 조정할 수 있다.
- 원래 크기보다 더 작게 축소 재할당하는 것도 가능하기는 하지만 보통은 확대 재할당 하는 경우가 많다.
- 첫 번째 이수로 malloc이나 calloc으로 할당한 메모리의 시작 번지를 주고, 두 번째 인수로 재할당할 크기를 전달한다.
- 만약 첫 번째 인수가 NULL일 경우, 즉 할당되어 있지 않은 경우에는 새로운 메모리를 할당하므로 realloc의 동작은 malloc과 같다.
- 두 번째 인수 size가 0일 경우에는 할당을 취소하라는 얘기이므로 free와 같아진다.
- 재할당 후에 새로 할당된 메모리 번지를 리턴하는데 이 번지는 원래 번지와 같을 수도 있고 아닐 수도 있다.
- 일반적으로 축소 재할당했을 때는 같은 번지이며, 확대 재할당했을 때는 다른 번지로 이동될 확률이 높다.
- 동적 할당이 필요한 이유는 컴파일할 시점에 필요한 메모리양을 모를 때가 있기 때문이다.
재할당이 필요한 이유는 실행 중에라도 필요한 메모리양을 가늠할 수 없을 때가 있기 때문이다.
- 재할당이 필요한 사례를 들자면, 네트워크에서 전송받은 데이터를 받기 위해서 초기에서 1M 정도 버퍼를 최초로 할당하고, 1M가 다 채우다가 부족하면 또 1M를 늘여 재할당하여 하나의 데이터를 전송받을 수 있다.
size_t _msize(void *memblock);
- malloc, calloc으로 할당한 메모리의 크기를 실행 중에 조사할 수 있다.
- 할당한 메모리가 충분한지를 조사하고 싶을 때 이 함수가 유용하게 사용된다.
- windonws에서만 쓸 수 있다.
5. 이중 포인터
- 이중 포인터란 포인터 변수를 가리키는 포인터라는 뜻 (포인터의 포인터)
- 포인터 변수도 메모리를 차지하고 있으므로 이 변수도 당연히 번지가 있다.
int **ppi;
int * 타입 변수의 번지값을 저장할 수 있는 int **ppi; 변수.
- 5중, 8중 포인터도 만들 수 있다.
int i;
int *pi;
int **ppi;
i=1234;
pi=&i;
ppi=π
printf("%d\n", **ppi);
#include <string.h>
void inputName(char **pName){
*pName=(char *)malloc(12);
strcpy(*pName, "Cabin");
}
void main(){
char *Name;
inputName(&Name);
printf("%s\n", Name);
free(Name);
}
6. main 함수의 인수
- main 함수의 원형
void(또는 int) main(int argc, char *argv[], char *env[]);
- 인수의 경우 뒤에서 부터 생략이 가능하다.
- 자주 사용되는 형태는 다음과 같다.
int main(); 또는 int main(int argc, char *argv[]);
- 리턴값
- 리턴값은 없거나 있다면 정수형이여야 한다.
main 함수의 리턴 값은 int형의 타입을 가지는 것이 좋지만 구현 방식에 따라 다른 타입을 가지는 것도 가능하다.
- main 함수가 리턴하는 값을 탈출 코드(Exit Code)라고 하는데 프로그램이 실행을 마치고 운영체제로 복귀할 때 리터하는 값이다.
- main 함수의 리턴값이 곧 프로그램의 리턴값이 된다.
- 탈출 코드는 보통 사용되지 않고 무시되는데 이 프로그램을 호출한 프로그램(보통 쉘)이 곡 필요한 경우 탈출 코드를 사용하기도 한다.
- argc
- 운영체제가 이 프로그램을 실행했을 때 전달되는 인수의 개수다.
- argc 인수는 main함수로 전달되는 인수의 개수다.
- 첫 번째 인수는 실행 파일명으로 고정되어 있으므로 argc는 항상 1보다 크다.
- argv
- 프로그램으로 전달된 실제 인수값이다.
char *argv[]은 char **argv와 같다.
- env
- 운영체제의 환경 변수를 알려준다.
- 환경 변수를 조사할 수 있는 다른 방법이 있으므로 단순하게 순회해서 사용하는 방법은 사용하지 않는다. (
getenv("환경변수명"))
7. void 이중 포인터
void ** 라는 타입은 void * 타입을 가리키는 유도 타입이다.
void * 타입은 임의의 대상체를 가리키는 타입이지만, void **는 명백하게 가리키는 타입과 그 크기가 정해져 있으므로,
void *와 달리 일반 포인터 규칙이 적용된다.
- 또한 대상체가 분명히 정해져 있으므로
*연산자로 대상체를 읽거나 변경할 수 있고 ++, --, +n 등의 연산으로 앞 뒤 요소로 이동할 수 있으며, 같은 void ** 타입끼리 대입, 비교, 뺄셈도 가능하다.
**vpp; 같은 연산은 불가능하다.
*(*vpp) -> *vp 연산을 진행해야 하는데 이 결과물은 임의의 대상체를 가리키기기 때문에 캐스팅을 해야한다.
- 다음과 같이 수정해야 사용할 수 있다.
*(int*)*vpp
출처 : 혼자 연구하는 C/C++ 1 / 김상형 저 / 와우북스