TIL 11월 10일 2023년

ORCASUIT·2023년 11월 10일

포인터의 비교


void do_something(int* num1, int* num2)

{

if (num1 == num2) {/*주소 비교*/

/* 코드 생략 */

}

}

void do_something(int* num1, int* num2)

{

if (*num1 == *num2) {/*값 비교*/

/* 코드 생략 */

}

}
  • 포인터는 비교 연산자를 이용해서 서로를 비교할 수 있음
  • 여기서 NULL 외의 주소를 왜 비교하는지 의아할 수 있음
    - 그건 변수 하나가 아니라 큰 메모리 통째를 잡아두고 그 안에 복수의 데이터를 넣어 사용할 때 필요하다.

포인터의 크기

  • 모든 포인터는 동일한 크기를 가짐
  • 포인터 크기는 코드를 컴파일 하는 시스템 아키텍쳐에 따라 결정
    - 보통 CPU가 한번에 처리할 수 있는 데이터의 크기(word)와 동일함
    - 예 : 32비트 아키텍쳐에서 포인트 크기는 4바이트, 64비트는 8바이트

포인터의 크기 예

void print_pointer_size()

{

char ch = 'c';

int number = 934563;

float pi = 3.1415f

char* char_ptr = &ch;

int* int_ptr = &number;

float* float_ptr = π

printf("char size : %d, char* size: %d \n", sizeof(*char_ptr), sizeof(char_ptr));

printf("int size : %d, int* size: %d \n", sizeof(*int_ptr), sizeof(int_ptr));

printf("char size : %d, float* size: %d \n", sizeof(*float_ptr), sizeof(float_ptr));

}
  • char size: 1, char* size: 4
  • int size: 4, int* size: 4
  • float size: 4, float* size: 4

포인터의 크기가 4바이트…?

  • 함수의 매개변수로 전달한 배열의 sizeof()
  • 배열은 연속된 메모리 -> 그걸 다 스택에 넣을 수 없음
  • 따라서 시작 위치만 전달했음.

void print_scores(int score[])

{

size_t size = sizeof(scores); /* 4 반환 */

/* 코드 생략 */

}

매개변수로 받은 배열

  • score의 크기는 4바이트
  • socres는 my_scores의 시작 주소를 가지고 있음.
void print_scores(int scores[])

{

size_t size = sizeof(scores);

/*코드 생략*/

}

print_scores(my_scores);

포인터

  • dollar의 크기는 4바이트
  • dollar는 my_money의 시작 주소를 가지고 있음.

void print_money(float* dollar)

{
size_t size = sizeof(dollar);
/* 코드 생략 */

}

print_money(&my_money);

둘이 매우 유사함, 배열을 포인터에 배열할 수 있을까?

배열을 곧바로 포인터에 대입


int nums[6] = { 0, 1, 2,3, 4, 5 };

int* ptr = NULL;

ptr = nums; /* 컴파일 됨 */

ptr = nums[0]; /* 컴파일 오류 */

왜 nums[0]은 안 될까?

incompatible integer to pointer conversion assigning to 'int *' from 'int'

  • nums[0]은 intint*가 아님.

nums[0]의 주소를 얻으려면

  • &을 붙인다.

int nums[6] = { 0, 1, 2,3, 4, 5 };

int* ptr = NULL;

ptr = nums; /* 컴파일 됨 */

ptr = &nums[0]; /* 컴파일 됨 */

배열 속에서 각 요소의 위치

  • 배열에서 각 요소사이의 바이트 간격은 일정하다.

각 요소의 위치 계산하기

  • 따라서, 첫번째 요소의 주소와 자료형의 크기만 안다면 두번째 요소의 주소를 알아낼 수 있음
    - 두 번째 요소 주소 = 첫 번째 요소 주소 + 자료형의 크기(바이트)
    - 세 번째 요소 주소 = 두 번째 요소 주소 + 자료형의 크기(바이트)
    - …

포인터에 정수를 더한다는 건

  • 포인터에 정수 1을 더한다?
    - 포인터의 위치를 다음 데이터의 위치로 이동, 1 바이트를 더하는 게 아님.

  • 뺄셈도 마찬가지.

  • ++나 --도 마찬가지

그 덕에 이 두 코드는 같은 의미

int* ptr1 = nums + 3;

