[C# 기초강좌] 9. C# 배열

Posted in SW개발 // Posted at 2023. 11. 2. 08:32
728x90
이 글은 제가 과거에 운영했던 사이트인 http://dotnet.mkexdev.net 의 글을 옮겨온 것입니다. 원본 글은 2010년에 작성되었습니다.

그 전에 운영했었던 사이트(mkex.pe.kr)은 흔적도 없이 사라 졌습니다. 그속의 글들도 모두... 그래서 이 사이트도 사라지기 전에 옮기고 싶은 글을 조금씩 이 블로그로 이동시키려 합니다.
(원본글) http://dotnet.mkexdev.net/Article/Content.aspx?parentCategoryID=1&categoryID=5&ID=673

이 글은 닷넷 기초 지식을 전파하기 위해 2010경에 작성되었으며, 당시 윤성우의 프로그래밍 스터디그룹 네이버 카페에도 필진으로 참여하여 연재했던 글이기도 합니다. 현재 시점에서 조금 달라진 부분이 있을 수 있으나 기본 원리와 언어 기초에 해당하는 부분은 크게 변하지 않았을 것으로 생각하며 이런 부분들을 감안해서 봐 주시기 바랍니다.

 

“1차원, 2차원, 가변 배열 = [] , [,] , [][]”

 

안녕하세요. 박종명입니다. 닷넷 아홉 번째 강좌를 진행하도록 하겠습니다

이번 강좌는 C# 의 배열에 대해 알아보겠습니다.

배열은 1차원 배열과 2차원 이상의 다차원 배열 그리고 가변배열이 있습니다.


배열은 동일한 타입의 자료를 묶음으로 처리할 때 유용한 자료구조 입니다. 메모리 구조상 배열은 인접한 공간에 순차적으로 저장되지만 우리의 머리 속에서는 행과 열이 있는 행렬로 인식하면 편리합니다.

 

배열에는 차원이 있는데요. 차원이 하나인 1차원 2차원 이상의 다차원 배열이 있습니다.

그리고 배열의 요소로 또 다른 배열을 저장할 수 있는 가변배열도 지원됩니다. 지금부터 차례대로 알아 보겠습니다

 

 

1차원 배열 ([])

1차원 배열은 차원이 단 한 개로 일정한 수의 항목이 순차적으로 저장되는 구조입니다

1차원 배열을 굳이 행렬로 바라볼 필요는 없지만 굳이 행렬의 구조를 생각한다면

1차원 배열은 행은 오직 1개 이며 열의 크기가 중요합니다.

 

아래 그림은 1차원 배열에 값이 총 3(1,2,3)이 저장된 모습입니다(1 3열이라고 생각해도 무방합니다)

 

위와 같은 1차원 배열을 생성하기 위해서는 아래 코드 중 하나를 사용할 수 있습니다

방법1) int[] array1 = new int[3];

array1[0] = 1; array1[1] = 2; array1[2] = 3;

 

방법2) int[] array2 = new int[] { 1, 2, 3 };

 

방법3) int[] array3 = {1,2,3 };

 

 

다차원 배열 ([,])

차원이 2개 이상이면 다차원 배열이라 합니다.

일반적으로 2차원배열을 넘어서는 3차원 이상 배열은 잘 사용하지 않습니다.

 

2차원 배열은 그야말로 딱 행렬의 구조입니다.

그러나 3차원 배열은 행렬의 행렬 즉 큐브 형태를 떠올려야 합니다. 4차원 배열은요?? 머리 아픕니다 --;

개인적으로 3차원 이상의 배열은 꼭 필요한 경우를 제외하고 웬만해선 사용하지 않기를 바랍니다.

 

아래 그림은 2차원 배열에 값이 총 6개 저장된 모습입니다(2 3열의 행렬구조 입니다)

 

위와 같은 2차원 배열을 생성하기 위해서는 아래 코드 중 하나를 사용할 수 있습니다

방법1) int[,] array1 = new int[2, 3];

array1[0,0] = 1; array1[0,1] = 2; array1[0,2] = 3;

array1[1,0] = 4; array1[1,1] = 5; array1[1,2] = 6;

 

방법2) int[,] array2 = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } };

