[C] C lang 포인터
포인터
우리가 알던 메모리 저장 과정
나도 주소를 사용해서 메모리에 있는 내 변수에 접근이 가능한가?
다른 언어였다면 얼토당토 않은 일
- 그러나 C는 하드웨어와 가장 친한 친구
- 그러므로 가능함
- 그리고 이걸 잘 쓰면 매우 강력한 일도 가능함(사고 치는 거도 가능하고..)
주소 연산자 &
지역변수 주소 출력하기
#include <stdio.h>
void print_address(void)
{
int num =10;
printf("Address of num : %p\n",(void *) &num);
}
int main(void)
{
print_address();
return 0;
}
주소 연산자 &
비트 연산자 &가 아님.
비트 연산자는 피연산자가 2개, 주소 연산자는 피연산자가 1개
const unsigned char result = num1 & num2; /* 비트연산자 */ printf("address of num %p\n", (void*) &num); /* 주소 연산자 */
num 이라는 변수가 있으면 그 &num은 그 변수가 위치한 메모리 주소
- 보통 주소 보여줄 떄는 16진수를 사용
- 읽기 편하기 때문
- 그래서 printf()에서 서식문자 %p는 주소를 16진수로 보여줌
- 참고: 실행할 때마다 주소가 달라질 수 있음
- 요즘 운영체제는 보안 강화를 위해 실행시마다 주소를 바꿔줌(ASLR)
메모리 주소 저장하기
근데 이건 어떻게 쓰나?
- 주소를 구할 때 마다 그 변수가 필요하면 그냥 그 변수를 쓰지
- 그 보다는 그 주소를 어딘가에 저장하면 좋을텐데
- 그러면 변수가 없어도 주소만으로 여기저기 옮겨다닐 수 있음
- 가령 ‘변수의 주소’ 다음 메모리 위치를 읽는다던가..
혹시 메모리 주소를 저장할 수 있는가?
- 어떤 데이터를 저장 시 쓰는 건 -> 변수
- 숫자, 문자 다 변수에 저장하는데?
그럼 메모리 주소도 변수에 저장할 수 있지 않을 까?
- 메모리 주소도 숫자긴 함.(조금 큰 수)
주소 저장해보자
void try_save_address(void)
{
int num =10;
int num_address = #
}
컴파일 하니까 오류나는데? 포인터를 저수로 반환하는게 에러남. 또 int에 별표달렸네? 여기보면 또 뭐가 쭉 나옴. 아 안되네
만약 이게 되도 문제
이 중 어떤게 주소고 어떤게 값?
void play_game(void)
{
int a = 202020;
int b = 212121;
}
- 이러면 정말 헷갈릴듯.
- 따라서 주소 저장하기 위한 특별한 변수 가 있어야 함.
-> 그 특별한 변수가 포인터 이다.
포인터의 의미
- 주소를 저장하기 위한 변수형
- 이건 외워햐한다 -> 변수인데 속에 담긴 내용은 메모리 주소
- 다른데선 뭘 가리키고 어쩌고 설명하는데 이렇게 외우면 더 헷갈리게 된다.
결국 포인터는 메모리를 저장하는 변수
그 주소에 저장된 자료형은?
- 사실 하드웨어는 신경 안 씀
- 그 비트패턴을 char로 읽으면 char고
- int로 읽으면 int고,
- float으로 읽으면 float이 됨
- 예 : 주소 0x100으로 접근시 ‘char’로 읽는다면
- 그러나 해당 주소에서 부터 몇 바이트를 읽어야 하는지는 하드웨어에 알려줄 필요가 있음.
- 그래서 포인터 변수 선언 시에는 ‘ 그 주소에 어떤 형의 데이터가 있는지 ‘ 명시하기 위해 포인터 앞에 자료형을 붙임
- int 포인터, float포인터, char 포인터..
포인터 변수를 선언하는 방법
void save_address(void){
int num = 10;
int * num_address = #
}
포인터 변수 선언하려면 자료형 뒤에 *를 붙임 int * char * float *
코딩 표준: 자료형에 *를 붙인다
- int *address로 쓰는 경우도 있음
- 우리의 코딩 표준은 int * address;
위 둘다 같은거.
포인터 변수를 부르는 법.
void save_address(void){
int num = 10;
int * num_address = #
}
- 보통 num_address를 int 포인터라고 부름
- 근데 영어로는 int로의 포인터(pointer to an int)라고도 함
- 포인터는 읽을 떄 오른쪽에서 왼쪽으로 읽음. 다른건 왼쪽에서 오른쪽에서
- 포인터는 메모리를 저장하는 변수 라고 했다.
- 이 변수는 메모리 어딘가에 저장되어 있다.
- 따라서 아래 예처럼 되어 있음
- 이처럼 다른 위치를 가리키는 변수라 해서 포인터라고 함.
- 근데 이렇게 기억하면 이중 포인터 나올 때 헷갈릴 수 있음
- 따라서 기본 개념은 메모리 주소를 저장하는 변수 라 기억하고 가끔 시각적으로 필요하면 그려볼 것
- 처음엔 잘 이해 안되니 그려보면서 하는게 좋다.
- 눈으로만 보면 헷갈리게 됨
포인터 변수의 실제 메모리 뷰
실제 메모리
num이라는 변수의 주소, num_address 안에는 뭐가 들어가 있나. -> num의 주소가 들어가 있다.
잠깐 복습하면 데이터가 끝나는 마지막 단위가 가장 작은 메모리 주소에 위치하는 저장순서를 리틀 엔디언이라 했다.
다른 포인터 예 : char 포인터
void create_char_pointer(void){
char ch = 'c'; /* 주소 : 0x007EF7D3
char * ch_pointer = &ch; /* 저장된 값 : 0x007EF7D3
}
cc에서 4바이트 더하면 D0이 되고 1,2,3, 더하면 D3이 된다.
다른 포인터 예 : short 포인터
void create_short_pointer(void){
short num = 2154; // 주소: 0x00F3FE36
short * num_pointer = # // 저장된 값 : 0x00F3FE36
}
다른 포인터 예 : float 포인터
void create_float_pointer(void){
float num = 2154; // 주소: 0x012FF7F8
float * num_pointer = # // 저장된 값 : 0x012FF7F8
}
다른 포인터 예 : double 포인터
void create_double_pointer(void){
double num = 2154; // 주소: 0x005DF9D8
double * num_pointer = # // 저장된 값 : 0x005DF9D8
}
포인터에 저장된 주소도 바꿀 수 있나?
- 물론, 가능
- 포인터도 변수(중요하니까 반복)
- 따라서 포인터에 저장한 값도 변경 가능
- 즉, 다른 주소로 바꿀 수 있음.
역 참조 연산자 *
포인터와 함수 매개변수
포인터도 변수니까 당연히 변수 쓰는 곳에는 다 쓸 수 있다.
따라서 매개변수도 가능
void print_address(int)
{
printf("address of num :%p\n" , (void *) num);
}
//메인함수
int score = 98;
print_address (&score);s
- 근데 이 함수에서 뭔 짓을 할 수 있지?
- 주소만 보여주는 건 의미 없을텐데
주소에 저장된 값 출력하기
주소 가져와서 출력해줌.
역참조 연산자 *
- 곱하기 연산자 아님
- 곱하기 연산자는 피연산자 2개를, 역참조 연산자는 피연산자 1개를 가짐
const int result = num1 * num2
printf(*num)
- 포인터가 저장하고 있는 메모리 위치로 가서
- 거기에 저장된 값에 접근(읽거나 쓰거나)함
- 포인터가 가리키는 값에 접근한다고도 말함
참조와 역 참조
- 참조
- 포인터가 이미 하고 있는 일
- 어떤 변수의 값을 직접 가져다 쓰는 게 아닌, 그게 어디 있다고 ‘참조’
- 즉, 값이 어디 있는지 가리키고 있는 것
- 역 참조
- 주소로 직접 가서 저장되어 있는 값에 접근하는 것
- 참조(reference)의 반대라 역참조
실제 데이터에 간접적으로 접근
- 값에 직접 접근하는 게 아님.
- 주소를 이용해 간접적(한 단계 거쳐서)으로 접근
- 따라서 간접 연산자라고도 함
- 매우 중요한 개념이므로 반드시 숙지해야함
- 여태까지는 모든 데이터를 복사해서 썼다.
- 이제 그렇게 안하고 원본 접근 가능
- 컴퓨터 구조에서 데이터를 오랫동안 메모리에 저장하는 방법
- 바로 눈에 보이지 않는 추상적 개념을 이해하는 능력이기도 함.
역참조를 이용한 값 변경의 예
- 이 두 코드는 결과적으로 동일하다.
포인터 변수 선언 vs 역참조
헷갈리지 말자
- 앞에서 굳이 int * 를 코딩 표준으로 삼은 이유도 헷갈리지 말라는 뜻
int score = 100;
int * pointer = &score; // 포인터 변수 선언
*pointer = 50; //역참조
포인터로 두 변수의 값 바꾸기
위 스왑은 실제로 교체가 안된다.
- 스택 메모리에 값을 복사하기 때문
- C#은 ref키워드로 해결
- C는 그런거 없다.
이걸 포인터로 이용해서 바꿔야 한다.
값에 의한 전달 vs 참조에 의한 전달.
- 전에 매개변수로 배열 전달할떄 했듯이 의견이 분분
- 원본이 바뀌니 -> 참조에 의한 전달
- 근데 메모리 주소 복사했는데? -> 값에 의한 전달
아주 엄밀히 말하면 C는 값에 의한 전달
- 함수를 호출 할 떄 언제나 변수(그게 포인터든 아니든) 복사를 함
- 고로 값에 의한 전달
- 단 , 포인터를 사용해서 참조에 의한 전달을 흉내 낼 뿐.
- 뭐든 간에 말장난 여기서 중요한건
- 원본이 바뀌는지 안바뀌는지가 제일 중요
- 그걸 굳이 xx에 의한 전달 이라고 해야한다면
- 참조에 의한 전달 이라고 말하는 게 차라리 나음
포인터와 함수 반환 값
- 당연히 포인터도 변수니까 함수 반환값으로 사용 가능
int * do_something(const int op1, const int op2)
- 다만 포인터 반환시 주의할 점이 있음
지역변수의 주소를 반환: 매우 위험한 코드
int* add (const int op1, const int op2)
{
int result = op1+ op2;
return &result;
}
int main(void)
{
int * result;
result = add(10,20);
return 0;
}
컴파일러도 문제 있는 걸 안다.
포인터가 잘못된 주소를 가르킴
- 함수의 지역변수는 어디 저장되나? => 스택
- 함수 호출이 끝나면 지역변수도 사라짐
여기서 함수를 나가면 스택 메모리에서 나가게 됨(add 프레임에서 나가게 되고, 더이상 유효하지 않음)
댕글링 포인터(dangling pointer)
- 지역변수가 사용한 “주소” 자체가 사라지는 것은 아님.
- 따라서 그 주소를 반환한다고 컴파일 오류가 나지는 않음.
- 컴파일러에 따라 경고는 줄 수 있음
- 문제는 포인터가 유효하지 않은 주소를 가리키는 것.
- 이 경우 예측하지 못한 결과가 발생할 수 있다.
- 잘못된 메모리를 가리킨다? 엄처어어ㅓ엉 큰 문제 발생ㄴ
- 이러한 포인터를 댕글링 포인터라고 할 수 있다.
- 절대 작성해선 안 되는 코드
포인터와 함수 반환값 다시 보자.
- 포인터를 반환 할 경우 댕글링 포인터 조심
- 포인터 반환해도 되는 경우
- 전역 변수
- 파일 속 static 전역변수
- 함수 내 static 변수
- 힙 메모리에 생성한 데이터
- 언제 포인터를 반환할까
- 도우미 함수 안에 생성한 변수를 다른 함수에서 사용하고자 할 때
- 이건 아까 말한 정적변수라던가. 힙 메모리에서한. 아주 좋은 패턴 아닌데 가끔 해야될 때가 있음.
- 단, 일반 지역변수면 안됨(함수 호출이 끝나면 스택에서 사라짐)
- 함수 안에서 대용량 데이터를 생성하고 그걸 반환하고자 할 떄
- 이 경우에는 데이터를 스택메모리가 아니라 힙 메모리라는 곳에 생성함.
- 도우미 함수 안에 생성한 변수를 다른 함수에서 사용하고자 할 때
널(NULL) 포인터
- 근데 반환할 주소가 없는 경우는 어떻게 해야하나?
- 메모리 주소에서 유효하지 않은 값은 어디냐라는 질문과 같다.
void do_something()
{
int number;
int * num_ptr = &number;
num_ptr = NULL; //얘는 아무것도 안 가르팀. 주소가 없다.
//이걸 대입하면 실제 메모리주소가 없다고 말하는 것
}
- 아무것도 가리키지 않는 포인터
- 값이 ‘0’인 상수 표현식, 혹은
- void * 로 캐스팅 된 표현식
- 전용 매크로가 있음
- #define NULL((void*)0)
널 포인터 표현시 이 매크로를 사용할 것.
- 포인터 변수와 NULL은 비교 (==, !=) 가능
int *ptr;
if(ptr==NULL) //만약 ptr이 널 포인터면
{
}
if (ptr!=null) //만약 ptr이 널 포인터가 아니라면
{
}
코딩 표준 : 매크로 NULL을 반드시 사용할 것.
- 0은 사용하지 않는다.
포인터와 if 쓸때는 재밌는게 많다 이전에 if문 쓸떄는 0은 자동으로 거짓이 되고 아니면 참이 되서 C는 넣는 경우가 있는데 가독성을 위해 넣자고 했다.
포인터에선 의미가 바뀔 수 있음.
포인터 ptr이 있는데 null이고 말고 비교안하는 코드가 있을 떄
그럼 이렇게 이해도 됨. 포인터가 존재하면 뭘해라 아니면 0이면(false)하지 말아라
이거를 직접 안 붙여도 명확하게 할 수있음.
근데 일관되게 하려고 오른쪽 처럼함.(NULL)
NULL이 가지는 문제점들
NULL은 골칫덩어리다. : 매개변수편
- 어떤 함수의 매개변수로 포인터가 들어온다. 근데 포인터가 들어올 떄 그 포인터의 주소가 유효하다고 믿고 싶다. min,max 구하는 거 처럼. 근데 메모리 주소 없는데 어떻게 함수롤 왜 호출하지? 말이 안되는데? 이제 복잡해진다. NULL이라는게 포인터가 들어올 떄 NULL이 될 수 있다는 거 자체가 복잡해 지게 되는 것.
이런건 함수의 선조건으로 해결하는 게 좋다. 함수를 호출했는데, 참조형 타입, 아니면 NULL포인, 포인터가 들어온다고 해서 그게 NULL이 될 수 있다는 가정을 하고 그것을 다 해결하는 함수를 작성하는 것 자체가 함수의 본래 의도를 너무 벗어남. 그래서 기본적으로 함수를 작성할 때는 매개변수로 넘어오는 것, 얘는 무조건 NULL이 아니라고 기본 가정을 하고 시작하면, 그리고 회사 안에서 규율이 다 그래버리면 코드 작성 시 깨끗하다.
- 근데 NULL을 집어넣어야 하는 함수들도 있음.
- 그런 경우는 매개변수 이름에서 분명히 밝히자는 의미
- 함수 매개변수로 포인터가 들어올 때는 언제나 골치 골칫덩어리
- 누구나 NULL을 넣을 수 있기 떄문
- 함수의 선 조건(precondition) 문제
- 기본적으로 NULL이 안 들어온다고 가정하고 함수를 작성할 것
- NULL 이 들어올 수 있는 함수는 매개변수 명에서 분명히 밝힐 것.
코딩표준 : 널 포인터를 허용하는 매개변수도
- 함수의 매개변수가 널 포인터를 허용한다면, 매개변수 이름 끝에 ‘_or_null’을 붙인다.
int get_score(const char * const student_id_or_null)
{
}
그러면 null이 들어와도 함수가 알아서 잘 처리해준다. 그렇지 않으면 null이 들어오면 제대로 작동하지 않는 선 조건이 존재하게 됨. 선 조건은 반드시 NULL 이 아니어야 선조건으로
그러면 assert()가 날 수 있음. 프로그램에서 assert()를 넣는다, 실행중에 NULL이 들어오면, 또 릴리스 빌드에서 NULL이 들어오면, 프로그램이 오작동 하거나 크래시가 난다. 그래서 반드시 적어주는게 좋은 표준이긴 하다.
NULL이 안 들어 온다고 가정하는 경우 assert()를 사용해 검증한다.
#include <assert.h>
#define PRICE(2)
void increase_price(int* current_price)
{
assert(current_price != NULL)
*current_price += PRICE;
}
NULL은 골치덩어리다 : 반환값
- NULL을 반환할 떄도 마찬가지
- 기본적으로 함수는 NULL을 반환 안함
- 근데 NULL 반환을 해야한다면 (NULL 반환이 올바른 함수면) 함수 이름에 NULL을 반환하는 것을 명시할 것.
코딩 표준: 널 포인터를 반환하는 함수 명
- 함수가 널 포인터를 반환할 수 있다면, 함수 끝에 ‘or_null’을 붙인다.
const char *get_name_or_null(const int id) { return null; }
널 포인터는 언제 사용하나?
- 포인터 변수를 초기화 하고 싶을 떄
- 아직 참조할 주소가 없을 떄
void do_something(void)
{
int * ptr = NULL //당장 사용하지 않으므로 널 포인터로 초기화
//코드생략
ptr = &g_monster_count; //전역 변수의 주소 저장
}
포인터 변수 초기화 하고 싶을 떄 가끔 사용한다. C나 C++ 같은 언어는 여기에 스택에 딱 공간을 잡을 뿐, 이 값을 초기화 해주지는 않는다.
즉, 여기에 세미콜론(;)만 적어두면 ptr 뒤에 초기화를 안 해줌.
그래서 그 값이 예전에 스택 사용한 사람들이 사용한 또 함수들이 남겨둔 값, 그 똑같은 위치로 내 로컬변수, 지역변수가 생겼기 떄문에 그 값은 그냥 가지고 있다.
그래서 그거 쓰는 순간 실제 메모리 주소 가보니 하드웨어가 보호하고 있는 메모리거나 접근이 불가능하면 크래시 나고 그럼. 그런거 방지하기 위해 NULL로 초기화 하는 경우들이 있다.
성능이 중요하면 이런거 까지 초기화 하지 말라는 곳도 있고 안정성 떄문에 초기화 권장하는 회사도 있다.
프로그래머가 자기 역량에 따라 함,
C#이나 다른언어는 초기화 하라고 강요하는 부분은 C는 프로그래머에 맡김. 그래서 중요.
- 포인터 변수가 유효한 주소를 참조하는지 확인하고 싶을 떄
- 아무것도 가리키지 않는 포인터 변수를 역 참조하면?
- 결과가 정의되지 않음(undefined behavior)
만약 유효하지 않은 주소를 역참조 한다거나, NULL같은거, NULL로 대입해준 것들 얘를 곧바로 역참조 하려고 하면 존재하지 않는 주소를 역참조 해야한다.
c표준에선 이걸 어떻게 돌아야 한다고 정의하나? => 정의하지 않고 있음.
어떻게 될 지 얘기를 안해둠. 그렇기 때문에 무슨 일이 일어날 지 모름.
일반적으로 크래시가 난다고 말함. OS에서 잡는 경우가 많음.
결과가 정의되지 않은 코드. 널 포인터 크래시가 바로 이 경우.
이게 포인터의 위험성 1 이다.
- 올바르게 쓰려면 역참조를 하기 전에 널 포인터인지 확인 할 것.
- 댕글링 포인터를 막기 위해
- 동적 메모리 할당된 메모리를 더 필요 없어서 해제했는데, 이를 여전히 가리키는 포인터가 있다면?
- 더 이상 사용할 수 없는 데이터니, 포인터 변수에 저장되어 있는 그 주소를 초기화 해야함.
- 이떄 널 포인터를 이용해 리셋함.
- 댕글링 포인터 얘기할 떄 동적 메모리 할당해서 내가 메모리를 사용하려고 불러옴. S에 나 메모리 큰거 필요하니까 스택에 있는 거 말고 내가 오래 쓸 수 있는 메모리 좀 줘 해서 받아옴. 아니면 스택 메모리에 들어갈 수 없는 용량이야 큰 거 줘 이렇게 함. 받아오면 이거 어떻게 쓰는 지는 중요하지 않다. 그걸 한 거고 포인터에 들고 있다.
그러다가 어느 순간 포인터를 지운다. 이게 지우는게 포인터를 지운다. 기보다 그걸 지워달라고 운영체제에 부탁함. 그럼 이제 포인터가 가리킨 주소는 지워진 메모리. 그럼 댕글링 포인터와 같은 개념이 나온다. 그걸 다시 NULL로 초기화 안해주고 있으면 포인터에 주소값은 들어가 있음.
free 한다고 주소를 지워주는 게 아님. 메모리를 지워줌
주소는 여전히 ptr이라는 변수에 들어가 있음.
걔를 나중에 변수 봤는데 NULL이 아님. 어 유용한가 쓰려고 하면 그 순간 문제가 발생함. 막 실행 문제 없이 됐는데 2시간 후 문제 생길수 있고 이럼. 유효하지 않은 포인터에 대해 값을 갑자기 적어버리는 행위. 이걸 메모리 스톰프 라고 한다. (메모리를 짓밟는다.)
그걸 막기 위해 이미 지운 메모리인걸 확인하기 위해 ptr = NULL 처럼 널 포인터 명시해준다.
결과적으로는 존재하지 않는 메모리 주소에서 값을 읽어오려고 하면 문제가 엄청 터진다!
포인터 이런건 메모리 크래시 이런거로 문제가 보이게 된다. 대부분 메모리 이상한 걸 작성한 것. 그래서 포인터 관리 못하면 정말 문제가 많이 터지고 포인터 대충 이해하고 쓰면 차라리 C 언어 안 쓰는게 낫다.
포인터의 비교
- 포인터를 비교할 수 있다?
- 널 포인터와 비교한 거 생각
- 주소를 비교하는 코드
void do_something(int *num, int* num2)
{
if(num1 == num2){ //주소 비교
}
}
- 값을 비교하는 코드
void do_something(int *num, int* num2)
{
if(*num1 == *num2){ //주소 비교
}
}
주소 비교하는 코드는 둘이 똑같은 주소 비교할 수 있음. 근데 주소 달라도 값 다를 수 있기도 함.
메모리 주소 비교하는게 첫번쨰, 메모리 주소 상관 없이 값 비교하는게 두번쨰
- 포인터는 비교연산자를 이용해 서로 비교 가능
- ==, >,<,>= , != 등..
- 여기서 NULL 외의 주소를 외 비교하는 지 의아할 수 있음.
- 그건 변수 하나가 아니라 큰 메모리를 통째로 잡아두고
- 그 안에 복수의 데이터를 통쨰로 넣어서 사용할 때 필요
- 메모리 통쨰.. 어디서 본거같다.
포인터의 크기
- 포인터는 결국 주소 저장하는거. 그럼 포인터의 크기는 주소의 크기.
- 그 주소는 컴퓨터 따라 달라진다.
- 모든 포인터는 동일한 크기를 가짐.
- 포인터 크기는 코드를 컴파일 하는 시스템 아키텍처아 따라 결정
- 보통 cpu가 한번에 처리할 수 있는 데이터 크기.(=워드,word)와 동일함.
- 예 : 32 비트 아키텍처에서 포인트 크기는 4바이트, 64비트 아키텍처에서 포인트 크기는 8바이트.
- 32비트에서 가리키는 주소는 항상 4바이트, 64는 8바이트 이런식(char,int double 상관 없이)
- 데이터 크기는 1,4, 8이 맞다. 데이터와 주소 크기 구분해야.
CPU에서 주소의 크기와 CPU 그 아키텍처의 비트수와 맞추는 경우가 많다.(일반적.) char도 실제 저장된 거 상관없이 주소는 4바이트. int라고 주소 바뀌나? 안 바뀜. 주소는 주소일 뿐. int도 4바이트, double도 4바이트
예전이 32비트 아키텍처가 2의 32승까지 표현이라 4기가 못 넘어가니 이런말 나오던거랑 비슷한 맥락
포인터의 크기가 4바이트라
함수에 매개변수로 배열 전달하면 sizeof() 했을 때 배열의 실제 길이가 안 나왔음. 총 바이트수가, 무조건 4바이트가 나왔었음.
- 함수의 매개변수로 전달한 배열의 sizeof()
- 배열은 연속된 메모리 -> 그걸 다 스택에 넣을 수 없음.
- 따라서 시작 위치만 전달함.
void print_scores(int scores[])
{
size_t size = sizeof(scores); //4반환
}
배열은 전에 말했든 구멍이 없이 촘촘히 연속된 메모리다. int 4개짜리 만들어 int 4개가 줄줄이 서있다.
16바이트가 있고 이걸 스택에 다 넣을수 없을 수 있으니 시작 위치만 전달(배열이 10짜리면?) 그래서 시작 위치만 전달. (메모리 주소)
이 메모리 주소 하는 거 시작주소 넣어줌. 그럼 배열은 어디에 들어가 있나? 스택 메모리에 줄줄이 들어간다.
포인터와 배열의 비교
- 그럼 배열을 포인터에 대입할 수 있지 않나?
배열 포인터에 대입하기
int nums[6] = {0,1,2,3,4,5};
int * ptr = NULL;
ptr = nums; //컴파일 됨.
ptr = nums[0] //컴파일 에러
배열 그 자체의 이름넣음. 컴파일도 정상으로 된다.
위 사진에서 nums의 주소(시작주소). 거기 안에 메모리 값 보면 많은 값이 들어가있는게 보인다.
ptr 보면 0x006ffcb8로 감. int형임. 처음이 0이여야됨. 봤더니 0000. 0이다.
배열의 이름 자체는 시작주소. 그 주소를 포인터 변수에 대입할 수 있다.
근데
ptr = nums; //컴파일 됨.
ptr = nums[0] //컴파일 에러
이렇게 아래처럼 대입하면 컴파일 에러가 남.
첫번쨰 요소를 넣으면 에러가남. 에러 내용은 Integer를 포인터로 변환할수 없다고 에러 띄움.
왜 nums[0]은 안 될까?
- 오류 메시지를 보면 답이 나온다.
- incompatible integer to pointer conversion assigning to ‘int * ‘ from ‘int’;
- int형은 주소가 아님.
- nums[0]은 int지, int* 가 아니다.
- 말이 아 다르고 어 다른데 포인터는 이런식으로 세세하게 볼 수 밖에 없다.
nums[0]의 주소를 얻으려면?
- nums의 첫번쨰 요소는 nums[0]이 맞음
근데 이 아이의 주소를 얻으려면 하나 더 필요함.
nums[0]은 값인데 그 값의 위치는 배열의 두번째 위치에 있다!
- 배열은 줄줄이 비엔나인데 여기서 두번째에 위치한다는 뜻.
- 이 값의 위치를 알고싶으면? 이 값이 저장된 메모리 주소 앞에다 주소 연산자 붙이면 됨. 주소 얻으려면
오류메시지에 힌트 줌.
주소 연산자 써야됨.
두번쨰꺼 가지고 싶으면 nums[1]해두고 두번쨰꺼 주소 내놓으라 함.
int nums[6] = {0,1,2,3,4,5};
int * ptr = NULL;
ptr = nums; //컴파일 됨.
ptr = &nums[0] //주소연산자 주면 컴파일 됨.
즉 아래 코드는 동일한 결과
ptr = nums; //컴파일 됨.
ptr = &nums[0] //주소연산자 주면 컴파일 됨.
배열 속 각 요소의 위치, 각 요소의 위치 계산하기
- 배열에서 각 요소 사이의 바이트 간격은 일정.
int nums[5];
char chars[5];
각 요소의 위치 계산하기
- 따라서, 첫 번째 요소의 주소와 자료형의 크기만 안다면 두 번쨰 요소의 주소를 알아낼 수 있음
- 두번 쨰 요소 주소 = 첫 번쨰 요소 주소 + 자료형의 크기(바이트)
- 세번 쨰 요소 주소 = 듀 번쨰 요소 주소 + 자료형의 크기(바이트) … (반복)
int * ptr = nums;
ptr = ptr + sizeof(int)
여기서 int 크기만큼 더하면 nums[1]이 아닌 nums[4]로 간다.(메모리 주소로 간게 아니네?)
포인터에 정수를 더한다는 건
- 포인터에 정수 1을 더한다?
- 포인터의 위치를 다음 데이터의 위치로 이동. 1바이트를 더하는 게 아님. 예:
int * ptr = nums; //ptr : 0x100 ptr = ptr +3;
0x100+4+4+4 //int 사이즈를 3번 더함. = 0x10C
int * 가 아니라 short* 라면? 2바이트씩 증가.
- 뺄셈도 마찬가지
- ++, – 같은 증감 연산자도 마찬가지.
- 포인터의 위치를 다음 데이터의 위치로 이동. 1바이트를 더하는 게 아님. 예:
그래서 이 두 코드는 같은 의미
int * ptr1 = nums +3; //ptr1는 nums[3]를 가리킴.
int * ptr2 = &nums[3]; //ptr2는 nums[3]를 가리킴.
배열 요소에 포인터로 접근하기
int nums[3] = {10,20,30};
int * ptr = nums;
printf("%d, %d ,%d", nums1[], ptr[1], *(ptr+1));
이런거도 되나? 배열 원소를 포인터로 접근하기.
배열 명은 시작 주소이기 떄문에 포인터 변수에 대입할 수 있다
- int * ptr = nums;
근데 재밌게도 배열의 첨자 연산자 ([]) 더 포인터에 쓸 수도 있음.
- printf(“%d, %d ,%d”, nums1[], ptr[1], *(ptr+1));
nums[1] == ptr[1] == *(ptr+1)
- 이 모두 컴파일러에게 똑같은 의미
- 지금 연속된 메모리에서 시작에서 한 칸 건너 뛰어서 두번째를 보여줌.
- 그게 지금 어디있는지?
- 지금 데이터는 int, int는 4바이트
- 그러면 4 바이트 한 번 건너뛰면 된다.
- 포인터 산술 연산에서도 배열 첨자 연산자에서도 똑같이 적용됨.ㄴ
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+i) /* 이 방법이 약간 더 빠름 */
}
return result;
}
//메인함수
int nums[6] = {0,1,2,3,4,5,6};
int result = sum(nums,6); //15
result += data[i];
result += *(data+i) /* 이 방법이 약간 더 빠름 */
에서 앞에 방법이 약간 더 좋은거 같긴 한데, 포인터 쓰는게 약간 더 빠름. 이 방식은 아니고 빠른 방식이 있다.
포인터에 들어가는 값은 주소
- C에서는 이 주소를 얻기 위한 방법은 딱 2가지
- 주소 연산자 (&)
float pi = 3.14f;
float *p = π
- 배열의 이름
- 배열의 이름은 배열의 시작 주소를 알려 줌.
int days[] = {1,2,3,,31};
int *p = days;
포인터에 정수를 더하면 주소 이동
- 포인터에 정수 n을 더하거나 빼면 언제나 “sizeof(자료형) *n” 한 만큼 메모리 주소 이동
char* char_ptr = char_array; //0x100;
char_ptr = char_ptr+10; //0x10A
int * int_ptr = int_array; //0x100
int_ptr = int_ptr+10; //0x128
정말 딱 한 바이트만 옮기고 싶으면?
한 바이트 짜리 포인터로 캐스팅
- int_ptr = (char*) int_ptr+1;
- 캐스팅이 무엇인지는 프로그램 입문에서 배움
- 그러니 포인터의 캐스팅이 무엇인지 살펴보면 됨.
int * -> char * 캐스팅은 무엇을 위해 캐스팅 하나?
- 그 메모리 주소에 들어있는 값?
- 그 메모리 주소에 어떤형이 들어있는 지 알려주는 거?
-> 답은 2번.
딱 ‘한’ 바이트만 옮기기
첫 번쨰 꺼는 int* 부터 말이 안됨. 이 캐스팅이 말이 안 되는 거. int, char 캐스팅을 해야지. int, char 캐스팅이 아니니까.
그래서 어쩔수 없이 2번쨰 꺼
어떤 포인터 형이여도 크기는 일정. 32비트 기계 쓸떄 4바이트.
그래서 int형 int포인터에서 char포인터로 바꾼다고 바이트 수가 바뀌는 것도 아님.
결국 포인터 벼수에 저장된 그 주소는 바뀌지 않는다.
그럼 뭐가 바뀌나? 그 주소로 갔을 때 몇 바이트를 읽어와서 어떤 데이터형으로 실제 값을 읽어와야 하는지 그 의미만 바뀜.
그래서 그 주소에 가면 어떤형이 들어있는지 알려주는 거만 바뀌는 거.
- 바꾸고 나면 실제 이 속의 데이터 내용은 char*인가? -> 아님.
- 그래서 프로그래머가 이상한 짓을 할 수 있는 부분.
int int_array[] = {27,65};
int * int_ptr = int _array;
int_ptr = (char*) int_ptr+1;
4바이트 옮겨서 65를 읽는게 아니라 1바이트만 이동
포인터를 바꿔서 1 증가후 대입 다시함. 그니까 1만큼 증가함. 27 = 1b 00 00 00 65 = 41 00 00 00(리틀엔디언이라 실제 읽으면 반데)
근데 한 바이트 이동 후 뒤에부터 읽으니까 41 00 00 00
근데 가보니까 말도 안되는 값이 나옴. 그래서 피곤하다.
int 메모리 뷰어
#include <stdio.h>
int main(void)
{
const int num = 0x12345678;
const char * num_address = (char*) #
//4바이트씩이 아니라 1바이트씩 접근하겠다. 4바이트가 아니라 1바이트씩 점프해가면서 보겠다.
size_t i;
for (i = 0; i< sizeof(num) ; ++i)
{
printf("%hhx", *(num_address+i));
}
printf("\n");
printf("num in hex form : 0x%x", num);
return 0;
}
두 배열이 겹치는가
#include <stdio.h>
#include "memory.h"
int main(void)
{
int nums1[10] = {1,2,3,4,5,6,7,8,9,10};
int nums2[5] = {1,2,8,9,10};
int * nums = nums1+2;
const size_t NUMS_LENGTH = 5u;
char * result = NULL;
result = is_overlap(nums1, ARRAY_LENGTH(nums1), nums2,ARRAY_LENGTH(nums2))
? "YES" : "NO";
printf("Are nums1 and nums2 overlapped? : %s\n", result);
result = is_overlap(nums1, ARRAY_LENGTH(nums1), nums3, NUMS3_LENGTH)
? "YES" : "NO";
printf("Are nums1 and nums3 overlapped? : %s\n", result);
return 0;
}
두 주소의 사칙연산
주소에 소수점을 더하거나 뺄 수는 없음.
- 주소엔 정수만 더하거나 뺄 수 있음.
int nums[6] = {0,1,2,3,4,5};
int *ptr = nums + 2.5f;//컴파일 오류
두 주소간에 덧셈,곱셈,나눗셈은 안되지만, 뺄셈은 가능하다.
int * ptr1 = &nums[1] + &nums[2]; //컴파일 오류
int sum = &nums[1] + &nums[2]; //컴파일 오류
int * ptr1 = &nums[1] * &nums[2]; //컴파일 오류
int sum = &nums[1] * &nums[2]; //컴파일 오류
int * ptr1 = &nums[5] / &nums[1]; //컴파일 오류
int sum = &nums[5] / &nums[21]; //컴파일 오류
int * ptr1 = &nums[5] - &nums[2]; //컴파일 오류
int sum = &nums[5] - &nums[2]; //컴파일 가능
- 뺄셈을 제외한 사칙 연산은 모두 지원 안함.
- 두 주소를 더한다고 무슨 의미가 있나?
- 곱셈 나눗셈은 더더욱..
- 뺼셈의 경우, 두 주소 사이에 들어갈 수 있는 데이터 수를 반환
- 따라서 포인터가 아니라 정수를 반환
INT SUB = &NUMS[5] - &NUMS[1]; // 4
=(0x00fafc64- 0x00fafc54) //int 크기 = 0x10 /0x4 = 16/4 =4
- 배열의 첫 번쨰 및 마지막 요소의 주소를 알면 배열의 크기를 구할 수 있음.
- 배열의 첫번쨰 요소와 마지막 요소를 알면 배열 크기 알기 가능.
자바와 C#에서는 모든 것이 포인터다
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++ 프로그래머가 존경 받는 거도 이걸 문제없이 다룰 수 있기 때문
포인터와 배열의 차이
- sizeof 연산자
- 이미 본 내용
- sizeof(배열)과 sizeof(포인터)는 다른 값을 반환
- sizeof(배열): 배열 총 크기를 반환
- sizeof(포인터): 포인터의 크기를 반환
int nums[3] = {34,135,49}
int * ptr = nums;
size_t size1 = sizeof(nums); // 12= 3*4;
size_t size2 = sizeof(ptr); //4;
- 문자열 초기화
- C는 C#이나 JAVA 처럼 문자열(STRING) 자료형이 없음.
- 그럼 어떻게 문자열을 표현하나?
- char 배열 이용해서 문자열 표현
가령 “Friday”라는 단어 저장하면 총 6+1 개의 요소를 가진 char배열을 만듦.
문자열이 끝나는 지점을 알려주기 위해 널 문자(null character)라고 하는 특별한 문자를 하앙 맨 마지막으로 넣어줌.
널 문자 : 값은 0으로 ‘\0’. 백 슬래시와 0 합쳐서 표현
문자열 초기화 하는 방법은 2가지
방법 1.
char day1[] = “MONDAY”;
- 배열에 차례대로 ‘M’,’O’,’N’,’D’,’A’,’Y’가 들어간 후 마지막에 \0이 들어감
- 함수 안에서 사용하면 스택 메모리에 저장됨.
방법 2.
char day2 = “MONDAY”;
- 포인터 변수는 스택에 저장
- 실제 문자열은 데이터 섹션에 저장
스택에 저장된 문자열은 수정해도 괜찮지만, 데이터 섹션에 저장된 문자열은 수정할 경우 “결과가 정의되지 않음”
- 후자의 경우 문자열이 읽기 전용
char day1[] = "MONDAY";
char* day2 = "MONDAY";
day1[0] ='P'; //OK
day2[0] ='P'; //결과가 정의되지 않음.
- 대입
- 포인터 변수에 값을 대입할 수는 있으나, 배열 변수에는 할 수 없다.
- 배열은 한번 정해지면 주소를 바꾸기 불가능하다. 주소 고정되서 다른 주소로 바꾸기 안된다.
int *pointer1;
int *pointer2;
int array1[5];
int array2[5];
int x = 5;
pointer1 = array1;
array1= pointer1; //컴파일 오류
pointer1 = &x;
array1= &x; //컴파일 오류
pointer1 = pointer2;
array1= array2; //컴파일 오류
- 포인터 산술 연산
- 포인터는 산술 연산이 가능하지만 배열은 불가능
- 배열의 주소를 증가하거나 감소하고 싶다면, 포인터에 배열의 주소를 대입 후 그 포인터 변수를 증가/감소하면 됨
++pointer;
--pointer;
pointer +=1;
pointer -=1;
++array; //컴파일 오류
--array; //컴파일 오류
array +=1; //컴파일 오류
array -=1; //컴파일 오류
다시 만나는 연산자 결합 법칙
- 연산자 결합 법칙은 별로 고민할 이유가 없다.
- 익숙한 것들은 그냥 쓰고 아닌 것들은 괄호치는 게 일반적
- 그래서 사람들이 별 신경 안 씀
- 연산자 결합 법칙이란?
동일한 우선순위를 가지는 연산자들이 있으면 어떤 방향으로 연산자를 적용하냐의 의미(왼쪽에서 오른쪽, 혹은 오른쪽에서 왼쪽)
- 대부분이 왼쪽에서 오른쪽 -> 그래서 신경 안 씀(전에도 나옴)
- 왜 C에선 이걸 묻나? 다른 언어에서는 안 봐서 익숙하지 않은 연산자. * 혹은(&)가 나와서..
포인터와 연산자 우선순위 및 결합 법칙
- int num = *p++;
우선순위 | 연산자 | 연산자 결합 법칙 |
---|---|---|
1 | ++, – (후위 연산) | -> |
2 | ++, –, * (전위 연산) | <- |
위 경우엔 p++가 우선임. p를 먼저 증가한다.
여기서 p로 평가 아직 p = p+1 실행 안함
0x104에 가서 134를 읽어옴
그리고 num에 134를 대입함. 그 뒤 후위 증가해서 p=p+1실행 0x108이 p에 저장됨.
- int num = *++p;
우선순위 | 연산자 | 연산자 결합 법칙 |
---|---|---|
1 | ++, – (후위 연산) | -> |
2 | ++, –, * (전위 연산) | <- |
*과 ++의 두 연산자의 우선 순위가 같지만, 연산자 끼리 합치지는 않음.(왼쪽에서 오른쪽은 어차피 말 안됨)
0x108 값에 접근해서 값을 가져온다. 68을 가져와서 num에 저장한다.
- int num = ++*p;
우선순위 | 연산자 | 연산자 결합 법칙 |
---|---|---|
1 | ++, – (후위 연산) | -> |
2 | ++, –, * (전위 연산) | <- |
p가 104였음. 그래서 0x104에 접근하고 접근하면 134. 그리고 접근한 값을 1 증가시킴 -> 135
- int num = (*p)++; //괄호가 있으니 명확
우선순위 | 연산자 | 연산자 결합 법칙 |
---|---|---|
1 | ++, – (후위 연산) | -> |
2 | ++, –, * (전위 연산) | <- |
p는 134로 평가됨. 아직 1을 더하진 않음. 평가하고 대입부터 해야 함.
그리고 그 다음 1를 증가 그래서 135가 됨.
동일한 우선순위를 갖는 연산자들
- 참고: 동일한 우선순위를 가지는 연산자들은 결합방향이 다 같다.
- 당연한 것.
- 그게 아니라면 컴파일러가 동일한 우선순위를 갖는데, 다른 결합방향을 갖는 표현식을 어떻게 평가하나?
C를 자주 쓰면 암기
- 아니라면 괄호를 쓰자. 실수를 예방할 수도 있고, 포인터가 없는 언어에 익숙한 사람도 읽기 편함.
조금 더 빠른 배열의 요소 더하기 함수
int sum(int *start, int *end)
{
int result = 0;
int *p = start; //주소 복사해옴, start의 주소 복사해오는거로, p의 주소 바꾸는거로 start주소 바뀌진 않음.
while(p<end)
{
result+= *p++;
}
return result;
}
//메인함수
int nums[] ={10,20,30,40,50};
int result = sum(nums, nums+5); //시작 위치, 끝 위치 전달
p가 지금 여긴데 나를 다음 위치로 옮기고 더해줘 이런식. 그래서 이런식으로 배열 접근하는 걸 포인터로 접근이 가능하다.
함수 시그니쳐가 배열이면 arr에 length가 들어왔다 치면(매개변수가 들어왔다 치면) int *p = arr int *end = arr+length;
- 당연히 *p++이렇게 접근하는ㄱ네 배열보다 개미 눈곱만큼은 빠르다.
- 배열은 언제나 첫 주소 + 요소 위치까지의 오프셋
- 포인터는 이미 다음 주소에 가있기 떄문에 그대로 참조함.’
데이터는 언제나 첫번쨰 요소 가리킴. 여기서 &data[0]+ (i *4) 여기서 i 만 증가해가면서 늘려가며 접근함.
근데 또 다른 방식은
맨 처음 10에 있다. 그리고 뭐를 하나? 다음 위치 이동.(p +=4) 이걸 반복
그래서 처음 위치에서 몇 바이트 몇바이트 덧셈을 해서 그 위치에 가서 읽어올 필요가 없는 것
이미 나는 다음에 읽어올 위치에 가있어서 아주 조금 더 빠르다.
요즘 컴파일러는 최적화 잘해서 두 코드가 비슷한 성능을 보일듯.
- 근데 포팅도 생각해서 C는 여전히 *p++를 더 많이 씀
- 나중에 C스타일 문자열 만들면서 다양한 함수 사용할 떄도 이 방식이 많이 사용
포인터와 const
int display_user(int *id, char* name)
{
int result;
//id를 읽기 전용으로 막 사용
//name을 읽기 전용으로 막 사용
//코드 100줄
*id=0 //만약 이런 코드가 있으면?
return result;
}
이런 코드가 있다고 가정한다. 참고로 포인터는 모두 다 알아야.
유저 정보 출력하는데 둘다 읽기전용으로 썼다.
근데 id가 바뀌네?
근데 누군가 *id=0을 중간에 넣으면?
int*로 되어 있어서 바꿔도 되는 줄 알았는데 읽기전용인 줄 몰랐다.
그래서 나온게 const
- 누가 변수 수정하는 걸 막는게 const
- 근데 포인터의 const는 헷갈림. 포인터는 const로 보호할 게 2개나 있기 떄문
주소를 보호하는 const 포인터
- 기본 자료형 변수의 경우 const를 붙이면 그 변수에 저장한 값을 변경할 수 없었음.
- 보통 이게 반드시 필요하다고 느끼진 않음.
- 따라서 이걸 반드시 붙이라고 강요 안하는 코딩 표준도 많음.
- 실수가 발생해도 큰 문제가 발생하지 않기 때문
- 함수 범위 내에서 발생할 수 있는 실수를 막는 정도
void do_something(const int op1) { op1 = 20; //컴파일 오류 }
- 그럼 포인터 변수에 const 붙이면 뭐가 바뀌지 말아야 하나?
- 포인터 변수에 저장되어 있는 것은 무엇?
- 메모리 주소
- 그래서 const 포인터는 메모리 주소를 바꿀 수 없다.(본질을 보면 됨)
- const int 하면 int형 변수 못 바꾼거 처럼 const int * 하면 이 int형 포인터 메모리 주소 못바꾼다.
그럼 const int *a라 쓰면 되나? -> NO
포인터 변수는 오른쪽에서 왼 쪽으로 읽음 따라서
int * const p = #
이런식이 되어야 한다.
영어로 표현시 p is a const pointer to int
- 뭐가 const? 그 주소 자체. 즉, 포인터
- 왜 오른쪽에서 왼쪽으로 읽나? - > 두번쨰 const 떄문
위 사진에서 p 값을 못 바꾼다.
값을 보호하는 const를 가리키는 포인터
const 변수
- 생성과 동시에 초기화 해야함.
- 초기화 이후 다른 값으로 변경 불가
- const가 아닌 변수에 대입은 가능
- (포인터 전용) const 포인터가 가리키는 대상의 값은 변경 가능.
const를 가리키는 포인터 : 값을 보호함
const int * p = &num1 //방법1
int const * p = &num1 //방법2
- 실수가 있을 경우, 함수 내에서 뿐 아니라 전역적으로 문제 발생
이게 바로 전의 경우(주소 보호)보다 더 중요
- 이 const는 반드시 신경써야함
- 그 주소에 저장되어있는 값을 변경하는 것을 방지
오른쪽에서 왼쪽으로 읽음.
- 논리적으로 방법 2가 더 말이 되나, 흔히 방법 1로 씀.
- 포인터 아닌 int를 const로 만들 때도 const int라고 하므로 비슷해 보이려고 방법 1을 더 많이 쓰는듯
- 그래서 방법 1이 코딩 표준에 좀 더 많이 쓰임.
두 const의 정리와 예
- 메모리 주소를 변경하는 것을 금지하는 것과,
- 그 메모리 주소에 저장되어 있는 값을 변경하는 것을 금지.
이 두개를 구분해야 한다.
두 const의 예
주소와 값 모두 지키는 const
두 const 합체
const int * const p = & num;
- 역시 오른쪽에서 왼 쪽 읽기
- 초기화 된 후, 절대 바뀌지 않는 변수가 있을 떄 정도만 유용할 듯
- 예) 전역변수, (나중에 다룰) 구조체 멤버 변수
- 주소에 저장되어 있는 값을 보호하는 const가 더 중요함.
const 포인터 읽는 방법 정리
최종적으로 읽는 방법 다시 정리
const int* p = # //주소에 저장된 값을 바꿀 수 없음.
int const* p = # //주소에 저장된 값을 바꿀 수 없음
int* const p = # //p가 가리키는 주소를 바꿀 수 없음.
const int* const p = # //둘 다 바꿀 수 없음.
헷갈릴 떄는 오른쪽에서 왼쪽으로 읽어보자. *를 “포인터, 무엇을 가리키냐면” 이라고 바꿔보자🤔
p는 포인터, 무엇을 가리키냐면 int const(const int) p는 포인터, 무엇을 가리키냐면 const int p는 const 포인터, 무엇을 가리키냐면 int p는 const 포인터, 무엇을 가리키냐면 int const
const는 절대 제거하지 말자.
void print_array(const int*data, const int length)
{
*((int*) data) = 10; //int 포인터 위치에 가서 값을 접근해 10을 대입
//const *((int*) data) = 10; //int 포인터 위치에 가서 값을 접근해 10을 대입
//여기서 내 맘대로 const를 빼고 캐스팅 할수도 있음. 그리고 이거 하면 const int를 int 포인터로 바꾸는 거.
}
//메인함수
int nums[] = {1024,9};
print_array(nums,2) //함수 호출 후 , nums의 값이 10이 됨.
- 기본 자료형에선 큰 문제가 아님
- 어차피 매개 변수가 값을 복사해 옴
- 그 매개 변수의 값을 바꾼다고 원본이 바뀌진 않음
- 그러나 const를 가리키는 포인터의 경우 문제가 됨
- const를 제거하고 값을 바꾸면 원본이 바뀜
const 베스트 상황
- const는 최대한 다 붙이는 게 좋다
- 반드시 const가 필요 없는 경우가 아니라면
- const 캐스팅은 하지 말자.
- 함수 시그니처에서 안 바꾼다고 약속하고 어기지 말자.
물론 제거할 수도 있는데 절대 하지 말것.
벡터 덧셈 예제
vector.h
#ifndef VECTOR_H
#define VECTOR_H
#define VECTOR_LENGTH(3)
void add_vec3(const int* v1, const int * v2, int * out_v3)
#endif
vector.c
#include "vector.h"
void add_vec3(const int* v1, const int * v2, int * out_v3)
{
size_t i = 0;
{
for (i = 0; i<VECTOR_LENGTH ; ++i)
{
*out_v3++ = *v1++ + *v2++;
}
}
}
main.c
#include <stdio.h>
#include "vector.h"
int main(void)
{
const int v1[VECTOR_LENGTH] = {1,2,3};
const int v2[VECTOR_LENGTH] = {1,2,8};
int v3[VECTOR_LENGTH];
add_vec3(v1,v2,v3);
printf("v3: %d, %d, %d", v3[0], v3[1], v3[2]);
return 0;
}
포인터의 용도
- 큰 데이터를 함수의 매개변수로 전달 할 때.
- const 배열에 요소가 10개면 크지 않을 수 있다.
- 근데 요소가 10만개면?
- 자료가 커질수록 데이터를 복사하느라 시간을 낭비함.
- 그래서 배열이 매개변수로 전달 될 경우, 첫 번쨰 요소의 주소를 전달.
- 큰 데이터를 함수의 매개변수로 전달 할 때.
- 반환값이 둘 이상일 때
- C에서 return 으로 불가능
- 언제나 하나만 반환해야 함.
- 하지만 포인터를 사용하면 함수 안에서 원본을 직접 변경할 수 있음.
- 원본의 값을 읽지 않고 그냥 덮어 쓰는 거면 반환이나 마찬가지
- 최댓값 최솟값을 한번에 반환하는 함수 예에서 봄.
- 반환값이 둘 이상일 때
- 동적 메모리 할당
- 함수 범위에 상관 없이 한동안 사용하고자 하는 데이터가 있는 데 다음과 같은 경우에 해당하면 사용
- 그 데이터의 크기를 컴파일 도중 알 수 없거나
- 프로그램 실행 수명보다는 짧은 시간동안만 사용해 보려고 할 때
- 동적 메모리 할당
- 동적으로 할당된 메모리는 역시 연속된 메모리 덩어리
따라서 포인터가 적합(배열과 비슷한 이유)
이거는 배열같은 걸 스택이 아니라 함수에 이제 한정한 스택이 아니라 그 함수에 범위에 구애받지 않는, 즉 개발자가 언제 메모리를 잡고 언제 반환할거다 언제 할당하고 언제 할당한걸 풀거다 이걸 하는게 힙 메모리. 이런 메모리가 있다면 힙 사용하는 게 좋은데 스택 둘다 사용할 수 있다면 무조건 스택 사용하는 게 좋다.
- 그 이유는 스택이 굉장히 빠르기 떄문
- 스택이 가끔 못 쓰는 경우도 있음. 함수 범위 안들어 가거나 넘어간 다음 사용되거나 스택이 들어가기에 데이터가 너무 큰 경우들이 있는 데 이런 경우는 어쩔수 없이 힙 사용.
- 그 외
- 데이터 구조를 구현할 떄
- 연결 리스트, 트리 등 과 같은 데이터 구조에 포인터가 적합.
- 임베디드 프로그래밍 등에서 하드웨어에 있는 메모리에 직접 접근해야 할 때
- 예: 어떤 하드웨어는 화면을 보여주려면, 특정 메모리 위치에 이미지 데이터를 직접 복사해줘야 함.
- 데이터 구조를 구현할 떄
포인터 배열
- 포인터도 변수니 당연히 포인터를 저장하는 배열도 있음.
그렇다면 어떻게 선언해야 하는가
- int 를 담는 배열 선언
int nums[3];
- int * 를 담는 배열 선언
int * nums_pointer[3];
- C#에서의 배열의 배열과 비슷
- C#: string [][] classrooms = new String[3][];
배열의 배열과 비슷한 개념.
- 바깥쪽 배열은 행, 안쪽 배열은 열.
- 각 행 마다 열의 길이가 달라질 수 있다.
포인터 배열 예시
int nums1[3] = {11,22,33};
int nums2[1] = {93};
int nums3[4] = {86,37,64};
int * num_pointer[3];
num_pointer[0] = nums1;
num_pointer[1] = nums2;
num_pointer[2] = nums3;
포인터 배열 예시가 얘만 있는것도 아니고 int형 변수 3개 포인터 배열에 넣어도 됨. 그건 누구나 쉽게 할 수 있다.
만약 int형 배열이 ptr로 저장되어있따 치면 (int* ptr)
int p, int q 가 있으면, ptr[0]에 &p. ptr[1] 에 & q. 이건 뭐 워낙 간단.
주소 보면 af0, ad0, ae0으로 끝나는 것들도 있다. 그렇게 대입함.
포인터 배열 보면 배열의 첫번쨰 요소가 다 들어가 있음을 볼 수 있다.
근데 당연하게도 배열 처음 주소와 포인터 변수 배열의 주소는 다름
주소 위치에 가보면 값이 3개 드러가있는데, 그 값이 이 주소들. 즉 num1,num2,num3 의 주소들이 여기 들어가 있다.
포인터 배열은 안에 배열들의 주소가 저장 되는 것. 배열의 요소값들은 주소.
2차원 포인터 배열
내부 배열의 길이도 알려줘야 한다.
- 함수에서 접근하려면 각 내부 배열의 길이를 알려주는 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 (int i = 0; i< size; ++i)
{
p = data[i];
printf("nums[%d]:",i);
}
for (int j = 0; lengths[i]; ++j)
{
printf("%d", p[j]);
}
printf("\n);
}
그럼 배열 = 포인터라고 했는데, 2D 배열도 이렇게 가능한걸까?(배열의 배열과 되게 비슷한데..)
바로 포인터에 대입해 쓸 수 있지 않을까?(포인터로 자동 변환 되지 않을까?)
void do_magic(int *matrix[5])
{
//멋진 코드를 여기에
}
int main(void)
{
int matrix[5][10]={
{1,2,3,4,5,6,7,8,9,10},
{1,2,3,4,5,6,7,8,9,10},
{1,2,3,4,5,6,7,8,9,10},
{1,2,3,4,5,6,7,8,9,10},
{1,2,3,4,5,6,7,8,9,10}
}
do_magic(matrix);
}
근데 실행해 보면 에러남. …?
왜 컴파일 오류가 나는가?
- 2D 배열은 어차피 한 덩어리 메모리라 주소값이 저장된 곳이 없음.
- 올바른 방법
void do_magic(int matrix[][10], size_t m)
{
//어쩌구 코드 생략
}
- m은 행의 수
- 이러면 컴파일러가 매개변수가 2차원 배열이라는 걸 인지함
- 그리고 matrix[1][] 할 때 몇개 건너 뛰어야 하는지 앎.
중점으로 알아야 할 것: 포인터, 주소 연산자, 역 참조 연산자, 널 포인터, 포인터와 두가지 const, 포인터 산술 연산, 포인터와 배열, 포인터 배열