본문 바로가기
Programming/컴퓨터프로그래밍및실습

[컴프실] 제11장: 포인터

by Lizardee 2023. 8. 11.
포인터란?
  • 포인터: 주소를 가지고 있는 변수

 

변수는 어디에 저장되는가?
  • 변수는 메모리에 저장된다.
  • 메모리는 바이트 단위로 액세스된다.

 

변수와 메모리
  • 변수의 크기에 따라서 차지하는 메모리 공간이 달라진다.

 

변수의 주소
  • 변수의 주소를 계산하는 연산자: &
  • 변수 i의 주소: &i
//변수의 주소 출력하기
#include <stdio.h>

int main(void) {
	int i = 10;
	char c = 69;
	float f = 12.3;

	printf("i의 주소: %p\n", &i);
	printf("c의 주소: %p\n", &c);
	printf("f의 주소: %p\n", &f);

	return 0;
}

 

포인터의 선언
  • 포인터: 변수의 주소를 가지고 있는 변수

 

포인터와 변수의 연결
int i = 10; //정수형 변수 i 선언
int* p; //포인터 변수 p 선언
p = &i; //변수 i의 주소가 포인터 p로 대입됨

 

다양한 포인터의 선언
char c = 'A'; //문자형 변수 c
float f = 36.5; //실수형 변수 f
double d = 3.14; //실수형 변수 d

char* pc = &c; //문자를 가리키는 포인터 pc
float* pf = &f; //실수를 가리키는 포인터 pf
double* pd = &d; //실수를 가리키는 포인터 pd

 

//포인터 선언 예제
#include <stdio.h>
int main(void) {
	int i = 10;
	double f = 12.3;
	int* pi = NULL; //NULL: 0번지 
	double* pf = NULL;

	pi = &i;
	pf = &f;

	printf("%p %p\n", pi, &i); //포인터 == i의 주소
	printf("%p %p\n", pf, &f); //포인터 == f의 주소

	return 0;
}

 

간접 참조 연산자
  • 간접 참조 연산자 *: 포인터가 가리키는 값을 가져오는 연산자
int i = 10;
int* p;
p = &i;

printf("%d\n", *p);

 

& 연산자와 * 연산자
  • & 연산자: 변수의 주소 반환
  • * 연산자: 포인터가 가리키는 곳의 내용 반환
//포인터 예제1
#include <stdio.h>

int main(void) {
	int i = 3000;
	int* p = NULL;

	p = &i; //포인터 p에 정수형 변수 i의 주소 대입

	printf("p=%p\n", p);
	printf("&i=%p\n", &i); //i형 변수의 주소

	printf("i=%d\n", i);
	printf("*p=%d\n", *p); //포인터 p가 가리키는 곳의 내용

	return 0;
}

 

//포인터 예제2
#include <stdio.h>

int main(void) {
	int x = 10, y = 20;
	int* p;

	p = &x;
	printf("p=%p\n", p);
	printf("*p=%u\n", *p); //%u

	p = &y;
	printf("p=%p\n", p);
	printf("*p=%u\n", *p); //%u

	return 0;
}

 

//포인터 예제3
//포인터를 이용하여 변수의 값 변경하기
#include <stdio.h>

int main(void) {
	int i = 10;
	int* p;

	p = &i; //포인터 p = 변수 i의 주소
	printf("i=%d\n", i);

	*p = 20; //포인터를 이용하여 변수의 값 변경
	printf("i=%d\n", i);

	return 0;
}

 

Lab: 임베디드 프로그래밍 체험1
//임베디드 프로그래밍 체험1
#include <stdio.h>

int main(void) {
	volatile char* p = (volatile char*)0x30000000;
	int i;

	while (1) {
		*p |= 0x1; //첫 번째 비트를 1로 만든다. 다른 비트들은 건드리지 않는다.
		
		for (i = 0; i < 100000; i++) //시간 지연 루프
			*p &= ~(0x1); //첫 번째 비트를 0으로 만든다.
		for (i = 0; i < 100000; i++); //시간 지연 루프
	}

	return 0;
}

 

포인터 사용 시 주의점
1) 초기화가 안 된 포인터를 사용하면 안 된다.
    포인터가 아무것도 가리키고 있지 않은 경우에는 NULL로 초기화
  • int *p=NULL;
#include <stdio.h>

int main(void) {
	int* p; //포인터 p는 초기화가 안 되어 있음
	*p = 100;

	return 0;
}

 

2) 포인터의 타입과 변수의 타입은 일치하여야 한다.
#include <stdio.h>

int main(void) {
	int i;
	double* pd;

	pd = &i; //오류!!!
	*pd = 36.5; //포인터를 이용하여 변수의 값 변경
	
	return 0;
}

 

포인터 연산

▶ 가능한 연산: 증가, 감소, 덧셈, 뺄셈 연산

▶ 증가 연산의 경우, 증가되는 값은 포인터가 가리키는 객체의 크기이다.

  • char: 1
  • short: 2
  • int: 4
  • float: 4
  • double: 8
//증가 연산 예제
#include <stdio.h>

