본문으로 바로가기

 

 

다차원 포인터의 실수1

 

매개변수에 포인터 변수를 잘못 사용한 경우

 

함수의 매개 변수로 포인터를 사용할 때 차원 개념을 잘못 적용하면 원하는 값을 반환 받지 못하는 오류가 발생할 수 있다.

 

문제가 발생하는 이유

 

 

동적으로 할당된 주소 값을 포인터 변수에 대입하면 문제 해결?

 

함수의 매개변수로 2차원 포인터 사용하기

 

 

 

다차원 포인터의 실수2

 

 

 

다차원 포인터의 실수3

 

short **pp;            //  2차원 포인터

pp                         //  short **를 의미한다.  (2차원)

*pp                       //  short *를 의미한다.  (1차원)

**pp                     //  short 를 의미한다.  (0차원)

&pp                      //   (3차원)

 

변수를 선언하고 해당 변수에 * 연산자를 사용하면 자신의 차원은 1씩 줄어들고 &를 사용하면 자신의 차원이 1씩 증가한다.

 

int a_loc;

short **ptr = (short **)&a_loc; // 아래 선언한 것과 같은 형태이다.

 

int a_loc;

short **pptr;

pptr = (short **)&a_loc;

 

int a_loc;

short **pptr;

**pptr = (short **)&a_loc; // 다음과 같이 명령어를 입력하면 차원이 맞지 않아 오류가 발생한다.

 

여러 개의 1차원 포인터를 정적으로 할당하기

 

short *p[100];    //  short * 형식의 1차원 포인터 100개를 선언한다.

배열의 요소가 100개, 각 요소의 크기는 4바이트이다.

할당된 전체 메모리 크기는 400바이트이다.

p[0] - p[99] 까지 총 100개의 포인터를 사용할 수 있다.

배열을 사용하여 p 변수의 크기가 400바이트로 고정된다.

100개의 포인터를 모두 사용하지 않는다면 메모리가 낭비된다.

 

여러 개의 1차원 포인터를 동적으로 할당하기

 

short **pp;

pp = (short **)malloc(sizeof(short *));    //  1차원 포인터 1개 할당한다.

 

int n;

short **pp;

scanf("%d", &n);    //  사용할 1차원 포인터 개수를 사용자에게 입력을 받을 수 있다.

pp = (short **)malloc(sizeof(short *) * n);    //  short * 형식의 1차원 포인터 n개를 할당한다.

 

2차원 배열과 동적 메모리 할당

 

어떤 회사에서 직원들의 체력을 테스트한 결과를 저장하는 프로그램을 만든다고 가정하자

이 회사의 연령별 인원수는 20대가 4명, 30대가 2명, 40대가 3명이라고 가정한다.

직원들이 1분간 윗몸 일으키기를 하고 이 횟수를 연령별로 저장한다.

 

2차원 배열로 메모리를 할당하는 방법

 

1명이 1분 동안 수행한 윗몸 일으키기 횟수를 저장하기 위한 자료형(메모리 크기)을 결정하기

-> 0~255 사이의 값을 저장하면 되기 때문에 unsigned char 자료형을 사용한다.

 

어떤 배열 구조를 사용할지 정하기

-> 연령별 그룹 수(3그룹)의 각 그룹별 인원수(최대 4명)를 고려하여 2차원 배열 구조를 사용한다.

 

unsigned char count[3][4];    //  3개의 그룹에 최대 4명의 사람을 관리한다.

 

 

 

 

배열에 직원들의 윗몸 일으키기 횟수를 대입하기

 

count[0][3] = 50;    //  20대 연령의 4번째 사람

count[1][2] = 49;    //  30대 연령의 3번째 사람

count[2][0] = 55;    //  40대 연령의 1번째 사람

 

조건 추가 1 : 각 연련층에 포함된 인원수가 변한다면?

 

회사에 인원 변동이 있어 각 연령층에 포함된 인원수에 변화가 생길 수 있다는 조건을 추가한다.

특정 연령층의 수가 4명 이상이 되면 count 배열에 모든 직원의 윗몸 일으키기 횟수를 저장하지 못하는 문제가 발생한다.

