본문 바로가기

삼성전자 알고리즘/기타

9. 포인터 [Pointer]

포인터는 프로그래밍 언어에서 다른 변수, 혹은 그 변수의 메모리 공간주소를 가리키는 변수를 말한다.

출처 : 위키피디아

 

포인터를 사용하는 이유 : call by reference 와 call by address 로 접근하기 위해

1. call by reference : 참조자 &를 붙여줌 (참조자 : 변수의 별칭을 하나 붙여주는 것)

#include <iostream>
using namespace std;
int main(){
  int a = 5;
  int &b = a;
  
  printf("a = %d, b = %d\n", a,b); // a = 5, b = 5
  
  b = b + 1;

  printf("a = %d, b = %d\n", a,b); // a = 6, b = 6

  return 0;
}

2. call by address : *를 사용해서 주소값에 접근

#include <iostream>
using namespace std;
int main(){
    int a = 5;
    int *b = &a;
    
    printf("a = %d, *b = %d\n", a,*b);      //a = 5, *b = 5
    printf("a = %d, b = %p\n", a,b);        //a = 5, b = 0x7ffeefbff678
    printf("&a = %p, &*b = %p\n", &a,&*b);  //&a = 0x7ffeefbff678, &*b = 0x7ffeefbff678
    printf("&a = %p, &b = %p\n", &a,&b);    //&a = 0x7ffeefbff678, &b = 0x7ffeefbff670

    *b = *b+1;

    printf("a = %d, *b = %d\n", a,*b);      //a = 6, *b = 6
    printf("a = %d, b = %p\n", a,b);        //a = 6, b = 0x7ffeefbff678
    printf("&a = %p, &*b = %p\n", &a,&*b);  //&a = 0x7ffeefbff678, &*b = 0x7ffeefbff678
    printf("&a = %p, &b = %p\n", &a,&b);    //&a = 0x7ffeefbff678, &b = 0x7ffeefbff670

    return 0;
}

형식 매개변수를 통해 실질 매개변수를 변형 할 수 있기 때문에 포인터를 사용.

 

3. 포인터를 사용하면 구조체를 파라미터로 받을 수 있다.

출처 : https://blog.naver.com/whdgml1996/221038067627

 

4. 포인터를 사용하면 함수를 파라미터로 받을 수 있다. 

출처 : https://blog.naver.com/whdgml1996/221083544645

 

5. C에서는 call_by_reference를 지원하지 않는다. 기본적으로 값에 의한 호출(call_by_value)만 지원.

다만 pointer를 사용해서 참조에 의한 호출(call_by_reference)를 흉내 내는것. 

실제로는 주소값(value)를 주고 받기 때문에 call_by_value가 맞는 표현임.

결국 주소도 값이기 때문에 주소를 사용하여 호출하는 것도 값에 의한 호출임.

(C++에서는 call_by_reference를 지원함.)

 

6. C에서는 에러 / C++에서는 가능한 문법

void init_Cplus_int(int& i) {
	i = 0;
}

7. C에서 제대로 해주려면 
포인터 변수 선언 방법 : 타입* 변수명;

//포인터 변수 선언 방법 : 타입* 변수명;
void init_point_reference_int(int *l) {
	//포인터를 사용하여 메모리에 접근하려면 간접 참조(=역참조 연산자) '*' 를 사용해야 한다.
	*l = 0;
}

8. 포인터와 상수

const 키워드를 사용하면 값의 변경을 할 수 없는데 이를 '논리적 상수성'을 갖는다 라고 한다.
const의 위치에 따라 달라지는 것들

	int j = 0;
	const int*p1 = &j;	//상수 지시 포인터 : pointer to constant int
						//포인터를 사용할 떄, 대상체의 값을 변경하지 않도록 하기 위해 사용
