문타쿠, 공부하다.
[독하게 시작하는 C 프로그래밍] 섹션 15. 메모리와 포인터 본문
컴퓨터 메모리 종류
컴퓨터와 메모리
- CPU는 연산할 코드(기계어)와 대상 정보를 모두 메모리로부터 가져옴
- 메모리는 변수를 통해 사용
- 모든 메모리는 바이트 단위의 고유한 주소(=위치정보)를 가짐
- 64bit 시스템에서 메모리 주소 길이는 64bit
메모리 종류
- Stack
- 지역/자동변수, 보통 1MB
- Heap
- 동적 할당 메모리
- 실행 코드
- text section(실행 코드 기계어)
- data section: Read Only(문자열 상수), Read/Write(정적 메모리, 전역변수)
메모리 관리 함수
- malloc(), calloc() / free()
- realloc()
- memcpy(), strcpy()
- memcmp(), strcmp()
- sprintf()
C언어는 메모리 관리에 있어서 자유도가 매우 높다. => 그만큼 그에 따르는 책임도 매우 크다.
+ 메모리 관련 내용들은 OS 공부를 통해 보충하자.!
포인터 변수 기본 문법
포인터 변수
- 메모리 주소를 저장하기 위한 전용 변수
- 64bit 시스템에서 주소 상수, 포인터 변수는 모두 64bit(=8bytes)
- 1byte char형 변수의 메모리 주소는 64bit
직접 지정과 간접 지정
- 특정 메모리 공간을 int로 지정할 때 상수로 지정하면 직접 지정
- 포인터 변수로 지정하면 간접 지정
포인터와 1차원 배열
- 배열을 이루는 요소 형식에 대한 포인터 변수를 선언하는 것이 일반적
- char [ ]은 char *로 관리
- int [ ]은 int *로 관리
- 간접 지정 연산 (*)의 결과는 형식이 있는 변수로 생각할 수 있음
- 포인터 변수나 배열 이름에 대해 덧셈, 뺄셈 연산을 할 수 있음 (곱셈, 나눗셈은 x)
- 이 덧셈, 뺄셈은 산술 연산이 아니라 상대 위치를 계산하기 위한 연산이며 배열 요소의 개수를 의미
- 포인터 변수에 대해서는 단항 증/감 연산도 가능
다중 포인터 부분은 포인터를 완벽히 이해한 후 필요한 사람만 보는걸 추천.!
메모리 동적 할당 및 해제
- 메모리 동적 할당 = Heap 영역을 사용
- 프로그램 실행 중 필요한 메모리를 OS에 요청(할당)해 사용하며, 반환(해제)의 책임이 있음
- 할당 받은 메모리는 쓰레기 값이 들어 있음
- malloc() / free() -> #include <stdlib.h> 추가해야 사용 가능
- malloc()으로 메모리 동적 할당하기 -> 메모리 사용 -> free()로 메모리 해제는 선택이 아닌 필.수!
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
// int의 크기 4bytes * 5 = 20bytes 할당
// 즉 int의 크기로 5개 할당
int* pList = malloc(sizeof(int) * 5);
// 초기화 전에는 쓰레기 값이 들어가 있음
for (int i = 0; i < 5; i++)
printf("%d\n", pList[i]);
printf("\n");
// 배열처럼 인덱스로 접근하여 값 할당
pList[0] = 100;
pList[1] = 101;
pList[2] = 102;
pList[3] = 103;
pList[4] = 104;
// pList[5] = 104; -> 할당한 크기를 넘어서 값을 넣으면 에러남 주의
for (int i = 0; i < 5; i++)
printf("%d\n", pList[i]);
// 동적으로 할당한 메모리 해제
free(pList);
return 0;
}
int* pList = malloc(sizeof(int));
VS
int* pList = (int*)malloc(sizeof(int));
- 위에 두 개의 코드는 기본적으로 '메모리를 할당한다'라는 동일한 동작을 수행한다.
- 다만 첫 번째 코드에서는 형 변환을 생략하였고, 두 번째 코드에서는 명시적으로 형 변환을 수행한 것이 차이점이다.
- C언어에서 malloc 함수는 메모리를 할당할 때 (void*)형을 반환하는데, 전자는 형 변환 없이 malloc 함수의 반환 값을 그대로 사용하고 있다. -> 일반적으로 문제가 되지 않지만, 몇몇 컴파일러에서는 경고를 발생시킬 수 있다.
- 후자는 명시적으로 (int*)형으로 형 변환이 이루어지고 있다. -> 명시적으로 형 변환을 하여 컴파일러에게 반환 값의 형을 알려주고 있는데, 이것은 컴파일러가 형 변환에 관련된 경고를 발생시키지 않도록 하는 데 도움이 된다.
메모리 초기화, 복사, 비교
간단 복습
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
// 배열에 문자열
char List1[] = { "Hello1" };
printf("List1의 주소: %p\n", List1);
puts(List1);
for (int i = 0; i < sizeof(List1); i++)
printf("List1[%d]의 값: %c\n", i, *(List1 + i));
printf("\n");
// 포인터에 문자열
char* List2 = "Hello2";
printf("List2의 주소: %p\n", List2);
puts(List2);
for (int i = 0; i < sizeof(List2); i++)
printf("List2[%d]의 값: %c\n", i, *(List2 + i));
printf("\n");
// 포인터에 메모리 동적할당 후 문자열
char* List3 = (char*)malloc(sizeof(char) * 7);
List3[0] = 'H';
List3[1] = 'e';
List3[2] = 'l';
List3[3] = 'l';
List3[4] = 'o';
List3[5] = '3';
List3[6] = '\0';
printf("List3의 주소: %p\n", List3);
puts(List3);
for (int i = 0; i < sizeof(List3); i++) // List3의 크기는 7아니고 8바이트임을 명심!
printf("List3[%d]의 값: %c\n", i, *(List3 + i));
free(List3);
return 0;
}
메모리 값 초기화
- memset() -> #include <string.h>
- calloc() -> #include <stdlib.h>
- 메모리를 동적으로 할당하면서 0으로 초기화
메모리를 동적 할당한 후 0으로 초기화 하는 작업은 꼭 필요한가?
-> 꼭 필요한건 아니지만, 본인이 거기에 문자열을 저장할 것 같으면 가급적 0으로 초기화 해주는 것을 ㅊㅊ
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
// 메모리 값 초기화 방법 1: memset()으로 초기화
int* List1 = (int*)malloc(sizeof(int) * 3);
memset(List1, 0, sizeof(int) * 3);
// 메모리 값 초기화 방법 2: calloc()으로 초기화
int* List2 = (int*)calloc(3, sizeof(int));
for (int i = 0; i < sizeof(List1); i++)
printf("List1[%d]의 값: %d\n", i, List1[i]);
printf("\n");
for (int i = 0; i < sizeof(List2); i++)
printf("List2[%d]의 값: %d\n", i, List2[i]);
free(List1);
free(List2);
return 0;
}
메모리 값 복사
- memcpy() -> #include <string.h>
- 단순 대입 연산자의 두 피연산자가 모두 변수라면 메모리 값을 복사하는 것으로 생각할 수 있음
- 배열에 대해서는 단순 대입 연산으로 배열 전체를 복사할 수 없으며 반복문을 통해 개별 요소룰 하나씩 복사(단순 대입)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char List1[] = { "Hello_World!" };
char List2[13] = { 0, };
memcpy(List2, List1, 5);
puts(List2);
memcpy(List2, List1, sizeof(List1));
puts(List2);
printf("\n");
// memcpy()가 내부적으로 하는 일을 표현하자면...
char Array1[] = { "Hello_World!" };
char* Array2 = (char*)malloc(sizeof(char) * 13);
for (int i = 0; i < 13; i++)
Array2[i] = Array1[i];
puts(Array2);
free(Array2);
return 0;
}
메모리 값 비교
- memcmp() -> #include <string.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
char List1[] = { "Hi?!" };
char* List2 = "Hi?!";
// 값이 같으면 0을 반환
printf("%d\n", memcmp(List1, List2, 4));
printf("%d\n", memcmp("Hi?!", List1, 4));
printf("%d\n", memcmp("Hi?!", List2, 4));
// 값이 다르면 1을 반환
printf("%d\n", memcmp("hI?!", List1, 4));
printf("%d\n", memcmp("hI?!", List2, 4));
return 0;
}
[필수 실습 문제] 잘못된 메모리 복사
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char szBuffer[12] = { "HelloWorld" };
char* pszData = NULL;
pszData = (char*)malloc(sizeof(char) * 12);
// 방법 1
// memcpy(pszData, szBuffer, sizeof(szBuffer));
// 방법 2
// #define _CRT_SECURE_NO_WARNINGS 추가해야 함
// strcpy(pszData, szBuffer);
// 방법 3
// 방법 2 strcpy의 보안상의 문제로 strcpy_s를 권장
strcpy_s(pszData, sizeof(szBuffer), szBuffer);
puts(pszData);
free(pszData);
return 0;
}
배열 연산자 풀어 쓰기
배열 연산자 포인터로 풀어쓰기
- 배열의 이름은 기준주소
- 인덱스를 이용해 상대위치를 계산
- *(기준주소 + 인덱스)는 기준주소[인덱스]로 쓸 수 있음
#include <stdio.h>
int main(void)
{
char List[] = { "ABCDEFG" };
printf("%c\n", List[0]);
printf("%c\n", *List);
printf("%c\n\n", *(List + 0));
printf("%c\n", List[1]);
printf("%c\n\n", *(List + 1));
printf("%s\n", &List[2]);
printf("%s\n", &*(List + 2));
printf("%s\n", List + 2);
return 0;
}
문자열 복사, 비교, 검색
문자열 복사
- strcpy(), strcpy_s() -> #include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char List1[] = { "ABCDEFG" };
char* List2 = List1;
char* List3 = (char*)malloc(sizeof(char) * 16);
char* List4 = (char*)malloc(sizeof(char) * 16);
strcpy_s(List3, 16, List1);
strcpy_s(List4, 16, List2);
printf("List1이 가지고 있는 주소 값: %p\n", List1);
puts(List1);
printf("\n");
printf("List2가 가지고 있는 주소 값: %p\n", List2);
puts(List2);
printf("\n");
printf("List3가 가지고 있는 주소 값: %p\n", List3);
puts(List3);
printf("\n");
printf("List4가 가지고 있는 주소 값: %p\n", List4);
puts(List4);
free(List3);
return 0;
}
문자열 비교
- strcmp -> #include <string.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
char List1[] = { "Hi?!" };
char* List2 = "Hi?!";
// 값이 같으면 0을 반환
printf("%d\n", strcmp(List1, List2));
printf("%d\n", strcmp("Hi?!", List1));
printf("%d\n", strcmp("Hi?!", List2));
printf("%d\n\n", strcmp("Hi?!", "Hi?!"));
// 값이 다를 때 1 또는 -1을 반환
printf("%d\n", strcmp("hI?!", List1));
printf("%d\n\n", strcmp("hI?!", List2));
// 왼쪽 값이 더 크면 1을 반환
printf("%d\n", strcmp("Hi?!?!", "Hi?!"));
// 오른쪽 값이 더 크면 -1을 반환
printf("%d\n", strcmp("Hi?!", "Hi?!?!"));
return 0;
}
문자열 검색
- strstr() -> #include <string.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
char List[32] = { "I am good." };
printf("%s\n", strstr(List, "am"));
printf("Index: %d\n\n", strstr(List, "am") - List);
printf("List의 주소: %p\n", List);
printf("I의 주소 : %p\n", strstr(List, "I"));
printf("am의 주소 : %p\n", strstr(List, "am"));
return 0;
}
동적 할당된 메모리 구조와 realloc()
realloc()
- 기존에 할당 받은 메모리의 크기를 조정해 다시 할당
- 메모리 Chunk 크기 조절에 실패할 경우 다른 새로운 위치로 이동
이런 함수가 있구나~ 하고 간단한 개념과 내부적인 동작 방식만 알고 넘어가자요
대충 살피는 다중 포인터
- char*에 대해 *(char*) == char
- char**에 대해 *(char**) == char*
- char***에 대해 *(char***) == char**
다중 포인터는 특별한 경우 아니고선 잘 안쓴다고 함.
단일 포인터에 대해 정확히 이해한 후에 공부하는 것을 추천.
포인터 배열
#include <stdio.h>
int main(void)
{
char* List[3] = {"Hello",
"world",
"user!"};
printf("%s\n", List[0]);
printf("%s\n", List[1]);
printf("%s\n\n", List[2]);
printf("%s\n", List[0] + 1);
printf("%s\n", List[1] + 2);
printf("%s\n\n", List[2] + 3);
printf("%c\n", List[0][0]);
printf("%c\n", List[1][1]);
printf("%c\n\n", List[2][2]);
return 0;
}
다차원 배열에 대한 포인터
- 2차원 배열은 1차원 배열을 요소로 갖는 1차원 배열로 이해
- char aList[2][12] -> char[12]를 요소로 갖는 배열
- char (*List)[12] -> 요소가 char[12]인 배열에 대한 포인터
#include <stdio.h>
int main(void)
{
char List1[2][6] = { "Hello", "World" };
// char* List2 = List1;
char(*List2)[6] = List1;
puts(List2[0]);
puts(List2[1]);
printf("%c\n", List2[0][0]);
printf("%c\n", List2[1][0]);
return 0;
}
*주의
// int형 포인터를 4개 담는 포인터 배열
int *numPtr[4];
// 열의 크기가 4인 배열을 가리키는 배열 포인터
int (*numPtr)[4];
정적 메모리와 기억 부류 지정자
기억부류 지정자
- extern, auto, static, register
- 자동변수는 Stack을 사용하며 일반적인 지역변수는 모두 자동변수
- 정적 메모리는 프로그램 시작 시 확보되는 영역이며 프로그램 종료시까지 유지 (동시성 이슈 있음)
'C언어 > 독하게 시작하는 C 프로그래밍' 카테고리의 다른 글
[독하게 시작하는 C 프로그래밍] 섹션 17. 구조체와 공용체 (0) | 2023.11.26 |
---|---|
[독하게 시작하는 C 프로그래밍] 섹션 16. 함수 응용 (0) | 2023.11.26 |
[독하게 시작하는 C 프로그래밍] 섹션 14. 함수에 대한 기본 이론 (0) | 2023.11.12 |
[독하게 시작하는 C 프로그래밍] 섹션 13. 배열과 프로그래밍 기법 (Part 3. C언어 수준향상) (0) | 2023.11.09 |
[독하게 시작하는 C 프로그래밍] 섹션 12. 반복문 (0) | 2023.11.02 |