여러분들께서 가장 아기다리고기다리던 배열과 포인터 시간이 다가왔습니다.
먼저 배열이란 자료형이 같은 여러개의 데이터들을 한 번의 선언으로 효율적으로 관리하기 위해 태어났습니다.
int data1,data2,data3,data4,data5; data1=1; data2=2; data3=3; data4=4; data5=5;
이렇게 같은 데이터 타입을 가진 데이터들에 일일이 입력하는 것도 관리하는 것도 여간 불편한 일이 아닐 수 없습니다.
그래서 배열을 사용 하면 다음과 같이 바꿀 수 있습니다.
int data[5]; data[0] = 1; data[1] = 2; data[2] = 3; data[3] = 4; data[4] = 5;
배열을 선언하기 위해서는 배열의 앞에 자료형을 명시하고, 배열의 이름 뒤에 '[' ']' 와 함께 배열의 크기를 지정해주면 됩니다.
32비트 운영체제에서 int는 32bit, 4byte를 할당받게 됩니다. 이 배열은 5칸을 가지고 있으므로 4byte*5 = 20byte 의 공간을 연속적으로 가지게 됩니다.
배열안의 공간을 요소(element)라고 하며, data배열의 [ ] 속의 숫자들은 색인(index)라고 부릅니다. C에서 인덱스는 인간계 순위인 1부터 시작하는 것이 아닌 0부터 시작합니다.
배열을 초기화 하기 위해서 0을 각 요소에 입력해 줍니다.
int data[5] = {0, 0, 0, 0, 0};
or int data[5] = {0,};
or int data[5] = {1,2,};
일일이 0을 넣을 수도, 두번째와 같이 0을 한번 적고 뒤는 자동으로 0을 넣을 수도, 세번째와 같이 특정 지역에 특정 값을 넣고 뒤로 자동으로 0을 넣을 수도 있습니다.
문자열을 저장하기 위해서는 여러개의 char 변수가 필요합니다. 개별 변수로 처리하면 관리가 불편하기 때문에 배열을 사용해 처리합니다.
char string[4] = "HI~"; -------> H, I, ~, 0
문자열의 끝에는 0이 있어야 하므로 저장할 크기보다 1byte 더 크게 잡으셔야 합니다.
sizeof(배열) 사용하여 배열의 크기를 확인할 수 있습니다. 이 때 속도저하는 발생하지 않습니다.
포인터
포인터는 프로그래밍 언어에서 다른 변수, 혹은 그 변수의 메모리 공간 주소를 가리키는 변수를 말합니다. 포인터가 가리키는 값을 가져 오는 것을 역참조(dereferencing)라고 합니다. 포인터는 Low level을 제어할 수 있는 어셈블리와 C, C++, PASCAL, Go 언어 등에서 많이 쓰이며 자바와 같은 언어는 숨겨져 사용할 수 없을 때도 있습니다.
포인터는 메모리를 효율적으로 사용하게 함과 동시에 문법이 가지고 있는 태생적인 단점을 극복할 수 있도록 합니다.
변수 값을 변경하려면 해당 변수의 이름으로 접근해야 가능하지만 포인터를 사용하면 해당 변수의 주소를 가지고 접근이 가능해 집니다.
주소를 사용하여 다른 변수에 접근하기 위해 특별한 능력을 가진 변수가 필요한데 이 역할을 포인터가 해줍니다.
포인터 변수를 선언할 때 *(Asterisk)를 사용하여 일반 변수와 구별합니다. 당연한 이야기지만 이 애스터리스크를 붙이지 않으면 자신의 값을 변경합니다.
다른 변수의 주소를 알아낼때에는 &(Ampersand)를 사용합니다. 포인터가 가지고 있는 주소에 접근할 때에도 *를 사용합니다.
int data = 5; int *p = &data; //포인터형 int *p; //포인터 선언 p = &data; //데이터 삽입
result: *p = 3 //data가 3으로 바뀝니다. (변수형)
포인터의 길이는 사용할 메모리 공간의 크기와 같습니다. 32비트 운영체제에서 주소의 길이는 4바이트 입니다.
자료형에 따라 공간의 크기는 다르며, char = 1byte, short = 2byte, int = 4byte, double = 8byte
void pointer 는 주소는 가지고 있지만 포인터의 크기는 알 수 없는 포인터입니다. 일명, 만능포인터라고 부르며, 알맞은 자료형으로 캐스팅 해서 사용합니다.
만약 포인터를 증감연산자 (++)를 사용하면 자신이 가진 크기만큼 증가합니다.
포인터에 수치값을 더하는 것을 포인터 연산이라고 하는데요. 이는 예제를 통해 알아보겠습니다.
int i = 0; int p[5]; int *p = p; i = i + 5; printf("%d\n", i); i = i - 2; printf("%d\n", i); p = p + 5; printf("%d\n", p); p = p - 2; printf("%d\n", p); i++; printf("%d\n", i); i--; printf("%d\n", i); p++; printf("%d\n", p); p--; printf("%d\n", p);
정수 연산은 우리가 알고 있는 정수의 연산을 뜻하지만, 포인터 연산은 포인터가 가리키는 주소의 위치 변경을 뜻합니다.
포인터 연산시 자료형에 따라 주소 뜀뛰기를 하며, 위의 경우 처럼 5를 더하면 자신의 크기만큼 5번을 움직인다는 의미입니다.
포인터 변수는 다른 변수의 주소를 기억하고 있습니다. 따라서 32비트 운영체제에서 32비트의 크기를 가집니다. 포인터 변수에 저장된 주소값은 자신이 가리킬 변수의 시작주소인데요. 여기서 어디까지 가리킬 것인지에 대한 정보가 필요한데 이 정보를 포인터의 변위로 표현합니다. 변위는 포인터 선언시 데이터 형처럼 명시합니다.
여기서 포인터의 변위와 포인터 변수가 가리키는 변수의 데이터형은 일치하지 않아도 됩니다.
int data = 0x12345678; char *p = (char *)&data; *p = 0x09;
포인터의 값을 변화시킬 때 주소의 기입방법에 따라 넣는 위치가 달라지는데요.
Endian 은 메모리에 대상을 배열하는 방법을 뜻하며, Big endian과 Little endian 법이 있습니다.
현재 대부분의 사용자가 PC용으로 사용중인 인텔 프로세서는 Little endian을 따르고 있습니다.
Little endian은 0x12345678을 저장할 때, 78 56 34 12의 순서대로 메모리에 값을 기입합니다.
하지만, ARM 프로세서 등의 RISC 방식 프로세서는 Big endian 방식을 채용하며, Little endian과 반대로 값을 저장합니다.
12 34 56 78, 이렇게 말이죠.
리틀 엔디언은 저장값의 하위바이트만 사용할 때 계산이 필요없다는 장점이 있습니다. 32비트 숫자인 0x5F는 (5F 00 00 00)으로 저장이 되는데 여기서 16비트나 8비트로 변환하기 위해서 (5F 00)과 (5F) 이렇게 하위 바이트만 떼어내면 그 숫자를 얻을 수 있습니다.
빅 엔디언은 사람이 숫자를 읽는 것과 같은 구조이기 때문에 디버깅할 때 메모리 값을 보기 편합니다.
두 방식 중 어떤 것이 좋다 나쁘다를 논할 순 없지만 Java에서 돌아가는 안드로이드는 문제가 좀 있습니다.
안드로이드 중 Java를 통해 앱을 설계하면 안드로이드는 리틀 엔디언이지만 Java는 빅 엔디언입니다. 따라서 앱을 만들고 값을 저장할 때 메모리 주소를 지속적으로 파전 뒤집듯 뒤집는 과정이 수반됩니다. 하지만 리틀 엔디언인 NDK로 앱을 작성하면 이러한 과정 없이 앱이 동작하기 때문에 동작속도가 빨라집니다.
앞서 본 포인터와 배열은 혼용이 (정확히는 표기법이) 가능합니다.
int data = [5]; data[2] = 10; //same as *(data+2)=10;
배열의 이름은 배열의 시작주소와 같습니다.
배열의 첫번째 요소는 연속적으로 나열된 배열의 가장 처음에 위치하기 때문에 첫번째 요소의 주소는 배열의 시작주소가 됩니다.
int data = [5]; &data[0]; // -> &*(data+0) -> &*data(100번지로 가서 100번지의 주소를 물어봄: 의미없음) -> data
//이렇게 하시면 안됩니다. void swap(int parm1, int parm2) { int temp = parm1; parm1 = parm2; parm2 = temp; } void main() { int num 1 = 5, num2 = 3; printf("num1 = %d, num2 = %d\n", num1, num2); swap(num1,num2); printf("num1 = %d, num2 = %d\n", num1, num2); }
//위 예제의 정답 void swap(int *parm1, int *parm2) { int temp = *parm1; *parm1 = *parm2; *parm2 = temp; } void main() { int num 1 = 5, num2 = 3; printf("num1 = %d, num2 = %d\n", num1, num2); swap(&num1,&num2); printf("num1 = %d, num2 = %d\n", num1, num2); }
이제 거의 다왔습니다. 힘내세요!!
배열은 [대괄호]를 사용하여 연속적인 메모리를 논리적으로 여러개의 묶음으로 나눌 수 있습니다. [대괄호] 갯수가 증가할 때 마다 한 차원씩 증가합니다.
2차원 배열은 1차원 배열을 두개의 묶음을 나눈 것과 같습니다.
data[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
data[2][5] = {0, 0, 0, 0, 0
0, 0, 0, 0, 0}
int data[m][n]; int one [m*n]; 1차원으로 변환 data[y][x] -> one[y*n + x] 2차원으로 변환 one[k] -> data[k/n][k%n]
2차원 배열을 배웠으니 2차원 포인터를 배워야 겠죠?
2차원 포인터는 한 차원 높은 포인터가 한 차원 낮은 포인터를 가리키는 것을 말합니다.
*(asterisk)를 이용하여 한 차원 더 높은 포인터를 선언할 수 있으며, * 개수가 증가할 때 마다 한 차원씩 증가합니다.
즉, 2차원 포인터(**)가 1차원 포인터(*)를 가리키고, 1차원 포인터(*)는 차원이 없는 일반 변수를 가리킬 수 있습니다.
4바이트 메모리에 2차원 포인터를 넣으면 해당 번지에 포인터 효과를 적용시킬 수 있습니다.
다차원 포인터는 포인터에 메모리 할당이 필요할 때 사용합니다.
int data = 3; //3 int *p = &data; //data int **pp = &p; //p
변수 혹은 데이터형의 크기를 byte 단위로 얻고싶을 때에는 sizeof 함수를 이용하면 됩니다.
int data, array[5]; sizeof(int); //4 sizeof(data); //4 sizeof(array); //20
데이터 타입을 다른 타입으로 변환할 때에는 Casting을 사용합니다.
어떤 변수가 int 형으로 선언되었다고 가정합니다.
int num;
여기서 char 타입을 쓰고 싶다면
data = (char)num;
이렇게 해주시면 4바이트가 아닌 1바이트 캐릭터형으로 사용하실 수 있습니다.
'Tech > C/C++/C#' 카테고리의 다른 글
Microsoft Tech Camp Day 3 C++의 역사부터 함수포인터까지 (0) | 2015.12.26 |
---|---|
Microsoft Tech Camp Day 2 (3) 메모리 동적할당 (0) | 2015.12.23 |
Microsoft Tech Camp Day 2 (1) C 전처리기부터 변수까지 (0) | 2015.12.16 |
Microsoft Tech Camp Day 1 (2) C의 구성요소와 함수들 (0) | 2015.12.08 |
Microsoft Tech Camp Day 1 (1) C의 역사와 자료형 (0) | 2015.12.07 |