사용자가 입력한 인원수만큼 메모리를 동적으로 할당하여 문제를 해결한다.

 

unsigned char limit_table[3] = {4, 2, 3};    //  사용자가 연령층 별로 입력한 인원수

unsigned char *p[3];    //  1차원 포인터 3개를 선언한다.

int age;

for(age=0; age<3; age++) {

p[age] = (unsigned char *)malloc(limit_table[age]);

}

for(age=0; age<3; age++) free(p[age]);

 

 

 

조건 추가 2 : 직원의 연령층이 다양해진다면?

 

20~40대 외에도 50대나 60대 연령층이 추가될 수 있다고 가정한다.

연령층의 개수도 사용자에게 입력 받을 수 있도록 변경한다.

 

unsigned char *p_limit_table;    //  연령층 별 인원수를 저장할 포인터

unsigned char **p                    //  1차원 포인터를 n개 선언할 2차원 포인터

int age_step = 3, age;                //  연령대의 개수. 20대, 30대, 40대

 

p_limit_table = (unsigned char *)malloc(age_step);    //  연령층별 인원수를 저장할 메모리를 동적으로 할당한다.

p_limit_table[0] = 4;    //  20대 4명

p_limit_table[1] = 2;    //  30대 2명

p_limit_table[2] = 3;    //  40대 3명

 

p = (unsigned char **)malloc(sizeof(unsigned char *) * age_step);    // 연령층별로 윗몸 일으키기 횟수를 저장할 포인터를 연령층 개수별로 만든다.

for(age = 0; age < age_step; age++) {                                                   // 연령층별로 입력된 인원수만큼 메모리를 할당한다.

*(p + age) = (char *)malloc(*(p_limit_table + age));

}

 

 for(age = 0; age < age_step; age++) free(*(p + age));    //  프로그램이 끝나기 전에 동적 할당된 메모리를 정리한다.

free(p);                                                                            //  동적 할당된 1차원 포인터 메모리를 해제

free(p_limit_table);                                                           //  연령별 인원수를 저장하기 위해 만든 메모리를 해제함

   //  메모리 할당 해제는 메모리 할당 순서의 반대로 해야한다.

 

구조체란?

 

C언어에서 메모리를 그룹 짓는 법이다.

 

배열 VS 구조체

 

배열은 동일한 데이터를 그룹 짓는다.

구조체는 다양한 데이터를 그룹 짓는다.

 

typedef 문법 사용하기

 

typedef는 'type define'의 줄임 표현이다.

기존 자료형 이름의 길이가 긴 경우 자료형을 재정의하여 사용하는 문법이다.

#define과 비슷해 보이지만 다른 기능을 한다.

typedef는 전처리기가 아닌 문법이다. 즉, 타입을 치환하는 문법이다.

 

기본 자료형을 단순한 형태의 새 자료형으로 바꾸기

 

typedef unsigned short int US;    //  unsigned short int형을 US라는 새로운 이름으로 정의한다.

US temp;                                     //  unsigned short int temp; 라고 선언한 것과 같다.

typedef를 제외한 나머지 부분이 변수 선언처럼 생겨야한다.

 

typedef의 장점1 : 복잡해 보이는 문법을 쉽게(단순화) 표현할 수 있다.

 

typedef int MY_DATA[5];

MY_DATA temp;                //  int temp[5]; 라고 선언한 것과 같다.

 

int (*p)[5];                       //  20바이트의 사용범위를 가지는 포인터 변수이다.

 

typedef int MY_DATA[5];

MY_DATA *p;                    //  int (*p)[5];와 같이 선언한 것과 같다.

 

typedef의 장점2 : 자료형의 크기를 쉽게 바꿀 수 있다.

 

data type이 바뀔 가능성이 있는 변수는 typedef를 사용하면 변경해야 할 때 쉽게 변경할 수 있다.

char age; --> short int age;    //  age 변수의 크기를 1바이트에서 2바이트로 변경해야 하는 경우

 

#define vs typedef의 차이

 

#define은 상수 치환을 한다.

#define MAX 100

 

#define us unsigned short

us temp;    //  us = unsigned 라고 생각할수도 있지만 완전히 다르다.

 

#define PTR char *

 

PTR p;        //  char *p;

