혼자 연구하는 C/C++ 10장 정리(포인터)

2015. 3. 2. 02:09창고

728x90
728x90

1.포인터 연산

T형 포인터

포인터 - 메모리의 한 지점, 간단히 말해 번지값을 가지는 변수. (register형 제외) 변수는 반드시 메모리에 보관.


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


*기호

i = 3*4 - 곱하기 연산.

printf("%d",*pi); - 포인터 변수가 가리키는 번지의 내용을 읽어옴.

int *pi; - int와 pi를 곱하란 뜻 X. 포인터를 선언할 때 사용하는 구두점.


int* pi 와 int* pi는 같음. 구두점 위치는 관계없음. 

int*pi, int * pi도 가능.


단, 이런것들은 좀 문제.

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

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

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


C 스펙문서 - int *pi 형식, C++ 스펙문저 - int* pi, int *pi 혼재, MFC - int* pi

개인의 취향. 일관되게만 코딩하면 문제 없음!


문법은 포인터형으로 선언하고 싶은 변수명 앞에 일일이 * 구두점을 붙이도록 규정하고 있다.


포인터의 타입

정수형 포인터 int *pi;

좀 더 정확한 타입은 "정수에 대한 포인터형"이라고 표현.

int*라는 타입이 정수형 변수를 가리키는 포인터형이라는 뜻.

32비트 환경에서 대상체에 관계 없이 주소값은 32비트(4바이트) 번지는 0을 포함한 양수.


@중요!

포인터 변수는 크기와 형태가 고정(32비트일때 4바이트)되어 있는데 왜 데이터형을 꼭 밝혀야 하나?

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

두 번째 이유 - 인접한 다른 대상체로 이동할 때 이동거리를 알기 위해서.

타입을 모르면 둘 다 엉망으로 꼬이게 된다.\


T형 포인터 변수 px에 정수 i를 더하면 px=px+(i*sizeof(T))가 된다.


포인터 연산시 주의 사항

- 포인터끼리는 더할 수 없다.

결과는 있지만 의미가 없다. 주소를 두개 더한다고 나오는 번지가 의미가 있을리가 없다.

포인터의 덧셈은 99.99% 프로그래머의 실수! 컴파일하면 대개 오작동.


- 포인터끼리 뺄 수는 있다.

두 요소간의 상대적인 거리. ptr2 - ptr3은 단순한 정수형. 거리!


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

++, -- 가능. ptr + i = ptr이 가리키고 있는 번지에서부터 i번째 요소의 번지를 나타내는 의미있는 값.

ptr + 1은 이것도 역시 포인터!


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

p1 = p2로 대입이 가능.


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


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


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

if (ptr1 > ptr2) - ptr1이 ptr2보다 뒤쪽의 요소를 가리키고 있으면?


본격적으로 어려워지기 전에 이 정도는 반드시 암기해야 하는것!

포인터끼리 더할 수 없다.

포인터끼리 뺄 수 있으며 연산 결과는 정수이다.

포인터와 정수의 가감 연산은 가능하며 연산 결과는 포인터이다.


다음은 포인터 연산의 간단한 예이다.




*ptr++ ?

포인터 연산문.

*ptr이 먼저 연산되어 ptr이 가르키는 정수값이 읽힌다.

++연산이 실행되는데 이는 ptr++이 실행이 됨.

(*ptr)++과는 다르다. ptr내부의 값을 ++해주는 의미.

*ptr++의 동작을 잘 외워두면 간결한 코드를 작성할 수 있음.


2. void형 포인터

void형

일반적인 포인터와는 다르게 선언할 때 대상체의 타입을 명시하지 않는 특별한 포인터형이다.


void* vp;


이렇게 선언하면 vp 포인터 변수의 대상체는 void형이 되며 곧 대상체가 정해져 있지 않다는 뜻!


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

정수든 실수든 문자열이든 가리지 않고 메모리 위치를 기억할 수 있다. void형 포인터는 임의의 포인터를 대입할 수 있지만

반대로 임의의 포인터에 void형 포인터를 대입하기 위해서는 반드시 캐스팅을 해야 한다.


1
2
pi = (int*)vp;
pd = (double*)vp;
cs


- *연산자를 쓸 수 없다.

이 포인터가 가리키는 메모리의 값을 읽을 수 없다. 

