본문 바로가기

컴퓨터공학/C, C++

C,C++> 포인터

포인터는 주소값을 가지는 변수이다. 

레지스터형을 제외하고 어떤 변수든지 메모리에 보관되며 메모리는 주소값을 가진다. 따라서 포인터 변수를 항상 선언할 수 있다. int, char, double, 구조체, 공용체, 배열에 대해서 포인터형을 만들 수 있다. 사용자가 직접 만든 것도 가능하고 포인터형 변수의 포인터를 선언할 수 있다. 

 

*기호를 구분해보자.  

1) i = 3*4;

2) printf("%d", *pi);

3) int *pi;

순서대로 곱하기 연산자,

포인터 변수가 가리키는 주소값의 내용을 읽는 포인터 연산자,

포인터 선언할 때 사용하는 구두점

 

공백은 어디에 있든 상관없다. int* pi 나 int *pi는 같다.

 

 

포인터 타입

포인터가 가리키는 주소값에 있는 값을 대상체(object)라고 한다. 

대상체 값 타입을 반드시 명시해야 한다.

대상체 타입을 포인터 타입이라고 한다. 

int *pi;

이 있을 경우 정수에 대한 포인터형 이라고 한다. 

포인터가 저장하는 주소값은 4바이트 크기로 고정되어 있다.

이 변수에 저장되는 값은 부호가 없는 정수형(unsigned int)이다. 

32비트 환경 주소값은 항상 32비트이다. 주소값은 0을 포함한 양수값이다. 

정수, 실수, 구조체, 배열 등 대상체 타입과 상관없이 포인터는 항상 4바이트의 부호없는 정수값을 가지고 있다.\

 

포인터 변수는 크기와 형태가 이미 고정되어 있는데 왜 대상체 데이터형을 꼭 밝혀야 할까?

첫 번째 이유는 *연산자로 포인터 대상체를 읽거나 쓸 때 대상체 바이트 수와 비트 해석법을 알아야 하기 때문이다.

주소값은 시작점일 뿐 얼만큼 크기의 데이터를 읽어야할 지, 어떤 해석법으로 읽어야할 지 정보가 부족하다. 

시작 주소값으로부터 값의 길이와 비트 해석법을 알기 위해 대상체 타입을 명시한다. 

예를 들어 정수형 변수를 가리키는 포인터 변수는 pi가 가리키는 주소값에서 4바이트를 일겅내고 부호가 있는 정수형이라는 걸 알고 있기에 제일 앞 쪽 비트(MSB)를 부호 비트로 해석하고 나머지 비트는 절대값으로 평가한다.

만약 실수형 변수를 가리키는 포인터 변수는 주소값으로부터 8바이트를 읽고 부호, 가수, 지수를 분리한 다음 실수값을 얻게 된다. 

즉, 메모리 위치만 가지고는 정보가 부족하여 대상체를 제대로 읽을 수 없기 때문에 대상체 타입을 적는다. 

포인터 타입과 대상체 타입이 맞지 않으면 캐스팅해서 강제로 대입할 수 잇지만 엉뚱한 값이 출력된다. 

읽어야 할 값 길이도 안 맞고 비트 패턴도 다르기 때문에 엉뚱한 값이 읽혀진다. 

 

두 번째 이유는 인접한 다른 대상체로 이동할 때 이동 거리를 알기 위해서 적는다. 이동 거리란 곧 대상체 크기 정보를 의미한다. 다른 번지를 가리키도록 변경할 수 있는데 보통 증감 연산자를 사용한다. 현재 위치에서 앞 뒤로 이동하면서 인접한 대상체로 이동한다. 

예를 들어 다음 번지를 이동하기 위해서 pi 포인터 변수에 pi++ 연산을 했는데 1000번지가 pi에 있다면 1001번지가 아니라 대상체 크기에 따라서 1004가 될 수 있다. 크기만큼 주소값을 변경해야 하는데 크기값을 알기 위해서 대상체 타입 정보가 필요하다. 

 

 

포인터 연산

포인터 변수나 포인터 상수가 피연산자 중에 하나라도 있으면 포인터 연산이라고 한다. 

독특한 타입이라서 포인터 연산은 다른 규칙을 적용한다. 

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

2. 포인터끼리 뺄 수 있다. 뺄 수 있는 이유는 상대적 거리를 구하기 위해서다.

