책정리/혼자 연구하는 C,C++ 1

10장 포인터

GONII 2015. 2. 19. 17:05

10.1 포인터 연산

10.1.1 T형 포인터

  • 포인터

    메모리의 한 지점, 간단히 말해 번지값을 가지는 변수

    어떠한 형태의 변수든지 반드시 메모리에 보관되며 모든 메모리는 번지를 갖고 있다.

    따라서 이 변수의 번지를 가리키는 포인터 변수를 항상 선언할 수 있다.

       

    임의의 타입 T가 있을 때 T형의 포인터 변수를 선언할 수 있다.

    int *, char*, double * 변수 선언 O

    구조체, 공용체, 배열 포인터 O

       

    • 포인터 선언 방식

      int *i, j; // i는 포인터, j는 정수형

      int* i, j; // i는 포인터, j는 정수형

      int *i, *j; // i와 j 모두 포인터

10.1.2 포인터의 타입

  • 대상체(object)

    포인터가 가리키는 번지에 들어있는 값, 즉 포인터가 가리키는 실체

       

  • int *pi;

    "정수에 대한 포인터형" pi

    포인터가 저장하는 번지값이라는 것은 4바이트 크기로 고정(32비트 운영체제에서)되어 있음

    이 변수에 저장될 값은 항상 부호없는 정수형(unsigned int)

       

  • 포인터가 타입을 가져야 하는 이유
    • *연산자로 포인터의 대상체를 읽거나 쓸 때 대상체의 바이트 수와 비트 해석 방법을 알아야 하기 때문
    • 인접한 다른 대상체로 이동할 때 이동 거리를 알기 위해서 (ex : 배열)
  • 예제 PointerType

#include <stdio.h>

   

void main(void)

{

int i=1234;

int *pi;

double d=3.14;

double *pd;

   

pi=&i;

pd=&d;

printf("정수 = %d\n",*pi);

printf("실수 = %f\n",*pd);

   

pi=(int*)&d;

printf("pi로 읽은 d번지의 값=%d\n",*pi);

}

pi나 pd나 똑같이 메모리의 한 지점을 가리키는 포인터형 변수이지만 선언할 때 대상체의 타입을 명시했기 때문에 *연산자가 이 포인터들로부터 읽는 값이 달라질 수 있다.

pi=(int*)&d;를 하면 엉뚱한 값이 출력될 수밖에 없다.

  • PointerType2

#include <stdio.h>

   

void main()

{

int ar[]={1,2,3,4,5};

int *pi;

   

pi=ar;

printf("첫 번째 요소 = %d\n", *pi);

pi++;

printf("두 번째 요소 = %d\n", *pi);

}

pi=ar 배열의 선두 번지를 가리키도록 했음.

배열명 자체배열의 시작번지를 가리키는 포인터 상수이므로 pi가 이 포인터 상수를 대입 받은 것이다.

   

T형 포인터 변수 px에 정수 i를 더하면

px=px+(i*sizeof(T))가 됨. // sizeof(int) 만큼 증가

10.1.3 포인터 연산

  1. 포인터끼리 더할 수 없다.

    포인터 변수는 주소값을 저장하기 때문에 주소값끼리 더하더라도 아무런 의미가 없다.

  2. 포인터끼리 뺄 수는 있다. (연산 결과는 정수이다.)

    두 요소간의 상대적인 거리라는 의미가 있다.

  • 예제 PointerMinus

#include <stdio.h>

   

void main()

{

char ar[]="Pointer";

char *pi1, *pi2;

   

pi1 = &ar[0];        //포인터 변수 pi1에 ar[0]의 주소 대입

pi2 = &ar[5];

   

printf("%c와 %c의 거리는 %d\n", *pi1, *pi2, pi2-pi1);

}

  1. 포인터에 정수를 더하거나 뺄 수 있다. (결과는 포인터이다)

    정수형 덧셈, 뺄셈 O

    ++, -- 연산 o

    이동거리 = sizeof(타입)만큼 이동

  2. 포인터끼리 대입할 수 있다.

    p1=p2 이면 p2가 기억하고 있는 번지를 p1에 대입

    만약 두 포인터의 타입이 다르면 캐스트 연산자를 사용하여 타입을 강제로 맞추어야 한다.

  3. 포인터와 실수와의 연산은 허용되지 않는다.

    번지를 기억하기 때문에 실수 연산은 허용 x

  4. 포인터에 곱셈이나 나눗셈을 할 수 없다.

    음수부호(-), 나머지 연산자(%), 비트 연산자, 쉬프트 연산자 등 모두 포인터와 함께 쓸 수 없다.

  5. 포인터끼리 비교는 가능하다.

    포인터의 유효성을 점검하기 위해 NULL값과 비교하는 연산은 자주 사용된다.

    if ( ptr == NULL)

    if ( ptr != NULL)

  • 예제 MidPointer