PTR p, k;     //  char *p, k;    // p는 포인터이지만 k는 상수이다.

 

typedef char *p PTR

 

PTR p, k;    // char *p, *k;    // p, k 둘다 포인터이다.

 

단, typedef는 상수치환을 할 수 없다. typedef는 반드시 데이터 타입이 와야한다.

 

비슷한 형태의 데이터를 관리하려면?

 

사람 3명의 나이, 키, 몸무게를 관리하는 프로그램을 만든다고 가정하자

 

// 3명의 나이, 키, 몸무게를 저장할 수 있는 변수를 각각 선언한다.

int age1, age2, age3;

float height1, height2, height3;

float weight1, weight2, weight3;

 

이렇게 선언을 하면 scanf() 함수를 사용해서 9번의 정보를 받아야 하고 사람의 수와 종목이 늘어날수록 소스코드가 길어지고 반복이 많이된다.

 

데이터의 그룹화 1 : 배열

 

사람의 나이, 키, 몸무게를 저장하는 변수들을 배열을 사용하여 관리한다.

 

int age[3];                //  3명의 나이를 저장할 age 배열을 선언한다.

float height[3];          //  3명의 키를 저장할 height 배열을 선언한다.

float weight[3];         //  3명의 몸무게를 저장할 weight 배열을 선언한다.

 

for(int i=0; i<5; i++)  scanf("%d", age + i);    //  나이를 입력받는다.

 

배열의 한계

 

배열은 크기가 같은 데이터만 그룹으로 묶을 수 있다.

나이, 키, 몸무게 별로 그룹을 나누는 것보다 특정 사람의 정보를 모아서 그룹으로 만드는 것이 효율적이다.

 

데이터의 그룹화 2 : 구조체

 

사용자 정의 자료형 정의 방법 중 한가지이다.

C언어는 크기나 형식이 다른 데이터를 그룹으로 묶어 사용할 수 있도록 '구조체(Structrue)' 문법을 제공한다.

기본 자료형이나 사용자가 정의한 자료형을 그룹으로 묶어 새로운 자료형을 만들 수 있다.

 

구조체로 새로운 자료형 만들기

 

구조체 이름에 예약어를 사용하면 안된다. 구조체의 요소들을 나열한다.

구조체는 프로그래머가 만든 새로운 data type이다. 구조체는 복합문이 아니다.

구조체로 만든 자료형으로 변수 선언하기

 

구조체로 만든 자료형의 크기는 { }안에 선언한 요소들의 크기의 합이다.

 

struct People data;                        //  일반 변수 : data 변수의 크기는 22바이트이다.

struct People friend_list[32];         //  배열 변수 : friend_list 변수의 크기는 22 X 32이다.

struct People *p;                           //  포인터 변수 : p변수의 크기는 4바이트(주소값을 저장한다.)

 

구조체 변수를 선언할 때 매번 struct 키워드를 붙여야 하는 불편함이 있다.

 

struct People

{

};

typedef struct People Person;        //  Person이라는 자료형으로 재정의한다.

Person data;        //  struct People data;를 의미한다.

 

struct와 typedef를 조합해서 구조체 변수를 선언하는 방법

 

 

 

구조체로 선언한 변수의 요소 사용하기

 

구조체로 선언한 변수는 .(요소 지정) 연산자와 자신이 사용할 요소의 이름을 함께 적어서 사용한다.

 

 

구조체로 선언한 변수를 포인터로 사용하기

 

Person data;        //  Person 자료형으로 data 변수를 선언한다.

Person *p            //  Person 형식으로 선언한 메모리에 접근할 수 있는 포인터 선언한다.

p = &data;             //  포인터 변수 p는 data 변수의 주소 값을 저장한다.

(*p).age = 23;       //  p에 저장된 주소에 가서 age 요소에 값 23을 대입한다.

*p.age = 32          //  오류 발생, * 연산자의 우선순위가 . 연산자의 우선 순위보다 낮다.

 

연산자 우선순위 문제를 해결하는 -> 연산자

 

Person data;

Person *p;

p = &data;

p->age = 23;        //  (*p).age = 23과 같다

 

구조체 문법으로 선언한 변수의 초기화 방법

 