3. 포인터에 정수를 더하거나 뺄 수 있다.

4. 포인터끼리 대입할 수 있다. 주의점은 포인터 타입이 일치해야 한다. 다르면 캐스트 연산자로 맞춰야 한다. 

5. 포인터와 실수 간 연산은 안된다.

6. 포인터에 나눗셈과 곱셉은 할 수 없다.

7. 포인터끼리 비교 가능 

 

*ptr++

*ptr이 먼저 연산해서 값을 부르고 ptr++이 실행되어 다음 위치로 이동한다. *ptr과 ptr++을 합쳐 놓은 것이다.

*++ptr은 ptr 다음 번지로 이동하고 그 번지 내용을 읽는다. ++ptr, *ptr 으로 분리할 수 있다.

(*ptr)++은 대상값이 증가하지만 같은 번지만 가리킨다. ++*ptr와 차이점은 전위 연산자, 후위 연산자 차이이다. 

  

 

void형 포인터

대상체 타입을 명시하지 않는 포인터형이 있는데 void형 포인터이다. 

함수와 포인터 변수에게만 적용되는 타입이다. 일반 변수에는 쓸 수 없다.

 

1. 임의의 대상체를 가리킬 수 있다.

pi가 정수형 포인터 변수, pd가 실수형 포인터 변수, vp가 void형 변수일 때 

다음 대입문은 적합하다. 

vp = pi;

vp = pd;

원래 정수형 포인터 pi가 가리키는 번지를 실수형 포인터 pd에 대입하고 싶으면 

pd=pi;

대입식을 곧바로 쓸 수 없다. 반드시 

pd = (double *)pi; 

로 캐스팅해야 한다. 

대입문의 좌변과 우변의 타입이 같아야 정상적인 대입이 가능하다. 

그러나 void형 포인터는 임의의 대상체를 모두 가리킬 수 있어서 대입받을 때 어떠한 캐스팅도 필요 없다.

즉, void형 포인터는 대상체 종류를 가리지 않고 메모리 위치를 기억할 수 있다.

반대로 대입할 때는 반드시 캐스팅을 해줘야 한다. 

pi = (int *)vp;

pd = (double *)vp;

 

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

void형 포인터는 주소값만 저장하며 이 위치에 어떤 값이 있는 지 알 수 없다. 

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

왜냐하면 읽어낸 위치에서 몇 바이트를 읽어야 할 지, 비트를 어떤 식으로 해석해야할 지 모르기 때문이다. 

값을 읽고 싶다면 캐스트 연산자로 임시로 대상체 타입을 지정해주고 * 연산자를 사용하면 된다. 

*(int *)vp

캐스트 연산자가 실행되어 정수형 포인터로 바꾸어 놓고 * 연산자가 이 위치에서 정수값을 읽는다. 

 

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

대상체 타입이 정해져 있지 않아서 증감 연산자도 곧바로 쓸 수 없다. 대상체 크기를 모르기 때문에 얼마나 이동해야 할 지 모르기 때문이다. 

 

예제

1) vp = vp +1 

대상체 크기를 모르므로 증가 양 결정하지 못함

2) vp++

위와 같은 이유로 에러 처리

3) vp = (int *)vp + 1

가능하다

4) (int *)vp++

결합 순서에 의해 ++이 캐스트 연산자보다 먼저 연산되어서 안된다. 

5) ((int *)vp)++

++피연산자는 좌변값이어야 하는데 캐스팅을하면 좌변값이 아니므로 증가할 수 없다. 

캐스트 연산자의 피연산자로 좌변값이 주어지면 좌변값이 아니라 저장된 값이 되기 때문에 증감연산자가 안된다. 

 

https://kldp.org/node/81858

 

void형은 대상체가 정해져 있지 않아서 임의의 주소값을 저장할 수 있지만 값을 읽거나 증감 연산자로 이동할 때 반드시 캐스트 연산자가 필요하다. 순수하게 메모리 한 지점을 가리키는 기능만 가지는 포인터라고 할 수 있다. 

 

void형 포인터 활용

포인터로 액세스해야할 대상체가 분명하게 정해져 있으면 대상체형 포인터 변수를 사용하면 된다. 