// *p1 = 0;	            // 대상체 : ERROR (const 때문에 에러)
	p1 = 0;		        // 포인터 : OK

    int k = 0;
	int* const p2 = &k;	//상수 포인터 : constant pointer to int
	*p2 = 0;	        // 대상체 : OK
//	p2 = 0;		        //포인터 : ERROR

	int l = 0;
	int const *p3 = &l;	//상수 지시 포인터 : pointer to constant int
	//C++에서 templete 사용시 이렇게 사용하는 사람들도 있다.
	//C에서는 const int*p1으로 사용.
//	*p3 = 0;	        // 대상체 : ERROR
	p3 = 0;		        //포인터 : OK
    
	int m = 0;
	int const* const p4 = &m;	//상수 지시 상수 포인터 : constant pointer to constant int
//	*p4 = 0;		    // 대상체 : ERROR
//	p4 = 0;			    // 포인터 : ERROR    

별표 기호를 중심으로 const 키워드가
쪽에 있으면 상체가 수화
른쪽에 있으면 인터가 수화

 

9. 문자열과 포인터
(문자열을 포인터에 저장할 때는 상수 지시 포인터에 저장하자! const !! 칼퇴 가능.)

C언어에서 문자열을 저장하는 방법은 다음의 2가지가 있다.

배열, 포인터

	char hello1[] = "hello";	  //문자열 변수 
//	char* hello2 = "hello";		  //문자열 상수 -> 라고 얘기하면 틀린거다. 수정 못하는게 아니라 주소로 접근해야 함!
    const char* hello2 = "hello"; //이렇게 써줘야 에러가 나도 컴파일 에러가 나기 때문에 이걸로 쓰길 권장.