구조체 문법 또한 배열과 같은 형식으로 초기값을 대입한다.

 

struct People

{

    char name[12];

    unsigned short int age;

    float height;

    float weight;

};

 

void main( )

{

    struct People data = {"홍길동", 51, 180.2 , 87.2};

}

 

구조체 멤버 정렬 기준

 

구조체의 요소를 일정한 크기로 정렬하여 실행 속도를 높이기 위해 C 컴파일러가 구조체 멤버 정렬(Struct Member Alignment) 기능을 제공한다.

1, 2, 4, 8 바이트 단위로 정렬 가능하다.

컴파일러에 설정된 구조체 정렬 기준에 따라 구조체의 크기가 달라진다.

VC++는 기본적으로 8바이트 Alignment를 사용한다. 구조체에 8바이트 데이터 타입이 없으면 하위 데이터 바이트로 적용한다.

 

구조체의 정렬

 

struct Test

{

    char a;            //  1바이트

    int b;               //  4바이트

    short c;           //  2바이트

    char d;            //  1바이트

}

 

1바이트 정렬

 

1바이트로 정렬된 구조체의 크기는 요소들의 크기의 합과 같다

Test 자료형의 크기는 8바이트 이다.

 

 

 

 

2바이트 정렬

 

구조체의 각 요소는 2의 배수에 해당하는 주소에서 시작할 수 있다.

구조체의 크기는 2의 배수가 되어야 한다.

요소의 자료형이 2바이트보다 작은 경우에는 해당 요소의 크기로 정렬된다.

 

4바이트 정렬

 

구조체의 각 요소는 4의 배수에 해당하는 주소에서 시작할 수 있다

구조체의 크기는 4의 배수가 되어야 한다.

요소의 자료형이 4바이트보다 작은 경우에는 해당 요소의 크기로 결정된다.

 

 

8바이트 정렬

 

구조체의 각 요소는 8의 배수에 해당하는 주소에서 시작할 수 있다.

구조체의 크기는 8의 배수가 되어야 한다.

요소의 자료형이 8바이트보다 작은 경우에는 해당 요소의 크기로 정렬된다.

 

 

struct Test

{

    char a;            //  1바이트

    double b;        //  8바이트

    short c;           //  2바이트

    char d;            //  1바이트

}

 

구조체의 요소는 같은 크기끼리 모아 주는 것이 좋다.

 

구조체로 자료형을 선언할 때 같은 크기의 요소들끼리 모아주면 프로그램의 효율을 높일 수 있다.

 

 

 

구조체의 크기를 구할 때에는 위와 같은 구조체 멤버 정렬 기준 때문에 sizeof 연산자를 이용하는 것이 안전하다.

 

struct Test *p1 = (struct Test *)malloc(16);                                      //  설정에 따라 오류 발생한다.