방법3) int[,] array3 = { { 1, 2, 3 }, { 4, 5, 6 } };

 

 

3차원 이상의 배열은 사용하지 않기를 권장하지만 필요한 경우가 있을 수도 있습니다

간단히 알아 봅니다. 3차원 배열은 행렬의 행렬 구조 즉 큐브와 같은 형태를 떠올려야 합니다.

 

사실 이렇게 떠올려야 하는 자체가 피곤합니다 --; 그래서 사용하지 않았으면 한답니다 ㅎㅎ

 

(2,2,3)  3차원 배열을 생성해 보겠습니다

int[, ,] array = 
           new int[,,] { { { 1, 2, 3 }, { 4, 5, 6 } }, { { 7, 8, 9 }, { 10, 11, 12 } } };

 

다차원 배열의 차수를 계산하기 위해서는 중괄호의 개수와 꼼마(,)를 보면 됩니다

양 끝 중괄호는 무시하고 그 다음 중괄호의 개수와 그리고 그 다음 중괄호 개수

리고 최종적으로 숫자를 꼼마(,) 나열한 개수가 차원이 됩니다. 즉 위 코드에서는 2, 2, 3 의 차수를 가지게 됩니다

 

이는 2차원 이상 다차원 배열에 공히 적용되는 계산법입니다

 

아래 그림을 참고하세요

 

다차원배열의 모든 요소를 순회하기 위해서는 각 차원의 수를 계산하여 중첩루프를 돌려야 합니다

아래는 앞서 정의한 3차원 배열의 모든 요소를 순회하는 코드 입니다

 

for (int i = 0; i < array.GetLength(0); i++) //1차원 수(2)

{

      for (int j = 0; j < array.GetLength(1); j++) //2차원 수(2)

{

          for (int z = 0; z < array.GetLength(2); z++) //3차원 수(3)

{

              Console.Write( array[i, j, z]);

          }

      }

}

이전 강의에서 말씀 드렸듯이 다차원 배열의 모든 요소를 순회하기 위해 for 루프 대신 foreach 루프를 사용하면

보다 편리하게 순회할 수 있습니다. 아래 코드는 위의 for 루프와 동일한 결과를 가져다 줍니다

 

foreach (int value in array){

      Console.Write(value);

}

 

 

가변 배열 ([][])

지금까지 살펴본 1차원, 다차원 배열은 각 차원의 크기가 고정되지만 가변배열을 이용하면 다양한 차원과 크기를

가지도록 구조화 할 수 있습니다

 

가변배열의 각 요소에는 또 다른 배열이 저장되는데요. 이 때 저장되는 배열의 크기는 서로 다른 크기를 가질 수 있습니다. 따라서 가변배열은 하나의 배열에 다양한 크기를 가진 듯 한 효과를 줄 수 있으며 요소로 배열을 저장하기 때문에 배열의 배열이라고도 합니다

 

아래 코드는 2개의 요소를 가진 1차원 가변배열을 생성하는 코드입니다

int[][] array = new int[2][];

array[0] = new int[] { 1, 2, 3 };

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

 

배열 0번째 요소는 크기가 3 1차원 배열을 저장하며 1번째 요소는 크기가 4 1차원 배열을 저장합니다

사실 저장한다기 보다는 가리킨다는 표현이 맞겠네요. 아래 그림을 참고하세요

 

이렇게 생성된 가변배열을 순회하려면 다음과 같이 작성할 수 있습니다

 

for (int i = 0; i < array.Length; i++){

      for (int j = 0; j < array[i].Length; j++){

          Console.Write("{0} ", array[i][j]);

      }

 

      Console.WriteLine();

}

 

주의할 점은 가변배열과 다차원 배열은 엄연히 다른 자료구조입니다.

그 표현법도 [,] (다차원 배열) , [][] 가변배열로 서로 상이합니다

 

 

배열의 기본 성질 및 추가 내용

- 숫자 타입의 배열 요소에는 기본 값으로 0이 저장됩니다.

단 가변배열을 포함하여 배열의 요소가 참조타입일 경우에는 null 이 기본 값으로 설정됩니다

 

- 모든 배열은 참조형식 입니다