"hello" 는 리터럴 상수라 변경이 일어나지 않아야한다.
(수는 변하지 않는 변수를 의미하며(메모리 위치) 메모리 값을 변경할 수 없다.

리터럴은 변수의 값이 변하지 않는 데이터(메모리 위치안의 값)를 의미한다. 

출처: https://mommoo.tistory.com/14 [개발자로 홀로 서기])

	hello1[0] = ' '; //가능
//	hello2[0] = ' '; //불가능

10. 다중 포인터

일중 포인터를 사용하는 이유 : 일반적으로 다른 지역의 일반 변수를 참조하기 위함.
이중 포인터를 사용하는 이유 : 일반적으로 다른 지역의 일중 포인터를 참조하기 위함.
삼중 포인터를 사용하는 이유 : 일반적으로 다른 지역의 이중 포인터를 참조하기 위함.

 

11. 배열과 포인터

배열 : 동일한 원소가 연속되어 나열되어 있는 것
배열 선언 방법 : 타입 배열명[길이];

	//원소는 정수이고 길이가 3인 배열을 선언
	int arr[3];
	int brr[3];

	init_arr(arr);	//이것도 돌아감. 배열은 시작주소를 넘기기 때문
	init_arr(&brr);	//이코드는 잘못된 코드. CPP에서는 에러가 난다. C언어에서는 되긴한다. 이유 : 함수호출시 stack에 복사되는 배열을 다 넘기는게 아니라 첫번째 원소의 시작 주소만 넘기려고 허용. (성능 이슈)
	//배열명 : 자신의 첫 번째 시작 주소를 의미하는 상수
	int arr[3];
	printf("arr = %p\n", arr);	//C에서는 배열의 시작주소를 넘김

	int* p = arr;
	printf("&arr = %p\n", &arr);

	p = &arr;	//C에서는 되지만 CPP에서는 안된다. ?
	//&arr 와 arr은 타입이 다르다. 절대 같은 것이 아님.

배열 타입 : 배열 전체 타입을 의미하고 타입[길이]가 배열의 타입
arr : 배열의 시작 주소, 타입은 첫 번째 원소
&arr : 배열의 시작 주소, 타입은 배열 타입

int[3]* pArr2 = &arr; //안되는 이유 : 배열 선언 방법과 다르기 때문: 타입[길이] 변수명(X) <-> 타입 배열명[길이](O)

int* pArr2[3] = &arr; //상수에 상수가 들어가는 꼴이라 에러. (부과설명 : 괄호를 안넣으면 컴파일러가 뒤에서부터 해석하는데 []부터 인식. C언에서는 []는 배열 밖에 없다. //배열명 : 자신의 첫 번째 시작 주소를 의미하는 상수.)

int(*pArr2)[3] = &arr; //배열 포인터 : pointer to array (포인터는 변수기 때문에 들어가짐)

 

배열 타입이 첫 번째 타입의 원소로 퇴화되는 것을 Decay라고 한다.
//단, Decay가 발생되지 않는 예외가 한 가지 있는데 바로 sizeof 연산자를 사용할 때다.

printf("sizeof(arr) = %d\n", sizeof(arr));

유일한 예외 : sizeof 함수에서는 Decay가 발생하지 않는다.

 

함수 파라미터에서 배열과 포인터 :

1차원 배열 :

void print_arr1(int* arr, int len){
	printf("print_arr1\n");
	for (int i = 0; i < len; i++) {
		printf("arr[%d] = %d\n", i, arr[i]);
	}
}

2차원 배열/이중 포인터 : int(*arr)[3] 보다는 int arr[][3] 형태로 쓰는것을 권장.

void print_arr2(int arr[][3], int row){		//[3]앞에는 0만 아니면 어떤 수가 들어와도 상관없다.
	printf("print_arr2\n");
	for (int i = 0; i < row; i++) {
		for (int j = 0; j < 3; j++) {
			printf("%2d ", arr[i][j]);
		}
		printf("\n");
	}
}
void print_arr2(int(*arr)[3], int row) {
	for (int i = 0; i < row; i++) {
		for (int j = 0; j < 3; j++) {
			printf("%2d ", arr[i][j]);
		}
		printf("\n");
	}
}

3차원 배열/삼중 포인터 :

void print_arr3(int arr[][2][3], int n) {		//[3]앞에는 0만 아니면 어떤 수가 들어와도 상관없다.
	printf("print_arr3\n");
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < 2; j++) {
			for (int k = 0; k < 3; k++) {
				printf("%2d ", arr[i][j][k]);
			}
			printf("\n");
		}
	}
}
void print_arr3(int (*arr)[2][3], int n) {		//[3]앞에는 0만 아니면 어떤 수가 들어와도 상관없다.
	printf("print_arr3\n");
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < 2; j++) {
			for (int k = 0; k < 3; k++) {
				printf("%2d ", arr[i][j][k]);
			}
			printf("\n");
		}
	}
}
int main() {
	int a1[6] = { 1,2,3,4,5,6 };
	print_arr1(a1, 6);
	
	int a2[2][3] = { {1,2,3},{4,5,6} };	//물리적으로는 2개의 메모리 영역	//int[3] ar[2];
	print_arr2(a2, 2);

	//연습 문제
	int a3[2][2][3] = { {{1,2,3},{4,5,6}},{{1,2,3},{4,5,6}} };
	print_arr3(a3, 2);
}

12, 포인터 타입

#include <stdio.h>
typedef struct {
    char name[32];
    int age;
} Person;

int main() {
    //현재 시스템이 32비트 시스템일 경우, 모든 포인터의 크기는 4바이트.
    char *pC;
    int *pI;
    float *pF;
    double *pD;
    Person* pP;
    Person P;
    
    printf("sizeof pC = %d\n", sizeof pC); // 4
    printf("sizeof pI = %d\n", sizeof pI); // 4
    printf("sizeof pF = %d\n", sizeof pF); // 4
    printf("sizeof pD = %d\n", sizeof pD); // 4
    printf("sizeof pP = %d\n", sizeof pP); // 4
    printf("sizeof pP = %d\n", sizeof P);  // 36

}

