배열 (array)
asm(어셈블리어) 같은 경우 배열을 사용하기 위해선 메모리 주소들을 하나하나 외워서 사용해야 했었다. 하지만 c 같은 경우 변수 선언 개념을 통해서 외울 필요가 없어졌다.
int arrayA[10];
만약 배열의 크기가 10이라면 a[0] ~ a[9]로 총 10개로 나타낸다. 또한 각각의 간격의 크기는 int형으로 4byte이다. 배열은 무조건 각 위치가 붙어있다.
#include <stdio.h>
#define MAX 100
int main() {
int arrayA[MAX];
for(int i = 0; i < MAX; i++){
arrayA[i] = i * 2;
printf("arrayA[%d] = %d\\n", i , arrayA[i]);
}
// for문을 줄일 수 있으면 줄여라.
int a = arrayA;
printf("a = %d", a);
return 1;
}
이런 코드가 있다고 해보자.
먼저 for문을 돌렸을때 나오는 결과는 아래와 같아진다.
arrayA[0] = 0
arrayA[1] = 2
arrayA[2] = 4
arrayA[3] = 6
arrayA[4] = 8
arrayA[5] = 10
arrayA[6] = 12
arrayA[7] = 14
arrayA[8] = 16
arrayA[9] = 18
arrayA[10] = 20
arrayA[11] = 22
arrayA[12] = 24
arrayA[13] = 26
arrayA[14] = 28
arrayA[15] = 30
arrayA[16] = 32
arrayA[17] = 34
arrayA[18] = 36
arrayA[19] = 38
arrayA[20] = 40
arrayA[21] = 42
arrayA[22] = 44
arrayA[23] = 46
arrayA[24] = 48
arrayA[25] = 50
arrayA[26] = 52
arrayA[27] = 54
arrayA[28] = 56
arrayA[29] = 58
arrayA[30] = 60
arrayA[31] = 62
arrayA[32] = 64
arrayA[33] = 66
arrayA[34] = 68
arrayA[35] = 70
arrayA[36] = 72
arrayA[37] = 74
arrayA[38] = 76
arrayA[39] = 78
arrayA[40] = 80
arrayA[41] = 82
arrayA[42] = 84
arrayA[43] = 86
arrayA[44] = 88
arrayA[45] = 90
arrayA[46] = 92
arrayA[47] = 94
arrayA[48] = 96
arrayA[49] = 98
arrayA[50] = 100
arrayA[51] = 102
arrayA[52] = 104
arrayA[53] = 106
arrayA[54] = 108
arrayA[55] = 110
arrayA[56] = 112
arrayA[57] = 114
arrayA[58] = 116
arrayA[59] = 118
arrayA[60] = 120
arrayA[61] = 122
arrayA[62] = 124
arrayA[63] = 126
arrayA[64] = 128
arrayA[65] = 130
arrayA[66] = 132
arrayA[67] = 134
arrayA[68] = 136
arrayA[69] = 138
arrayA[70] = 140
arrayA[71] = 142
arrayA[72] = 144
arrayA[73] = 146
arrayA[74] = 148
arrayA[75] = 150
arrayA[76] = 152
arrayA[77] = 154
arrayA[78] = 156
arrayA[79] = 158
arrayA[80] = 160
arrayA[81] = 162
arrayA[82] = 164
arrayA[83] = 166
arrayA[84] = 168
arrayA[85] = 170
arrayA[86] = 172
arrayA[87] = 174
arrayA[88] = 176
arrayA[89] = 178
arrayA[90] = 180
arrayA[91] = 182
arrayA[92] = 184
arrayA[93] = 186
arrayA[94] = 188
arrayA[95] = 190
arrayA[96] = 192
arrayA[97] = 194
arrayA[98] = 196
arrayA[99] = 198
그리고 변수 a의 출력결과는 계속 달라지는데 먼저 배열의 번지수를 접근하지않고 배열의 이름만 접근해서 출력하게 된다면 그 의미는 메모리의 시작 주소를 나타내는 것이다.
= 배열의 이름 → 배열의 시작지점 → 메모리 주소
a[0] = 0
// 이것이 arrayA라는 것이다.
이게 뭔말인지 온전히 이해를 하려면 앞으로 나오는 포인터라는 개념을 이해해야한다.
포인터
- 솔직히 포인터는 아주 쉬운것이 정상이다.
하지만 난 처음에 박살났었다 ㅋㅋ
컴퓨터를 생각해보자 (메모리)
메모리라는 공간에는 01010101 이렇게 적혀있는 것이 전부다. 중요한것은 메모리입장에서는 이것이 CPU 명령어인지 변수인지 또 다른 의미인지 알 수가 없다.
결국엔 메모리에 똑같은 값이 써있더라도 CPU명령이냐 변수냐 등등 입장에 따라 결과가 달라질 수 있다는 것이다.
포인터는 → 메모리 값을 담아두는 그릇이다!
예를 들어서
int a = 10;
이 있다고 치자. 그리고 이 값이 들어있는 메모리 주소는 0x123이라고 하자
어셈블리였다면 이 메모리 주소를 기억했어야 했지만 변수 지정이라는 개념이 나온 이후 그냥 a를 이용하면 알아서 이 0x123이라는 메모리 주소를 따라서 값을 이용하게 됐다.
포인터는?
포인터도 재밌게 똑같이 메모리 공간을 차지한다.
하지만 메모리 공간에 뭘 저장하냐면 0x123(a의 메모리 주소)를 저장하고 있다. 이것의 이름표도 있는데 구분하기 위해 *모양을 붙인다.
int *pa
// 이름은 아무렇게나 지어도 된다.
포인터 또한 메모리 주소를 가지고 있다. 대충 0x200이라고 하자.
컴퓨터는 이 *pa를 접근했을때 0x123이라는 메모리 주소를 보고 그 주소에 접근을 한다. 그리고 해당 주소에 있는 값을 이용하게 되는 순서다.
물론 그냥 값을 넣을 수도 있다. 하지만 메모리 주소를 넣기로 우리는 약속을 했다. 주소를 넣을때 쌩으로 메모리 주소 0x123을 넣어도 되지만 지금에야 예시로 메모리 주소를 정해서 하였으니 쉽지 원래는 그냥 알 수가 없다.
컴파일러가 알아서 임의로 메모리 주소를 넣기 때문에 그렇다. a 주소를 알기 위해서 물어보는 키워드가 있는데 그것이 바로 **&**다.
&a라고 하면 a의 주소라는 의미가 된다!
int pa = &a;
→ a의 주소를 pa에 넣어라. (이때 *은 포인터를 구별하기 위한 이름표라고 생각하자)
이 다음에 *연산이 있다.
*pa = 4;
pa에 0x123이 들었으니 그 주소로 먼저 가라. 그 다음 해당 주소에 4를 넣어라. 라는 뜻이다.
사실상
a = 4;
와 같은 의미가 된다.
- 순서
- pa에 들어있는 메모리 주소로 간다.
- 해당 메모리 주소에 4를 넣는다.
예시
#include <stdio.h>
int main() {
int a;
int *pa;
a = 3;
printf("a = %d\\n", a);
printf("&a = %d\\n", &a);
pa = &a;
printf("pa = %u\\n", pa);
printf("*pa = %d\\n", *pa);
printf("&pa = %u\\n", &pa);
return 1;
}
// 실행 결과
a = 3
&a = 6422044
pa = 6422044
*pa = 3
&pa = 6422032
알아야 할 점은 모든 메모리는 그 메모리의 주소가 있다는 것이다.
그리고 그 주소를 하나하나 이용하기 귀찮으니 이름을 붙여서 이용하는 것 뿐이다.
사실 위의 개념, 실행결과들만 이해하면 n중 포인터도 모두 이해할 수 있다.
다중 포인터
- 어려워 하는 다중 포인터를 박살내자.
#include <stdio.h>
int main() {
int a;
int * pa;
int ** ppa;
a = 3;
printf("a = %d\\n", a);
printf("&a = %d\\n", &a);
pa = &a;
printf("pa = %u\\n", pa);
*pa = 5;
printf("*pa = %d\\n", *pa);
printf("&pa = %u\\n", &pa);
ppa = &pa;
printf("*ppa = %u\\n", *ppa);
printf("**ppa = %u\\n", **ppa);
printf("&ppa = %u\\n", &ppa);
return 1;
}
//실행 결과
a = 3
&a = 6422044
pa = 6422044
*pa = 5
&pa = 6422032
*ppa = 6422044
**ppa = 5
&ppa = 6422024
이중 포인터가 햇갈려 보이지만 위와 똑같은 원리다.
- 순서
- printf("*ppa = %u\n", *ppa);
ppa의 주소에 접근했더니 6422032라는 값이 들어있었다. 하지만 *이 있으므로 이 값을 메모리 주소로 생각한다. 그래서 그 주소로 접근하고 해당 값을 출력한다. (출력 결과가 6422044).
2. printf("**ppa = %u\n", **ppa);
1번과 마찬가지로 출력 빼고 동일하게 접근한다. 그리고 *이 두개 이므로 똑같은 일을 두번 한다는 뜻이다. ppa의 주소를 접근하고 안에 있는 값(새로운 주소)에 접근한다. → 그랬더니 pa의 주소가 나왔으며 이번엔 pa 주소를 접근하고 안에 있는 값(새로운 주소)에 접근 한다. → 최종적으로 a의 주소를 접근하게 됐다.
만약 삼중 포인터문이었다면?
**ppa 까지는 동일하게 작용했을 것이다. 그리고 마지막 ***ppa까지 왔을 경우에는 마지막 값 5가 나왔을 것이고 나온 값 5를 메모리 주소로 생각하여 0x5로 변환하여 접근 했을 것이다. 하지만 이는 메모리에 접근하면 안되는 주소일 수도 있다. 그렇기 때문에 운영체제가 철퇴를 때려버린다.
즉, 접근 하면 안되는 메모리 주소에 접근을 하려하는 프로그램을 위험 프로그램이라 생각하여 운영체제가 막아버린다.
죽어라!
- 햇갈려요
그러면 위에 있는 소스 코드를 똑같이 (복붙 하지말고) 따라치고 똑같이 그림을 그려서 하나하나 처음 부터 따라가 보면 금방 이해가 될 수 있다. 외우지 말자 나는 처음에 포인터 부분을 아예 외우면서 했는데 나중엔 더 힘들어 졌다 ㅜㅠ
삼중은 거의 안쓰니 이중 정도만 알아두자.
마지막으로
이쪽에 있는 *연산과
이쪽에 있는 *연산은 서로 다른 것이다.
printf쪽 *연산은 실제로 연산이 일어난다. 하지만 위에 있는 것은 사실상 연산이라기 보단 자료형 형태다.
int *
이 말을 해석해보자면
int형의 자료형을 만들껀데 int형 이니 크기는 4byte로 하고 앞으로 이곳엔 integer 형태의 주소 값을 넣을 것이다. 라는 의미다.
그래서 처음부터 0x123 같은 주소값을 넣거나 아니면 정수 형태의 값을 넣으면 들어가긴 들어간다. 하지만 warning이 일어난다.
🤔 (컴파일러) : 여기는 너가 아까 integer 포인터 라매? 그런데 여기에 갑자기 변수형 integer를 넣으면 어떡해?
버그아님?
하지만 c에서는 무시하고 쓸 수는 있다.
꼭 알아야하는 개념.
- 포인터 자료형 사이즈에 대한 비밀.
int *
char *
float *
double *
short *
//등등
전부다 다른 자료형들의 포인터가 등장했다. 이들이 메모리를 차지하는 실제 크기는 얼마일까?
정답은 8byte로 모두 같다!
확인해보자.
#include <stdio.h>
int main() {
int * a;
char * b;
float * c;
double * d;
short * e;
printf("%d\\n", sizeof(a));
printf("%d\\n", sizeof(b));
printf("%d\\n", sizeof(c));
printf("%d\\n", sizeof(d));
printf("%d\\n", sizeof(e));
}
// 실행 결과
8
8
8
8
8
왜지?
char 는 1byte가 나와야 할 것 같지만 8byte가 나온다.
여기에서 컴퓨터 구조 개념이 나오는데 일단은 알아야 할 것이 있다. 아까도 말했지만
int *
여기에 들어갈 것은 메모리 값이다.
그러면 왜 8byte냐? 여기선 컴퓨터 구조 개념이 필요하다.
먼저 32bit 컴퓨터는 포인터가 4byte다.
32bit 컴퓨터는 램을 4g이상 못 뽑는다. 왜? 먼저 메모리는 랜덤 엑세스가 가능하다.
이게 뭔 말이냐면 각 주소마다 일대일 매칭이 되야한다. 먼저 계산기로 32bit를 만들어 보자.
이것이 약 4g 정도 될 것이다.
메모리의 1번지당 그것에 해당하는 주소가 있어야 한다.
말이 좀 어려운가?
32bit 컴퓨터라는 말은 라인이 32개라는 말이다. 그래서 32bit 메모리 번지를 통해서 그 주소에 일대일 매칭이 되게 접근한 다음 32bit 만큼의 숫자를 한꺼번에 읽어 올 수 있는 컴퓨터란 말이다. 그리고 그 최대 수가 위에 적혀있는 약 4gb의 숫자이다.
그래서 32bit 컴퓨터에선 4gb 이상의 램을 이용할 수가 없었다. 왜냐면 그 이상 꼽아봤자 주소 할당을 못해줬었다. 그래서 주소 값을 넣기 위해선 위에 적힌 값이 들어가야한다. (최대로) 결국은 32bit를 담을 수 있는 4byte의 주소 공간이 필요하다.
그러면 왜 4byte가 아니고 8byte인가?
그 이유는 간단하다. 지금 쓰고 있는 컴퓨터가 64bit 컴퓨터라 그렇다.
결국
int *
를 입력하면 시스템을 타는 것이다.
내가 만약 32bit 컴퓨터를 쓴다면 4byte가 될 것이고
지금처럼 64bit 컴퓨터를 쓴다면 8byte가 될 것이다. 앞에 자료형이 int던 double이던 너구리던 뭐던 상관이 없다.
그러면 용량이 같은 것은 이해가 되는데 그럼 자료형을 나누지 앟고 해도 상관이없는거 아니냐 할 수 있는데 그것은 다음 포스팅때 다루겠다.
출처 : 오제이 튜브 c 강의
'언어공부 > c' 카테고리의 다른 글
C 포인터 / 배열은 포인터다? (0) | 2022.09.24 |
---|---|
리눅스로 c를 배워보자. (0) | 2022.09.12 |