배열의 요소가 값 형식이라 할 지라도 배열 자체는 참조형식 입니다

 

- 배열 표기법: int[] array (O), int array[] (X), int[3] array (X)  

 

- 모든 배열은 System.Array 클래스로부터 파생됩니다

 배열을 정의하면 컴파일러에 의해 자동으로 Array 클래스로부터 파생되도록 구현됩니다

 따라서 Array 클래스에 정의된 배열을 조작하는 다양한 멤버들을 이용할 수 있습니다

 

 

System.Array 클래스이 유용한 멤버

 

모든 배열은 Array 클래스로부터 파생됩니다. 배열의 조작에 있어 Array 클래스에 정의된 유용한 멤버
몇 가지 살펴 봅니다

1) Rank 속성: 배열의 차원 반환

2) Length: 배열의 총 요소 수 반환

3) GetLength(int dimension): dimension에 지정된 차원의 요소 수 반환

4) Clone, CopyTo: 배열 복사

5) Array.Sort(Array): 1차원 배열의 정렬(퀵 정렬)

6) Array.Clear(Array,int,int): 배열 요소 값 지우기(초기화 시킴)

7) Array.IndexOf(Array,obejct): 배열에서 특정 요소 검색(일치하는 값의 인덱스 반환)

8) Array.BinarySearch(Array,object): 정렬된 배열에서 이진탐색 수행

 

System.Array 클래스에는 이 이외에도 많은 기능이 정의되어 있습니다.

필요 시 msdn을 참고바랍니다

 

좋은 한 주 되세요~~~

 

배열 인덱싱의 메커니즘(arr[i] == *(arr+i))

Posted in SW개발 // Posted at 2023. 10. 27. 09:20
728x90
이 글은 제가 과거에 운영했던 사이트인 http://dotnet.mkexdev.net 의 글을 옮겨온 것입니다. 원본 글은 2008년 5월에 작성되었습니다.

그 전에 운영했었던 사이트(mkex.pe.kr)은 흔적도 없이 사라 졌습니다. 그속의 글들도 모두... 그래서 이 사이트도 사라지기 전에 옮기고 싶은 글을 조금씩 이 블로그로 이동시키려 합니다.
(원본글) http://dotnet.mkexdev.net/Article/Content.aspx?parentCategoryID=2&categoryID=9&ID=105

『 배열 인덱싱의 메커니즘 』

 배열은 다수개의 요소를 가진다.
특정 요소에 접근하기 위해서는 Sequence 한 인덱싱을 사용한다.

예를 들어, int arr[] = {1,2,3} 으로 정의된 배열의 요소에 접근하기 위해서 arr[0] , arr[1], arr[2] 로 접근할 수 있다.

앞서 우리는 배열 변수는 결국 배열의 첫 번째 요소를 포인팅 하고 있는 포인터라고 배웠다.
이전 그림을 다시 보자.

이전 아티클 보기 -> 배열과 포인터의 상관관계

 그렇다면 arr[2] 와 같은 인덱싱은 어떤 처리과정을 통해 3번째 메모리 값을 가져올 수 있는 것일까?

 가장 먼저 알아야 할 중요한 내용이 있다. 

배열은 연속된 메모리 공간에 순차적으로 저장된다

위의 예에서 제시된 int 형 배열 arr  0x20 번지부터 4byte 씩 증가하는 연속된 메모리 공간에 값이 저장되는
것이다

, 아래 그림과 같이 저장된다.

 

그 다음으로 알아야 할 중요한 내용이다.

배열은 포인터이다. 배열을 인덱싱은 포인터 연산으로 이루어 진다.”

일단, 배열을 알아보기 이전에 포인터 연산 중 포인터의 주소를 이동 시키는 예제를 살펴 보자.

void main(void){      
        int i = 1;
        int *p = &i;
 
        printf("%d<=포인터의주소값\n",*&p);
        p = p + 1; //포인트의주소를한칸이동한다(4byte 만큼메모리이동)      
        printf("%d<=포인트주소이동후주소값\n",*&p);
}


포인터 변수 p  1을 더한다.

이 의미는 주소 값인 p  1을 더한다는 의미로,
메모리 주소를 한 블럭 증가시키게 된다. 이때 p는 int 형이기 때문에 1 증가는 4byte 단위(블럭)의 증가를 의미한