구조체의 크기는 가장 큰 타입의 배수. (4*9) 만약 name[33] 이었다면 P의 사이즈는 40..

따라서 스택에 올리는 것보다 힙에 올리고 포인터로 관리하면 스택에서 메모리 절약할 수 있음.

 

의도하거나 특별한 경우가 아니라면 포인터 변수는 반드시 대상체의 타입과 일치해서 선언해야 한다

포인터 타입의 의미 : 시작 주소에서 읽어들여야 하는 옵셋(offset) 정보
.

int main() {
	int i = 300;
	char *pC = &i;
	int* pI = &i;
	printf("%d\n", *pC);	//44가 출력됨 Dec 변환값
	printf("%d\n", *pI);	//300이 출력됨.
	//포인터 타입을 선언해주는 이유는 주소값 + 얼마만큼의 주소를 읽어야 하는지 offset 정보까지 담기 때문
}

13. 포인터와 산술 연산

#include <stdio.h>
int main() {
	int* p = 0;
	int* q = 0;

//	p + q;		// 포인터 + 포인터 = 에러 (니주소 내주소 더해서 모르는 주소 나온다)
	(int)p + (int)q;	//는 됨.
	//예전에는 됐지만 지금은 포인터 연산의 위험성 때문에 컴파일에서 막음.
	//주소값 끼리 더해진 값에 역참조 값에 뭐가 있을지 모름.

	p - q;		//포인터 - 포인터 = 정수(둘 사이의 거리)
//	p*q;		//포인터 * 포인터 = 에러
//	p / q;		//포인터 / 포인터 = 에러
	p + 1;		//포인터 + 정수 = 포인터
	p - 1;		//포인터 - 정수 = 포인터
	//p * 1;		//포인터 * 정수 = 에러
	//p / 1;		//포인터 / 정수 = 에러
}

결론! 포인터는 제한적인 산술 연산이 가능하다.

  a. 포인터 - 포인터 = 정수(둘 사이의 거리)
  b. 포인터 + 정수 = 포인터
  c. 포인터 - 정수 = 포인터

#include <stdio.h>
typedef struct {
    int x, y;
} Point;
int main() {
    char *pC = 0;
    short *pS = 0;
    int *pI = 0;
    float *pF = 0;
    double *pD = 0;
    Point* pP = 0;
    
    printf("sizeof pC = %d\n",  pC + 1); //1
    printf("sizeof pS = %d\n",  pS + 1); //2
    printf("sizeof pI = %d\n",  pI + 1); //4
    printf("sizeof pF = %d\n",  pF + 1); //4
    printf("sizeof pD = %d\n",  pD + 1); //8
    printf("sizeof pP = %d\n",  pP + 1); //8
}

포인터에 + 연산을 시도해도 pC +1 의 값이 2가 아니라 1로 나오는것은 포인터 연산이 됐기 때문.

int main() {
	int arr[6] = { 1,2,3,4,5,6 };

	int* p = arr;
	for (int i = 0; i < 6; i++) {
		//이 방식은 잘 사용하지 않는다.
		//printf("p[%d] = %d\n", i, p[i]); //를 많이 씀.
		printf("*(p + %d ) = %d\n", i, *(p+i));
	}
}

*(p+i) 보다는 p[i]를 선호

arr[i] == *(arr+i);
arr[i][j] == *(*(arr+i)+j);

	printf("arr[1][2] = %d\n", arr[1][2]);
//	printf("arr[1][2] = %d\n", *(arr+1)[2]);		            //우선순위상 *역참조보다 []가 우선순위임. 따라서 원하는 값이 안나옴.
	printf("(*(arr + 1))[2] = %d\n", (*(arr + 1))[2]);	        //우선순위를 맞춰줌.
	printf("*((*(arr + 1)) +2) = %d\n", *((*(arr + 1)) +2));	//우선순위를 맞춰줌.
	printf("*(*(arr + 1) + 2) = %d\n", *(*(arr + 1) + 2));	    //우선순위를 맞춰줌.