그러나 모든 상황에서 대상체를 미리 결정할 수 없다. 

예를 들어 메모리를 특정값으로 채우는 memset 함수를 보면

void *memset(void *s, int c, size_t n);

으로 되어 있는데, s 번지에서 n바이트만큼 c값으로 채운다. 주로 배열 전체를 0으로 초기화할 때 사용된다. 

첫 번째 인수가 void형 포인터로 받아서 정수형, 문자형, 실수형 배열을 구분하지 않고 다 받을 수 있다. 

캐스트 연산자를 쓸 필요 없이 배열 이름만 적으면 된다.

만약 void형 포인터가 없다면 각각 전용 타입 함수를 따로 만들어야 해서 매우 불편하다. 

 

Null 포인터

어떤 포인터 변수가 Null 값을 가지고 있다면 이 포인터는 0번지를 가리키고 있는 것이다. 0번지는 메모리 공간 맨 처음에 해당하는 첫 번째 바이트이다. 대부분 플랫폼에선 0번지는 ROM이거나 시스템 예약 영역에 해당한다. 응용 프로그램이 이 번지에 어떤 값을 저장하거나 읽을 수 없도록 보호되어 있다. 

이 영역을 일거나 쓰게 되면 프로그램은 다운된다. 

 

동적 메모리 할당

프로그램을 작성할 때 미리 메모리 필요량을 알려주는 할당을 정적 할당(Static Allocation)이라고 한다. 

동적 할당(Dynamic Allocation)은 프로그램을 작성할 때(Compile Time 또는 Design Time) 메모리 필요량을 지정하는 정적 할당과는 달리 실행 중(Run time) 필요한 만큼 메모리 할당하는 기법이다. 

 

동적 할당이 필요한 경우는 임시 메모리가 필요할 때이다. 예를 들어 텍스트 파일을 검색하려면 일단 파일을 읽어야 하는데 텍스트 파일을 읽기 위한 버퍼가 필요하다. 이런 버퍼를 미리 정적 할당할 필요없이 파일 크기만큼 동적 할당한 후 원하는 작업을 하고 해제하면 된다. 

 

임시 기억 공간인 스택은 용량이 크지 않아서 지역 변수로 선언할 수 없으며 오직 전역으로만 선언할 수 있다.

동적 할당 메모리는 이름 없는 변수라고 할 수 있다. 독점적인 메모리 영역을 차지하고 있어서 일단 값을 기억할 수 있지만 이름이 없어서 오로지 포인터로만 접근할 수 있다. 그래서 malloc 함수가 반환하는 포인터는 반드시 적절한 타입의 포인터 변수로 받아야 한다. 만약 시작 번지를 읽어버리면 할당 메모리를 쓸 수 없고 해제도 못하기 때문이다. 

 

 

메모리 관리 원칙

1) 메모리 관리 주체는 운영체제라서 응용 프로그램은 직접 메모리를 관리할 수 없다. 메모리가 필요할 경우 운영체제에 할당 요청을 해야 한다. 16비트 운영체제는 응용 프로그램이 임의 주소 공감을 액세스할 수 있지만 32비트 운영체제는 안정성을 높이기 위해 응용 프로그램이 임의 메모리를 액세스하는 걸 금지하고 있다. 

2) 운영체제는 메모리가 있는 한 할당 요청을 거절하지 않는다. 요청한 만큼 메모리가 남아 있지 않으면 에러를 반환한다. 최근 운영체제는 가상 메모리 공간을 늘려서 필요한 메모리를 만들어 준다.

3) 운영체제는 응용 프로그램이 메모리 공간을 반납하기 전까지 독점적으로 사용할 수 있도록 보장한다. 

4) 응용 프로그램이 할당 메모리를 해제하면 운영체제는 이 공간을 빈영역으로 인식하고 다른 목적을 위해 사용할 수 있다. 

 

할당 및 해제

mallock(엠얼록이라고 읽는다)은 운영체제가 사용하지 않는 빈 영역(힙)을 찾아 요청한 만큼 메모리를 할당하여 그 시작 번지를 반환한다. 응용 프로그램이 할당한 메모리르 어떤 목적에 사용하는 지 알 수 없으므로 malloc은 void * 형을 반환한다. 받는 입장에서는 원하는 타입으로 캐스팅해야 한다. 

