C언어 포인터

Minimal_user·2024년 5월 10일

c언어

목록 보기
10/17

1. 포인터의 타입

  • 포인터 변수를 선언할 때는 가리키고자 하는 대상체(object)의 타입을 반드시 명시해야 한다.
    • 대상체의 타입을 포인터의 타입이라고 한다.
  • 포인터가 저장하는 번지값이라는 것은 4바이트 크기로 고정되어 있고, 이 변수에 저장될 값은 항상 부호없는 정수형이다.
    32비트 호나경에서 주소값은 항상 32비트이며 번지는 0을 포함한 양수값이다. 포인터 변수가 정수를 가리키든 실수를 가리키트 도는 구조체나 배열 같은 큰 데이터를 가리키든 대상체의 타입과는 상관없이 포인터는 항상 4바이트의 부호없는 정수값인 것이다.
    • 포인터 변수 크기와 형태가 이미 고정되어 있음에도 대상체의 데이터형을 기술하는 이유는
      • 첫 번째 이유는 '*'연산자로 포인터의 대상체를 읽거나 쓸 대 대상체의 바이트 수와 비트 해석 방법을 알아야 하기 때문이다.
      • 두 번째 이유는 인접한 다른 대상체로 이동할 때 이동 거리를 알기 위해서다.
        • 예를 들면 배열의 인덱스가 아니라 포인터를 이용하여 배열을 순회할 수 있다. 이때 포인터 변수의 증감 연산자를 이용하면 포인터에 대한 실체의 타입 크기 만큼 증감 된다.

2. 포인터의 연산

  • 포인터 변수의 연산은 일반적인 산술 연산과는 다른 규칙이 적용된다.
    • 포인터끼리 더할 수 없다.
    • 포인터끼리 뺄 수는 있다. 결과값은 단순한 정수값이다.(즉, 결과값이 포인터가 아니다)
      • 두 포인터 뺄셈의 결과는 메모리 상에서 두 주소값 차이(의 크기)에 포인터 변수의 타입의 크기로 나눈 값이다.
        • 일단 이 경우는 배열과 같이 동일한 타입이 연속적으로 나열 되어 있을 때 적용되는 듯.
        • 일반적으로 선언된 변수나 구조체 타입에서는 다른 룰이 적용되어 계산되는 것 같다.
    • 포인터에 정수를 더하거나 뺄 수 있다. (결과는 포인터다)
      • 정수값에 의해서 더해지거나 감소되는 실제 포인터값의 이동 거리는 '정수값*sizeof(타입)' 만큼이다.
    • 포인터끼리 대입할 수 있다. 단, 대입식의 좌변과 우변의 포인터 타입이 일치해야 한다.
      • 캐스팅 또한 포인터 구두점을 함께 기술해서 캐스팅해야함.
    • 포인터와 실수와의 연산은 허용되지 않는다.
    • 포인터에 곱셈이나 나눗셈을 할 수 없다.
    • 포인터끼리 비교는 가능하다. (물론, 좌변과 우변의 포인터 타입이 일치해야 한다)
      * if (ptr==NULL)
      • 대소 비교 연산자는 메모리상 위치를 비교하는 의미로 사용됨.
  • *ptr++
    • * 연산자와 ++연산자는 연산 우선순위가 값으며 우측 우선의 결합순서를 가진다.
    • 따라서 ++가 먼저 연산되어야하지만 이 경우 후위형으로 기술되어 있기 때문에 *가 먼저 연산된다.
    • *(ptr++)로 했다 하더라도 후위형 증감 연산자는 연산 순위와 상관없이 평가된 후에 증가하기 때문에 결과는 동일하다.
    • 참고 : (*ptr)++는 ptr 포인터 변수가 가리키는 대상체에 증감연산을 실행한다.

3. void형 포인터

  • 선언할 때 대상체의 타입을 명시하지 않는 포인터 다.
    • void *vp;
  • 특징
    • 임의의 대상체를 가리킬 수 있다.
      • 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=&pi;

printf("%d\n", **ppi);

//...
#include <string.h>

void inputName(char **pName){ 
// 예를 들어 이 함수는 DB에서 데이터를 받아와 이 함수를 호출한 곳의 변수에 저장할 때 이렇게 함수를 설계할 수 있다.
// 장점으로 이 함수를 부른 곳에서는 DB에서 불러온 값의 크기를 신경쓸 필요 없다.
	*pName=(char *)malloc(12);
    strcpy(*pName, "Cabin");
    // 참고 : 포인터 변수에 직접적으로 대입한 문자열 상수는 실행파일의 text 세그먼트에 저장되는데
    //       이 영역은 수정이 불가능하다. 따라서 strcpy 첫 번째 인수가 단순히 문자열 상수가 직접적으로 대입된
    //       char * 타입 변수라면 Segmentation fault (core dumped) 에러가 발생한다.
}

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 / 김상형 저 / 와우북스

profile
White book for everything I need.

0개의 댓글