14. 길이가 없는 배열

길이가 없는 배열은 기본적으로 에러임. int arr[0]; //ERROR

#include <stdio.h>
typedef struct {
	int arr[0];
} Test;

int main() {
	Test t;	      //C99에서 기능이 추가됨.
}

이런 형태는 사용 가능.

 

활용방안 : 사용자의 이름과 나이를 저장하는 코드 예)

#include <stdio.h>
#include <stdlib.h> //malloc()
typedef struct {
	int age;
	char name[32];
} Person;
int main() {
	//사용자 정의 타입은 가급적 힙에 생성하는것이 좋다.
	Person* p = malloc(sizeof(Person));
	if (p == NULL) {		//malloc return 값으로 할당 됐는지 안됐는지 확인해야 한다. (항상 성공하는게 아니기 때문)	}

    printf("input name: ");
	scanf("%s", p->name);
//	scanf("%s", &p->name);	//이것도 됨.

	printf("input age: ");
	scanf("%d", &p->age);

	printf("%s(%d)\n", p->name, p->age);

	//할당된 자원은 반드시 사용 후, 파괴해야 한다.
	free(p);
}

이전의 코드는 메모리의 낭비가 발생함. name[32]에 저장받고 남은 공간은 다 낭비.

이를 해결 하기 위해 정적 배열이 아닌 포인터를 사용하여 동적 할당을 수행한다.

#include <stdio.h>
#include <stdlib.h> //malloc()
#include <string.h>	//strcpy()
typedef struct {
	int age;
	char* name;		// char name[32];
} Person;

int main() {
	//사용자 정의 타입은 가급적 힙에 생성하는것이 좋다.
	Person* p = malloc(sizeof(Person));
	if (p == NULL) {	//malloc return 값으로 할당 됐는지 안됐는지 확인해야 한다. (항상 성공하는게 아니기 때문	}

	char name[32];
	printf("input name: ");
	scanf("%s", name);

	//malloc의 성능상의 오버헤드가 심하다, 자원의 해지를 제대로 안할 확률이 생김(메모리 누수)
	p->name = malloc(sizeof(char) * (strlen(name) + 1));
												//  ^---- ASCII NULL
	strcpy(p->name, name);

	printf("input age: ");
	scanf("%d", &p->age);

	printf("%s(%d)\n", p->name, p->age);

	//자원의 파괴는 생성의 역순으로 진행해야 한다.
	free(p->name);
	free(p);
}

이전 코드는 동적 할당의 오버헤드와 메모리 누수의 가능성이 있다. (동적할당이 2번 일어남.)

이를 해결 하기 위해 길이가 1인 배열을 사용한다.

#include <stdio.h>
#include <stdlib.h> //malloc()
#include <string.h>	//strcpy()
typedef struct {
	int age;
	char name[1];
//	char name[0];	//표준코드는 아니지만 허용 C99 구조체 안에서는 길이가 0인 배열 허용.
//  char name[];	//이건 표준	flexible array member in C99 //주의! 이 멤버는 반드시 구조체의 마지막에 위치해야 한다. (컴파일시 전체 크기를 알아야 하기 때문->그래야 malloc에서 sizeof에서 에러가 안남)
//  char name[];    << 이거는 sizeof 에서 size에 안잡힘.
} Person;	//^--ASCII NULL

int main() {
	char name[32];
	printf("input name: ");
	scanf("%s", name);

	int age;
	printf("input age: ");
	scanf("%d", &age);

	Person* p = malloc(sizeof(Person) + (sizeof(char)*strlen(name)));
//	Person* p = malloc(sizeof(Person) + (sizeof(char)*(strlen(name)+1)));	//표준코드는 아니지만 허용
	strcpy(p->name, name);
	p->age = age;


	printf("%s(%d)\n", p->name, p->age);
	free(p);
}