void *mallock(size_t size);

void free(void *memblock);

size_t는 메모리 양을 나타내는 단위이다. 플랫폼에 따라 다르게 정의되어 있는데 대부분 32비트 컴파일러들은 size_t를 부호없는 정수형인 unsigned로 정의한다. 이 함수로 할당할 수 있는 최대 용량은 4기가 바이트이다. 

10바이트가 필요하면 malloc(10)이라고 호출하고 100바이트는 malloc(100)이라고 하면 된다. 

int stNum;

scanf("%d", &stNum);

int arScore[stNum];

 

위 예시는 작동하지 않는다. 배열 선언문 크기값은 변수로 지정할 수 없고 반드시 상수로만 지정할 수 있다. 

대신 다음과 같이 써야 한다. 

int *arScore;

int stNum;

scanf("%d", &stNum);

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

free(arScore)

 

시작 주소값을 알고 있으니 마음대로 이용할 수 있으며 마지막에는 해제하면 된다. 

 

재할당

calloc(씨얼록이라고 읽는다) malloc은 필요 메모리를 바이트 단위로 하나의 인수로 전달받지만 calloc은 두 개의 인수로 받는다. malloc은 몇 바이트 할당해 주세요  라고 하는 반면 calloc은 몇 바이트짜리 몇 개 할당해 주세요 라고 요청하는 것이다. 

또 다른 차이점은 메모리 할당 후 전부 0으로 초기화한다는 점이다. malloc은 메모리 할당만 하는 거라서 쓰레기값이 들어 있다. 

다음 함수는 이미 할당된 메모리 크기를 바꾸는 것이다. 첫 번째 인수는 malloc이나 calloc으로 할당한 메모리 시작 번지를 준다. 두 번째 인수는 재할당할 크기를 전달한다.

만약 첫 번째 인수가 NULL이면 새로 메모리를 할당하므로 realloc은 malloc과 같다.

두 번째 인수가 0이면 할당 취소를 하는 것이므로 free 함수와 같다. 

재할당 후 새로 할당된 메모리 주소를 반환하는데 이 주소값은 원래 주소값과 같을 수도 있고 다를 수도 있다.

크기를 확대했을 경우 뒤에 비어있으면 그냥 늘리기만 하면 되지만 다른 변수가 있다면 이사가야 하기 때문이다. 

 

C++에서는 new와 delete 할당 연산자가 있는데 이 연산자들은 메모리 할당 뿐만 아니라 객체 생성자와 파괴자를 호출하기도 한다. 

 

이중 포인터

포인터 변수를 가리키는 포인터, 포인터의 포인터이다. 

예를 들어서 정수형 포인터는 int *pi; 과 같이 선언된다. 

int *라는 표현이 정수형 포인터라는 뜻으로 그 자체가 하나의 타입이 된다. 

따라서 정수형 포인터 값을 가리키는 포인터는 (int *) *ppi; 과 같이 선언할 수 있다.

괄호를 제거하면 int** ppi; 이중포인터 선언문이 된다. 

여기서 사용한 괄호는 설명을 위한 것이지 실제로 사용하면 컴파일 에러가 된다. 

 

* 연산자와 & 연산자는 서로 반대되는 동작을 한다. 

두 연산자에 의해 가리키고 끄집어 내오면 동등한 수식이 생길 수 있다.

&& 연산자를 두 번 쓰는 건 적합하지 않다.

왜냐하면 &연산자의 피연산자는 메모리상 실제 번지를 점유하고 있는 좌변값이 나와야 한다. 

하지만 &를 한 번 쓰면 저장된 주소값을 나타내는 포인터 상수이고 좌변값이 아니기 때문이다. 

 

예시)

void InputName(char **pName){

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

strcpy(*pName, "Cabin");

}

void main(){

char *Name;

InputName(&Name);

printf("%s", Name);

free(Name);

}

 

인수 X값을 함수 내부에서 변경하려면 X의 포인터를 넘기는 참조 호출을 해야 한다. 

char * 형 인수 Name을 변경하려면 char *의 포인터인 char**를 넘겨야 한다. 

따라서 InputName에서는 *pName으로 실인수를 참조한다. 

 

 

main 함수의 인수 

소스 코드에서 함수를 작성하고 사용하는 것처럼

