ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C 언어 기초#8 포인터(pointer)
    Programming 기초/C Language 2023. 4. 13. 16:19

    *포인터(pointer)

    포인터(pointer)는 메모리 주소를 가지고 있는 변수이다.

    • 간접 참조(dereferencing, 역참조; indirection) 연산자 * : 만약 p가 변수 I를 가리킨다고 하면 *p는 변수 I와 같다. *p는 p가 가리키는 위치에 있는 데이터를 가져오라는 의미이다. 만약 p가 int형 포인터이면 p가 가리키는 위치에 정수가 있다고 가정하고 4바이트를 읽어 들인다. 만약 double형 포인터이면 p가 가리키는 위치에 실수가 있다고 생각하고 8바이트를 읽어 들이는 것이다. 이것이 포인터의 타입이 필요한 이유이다.
    • &(앰퍼샌드) - 주소 연산자(address-of oprerator), 변수의 주소를 구하여 포인터에 대입할 때 사용
    • *(star) - 포인터를 통하여 변수를 간접 참조할 때 사용
    • 참고자료 : 유튜브

    *포인터 연산자로 포인터를 선언해야하는 이유 == 포인터 선언시 자료형을 명시해야 하는 이유

    int q = 10;
    int *p = &q;
    *p = 50;
    printf("%d", *p);
    -----------------
    50
    포인터와 메모리의 관계 도식화


    위와 같이 int자료형을 명시해서 포인터를 선언하고, 포인터는 주소를 참조하고자 하는 변수의 첫번째 메모리 주소만 참조한다. int형은 4byte이므로, 포인터에는 4byte 중 첫 번째 위치의 메모리만 기억하는 것이다.( 위 그림에서는 q가 할당된 메모리 주소의 시작 위치인 1101을 포인터 p에 저장한다.)

    int q = 10;
    int p = NULL;
    *p = &q;  // 포인터가 앞서 선언되지 않았다며 오류가 발생한다.
    
    int q = 10;
    *p = NULL;		// 자료형을 선언하지 않고 포인터를 선언하고자 의도했지만, 문법상 틀렸다. 컴파일러는 포인터가 앞서 선언되지 않았다고 오류를 낸다.
    
    p = &q;
    *p = 50;


    만약 위와같이 자료형을 명시해주지 않는 것이 오류를 발생시키겠지만 어찌저찌 넘어갔다 치더라도 문제가 발생한다. 바로, 포인터는 참조할 주소의 첫 번째 위치주소만 가지고 있기 때문에, 주소를 참조할 때 몇 byte를 읽어야하는지 모른다. 8byte를 읽어야할지, 4byte를 읽어야할지 말이다.

    *포인터의 크기는 8byte이다.(64bit 운영체제 기준)

    포인터의 크기는 앞에 적힌 int, double 등의 자료형과는 무관하다. 포인터의 크기는 운영체제의 메모리 주소의 저장 크기로 결정된다. window 32bit, 64bit에 대해 들어봤을텐데, 여기서 bit는 메모리를 저장할 때 몇 비트를 사용하냐를 의미한다. 64bit 운영체제는 메모리를 저장할 때 64bit를 사용한다는 의미로서, 즉 8byte를 메모리 주소로서 사용한다. 포인터는 메모리 주소를 저장하는 곳이기 때문에 그 크기는 운영체제와 관계있는 것이지, 우리가 임의로 저장 크기를 지정해주는 자료형과는 전혀 무관한 것이다.

    *포인터 사용시 주의사항

    1.초기화 시 NULL 값으로 초기화 = 주소 0에 해당.
    초기화를 시켜야 하는 이유 : 초기화되지 않았다면 포인터는 임의의 주소를 가리키게 된다. 따라서 이런 상태에서 포인터를 이용하여 메모리의 내용을 변경한다면 문제가 발생할 수 있다. 예를 들어 다음과 같은 코드는 포인터 p를 초기화시키지 않고 포인터 p가 가리키는 곳에 값 100을 대입하고 있어 위험한 코드이다. 만약 우연히 p가 중요한 정보를 덮어 쓸 수도 있으며 따라서 전체 시스템을 다운시킬 수도 있다. 보통의 경우에는 운영체제가 이러한 잘못된 메모리의 접근을 차단하게 되지만 이러한 코드는 사용하지 않는 것이 좋다.
     
    - 포인터 선언시 사용되는 *는 간접참조연산자(역참조연산자)가 아닌 포인터 연산자로, 예약어(keyword)이다.
    - 선언 외의 *는 간접참조연산자이다.

    int main(void)
    {
     	int *p; 		// 포인터 p는 초기화가 안 되어 있음 p는 랜덤한 주소값을 가지고 있음
        		// 포인터 선언시 사용되는 *는 간접참조연산자가 아닌 '자료형'처럼 예약어(keyword)로 쓰임.
    	
        *p = 100;		//  랜덤하게 정해진 어떤 한 주소에 100이라는 값을 덮어씌우므로 위험함.
    			//여기서 *는 간접참조연산자이다.
        return 0;
    }

    2.포인터가 아무것도 가리키고 있지 않을 때는 NULL(0)로 설정하는 것이 바람직.
    stdio.h 헤더파일에 #define NULL ((void*)0) 로 정의되어 있음.

    #include<stdio.h>
    
    int main(void)
    {
    	int i = 3000;
    	int* p = NULL;	// 포인터 선언 및 포인터 p가 가리키는 주소를 NULL또는 0으로 초기화
    
    	p = &i;	//i의 주소를 포인터 p에 저장
    
    	printf("i=%d\n", i);	
    	printf("&i = %u\n\n", &i);	// 변수 i의 주소를 출력
    // 원래 주소 출력 형식 지정자는 %p인데, %p는 주소를 16진수로 출력한다. 
    // 여기선 보기 편하게 %u(10진 정수 형식)을 사용.
    
    	printf("*p = %d\n", *p);	// 변수 i의 데이터 값을 출력. *는 간접 참조 연산자
    	printf("p = %u\n", p);	// 변수 i의 주소를 출력
        
       	return 0;
    }
    -----------------------------------------------------------------------
    i=3000
    &i = 9435360
    
    *p = 3000
    p = 9435360

    3. 포인터 타입과 변수의 타입은 일치하여야 한다.
    int I 변수를 double *pd로 가리키면 int가 4바이트이고 double은 8바이트이므로 double을 int에 저장할 경우, 이웃 4바이트의 값이 변경된다. 따라서 특별한 경우가 아니면 포인터로 다른 타입의 데이터를 가리키게 하면 안 된다.
     
    4. 절대 주소 사용
    특수한 경우에만 사용해야 한다. 절대주소가 어떤 용도로 사용되는 공간인지 모른다. 메모리 관리는 운영체제의 고유한 권한으로 프로그래머가 관여하면 안 된다.
     

    *포인터 연산

    포인터 타입++ 연산 후 증가되는 값 (단위 : 바이트)
    char1
    short2
    int4
    float4
    double8

    사칙연산 중에 덧셈과 뺄셀만이 허용됨. 포인터 p의 값이 1000라고 하면, p는 1000번지를 가리키고 있다. 만약 p++을 하면 p의 자료형에 따라 증감이 다르다. int형 포인터라면 p는 1004가 된다. char형은 char형의 크기인 1바이트만큼 증가한다.

    포인터 타입++ 연산 후 증가되는 값 (단위 : 바이트)
    char1
    short2
    int4
    float4
    double8

     

    *간접 참조 연산자와 증감 연산자

    • *p++; 과 (*p)++; 은 다르다. *p++은 포인터 자체에 적용하여 주소의 증감을 야기하고, (*p)++은 포인터 대상에 적용하여 데이터 값의 증감을 야기한다.
    수식의미
    v = *p++p가 가리키는 데이터 값을 v에 대입한 후에 p의 주소 값을 증가한다.
    (처음 v가 쓰일 때는 원래 값이나 두 번째 v가 쓰일 때부터 증가 값이 적용됨)
    v = (*p)++p가 가리키는 데이터 값을 v에 대입한 후에 p가 가리키는 데이터 값을 증가한다.
    (처음 v가 쓰일 때는 원래 값이나 두 번째 v가 쓰일 때부터 증가 값이 적용됨)
    v = *++pp의 주소 값을 증가시킨 후에 p가 가리키는 주소 값을 v에 대입한다.
    v = ++*pp가 가리키는 데이터 값을 가져온 후에 그 데이터 값을 증가하여 v에 대입한다.

     

    *포인터의 형변환

    명시적으로 포인터의 타입 변경 가능. double형 포인터를 int형 포인터로 타입 변경 가능. 반드시 형변환 연산자를 앞에 써주어야 한다.

    	double *pd = &f;	//변수 f의 주소를 포인터 pd에 대입.
    	int *pi;
    	
    	pi = (int*)pd;		// double형 포인터를 int형 포인터로 변환

     

    *배열과 포인터

    배열의 이름이 포인터이기는 하지만 배열의 이름에다 다른 변수의 주소를 대입할 수는 없다. 배열의 이름은 포인터 상수이다. 그 값이 변경될 수는 없다.

    #include<stdio.h>
    
    int main(void)
    {
    	int a[] = { 10,20,30,40,50 };
    	
    	printf("a = %u\n", a);	// 배열의 이름을 포인터처럼 사용.
    	
    	printf("a+1 = %u\n",a+1);	// a+1은 a가 int형이므로 4 더 크고 a[1]의 주소와 같다.
    	// a가 char형이면 주소가 1 커지고, double형이면 주소 8커진다.
    	
    	printf("*a=%d\n", *a);
    	
    	printf("*(a+1) = %d\n", *(a + 1));
    	return 0;
    
    }
    -----------------------------------------------------------------------------
    a = 7928240
    a+1 = 7928244
    *a=10
    *(a+1) = 20

    * 배열에서의 포인터 사용의 장점.-> 영상처리에서 포인터 많이 사용됨.
     

    *포인터와 함수

    함수 호출시 인수 전달 방식은 두 가지가 있다.

    • 값에 의한 호출(call-by-value) : 복사본이 전달된다.
    • 참조에 의한 호출(call-by-value) : 원본이 전달된다.

    예를 들어 두 변수를 교환하는 함수를 swap()이라고 하자. swap(&a, &b); void(swap(int *px, int *py){} 로 작성.
    - 참조에 의한 호출의 전형적인 예가 scanf()함수이다.
    - 참고로, 함수가 포인터를 통하여 값을 변경할 수 없게 하려면? 매개 변수 선언 시 const 붙이면 됨. const를 앞에 붙이면 포인터가 가리키는 내용이 변경 불가능한 상수라는 뜻.

    void sub(const int *p)
    {
    	*p=0; // 오류!! const로 선언되면 매개 변수 p를 통하여 값을 변경할 수 없다.
    }

    - 포인터 인수를 통하여 결과를 반환하는 함수. C언어에서 return 문장은 하나의 값만을 반홚날 수 있다. 따라서 하나 이상의 값을 반환할 때는 포인터 인수를 사용하여 반환하는 것이 보통이다.
    - 지역 변수의 경우, 함수가 종료되면 사라지기 때문에 지역 변수의 주소를 반환하면 안 된다.

    void *add(int x, int y)
    {
    	int result;
    
    	result = x + y;
    	return &result // 오류!! 함수가 종료되면 소멸되는 지역 변수의 주소를 반환하면 안 된다.
    }
    #include<stdio.h>
    
    void sub(int b[], int n);
    
    int main(void)
    {
    	int a[3] = { 1,2,3 };
    	
    	printf("%d, %d, %d\n", a[0], a[1], a[2]) ;
    	sub(a, 3);
    	printf("%d, %d, %d\n", a[0], a[1], a[2]);
    	
    	return 0;
    
    }
    
    void sub(int b[], int n)	// 두 번째 매개변수 int n은 배열의 크기를 받는다.
    {
    	b[0] = 4;
    	b[1] = 5;
    	b[2] = 6;
    }
    ------------------------------------------------------------------------------------
    1, 2, 3
    4, 5, 6

     

    *포인터의 활용

    • 메모리 매핑 하드웨어(memory-mapped hardware)
    • 메모리처럼 접근할 수 있는 하드웨어 장치를 의미. 임베디드 시스템에서 흔히 포인터를 이용하여 메모리 매핑 하드웨어를 직접 조작.
    • 포인터를 이용하면 연결 리스트나 이진 트리 등의 향상된 자료 구조를 만들 수 있다.
    • 동적 메모리 할당
    • 참조에 의한 호출( 기본적으로 "값에 의한 호출"이다.)을 구현할 수 있다.

    댓글

Designed by Tistory.