struct Test *p2 = (struct Test *)malloc(sizeof(struct Test)              

 

연결 리스트

 

사용자에게 묻지 않고 프로그램이 알아서 내부적으로 메모리를 할당하는 기술이다. 길이제한이 없다.

 

여러 개의 숫자를 입력 받아서 합산하는 '더하기 프로그램'을 만든다고 가정한다.

 

 

사용자가 입력한 숫자를 저장하기 위해 동적으로 할당된 메모리의 주소 값을 저장하려면 그 개수만큼 포인터가 필요하다.

 

 

 

포인터가 늘어나면 '포인터 1'과 '포인터 2' 사이에도 연결 고리가 있어야 연결이 유지된다.

 

'포인터 1'과 '포인터 2'를 가리키기 위해서는 '숫자 1'과 '포인터 2'를 하나의 메모리로  묶어서 동적으로 할당하면 문제 해결

 

'숫자 2'가 추가로 입력되면 다음 '숫자 2' 값을 저장할 메모리와 그 다음 숫자를 가리킬 '포인터 3'이 함께 사용하는 동적 메모리를 할당해서 그 시작 주소를 '포인터 2'에 저장 

 

 

노드(Node) : 연결 리스트에서 숫자와 포인터를 함께 저장하기 위해 할당한 메모리

 

typedef struct node                    //  이 구조체를 연결 리스트에서 노드라고 한다.

{

    int number;                            //  숫자를 저장할 변수이다.

    struct node *p_next;              //  다음 노드를 가리킬 포인터이다.

} NODE;

 

연결 리스트에 노드를 추가하며 이어가기

 

1단계 : 연결 리스트의 시작 상태

 

동적으로 할당되는 첫 노드의 주소 값을 저장해야 하기 때문에 포인터가 필요하다.

연결 리스트의 시작점이 되는 포인터가 헤드 포인터이다.(Head Pointer)

 

 

NODE *p_head = NULL    //  첫 노드를 가리킬 헤드 포인터를 선언하고 NULL을 초기값으로 대입한다.

 

2단계 : 숫자 12를 저장하기 위한 새 노드를 추가한다.

 

1. 사용자가 입력한 12라는 숫자를 저장하기 위해 새로운 노드를 추가한다.

2. 새로운 노드를 위한 메모리를 malloc 함수를 사용해 동적으로 할당한다.

3. 할당된 새 노드의 주소 값은 헤더 포인터에 저장한다.

4. 할당된 노드의 number 에는 12를 저장하고, p_next에는 NULL을 대입한다.

 

p_head = (NODE *)malloc(sizeof(NODE));

p_head -> number = 12;    // 노드의 number에 값 12를 저장한다.

p_head -> p_next = NULL;  // 다음의 노드가 없음을 명시 한다.

 

3단계 : 숫자 15를 저장하기 위한 새 노드 추가

 

1. 사용자가 추가로 입력한 15라는 숫자를 저장하기 위해 노드를 추가한다.

2. 새로운 노드를 위한 메모리를 malloc 함수를 사용해 동적으로 할당한다.

3. 할당된 새 노드의 주소 값은 첫 노드의 p_next포인터에 저장한다.

4. 할당된 노드의 number에는 15를 저장하고, p_next에는 NULL을 대입한다.

 

p_head -> p_next = (NODE *)malloc(sizeof(NODE));

p_head -> p_next -> number = 15;                //  노드의 number에 15를 저장한다.

p_head -> p_next -> p_next =NULL;              //  다음 노드가 없음을 명시한다.

 

 

반복문으로 연결 리스트에서 마지막 노드 탐색하기

 

NODE *p = p_head;                //  반복문은 p_head에 저장된 주소 값에서 시작한다.

while(NULL != p -> p_next) {  //  p_next가 NULL일 때까지 반복, p_next 값이 NULL이면 마지막 노드라는 뜻이다.

    p = p -> next;                     //  p -> p_next 값을 p에 대입하면 p는 다음 주소로 이동한다.     

}

 

연결 리스트의 마지막 노드 기억하기

 

노드가 추가될 때 마다 마지막 노드를 찾기 위해 탐색을 하면 노드가 많아질수록 수행 시간이 점점 길어진다.

마지막 노드의 값을 기억하는 테일 포인터(Tail Pointer)를 사용한다.

 

while(NULL != p -> p_next) p = p -> p_next;        //  마지막 노드를 탐색하는 반복문이 필요하지 않는다.

 

연결 리스트의 전체 노드 제거하기

 

프로그램이 끝날 때 동적으로 할당된 노드를 모두 제거해야 한다.

연결 리스트를 구성하는 노드를 탐색하여 하나씩 노드를 제거한다.

 

NODE *p = p_head;

while(NULL != p) {

    free(p);                        //  p가 가리키는 노드를 삭제한다.

    p = p -> p_next;           //  오류발생 : 다음 노드로 이동이 불가능하다.

}

 

위 소스코드에서 이미 해제된 메모리를 사용하려 했기 때문에 오류가 발생했다.

따라서 아래 소스코드에 이동할 다음 노드의 주소를 미리 저장하여 문제를 해결했다.

 

 

NODE *p = p_head, *p_save_next;

while(NULL != p) {

    p_save_next = p->p_next;        //  다음 노드의 주소를 미리 저장한다. 

    free(p);                                    //   p가 가리키는 노드를 삭제한다.

    p = p_save_next;                     //   다음 노드로 이동한다.

}

p_head = p_tail = NULL;               //  연결 리스트의 시작과 끝을 초기화 한다.