함수 원형을 정의해 놓으면 운영체제가 이 프로그램을 사용할 때 main 함수 원형을 토대로 사용한다. 

 

프로그램 시작점으로 이름은 고정되어 있지만 함수 원형은 고정이 아니다. 

반환값은 int 또는 void 중 하나이다. 

세개의 인수를 가지는데 인수는 뒤쪽부터 차례대로 생략이 가능하다. 

그래서 조합을 여러가지 가질 수 있다. 

그 중에서 완벽한 원형은 다음과 같다. 

(void 또는 int) main(int argc, char *argv[], char *env[]);

반환값과 각 인수의 특징을 알아보자 

 

반환값 

리턴값은 없거나 정수형이다.

다른 타입을 가질 수 있지만 호환성을 위하여 정수를 쓴다. 

main 함수가 반환하는 값을 탈출 코드라고 한다. 

프로그램 실행을 마치고 운영체제로 복귀할 때 반환하는 값이다. 

보통 사용하지 않고 무시하지만 이 프로그램을 호출한 프로그램(쉘)이 필요할 경우 탈출 코드를 사용한다. 

예를 들어 도스 배치파일(*.bat) 내에서 응용 프로그램을 실행할 때 이 프로그램 실행 결과를 ERRORLEVEL이라는 환경 변수로 참조할 수 있다. 

32비트 환경에서는 탈출 코드 외에도 응용 프로그램 간 통신을 위한 장치가 많이 있어서 요즘은 main 반환값을 사용하지 않는다. 

 

argc

운영체제가 프로그램을 실행했을 때 전달되는 인수의 갯수이다. 

예를 들어 파일 복사 프로그램이라면 원본 파일과 복사 대상 파일 이름을 

boksa file1.txt file2.txt 과 같이 인수로 전달받을 것이다. 

실행 파일 이름 뒤가 프로그램 인수이다. 

main 함수가 프로그램 시작점이니까  운영체제가 main 함수로 전달할 것이다. 

여기사 argc는 mai 함수로 전달된 인수의 갯수이다. 

첫 번째 인수는 실행 파일명으로 고정이다. 따라서 argc는 항상 1보다 크다. 

여기서는 argc는 3이다. 

이 값이 왜 있을까?

인수를 필요로 하는 프로그램에서 인수가 제대로 입력됐는지 검사하기 위해서 필요하다. 

인수가 없으면 실행할 수 없는 경우 이 값을 조사해서 인수가 제대로 전달되었는지 확인한다. 

인수가 없거나 남는 경우 에러를 출력하기 위해서 필요하다. 

 

argv 

프로그램으로 전달된 실제 인수값이다. 

이 값을 읽으면 운영체제로부터 어떤 인수가 전달되었는지 알 수 있다. 

운영체제가 프로그램을 실행할 때 항상 문자열 형태의 쉘 명령을 입력하기 때문에 인수 타입은 항상 문자열일 수 밖에 없다. 명령행에서는 정수나 실수가 없으므로 입력된 값 그대로 문자열 인수가 전달된다. 

만약 전달된 인수가 정수라면 문자열로 받아서 변환 함수로 정수로 바꿔 사용해야 한다. 

따라서 argv는 문자형 포인터를 가리키는 포인터이다. 

argv는 문자형 이중 포인터이다.  문자열 배열이다. 

원형은 char *argv[]인데 char **argv와 같다. 

argv[0]은 프로그램 이름이 전달되는데 통상 완전 경로라고 본다. 

쉘이 이 프로그램을 실행할 때 입력한 실행 파일명이다. 

c:\prg 디렉토리에 있는 boksa.exe를 실행하면 argv[0]은 "c:\prg\boksa.exe"이다.

프로그램에 전달되는 첫 인수는 argv[1]이다. 두 번째 인수부터는 argv[2], argv[3] 이런 식으로 전달된다. 

 

env 

운영체제 환경 변수를 알려준다. 프로그램에게 자신이 실행되는 환경을 알 수 있도록 해준다는 의도로 전달되는 인수이다. 그러나 환경 변수를 조사할 수 있는 다른 방법이 있기 때문에 실질적으로 잘 사용되지 않는다. 

 

 

동적 문자열 배열 

배열 선언 시 그 크기는 반드시 상수로 지정해야 한다. 따라서 다음과 같이 선언할 수 없다. 