#include <stdio.h>

   

void main()

{

int ar[]={1,2,3,4,5};

int *p1, *p2, *p3, *p4;

   

p1=&ar[0];

p2=&ar[4];

p3=p1+(p2-p1)/2;

   

printf("중간의 요소 = %d\n", *p3);

}

10.1.4 *ptr++

배열의 요소들을 순서대로 출력

  • 예제 ptrplusplus

#include <stdio.h>

   

void main(vodi)

{

int ar[]={10,20,30,40,50};

int i, *ptr;

   

ptr=ar;

for(i=0; i<5; i++)

{

printf("%d\n", *ptr++);

}

}

*ptr과 ptr++을 하나로 합쳐 놓은 것이며 순서대로 실행된다.

  • 예제 ptrppsum

#include <stdio.h>

   

void main()

{

int ar[]={10,20,30,40,50,0};

int *ptr=ar;

int sum=0;

   

while(*ptr)

{

sum+=*ptr++;

printf("sum=%d\n",sum);

}

}

while(*ptr) // ptr이 가리키는 곳의 값이므로, 0이 나올때 까지 루프된다.

10.2 void형 포인터

10.2.1 void 형

void *ptr

타입을 명시하지 않는 특별한 포인터형

대상체가 정해져 있지 않다는 뜻이다.