int main(void) {
	char* pc;
	int* pi;
	double* pd;

	pc = (char*)10000; //char형 변수: 1
	pi = (int*)10000; //int형 변수: 4
	pd = (double*)10000; //double형 변수: 8

	printf("pc=%u, pc+1=%u, pc+2=%u\n", pc, pc + 1, pc + 2); //1
	printf("pi=%u, pi+1=%u, p1+2=%u\n", pi, pi + 1, pi + 2); //4
	printf("pd=%u, pd+1=%u, pd+2=%u\n", pd, pd + 1, pd + 2); //8

	return 0;
}

 

간접 참조 연산자와 증감 연산자
  • *p++; --> p가 가리키는 위치에서 값을 가져온 후에 p를 증가한다.
  • (*p)++; --> p가 가리키는 위치의 값을 증가한다.

간접 참조 연산자, 증감 연산자

//포인터의 증감 연산
#include <stdio.h>

int main(void) {
	int i = 10;
	int* pi = &i;

	printf("i=%d, pi=%p\n", i, pi);
	
	(*pi)++; //pi가 가리키는 위치의 값을 증가한다.
	printf("i=%d, pi=%p\n", i, pi); 

	*pi++; //pi가 가리키는 위치에서 값을 가져온 후에 pi를 증가한다.
	printf("i=%d, pi=%p\n", i, pi);

	return 0;
}

 

포인터의 형변환
double* pd = &f;
int* pi;

pi = (int*)pd; //pi에 pd의 값을 대입(int형으로 변환하여)

 

//포인터 형변환 예제
#include <stdio.h>

int main(void) {
	int data = 0x0A0B0C0D;
	char* pc;
	int i;

	pc = (char*)&data;

	for (i = 0; i < 4; i++)
		printf("*(pc+%d)=%02X\n", i, *(pc + i)); //*(pc+i)

	return 0;
}

 

참고!
  • 포인터는 우리가 마음대로 증감시킬 수 있지만, 증감된 포인터가 잘못된 위치를 가리킬 수도 있다.
  • 포인터는 우리가 만든 데이터가 아닌 남의 데이터를 가리킬 수도 있고, 운영체제가 사용하는 데이터 영역을 가리킬 수도 있다.

 

함수 호출 시에 인수 전달 방법

▶ 값에 의한 호출(call by value)

  • 함수로 복사본이 전달된다.
  • C언어에서의 기본적인 방법

▶ 참조에 의한 호출(call by reference)

  • 함수로 원본이 전달된다.
  • C에서는 포인터를 이용하여 흉내낼 수 있다.

 

값에 의한 호출

▶ swap() 함수

//swap 함수 예제
#include <stdio.h>

void swap(int x, int y); //함수 선언

int main(void) {
	int a = 100, b = 200;
	printf("a=%d, b=%d\n", a, b);

	swap(a, b); //swap 함수

	printf("a=%d, b=%d\n", a, b);

	return 0;
}

void swap(int x, int y) {
	int temp;
	printf("x=%d, y=%d\n", x, y);

	temp = x;
	x = y;
	y = temp;

	printf("x=%d, y=%d\n", x, y);
}

 

참조에 의한 호출

▶ swap() 함수

#include <stdio.h>

void swap(int *px, int *py);

int main(void) {
	int a = 100, b = 200;
	printf("a=%d, b=%d\n", a, b);

	swap(&a, &b); //px = &a, &를 붙인다.

	printf("a=%d, b=%d\n", a, b);

	return 0;
}

void swap(int* px, int* py) {
	int temp;

	temp = *px;
	*px = *py;
	*py = temp;
}

 

scanf() 함수
  • scanf(): 변수에 값을 저장하기 위하여 변수의 주소를 받는다.

※ 참고: 함수가 포인터를 통하여 값을 변경할 수 없게 하려면?

--> 함수의 매개 변수를 선언할 때 앞에 const를 붙이면 된다. const를 앞에 붙이면 포인터가 가리키는 내용이 변경 불가능한 상수라는 뜻이 된다.

void sub(const int* p) {
	*p = 0; //오류!!! 
}

 

예제: 기울기와 y절편 계산
  • 만약 함수가 하나 이상의 값을 반환하여야 한다면, 포인터를 사용하는 것이 하나의 방법이다.
//포인터 예제: 기울기와 y절편을 계산
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int get_line_parameter(int x1, int y1, int x2, int y2, float* slope, float* yintercept) {
	if (x1 == x2)
		return -1;
	else {
		*slope = (float)(y2 - y1) / (float)(x2 - x1); //기울기
		*yintercept = y1 - (*slope) * x1; //y절편

		return 0;
	}
}

int main(void) {
	float s, y;
	if (get_line_parameter(3, 3, 6, 6, &s, &y) == -1)
		printf("에러\n");
	else
		printf("기울기는 %f, y절편은 %f\n", s, y);

	return 0;
}

 

포인터를 반환할 때 주의점
  • 함수가 종료되더라도 남아있는 변수의 주소를 반환하여야 한다.
  • 지역 변수의 주소를 반환하면, 함수가 종료되면 사라지기 때문에 오류이다.

 

