Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

문타쿠, 공부하다.

[독하게 시작하는 C 프로그래밍] 섹션 15. 메모리와 포인터 본문

C언어/독하게 시작하는 C 프로그래밍

[독하게 시작하는 C 프로그래밍] 섹션 15. 메모리와 포인터

개발새발 문타쿠 2023. 11. 19. 20:59

컴퓨터 메모리 종류

컴퓨터와 메모리

  • 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을 사용하며 일반적인 지역변수는 모두 자동변수
  • 정적 메모리는 프로그램 시작 시 확보되는 영역이며 프로그램 종료시까지 유지 (동시성 이슈 있음)