함수와 포인터 변수에게만 적용되는 타입이므로, 일반 변수에는 쓸 수 없다.( void i; // 선언 안됨)

  • void 포인터의 특징
    • 임의의 대상체를 가리킬 수 있다.

      어떠한 대상체라도 가리킬 수 있다.(정수, 문자열, 실수 등 모두 상관없음)

  1. *연산자를 쓸 수 없다.

    임의의 대상체에 대해 번지값만을 저장하며 이 위치에 어떤 값이 들어 있는지는 알지 못함.

    따라서 *연산자로 이 포인터가 가리키는 메모리의 값을 읽을 수 없다.

  • 예제 voidPointer

#include <stdio.h>

   

void main()

{

int i=1234;

void *vp;

   

vp=&i;

//printf("%d\n", *vp);        // error : 간접 참조가 잘못되었습니다.

}

void형 포인터에 대상체의 타입을 정확히 알고 있고 그 값을 꼭 읽고 싶다면 캐스트 연산자를 사용하는 방법이 있다.

*(int *)vp

  1. 증감 연산자를 쓸 수 없다.

    대상체의 타입이 정해져 있지 않으므로 증감 연산자도 곧바로 사용 할 수 없다.

  • 예제 voidPointer2

#include <stdio.h>

   

void main()

{

int ar[]={10,20,30,40,50};

void *vp;

   

vp=ar;

//vp=vp+1;                // error : void * 알 수 없는 크기입니다.

printf("%d\n", *(int *)vp);

}

+1이 몇 바이트 뒤인지 모르므로 연산 안됨.

   

  • void형 포인터 특징 정리

    임의의 번지를 저장할 수 있다.

    *연산자로 값을 읽거나 증감 연산을 할 수 없다.

    캐스트 연산자를 통해 증감 연산을 할 수 있다.

    값을 읽거나 전후 위치로 이동하는 기능 빼고 순순하게 메모리의 한 지점을 가리키는 기능만 가지는 포인터

10.2.2 void형 포인터의 활용

함수가 임의의 타입에 대해 메모리 채우기를 하기 위해서는 임의의 대상체에 대한 포인터를 모두 전달받을 수 있어야 하며 이럴 때 사용하는 것이 바로 void *형 이다.

  • 예제 voidDump

#include <stdio.h>

   

void arDump(void *array, int length);

   

void main(void)

{

int ari[]={1,2,3,4,5};

char arc[]="Pointer";

   

arDump(ari, sizeof(ari));

arDump(arc, sizeof(arc));

   

}

   

void arDump(void *array, int length)

{

int i;

   

for ( i=0; i<length; i++)

{

printf("%02X ", *((unsigned char *)array+i));

}

printf("\n");

}

10.2.3 NULL 포인터

NULL 포인터는0으로 정의되어 있는 포인터 상수값이다.

0이라는 상수보다는 좀 더 쉽게 구분되고 의미를 명확히 표현할 수 있는 NULL이라는 명칭의 매크로 상수를 쓰는 것이 좋다.

#define NULL 0

   

대부분의 플랫폼에서 0번지는 ROM이거나 시스템 예약 영역에 해당되므로 응용프로그램이 고유의 데이터를 저장하거나 읽을 수 없도록 보호되어 있다.

그래서 이런 상황은 일종의 에러로 간주되며 그렇게 하기로 약속되어 있다.

포인터를 리턴하는 거의 대부분의 함수는 에러가 발생 했을 때 NULL값을 리턴한다.

  • 예제 NullTest

#include <stdio.h>

#include <string.h>

   

void main(void)

{

char str[]="korea";

char *p;

   

p=strchr(str,'r');

if (p !=NULL) {

*p='s';

}

puts(str);

}

"korea"라는 문자열에서 'r'을 찾아 's'로 변경하되 'r'이 발견되지 않으면 아무것도 하지 않도록 한다.

10.3 동적 메모리 할당

10.3.1 할당의 필요성

  • 정적 할당(Static Allocation)

    프로그램을 작성할 때 미리 메모리 필요량을 알려주는 할당

  • 동적 할당(Dynamic Allocation)

    실행 중에(Run Time) 필요한 만큼 메모리를 할당하는 기법

    메모리 필요량을 프로그램 작성 중에 결정할 수 없을 때는 정적할당 할 수 없으며 동적 할당을 해야 한다.

    필요할 때 필요한 만큼만 메모리를 할당해 사용하고 다 쓰면 버리는 것이다.

10.3.2 메모리 관리 원칙

메모리의 실체는 시스템에 장착되어 있는 RAM이다.

메모리 관리는 운영체제가 직접 한다.

   

  • 운영체제가 메모리를 관리하는 일반적인 원칙
    • 메모리 관리의 주체는 운영체제이다.
    • 운영체제는 메모리가 있는 한은 할당 요청을 거절하지 않는다.
    • 한 번 할당된 메모리 공간은 절대로 다른 목적을 위해 재할당 되지 않는다.
    • 응용 프로그램이 할당된 메모리를 해제하면 운영체제는 이 공간을 빈 영역으로 인식하고 다른 목적을 위해 사용할 수 있도록 한다.

10.3.3 할당 및 해제

메모리를 동적으로 할당 및 해제할 때는 다음 두 함수를 사용한다.

  • void *malloc(size_t size);

    인수로 필요한 메모리양을 바이트 단위로 전달하면 요청한만큼 할당한다.

    100바이트가 필요하면 malloc(10), 변수 사용 가능 malloc(Num)

    운영체제는 사용되지 않는 빈 영역(힙)을 찾아 요청한만큼 메모리를 할당하여 그 시작 번지를 리턴한다.

  • void free(void *memblock);

    free함수는 동적으로 할당한 메모리를 해제한다.

    메모리를 다 사용한 후에 반드시 free함수를 호출하여 메모리를 해제해야 한다.

    그래야 이 영역이 다른 프로그램을 위해 재활용될 수 있다.

       

  • int *ar ;

    ar=(int *)malloc(10*sizeof(int)) ;

    //ar 사용

    free(ar) ;

    동적으로 할당된 메모리를 사용하려면 그 시작 번지를 기억해야 하므로 포인터 변수가 필요하다.

    필요한 메모리양을 바이트 단위로 전달하는데 정수형 변수 10개를 담을 수 있는 메모리의 총 기는 10*sizeof(int), 즉 40바이트여야 한다. int가 항상 4바이트라는 보장이 없으므로 여기에도 반드시 sizeof 연산자로 크기를 계산한다.

    malloc은 인수로 전달된 크기만큼의 메모리를 할당받고 그 시작 번지를 리턴하되 리턴 타입이 void *형이므로 이 포인터를 변수에 대입할 때 반드시 원하는 타입으로 캐스팅할 필요가 있다.

       

    포인터 ar이 가리키는 번지는 마치 ar[10] 정수형 배열과 같아지며 메모리 내에서의 실제 모양과 용도, 적용되는 문법도 배열과 동일하다.

  • 예제 malloc

#include <stdio.h>

   

void main()

{

int *arScore;

int i, stNum;

int sum;

   

printf("학생수를 입력하세요 : ");

scanf("%d", &stNum);

arScore = (int *)malloc(stNum*sizeof(int));

   

if( arScore == NULL ) {

printf("메모리가 부족합니다.\n");

exit(0);

}

   

for(i=0; i<stNum; i++)

{

printf("%d번 학생의 성적을 입력하세요 : ", i+1);

scanf("%d", &arScore[i]);

}

   

sum=0;

for (i=0; i<stNum; i++)

{

sum+=arScore[i];

}

   

printf("\n총점은 %d점이고 평균은 %d점입니다.\n", sum, sum/stNum);

free(arScore);

}

malloc 함수는 할당에 실패하면 에러의 표시로 NULL을 리턴하며 그래서 이 함수를 호출 할 때는 위 예제처럼 malloc이 리턴한 번지를 반드시 점검하는 것이 원칙이다. 이 점검을 하지 않으면 0번지를 액세스 할 위험이 있다. 제대로 만든 프로그램은 어떠한 상황에서도 최소한 죽지는 말아야 한다.

10.3.4 재할당

  • calloc

    void *calloc( size_t num, size_t size );

    첫 번째 인수 num은 할당할 요소의 개수

    size는 요소의 크기

       

    ar=(int *)malloc(10*sizeof(int)); // 몇 바이트 할당해 주세요~

    ar=(int *)calloc(10*sizeof(int)); // 몇 바이트짜리 몇 개 할당 해주세요~

       

  • malloc과 calloc 차이점

    malloc과 calloc 결과는 동일하다, 다만 필요한 메모리양을 단위와 개수로 나누어 좀 더 논리적으로 표현한다는 점만 다르다.

    메모리 할당 후 전부 0으로 초기화한다는 것이 다르다.

       

  • realloc

    이미 할당된 메모리의 크기를 바꾸어 재할당( 크게 O, 작게 O )

    void *realloc( void *memblock, size_t size );

    첫 번째 인수로 malloc이나 calloc으로 할당한 메모리의 시작 번지를

    두 번째 인수로 재할당할 크기를 전달

       

  • 예제 realloc

#include <stdio.h>

#include <stdlib.h>

   

void main()

{

int *ar;

   

ar=(int *)malloc(5*sizeof(int));

ar[4]=1234;

   

ar=(int *)realloc(ar, 10*sizeof(int));

ar[9]=5678;

   

printf("ar[4]=%d, ar[9]=%d\n", ar[4], ar[9]);

free(ar);

}

10.4 이중 포인터

10.4.1 이중 포인터

  • 이중 포인터

    포인터 변수를 가리키는 포인터 ( 포인터의 포인터 )

    int **ppi ;

  • 예제 dbPointer

#include <stdio.h>

   

void main()

{

int i;

int *pi;

int **ppi;

   

i=1234;

pi=&i;

ppi=&pi;

   

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

}

   

  • 예제 FuncAlloc

#include <stdio.h>

   

void inputName(char **pName);

void main()

{

char *Name;

   

inputName(&Name);

printf("이름은 %s입니다.\n", Name);

free(Name);

}

   

void inputName(char **pName)

{

*pName=(char *)malloc(12);

strcpy(*pName, "Cabin");

}

main 에서 char*형의 변수 name을 선언하고 이 포인터 변수의 번지, 즉 char **형의 이중 포인터를 InputName 함수로 전달했으며 이 함수는 이중 포인터를 형식 인수 pName으로 대입받는다. name은 함수 내부에서 값이 결정되는 출력용 인수이기 때문에 호출원에서 초기화하지 않아도 상관없다. InputName 함수는 필요한 만큼 동적으로 메모리를 할당하여 할당된 번지를 pName이 가리키는 번지인 *pName에 대입했다. 여기서 *pName이라는 표현식은 곧 main에서 InputName으로 전달한 실인수 Name을 의미한다. 그리고 할당된 번지에 어떤 문자열을 복사했다. 결국 InputName 함수는 main의 name 포인터 변수를 참조 호출로 전달받아 name에 직접 메모리를 할당하고 이 번지에 이름까지 복사한 것이다.

10.4.2 main 함수의 인수

10.4.3 동적 문자열 배열

배열을 선언할 때는 그 크기를 반드시 상수로 지정해야 함

int len = 원하는 값 ;

char name[len] ; //< 에러

   

실행 중에 가변적인 크기의 배열을 생성하려면 동적 메모리 할당 함수인 malloc 함수를 사용해야 함

int len ;

char *name ;

scanf("%d", &len ) ;

name = (char *)malloc( len * sizeof(char) ) ;

// name 사용

free(name) ;

   

  • 예제 DynStrArray

#include <stdio.h>

#include <stdlib.h>

   

void main ( void )

{

int len = 10, num = 5, i ;

char **name ;

   

name = (char **)malloc( num * sizeof(char*) ) ;

for ( i = 0 ; i < num ; i++ )

{

name[i] = (char*)malloc( len * sizeof(char) ) ;

}

   

for ( i = 0 ; i < num ; i++ )

{

sprintf( name[i], "string %d", i ) ;

puts( name[i] ) ;

}

   

for ( i = 0 ; i < num ; i++ )

{

free( name[i] ) ;

}

free(name) ;

}

len은 문자열의 길이며 num은 이런 문자열의 개수인데 둘 다 변수이므로 실행 중에 사용자나 외부에서 주어지는 값이다. 편의상 선언문에서 10과 5로 초기화했지만 이 값은 언제든지 바뀔 수 있다.

   

char형의 배열을 할당할 때 char *형 변수가 필요한 것처럼 char*형의 배열을 동적 할당 할 때는 char**형, 즉 이중 포인터 변수가 필요한 것이다.

이 할당에 의해 name은 num개의 char*형으로 구성된 배열이 되는데 malloc은 할당만 하지 초기화는 하지 않으므로 name 배열은 쓰레기 값을 가지고 있을 것이다. 이제 0~num까지 루프를 돌며 name의 각 요소에 대해 다시 메모리를 할당하여 len 길이의 문자형 배열을 만들면 name은 2차원 문자형 배열이 되며 개념적으로 1차원 문자열 배열이라고 할 수 있다.

정적으로 선언한 변수는 name뿐이며 나머지는 모두 동적으로 할당된 것들이다.

동적으로 할당한 배열을 해제할 때는 name 배열의 각 요소가 가리키는 메모리 블록을 순서대로 해제하고 마지막으로 name이 가리키는 배열을 해제한다. 순서를 바꿔 해제하면 하위 블록의 번지를 잃어 버리므로 일부를 해제할 수 없게 될 것이다. name 그 자체는 지역적으로 선언된 포인터 변수이므로 해제할 필요가 없다.

10.4.4 void 이중 포인터

  • 예제 voidpp

#include <stdio.h>

#include <stdlib.h>

   

void main ( void )

{

void *vp ;

void *av[5] ;

void **vpp ;

int i, *pi = &i ;

   

vpp = &vp ;

vpp = av ;

vpp++ ;

*vpp ;

//vpp = &pi ; //< 안됨

//**vpp ;

}

void **로 선언된 vpp가 대입받을 수 있는 값은 void *형 변수 vp의 번지, void *배열 av의 선두 번지 등이다. 다른 타입의 포인터는 대입받을 수 없다.

   

void * 는 원래 임의의 타입을 모두 가리킬 수 있는 타입이다. 이 임의의 타입에는 포인터 타입도 당연히 포함되며 포인터 변수도 분명히 변수이므로 번지가 있고 이 번지를 void *의 변수가 가질 수 있는 것이다.

void *vp가 int *pi를 가리키고 있을 때 대상체를 읽고 싶다면 캐스트 연산자를 적절히 잘 사용해야 한다.

**(int**)vp 이렇게 되는데 vp를 int형 이중 포인터로 잠시 바꾼 후 *를 두 번 적용하면 pi가 가리키는 정수를 읽을 수 있다.

  • 예제 voidalloc

#include <stdio.h>

#include <stdlib.h>

   

void alloc( void **mem, size_t size )

{

*mem = malloc(size) ;

}

   

void main ( void )

{

void *vp ;

   

alloc( &vp, sizeof(int) ) ;

*(int *)vp = 1234 ;

printf("%d\n", *(int*)vp ) ;

   

free(vp) ;

}

메모리를 대신 할당하는 함수를 만들고 싶을 때 이 함수가 받아야 할 타입이 바로 void**이다.

반응형

'책정리 > 혼자 연구하는 C,C++ 1' 카테고리의 다른 글

12장 문자열 함수  (0) 2015.02.19
11장 배열과 포인터  (0) 2015.02.19
9장 배열  (0) 2015.02.19
8장 표준 함수  (0) 2015.02.19
7장 기억 부류  (0) 2015.02.19