int* ptr2 = &nums[3];

배열 요소에 포인터로 접근하기 예


int nums[3] = { 10, 20, 30 };

int* ptr = nums;

printf('%d, %d, %d \n", nums[1], ptr[1], *(ptr + 1));

20, 20, 20

배열 요소에 포인터로 접근하기 1

  • 배열명은 시작 주소이기 때문에 포인터 변수에 대입할 수 있다.

int* ptr = nums;

  • 근데 재밌게도 배열의 첨자 연산자([]) 도 포인터에 쓸 수 도 있음.

printf("%d, %d, %d \n", nums[1], ptr[1], *(ptr+1));

nums[1] = ptr[1] = *(ptr +1)

  • 이 모두 컴파일러에게 똑같은 의미

  • 지금 이 연속되는 메모리에서 시작에서 한 칸 건너 뛰어서 두 번째를 보여줘

  • 그게 어디에 있다고?

  • 지금 데이터는 int, int는 4바이트

  • 그러면 4바이트 한 번 건너뛰면 되지

  • 포인터 산술 연산에도 배열 첨자 연산자에도 동일하게 적용됨

배열의 모든 요소 더하기 예1


int sum(int* data, const size_t length)

{

int result = 0;

size_t i;

for (i = 0; i < length; ++i) {

result += data[i];

//result += *(data + 1);

}

return result;

}

/*메인 함수 */

int nums[6] = { 0, 1, 2, 3, 4, 5 };

int result = sum(nums, 6); /* 15 */

정말 딱 '한' 바이트만 옮기고 싶다면?

  • int -> char 캐스팅은 무엇을 캐스팅 하는 걸까?

  • 그 메모리 주소에 어떤 형이 들어있는지 알려주는 것?

  • 바꾸고 나면 실제 이 속에 있는 데이터 내용은 char* 일까? No.


int int_array[] = { 27, 65};

int* int_ ptr = int_array;

int_ptr (char*)int_ptr + 1;

한 바이트만큼 이동 후 거기서 4바이트를 읽음.

두 주소 간의 사칙연산

  • 뺄셈을 제외한 사칙 연산은 모두 지원 안함

  • 두 주소를 더한다고 무슨 의미가?

  • 곱,나눗셈은 더더욱…

  • 뺄셈의 경우 두 주소 사이에 들어갈 수 있는 데이터 수를 반환

  • 따라서 포인터가 아니라 정수를 반환

`int sub = &nums[5] - &nums[1]; / 4 /

  • 배열의 첫 번째 및 마지막 요소의 주소를 알면 배열의 크기를 구할 수 있음
  • 두 주소 사이에 오프셋이 몇인가? 를 알 수 있음.

자바와 C#에서는 모든 것이 포인터

  • C#의 기본 자료형 외의 모든 것은 사실 포인터와 같다
  • 자바도 마찬가지.
  • 다른 언어들에서는 주소 이동을 허용하지 않음 왜? 안전하지 않으니까.

안전하지 않은 코드


int i ;

int num = 1024;

int nums[3] = { 34, 135, 49 };

int* ptr = nums;

for (i = -1; i <= 3; ++i) {

printf("%p: %d\n", (void*)(ptr + i), *(ptr + i));

}
  • 배열 외의 데이터에 접근하게 된다.

안전하지 않다고 안 쓰기엔

  • 그렇다고 안 쓰기엔 너무나 강력함

  • 값을 복사하는 것보다 주소에 접근하는 게 훨~!씬 빠름.

  • 무작정 안 스는 것보단 잘 쓰는 게 좋음

  • 훌륭한 프로그래머가 되기 위함.

  • 포인터 정도는 씀.

  • C/C++ 프로그래머가 엄청난 존경을 받는 이유도 이걸 할 수 있어서.

포인터와 배열의 차이

  • 대부분의 경우 포인터와 배열은 동일하게 처리가능하나 이 둘이 다를 때도 있다.
  1. sizeof 연산자
  • sizeof(배열) 과 sizeof(포인터)는 다른 값을 반환

  • sizeof(배열) : 배열의 총 크기를 반환

  • sizeof(포인터): 포인터의 크기를 반환


int nums[3] = { 34, 135, 49 };

int* ptr = nums;

size_t size1 = sizeof(nums);

size_t size2 = sizeof(ptr);
  1. 문자열 초기화
  • C는 C#이나 Java 처럼 문자열(string) 자료형이 없음

  • 그럼 어떻게 문자열을 표현?

  • char 배열을 이용해서 문자열을 표현

  • 가령 "Friday" 라는 단어를 저장한다면 총 6rodml 요소를 가진 char배열을 만듦

  • 문자열이 끝나는 지점을 알려주기 위해 널 문자(null character)라고 하는 특별한 문자를 항상 맨 마지막에 넣어줌

  • 널 문자 : 값은 0으로 \0 <-백 슬래시와 0을 합쳐서 표현함.

  • 이 문자열을 초기화 하는 방법은 두가지

  • 방법 1

  • char day1[] = "Monday";

  • 배열에 차례대로 문자가 들어간 후 마지막에 \0 가 들어감

  • 함수 안에서 사용하면 스택 메모리에 저장됨

  • 함수 호출할때 배열을 위한 스택메모리를 잡아주는데 다른 메모리(데이터 섹션)에서 가져와 스택에 담아줌.만약 함수 밖, 파일에 있어도 데이터 섹션에서 가져옴.

  • 방법 2

  • `char* day2 = "Monday";

  • 포인터 변수는 스택에 저장

  • 실제 문자열은 데이터 섹션에 저장

  • 스택에 저장된 문자열은 수정해도 괜찮지만 데이터 섹션에 저장된 문자열은 수정할 경우 '결과가 정의되지 않음'

  • 후자의 경우 문자열이 읽기 전용

  1. 대입
  • 포인터 변수에 값을 대입할 수 있으나 배열 변수에는 할 수 없음.
  • 한번 배열을 만들면 그 주소는 고정되어 있으므로 다른 주소로 바꿀 수 없다.
  1. 포인터 산술 연산
  • 포인터는 산술 연산이 가능하지만 배열은 불가능
  • 배열의 주소를 증가하거나 감소하고 싶다면 포인터에 배열의 주소를 대입 후 그 포인터 변수를 증가/감소하면 됨

연산자 결합 법칙

  • 연산자 결합 법칙은 사실 별로 고민할 이유가 없음

  • 익숙한 것들은 그냥 쓰고 아닌 것들은 괄호 치는게  일반적

  • 그래서 사람들이 별 신경 안씀

  • 연산자 결합 법칙이란?

  • 동일한 우선순위를 가지는 연산자들이 있으면 어떤 방향으로 연산자를 적용하냐 의미 (왼 쪽에서 오른쪽 , 혹은 오른쪽에서 왼쪽)

  • 대부분이 왼쪽에서 오른쪽 그래서 신경 안씀

  • 왜 C에서는 이걸 묻냐? 다른 언에서는 안봐서, 익숙하지 않은 연산자 *, &가 나와서…


int num = *p++;

int num = *++p;

int num = ++*p;

int num = (*p)++;
  • C를 자주 쓰는 사람이라면 암기해야함.
  • 아니라면 괄호를 쓰자 실수를 예방할 수도 있고 포인터가 없는 언어에 익숙한 사람도 읽기 편함.

조금 더 빠른 배열의 요소 더하기 함수


int sum(int* start, int* end)

{

int result = 0;

int* p = start

while (p < end) {

result += *p++;

}

return result;

}

/* 메인 함수 */

int nums[] = { 10, 20, 30, 40, 50 };

int result = sum(nums, nums + 5);
  • 당연히 *p++ 이렇게 접근하는게 배열보다 빠름. 왜?
  • 배열은 언제나 첫 주소 + 요소 위치까지의 오프셋
  • 포인터는 이미 다음 주소에 가있기에 그대로 참조

*p++ 이 더 빠른가?


int sum(int* start, int* end)

{

int result = 0;

int* p = start;

while (p < end) {

result += *p++;

}

return result;

}
  • 요즘 컴파일러는 최적화를 잘해서 두 코드가 비슷한 성능을 보일 듯.
  • 그러나 포팅도 생각해서 C에서는 *p++ 을 더 많이 씀
  • C 스타일 문자열을 배우면서 다양한 함수 만들 때도 봄

int display_user(int* id, char* name)

{

int result;

/* id를 읽기 전용으로 막 사용 */

/* name를 읽기 전요응로 막 사용 */

/* 코드 100줄 */

*id = 0;

/* 코드 100줄 */

return result;

}

포인터와 const

  • 누가 변수를 수정하는 걸 막으려면 어떤 게 필요하다?
  • 근데 포인터의 const는 매우 헷갈림
  • 그것은 const로 보호할 것들이 두개가 있기 때문.

const 포인터 : 주소를 보호함

  • 기본 자료형 변수의 경우 const 를 붙이면 그 변수에 저장한 값을 변경할 수 없었음

  • 보통 이게 반드시 필요하다고 느끼진 않음.

  • 따라서 이걸 반드시 붙이라고 강요 안하는 코딩 표준도 많음

  • 실수가 발생해도 큰 문제가 발생하지 않기 때문

  • 함수 범위 내에서 발생할 수 있는 실수를 막는 정도

  • 그럼 포인터 변수에 const를 붙이면 뭑 ㅏ바뀌지 말아야 할까?

  • 포인터 변수에 저장되어 있는 것은 무엇

  • 메모리 주소

  • 그래서 const 포인터는 메모리 주소를 바꿀 수 없음

  • 포인터 변수는 오른쪽에서 왼쪽으로 읽음 따라서.

  • int* const p = &num;

  • p is a const pointer to int

  • 뭐가 const? 그 주소 자체, 즉, 포인터

  • 왜 오른쪽에서 왼쪽으로 읽냐? 두 번째 const 때문.

const 변수

  1. 생성과 동시에 초기화해야함
  2. 초기화 이후 다른 값으로 변경 불가
  3. const 가 아닌 변수에 대입은 가능
  4. (포인터 전용) const 포인터가 가리키는 대상의 값은 변경가능

const int* p = &num1; /* 방법 1 */

int const * p = &num1; /* 방법 2 */
  • 실수가 있을 경우 함수 내에서 뿐만 아니라 전역적으로 문제가 발생

  • 이게 바로 전의 경우(주소 보호) 보다 더 중요

  • 이 const는 반드시 신경써야 함

  • 그 주소에 저장되어 있는 값을 변경하는 것을 방지

  • 오른쪽에서 왼쪽

  • 방법 1 : "p is a pointer to int, which is const"

  • 방법 2 : "p is a pointer to const int"

  • 논리적으로 방법 2가 더 말이 되나 흔히 방법 1로 씀

  • 포인터 아닌 int를 const로 만들때도 const int 라고 함으로 비슷하게 보이게하려고 방법 1을 많이 씀

두 const 의 정리

int const  p = &num1; / 그 메모리 주소에 저장되어 있는 값을 변경하는 걸 금지 */

const int p = &num1; / 메모리 주소를 변경하는 것을 금지 */


// 주소를 보호하는 const 포인터

char ch = 'A';

char* const p = &ch;

*p = 'Z';

// 주소가 가리키는 값을 보호하는 const 포인터

char ch1 = 'A';

char ch2 = 'A';

const char* p = &ch1;

p = &ch2;

두 const 합체!

const int* const p = &num;

  • 오른쪽에서 왼쪽 읽기

  • "p is a const pointer to const int"

  • 초기화 된 후 절대 바뀌지 않는 변수가 있을 때 정도만 유용.

  • 예 : 전역변수, 구조체 멤버 변수

  • 주소에 저장되어 있는 값을 보호하는const가 더 중요함.

최종적으로 읽는 방법 정리


const int* p = &num; /* 주소에 저장된 값을 바꿀 수 없음 */

int const* p = &num; /* 주소에 저장된 값을 바꿀 수 없음 */

int* const p = &num; /*p가 가리키는 주소를 바꿀 수 없음 */

const int* const p = &num; /* 둘 다 바꿀 수 없음 */
  • 헷갈릴 때는 오른쪽에서 왼쪽으로 읽어보면 됨
  • * "포인터, 무엇을 가리키냐면"
  • p는 포인터 무엇을 가리키냐면.. int const
  • p는 const 포인터, 무엇을 가리키냐면 const int
  • p는 const 포인터, 무엇을 가리키냐면 int
  • p는 const 포인터, 무엇을 가리키냐면 int const

const를 제거할 수도 있음.

  • 근데 제거하지 말아야함.

  • 기본 자료형에서 큰 문제가 아니나

  • 어차피 매개 변수가 값을 복사해옴

  •  그 매개 변수의 값을 바꾼다고 원본이 바뀌지 않음

  • 그러나 const를 가리키는 포인터의 경우 문제가 됨

  • const를 제거하고 값을 바꾸면 원본이 바뀜

const 베스트 프랙티스

  • const는 최대한 다 붙이는게 좋다

  • 반드시 const가 필요없는 경우가 아니라면!

  • const 캐스팅은 하지 말 것

  • 함수 시그니처에서 안 바꾼다고 약속하고 어기지 말자.

포인터의 용도

  1. 큰 데이터를 함수의 매개변수로 전달 할 때
  • const 배열에 요소가 한 10개 정도 있다면 크지 않을 수 있음
  • 근데 요소가 한 10만개 정도 있다면
  • 자료가 커질 수록 데이터를 복사하느라 시간을 낭비함
  • 그래서 배열이 매개변수로 전달될 경우, 첫 번째 요소의 주소를 전달
  1. 반환 값이 둘 이상일 때
  • C에서 return 문으로 불가능
  • 언제나 하나만 반환해야함
  • 하지만 포인터를 사용하면 함수 안에서 원본을 직접 변경할 수 있음
  • 원본의 값을 읽지 않고 그냥 덮어 쓰는 거라면 반환이나 마찬가지
  • 최대값, 최솟값을 한번에 반환하는 함수의 예에서 볼 수 있음.
  1. 동적 메모리 할당
  • 함수의 범위에 상관 없이 한동안 사용하고자 하는 데이터가 있는데 다음과 같은 경우에 해당하면 사용

  • 그 데이터의 크기를 컴파일 도중에 알 수 없거나

  • 프로그램 실행수명 보다는 짧은 시간 동안만 사용하려고 할 때

  • 동적으로 할당된 메모리는 역시 연속된 메모리 덩어리

  • 따라서 포인터가 적합 (배열과 비슷한 이유)

  1. 그 외..
  • 데이터 구조를 구현할 때

  • 연결 리스트, 트리 등과 같은 데이터 구조에 포인터가 적합

  • 임베디드 프로그래밍 등에서 하드웨어에 있는 메모리에 직접 접근해야 할 때

  • 예 : 어떤 하드웨어는 화면을 보여주려면 특정 메모리 위치에 이미지 데이터를 직접 복사해줘야 함.

포인터 배열

  • 포인터도 그냥 변수니 당연히 포인터를 저장하는 배열도 있음

  • 그러면 어떻게 배열을 선언해야할까?

  • int* num_pointers[3];

  • C# : string[][] classrooms = new string[3][]; 과 비슷한 개념.

배열의 배열과 비슷한 개념

  • 바깥 쪽 배열은 행, 안쪽 "배열"은 열
  • 각 행마다 열의 길이가 달라질 수 있다

포인터 배열의 예


int num1[3] = { 11, 22, 33};

int num2[1] = { 90 };

int num3[4] = { 88, 36, 37};

int* num_pointers[3];

num_pointers[0] = nums1;

num_pointers[1] = nums2;

num_pointers[2] = nums3;

내부 배열의 길이도 알려줘야함

  • 함수에서 접근하려면 각 내부 배열의 길이를 알려주는 size_t 배열이 필요

void print_array(int* const data[], const size_t size, const size_t lengths[])

{

size_t i;

size_t j;

const int* p;

for (i = 0; i < size; ++i){

p = data[i];

printf("nums[%d]:" , i);

for (j =0; j < lengths[i]; ++j) {

printf(" %d", p[j]);

}

printf("\n");

}

}

2D 배열도 배열의 배열처럼 만들 수 있을까?

  • 2D 배열은 어차피 한 덩어리 메모리라 주솟값이 저장된 곳이 없음
  • 올바른 방법

void do_magic(int matrix[][10], size_t m)

{

}
  • m은 행의 수
  • 이러면 컴파일러가 매개변수가 2차원 배열이라는 걸 인지함
  • 그리고 matrix[1][] 할 때 몇 개를 건너뛰어야 하는지 앎

정리

  • 포인터
  • 주소 연산자, 역참조 연산자
  • 널 포인터
  • 포인터와 두 두가지 const
  • 포인터 산술 연산
  • 포인터와 배열
  • 포인터 배열

0개의 댓글