몇 바이트를 읽어야 할지, 또 읽어낸 비트를 어떤식으로 해석해야 할지를 모르기 때문이다.



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

대상체의 타입을 알 수 없기 때문에 연산자도 곧바로 사용할 수 없다.

꼭 캐스팅을 해줘야한다!


void형 포인터의 활용

모든 상황에서 대상체를 미리 결정할 수 있는 것은 아니다!

그래서 임의의 대상체에 대해 동작해야할 경우가 있다.


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


대표적인 예가 메모리를 특정한 값으로 채우는 memset 함수이다.


1
2
3
4
5
6
7
int ari[10];
char arc[20];
double ard[30];
 
memset(ari, 0sizeof(ari));
memset(arc, 0sizeof(arc));
memset(ard, 0sizeof(ard));
cs


 void형 포인터로 첫 번째 인자를 받으므로 정수형, 문자형, 실수형 배열을 구분하지 않고

모두 인수로 받아들일 수 있다.

시작 번지와 길이만 던져주면 몽땅 원하는 값으로 채워준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main()
{
    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));
    }    
}
 
cs


해당 예는 int형 배열인 ari와 캐릭터형 배열인 arc를 같은 함수인 arDump를 이용하여 바이트 단위로 읽어 출력하는 것이다.

첫 번째 인자로 void* 로 인자를 받고 출력에서 unsigned char*로 캐스팅하여 출력한다.

void형 포인터를 적절하게 활용한 예제다.


결과값

01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00

50 5F 69  74 65 72 00


NULL 포인터

NULL 포인터는 0으로 정의되어 있는 포인터 상수값이다. 아주 특수한 시스템에서는 0이 아닐 수도 있지만

일반적으로 0이라고 생각하면 큰 무리가 없다.

어떤 포인터 변수가 NULL 값을 가지고 있다면 이 포인터는 0번지를 가리키고 있는것이다.

0번지 또한 메모리 공간의 제일 처음 위치이기 때문에 실존하는 메모리 공간이다.

0번지는 ROM이거나 시스템 예약영역에 해당하므로 저장하거나 수정할 수 없도록 보호되어 있다.


그래서  NULL 값을 리턴하면 거의 대부분의 함수가 에러처리라고 생각하면 된다.


1
2
3
4
5
6
7
 
if(func() == NULL){
    // 에러 처리
}
else{
    // 하고 싶은 일
}
cs


3. 동적 메모리 할당

할당의 필요성

프로그램이 실행되기 위해서는 메모리가 필요하다. 실행 파일 자체가 메모리에 로드(Load)되어야 실행될 수 있다.

프로그램이 작업을 위해 선언한 변수들도 모두 메모리에 할당된다.


동적할당(Dynamic Allocation)은 메모리 필요량을 지정하는 정적할당과는 달리

실행 중에(Run Time) 필요한 만큼 할당하는 기법이다.


동적 할당된 메모리는 이름 없는 변수라고 할 수 있다.

독점적인 메모리 영역을 차지하고 있으므로 일단 값을 기억할 수 있지만 이름이 없으므로 포인터로만 접근이 가능하다.

malloc 함수가 리턴하는 포인터(void*) 반드시 적절한 타입의 포인터 변수로 대입 받아야 한다.(타입 캐스팅)


메모리 관리 원칙

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

복수 개의 프로그램이 꼭 필요한 만큼의 메모리 충돌 없이 사이좋게 잘 사용하려면 정교한 메모리 관리 원칙이 필요하다!

- 메모리 관리의 주체는 운영체제다.

- 운영체제는 메모리가 있는 한은 할당 요청을 거절하지 않는다. 

가상 메모리 공간을 늘려서라도 필요한 메모리를 만들어 주는 친절한 운영체제씨!

- 한 번 할당된 메모리 공간은 절대로 다른 목적을 위해 재할당되지 않는다.

- 응용 프로그램이 할당된 메모리를 해체하면 운영체제는 

이 공간을 빈 영역으로 인식하고 다른 목적을 위해 사용할 수 있도록 한다.


할당 및 해제

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


1
2
void* malloc(size_t size);
void free(void* memblock);
cs


먼저 malloc(엠얼록이라고 읽는다) 함수부터 알아보자.

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