포인터와 배열
  • 배열과 포인터는 아주 밀접한 관계를 가지고 있다. 배열 이름이 바로 포인터이다.
  • 포인터는 배열처럼 사용이 가능하다.
//포인터와 배열의 관계1
#include <stdio.h>

int main(void) {
	int a[] = { 10, 20, 30, 40, 50 };

	printf("&a[0]=%u\n", &a[0]);
	printf("&a[1]=%u\n", &a[1]);
	printf("&a[2]=%u\n", &a[2]);

	printf("a=%u\n", a);

	return 0;
}

 

  • 인덱스 표기법을 포인터에 사용할 수 있다.
//포인터를 배열처럼 사용
#include <stdio.h>

int main(void) {
	int a[] = { 10, 20, 30, 40, 50 };
	int* p;

	p = a;
	printf("a[0]=%d, a[1]=%d, a[2]=%d\n", a[0], a[1], a[2]); //10 20 30
	printf("p[0]=%d, p1[1]=%d, p[2]=%d\n", p[0], p[1], p[2]);
	printf("\n");
	//배열은 결국 포인터로 구현된다.

	p[0] = 60, p[1] = 70, p[2] = 80;
	printf("a[0]=%d, a[1]=%d, a[2]=%d\n", a[0], a[1], a[2]); //60 70 80
	printf("p[0]=%d, p1[1]=%d, p[2]=%d\n", p[0], p[1], p[2]);

	return 0;
}

 

배열 매개 변수
  • 일반 매개 변수 vs. 배열 매개 변수
  • 배열 매개 변수는 포인터로 생각할 수 있다.
//일반 매개변수
void sub(int x) { //매개변수 x에 기억장소가 할당된다.
	...
}
//배열 매개변수
void sub(int b[]) { //b에 기억장소가 할당되지 않는다.
	...
}

 

//포인터와 함수의 관계
#include <stdio.h>

void sub(int b[], int n); //sub 함수 선언

int main(void) {
	int a[3] = { 1, 2, 3 };

	printf("%d %d %d\n", a[0], a[1], a[2]); //1 2 3
	
	sub(a, 3); //sub 함수
	printf("%d %d %d\n", a[0], a[1], a[2]); //4 5 6

	return 0;
}

void sub(int b[], int n) {
	b[0] = 4;
	b[1] = 5;
	b[2] = 6;
}

 

다음 2가지 방법은 완전히 동일하다.
  • 배열 표기법
  • 포인터 표기법
//포인터 매개 변수
void sub(int* b, int size) {
	//int *b -- 배열 이름과 포인터는 근본적으로 같다.

	b[0] = 4; //배열 표기법
	b[1] = 5;
	b[2] = 6;
}
//포인터 매개 변수
void sub(int* b, int size) {
	//int *b

	*b = 4; //포인터 표기법
	*(b + 1) = 5;
	*(b + 2) = 6;
}

 

포인터를 사용한 방법의 장점
  • 포인터가 인덱스 표기법보다 빠르다.
    <-- 이유: 인덱스를 주소로 변환할 필요가 없다.
//인덱스 표기법
int get_sum1(int a[], int n) {
	int i;
	int sum = 0;

	for (i = 0; i < n; i++)
		sum += a[i];

	return sum;
}
//포인터
int get_sum2(int a[], int n) {
	int i;
	int sum = 0;
	int* p; //포인터

	p = a; //포인터 = 배열
	for (i = 0; i < n; i++)
		sum += *p++;

	return sum;
}

 

Lab: 영상 처리

: 이미지 내의 모든 픽셀의 값을 10씩 증가시킨다.

#include <stdio.h>
#define SIZE 5

void print_image(int image[][SIZE]) { //print_image 함수
	int r, c;

	for (r = 0; r < SIZE; r++) {
		for (c = 0; c < SIZE; c++) {
			printf("%03d ", image[r][c]);
		}
		printf("\n");
	}
	printf("\n");
}

void briten_image(int image[][SIZE]) { //briten_image 함수
	int r, c;
	int* p;
	p = &image[0][0]; //초기화

	for (r = 0; r < SIZE; r++) {
		for (c = 0; c < SIZE; c++) {
			*p += 10;
			p++;
		}
	}
}

int main(void) {
	int image[5][5] = {
		{10, 20, 30, 40, 50},
		{10, 20, 30, 40, 50},
		{10, 20, 30, 40, 50},
		{10, 20, 30, 40, 50},
		{10, 20, 30, 40, 50} };

	print_image(image);
	briten_image(image);
	print_image(image);

	return 0;
}

 

포인터 사용의 장점
  • 연결 리스트, 이진 트리 등의 향상된 자료구조를 만들 수 있다.
  • 참조에 의한 호출: 포인터를 매개변수로 이용하여 함수 외부의 변수 값을 변경할 수 있다.
  • 매모리 매핑 하드웨어: 메모리 매핑 하드웨어란, 메모리처럼 접근할 수 있는 하드웨어 장치를 의미한다.
volatile int* hw_address = (volatile int*)0x7FFF;
*hw_address = 0x0001; //주소 0x7FFF에 있는 장치에 0x0001을 쓴다.
  • 동적 메모리 할당