임베디드/임베디드 C언어

[C] C언어 메모리 주소와 포인터, 변수 배열 문자열 포인터

마달랭 2024. 9. 29. 18:35
반응형

개요

변수와 주소의 관계에 대해 학습하고 이해한다.
배열의 주소와 배열 이름의 관계에 대해 학습하고 이해한다.

 

 

변수의 메모리 주소

int x = 10 변수가 있다.

  1. 프로그램이 실행되어 메모리에 적재되면, 변수를 위한 공간이 메모리에 생긴다.
  2. 그 공간을 나타내는 이름이 변수이름, 그 공간을 나타내는 시작 값을 주소라 한다.
  3. 변수 이름은 개발자( 사람 ) 을 위한 것이고, 주소는 컴퓨터를 위한 값이다.

모든 변수들은 주소가 존재한다.

 

x에 10을 대입하고 return까지 트레이스 후 조사식을 확인하면 &x를 통해 변수 x의 메모리 주소를 알 수 있다.

 

배열의 메모리 주소

배열의 경우 배열의 이름이 바로 “배열의 시작 메모리 주소”이다.

 

배열 a의 주소와 a[0]의 주소가 같은 것을 볼 수 있다.

 

메모리 주소

int a = 512; 라는 변수가 있다.

그림으로 간략하게 표현하면 다음과 같다.

주소 0x1000 은 a의 시작 주소 값이다.

 

주소는 Byte 단위이다, OS는 메모리 주소를 1 Byte 단위로 관리한다. (1Byte = 8Bit)

int는 일반적으로 4 Byte이다. 따라서 int 타입 변수 a는 0x1000에서 4Byte 만큼을 사용하고 있는 것이다.

 

 

 

메모리 주소 덧셈

주소+ 1 은단순덧셈이아니다.

&a 는 0x1000 이라면, &a + 1 값은 0x1004 이다.

  1. a가 int 형 변수 4 byte 이기 때문이다.
  2. 주소에 1 을 더하면 다음 변수의 주소를 나타낸다.
  3. 따라서 0x1001 이 아닌, 0x1004 이다

 

위의 a배열은 int타입이다, a 즉 a[0]의 주소는 현재 0x00...f558을 가르키고 있다.

배열의 다음 요소인 a[1]의 주소를 참조해 보면 0x00...f55c를 가르키고 있는 것을 확인할 수 있다.

즉 &a[0] + 1은 &a[0]에서 + 4가 되었으며, &a[1]주소의 값이 된다.

 

 

포인터란?

메모리 주소를 보관하는 변수로, “포인터 변수”를 줄여서 포인터 라고 한다.

기계어나 어셈블리 언어처럼 메모리 주소를 갖고 직접 메모리의 내용에 접근해서 조작할 수 있다.

동적 메모리 할당, 연결 리스트 등의 향상된 자료구조를 생성할 때 쓰인다.

 

포인터를 쓰는 이유?

  • 데이터의 복사를 피하고, 데이터를 공유하여 작업하고자 할 때
  • 데이터를 복사하지 않는다? → 메모리를 더 사용하지 않는다.
  • 데이터를 공유한다? → 여러 부분에서 동일한 데이터를 참조한다.

 

포인터 변수 작성 방식

표기 방법이 다르기 때문에 처음 포인터를 학습할 때 어렵게 느껴진다.

 

 

a라는 정수형 변수에 10의 값을 할당하였다.

p라는 포인터 변수 선언하면서 동시에 a의 주소를 p에 할당해 주었다.

조사식을 보면 알겠지만 *p는 해당 주소에 저장된 값을 나타내며 p자체는 해당 주소를 나타낸다.

 

포인터가 주소를 저장할 때, “가리킨다” 라고 표현한다. (화살표로 표현 가능)

가리키고 있을 때, *을 사용해서, 가리킨 영역의 값을 제어할 수 있다.

 

 

*p에 500을 더했을 뿐인데 변수 a에 저장된 값 또한 변한것을 볼 수 있다.

실제로 a에 500을 더한 것과 동일한 결과가 적용된다, a와 b는 동일한 주소를 갖고 있기 때문이다.

 

배열과 포인터

배열도 마찬가지로, 배열의 이름이 바로 “배열의 시작 메모리 주소”이다.

 

 

포인터 변수 p에는 a[0]의 주소를 초기화 해줬다.

포인터 변수 q에는 a의 시작 주소를 초기화 해줬다.

위에서 언급했듯 a[0]과 a는 모두 배열의 첫 주소를 갖고 있다.

 

 

q에 1을 더해주었다 (*q에 1을 더해준 것이 아니다.)

a는 int타입의 배열이므로 q의 포인터를 1증가 시킨다는 것은 메모리 주소를 4Byte 만큼 움직인다는 것이다.

따라서 q에는 a[1]의 주소값이 할당되어 있고 따라서 *q에는 a[1]의 값인 20이 저장된 것을 볼 수 있다.

 

#include<stdio.h>

int main() {
	int a[4] = {10, 20, 30, 40};
	int* p = &a[0];
	int* q = a;

	for (int i = 0; i < 4; i++) printf("%d ", *(p + i));
	printf("\n");
	for (int i = 0; i < 4; i++) printf("%d ", *q++);

	return 0;
}

 

 

위 코드는 정확히 같은 값을 출력해 준다.

p에는 i만큼 포인터 주소를 이동한 후에 해당 값을 출력해 주었다.

q에는 현재 포인터의 값을 추렭해 준 뒤에 후위 증가 연산 ++을 통해 포인터의 값을 증가시켜 주었다.

하지만 위 for문을 모두 실행한 후의 p와 q의 위치는 다르다.

 

 