size_t는 메모리 양을 나타내는 단위인데 _t로 끝나는 사용자 정의 타입은 표준에 의해

반드시 정의하도록 되어 있으므로 기본 타입과 거의 대등한 자격을 가진다.

플랫폼에 따라 다르게 정의 되긴 하지만 대부분의 32비트 컴파일러들은 size_t를

unsigend의 부호없는 정수형으로 정의한다!


응용 프로그램이 할당한 메모리를 어떤 목적에 사용할지는 알 수 없으므로

maaloc은 void& 형을 리턴하기때문에 받는 쪽에서는 원하는 타입으로 캐스팅 해야 한다!


예시

1
2
3
4
int* ar;
ar = (int *)malloc(10*sizeof(int));
// ar use()
free(ar);
cs


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

해당 예시는 정수형 변수 10개를 담을 수 있는 메모리를 잡는 것이다.

그래서 4바이트 씩 10개로 10*sizeof(int)로 40바이트를 잡는 것이다.

malloc을 담을 공간이 int* 이므로 (int*)형으로 캐스팅하여 사용할 수 있다.

free(ar)을 통해 메모리 공간을 해제할 수 있다.

malloc 함수는 할당에 실패하면 에러의 표시로 NULL을 리턴한다.


재할당(calloc)

다음 함수(씨얼록이라고 읽는다)는 malloc 함수와 마찬가지로 메모리를 할당하되 필요한 메모리양을 지정하는 방법만 다르다.


1
void* calloc(size_t num, size_t size);
cs


첫 번째 인수 num은 할당할 요소의 개수,  size는 요소의 크기이다.


malloc과 비교하면,

1
2
ar = (int*)malloc(10*sizeof(int));
ar = (int*)calloc(10,sizeof(int));
cs


calloc이 malloc과 또 다른 차이점은 메모리 할당 후 전부 0으로 초기화한다는 것이다.


재할당(realloc)

이미 할당된 메모리의 크기를 바꾸어 재할당 한다.

최초 할당한 크기보다 더 큰 메모리가 필요할 때는 이 함수로 크기를 조정할 수 있다.

원래 크기보다 더 작게 축소 재할당하는 것도 가능하기는 하지만 보통은 확대 재할당한다.


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


첫 번째 인수로 malloc이나 calloc으로 할당한 메모리의 시작 번지를 주고 두 번째 인수로 재할당 크기를 전달한다.

두 번째 인자로 size가 -일 경우는 할당을 취소하라는 이야기이므로 free와 같아진다.

일반적으로 축소 재할당은 같은 번지이며 확대 재할당은 다른 번지로 이동할 가능성이 높다.

왜냐면 확대 할당하는 공간에 다른 변수가 존재한다면 메모리는 선형적이기 때문에

그 공간을 할당할 수 없고 다른 곳으로 이사를 가게 된다.

(이건 저번에 실험을 해봤다. 포스팅하는 걸 깜빡..)


4. 이중 포인터

이중 포인터

포인터 변수를 가리키는 포인터라는 듯이며 포인터의 포인터라고 할 수 있다.

포인터 변수도 메모리를 차지하고 있으므로 이 변수도 당연히 번지가 있다.

* 구두점 두개로 표현한다.


int **ppi;


ppi는 정수형 대상체를 가리키는 포인터 변수의 번지를 가리키는 포인터 변수로 선언되었다.


T*형은 하나의 타입으로 인정된다.

T형 변수를 선언할 수 있으며 T*형도 항상 선언할 수 있다.

1
2
3
4
5
6
7
int i;
int* pi;
int** ppi;
 
= 1234;
pi = &i;
ppi = &&pi;
cs




i는 일반 변수이므로 메모리 상에 4바이트 만큼 차지하고 1234가 대입되어 있다.

pi는 i를 가리키고 있으므로 *pi를 통해 i의 값을 읽을 수 잇다.

ppi는 pi를 가리키고 잇으므로 i의 값을 접근하기 위해선 2번의 역참조가 필요하다. **ppi.


main함수의 인수

main도 일종의 함수이므로 인수를 가질 수 있고 리턴값도 가질 수 있다.

main 함수의 원형은 다소 복잡한데 다음과 같은 조합을 가진다.


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

리턴값은 int형이거나 void형 중 하나를 선택할 수 있으며 세 개의 인수를 가지는데