포인터가 가지는 값은 자신이 포인팅 하고 있는 대상 메모리의 주소 값을 가지고 있다.
따라서 이 포인터의 값을 변경한다는 것은 대상 메모리 주소를 변경한다는 것과 같은 의미이다.

주의해야 할 것은 포인터 값을 증가 시킬 경우에는 포인터의 타입에 따라 자동으로 크기만큼 곱해 진다는 것
을 알아야 한다

이 경우 p  int 형 포인터이기 때문에 p + 1 = p + 1 * 4 가 된다.  내부적으로 4(byte) 가 곱해 지는 것이다.

당연하겠지만 4byte 길이 int 형 메모리 공간을 한 칸 이동 한다는 것은 4byte 만큼 이동한다는 것이기 때문이다.
(만일 자동으로 곱해지는 데이터 길이가 헷갈린다면 간편한 이해를 위해서는 그냥 메모리 한 칸 이동한다고 생각하자)

. 이제 위의 샘플을 실행한 결과를 보자.

포인터의 값이 4 만큼 증가된 것을 보여준다
결론적으로 포인터가 포인팅하는 주소 값이 변경된 것이다.

p 의 변경 전,변경 후 의 실제 포인팅 대상의 값을 보자.

포인팅 주소를 변경하기 전에는 int i =1 이었던 놈을 여전히 포인팅 하고 있고 변경 후에는 이상한 값이 나왔다.

이제 배열로 말해 보자.
다시 말하지만, 배열은 포인터 이며 위와 같은 포인터 연산으로 배열 각 요소를 인덱싱 하는 것이 가능하다

int arr[] = {1,2,3}  라고 정의된 배열이 있으며 이의 메모리 구조는 아래와 같다고 했다.

배열 변수인 arr 은 배열의 첫 번째 요소를 포인팅 하는 (상수) 포인터이다.

arr[1] 를 풀이해 보면,
arr 의 값을 1 증가시키고 그 값이 포인팅 하는 대상 메모리의 값을 가져온다.

1. arr  1 증가 (int 형 배열일 경우)

arr + 1 = arr + 1 * (4byte) 

사실 * 4byte는 생략해도 무방하다
arr 타입에 따라 메모리 블럭 증가는 해당 타입의 메모리 블럭만큼 되는 것은 당연하기 때문이다

2. arr 의 포인팅 하고 있는 대상 메모리 값 참조

*(arr + 1)

결국 arr[1] = *(arr + 1)  이다.

( *(p + 1) 의 식은 p[1] 과 동일하다)

 위의 예에서는 arr 을 한 칸 이동하면 0x24가 되고 이 메모리 주소의 값은 2가 되는 것이다.

지금까지의 내용을 그림으로 담아 보았다.

 

너무 장황하게 설명했지만, 배열의 인덱싱 메커니즘을 한마디로 한 마디로 요약하자면 다음과 같이 할 수 있겠다

연속된 메모리 공간을 포인터 연산으로 찾아가는 일련의 과정 

배열과 포인터에 관한 중요한 아래의 연산식을 기억하자
arr[i] == *(arr+i)

'SW개발' 카테고리의 다른 글

이런... 계집 녀(女)  (0) 2023.10.31
다차원 배열의 인덱싱  (0) 2023.10.27
배열과 포인터(Pointer) 의 상관관계  (0) 2023.10.27
리터럴(Literal) 상수  (0) 2023.10.27
ASCII  (0) 2023.10.27

배열과 포인터(Pointer) 의 상관관계

Posted in SW개발 // Posted at 2023. 10. 27. 09:15
728x90
이 글은 제가 과거에 운영했던 사이트인 http://dotnet.mkexdev.net 의 글을 옮겨온 것입니다. 원본 글은 2008년 5월일에 작성되었습니다.

그 전에 운영했었던 사이트(mkex.pe.kr)은 흔적도 없이 사라 졌습니다. 그속의 글들도 모두... 그래서 이 사이트도 사라지기 전에 옮기고 싶은 글을 조금씩 이 블로그로 이동시키려 합니다.
(원본글) http://dotnet.mkexdev.net/Article/Content.aspx?parentCategoryID=2&categoryID=9&ID=104