p는 i만큼 더한 값을 참조한 것이고, q에는 실제로 포인터가 계속 증가하였기 때문이다.

따라서 p는 그대로 a의 시작 주소를 갖고 있지만, q는 배열 a의 범위를 벗어나 쓰레깃값이 저장되어 있다.

 

 

포인터로 배열 표현하기

포인터는 배열과 같이 사용할 수 있을까?

사실 반대다 C언어 내부에서 배열이 포인터처럼 처리된다.

 

배열과 포인터는 엄연히 다르다.

  • 포인터 변수의 이름 : 주소를 저장하는 공간의 값 ← 포인터에 다른 값을 넣을 수 있다.
  • 배열의 이름 : 배열의 시작 메모리 영역의 주소 ← 배열 이름에 다른 값을 넣을 수 없다.

 

 

배열을 매개변수로 전달하기

배열을 직접 매개변수로 전달하는 것과 포인터를 매개변수로 전달하는 것 역시 동일하게 작동한다.

 

#include<stdio.h>

void A(int p[5]) {
	printf("%d\n", *(p + 1));
}

void B(int *p) {
	printf("%d\n", *(p + 1));
}

int main() {
	int a[4] = {10, 20, 30, 40};
	A(a);
	B(a);
	return 0;
}

 

 

A, B 함수에 모두 배열 a를 전달해 줬고 하나는 배열로 매개변수를 받았고, 하나는 포인터로 매개변수로 받았다.

위 코드를 실행한다면 둘 모두 동일하게 작동하는 것을 확인할 수 있다.

C언어 내부적으로 1차원 배열을 포인터로 자동 변환한다.

따라서, A함수에 명시되어 있는 매개변수의 타입을 이 부분을 배열로 보면 안된다, 이는 포인터이다.

 

매개변수로 쓰이는 배열은 포인터!

main() : 불가능, a는 배열이므로 주소 세팅 불가
A() : 가능, p[5]는 배열이 아닌 포인터 이므로 주소 세팅 가능

 

 

2차원 배열과 포인터

2차원 배열은 가로 세로로 된 격자 형태가 아니다.

 

2차원 배열은 1차원 배열이 여러개가 모여있는 것이다.

 

int arr[3][4] 라고 가정하자. 그럼 arr (배열의 이름)은 무슨 값일까?

배열 시작 메모리 영역의 주소이다, arr == &arr[0][0] ==&arr[0] 모두 동일한 것으로 이해를 하자

 

#include<stdio.h>

int main() {
	int arr[2][3] = {
		{1, 2, 3},
		{6, 7, 8}
	};
	return 0;
}

 

위 코드에서의 배열 arr는 2 * 3의 2차원 int타입 배열이다.

 

2차원 배열은 1차원 배열이 여러 개 모인 것이라 했다.

메모리를 확인해 보면 위처럼 생성되어 있다. 실제로, 여러 개의 1차원 배열이 일렬로 연결된 모습이다.

 

#include<stdio.h>

int main() {
	int arr[2][3] = {
		{1, 2, 3},
		{6, 7, 8}
	};
	int* p = arr;
	printf("%d", *(p + 5));
	return 0;
}

 

위 코드를 실행해 본다면?

 

 

위 메모리 주소를 참조한 그림을 본다면 당연히 arr의 5번 인덱스 요소 8이 출력된다.

 

 

문자열과 포인터

기기는 0/1 만 아는 데 왜 문자열 처리까지 해야 할까?

  • 임베디드 SW 개발자에게 문자열 처리는 매우 중요하다.
  • 통신 프로토콜처리부터 로그 메시지를 생성하여 디버깅 용도로 사용한다.
  • 문자열은 데이터를 표현하고 전송하는 데 쓰인다.
  • 그렇기 때문에 문자열도 매우 중요하다.

문자열은 포인터로 처리된다.

 

#include<stdio.h>

int main() {
	char v[4] = "ABC";
	const char* v = "ABC";
	return 0;
}

 

1. char v[4] = “ABC”;

배열을 하나 만들고, 네 글자를 삽입하는 방식

 

2. const char *v = “ABC”;

포인터를 이용해 문자열 상수를 가리키는 방식, 읽기 전용

  1. 읽기 전용 메모리공간에 “ABC“ 를 넣어둔다.
  2. 지역변수 v를 하나 만든다.
  3. v가 첫번째 칸을 가리키도록 한다.

 

읽기 전용 메모리에 저장되었으므로, 변경이 불가능하다.

 

 

다중 문자열과 포인터

#include<stdio.h>

int main() {
	char vs[3][6] = { "ABCDE", "BTS", "KK" };
	const char* vs[3] = { "ABCDE", "BTS", "KK" };
	return 0;
}

 

1. char vs[3][6] = { “ABCDE”, “BTS”, “KK” };

2차원 배열


2. const char* vs[3] = { “ABCDE” , “BTS”, “KK” };
포인터를 이용한 방법, 읽기 전용

 

 

2차원 배열과 동일한 모습을 가지고 있다.

 

#include<stdio.h>

int main() {
	const char* vs[3] = { "ABCDE", "BTS", "KK" };
	printf("%s\n", *vs);
	printf("%s\n", *(& vs[0]));
	return 0;
}

 

 

vs의 시작 주소의 값을 출력하는 것과 vs[0]의 주소에 저장된 값을 출력하는 것은 같다.

이전에 다룬 내용과 동일하다. (printf()의 %s는 \0 (널문자) 까지 출력을 의미한다.)

 

728x90
반응형

'임베디드 > 임베디드 C언어' 카테고리의 다른 글

[C] C언어 비트 연산  (0) 2024.09.29
[C] C언어 진수 변환  (0) 2024.09.29