int len = 원하는 값;

char name[len];

여기서 len은 함수 인수로 전달되었거나 또는 사용자로부터 입력된 값, 즉 실행 중에 결정되는 값이라고 설정하자. 

컴파일러는 배열 선언 시점에서 크기를 알아야 한다. 

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

int len;

scanf("%d", &len);

char *name;

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

이제문자형 배열 여러 개가 필요하며 그 개수도 실행 중에만 알 수 있다면 어떻게 할까?

즉, 가변 길이(len) 문자형 배열을 가변 개수(num)만큼 생성해야 한다. 

 

void main()
{
	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);
}

name배열은  char *형을 num개 크기를 가지는 배열이 된다.  

초기화는 하지 않아서 쓰레기 값을 가지지만 0~num까지 루프를 돌려 메모리 할당한다. 

len크기의 문자형 배열을 만들면 name은 2차원 문자형 배열이 된다. 

개념상 name은 1차원 문자열 배열이라고 할 수 있다. 

name이 가리키는 곳은 크기 char * 배열이고, 이 배열 요소가 가리키는 곳에는 len 길이 char 배열이 있다. 

name을 통해 문자열 시작 번지를 가지는 배열을 찾을 수 있다. 

이 배열로 각 문자열에 접근한다. 

 

 

void 이중 포인터

 

void ** 타입은 void * 타입을 가리키는 타입이므로 void ** vpp; 변수를 선언할 때 vpp 대상체는 임의 대상체를 가리키는 void * 타입이다. void * 와는 다르게 가리키는 대상체 타입이 void *로 정해져 있고 대상체 크기도 명확하다. 따라서 vpp는 void 형 포인터 적용 규칙 대신 일반 포인터 규칙이 적용된다. 임의 타입 포인터를 대입받을 수 없으며 반드시 void * 형 변수 주소값만 대입받을 수 있다. 

void main()
{
	void *vp;
    void *av[5];
    void **vpp;
    int i, *pi = &i;
    
    vpp = &vp;	// 가능
    vpp = av;	// 가능
    vpp++;		// 가능
    *vpp;		// 가능
    vpp = &pi;	// 불가능
    **vpp;		// 불가능
}

av 배열 각 요소는 void * 타입이므로 임의 타입 변수 주소값을 가진다.

따라서 av 배열 요소가 가리키는 값을 읽으려면 캐스트 연산자가 필요하다. 

그리고 ++, -- 연산도 직접 적용할 수 없다. 

반면 vpp가 가리키는 대상체는 void * 타입으로 정해져 있다.

vpp 자체를 증가하여 av 다음 요소로 이동할 수 있다.

*vpp로 가리키는 값은 주소값이다. 

 

void * 는 임의의 타입 모두 가리킬 수 있는 타입이다. 여기에는 포인터 타입도 포함된다. 포인터 변수도 변수이기 때문에 주소값을 갖고 void * 변수가 가질 수 있는 것이다. 

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

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

 

**vpp로 vpp가 가리키는 포인터가 가리키는 대상체를 바로 읽을 수 없다. 

vpp에 *를 한 번 적용하여 void *를 읽는 건 가능하지만 이렇게 읽은 void * 대상체 타입을 알 수 없기 때문이다.

읽고 싶다면 *(int *)*vpp로 일단 void *를 읽은 후 그 결과 포인터를 원하는 타입으로 캐스팅하여 다시 *연산자를 적용한다. 

 

 

void alloc(void **mem,size_t size)
{
	*mem  = malloc(size);
    
}

void main()
{
	void *vp;
    
    alloc(&vp, sizeof(int));
    *(int *)vp = 1234;
    printf("%d\n",*(int *)vp);
    free(vp);
}

동적 할당된 메모리 주소값 저장을 위해 void *형의 vp를 선언한다.

이 변수를 할당 함수에게 참조 호출하기 위해서는 &vp를 넘겨야 한다.

그러므로 alloc이 받는 타입은 void **일 수 밖에 없다. 

 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 구조체  (0) 2022.07.21
C,C++> 배열과 포인터  (0) 2022.07.19
C,C++> 배열  (0) 2022.06.09
C,C++> 기억 부류  (0) 2022.06.08
C,C++> 함수  (0) 2022.06.06