『 배열과 포인터(Pointer) 의 상관관계 』 

배열은 포인터이다

대부분의 프로그래밍 언어에서는 배열이라는 복합 값을 다룰 수 있는 자료구조를 제공한다.
배열과 포인터는 내부적인 처리 방식이 거의 동일하다. 단 배열변수는 상수 포인터 이다

일단 포인터의 내부 구조를 살펴 보자.
아래 샘플 코드는 int 형 변수를 가리키는 포인터를 생성해서 포인터를 통해서 i 의 값을 변경하는 예제이다.

void main(void){      
        int i = 1;
        int *p = &i;
        *p = 2;
        printf("%d",i);
}

 시스템은 내부적으로 변수를 메모리에 저장하고 참조하기 위한 key-value 와 유사한 형태의 Symbol Table 라는 것을 관리한다.  Symbol Table 에는 변수명과 값이 저장된 메모리 주소 그리고 타입정보를 저장한다.

이것은 포인터도 마찬가지이다.

, Symbol Table 에서 포인터는 자신의 타입정보 뿐만 아니라 포인팅하고 있는 대상의 타입정보도 같이 저장한다.

편의상 Symbol Table 가 다음과 같이 구성된다고 가정하자.


위의 예제를 Symbol Table 과 메모리 구조로 표현하면 다음과 같다.

int 형 변수 i  1을 저장했으므로 4byte 메모리 공간에 1을 저장하고 그 주소 값인 0x10 번지를 가리킨다.

그리고 i 의 주소 값을 포인팅 하는 포인터 변수 p 는 역시나 4byte 메모리 공간에 i 의 주소값인 0x10 을 저장하고
있는 0x100 번지를 가리킨다

 포인터 변수가 가리키는 메로리 공간에는  i 의 메모리 주소가 저장되어 있는 것이다(32 bit 시스템에서 메모리 주소 값은 4byte 로 표현되므로 포인터 p  4byte 의 메모리 공간을 차지하게
되는 것이다)

 
.. 그럼 이제 배열을 알아 보자. 다음과 같이 선언된 배열이 있다고 가정하자.

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

 이 배열을 Symbol Table 과 메모리 구조로 표현하면 다음과 같다.

배열 변수인 arr 은 배열의 첫 번째 요소(arr[0]) 를 포인팅 하는 별도의 상수형 메모리 공간인 것이다.

 arr  0x200 을 가리키고 여기에는 배열의 첫 번째 요소(arr[0]) 의 메모리 주소를 포인팅
하고 있는 것이다.
 이 구조는 포인터와 완전히 동일한 구조이다.

실제로 이 배열을 정수형 포인터에 대입하면 포인터를 배열처럼 사용할 수 있다.

void main(void){      
        int arr[] = {1,2,3};
        int *p = arr;
        printf("%d,%d,%d",p[0],p[1],p[2]); //1,2,3 이 출력된다
}

, int *p = arr 은 가능하지만 arr = p 는 성립하지 않는다.
앞서 말했듯이 arr은 상수이기 때문이다. 상수를 변경할 수는 없는 것이다.

 또 하나, 일반적인 포인터에서 *p = 10 하면 p 가 포인팅 하고 있는 곳이 변경된다.

그렇다면 위의 배열 변수를 가지고 *arr = 10 하면 어떻게 되겠는가?
arr 은 배열의 첫 번째 요소를 포인팅하는 포인터이기 때문에 위의 식은 배열의 첫 번째 요소의 값을 변경하게 된다.

void main(void){      
        int arr[] = {1,2,3};
        *arr = 10;
        printf("%d,%d,%d",arr[0],arr[1],arr[2]); //10,2,3 이 출력된다
}

이로써 배열은 포인터 이다 라는 명제를 풀어 보았다.

'SW개발' 카테고리의 다른 글

다차원 배열의 인덱싱  (0) 2023.10.27
배열 인덱싱의 메커니즘(arr[i] == *(arr+i))  (0) 2023.10.27
리터럴(Literal) 상수  (0) 2023.10.27
ASCII  (0) 2023.10.27
비트(bit) 단위 연산  (0) 2023.10.27