41
Array and Pointer C-언어의 활용을 위한 주요 기법 (2) Dong Kyue Kim Hanyang University [email protected]

Array and Pointeresslab.hanyang.ac.kr/uploads/data_structure_2019_1... · 2019. 3. 11. · Array and Pointer C-언어의활용을위한주요기법(2) Dong Kyue Kim Hanyang University

  • Upload
    others

  • View
    0

  • Download
    0

Embed Size (px)

Citation preview

  • Array and Pointer

    C-언어의 활용을 위한 주요 기법 (2)

    Dong Kyue Kim

    Hanyang University

    [email protected]

  • 배열

  • int a[7];int a0, …, a6;

    • 배열

    – 유도형 자료 유형의 하나로 기본 자료 유형을 변형하여 만들어진다.

    – 동일한 자료 유형을 가지는 여러 변수의 집합

    – 배열을 구성하는 데이터들을 배열 원소(Element)라고 한다.

    – 배열은 하나의 이름을 공유

    – 배열 원소는 변수와 100% 동일

    배열 (1/2)

    3

    배열

    a[0] a[1] a[2] a[3] a[4] a[5]

    배열은 하나의 이름을 공유한다

    a4

    a5

    a1

    a3

    a2

    a6

    a0

    변수

    a[6]

    배열 원소

  • 배열 (2/2)

    4

    • 배열이 필요한 이유

    – 어떤 프로그램의 경우 변수를 100개, 200개 선언해야 하는 경우가 생긴다.

    – 하나하나 a1, a2, … ,a100 선언을 하는 것은 시간 낭비

    – 많은 양의 데이터 처리를 위해 많은 변수를 선언해야 하는 경우 배열을 사용함으로써 시간적 낭비가 줄고 편리해 진다.

    int a1, a2, a3, a4, a5, a6, a7, a8, …, a98, a99, a100;

    int a1, …, a100; int a[100];

  • 배열의 구조

    • 배열의 일반적인 구조

    – 자료형 : 배열의 요소가 어떤 값들의 집합인가를 지정한다. int, char, double과같이 기본 자료형을 사용한다.

    – 배열이름 : 배열도 변수이므로 이름이 있어야 한다.

    – 크기 : 요소의 개수가 몇 개인가를 []괄호 안에 정수 상수로 지정한다. 크기 지정이 하나만 있으면 1차원 배열 두 개 이상이면 다차원 배열이라고 한다.

    5

    자료형 배열이름 [크기] [크기] … ;

  • double array2[20][30];int array1[20];

    배열의 선언

    • 배열의 선언

    – 주요 요소는 자료형, 배열 이름, 배열의 크기 이다.

    – 배열의 자료 유형은 모든 기초 자료형이 사용 가능하다.

    – 배열의 이름은 변수의 이름과 같은 규칙을 갖는다.

    – 배열의 크기는 반드시 대괄호 사이에 입력하고 한 개 이상 있어야 한다.

    – 배열의 크기는 반드시 양의 상수이다. (변수로 지정하면 안 된다.)

    6

    정수형 20개짜리 배열 실수형 20개씩 30개인 배열 (총 600개)

    int n = 20;int array1[n];

  • 배열 요소 접근 방법 (1/2)

    • 인덱스

    – 임의의 위치에 존재하는 배열 요소에 접근하기 위한 요소의 위치 표시.

    – [0]에서 [크기 – 1] 까지 있다. (1이 아닌 0에서 시작한다.)

    7

    int array [5];

    array [0]

    int int int int int

    array [1] array [2] array [3] array [4]

  • int main (void){

    int array [5];array [0] = 20;array [1] = 10;array [2] = 97;array [3] = 81;array [4] = -49;array [5] = 20;return 0;

    }

    배열 요소 접근 방법 (2/2)

    8

    첫 번째 요소 접근.

    다섯 번째 요소 접근.

    배열의 크기가 5이므로에러 발생.

  • 배열의 초기화

    • 선언과 동시에 초기화

    9

    int array [5] = {1,2,3,4,5};

    int array [] = {1,2,3,4,5,6,7};

    int array [5] = {0,1};

    1 2 3 4 5

    1 2 3 4 5 6 7

    0 1 0 0 0

    int array [5] = {0}; 0 0 0 0 0

    앞에서 부터 순서대로채워진다.

    초기화 리스트가 배열크기보다 작으면 나머진 0으로 채운다.

    초기화 리스트

    초기화 리스트의 수만큼 자동으로 크기가 잡힌다.

    크기가 7로 잡힌다.

  • 배열과 메모리

    • 배열과 메모리

    – 배열이 선언되면 배열의 자료형 X 크기 만큼의 공간이 메모리에 할당된다.

    10

    int array [10];

    4byte X 10 = 40byte

    123456781234567912345680123456811234568212345683123456841234568512345686

    12345711123457121234571312345714123457151234571612345717

    array [0]

    array [1]

    array [9]

    ……

  • 다차원 배열

  • 12

    다차원 배열

    • 다차원 배열

    – 배열이 원소를 배열로 갖는 것이 다차원 배열. 즉 배열의 배열

    – 배열의 원소가 배열이고, 그 원소인 배열들의 원소가 일반 자료유형(int, char 등)인 경우 2차원 배열

    – 배열의 원소가 배열이고, 원소인 배열의 원소가 또 배열이고, 그 배열의 원소가 일반 자료유형인 경우 3차원 배열

    – 3차원 이상의 배열도 가능

    자료형 배열이름 [크기] [크기] … ;

    a

    a[0][0] a[0][1] a[0][2]

    a[0]

    a[2]

    a[1] a[1][0] a[1][1] a[1][2]

    a[2][0] a[2][1] a[2][2]

    배열의 원소원소인 배열의 원소들

    2차원 배열

    char arr1[100]; //1차원 배열int arr2[10][10]; //2차원 배열double arr3[2][4][3]; //3차원 배열

  • int arr2[3][4];

    2차원 배열의 선언

    • 2차원 배열의 선언

    – Arr[ i ][ j ] 로 선언하였다면 Arr[ j ] 인 배열이 i개가 있다고 생각

    13

    int int int int

    int int int int

    int int int int

    arr[0][0] arr[0][1] arr[0][2] arr[0][3]

    arr[1][0] arr[1][1] arr[1][2] arr[1][3]

    arr[2][0] arr[2][1] arr[2][2] arr[2][3]X 3

    X 4

  • • 2차원 배열의 메모리 할당

    – 메모리는 2차원적인 구조가 아니다.

    – 그림과 같이 1차원 구조로 할당 된다.

    2차원 배열의 메모리상의 구조

    14

    a[0][0]

    a[0][1]

    a[0][2]

    a[1][0]

    a[1][1]

    a[1][2]

    a[0][ ]

    a[1][ ]

    int arr2[3][4];

  • #include

    int main(){

    int arr[3][3];arr[0][0]=2;arr[1][1]=4;arr[2][2]=8;arr[1][0]=3;return 0;

    }

    2차원 배열 요소의 접근 방법

    • 2차원 배열 요소의 접근 방법

    15

    2 ? ?

    3 4 ?

    ? 8?X 3

    X 3

    ?는 쓰레기 값

  • 2차원 배열 초기화

    • 행 단위로 모든 요소들을 초기화

    16

    1 2 3

    4 5 6

    7 98X 3

    X 3

    [0][0] [0][1] [0][2]

    [1][0] [1][1] [1][2]

    [2][0] [2][1] [2][2]

    int arr2[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };

  • 2차원 배열 초기화

    • 행 단위로 일부 요소들만 초기화

    17

    1 0 0

    4 5 0

    7 98X 3

    X 3

    [0][0] [0][1] [0][2]

    [1][0] [1][1] [1][2]

    [2][0] [2][1] [2][2]

    int arr2[3][3] = { {1}, {4, 5}, {7, 8, 9} };

  • 2차원 배열 초기화

    • 1차원 배열 형태로 초기화

    18

    1 2 3

    4 5 6

    7 00X 3

    X 3

    [0][0] [0][1] [0][2]

    [1][0] [1][1] [1][2]

    [2][0] [2][1] [2][2]

    int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7};

  • • 배열 크기를 가르쳐 주지 않고 초기화

    – 2차원 배열도 1차원과 마찬가지로 선언과 동시에 초기화할 경우 그 크기를 명시 하지 않아도 된다.

    – 배열의 크기 선언의 위치에서 앞의 크기는 알려주지 않아도 되지만, 뒤의크기는 꼭 알려주어야 한다.

    int arr2[ ][5] = { {1, 2, 3}, {4, 5, 6, 7, 8} };

    int arr2[ ][4] = { 1, 2, 3, 4, 5, 6, 7, 8 };

    2차원 배열 초기화

    19

    뒤의 크기는 알려주어야 한다.앞의 것만 알려줄 경우 컴파일 에러가 난다.

    int arr2[ ][6] = { {1, 2, 3}, {4, 5, 6, 7, 8} };

  • 2차원 배열 초기화

    • 2차원 배열 초기화 중 뒤 크기를 가르쳐 주어야 하는 이유

    – 배열 a [ i ][ j ]를 메모리 할당을 할 때 a [ j ]가i개 만큼 있는 것으로 메모리가 할당

    – a [ j ]가 결정되어야 그 만큼의 크기의 간격으로메모리를 할당

    20

    a[0][0]

    a[0][1]

    a[0][2]

    a[1][0]

    a[1][1]

    a[1][2]

    a[0][ ]

    a[1][ ]

    위와 같이 선언할 경우 컴퓨터는2x4로 할지, 3x3인 것인데 하나가 초기화 안된 것 할지 4x2개로 할지 등 여러 경우가 생겨 배열의 메모리 할당이 불가능해 진다.

    위와 같이 앞의 크기만 알려주어도 2x3인지,3x3인데 일부가 초기화 되지 않은 것인지 알수 없다. 메모리 할당의 구조상 ixj 에서 j가 추가되는것은 구조 변화 없이 연장만 하면 됨으로 쉽지만 i가 잘못 결정되어 추가되는 것은 전체구조의 변화를 필요로 하기 때문에 어렵다.

    int a[ ][ ] = { 1, 2, 3, 4, 5, 6, 7, 8 };

    int a[3][ ] = { 1, 2, 3, 4, 5, 6 };

  • 포인터

  • 포인터의 개요

    • 포인터

    – 포인터는 C언어를 다른 언어들과 차별화 시키는 특징

    • 포인터를 이용하면 기계어나 어셈블리어처럼 메모리 주소를 사용해 직접 메모리에접근하여 데이터 조작이 가능

    • 배열과 함수를 포인터와 사용하면 효율적인 프로그래밍이 가능

    – 포인터는 메모리 공간의 주소를 값으로 갖는 변수

    • 포인터는 변수지만 자료 값이 아닌, 자료가 있는 메모리 영역의 주소 값을 갖는 변수

    • 포인터의 정확한 이해를 위해서, 기존에 배운 변수와 포인터 변수의 차이점과 서로의관계를 이해해야 할 필요가 있다.

    22

    블랙홀이뭐야?

    블랙홀을 설명하자면 블랙홀은 일반상대성이론을 통해 예측된 천체야. 별이 폭발할 때반지름이 극단적인 수축......

    블랙홀의 설명은 백과사전 520쪽에 있어!

    포인터 변수일반 변수

  • 기존의 변수와 포인터 변수

    • 기존의 변수

    – 메모리 공간을 사용하기 편하게 이름 지은 것이 변수의 이름

    – 변수가 갖는 데이터는 우리가 주로 연산에 사용할 피연산자

    • 포인터 변수와 기존의 변수의 관계

    – 포인터 변수가 갖는 데이터는 메모리 공간의 주소

    – 변수의 주소는 메모리 공간의 주소

    – 즉, 포인터는 데이터로 다른 변수의 주소를 갖고 있다.

    – 포인터 변수는 다른 변수를 가리키고(point) 있다.

    23

    기존의 변수 포인터 변수

    데이터 (메모리공간의 주소)

    12345678123456791234568012345681

    ptr = 12345678

    a = 21

    12345682

    123456831234568412345685

    123456861234568712345688

    ptr

    12345678 = &a(a라는 메모리공간의 주소)

    a = [12345678](메모리 공간의 이름)

    21

  • 포인터 선언

    • 포인터 선언

    – 포인터는 가리키고자 하는 기본 자료형에 *연산자를 붙여서 선언

    • * 연산자를 주위로 띄어쓰기는 상관이 없다.

    – 메모리의 주소 값은 항상 4byte (32bit)이기 때문에 포인터 변수는 모두 4byte

    – 포인터는 가급적이면 선언과 동시에 초기화를 시켜주는 것이 좋다.

    • 이유는 뒤에서 설명

    24

    int형 변수의 주소 값을 지닐 수 있는 int형 포인터

    char형 변수의 주소 값을 지닐 수 있는 char형 포인터

    double형 변수의 주소 값을 지닐 수 있는 double형 포인터

    모두 같은 선언이다.

    int *a;char *b;double *c;

    int *a;int* a;int * a;

  • & 연산자

    • 주소 연산자 &

    – 변수(메모리공간)의 주소를 알려주는 연산자

    – 변수의 이름 앞에 & 연산자를 붙이게 되면 해당 변수의 주소 값이 반환된다.

    25

    #include

    int main(void){

    int a = 2005;printf(“변수 a의 메모리 주소는 %d입니다.\n”, &a);printf(“변수 a의 메모리 주소는 %X입니다.\n”, &a);return 0;

    }

    변수 a의 메모리 주소는 1245024입니다.변수 a의 메모리 주소는 12FF60입니다.

  • * 연산자

    • 간접 참조 연산자 *

    – 포인터가 가리키는 주소에 있는 값을 읽거나 변경하도록 하는 연산자

    • 포인터는 단순히 메모리주소만 저장하는 기능 이외에도 포인터가 가리키는 주소(메모리 공간의 주소, 변수의 주소)에서 값을 읽거나 변경할 수 있다.

    – 포인터 변수 앞에 *연산자를 붙이면 포인터가 가리키는 메모리 공간에 존재하는값을 참조하겠다는 뜻

    – 이것을 간접 참조(indirection)라 한다.

    26

    void main(void) {

    int * ptr;int a = 21;

    prt = &a;printf(“%d\n”, ptr);printf(“%d\n”,*ptr);

    }

    1234567821

    포인터 변수 ptr에 &a (a의 메모리 주소)를 저장

    포인터 변수 int 변수

    *pa는 변수 a자체를 의미

    ptr

    12345678 = &a(a라는 메모리공간의 주소)

    a = [12345678](메모리 공간의 이름)

    21

  • 다양한 포인터 변수 타입

    • 포인터 변수의 자료 유형이 다양하게 존재하는 이유

    – 포인터 변수는 가리키는 자료유형에 상관없이 모두 4byte로 크기가 똑같다.

    • Memory의 주소가 32bit(4byte)이기 때문 char * , int * , double * 모두 크기가 같음

    – 포인터는 자료의 첫 번째 byte(첫 번째 메모리공간) 을 가리킨다.

    – 자료유형에 따라 메모리를 참조할 때 몇 바이트 읽어야 하는지 알아야 함.

    • char *는 주소가 가리키는 메모리 공간의 1byte만, double * 은 8byte

    27

    #include int main(void){

    char *pa, a=10;int *pb, b=20;double *pc, c=30;

    pa = &a;pb = &b;pc = &c;

    printf("%d의 메모리주소 : %d\n", a, pa);printf("%d의 메모리주소 : %d\n", b, pb);printf("%.2lf의 메모리주소 : %d\n", c, pc);return 0;

    }

    10의 메모리주소 : 1245010

    20의 메모리주소 : 1244988

    30.00의 메모리주소 : 1244960

  • 다양한 포인터 변수 타입

    28

    ……

    1245015

    pa = 1245015

    a = 10

    char형 포인터이므로해당 주소에서 1byte참조한다.

    ……

    1244988124498912449901244991

    pb = 1244988

    b = 20

    int형 포인터이므로해당 주소에서 4byte참조한다.

    12559661255967 …

    12449601244961124496212449631244964

    pc = 1244960

    1255965

    c = 30

    double형 포인터이므로 해당 주소에서8byte 참조한다.

  • 포인터 형 변환

    • 포인터 형 변환

    – 포인터 변수는 자동 형 변환이 불가능하다.

    – 형 변환을 하고 싶은 경우에는 강제 형 변환을 해야 한다.

    29

    #include

    int main(void){

    char *pa;int a=256;pa = (char*)&a;printf("pa : %d\n", pa);printf("*pa : %d\n", *pa);return 0;

    }

    int형 변수 a에서 1byte만 참조하게 된다.

    pa = 1245012*pa = 0

  • 포인터 변수의 초기화

    • 포인터 변수의 초기화

    – 포인터 변수는 초기화가 매우 중요

    – 메모리 영역 중에는 시스템이 현재 사용하는 중요한 부분이 많다.

    – 포인터 변수의 잘못된 초기화 이후 자료를 조작하는 것은 시스템에 치명적인 문제를 야기 시킴

    • 시스템이 사용하는 중요한 메모리 영역의 값을 변경하는 경우

    • 잘못된 초기화의 예시

    30

    만약 100이란 메모리 주소가 다른 시스템에서 아주 중요한부분이라면 시스템 전체에 심각한 문제가 발생할 수도 있다.다행히 요즘의 운영체제는 이러한 문제의 경우 실행을 허용하지 않고 중지시켜 버린다.#include

    int main(void){

    int *pA = 100;*pa = 10;return 0;

    }

  • 포인터 변수의 초기화

    31

    • 포인터 변수의 초기화

    – 포인터 변수를 초기화 하지 않고 사용하는 경우

    • 포인터는 쓰레기 값을 가져 임의의 주소를 가리키게 됨.

    • 이 상태에서 포인터를 이용하여 메모리의 내용을 변경한다면 문제 발생

    – NULL 포인터의 사용

    • 포인터가 아무것도 가리키고 있지 않을 때는 NULL(0)으로 설정

    • NULL은 stdio.h에서 0으로 정의

    • 주소 0의 경우 엑세스 하려고 하면 시스템에서 자동적으로 오류를 감지하고 해결

    int main (void) { int *p ; //포인터 p는 초기화가 안 되어 있음

    *p = 100;return 0;

    }

    아주 위험한 코드

    int *p = NULL; p가 아무것도 가리키고 있지 않다고 시스템이 인식함으로써 해결

  • 배열과 포인터

  • 배열과 포인터

    • 포인터와 배열의 관계

    – 배열 이름도 포인터 (배열의 이름은 포인터 상수)

    – 배열 이름에는 첫 번째 배열 원소의 메모리의 주소 값이 저장

    33

    배열 이름을 출력하면 포인터와 같이 메모리 주소가 출력

    a[0] = 1

    a[1] = 2

    ……

    &a[0]

    &a[1]

    a#include

    int main(void){

    int a[5] = {1,2,3,4,5};printf ("%d, %d\n", a[0], a[1]);printf ("%d, %d\n", &a[0], &a[1]);printf ("배열 이름 : %d\n", a);return 0;

    }

    1, 21245008, 1245012배열 이름 : 1245008

  • #include

    int main(void){

    int a[5] = {1,2,3,4,5};int b = 10;a = &b;return 0;

    }

    • 배열이름은 수정이 불가능한 포인터

    – 보통의 포인터 변수의 경우 가리키는 곳을 변경 가능

    – 배열의 이름은 가리키는 곳(메모리의 주소)이 수정 불가능

    – 배열의 이름은 포인터 변수가 아닌 상수 포인터

    34

    a는 상수 포인터이므로 에러가 발생한다.

    배열과 포인터

  • int arr1[20];double arr2[30];

    배열과 포인터

    • 배열 이름의 포인터 타입

    – 배열 이름도 포인터이므로 그에 따른 포인터 타입이 존재

    – 배열의 요소의 타입이 그대로 포인터 타입

    35

    배열의 요소가 double형배열이름 arr2는 double형포인터(double *)

    배열의 요소가 int형배열 이름 arr1은int형 포인터(int *)

  • 배열과 포인터

    • 포인터를 배열처럼 사용하기

    – 배열 이름이 포인터이므로 당연히 포인터를 배열 이름처럼 사용 가능

    – 변수에 숫자를 저장해서 상수처럼 쓰는 것과 마찬가지

    36

    #include

    int main(void){

    int arr[3] = {1,3,5};int *ptr;

    ptr = arr;printf("ptr[0] : %d\n", ptr[0]);printf("ptr[1] : %d\n", ptr[1]);printf("ptr[2] : %d\n", ptr[2]);return 0;

    }

    ptr[0] : 1ptr[1] : 3ptr[2] : 5

  • 포인터와 연산

  • 포인터 연산

    • 포인터 연산

    – 포인터 연산이란 포인터 값을 증가 혹은 감소시키는 연산을 말한다.

    – 포인터가 참조하는 부분을 연산하는 것이 아니라 포인터 안에 저장된 주소값을 증가 시키거나 감소시키는 것을 말한다.

    38

    int *ptr, *ptr2;

    ptr = 0;ptr2 = 10000;

    ptr = ptr + 1;ptr2 = ptr2 – 1;

    연산 결과:

    ptr = 1ptr2 = 9999

    위 와 같은 결과가나타날까?

  • #include

    int main(void){

    char *ptr1=0;int *ptr2=0;double *ptr3 = 0;

    printf("증가시키기전 : %d, %d, %d\n", ptr1, ptr2, ptr3);printf("1증가시킨 후 : %d, %d, %d\n", ++ptr1, ++ptr2, ++ptr3);printf("추가로 3증가 : %d, %d, %d\n", ptr1+3, ptr2+3, ptr3+3);return 0;

    }

    포인터 연산

    • 포인터 연산의 의미

    – 포인터 연산에 따른 실질적인 값의 변화는 타입에 따라 다르다.

    – 포인터는 가리키는 데이터 타입의 크기(byte) 만큼 곱해져서 증가하거나 감소

    • double * 의 경우 double의 크기(byte 단위)인 8만큼 증가, 감소

    39

    포인터를 0으로 초기화 시키면 아무것도 가리키지 않는다는 의미

    증가시키기전 : 0, 0, 01증가시킨 후 : 1, 4, 8추가로 3증가 : 4, 16, 32

  • 포인터 연산과 배열

    • 포인터 연산을 이용한 배열의 사용

    – 포인터 연산이 자료형 만큼 증가하거나 감소하는 특징을 이용해 배열을 호출

    40

    ※ 포인터 연산을 이용해 배열을 다룰 때범위를 넘어가는 접근을 하지 않도록 주의

    arr[0] = 1

    arr[1] = 2

    pArr

    ++pArr 연산

    arr[2] = 3

    ……

    ++pArr 연산

    #include

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

    printf("%d \n", *pArr);printf("%d \n", *(++pArr));printf("%d \n", *(++pArr));printf("%d \n", *(pArr+1));printf("%d \n", *(pArr+2));return 0;}

    12345

  • #include

    int main(void){

    int arr[5] = {1,2,3};int *pArr = arr;

    printf("%d %d\n", arr[0], arr[1]);printf("%d %d\n", *arr, *(arr+1));printf("%d %d\n", pArr[0], *(pArr+1));return 0;

    }

    포인터 연산 예제

    41

    배열의 주소와 원소 값에 대해 각각의3가지 표현 모두 같은 것을 확인 가능

    1, 21, 21, 2