C언어의 포인터 이야기
□ C언어 포인터 개념 I
□ C언어 포인터 개념 II - float,double,char, string 포인터
■ C언어 포인터 개념 III - struct 포인터 & void 포인터
□ C언어 포인터 개념 III - struct - C++ 포인터 Linked-List
□ C언어 포인터 개념 III - struct 포인터 2
경험 상 포인터를 이해할 때는 학습단계에서 필요한 과정이 있고, 익숙한 단계에서 C을 사용하는 개념이 있다. 학습 과정에서는 포인터를 CPU의 엑세스 시점까지(기계어 동작) 깊속히 따져 보면 완전한 이해가 가능하다. 따라서 이 글은 이 심화 학습과정에서 한번 쯤 일일히 따져 보는 과정을 기술한 것이다. 이 글은 포인터를 이해하는 과정 상 어렵게 기술하게 되었다. 설명 상 각각의 기계어는 몇개 되지 않고, 그 자체를 일일히 기술하기는 무리인것 같고... 어째든 포인터를 완전히 이해하기 위해 CPU가 어떻게 동작하는지 한번 쯤 따져보고 포인터 개념을 이해 한 다음 잊어버리면 된다. 기계어 까지 해석을 하고 학습을 하면 나중에 코딩을 하면서 대략 어떻게 컴파일이 될 지 짐작이 되고 이것은 CPU을 직접 다룬다든가 하는 과정에서 디버깅이 헐씬 쉬어진다. 다음에 나온 그림은 예시 이므로 스스로 새로운 struct구조를 그려보고 주소값을 일일이 적어보면 확실한 이해에 도움이 될것으로 생각한다. 실제 CPU가 엑세스하는 주소값...
다음글에 나오는 변수의 비트는 특별한 한정이 없는 한 32비트 CPU이다. 8비트 CPU가 아니라. 'int는 32비트 정수' 에서 처럼 8비트CPU라는 말이 없다. 이때 적용된 CPU는 32비트 CPU이고, default=32비트 CPU 기준 & Little-endian.
첨부파일 : struct 구조와 linked-list
서론 - struct 구조체의 이해
포인터의 사용에서 가장 복잡하고 어려운 부분이 struct와 결합된 포인터 이다. 그러나 이것은 struct를 사용함에 필수적인 요소이다. 리눅스와 같은 OS의 기본이 struct의 구조에 의해 구성된다고 생각해도 과언은 아니다. struct 가 그만큼 중요한 요소이다.
이 과정을 이해 한다면 C++로의 개념 확장도 쉽다. C++의 object는 결국 struct와 데이터 구조가 같다.
C++ object의 필요와 개념과 동작
이글에서 데이터의 유사성과 차이를 기술 하였다.
struct에 더 자세히 알고 싶다면 다음을 참고 하면 된다.
C언어 구조체 struct 변수
struct 구조체는 컴퓨터가 정보 처리를 할 때 연관된 object에 대해 묶어서 처리하는 방식을 구현하는 구조이다. 따라서 구조체는 하나의 연관된 정보가 뭉쳐 있는 것이다.
예를 사람의 정보를 처리한다면, 이름, 전화번호, ID 등이 하나의 object에 대한 정보가 묶어서 처리하는 것이다. 여기서 묶어서라는 말은 실제로 정보가 정해진 위치에 구조적으로 나열되어 있는 상태를 말한다.
정보가 묶여 덩어리로 나열되어 있고 이것이 struct에 선언된 타입 대로 나열되어 있음을 나타낸다. 이를 이해 하기 다음 두가지를 생각하자.
case 1 :
int name[100]; // 사람 이름
int id[15]; // 주민번호
int mcount; // 지금 처리하는 사람 수
int nbuff[1024]; // 임시 처리 버퍼
int tel[100]; // 전화 번호
int sztel;
int age; // 나이
int main()
{
. . .
}
case 2 :
//////////////////////////////////
// 사람 정보
char name[100]; // 사람 이름
char id[15]; // 주민번호
char tel[100]; // 전화 번호
int age; // 나이
//////////////////////////////////
// 처리 변수
int mcount; // 지금 처리하는 사람 수
char nbuff[1024]; // 임시 처리 버퍼
int sztel;
//////////////////////////////////
// 처리 processor
int main()
{
. . .
}
위의 2가지 프로그램 경우 case1은 사람 정보를 처리할 때 사람에 해당되는 변수가 분산되어 있다. 그러나 case2는 사람의 정보를 연속적으로 나열함으로 써 보기만 해도 변수의 기능을 연관해서 짝기가 쉽고 분석도 쉽다.
위의 간단한 예의 경우 간단한 프로그램이라서 쉽게 분석할 수 있지만 보통의 프로젝트에서 사용하는 코딩의 라인수가 많고 더군다나 모듈별로 파일을 나누어 개발 한다. 이런 경우 이 문제는 복잡함과 혼돈을 야기 한다.
이것의 해결 방안이 struct이다. case2의 object에 대한 정보 처리 묶음을 하나의 struct로 선언 한다.
struct Man {
char name[16]; // 사람 이름
char id[15]; // 주민번호
int age; // 나이
char tel[20]; // 전화 번호
};
이렇게 선언을 하면 사람의 정보를 처리하는 데이터들이 모여 있게 된고 모듈적 개념을 적용하여 쉽게 코딩과 분석과 관리가 이루어 진다.
이렇게 struct을 선언하는 행위 만으로 변수의 저장 공간이 확보 되지 않으므로 이 선언을 가지고 변수를 잡는다.
struct Man man;
struct Man man[100];
이렇게 함으로써 정보 처리을 할 수 있는 공간이 생긴다. 물론 이것은 변수 영역의 메모리에 위치 한다.
이 struct의 변수의 장점은 구조적으로 정해진 방식대로 메모리에 할당 된다데 있다. 어래이가 시작 주소부터 연결되어 나열되듯이 struct도 정보가 정해진 위치에 나열된다.
#include <stdio.h>
struct Man {
char name[16];
char id[15];
int age;
char tel[14];
};
struct Man man = {
"Kim",
"640101-1538575",
45,
"010-234-2343"
};
int main(int argc, char* argv[])
{
printf("name = %s\n", man.name );
printf("id = %s\n", man.id );
printf("age = %d\n", man.age );
printf("tel = %s\n\n", man.tel );
printf("sizeof(man) = %d\n", sizeof(man) );
printf("&man = 0x%08X\n", &man );
printf("&man.name = 0x%08X\n", &man.name );
printf("&man.id = 0x%08X\n", &man.id );
printf("&man.age = 0x%08X\n", &man.age );
printf("&man.tel = 0x%08X\n", &man.tel );
return 0;
}
실행 결과 :
name = Kim
id = 640101-1538575
age = 45
tel = 010-234-2343
sizeof(man) = 52
&man = 0x00403018
&man.name = 0x00403018
&man.id = 0x00403028
&man.age = 0x00403038
&man.tel = 0x0040303C
이 구조는 다음과 같다.
변수 man은 초기값을 갖는 변수이므로 DATA SEGMENT에 할당되고 main이 시작되기 전에 위와 같은 구조로 데이터 존재 한다. 여기서 몇가지 특징을 보면
- struct 시작 주소 부터 위와 같이 정해진 규칙 대로 메모리에 배치되고 이 배치를 기반으로 주소값이 계산 된다.
- struct 구조 안에서 정해진 변수의 길이와 다음 변수와의 여유 공간은 사용하지 않는다. Alignment로 이것은 변수가 한번 엑세스하기 위한 규칙에 따라 배치 된다. char id[15]은 4의 배수가 아니라서 다음 변수 int age와의 빈공간을 하나 넣는다. int age는 정수형이므로 32비트 변수이다. 따라서 이 변수에 값을 쓰거나 읽기 위해서는 기계어 코드 하나로 한번 엑시스로 이루어 진다.
man.age = 30;
이 코드에서 age의 주소값을 결정 하는 과정과 30을 넣는 과정으로 나누어 생각할 수 있는데, 두번째 과정에서 한번 기계어 코드로 30을 넣게 된다.
경우에 따라 다음과 같은 기계어 코드를 30을 넣을 수 있다.
000ee c7 40 20 1e 00 00 00 mov DWORD PTR [eax+32], 30 ; 0000001eH
만약 이 빈공을 사용하지 않는다면
이 경우 age 변수를 주소 배치 0x00403037 ~ 0x40303A까지 한 경우이다. alignment을 사용하지 않는 경우이다. 즉, 주소값이 4로 나누어 떨어지지 않는 변수 이다. 이렇게 되면 CPU의 특성 상 최종적으로 어떤 정수값을 넣을 때, 한번의 기계어로 엑세스를 할 수 없게 된다.
int age = man.age;
처럼 데이터를 읽을 경우 man의 age의 주소값을 계산하여 레지스터에 넣고, 이 레지스터를 사용 한 레지스터 address mode로 읽어 다른 레제스터에 넣는다.
이렇게 age을 읽을 때 최종적인 단계인 데이터 영역을 읽는 과정에서 위의 그림 처럼 alignment를 하지 않는 구조라면
- 0x00403037 번지의 데이터 한 바이트 만을 따로 읽어 레지스터 A에 넣고 : A=45
- 나머지 0x00403038번지 데이터 4바이트를 읽어 레지스터 B에 넣고 : B = 0x30000000 <- 상위 MSB 8비트 '0' 값이 읽힘
- 다시 레지스터 B을 왼쪽으로 4비트 쉬프트 비트연산하고 : B=0x00000000
- 레지스터 A와 B을 ALU을 통해 OR 조작 해야 한다. A | B => A = 45
한개의 명령으로 끝 날일을 4개의 명령과 2개의 레지스터를 사용 하여야 한다.
따라서 이렇게 복잡한 명령으로 엑세스 하지 않기 위해, 32비트 변수는 4로 나누어 떨어지는 주소값을 16비트 변수는 2로 나누어 떨어지는 변수 공간을 사용 한다.
마지막에 tel변수 역시 14바이트라서 2바이트가 남아 사용하지 않지만 이것은 다른 변수에 할당하지 않는다. 결국 위의 struct는 3바이트를 사용하지 않는다.
Alignment와 관련하여 sizeof(man)은 사용하지 않는 공간 3바이트 까지 합친 것이다.
실제 데이터 영역은
16 + 15 + 4 + 14 = 49바이트만을 사용하는 것인데,
sizeof(man)=52로 49(실제데이터변수) + 3(Alignment로 추가된 사용하지 않는공간) = 52.
8비트CPU의 프로그램 예
struct Man {
char *name;
char sex;
int age;
long int code;
char tel[5];
};
char name[] = "Hong";
struct Man man = {
name,
'F', 45,
0x01020304,
"3275"
};
이 예에서 Man의 구조는
Intel 계열
Motolora 계열
8비트 CPU의 struct는 정수형 변수의 alignment 문제가 없다. 거의 대부분은 CPU가 한번에 8비트 단위로 읽을 수 밖에 없으니...
따라서 int는 2번의 기계어 코드와 2개의 레지스터가 사용 되어 진다. 따라서 8비트 CPU은 32비트에 비해 상당히 느리다. 뿐만 아니라 대부분의 int는 16비트 변수 길이를 갖는다. 16비트가 부족할 때는 long int 32비트로 하면 되고, 그런데 32비트에서 long int을 붙이면 대부분 32비트.
그런데 C는 기본적으로 변수영역을 넘는 처리는 해결 방법이 없다. 예를 들어 id는 15바이트 문자 변수인데, 이것을 넘으면 처리 하는 과정에는 문제가 없으나 오버하면 다음 변수 age 변수가 문제가 생긴다.
strcpy(man.id, "12345678901234567");
을 실행하면 정해진 상수 문자가 끝을 나타내는 0을 포함하여 18바이트가 복사되어 바뀐다. 그런데 id는 15바이트에 alignment 1바이트를 추가하면 16바이트인데, 이것을 넘어가면 바로 int age 변수 영역이 된다. 변수 침범은 C에서 방어하지 못한다.
왜 C는 기계어와 고급어의 중간 언어이기 때문에 복잡한 처리는 하지 않는다. 바로 기계어로 바꾸어 처리 된다.
struct 안의 빨간색 부분이 strcpy 함수에 의해 복사 된 것인데, id의 영역을 넘어 int age값이 변하고 말았다.
만약
printf("%d", man.age);
을 하면 '7'의 ASCII 코드값 55가 출력 된다.
만약
strcpy(man.id, "123456789012345");
처럼 16바이트를 복사하면 age 변수와는 상관없이 alignment 영역에 0이 써지고 C의 구조상 아무 문제 없다.
이것은 struct을 조작할 때, man의 주소값을 시작 번지로 하여 구조체 내의 변수의 위치의 상대적 거리를 계산 한다. 그러면 내부의 변수 메모리 위치가 나오고 여기에 조작 하는 것이다.
man.age : man의 시작 주소 0x00403018 + age의 상대치 (16+15+1) => 0x00403038 => 단지 이 주소값을 말함.
결국
man.age = 10;
이라는 이야기는
* ((int*) 0x00403038 ) = 10;
와 같다.
그렇다면
struct Man {
char name[16];
char id[15];
char sex; int age;
char tel[14];
};
struct Man man = {
"Kim",
"640101-1538575",
'M',
45,
"010-234-2343"
};
한 바이트 짜리를 삽입하면,
name = Kim
id = 640101-1538575
sex = M
age = 45
tel = 010-234-2343
sizeof(man) = 52
&man = 0x00403018
&man.name = 0x00403018
&man.id = 0x00403028
&man.sex = 0x00403037
&man.age = 0x00403038
&man.tel = 0x0040303C
아무 문제없이 실행 된다. sex 변수가 id와 age 사이의 alignmnet 영역으로 배치되어 사용되므로 sizeof(man)=52로 늘어나지 않는다.
정리하면
- struct 구조체는 메모리 상에 시작 주소부터 정해진 규칙에 따라 배치 된다.
- CPU 입장에서 보면 하나의 주소값을 변환하여 처리 한다.
struct 포인트
지금 까지 struct 포인터 랑 상관없는 이야기를 했다. 그러나 struct 내의 변수에 엑세스 하려면 모두 시작주소를 베이스로한 주소값일 뿐이다. 이 개념을 알면 포인트는 어렵지 않다.
struct Man man = { . . . };
struct Man *pman;
int main()
{
...
pman = &man;
printf("&pman = 0x%08X\n", &pman );
printf("pman = 0x%08X\n", pman );
printf("pman->age = %d\n", pman->age );
if (pman->age >= 19)
printf("\n 성인 이시내요.\n");
return 0;
}
name = Kim
id = 640101-1538575
sex = M
age = 45
tel = 010-234-2343
sizeof(man) = 52
&man = 0x00403018
&man.name = 0x00403018
&man.id = 0x00403028
&man.sex = 0x00403037
&man.age = 0x00403038
&man.tel = 0x0040303C
&pman = 0x004033A4
pman = 0x00403018
pman->age = 45
성인 이시내요.
pman = &man;
이 과정은 man의 시작 주소 0x00403018을 pman의 변수에 넣는다. 이것은 단순히 pman 변수에 CPU의 메모리 주소값을 넣는 것이다. struct 이든 뭐든 포인터는 모두 실제 주소값을 저장하면 되는 것이다.
pman->age = 30;
을 실행하기 위해
struct의 내의 변수의 위치를 계산 한다.
pman->age
라는 말은
pman이 가지고 있는 struct 베이스로 부터 age의 상대 주소값 32을 더하면 age가 있는 위치가 된다.
pman의 시작 주소값 0x00403018 + 32 => 0x00403038
이 주소값에 값을 넣으면 된다.
자 이 주소값 0x00403038에 30을 넣으면 된다.
; pman->age = 30;
000e9 a1 00 00 00 00 mov eax, DWORD PTR _pman ; pman
000ee c7 40 20 1e 00 00 00 mov DWORD PTR [eax+32], 30 ; 0000001eH
결국 변수에 값을 넣는다는 것은 주소값을 알면 그 주소에 기계어 코드로 넣으면 된다.
포인트냐 아니냐는 주소값을 어떻게 다루는가의 문제이다.
man.age : man이 고정된 주소값(변경이 불가능)으로 한 베이스 주소값 + age의 struct 내의 상대값
pman->age : pman은 struct의 베이스 주소값( 변경이 가능한 주소값) + age의 struct 내의 상대값
다시 말하면 man과 pman은 고정된 주소값이냐 주소값을 변경할 수 있는냐 이다.
만약 pman이 없다면 모든 고정된 주소값을 사용할 수 밖에 없다. 고정된 주소값이라는 것은 man 변수 처럼 변수를 선언하고 이것의 주소값을 가지고 사용하는 것이다. 지역변수라면 스택에 전역변수라면 변수 공간에. 그런데 고정된 변수는 사용 상 몇가지 한계가 있다. 변수를 사용하기 위해서는 전역변수에 잡았다면 원하는 갯수를 정확히 알아야 한다.
int szMan = 10;
struct Man man[szMan];
이것 처럼은 선언이 불가능 하다.
지역변수라면 선언을 하고 사용한 후 블럭이 끝나면 삭제 된다.
동적인 할당이 불가능 해 진다. 다음과 같은 구조를 생각해 보자.
struct Man *pman;
struct Man *makeMan()
{
pman = malloc( sizeof(struct Man) );
memset(pman, 0 , sizeof(struct Man) );
return pman;
}
이 과정은 필요한 변수 공간을 동적으로 잡고 이것을 사용 한 후, free 함수로 삭제 할 수 있다.
기타 몇가지 포인터가 가지는 장점이 있다.
struct 포인터 표기법
C프로그램의 초보자는 포인터 자체가 어려운데다가 struct와 결합된 포인터가 어려움을 가중 시키다.
점을 붙여야 하는지 화살표인지???
기본적인 개념은 현재 변수가 포인터 인가 아닌가가 화살표인가 점인가를 결정하면 쉽다.
여러가지 포인터의 개념을 알아보기 위해 복잡한 프로그램을 작성해 본다. 그리고 최대화 여러가지 예를 통해 struct 파악...
*** stmember.h ******************************
#ifndef _STMEMBER_H
# define _STMEMBER_H
struct Car {
char name[8];
int mkyear;
char maker[8];
int engine;
int color;
};
struct Man {
char name[8];
char sex;
int age;
char tel[8];
};
struct Family {
struct Family *next;
char rel[8];
struct Man *man;
int pos;
};
struct Member {
struct Member *next;
char id[8];
struct Man man;
char *pos;
short int year;
short int dtype;
struct Car *car;
void *data;
int szdata;
};
////////////////////////////////
// Data type
// Point Data Exist Type
#define DEXISTTYPE_MASK 0xFF00
#define DEXISTTYPE_CAR_FIX 0x0100
#define DEXISTTYPE_CAR_MALLOC 0x0200
#define DEXISTTYPE_DATA_FIX 0x0400
#define DEXISTTYPE_DATA_MALLOC 0x0800
// Data Type
#define DTYPE_NULL 0x0000
#define DTYPE_FAMILY 0x0001
#define DTYPE_INT 0x0002
#define DTYPE_STRING 0x0003
////////////////////////////////
// Function
// Init. & List Member
struct Member* initstruct();
struct Member* LinkList(struct Member *pmem);
struct Member* getRootList();
// Display struct
void printstruct();
struct Member* printMember(struct Member *pmem);
// Member List
struct Member* nextMember(short int dt, int length);
// Delete Member
void deleteMemberCarData(struct Member *pmem);
void deleteAllMember();
#endif
*** stmember.c ******************************
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "stmember.h"
char *mJosunArmy[] = {
// 수사 : 수군 절도사의 준말로 각 도의 수군을 지휘.
// 임란 당시 삼도 수군 통제사는 전라 좌수사, 전라 우수사, 경상 좌수사, 경상 우수사, 충청수사를 지휘.
"통제사",
"첨사", "만호" // 수군 절도사의 실질적인 부하 장수.
"조방장", // 일종의 참모 역할을 하는 장수.
"군관", // 첨사, 만호의 부하 장수.
"도원수", // 유사시 통솔권으로 자지고 있는 장수
"목사", "부사", // 성주
NULL
};
char *mArea[] = {
"충청",
"순천",
NULL
};
struct Car mcar = {
"판옥선1", 1598, "좌수영", 2000,
0x345577
};
struct Man msunMyeon = {
"이면", 'M', 12, "서신1"
};
struct Family msun = {
NULL,
"아들",
&msunMyeon,
0
};
struct Member memCeo = {
NULL,
"34556",
{
"이순신", 'M', 45, "파발마1"
},
mJosunArmy[0], // "통제사",
1597,
DEXISTTYPE_CAR_FIX | DEXISTTYPE_DATA_FIX | DTYPE_FAMILY,
&mcar,
&msun,
sizeof(msun)
};
struct Member *pmemb;
struct Member *ptail;
struct Member* initstruct()
{
pmemb = &memCeo;
ptail = pmemb;
return pmemb;
}
struct Member* getRootList()
{
return pmemb;
}
struct Member* LinkList(struct Member *pmem)
{
ptail->next = pmem;
ptail = pmem;
return pmemb;
}
void printstruct()
{
printf("sizeof(memCeo) = %d\n", sizeof(memCeo) );
printf("sizeof(mcar) = %d\n", sizeof(mcar) );
printf("sizeof(Man) = %d\n", sizeof(Man) );
printf("sizeof(Family) = %d\n", sizeof(Family) );
printf("sizeof(pmemb) = %d\n", sizeof(pmemb) );
printf("\n" );
printf("&memCeo = 0x%08X\n", &memCeo);
printf("&mcar = 0x%08X\n", &mcar);
printf("&msunMyeon = 0x%08X\n",&msunMyeon);
printf("&msun = 0x%08X\n", &msun);
printf("&pmemb = 0x%08X\n", &pmemb);
printf("\n" );
printf("&pmemb->next = 0x%08X\n", &pmemb->next);
printf("&pmemb->id = 0x%08X\n", &pmemb->id);
printf("&pmemb->pmemb.man = 0x%08X\n", &pmemb->man);
printf("&pmemb->year = 0x%08X\n", &pmemb->year);
printf("&pmemb->pos = 0x%08X\n", &pmemb->pos);
printf("&pmemb->car = 0x%08X\n", &pmemb->car);
printf("&pmemb->data = 0x%08X\n", &pmemb->data);
printf("\n" );
printf("mJosunArmy = 0x%08X\n", mJosunArmy );
for (int cnt = 0;mJosunArmy[cnt];cnt++)
printf("0x%08X = %s\n", mJosunArmy[cnt], mJosunArmy[cnt] );
printf("\n" );
}
struct Member* printMember(struct Member *pmem)
{
printf("pmem = 0x%08X\n", pmem);
printf("pmem->next = 0x%08X\n", pmem->next);
printf("pmem->id = %s\n", pmem->id);
printf("pmem->man.name = %s\n", pmem->man.name);
printf("pmem->man.sex = %c\n", pmem->man.sex);
printf("pmem->man.age = %d\n", pmem->man.age);
printf("pmem->man.tel = %s\n", pmem->man.tel);
printf("pmem->pos = 0x%08X [%s]\n", pmem->pos, pmem->pos);
printf("pmem->year = 0x%08X [%d]\n", &pmem->year, pmem->year);
printf("pmem->dtype = 0x%08X [0x%04X]\n", &pmem->dtype, pmem->dtype);
printf("pmem->car = 0x%08X\n", pmem->car);
printf("pmem->car->name = %s\n", pmem->car->name);
printf("pmem->car->mkyear = %d\n", pmem->car->mkyear);
printf("pmem->car->maker = %s\n", pmem->car->maker);
printf("pmem->car->engine = %d\n", pmem->car->engine);
printf("pmem->car->color = 0x%X\n", pmem->car->color);
printf("pmem->data = 0x%08X\n", pmem->data);
printf("pmem->szdata = %d\n", pmem->szdata);
printf("\n" );
if (pmem->data) {
switch (pmem->dtype & ~DEXISTTYPE_MASK) {
case DTYPE_FAMILY :
{
struct Family *pfam;
pfam = (struct Family *) pmem->data;
printf("Data type : Family\n");
printf("pfam->rel = %s\n", pfam->rel);
printf("pfam->man->name = %s\n", pfam->man->name);
printf("pfam->man->sex = %c\n", pfam->man->sex);
printf("pfam->man->age = %d\n", pfam->man->age);
printf("pfam->man->tel = %s\n", pfam->man->tel);
}
break;
case DTYPE_INT :
{
int *pdata;
pdata = (int*) pmem->data;
printf("Data type : integer[]\n");
printf("pmem->data size = %d\n", pmem->szdata);
for (int cnt = 0;cnt < pmem->szdata;cnt++)
printf("%d\t", *pdata++ );
printf("\n");
}
break;
case DTYPE_STRING :
printf("Data type : string\n");
printf("pmem->data = %s\n", (char*) pmem->data);
break;
}
}
return pmem;
}
struct Member* nextMember(short int dt, int length)
{
struct Member *pmem;
pmem = (struct Member *) malloc( sizeof(struct Member) );
memset((void*) pmem, 0, sizeof(struct Member) );
pmem->car = (struct Car *) malloc( sizeof(struct Car) );
memset((void*) pmem->car, 0, sizeof(struct Car) );
pmem->dtype = DEXISTTYPE_CAR_MALLOC;
if (dt == DTYPE_FAMILY) {
pmem->dtype |= DEXISTTYPE_DATA_MALLOC
| DTYPE_FAMILY;
struct Family *pfam;
pfam = (struct Family *) malloc( sizeof(struct Family) );
pmem->data = (void*) pfam;
pmem->szdata = sizeof(struct Family);
return pmem;
}
if (! length) {
pmem->data = NULL;
pmem->szdata = 0;
return pmem;
}
switch ( dt ) {
case DTYPE_INT :
{
pmem->dtype |= DEXISTTYPE_DATA_MALLOC
| DTYPE_INT;
pmem->data = (void*) malloc( length * sizeof(int) );
pmem->szdata = length;
}
break;
case DTYPE_STRING :
{
pmem->dtype |= DEXISTTYPE_DATA_MALLOC
| DTYPE_STRING;
char *pstr;
pstr = (char *) malloc( length+4 );
pmem->data = (void*) pstr;
*pstr = 0;
pmem->szdata = length;
}
break;
}
return pmem;
}
void deleteMemberCarData(struct Member *pmem)
{
if (pmem->dtype & DEXISTTYPE_CAR_MALLOC) {
free( pmem->car );
pmem->car = NULL;
}
if (pmem->dtype & DEXISTTYPE_DATA_MALLOC) {
free( pmem->data );
pmem->dtype = 0;
pmem->data = NULL;
pmem->szdata = 0;
}
}
void deleteAllMember()
{
struct Member *pmem;
struct Member *pnextmem;
pmem = pmemb;
deleteMemberCarData(pmem);
pmem = pmem->next;
while (pmem) {
deleteMemberCarData(pmem);
pnextmem = pmem->next;
free ( pmem );
pmem = pnextmem;
}
}
*** main.c ******************************
#include <stdio.h>
#include <string.h>
#include "stmember.h"
int main(int argc, char* argv[])
{
struct Member* pmem = initstruct();
printf("*** 포인터의 구조 : addres *****\n");
printstruct();
printf("\n*** 처음 *********************\n");
printMember(pmem);
printf("\n*** 다음 *********************\n");
pmem = nextMember(DTYPE_STRING, 512);
LinkList(pmem);
struct Member* pmlist = getRootList();
printf("List Member : ");
while (pmlist) {
printf("0x%08X ", pmlist);
pmlist = pmlist->next;
if (pmlist)
printf(" -> ");
else
printf(" -> 0x%08X\n", pmlist);
}
printf("current pmem = 0x%08X\n\n", pmem);
// Member의 정보를 설정 한다.
strcpy(pmem->id, "01233");
strcpy(pmem->man.name, "권준");
pmem->man.sex = 'M';
pmem->man.age = 28;
strcpy(pmem->man.tel, "파발마2");
pmem->pos = "순천부사";
pmem->year = 1537;
// Car의 정보를 설정 한다.
strcpy(pmem->car->name, "거북선1");
pmem->car->mkyear = 1956;
strcpy(pmem->car->maker, "좌수영");
pmem->car->engine = 400;
pmem->car->color = 0x004467;
// Data의 정보를 설정 한다.
char *pstr = (char *) pmem->data;
strcpy(pstr, "전략가");
printMember(pmem);
deleteAllMember();
printf("\nEnd...");
return 0;
}
실행 결과 :
*** 포인터의 구조 : addres *****
sizeof(memCeo) = 60
sizeof(mcar) = 28
sizeof(Man) = 24
sizeof(Family) = 20
sizeof(pmemb) = 4
&memCeo = 0x0040308C
&mcar = 0x00403044
&msunMyeon = 0x00403060
&msun = 0x00403078
&pmemb = 0x0040341C
&pmemb->next = 0x0040308C
&pmemb->id = 0x00403090
&pmemb->pmemb.man = 0x00403098
&pmemb->year = 0x004030B4
&pmemb->pos = 0x004030B0
&pmemb->car = 0x004030B8
&pmemb->data = 0x004030C0
mJosunArmy = 0x00403018
0x00402148 = 통제사
0x00402140 = 첨사
0x00402134 = 만호조방장 -> 프로그램에서 "만호" 다음에 컴마가 빠져내...
0x0040212C = 군관
0x00402124 = 도원수
0x0040211C = 목사
0x00402114 = 부사
*** 처음 *********************
pmem = 0x0040308C
pmem->next = 0x00000000
pmem->id = 34556
pmem->man.name = 이순신
pmem->man.sex = M
pmem->man.age = 45
pmem->man.tel = 파발마1
pmem->pos = 0x00402148 [통제사]
pmem->year = 0x004030B4 [1597]
pmem->dtype = 0x004030B6 [0x0501]
pmem->car = 0x00403044
pmem->car->name = 판옥선1
pmem->car->mkyear = 1598
pmem->car->maker = 좌수영
pmem->car->engine = 2000 --> 가만히 있어봐, 차가 2000cc 이지 격군 2000명이면 너무 많은거 같음. 엉터리 데이터. 어째든...
pmem->car->color = 0x345577 --> RGB, 판옥선이 이 색깔 맞아? 나도 모름.
pmem->data = 0x00403078
pmem->szdata = 20
Data type : Family
pfam->rel = 아들
pfam->man->name = 이면
pfam->man->sex = M
pfam->man->age = 12
pfam->man->tel = 서신1
*** 다음 *********************
List Member : 0x0040308C -> 0x00393900 -> 0x00000000
current pmem = 0x00393900
pmem = 0x00393900
pmem->next = 0x00000000
pmem->id = 01233
pmem->man.name = 권준
pmem->man.sex = M
pmem->man.age = 28
pmem->man.tel = 파발마2
pmem->pos = 0x0040263C [순천부사]
pmem->year = 0x00393928 [1537]
pmem->dtype = 0x0039392A [0x0A03]
pmem->car = 0x00393950
pmem->car->name = 거북선1 -> 원래 거북선은 다른 사람이 지휘해는데, 생각이 안남.
pmem->car->mkyear = 1956
pmem->car->maker = 좌수영
pmem->car->engine = 400
pmem->car->color = 0x4467
pmem->data = 0x00395C60
pmem->szdata = 512
Data type : string
pmem->data = 전략가
End...
전체 실행 구조를 도식화 하면
포인터에 익숙하지 않는 개발자라면 위의 그림 만 보아도 머리 아프다. 디양한 포인터를 사용하여 머리를 복잡하게 한다. 그렇다고 포기할 수는 없는 일이다. 프로그램을 하는 한... 복잡할 수록 원리를 생각하고 원칙대로 따라가면 쉬어 진다. 우선 포현법 부터 생각 한다.
CPU가 어떤 타입의 데이터값을 엑세스하기 위해서는 일단은 주소값을 알아야 하고, 데이터가 있어야 한다.
예를 주어 정수값 45을 어느 변수에 넣는다는 이야기는 우선 age 변수가 위치 주소값 부터 알아 내야 한다. 그리고는 결정된 주소에 정수값을 넣으면 된다.
이 이야기에서 결정되어야 하는 것은
- 데이터를 쓰거나 읽기 위해 주소값 결정
- 결정에 주소값에 해당 타입의 데이터를 엑세스 한다.
어떤 주소에 데이터를 엑세스하기 위해서는 데이터의 길이가 결정 되어야 한다. 8, 16, 32, 64비트의 데이터 엑세스 단위가 결정 되어야 한다. 이 데이터 길이는 변수의 형태로 결정 한다.
char - 8비트
short int - 16비트
int - 32비트
로 길이가 결정되어 있고, 이 길이는 기계어 코드에 결저되어 있어 컴파일러가 적당한 기계어로 바꾸는 것이다.
지겹고 수없이 이야기 한 주소값이 결정 되는 과정을 다시 한번 생각한다. 포인터의 전부이기 때문이다.
memCeo.age = 45;
pmemb->age = 45;
이 코드는 CPU의 입장에서는 어떤 변수이건 간에 정수값을 메모리에 저장하면 된다. 여기서 나이라는 개념은 사람이 생각하는 것이지 CPU는 이것이 어떤 의미가 있는지는 모르고 중요하지도 않다. 단지 데이터의 길이가 중요할 뿐이다.
우선 주소값을 결정하는 과정이 필요하다.
memCeo변수는 전역변수로 메모리에 링커에 의해 할당되어 있는 상태이다. 따라서 해당 프로그램이 종료할 때 까지는 절대로 주소값이 변하는 일이 없다. 그래서 이것이 기계어 코드에 고정되어져 있다.
위의 예에서
&memCeo = 0x0040308C
이므로 0x0040308C번지이다.
이 고정된 상대값으로 부터 age의 offset 주소값을 더하면, 최종 쓰기 위한 주소값이 되는 것이다.
mov eax, OFFSET 0x0040308C - immediately mode
mov DWORD PTR (eax+24), 45
이것 처럼 3가지 요소
- 엑세스 대상 주소 : 0x0040308C + 0x18 => 0x004030A4 : EAX레지스터 사용, 상황에 따라
- 엑세스 데이터 : 45
- 엑세스 단위 : 32비트 - mov DWORD PTR (eax+24), 45
여기서 포인터가 아닌 변수는 고정된 주소값을 사용 했다는 이야기 이다.
이에 비해
pmemb->age = 45;
여기에서 엑세스 하기 위한 주소값을 결정하는 과정이 다르다.
&pmemb = 0x0040341C
처럼 이 변수가 존재하는 위치는 0x0040341C 이다. 이것은 struct Member 데이터가 위치를 지정하는 것이다. 즉, 메모리의 위치를 CPU에 따라 정해진 길이 만큼 가지고 있는 것이다.
번지 0x0040341는 struct 데이터가 있는 위치가 아니고 존재하는 위치의 주소값 이기 때문에,이 번지에서 읽으면 struct 주소값이 된다.이 때서야 베이스 주소가 결정된 것이다.
mov eax, DWORD PTR 0x0040341C ==> EAX = 0x0040308C: direct address mode
mov DWORD PTR (eax+24), 45
여기서의 3가지 요소
- 엑세스 대상 주소 :
1. 0x0040341C에 있는 주소값을 읽는다.
2. 0x0040308C + 0x18 => 0x004030A4
- 엑세스 데이터 : 45
- 엑세스 단위 : 32비트 - mov DWORD PTR (eax+24), 45
포인터 변수가 아닌 경우와 의 차이는 대상 주소가 다른 메모리(0x0040341C)에 존재하는 것이므로 이로 부터 한번 더 주소값을 읽어야 한다는 것이다. 한 단계의 과정이 더 필요하다.
그러나 미 명령 하나만 보다 처리 시작이 더 많이 필요 한것처럼 느낄수 있으나 대부분은 포인터 처리가 효율적이다.
struct Member memData[100];
struct Member *pmemb;
case 1:
for (int cnt = 0;cnt < 100;cnt++) {
memData[cnt].age = 10;
...
}
case 2 :
pmemb = memData;
for (int cnt = 0;cnt < 100;cnt++, pmemb++) {
pmemb ->age = 10;
...
}
이 두 경우 속도 차이는 분명 포인터가 효율적이다.
이제 포인터의 초기 과정을 예를 보면
; pmemb = &memCeo;
00000 b8 00 00 00 00 mov eax, OFFSET memCeo ; memCeo의 데이터 존재 위치 0x0040308C
00005 a3 00 00 00 00 mov DWORD PTR pmemb, eax ; pmemb의 위치 0x0040341C
이 과정은 모든 포인터의 주소값 저장 과정과 기계어 코드가 같다.
char buff[100];
char *pbuff = buff;
int *pdata = idata;
등 모든 포인터의 처리 방식이 같다는 거.
왜 모든 포인터 값은 해당 데이터가 있는 주소값이므로, 그리고 CPU가 결정 되면 주소체계는 결정 되어 있으므로...
계속 반복하여 100번 정도하면 머리속에 박힐 것 같으니...
struct에서 프로그램 코드이 표현을 생각 한다.
memCeo.age = 45;
pmemb->age = 45;
어떤 때는 점이고 어느 때는 화살표인가?
원칙 :
- car-> : car 변수가 포인터 변수이기 때문에 화살표
- car-> : car 변수가 포인터 변수이기 때문에 화살표
- man. : man 변수가 포인터가 아니므로 점
이것은 위의 그림의 struct 에서 보면 포인터 변수는 모두 4바이트 주소값으로 표시된 부분은 모두 화살표 이다.
계속 연결되어 위치를 결정할 때, 원칙은 간단하다. 그런데 이 원칙 하나를 몰라서 고민 하면 안돼지.....
pmamb->man.name -> man의 name 변수 주소값, char []에서 이름 만 쓰면 주소값을 나타낸다. 이것은 char 변수의 규칙이다.
pmamb->man.name[0] -> name의 첫 번째 char (한 바이트 문자)
중간 타입 변환 하기
pmem = &memCeo;
char* pname = ( (struct Family*)pmem->data )->man->name;
printf("((struct Family*)pmem->data)->man->name = %s\n", pname);
여기서 data 변수는 void* 이므로 다음 단계로 진행 할 수 없다. 따라서 다음 포인터 부터 data가 어떤 변수인지를 중간에 정의 하면 된다.
화살표 이든 점이든 계속 연결 된 변수는 차례대로 해당 위치에 주소값을 연속해서 결정해 가는 것 뿐이다.
연속된 변수는 차례대로 진행 되면서 주소값을 결정하는 것은 다음과 같은 과정으로 주소값이 결정 된다.
실행과정 :
1. pmem :
pmem의 변수에서 주소값을 읽는다 : (0x0040341C) => 결과 : 0x0040308C
2. pmem->data :
data가 포인터 변수 이므로 이 주소값을 읽는다.
0x0040308C+data의 offset값 = 0x0040308C+ 0x30 = 0x004030BC로 부터 포인터 주소값을 읽는다.
=> 결과 0x00403078
3. ((struct Family*)pmem->data)->man :
2에서 결정 된 주소가 데이터가 있는 위치는 확실한데, 어떤 데이터를 읽어 올지를 모른다.
여기서 struct Familiy구조가 예에서 주어진 것이므로 타입변환 한다.
(struct Family*) 0x00403078로 부터 구조가 결정으므로 이 struct 구조로 man의 offset값으로 부터 포인터 값을 읽는다.
0x0040307 + 0x0C = 0x00403084로 부터 주소값을 읽음 => 결과 : 0x00403060
4. ((struct Family*)pmem->data)->man->name :
다시 struct Man의 주고에서 name의 위치를 계산 한다.
0x00403060 + 0x0000 = 0x00403060 이 주소가 이름을 나타내는 스트링의 주소값이다.
struct 연결하기 위한 포인터
다음 멤버를 만들고 연결하는 방법은 포인터를 사용하여 관리 할 수 있다. '이순신'이 고정된 변수에 고정된 값을 사용 했다면 '권준'은 동적 메모리를 사용한 방법이다.
이 struct 변수를 malloc()함수나 new로 만들고 이를 포인터로 연결하여 관리할 수 있다. 이렇게 되면 필요한 멤버의 자료를 처리하고 끝나면 버리면 된다.
struct Member *next; 변수를 사용하여 다음 멤버들을 연결하는 과정인데 이것은 간단히 연결하려는 다음 데이터 영역의 주소값을 넣으면 된다.
'이순신'의 next 변수를 이용하여 다음 멤버의 주소를 넣어 연결하는 함수는
memCeo.next = (struct Member *) malloc( sizeof(struct Member));
이것에서 malloc에서 만들어진 것은 heap영역에서 원하는 사이즈의 바이트를 잡고 포인터 값을 준다. 이 포인터 역시 32비트 주소값이면 되므로 이것은 next 변수에 넣으므로써 다음 멤버의 위치를 파악할 수 있다.
struct Member *pmem = &memCeo; ---> 이순신
다음 멤버를 찾으려면
pmem = pmem->next;
하면 다음 멤버의 위치를 알 수 있어 이런 경우 '권준' 데이터의 위치가 된다.
pmem : 0x0040308C -> 0x00393900
그런데 '권준'에서 다시 '이순신'으로 가려면 이 변수만을 가지고는 찾을 수 없다.
다시 처음 부터 해당 멤버를 찾아야만 한다.
따라서 역방향으로도 진행이 가능하게 하려면 포인터 변수를 하나 더 사용하면 된다.
struct Member {
struct Member *next;
struct Member *prev;
. . .
};
두 struct간의 link-list을 만들 때, 가장 간단한 방식이다.
원하는 위치에서 prev변수를 사용하여 역으로도 쉽게 다음 멤버를 찾을 수 있게 된다.
처음에 이 struct 구조를 볼 때, 이해가 되지 않았던 부분이 바로 포인터 변수가 자체의 Member 와 같은 구조가 혼돈 스럽게 했다.
struct Member {
struct Member *next;
struct Member *prev;
char id[8];
struct Man man;
char *pos;
short int year;
short int dtype;
struct Car *car;
void *data;
int szdata;
};
같은 이름도 결국은 포인터는 다음 멤버의 주소값이라는거, 즉 주소값이 들어가는 변수이다.
그리고 처음과 끝을 나타내는 변수가 있으면 쉽게 시작과 끝을 찾을 수 있다.
struct Member *rootmemb;
struct Member *tailmemb;
struct Member *rootmemb;
struct Member *tailmemb;
struct Member *getroot() { return rootmemb; }
struct Member *gettail() { return tailmemb; }
void init_linkedlist()
{
rootmemb = NULL;
tailmemb= NULL;
}
struct Member *inserttail( struct Member *pmem) // Linked-List의 끝에 연결 하기 - tail
{
if (rootmemb == NULL) {
rootmemb = pmem;
tailmemb= pmem;
pmem->next = NULL;
pmem->prev = NULL;
return pmem;
}
tailmemb->next = pmem;
pmem->next = NULL;
pmem->prev = tailmemb;
tailmemb = pmem;
return pmem;
}
struct Member *deletelist( struct Member *pmem)
{
if (pmem->prev == NULL) {
rootmemb = pmem->next;
} else {
pmem->prev->next = pmem->next ;
}
if (pmem->next == NULL) {
tailmemb = pmem->prev ;
} else {
pmem->next->prev = pmem->prev;
}
pmem->next = NULL;
pmem->prev = NULL;
return pmem;
}
void printlist()
{
struct Member *pmem;
pmem = getroot();
for (;pmem; pmem=pmem->next) {
printf("이름 : %s\n", pmem->man.name);
// . .
}
}
*** main.c:
struct Member *pmem1;
struct Member *pmem2;
struct Member *pmem3;
int main(int argc, char**argv, char**env)
{
struct Member *pmem;
init_linkedlist();
pmem1 = pmem = (struct Member *) malloc( sizeof(struct Member)); // 원하는 멤버를 만든다.
// struct의 값들을 설정 한다.
strcpy(pmem->man.name, "이순신");
// ...
inserttail( pmem); // Linked-List의 끝에 연결 한다.
pmem2 = pmem = (struct Member *) malloc( sizeof(struct Member)); //새로운 멤버를 만든다.
// struct의 값들을 설정 한다.
strcpy(pmem->man.name, "권준");
// ...
inserttail( pmem);
// Linked-List 사용하기
pmem3 = pmem = (struct Member *) malloc( sizeof(struct Member)); //새로운 멤버를 만든다.
// struct의 값들을 설정 한다.
strcpy(pmem->man.name, "정만호");
// ...
inserttail( pmem);
// 원하는 멤버를 만들었으니 이제 이것들을 사용해 본다.
printlist();
// 만약 멤버 사용이 끝났다면 list에서 지워 본다.
printf("Delete 3\n");
deletelist(pmem3 );
printlist();
printf("Delete 1\n");
deletelist(pmem1);
printlist();
printf("Delete 2\n");
deletelist(pmem2);
printlist();
return 0;
}
이 구조를 그림으로 나타내면
이와같은 과정은 일반적인 linked-list을 만들어 보았다. 필요한 알고리즘에 따라 tree니 하는 구조적 프로그램을 포인터를 사용하여 할 수 있다.
다른 struct간의 linked-list로 연결하기
struct 포인터를 사용 한 linked-list는 같은 struct에서만 가능한 것은 아니다. 다르게 선언된 struct에서도 포인터를 지정할 수 있다. 쉬운 방법은 여러가지의 struct 선언을 하고 타입변환을 통해 원하는 프로그램 모듈을 하는 방법이 있다. 각각의 struct에서 링크에 필요한 포인터 만을 앞에 통일화 시키고 나머지 다른 부분의 구조화를 연결하는 방식이다.
이 방법은 다음 글을 참고 한다.
C언어 포인터 개념 III - struct 포인터 2
블로그의 글이 너무 길거나 첨부파일이 많으면 문제가 발생해 나누어 올림. 이 글은 하나로 스펨이 아닌데, naver 너 이러면 안되지...
C++ class 포인터 - 리스트
struct와 C++에서의 구조를 비교해 보면 거의 같다.
이 글에서 제시된 struct 프로그램을 C++로 다시 작성하여 기술하여 비교하였다. 다음과 글을 참조할 수 있다.
C언어 포인터 개념 III - 2_C++ 포인터
위의 예에서 고정된 변수를 사용한 포인터와 malloc함수에 의한 동적 메모리를 활용한 방법을 알아 보았다. 또 다른 방법은 다른 타입의 고정 된 메모리 영역으로 부터 sruct을 할당하는 방법이다.
이 때 포인터를 활용하면 다음과 같은 예가 가능하다.
char g_buff[1024];
int main()
{
struct Memeber *pmem;
char *pstr;
pstr = g_buff;;
pmem = (struct Memeber *) pstr;
pstr += sizeof(struct Memeber );
pmem->car = pstr;
pstr += sizeof(struct Car);
pmem->data = (void*) pstr;
pmem->dtype = DEXISTTYPE_CAR_FIX | DEXISTTYPE_DATA_FIX
| DTYPE_STRING;
pmem->szdata = 100;
pmem->next = NULL;
inputMember(pmem);
// A 사람 처리 계속
pstr = g_buff;;
pmem = (struct Memeber *) pstr;
pstr += sizeof(struct Memeber );
pmem->car = pstr;
pstr += sizeof(struct Car);
pmem->data = (void*) pstr;
pmem->dtype = DEXISTTYPE_CAR_FIX | DEXISTTYPE_DATA_FIX
| DTYPE_INT;
pmem->szdata = sizeof(int) * 100;
// B 사람 처리
...
}
이렇게 char 버퍼로 부터 타입을 변환하여 사용 할 수 있다. 동적으로 malloc함수를 사용한 것이므로 free함수를 사용할 필요가 없다.
결국 포인터를 사용한 프로그램은 CPU 입장에서 보면 메모리 주소값을 기반으로 데이터를 처리 하는 것으로 보면, 실제 저장공간은 할당된 메모리의 어느 위치 든 변수 타입에 상관없이 타입 변환 struct로 사용 할 수 있다.
포인터의 void*
예에서 사용한
void *data;
을 사용 할 때, void는 어떤 타입 인지도 모르는데 어떻게 포인터로 사용할 수 있는가?
그런데 포인터 변수는 CPU의 주소체계에 따라 주소의 길이는 결정 되어 있다. 그런데 포인터는 주소값 만을 가지고 처리 하므로 어느 타입 인지를 몰라도 주소값 저장은 가능하다. 타입은 나중에 실제 데이터 값을 저장할 때 결정하는 것이지 어느 위치를 가르키는 것은 상관이 없는 일이기 때문이다. 또한 포인터는 CPU 입장에서 보면 단지 메모리의 주소값을 뿐이다. 이것은 모든 포인터의 실제 데이터가 어떤 것인지는 포인터의 주소값과는 상관없어 진다.
void의 사용은 그래서 어떤 타입이든 모르고 다양한 포인터 값을 수용할 때 사용 할 수 있다. 그러나 void 포인터로 선언된 변수를 가지고 실제 테이터를 엑세스 할 수는 없다.
char g_buff[1024];
void *data;
data = (void*) g_buff;
*data = 10;
data 변수가 가지는 포인터 주소는 실제로 어떤 값이든 저장이 가능한 공간은 존재 한다. 그러나 프로그램 처럼 10을 넣고자 할 때 void 포인터를 사용이 불가능 하다.
CPU가 엑세스 하기 위해서 3가지 요소가 필요하다고 이미 언급 했는데 void 의 경우는 엑세스 처리 단위가 결정이 되지 않는 상태이다. 그러니까 CPU가 8, 16,32 중 어느 길이로 엑세스 할지를 모르는 상황이 되어 버린 것이다.
만약 data 변수가 있는 위치가 0x00403400이고 g_buff가 0x004030500이라면
data = (void*) g_buff;
이 코드는
mov DWORD PTR data, OFFSET g_buff
가 실행되어 현재 주소값은 data 변수에 저장 된 상태라면
이제
*data = 10;
을 실행하기 위해 우선 주소값을 data 변수로 부토 읽어 온다.
mov eax, DWORD PTR 0x00403400 --- 실행 결과 EAX= 0x004030500 -> EAX가 g_buff을 가르키고 있다.
다음 단계인 10을 저장하기 위한 단계가 필요 한데 10이 어떤 길이를 알 수 없다.
만약 data가
int *data; 라면
*data = 10; 이것은 10이 4바이트라는 것을 알기 때문에 컴파일러는 다음과 같이 4바이트 쓰기 명령으로 컴파일 한다.
mov DWORD PTR (eax+24), 10 ---> 만약 4바이트 정수형 10을 넣을 경우
그러나 위의 예 처럼
int idata;
void *data = (void *) &idata;
지금 void * 이므로 억세스를 위한 길이 결정이 되어 있지 않는 다는 것이 문제인 것이다. 그렇다고 컴파일러 마음대로 결정할 수는 없는 일이다. 이런 경우 컴파일러에세 전송 타입을 알려 주어여 한다.
*(int*) data = 10; ---> 10은 자동으로 int로 결정된다. 32비트 정수형.
이렇게 하면 이제서야 4바이트 메모리 쓰기 임을 컴파일러가 결정할 수 있다.
mov DWORD PTR (eax+24), 10
*(char *) data = 10;
라면 뒤의 10이 1바이트 값임을 알수 있어서
mov BYTE PTR (eax+24), 10
처럼 한바이트 쓰기가 이루어 진다.
정리하면 void 포인트는 엑세스 할 때의 엑세스 단위(8,16,32)을 모르기 때문에 실제값을 넣는 조작은 불가능하다. 그러나 포인터는 CPU에 의해 이미 결정되어 있기 때문에 포인터의 조작은 가능하다.
□ C언어 포인터 개념 II - float,double,char, string 포인터
■ C언어 포인터 개념 III - struct 포인터 & void 포인터
□ C언어 포인터 개념 III - struct - C++ 포인터 Linked-List
□ C언어 포인터 개념 III - struct 포인터 2
경험 상 포인터를 이해할 때는 학습단계에서 필요한 과정이 있고, 익숙한 단계에서 C을 사용하는 개념이 있다. 학습 과정에서는 포인터를 CPU의 엑세스 시점까지(기계어 동작) 깊속히 따져 보면 완전한 이해가 가능하다. 따라서 이 글은 이 심화 학습과정에서 한번 쯤 일일히 따져 보는 과정을 기술한 것이다. 이 글은 포인터를 이해하는 과정 상 어렵게 기술하게 되었다. 설명 상 각각의 기계어는 몇개 되지 않고, 그 자체를 일일히 기술하기는 무리인것 같고... 어째든 포인터를 완전히 이해하기 위해 CPU가 어떻게 동작하는지 한번 쯤 따져보고 포인터 개념을 이해 한 다음 잊어버리면 된다. 기계어 까지 해석을 하고 학습을 하면 나중에 코딩을 하면서 대략 어떻게 컴파일이 될 지 짐작이 되고 이것은 CPU을 직접 다룬다든가 하는 과정에서 디버깅이 헐씬 쉬어진다. 다음에 나온 그림은 예시 이므로 스스로 새로운 struct구조를 그려보고 주소값을 일일이 적어보면 확실한 이해에 도움이 될것으로 생각한다. 실제 CPU가 엑세스하는 주소값...
다음글에 나오는 변수의 비트는 특별한 한정이 없는 한 32비트 CPU이다. 8비트 CPU가 아니라. 'int는 32비트 정수' 에서 처럼 8비트CPU라는 말이 없다. 이때 적용된 CPU는 32비트 CPU이고, default=32비트 CPU 기준 & Little-endian.
첨부파일 : struct 구조와 linked-list
서론 - struct 구조체의 이해
포인터의 사용에서 가장 복잡하고 어려운 부분이 struct와 결합된 포인터 이다. 그러나 이것은 struct를 사용함에 필수적인 요소이다. 리눅스와 같은 OS의 기본이 struct의 구조에 의해 구성된다고 생각해도 과언은 아니다. struct 가 그만큼 중요한 요소이다.
이 과정을 이해 한다면 C++로의 개념 확장도 쉽다. C++의 object는 결국 struct와 데이터 구조가 같다.
C++ object의 필요와 개념과 동작
이글에서 데이터의 유사성과 차이를 기술 하였다.
struct에 더 자세히 알고 싶다면 다음을 참고 하면 된다.
C언어 구조체 struct 변수
struct 구조체는 컴퓨터가 정보 처리를 할 때 연관된 object에 대해 묶어서 처리하는 방식을 구현하는 구조이다. 따라서 구조체는 하나의 연관된 정보가 뭉쳐 있는 것이다.
예를 사람의 정보를 처리한다면, 이름, 전화번호, ID 등이 하나의 object에 대한 정보가 묶어서 처리하는 것이다. 여기서 묶어서라는 말은 실제로 정보가 정해진 위치에 구조적으로 나열되어 있는 상태를 말한다.
정보가 묶여 덩어리로 나열되어 있고 이것이 struct에 선언된 타입 대로 나열되어 있음을 나타낸다. 이를 이해 하기 다음 두가지를 생각하자.
case 1 :
int name[100]; // 사람 이름
int id[15]; // 주민번호
int mcount; // 지금 처리하는 사람 수
int nbuff[1024]; // 임시 처리 버퍼
int tel[100]; // 전화 번호
int sztel;
int age; // 나이
int main()
{
. . .
}
case 2 :
//////////////////////////////////
// 사람 정보
char name[100]; // 사람 이름
char id[15]; // 주민번호
char tel[100]; // 전화 번호
int age; // 나이
//////////////////////////////////
// 처리 변수
int mcount; // 지금 처리하는 사람 수
char nbuff[1024]; // 임시 처리 버퍼
int sztel;
//////////////////////////////////
// 처리 processor
int main()
{
. . .
}
위의 2가지 프로그램 경우 case1은 사람 정보를 처리할 때 사람에 해당되는 변수가 분산되어 있다. 그러나 case2는 사람의 정보를 연속적으로 나열함으로 써 보기만 해도 변수의 기능을 연관해서 짝기가 쉽고 분석도 쉽다.
위의 간단한 예의 경우 간단한 프로그램이라서 쉽게 분석할 수 있지만 보통의 프로젝트에서 사용하는 코딩의 라인수가 많고 더군다나 모듈별로 파일을 나누어 개발 한다. 이런 경우 이 문제는 복잡함과 혼돈을 야기 한다.
이것의 해결 방안이 struct이다. case2의 object에 대한 정보 처리 묶음을 하나의 struct로 선언 한다.
struct Man {
char name[16]; // 사람 이름
char id[15]; // 주민번호
int age; // 나이
char tel[20]; // 전화 번호
};
이렇게 선언을 하면 사람의 정보를 처리하는 데이터들이 모여 있게 된고 모듈적 개념을 적용하여 쉽게 코딩과 분석과 관리가 이루어 진다.
이렇게 struct을 선언하는 행위 만으로 변수의 저장 공간이 확보 되지 않으므로 이 선언을 가지고 변수를 잡는다.
struct Man man;
struct Man man[100];
이렇게 함으로써 정보 처리을 할 수 있는 공간이 생긴다. 물론 이것은 변수 영역의 메모리에 위치 한다.
이 struct의 변수의 장점은 구조적으로 정해진 방식대로 메모리에 할당 된다데 있다. 어래이가 시작 주소부터 연결되어 나열되듯이 struct도 정보가 정해진 위치에 나열된다.
#include <stdio.h>
struct Man {
char name[16];
char id[15];
int age;
char tel[14];
};
struct Man man = {
"Kim",
"640101-1538575",
45,
"010-234-2343"
};
int main(int argc, char* argv[])
{
printf("name = %s\n", man.name );
printf("id = %s\n", man.id );
printf("age = %d\n", man.age );
printf("tel = %s\n\n", man.tel );
printf("sizeof(man) = %d\n", sizeof(man) );
printf("&man = 0x%08X\n", &man );
printf("&man.name = 0x%08X\n", &man.name );
printf("&man.id = 0x%08X\n", &man.id );
printf("&man.age = 0x%08X\n", &man.age );
printf("&man.tel = 0x%08X\n", &man.tel );
return 0;
}
실행 결과 :
name = Kim
id = 640101-1538575
age = 45
tel = 010-234-2343
sizeof(man) = 52
&man = 0x00403018
&man.name = 0x00403018
&man.id = 0x00403028
&man.age = 0x00403038
&man.tel = 0x0040303C
이 구조는 다음과 같다.
변수 man은 초기값을 갖는 변수이므로 DATA SEGMENT에 할당되고 main이 시작되기 전에 위와 같은 구조로 데이터 존재 한다. 여기서 몇가지 특징을 보면
- struct 시작 주소 부터 위와 같이 정해진 규칙 대로 메모리에 배치되고 이 배치를 기반으로 주소값이 계산 된다.
- struct 구조 안에서 정해진 변수의 길이와 다음 변수와의 여유 공간은 사용하지 않는다. Alignment로 이것은 변수가 한번 엑세스하기 위한 규칙에 따라 배치 된다. char id[15]은 4의 배수가 아니라서 다음 변수 int age와의 빈공간을 하나 넣는다. int age는 정수형이므로 32비트 변수이다. 따라서 이 변수에 값을 쓰거나 읽기 위해서는 기계어 코드 하나로 한번 엑시스로 이루어 진다.
man.age = 30;
이 코드에서 age의 주소값을 결정 하는 과정과 30을 넣는 과정으로 나누어 생각할 수 있는데, 두번째 과정에서 한번 기계어 코드로 30을 넣게 된다.
경우에 따라 다음과 같은 기계어 코드를 30을 넣을 수 있다.
000ee c7 40 20 1e 00 00 00 mov DWORD PTR [eax+32], 30 ; 0000001eH
만약 이 빈공을 사용하지 않는다면
이 경우 age 변수를 주소 배치 0x00403037 ~ 0x40303A까지 한 경우이다. alignment을 사용하지 않는 경우이다. 즉, 주소값이 4로 나누어 떨어지지 않는 변수 이다. 이렇게 되면 CPU의 특성 상 최종적으로 어떤 정수값을 넣을 때, 한번의 기계어로 엑세스를 할 수 없게 된다.
int age = man.age;
처럼 데이터를 읽을 경우 man의 age의 주소값을 계산하여 레지스터에 넣고, 이 레지스터를 사용 한 레지스터 address mode로 읽어 다른 레제스터에 넣는다.
이렇게 age을 읽을 때 최종적인 단계인 데이터 영역을 읽는 과정에서 위의 그림 처럼 alignment를 하지 않는 구조라면
- 0x00403037 번지의 데이터 한 바이트 만을 따로 읽어 레지스터 A에 넣고 : A=45
- 나머지 0x00403038번지 데이터 4바이트를 읽어 레지스터 B에 넣고 : B = 0x30000000 <- 상위 MSB 8비트 '0' 값이 읽힘
- 다시 레지스터 B을 왼쪽으로 4비트 쉬프트 비트연산하고 : B=0x00000000
- 레지스터 A와 B을 ALU을 통해 OR 조작 해야 한다. A | B => A = 45
한개의 명령으로 끝 날일을 4개의 명령과 2개의 레지스터를 사용 하여야 한다.
따라서 이렇게 복잡한 명령으로 엑세스 하지 않기 위해, 32비트 변수는 4로 나누어 떨어지는 주소값을 16비트 변수는 2로 나누어 떨어지는 변수 공간을 사용 한다.
마지막에 tel변수 역시 14바이트라서 2바이트가 남아 사용하지 않지만 이것은 다른 변수에 할당하지 않는다. 결국 위의 struct는 3바이트를 사용하지 않는다.
Alignment와 관련하여 sizeof(man)은 사용하지 않는 공간 3바이트 까지 합친 것이다.
실제 데이터 영역은
16 + 15 + 4 + 14 = 49바이트만을 사용하는 것인데,
sizeof(man)=52로 49(실제데이터변수) + 3(Alignment로 추가된 사용하지 않는공간) = 52.
8비트CPU의 struct alignment
8비트CPU의 프로그램 예
struct Man {
char *name;
char sex;
int age;
long int code;
char tel[5];
};
char name[] = "Hong";
struct Man man = {
name,
'F', 45,
0x01020304,
"3275"
};
이 예에서 Man의 구조는
Intel 계열
Motolora 계열
8비트 CPU의 struct는 정수형 변수의 alignment 문제가 없다. 거의 대부분은 CPU가 한번에 8비트 단위로 읽을 수 밖에 없으니...
따라서 int는 2번의 기계어 코드와 2개의 레지스터가 사용 되어 진다. 따라서 8비트 CPU은 32비트에 비해 상당히 느리다. 뿐만 아니라 대부분의 int는 16비트 변수 길이를 갖는다. 16비트가 부족할 때는 long int 32비트로 하면 되고, 그런데 32비트에서 long int을 붙이면 대부분 32비트.
변수 영역을 넘어선 엑세스
그런데 C는 기본적으로 변수영역을 넘는 처리는 해결 방법이 없다. 예를 들어 id는 15바이트 문자 변수인데, 이것을 넘으면 처리 하는 과정에는 문제가 없으나 오버하면 다음 변수 age 변수가 문제가 생긴다.
strcpy(man.id, "12345678901234567");
을 실행하면 정해진 상수 문자가 끝을 나타내는 0을 포함하여 18바이트가 복사되어 바뀐다. 그런데 id는 15바이트에 alignment 1바이트를 추가하면 16바이트인데, 이것을 넘어가면 바로 int age 변수 영역이 된다. 변수 침범은 C에서 방어하지 못한다.
왜 C는 기계어와 고급어의 중간 언어이기 때문에 복잡한 처리는 하지 않는다. 바로 기계어로 바꾸어 처리 된다.
struct 안의 빨간색 부분이 strcpy 함수에 의해 복사 된 것인데, id의 영역을 넘어 int age값이 변하고 말았다.
만약
printf("%d", man.age);
을 하면 '7'의 ASCII 코드값 55가 출력 된다.
만약
strcpy(man.id, "123456789012345");
처럼 16바이트를 복사하면 age 변수와는 상관없이 alignment 영역에 0이 써지고 C의 구조상 아무 문제 없다.
이것은 struct을 조작할 때, man의 주소값을 시작 번지로 하여 구조체 내의 변수의 위치의 상대적 거리를 계산 한다. 그러면 내부의 변수 메모리 위치가 나오고 여기에 조작 하는 것이다.
man.age : man의 시작 주소 0x00403018 + age의 상대치 (16+15+1) => 0x00403038 => 단지 이 주소값을 말함.
결국
man.age = 10;
이라는 이야기는
* ((int*) 0x00403038 ) = 10;
와 같다.
그렇다면
struct Man {
char name[16];
char id[15];
char sex; int age;
char tel[14];
};
struct Man man = {
"Kim",
"640101-1538575",
'M',
45,
"010-234-2343"
};
한 바이트 짜리를 삽입하면,
name = Kim
id = 640101-1538575
sex = M
age = 45
tel = 010-234-2343
sizeof(man) = 52
&man = 0x00403018
&man.name = 0x00403018
&man.id = 0x00403028
&man.sex = 0x00403037
&man.age = 0x00403038
&man.tel = 0x0040303C
아무 문제없이 실행 된다. sex 변수가 id와 age 사이의 alignmnet 영역으로 배치되어 사용되므로 sizeof(man)=52로 늘어나지 않는다.
정리하면
- struct 구조체는 메모리 상에 시작 주소부터 정해진 규칙에 따라 배치 된다.
- CPU 입장에서 보면 하나의 주소값을 변환하여 처리 한다.
struct 포인트
지금 까지 struct 포인터 랑 상관없는 이야기를 했다. 그러나 struct 내의 변수에 엑세스 하려면 모두 시작주소를 베이스로한 주소값일 뿐이다. 이 개념을 알면 포인트는 어렵지 않다.
struct Man man = { . . . };
struct Man *pman;
int main()
{
...
pman = &man;
printf("&pman = 0x%08X\n", &pman );
printf("pman = 0x%08X\n", pman );
printf("pman->age = %d\n", pman->age );
if (pman->age >= 19)
printf("\n 성인 이시내요.\n");
return 0;
}
name = Kim
id = 640101-1538575
sex = M
age = 45
tel = 010-234-2343
sizeof(man) = 52
&man = 0x00403018
&man.name = 0x00403018
&man.id = 0x00403028
&man.sex = 0x00403037
&man.age = 0x00403038
&man.tel = 0x0040303C
&pman = 0x004033A4
pman = 0x00403018
pman->age = 45
성인 이시내요.
pman = &man;
이 과정은 man의 시작 주소 0x00403018을 pman의 변수에 넣는다. 이것은 단순히 pman 변수에 CPU의 메모리 주소값을 넣는 것이다. struct 이든 뭐든 포인터는 모두 실제 주소값을 저장하면 되는 것이다.
pman->age = 30;
을 실행하기 위해
struct의 내의 변수의 위치를 계산 한다.
pman->age
라는 말은
pman이 가지고 있는 struct 베이스로 부터 age의 상대 주소값 32을 더하면 age가 있는 위치가 된다.
pman의 시작 주소값 0x00403018 + 32 => 0x00403038
이 주소값에 값을 넣으면 된다.
자 이 주소값 0x00403038에 30을 넣으면 된다.
; pman->age = 30;
000e9 a1 00 00 00 00 mov eax, DWORD PTR _pman ; pman
000ee c7 40 20 1e 00 00 00 mov DWORD PTR [eax+32], 30 ; 0000001eH
결국 변수에 값을 넣는다는 것은 주소값을 알면 그 주소에 기계어 코드로 넣으면 된다.
포인트냐 아니냐는 주소값을 어떻게 다루는가의 문제이다.
man.age : man이 고정된 주소값(변경이 불가능)으로 한 베이스 주소값 + age의 struct 내의 상대값
pman->age : pman은 struct의 베이스 주소값( 변경이 가능한 주소값) + age의 struct 내의 상대값
다시 말하면 man과 pman은 고정된 주소값이냐 주소값을 변경할 수 있는냐 이다.
만약 pman이 없다면 모든 고정된 주소값을 사용할 수 밖에 없다. 고정된 주소값이라는 것은 man 변수 처럼 변수를 선언하고 이것의 주소값을 가지고 사용하는 것이다. 지역변수라면 스택에 전역변수라면 변수 공간에. 그런데 고정된 변수는 사용 상 몇가지 한계가 있다. 변수를 사용하기 위해서는 전역변수에 잡았다면 원하는 갯수를 정확히 알아야 한다.
int szMan = 10;
struct Man man[szMan];
이것 처럼은 선언이 불가능 하다.
지역변수라면 선언을 하고 사용한 후 블럭이 끝나면 삭제 된다.
동적인 할당이 불가능 해 진다. 다음과 같은 구조를 생각해 보자.
struct Man *pman;
struct Man *makeMan()
{
pman = malloc( sizeof(struct Man) );
memset(pman, 0 , sizeof(struct Man) );
return pman;
}
이 과정은 필요한 변수 공간을 동적으로 잡고 이것을 사용 한 후, free 함수로 삭제 할 수 있다.
기타 몇가지 포인터가 가지는 장점이 있다.
struct 포인터 표기법
C프로그램의 초보자는 포인터 자체가 어려운데다가 struct와 결합된 포인터가 어려움을 가중 시키다.
점을 붙여야 하는지 화살표인지???
기본적인 개념은 현재 변수가 포인터 인가 아닌가가 화살표인가 점인가를 결정하면 쉽다.
여러가지 포인터의 개념을 알아보기 위해 복잡한 프로그램을 작성해 본다. 그리고 최대화 여러가지 예를 통해 struct 파악...
*** stmember.h ******************************
#ifndef _STMEMBER_H
# define _STMEMBER_H
struct Car {
char name[8];
int mkyear;
char maker[8];
int engine;
int color;
};
struct Man {
char name[8];
char sex;
int age;
char tel[8];
};
struct Family {
struct Family *next;
char rel[8];
struct Man *man;
int pos;
};
struct Member {
struct Member *next;
char id[8];
struct Man man;
char *pos;
short int year;
short int dtype;
struct Car *car;
void *data;
int szdata;
};
////////////////////////////////
// Data type
// Point Data Exist Type
#define DEXISTTYPE_MASK 0xFF00
#define DEXISTTYPE_CAR_FIX 0x0100
#define DEXISTTYPE_CAR_MALLOC 0x0200
#define DEXISTTYPE_DATA_FIX 0x0400
#define DEXISTTYPE_DATA_MALLOC 0x0800
// Data Type
#define DTYPE_NULL 0x0000
#define DTYPE_FAMILY 0x0001
#define DTYPE_INT 0x0002
#define DTYPE_STRING 0x0003
////////////////////////////////
// Function
// Init. & List Member
struct Member* initstruct();
struct Member* LinkList(struct Member *pmem);
struct Member* getRootList();
// Display struct
void printstruct();
struct Member* printMember(struct Member *pmem);
// Member List
struct Member* nextMember(short int dt, int length);
// Delete Member
void deleteMemberCarData(struct Member *pmem);
void deleteAllMember();
#endif
*** stmember.c ******************************
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "stmember.h"
char *mJosunArmy[] = {
// 수사 : 수군 절도사의 준말로 각 도의 수군을 지휘.
// 임란 당시 삼도 수군 통제사는 전라 좌수사, 전라 우수사, 경상 좌수사, 경상 우수사, 충청수사를 지휘.
"통제사",
"첨사", "만호" // 수군 절도사의 실질적인 부하 장수.
"조방장", // 일종의 참모 역할을 하는 장수.
"군관", // 첨사, 만호의 부하 장수.
"도원수", // 유사시 통솔권으로 자지고 있는 장수
"목사", "부사", // 성주
NULL
};
char *mArea[] = {
"충청",
"순천",
NULL
};
struct Car mcar = {
"판옥선1", 1598, "좌수영", 2000,
0x345577
};
struct Man msunMyeon = {
"이면", 'M', 12, "서신1"
};
struct Family msun = {
NULL,
"아들",
&msunMyeon,
0
};
struct Member memCeo = {
NULL,
"34556",
{
"이순신", 'M', 45, "파발마1"
},
mJosunArmy[0], // "통제사",
1597,
DEXISTTYPE_CAR_FIX | DEXISTTYPE_DATA_FIX | DTYPE_FAMILY,
&mcar,
&msun,
sizeof(msun)
};
struct Member *pmemb;
struct Member *ptail;
struct Member* initstruct()
{
pmemb = &memCeo;
ptail = pmemb;
return pmemb;
}
struct Member* getRootList()
{
return pmemb;
}
struct Member* LinkList(struct Member *pmem)
{
ptail->next = pmem;
ptail = pmem;
return pmemb;
}
void printstruct()
{
printf("sizeof(memCeo) = %d\n", sizeof(memCeo) );
printf("sizeof(mcar) = %d\n", sizeof(mcar) );
printf("sizeof(Man) = %d\n", sizeof(Man) );
printf("sizeof(Family) = %d\n", sizeof(Family) );
printf("sizeof(pmemb) = %d\n", sizeof(pmemb) );
printf("\n" );
printf("&memCeo = 0x%08X\n", &memCeo);
printf("&mcar = 0x%08X\n", &mcar);
printf("&msunMyeon = 0x%08X\n",&msunMyeon);
printf("&msun = 0x%08X\n", &msun);
printf("&pmemb = 0x%08X\n", &pmemb);
printf("\n" );
printf("&pmemb->next = 0x%08X\n", &pmemb->next);
printf("&pmemb->id = 0x%08X\n", &pmemb->id);
printf("&pmemb->pmemb.man = 0x%08X\n", &pmemb->man);
printf("&pmemb->year = 0x%08X\n", &pmemb->year);
printf("&pmemb->pos = 0x%08X\n", &pmemb->pos);
printf("&pmemb->car = 0x%08X\n", &pmemb->car);
printf("&pmemb->data = 0x%08X\n", &pmemb->data);
printf("\n" );
printf("mJosunArmy = 0x%08X\n", mJosunArmy );
for (int cnt = 0;mJosunArmy[cnt];cnt++)
printf("0x%08X = %s\n", mJosunArmy[cnt], mJosunArmy[cnt] );
printf("\n" );
}
struct Member* printMember(struct Member *pmem)
{
printf("pmem = 0x%08X\n", pmem);
printf("pmem->next = 0x%08X\n", pmem->next);
printf("pmem->id = %s\n", pmem->id);
printf("pmem->man.name = %s\n", pmem->man.name);
printf("pmem->man.sex = %c\n", pmem->man.sex);
printf("pmem->man.age = %d\n", pmem->man.age);
printf("pmem->man.tel = %s\n", pmem->man.tel);
printf("pmem->pos = 0x%08X [%s]\n", pmem->pos, pmem->pos);
printf("pmem->year = 0x%08X [%d]\n", &pmem->year, pmem->year);
printf("pmem->dtype = 0x%08X [0x%04X]\n", &pmem->dtype, pmem->dtype);
printf("pmem->car = 0x%08X\n", pmem->car);
printf("pmem->car->name = %s\n", pmem->car->name);
printf("pmem->car->mkyear = %d\n", pmem->car->mkyear);
printf("pmem->car->maker = %s\n", pmem->car->maker);
printf("pmem->car->engine = %d\n", pmem->car->engine);
printf("pmem->car->color = 0x%X\n", pmem->car->color);
printf("pmem->data = 0x%08X\n", pmem->data);
printf("pmem->szdata = %d\n", pmem->szdata);
printf("\n" );
if (pmem->data) {
switch (pmem->dtype & ~DEXISTTYPE_MASK) {
case DTYPE_FAMILY :
{
struct Family *pfam;
pfam = (struct Family *) pmem->data;
printf("Data type : Family\n");
printf("pfam->rel = %s\n", pfam->rel);
printf("pfam->man->name = %s\n", pfam->man->name);
printf("pfam->man->sex = %c\n", pfam->man->sex);
printf("pfam->man->age = %d\n", pfam->man->age);
printf("pfam->man->tel = %s\n", pfam->man->tel);
}
break;
case DTYPE_INT :
{
int *pdata;
pdata = (int*) pmem->data;
printf("Data type : integer[]\n");
printf("pmem->data size = %d\n", pmem->szdata);
for (int cnt = 0;cnt < pmem->szdata;cnt++)
printf("%d\t", *pdata++ );
printf("\n");
}
break;
case DTYPE_STRING :
printf("Data type : string\n");
printf("pmem->data = %s\n", (char*) pmem->data);
break;
}
}
return pmem;
}
struct Member* nextMember(short int dt, int length)
{
struct Member *pmem;
pmem = (struct Member *) malloc( sizeof(struct Member) );
memset((void*) pmem, 0, sizeof(struct Member) );
pmem->car = (struct Car *) malloc( sizeof(struct Car) );
memset((void*) pmem->car, 0, sizeof(struct Car) );
pmem->dtype = DEXISTTYPE_CAR_MALLOC;
if (dt == DTYPE_FAMILY) {
pmem->dtype |= DEXISTTYPE_DATA_MALLOC
| DTYPE_FAMILY;
struct Family *pfam;
pfam = (struct Family *) malloc( sizeof(struct Family) );
pmem->data = (void*) pfam;
pmem->szdata = sizeof(struct Family);
return pmem;
}
if (! length) {
pmem->data = NULL;
pmem->szdata = 0;
return pmem;
}
switch ( dt ) {
case DTYPE_INT :
{
pmem->dtype |= DEXISTTYPE_DATA_MALLOC
| DTYPE_INT;
pmem->data = (void*) malloc( length * sizeof(int) );
pmem->szdata = length;
}
break;
case DTYPE_STRING :
{
pmem->dtype |= DEXISTTYPE_DATA_MALLOC
| DTYPE_STRING;
char *pstr;
pstr = (char *) malloc( length+4 );
pmem->data = (void*) pstr;
*pstr = 0;
pmem->szdata = length;
}
break;
}
return pmem;
}
void deleteMemberCarData(struct Member *pmem)
{
if (pmem->dtype & DEXISTTYPE_CAR_MALLOC) {
free( pmem->car );
pmem->car = NULL;
}
if (pmem->dtype & DEXISTTYPE_DATA_MALLOC) {
free( pmem->data );
pmem->dtype = 0;
pmem->data = NULL;
pmem->szdata = 0;
}
}
void deleteAllMember()
{
struct Member *pmem;
struct Member *pnextmem;
pmem = pmemb;
deleteMemberCarData(pmem);
pmem = pmem->next;
while (pmem) {
deleteMemberCarData(pmem);
pnextmem = pmem->next;
free ( pmem );
pmem = pnextmem;
}
}
*** main.c ******************************
#include <stdio.h>
#include <string.h>
#include "stmember.h"
int main(int argc, char* argv[])
{
struct Member* pmem = initstruct();
printf("*** 포인터의 구조 : addres *****\n");
printstruct();
printf("\n*** 처음 *********************\n");
printMember(pmem);
printf("\n*** 다음 *********************\n");
pmem = nextMember(DTYPE_STRING, 512);
LinkList(pmem);
struct Member* pmlist = getRootList();
printf("List Member : ");
while (pmlist) {
printf("0x%08X ", pmlist);
pmlist = pmlist->next;
if (pmlist)
printf(" -> ");
else
printf(" -> 0x%08X\n", pmlist);
}
printf("current pmem = 0x%08X\n\n", pmem);
// Member의 정보를 설정 한다.
strcpy(pmem->id, "01233");
strcpy(pmem->man.name, "권준");
pmem->man.sex = 'M';
pmem->man.age = 28;
strcpy(pmem->man.tel, "파발마2");
pmem->pos = "순천부사";
pmem->year = 1537;
// Car의 정보를 설정 한다.
strcpy(pmem->car->name, "거북선1");
pmem->car->mkyear = 1956;
strcpy(pmem->car->maker, "좌수영");
pmem->car->engine = 400;
pmem->car->color = 0x004467;
// Data의 정보를 설정 한다.
char *pstr = (char *) pmem->data;
strcpy(pstr, "전략가");
printMember(pmem);
deleteAllMember();
printf("\nEnd...");
return 0;
}
실행 결과 :
*** 포인터의 구조 : addres *****
sizeof(memCeo) = 60
sizeof(mcar) = 28
sizeof(Man) = 24
sizeof(Family) = 20
sizeof(pmemb) = 4
&memCeo = 0x0040308C
&mcar = 0x00403044
&msunMyeon = 0x00403060
&msun = 0x00403078
&pmemb = 0x0040341C
&pmemb->next = 0x0040308C
&pmemb->id = 0x00403090
&pmemb->pmemb.man = 0x00403098
&pmemb->year = 0x004030B4
&pmemb->pos = 0x004030B0
&pmemb->car = 0x004030B8
&pmemb->data = 0x004030C0
mJosunArmy = 0x00403018
0x00402148 = 통제사
0x00402140 = 첨사
0x00402134 = 만호조방장 -> 프로그램에서 "만호" 다음에 컴마가 빠져내...
0x0040212C = 군관
0x00402124 = 도원수
0x0040211C = 목사
0x00402114 = 부사
*** 처음 *********************
pmem = 0x0040308C
pmem->next = 0x00000000
pmem->id = 34556
pmem->man.name = 이순신
pmem->man.sex = M
pmem->man.age = 45
pmem->man.tel = 파발마1
pmem->pos = 0x00402148 [통제사]
pmem->year = 0x004030B4 [1597]
pmem->dtype = 0x004030B6 [0x0501]
pmem->car = 0x00403044
pmem->car->name = 판옥선1
pmem->car->mkyear = 1598
pmem->car->maker = 좌수영
pmem->car->engine = 2000 --> 가만히 있어봐, 차가 2000cc 이지 격군 2000명이면 너무 많은거 같음. 엉터리 데이터. 어째든...
pmem->car->color = 0x345577 --> RGB, 판옥선이 이 색깔 맞아? 나도 모름.
pmem->data = 0x00403078
pmem->szdata = 20
Data type : Family
pfam->rel = 아들
pfam->man->name = 이면
pfam->man->sex = M
pfam->man->age = 12
pfam->man->tel = 서신1
*** 다음 *********************
List Member : 0x0040308C -> 0x00393900 -> 0x00000000
current pmem = 0x00393900
pmem = 0x00393900
pmem->next = 0x00000000
pmem->id = 01233
pmem->man.name = 권준
pmem->man.sex = M
pmem->man.age = 28
pmem->man.tel = 파발마2
pmem->pos = 0x0040263C [순천부사]
pmem->year = 0x00393928 [1537]
pmem->dtype = 0x0039392A [0x0A03]
pmem->car = 0x00393950
pmem->car->name = 거북선1 -> 원래 거북선은 다른 사람이 지휘해는데, 생각이 안남.
pmem->car->mkyear = 1956
pmem->car->maker = 좌수영
pmem->car->engine = 400
pmem->car->color = 0x4467
pmem->data = 0x00395C60
pmem->szdata = 512
Data type : string
pmem->data = 전략가
End...
전체 실행 구조를 도식화 하면
포인터에 익숙하지 않는 개발자라면 위의 그림 만 보아도 머리 아프다. 디양한 포인터를 사용하여 머리를 복잡하게 한다. 그렇다고 포기할 수는 없는 일이다. 프로그램을 하는 한... 복잡할 수록 원리를 생각하고 원칙대로 따라가면 쉬어 진다. 우선 포현법 부터 생각 한다.
CPU가 어떤 타입의 데이터값을 엑세스하기 위해서는 일단은 주소값을 알아야 하고, 데이터가 있어야 한다.
예를 주어 정수값 45을 어느 변수에 넣는다는 이야기는 우선 age 변수가 위치 주소값 부터 알아 내야 한다. 그리고는 결정된 주소에 정수값을 넣으면 된다.
이 이야기에서 결정되어야 하는 것은
- 데이터를 쓰거나 읽기 위해 주소값 결정
- 결정에 주소값에 해당 타입의 데이터를 엑세스 한다.
어떤 주소에 데이터를 엑세스하기 위해서는 데이터의 길이가 결정 되어야 한다. 8, 16, 32, 64비트의 데이터 엑세스 단위가 결정 되어야 한다. 이 데이터 길이는 변수의 형태로 결정 한다.
char - 8비트
short int - 16비트
int - 32비트
로 길이가 결정되어 있고, 이 길이는 기계어 코드에 결저되어 있어 컴파일러가 적당한 기계어로 바꾸는 것이다.
지겹고 수없이 이야기 한 주소값이 결정 되는 과정을 다시 한번 생각한다. 포인터의 전부이기 때문이다.
memCeo.age = 45;
pmemb->age = 45;
이 코드는 CPU의 입장에서는 어떤 변수이건 간에 정수값을 메모리에 저장하면 된다. 여기서 나이라는 개념은 사람이 생각하는 것이지 CPU는 이것이 어떤 의미가 있는지는 모르고 중요하지도 않다. 단지 데이터의 길이가 중요할 뿐이다.
우선 주소값을 결정하는 과정이 필요하다.
memCeo.age = 45;
memCeo변수는 전역변수로 메모리에 링커에 의해 할당되어 있는 상태이다. 따라서 해당 프로그램이 종료할 때 까지는 절대로 주소값이 변하는 일이 없다. 그래서 이것이 기계어 코드에 고정되어져 있다.
위의 예에서
&memCeo = 0x0040308C
이므로 0x0040308C번지이다.
이 고정된 상대값으로 부터 age의 offset 주소값을 더하면, 최종 쓰기 위한 주소값이 되는 것이다.
mov eax, OFFSET 0x0040308C - immediately mode
mov DWORD PTR (eax+24), 45
이것 처럼 3가지 요소
- 엑세스 대상 주소 : 0x0040308C + 0x18 => 0x004030A4 : EAX레지스터 사용, 상황에 따라
- 엑세스 데이터 : 45
- 엑세스 단위 : 32비트 - mov DWORD PTR (eax+24), 45
여기서 포인터가 아닌 변수는 고정된 주소값을 사용 했다는 이야기 이다.
이에 비해
pmemb->age = 45;
여기에서 엑세스 하기 위한 주소값을 결정하는 과정이 다르다.
&pmemb = 0x0040341C
처럼 이 변수가 존재하는 위치는 0x0040341C 이다. 이것은 struct Member 데이터가 위치를 지정하는 것이다. 즉, 메모리의 위치를 CPU에 따라 정해진 길이 만큼 가지고 있는 것이다.
번지 0x0040341는 struct 데이터가 있는 위치가 아니고 존재하는 위치의 주소값 이기 때문에,이 번지에서 읽으면 struct 주소값이 된다.이 때서야 베이스 주소가 결정된 것이다.
mov eax, DWORD PTR 0x0040341C ==> EAX = 0x0040308C: direct address mode
mov DWORD PTR (eax+24), 45
여기서의 3가지 요소
- 엑세스 대상 주소 :
1. 0x0040341C에 있는 주소값을 읽는다.
2. 0x0040308C + 0x18 => 0x004030A4
- 엑세스 데이터 : 45
- 엑세스 단위 : 32비트 - mov DWORD PTR (eax+24), 45
포인터 변수가 아닌 경우와 의 차이는 대상 주소가 다른 메모리(0x0040341C)에 존재하는 것이므로 이로 부터 한번 더 주소값을 읽어야 한다는 것이다. 한 단계의 과정이 더 필요하다.
그러나 미 명령 하나만 보다 처리 시작이 더 많이 필요 한것처럼 느낄수 있으나 대부분은 포인터 처리가 효율적이다.
struct Member memData[100];
struct Member *pmemb;
case 1:
for (int cnt = 0;cnt < 100;cnt++) {
memData[cnt].age = 10;
...
}
case 2 :
pmemb = memData;
for (int cnt = 0;cnt < 100;cnt++, pmemb++) {
pmemb ->age = 10;
...
}
이 두 경우 속도 차이는 분명 포인터가 효율적이다.
이제 포인터의 초기 과정을 예를 보면
; pmemb = &memCeo;
00000 b8 00 00 00 00 mov eax, OFFSET memCeo ; memCeo의 데이터 존재 위치 0x0040308C
00005 a3 00 00 00 00 mov DWORD PTR pmemb, eax ; pmemb의 위치 0x0040341C
이 과정은 모든 포인터의 주소값 저장 과정과 기계어 코드가 같다.
char buff[100];
char *pbuff = buff;
int *pdata = idata;
등 모든 포인터의 처리 방식이 같다는 거.
왜 모든 포인터 값은 해당 데이터가 있는 주소값이므로, 그리고 CPU가 결정 되면 주소체계는 결정 되어 있으므로...
계속 반복하여 100번 정도하면 머리속에 박힐 것 같으니...
struct에서 프로그램 코드이 표현을 생각 한다.
memCeo.age = 45;
pmemb->age = 45;
어떤 때는 점이고 어느 때는 화살표인가?
원칙 :
앞에 있는 변수가 포인터가 아니면 점
앞에 있는 변수가 포인터 이면 화살표
memCeo.car->name
- memCeo. : memCeo 변수가 포인터가 아니므로 뒤에 뭐가 오든 점- car-> : car 변수가 포인터 변수이기 때문에 화살표
pmamb->car->mkyear
- pmamb-> : pmanb가 포인터 변수 이므로 화살표- car-> : car 변수가 포인터 변수이기 때문에 화살표
pmamb->man.name
- pmamb-> : pmanb가 포인터 변수 이므로 화살표- man. : man 변수가 포인터가 아니므로 점
이것은 위의 그림의 struct 에서 보면 포인터 변수는 모두 4바이트 주소값으로 표시된 부분은 모두 화살표 이다.
계속 연결되어 위치를 결정할 때, 원칙은 간단하다. 그런데 이 원칙 하나를 몰라서 고민 하면 안돼지.....
pmamb->man.name -> man의 name 변수 주소값, char []에서 이름 만 쓰면 주소값을 나타낸다. 이것은 char 변수의 규칙이다.
pmamb->man.name[0] -> name의 첫 번째 char (한 바이트 문자)
중간 타입 변환 하기
pmem = &memCeo;
char* pname = ( (struct Family*)pmem->data )->man->name;
printf("((struct Family*)pmem->data)->man->name = %s\n", pname);
여기서 data 변수는 void* 이므로 다음 단계로 진행 할 수 없다. 따라서 다음 포인터 부터 data가 어떤 변수인지를 중간에 정의 하면 된다.
화살표 이든 점이든 계속 연결 된 변수는 차례대로 해당 위치에 주소값을 연속해서 결정해 가는 것 뿐이다.
연속된 변수는 차례대로 진행 되면서 주소값을 결정하는 것은 다음과 같은 과정으로 주소값이 결정 된다.
실행과정 :
1. pmem :
pmem의 변수에서 주소값을 읽는다 : (0x0040341C) => 결과 : 0x0040308C
2. pmem->data :
data가 포인터 변수 이므로 이 주소값을 읽는다.
0x0040308C+data의 offset값 = 0x0040308C+ 0x30 = 0x004030BC로 부터 포인터 주소값을 읽는다.
=> 결과 0x00403078
3. ((struct Family*)pmem->data)->man :
2에서 결정 된 주소가 데이터가 있는 위치는 확실한데, 어떤 데이터를 읽어 올지를 모른다.
여기서 struct Familiy구조가 예에서 주어진 것이므로 타입변환 한다.
(struct Family*) 0x00403078로 부터 구조가 결정으므로 이 struct 구조로 man의 offset값으로 부터 포인터 값을 읽는다.
0x0040307 + 0x0C = 0x00403084로 부터 주소값을 읽음 => 결과 : 0x00403060
4. ((struct Family*)pmem->data)->man->name :
다시 struct Man의 주고에서 name의 위치를 계산 한다.
0x00403060 + 0x0000 = 0x00403060 이 주소가 이름을 나타내는 스트링의 주소값이다.
struct 연결하기 위한 포인터
다음 멤버를 만들고 연결하는 방법은 포인터를 사용하여 관리 할 수 있다. '이순신'이 고정된 변수에 고정된 값을 사용 했다면 '권준'은 동적 메모리를 사용한 방법이다.
이 struct 변수를 malloc()함수나 new로 만들고 이를 포인터로 연결하여 관리할 수 있다. 이렇게 되면 필요한 멤버의 자료를 처리하고 끝나면 버리면 된다.
struct Member *next; 변수를 사용하여 다음 멤버들을 연결하는 과정인데 이것은 간단히 연결하려는 다음 데이터 영역의 주소값을 넣으면 된다.
'이순신'의 next 변수를 이용하여 다음 멤버의 주소를 넣어 연결하는 함수는
memCeo.next = (struct Member *) malloc( sizeof(struct Member));
이것에서 malloc에서 만들어진 것은 heap영역에서 원하는 사이즈의 바이트를 잡고 포인터 값을 준다. 이 포인터 역시 32비트 주소값이면 되므로 이것은 next 변수에 넣으므로써 다음 멤버의 위치를 파악할 수 있다.
struct Member *pmem = &memCeo; ---> 이순신
다음 멤버를 찾으려면
pmem = pmem->next;
하면 다음 멤버의 위치를 알 수 있어 이런 경우 '권준' 데이터의 위치가 된다.
pmem : 0x0040308C -> 0x00393900
그런데 '권준'에서 다시 '이순신'으로 가려면 이 변수만을 가지고는 찾을 수 없다.
다시 처음 부터 해당 멤버를 찾아야만 한다.
따라서 역방향으로도 진행이 가능하게 하려면 포인터 변수를 하나 더 사용하면 된다.
struct Member {
struct Member *next;
struct Member *prev;
. . .
};
두 struct간의 link-list을 만들 때, 가장 간단한 방식이다.
원하는 위치에서 prev변수를 사용하여 역으로도 쉽게 다음 멤버를 찾을 수 있게 된다.
처음에 이 struct 구조를 볼 때, 이해가 되지 않았던 부분이 바로 포인터 변수가 자체의 Member 와 같은 구조가 혼돈 스럽게 했다.
struct Member {
struct Member *next;
struct Member *prev;
char id[8];
struct Man man;
char *pos;
short int year;
short int dtype;
struct Car *car;
void *data;
int szdata;
};
같은 이름도 결국은 포인터는 다음 멤버의 주소값이라는거, 즉 주소값이 들어가는 변수이다.
그리고 처음과 끝을 나타내는 변수가 있으면 쉽게 시작과 끝을 찾을 수 있다.
struct Member *rootmemb;
struct Member *tailmemb;
struct Member *rootmemb;
struct Member *tailmemb;
struct Member *getroot() { return rootmemb; }
struct Member *gettail() { return tailmemb; }
void init_linkedlist()
{
rootmemb = NULL;
tailmemb= NULL;
}
struct Member *inserttail( struct Member *pmem) // Linked-List의 끝에 연결 하기 - tail
{
if (rootmemb == NULL) {
rootmemb = pmem;
tailmemb= pmem;
pmem->next = NULL;
pmem->prev = NULL;
return pmem;
}
tailmemb->next = pmem;
pmem->next = NULL;
pmem->prev = tailmemb;
tailmemb = pmem;
return pmem;
}
struct Member *deletelist( struct Member *pmem)
{
if (pmem->prev == NULL) {
rootmemb = pmem->next;
} else {
pmem->prev->next = pmem->next ;
}
if (pmem->next == NULL) {
tailmemb = pmem->prev ;
} else {
pmem->next->prev = pmem->prev;
}
pmem->next = NULL;
pmem->prev = NULL;
return pmem;
}
void printlist()
{
struct Member *pmem;
pmem = getroot();
for (;pmem; pmem=pmem->next) {
printf("이름 : %s\n", pmem->man.name);
// . .
}
}
*** main.c:
struct Member *pmem1;
struct Member *pmem2;
struct Member *pmem3;
int main(int argc, char**argv, char**env)
{
struct Member *pmem;
init_linkedlist();
pmem1 = pmem = (struct Member *) malloc( sizeof(struct Member)); // 원하는 멤버를 만든다.
// struct의 값들을 설정 한다.
strcpy(pmem->man.name, "이순신");
// ...
inserttail( pmem); // Linked-List의 끝에 연결 한다.
pmem2 = pmem = (struct Member *) malloc( sizeof(struct Member)); //새로운 멤버를 만든다.
// struct의 값들을 설정 한다.
strcpy(pmem->man.name, "권준");
// ...
inserttail( pmem);
// Linked-List 사용하기
pmem3 = pmem = (struct Member *) malloc( sizeof(struct Member)); //새로운 멤버를 만든다.
// struct의 값들을 설정 한다.
strcpy(pmem->man.name, "정만호");
// ...
inserttail( pmem);
// 원하는 멤버를 만들었으니 이제 이것들을 사용해 본다.
printlist();
// 만약 멤버 사용이 끝났다면 list에서 지워 본다.
printf("Delete 3\n");
deletelist(pmem3 );
printlist();
printf("Delete 1\n");
deletelist(pmem1);
printlist();
printf("Delete 2\n");
deletelist(pmem2);
printlist();
return 0;
}
이 구조를 그림으로 나타내면
이와같은 과정은 일반적인 linked-list을 만들어 보았다. 필요한 알고리즘에 따라 tree니 하는 구조적 프로그램을 포인터를 사용하여 할 수 있다.
다른 struct간의 linked-list로 연결하기
struct 포인터를 사용 한 linked-list는 같은 struct에서만 가능한 것은 아니다. 다르게 선언된 struct에서도 포인터를 지정할 수 있다. 쉬운 방법은 여러가지의 struct 선언을 하고 타입변환을 통해 원하는 프로그램 모듈을 하는 방법이 있다. 각각의 struct에서 링크에 필요한 포인터 만을 앞에 통일화 시키고 나머지 다른 부분의 구조화를 연결하는 방식이다.
이 방법은 다음 글을 참고 한다.
C언어 포인터 개념 III - struct 포인터 2
블로그의 글이 너무 길거나 첨부파일이 많으면 문제가 발생해 나누어 올림. 이 글은 하나로 스펨이 아닌데, naver 너 이러면 안되지...
C++ class 포인터 - 리스트
struct와 C++에서의 구조를 비교해 보면 거의 같다.
이 글에서 제시된 struct 프로그램을 C++로 다시 작성하여 기술하여 비교하였다. 다음과 글을 참조할 수 있다.
C언어 포인터 개념 III - 2_C++ 포인터
포인터의 잇점 다른 예
위의 예에서 고정된 변수를 사용한 포인터와 malloc함수에 의한 동적 메모리를 활용한 방법을 알아 보았다. 또 다른 방법은 다른 타입의 고정 된 메모리 영역으로 부터 sruct을 할당하는 방법이다.
이 때 포인터를 활용하면 다음과 같은 예가 가능하다.
char g_buff[1024];
int main()
{
struct Memeber *pmem;
char *pstr;
pstr = g_buff;;
pmem = (struct Memeber *) pstr;
pstr += sizeof(struct Memeber );
pmem->car = pstr;
pstr += sizeof(struct Car);
pmem->data = (void*) pstr;
pmem->dtype = DEXISTTYPE_CAR_FIX | DEXISTTYPE_DATA_FIX
| DTYPE_STRING;
pmem->szdata = 100;
pmem->next = NULL;
inputMember(pmem);
// A 사람 처리 계속
pstr = g_buff;;
pmem = (struct Memeber *) pstr;
pstr += sizeof(struct Memeber );
pmem->car = pstr;
pstr += sizeof(struct Car);
pmem->data = (void*) pstr;
pmem->dtype = DEXISTTYPE_CAR_FIX | DEXISTTYPE_DATA_FIX
| DTYPE_INT;
pmem->szdata = sizeof(int) * 100;
// B 사람 처리
...
}
이렇게 char 버퍼로 부터 타입을 변환하여 사용 할 수 있다. 동적으로 malloc함수를 사용한 것이므로 free함수를 사용할 필요가 없다.
결국 포인터를 사용한 프로그램은 CPU 입장에서 보면 메모리 주소값을 기반으로 데이터를 처리 하는 것으로 보면, 실제 저장공간은 할당된 메모리의 어느 위치 든 변수 타입에 상관없이 타입 변환 struct로 사용 할 수 있다.
포인터의 void*
예에서 사용한
void *data;
을 사용 할 때, void는 어떤 타입 인지도 모르는데 어떻게 포인터로 사용할 수 있는가?
그런데 포인터 변수는 CPU의 주소체계에 따라 주소의 길이는 결정 되어 있다. 그런데 포인터는 주소값 만을 가지고 처리 하므로 어느 타입 인지를 몰라도 주소값 저장은 가능하다. 타입은 나중에 실제 데이터 값을 저장할 때 결정하는 것이지 어느 위치를 가르키는 것은 상관이 없는 일이기 때문이다. 또한 포인터는 CPU 입장에서 보면 단지 메모리의 주소값을 뿐이다. 이것은 모든 포인터의 실제 데이터가 어떤 것인지는 포인터의 주소값과는 상관없어 진다.
void의 사용은 그래서 어떤 타입이든 모르고 다양한 포인터 값을 수용할 때 사용 할 수 있다. 그러나 void 포인터로 선언된 변수를 가지고 실제 테이터를 엑세스 할 수는 없다.
char g_buff[1024];
void *data;
data = (void*) g_buff;
*data = 10;
data 변수가 가지는 포인터 주소는 실제로 어떤 값이든 저장이 가능한 공간은 존재 한다. 그러나 프로그램 처럼 10을 넣고자 할 때 void 포인터를 사용이 불가능 하다.
CPU가 엑세스 하기 위해서 3가지 요소가 필요하다고 이미 언급 했는데 void 의 경우는 엑세스 처리 단위가 결정이 되지 않는 상태이다. 그러니까 CPU가 8, 16,32 중 어느 길이로 엑세스 할지를 모르는 상황이 되어 버린 것이다.
만약 data 변수가 있는 위치가 0x00403400이고 g_buff가 0x004030500이라면
data = (void*) g_buff;
이 코드는
mov DWORD PTR data, OFFSET g_buff
가 실행되어 현재 주소값은 data 변수에 저장 된 상태라면
이제
*data = 10;
을 실행하기 위해 우선 주소값을 data 변수로 부토 읽어 온다.
mov eax, DWORD PTR 0x00403400 --- 실행 결과 EAX= 0x004030500 -> EAX가 g_buff을 가르키고 있다.
다음 단계인 10을 저장하기 위한 단계가 필요 한데 10이 어떤 길이를 알 수 없다.
만약 data가
int *data; 라면
*data = 10; 이것은 10이 4바이트라는 것을 알기 때문에 컴파일러는 다음과 같이 4바이트 쓰기 명령으로 컴파일 한다.
mov DWORD PTR (eax+24), 10 ---> 만약 4바이트 정수형 10을 넣을 경우
잠시 쉬어가지
그런데 int 변수 선언이 없어도 10 자체 만으로도 정수 4바이트 아닌가? 아니다. 이것은 사람의 머리속의 숙달된 경험적 결론이다. 사람이 많이 10이라는 정수에 익숙하기 때문에 논리적 고려없이 int로 해석한 것이다.
오류.
사람은 오류를 수용하는 정신세계를 가지고 있고, 컴퓨터는 오류를
수용할 수 없어서 AI가 불가능 하다. 사람은 오류를 최소화 하도록 수정하면서 과정 단계마다 결정을 내리고, 그리고 경험적
데이터를 가지고 어떤 때는 결론을 내린다. 부족한 정보를 가지고 과거 경험을 고려하여 결론을 내릴 수 있지만, 컴퓨터는 정확하게
모든 정보가 없이는 결론을 내릴 수 없다. 결국 컴퓨터는 정해진 일 밖에는 할 수 없다. 사람은 정보가 부족해도 결론을 내릴 수
있지만 오류를 어느 정도 포함하고 있다. 이것이 또한 정신 세계를 확장하는 역활도 한다. 아이너리다. 결국 AI을 만들기
위해서는 오류를 시스템적으로 적용하고 이를 기존의 정보와 통합해야 한다. 물론 이렇게 되면 많은 문제가 있을 것이고 영화에서 처럼
컴퓨터가 창조자를 배반하고 심각한 피해를 줄지도 모르지만... 어째든.
그런데 사람은 내가 내린 결론이 정확하고 무조건 맞다고 착각하는 경우가 많다는데 또한 문제이다. 정치, 경제, 철학, 종교... 학문 분야 까지... 미칠 노릇이다.
삼천포는 너무 멀므로 여기까지 하고 다시 C로
char g_buff[1024];
int main() {
void *data = (void*) g_buff;
*data = 10;
*data = 10;
...
}
여기서 10은 char의 10일 수도 있고,short 16비트 숫자
일수도 있고 int 32비트 정수일수도 있고, 또한 주소값 10번지 일수도 있다. 따라서 어느 것이냐는 = 을 왼쪽의 변수에
의해 결정 된다. 10의 타입이 정의 되지 않으면 원쪽의 변수 타입에 의해 결정 된다.
32비트 CPU 기준 :
char num = 10; ---------> 8비트의 정수형 10 - '0000 1010'
unsigned char num = 10;--> 부호없는 8비트의 정수형 10 - '0000 1010'
short num = 10; ---------> 16비트의 정수형 10 - '0000 0000 0000 1010'
int num = 10; ---------> 32비트의 정수형 10 - '0000 0000 0000 0000 0000 0000 0000 1010'
__int64 num = 10; ---------> 64비트의 정수형 10 - '0000 0000 0000 0000 . . . 0000 0000 0000 1010'
char *pnum = 10; ---------> 32비트의 주소값 10 - '0000 0000 0000 0000 0000 0000 0000 1010'
...
int *pnum = 10 ;---------> 32비트의 주소값 10 - '0000 0000 0000 0000 0000 0000 0000 1010'
이렇게 왼쪽의 타입에 따라 10은 자동 결정 된다.
타입이 맞지 않으면
char num = (int) 10; ---> Error : 32비트의 정수형 10 - '0000 0000 0000 0000 0000 0000 0000 1010'을 8비트의 정수형으로 변환하라.
변수에서 타입이 맞지 않으면 type cast 한다.
int inum;
char cnum;
inum = -1;
cnum = (char) inum; ---> 32비트를 정수를 LSB의 8비트 만을 남기고 제거하고 나머지를 넣는다.
1111 1111 1111 1111 1111 1111 1111 1111 => 1111 1111
cnum = -1;
inum = (int) cnum; ---> 8비트 부호있는 정수를 32비트 부호있는 정수로 만들어 넣어라.
1111 1111 => 1111 1111 1111 1111 1111 1111 1111 1111
cnum = 1;
inum = (int) cnum; ---> 8비트 부호있는 정수를 32비트 부호있는 정수로 만들어 넣어라.
000 0001 => 0000 0000 0000 0000 0000 0000 0000 0001
unsigned int inum;
unsigned char cnum;
cnum = 255;
inum = (unsigned int) cnum;---> 8비트 부호없는 정수를 32비트 부호없는 정수로 만들어 넣어라.
1111 1111 => 0000 0000 0000 0000 0000 0000 1111 1111
그러나 10이 정수형 이라는거는 한정 되어 있다. 컴파일러는 분명 float가 아님 정도는 결정되어 있다.
float fnum = 10; ---> 이렇게 되면 10이 정수형이기 때문에 float와는 맞지 않는다.
float fnum = 10.f; --> 여기서 f는 float - 32비트 float-point 형 변수
double fnum = 10.; --> 여기서 10는 double - 64비트 float-point 형 변수
그러나 위의 예 처럼
int idata;
void *data = (void *) &idata;
*data = 10;
지금 void * 이므로 억세스를 위한 길이 결정이 되어 있지 않는 다는 것이 문제인 것이다. 그렇다고 컴파일러 마음대로 결정할 수는 없는 일이다. 이런 경우 컴파일러에세 전송 타입을 알려 주어여 한다.
*(int*) data = 10; ---> 10은 자동으로 int로 결정된다. 32비트 정수형.
이렇게 하면 이제서야 4바이트 메모리 쓰기 임을 컴파일러가 결정할 수 있다.
mov DWORD PTR (eax+24), 10
*(char *) data = 10;
라면 뒤의 10이 1바이트 값임을 알수 있어서
mov BYTE PTR (eax+24), 10
처럼 한바이트 쓰기가 이루어 진다.
정리하면 void 포인트는 엑세스 할 때의 엑세스 단위(8,16,32)을 모르기 때문에 실제값을 넣는 조작은 불가능하다. 그러나 포인터는 CPU에 의해 이미 결정되어 있기 때문에 포인터의 조작은 가능하다.
댓글 없음:
댓글 쓰기