인수는 뒤족부터 차례로 생략이 가능하다. 


miain 함수의 가능한 원형 8가지

1
2
3
4
5
6
7
8
void main(void);
void main(int argc);
void main(int argc, char* argc[]);
void main(int argc, char* argc[], char* env[]);
int main(void);
int main(int argc);
int main(int argc, char* argv[]);
int main(int argc, char* argv[], char* env[]);
cs


리턴값

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

main 함수가 리턴하는 값을 탈출 코드(Exit Code)라고 하는데 프로그램이 실행을 마치고 운영체제로 복귀할 때 리턴되는 값이다.

main 함수가 프로그램 그 자체이므로 main 함수의 리턴값이 곧 프로그램의 리턴값이 된다.


int main()은 함수 내에  리턴값이 없다면 경고가 발생한다.

다른 int func() 함수는 리턴하지 않을 경우 에러 처리를 한다.

컴파일러가 main을 조금 특수하게 취급하는 것을 알 수 있다.


argc

운영체제가 이 프로그램을 실행할 때 전달되는 인수의 개수를 말한다.

예시) 파일끼리 복사하는 boksa.exe라는 프로그램이 있다고 가정하면,


boksa file1.txt file2.txt


첫 번째 인수는 실행 파일명으로 고정되어 있으므로 argc는 항상 1보다 크다.

boksa a b 식으로 호출할 경우 argc는 3이 된다.


argv

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

운영체제가 프로그램을 실행할 때는 항상 문자열 형태의 쉘 명령을 입력하기 때문에

인수의 타입은 항상 문자열일 수밖에 없다.

정수니 실수니 하는 것들이 없으므로 정수라면 일단 문자열로 받아서 atoi 등의 변환 함수로 정수로 바꿔 사용해야 한다.


env

운영체제의 환경 변수를 알려 준다. 

환경 변수는 운영체제마다 다르게 정의하는데 DOS의 경우 Path, Prompt 등이 있고

윈도우즈의 경우 컴퓨터 이름, 시스템 디렉토리 등의 정보가 있다.

이 인수가 아니더라도 환경 변수를 조사할 수 있는 다른 방법이 있기 때문에

실질적으로 사용되지 않는다.


동적 문자열 배열

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

배열을 선언할 시점에 크기를 알아야 하므로 변수로 배열 크기를 지정할 수 없으며

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


1
2
3
4
5
6
int len;
scanf("%d", &len_;
char* name;
name = (char*)malloc(len*sizeof(char));
// use name
free(name);
cs


len의 값은 실행 중에 주어지며 malloc은 이 크기만큼의 문자형 배열을 할당한다.


조금 더 확장하여 문자열을 여러개 저장하는 name 배열을 만들어 본다면?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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));
 
}
// use anme
 
for(i = 0; i<num; i++)
{
    free(name[i]);
}
free(name);
cs




name이 가리키는 곳에는 크기 num인 char* 배열이 있고 이 배열의 요소들이 가리키는 곳에는

또 len 길이의 char 배열이 있다.

char 배열은 char*에 의해 참조되며 이런 char* 들의 집합을 name 가리키고 있으니 name의 타입은 char**가 되어야 하는 것이다.


5. void 이중 포인터

그다지 실용성이 있는 내용은 아니다. 포인터에 대해 완벽하게 이해했다면 이해가 가능하다!


void**라는 타입은 void* 타입을 가리키는 유도 타입으로 void** vpp; 변수를 선언할 때

vpp의 대상체는 임의의 대상을 가리키는 void* 타입이다.

그러므로 대상체의 타입(void*)과 대상체의 크기(4바이트 주소값)도 명확하게 알고 있다.

따라서 vpp는 void형 포인터에 적용되는 규칙 대신 일반 포인터의 규칙이 적용된다!

임의 타입의 포인터는 대입받을 수 없으며 반드시 void*형 변수의 번지만 대입받을 수 있다.


1
2
3
4
5
6
7
8
9
10
11
void *vp;
void *av[5];
void **vpp;
int i, pi = &i;
 
vpp = &vp;    //O
vpp = av;    //O
vpp ++;        //O
*vpp;        //O
vpp = &pi;    //X
**vpp;        //X 
cs


int*형 변수인 pi릐 번지를 vpp에 대입할 순 없다.


728x90
반응형