C++ 2일차

JUSTICE_DER·2023년 2월 2일
0

C++

목록 보기
2/20

어제 정수자료형부터, 자료형의 범위, 그리고 음수의 2진수 표현방법,
실수형자료형과 그 표현방법, 분할 구현
구조체, 지역변수-스택, 전역변수-데이터,

그리고 마지막으로 정적변수-데이터를 보았고,
정적변수의 static이라는게, 자신이 정의된 위치에서만 동작한다는 강력한 특성덕에, 정의된 위치에 따라 다른 효과가 존재했고,

전역변수의 위치에 정의되었다면,
해당 파일에서만 쓰려고 하는 변수라고 컴퓨터에게 이해시킬 수 있으므로,
다른 파일에 같은 이름의 변수가 존재해도, 오류를 발생시키지 않는다.

지역변수의 위치에 정의되었다면,
해당 함수내에서만 쓰려고 하는 변수라고 컴퓨터에게 이해시킬 수 있고,
정의문은 단 한번만 초기화를 위해 동작하며,
일단 문법상으로는 함수 밖에선 정적변수를 수정할 수 없다.
그리고 함수라는 것은 스택영역이고, 함수가 종료되면 사라지는데,
정적변수는 데이터영역이라 프로그램이 끝날때까지 살아있으므로,
정적변수 혼자 계속 남는다.

복습을 마치고 그래서
exe파일 전역에서 쓸 수 있는
단 하나의 변수를 어떻게 만들지에 대해서 계속 찾아나간다.


25 - 정적변수 / 외부변수

static을 헤더에 넣어서 적용시키면 어떨까 생각을 했었다.
결론부터 말하면 헤더에 넣는다고 static이 다른 기능이 생기지 않는다.
그래서 그냥 각 파일에 같은 이름의 정적변수가 있을 뿐이다.
각 나라의 하나뿐인 김용명씨

그러면 이제 남은 것은
외부변수 뿐이다.

func.h // 헤더에는 다음과 같이 extern fin을 선언한다.
#pragma once
extern int fin;
func.cpp //그리고 해당 fin을 초기화 시키는 코드를 include한 파일에 작성한다.
#include "func.h"
int fin = 50;
basic.cpp //아래 구문의 출력값은???? 50이 된다.
#include <iostream>
#include "func.h";
int main()
{
	printf("%d", fin);
}

위처럼 사용한다.
헤더는 필요없고,
그냥 extern이라는 int형 fin이라는 변수를 쓸거다라는

extern int fin; 

선언만 사용할 파일에 적고,
(컴파일시점에 오류나면 안되기에 미리 존재를 알리고,)
하나의 파일이라도 해당 변수를 초기화를 했다면,
모든 파일에서 해당 변수에 대한 접근 및 사용이 가능하다.

참고 할만한 사이트

  • 따라서 헤더에 extern을 사용하여 전역변수 선언을 하고,
  • 한 CPP파일에 전역변수를 또 선언하고, 정의하여 사용한다.

27 - 포인터

포인터는 주솟값을 넣을 수 있는 변수.

#include <iostream>
#include "func.h";

int main()
{
	int i = 100;
    //nullptr은 0인데, 헷갈리는 것을 방지하기 위해 사용
    //포인터의 선언
	int* pInt = nullptr;

    //주소를 저장
	pInt = &i;
    //주소를 참조하는 기호 " * "
	(*pInt) = 300; //i=300
}

위와 같다.

우선 주소의 단위를 본다.
주소의 단위는 BYTE이다.
그리고 실수가 아니고 정수타입이다.
즉 1바이트씩 정수형태로, 1,2 ... 100 ... 번지로 주소가 적힐 수 있다.
주소 100과 102까지는 2바이트, 16비트만큼의 공간이 있는 셈이다.

이걸 인지한 태로 아래코드를 본다.

	int* pInt = &i;

넣어진 주소로 가서 int형인 4바이트만큼만의 저장공간을 읽으라는 셈이다.
만약 주소가 double이나 long long이더라도,
딱 앞의 4바이트만 가져가는 것이다.

그리고, int형이라고 붙여주는 이유는,
이전 강의에서 정수형을 저장하는 것과, 실수형을 저장하는것
둘다 2진수로 저장하지만, 저장한 것을 해석하는 방법에 차이가 존재했는데,
이를 막기 위함도 있다.

그러니까 pInt는 int형변수의 주소를 담을거라 4바이트씩 할당해야하고,
나중에, pInt에 저장된 주솟값을 참조할 때, int형으로 데이터를 인식해라
라는 의미가 위의 한 줄에 담겨있는 셈이다.

강제로 float(4바이트)의 변수를 참조하는
int(4) 포인터를 만들었다면,
float i = 4.0f더라도,
4가 아니라 이상한 값을 담을 것이다.
왜냐하면 컴퓨터가 정수와 실수의 해석하는 방법이 다르기 때문이다.

포인터의 자료형은, 해당 포인터에게 전달된 주소를 해석하는 단위
주소에 접근하는 것은 상당히 위험한 행위라, 이렇게라도 안전책을 건 것이다.

28강 - 포인터 배열

포인터
int a
float
b
char* c 에서 a,b,c의 크기는 같다.
주소만 저장하기 때문에. 아마 변수의ㅡ시작주소만 저장할듯

포인터 변수의 크기는 주소를 담는데,
운영체제가 32비트인지 64비트인지에 따라서
4바이트, 8바이트로 크기가 나뉜다.

64비트는 주소를 표현할 수 있는 개수가 8바이트개 있는 것이다.
그래서 포인터의 크기는 8바이트.

#include <iostream>
#include "func.h";

int main()
{
	int i=1;
	int* pInt = &i;
	pInt += 1;
}

위의 코드에서, pInt라는 포인터 변수를 1 증가시킨다면?

우선 주소값은 정수형이었고,
8바이트로 표현할 수 있는, 개수만큼 다룰 수 있는 공간에 존재하고,
그 하나하나가 1바이트 끊어진 단위였다.

만약 i가 가리키는 주소값이 100번째 주소 단위라면,
i는 100~104까지를 차지하고 있는 변수일 것이고,
100에서 +1을 한 형태기 때문에 101이 될거라고 생각하지만,
int형 포인터라고 선언을 했기 때문에,
해당 포인터 변수에 1을 증가하는 것은,
104번째 주소를 가리키게 된다.

배열에서도 그렇다
int Arr[10] = {};
*(Arr + 1) = 10; //Arr[1] = 10;

배열의 특징
1. 메모리가 연속적인 구조이다.
2. 배열의 이름은 배열의 시작 주소이다.
라는 것인데,
배열의 이름이 시작 주소를 갖고 있는 포인터처럼 동작하므로,
똑같이 +1을 했을때 다음 1번의 특성을 가지고 다음 int 공간
즉 다음 인덱스의 값에 접근할 수 있다는 것이다.

29 - 포인터 문제

#include <iostream>
#include "func.h";

int main()
{
	//문제1
	short sArr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pI = (int*)sArr;
	int result = *((short*)(pI + 2));
	printf("1번문제 정답 %d", result);

	//문제2
	char cArr[2] = { 1,1 };
	short* pS = (short*)cArr;
	result = *pS;
	printf("2번문제 정답 %d", result);

	return 0;
}

위의 문제에 대해서 나는 아래와 같이 풀이했다.

문제1은 틀렸다.
그 이유는 실수인데, 100이 인덱스 0이고, 102가 인덱스 1... 8/2 = 4니까
인덱스 4로해서 5라는 값을 썼어야 했다.


30강 - const

const는 아래와같이 사용

int main()
{
	//상수화 
	const int cint = 100; 
}

상수는 10, 100 이런 변할 수 없는 숫자를 의미하고,
const를 붙이면 cInt라는 값을 상수처럼, 바꿀 수 없는 값으로 취급한다.
하지만 어디까지나 문법적 방어이고, 변수를 선언했으므로,
포인터로 접근해서 바꿀 수도 있다고 한다.

31강 - const 포인터

포인터를 const화 하기전에,
포인터가 가질 수 있는 값은 2개가 존재한다고 볼 수 있다.
1. 주솟값, 2.주솟값에 저장될 값
그래서 포인터의 const방식도 2개가 존재한다.

int main()
{
	int a = 10;
	int b = 20;

	//const - 포인터
	const int* pt_1;
	pt_1 = &a;
	*pt_1 = 20; //값을 수정할 수 없다.
	pt_1 = &b; //주소는 수정할 수 있다.

	//포인터 - const
	int* const pt_2 = &a; //바로 초기화해야함
	*pt_2 = 30; //값은 수정할 수 있다.
	pt_2 = &b; //주소는 수정할 수 없다.
}

위처럼 const를 어디에 쓰느냐에 따라서
값을 수정못할수도, 주소를 수정못할수도 있게된다.

const int* const pt_3 = &a; 

위처럼 2개를 동시에 쓰는 것도 가능하다.

그리고 아래처럼 쓰는 경우도 가능하다.

int const* p = &a;

이 경우에는 1번의 경우 즉 *을 const시키는 것이므로 값 변경 불가이다.

헷갈리는게 당연하다고 본다.
const가 * 앞에 존재하면, 참조하는 포인터를 상수화 시키는것이므로 값(원본) 변경불가
const가 * 뒤에 존재하면, 포인터의 주소값을 상수화 시키는 것이므로 주소 변경불가

32강 - const 포인터 예시

그래서 저런 복잡한걸 언제쓰는가?

우선 게임을 만드는 경우에 있어서
Main함수에서 다른 함수를 호출할 때 매개변수로 플레이어 오브젝트나
어떤 거대한 데이터를 매개변수로 넘겨야 할 때가 존재하는데,

이 경우, Main에서 다루던 데이터를 매개변수로 담아서 다른 함수로 넘기면,
스택에 해당 함수가 쌓이고, 지역변수로 해당 거대한 데이터가 복사되어 생기게 된다. 그리곤 함수가 끝나면 사라진다.

이는 엄청나게 비효율적이고, 이는 포인터로 해결할 수 있다.
그냥 주솟값만 넘겨주면 참조하면 되기 때문에.

하지만,
포인터는 아주 강력한 존재라서
주솟값을 통해서 값을 바꿀 수도 있게 된다.

이걸 방지하기 위해 const-포인터를 사용한다.
일종의 방어책이다.

void func(const int* pt_1) {
	int i = *pt_1;
	*pt_1 = 300000000; //값 수정은 불가능
}

int main()
{
	int a = 1000;
	func(&a);
}

위처럼 func에 주솟값을 변수로 줘서 포인터로 받았다면,
const를 포인터 앞에 붙여서
가져온 변수의 값을 읽기만 하겠다고 명시해주고 방어할 수 있게 된다.
하지만, 강제로 const 포인터를 형변환하면 원본 값을 바꿀 수 있다.
완벽하진 않은 명시적인 표현일 뿐이다.


33강 - void

void로 포인터를 만들 수 있다.
기존의 포인터라는 것을 다시보면,

int* pt = &a;

이런 것이었고,
의미로는 a라는 곳에가서 int형인 4바이트를 인식하고,
해당 주소의 값들을 int형처럼 읽을것이다. 라는 선언이다.

void* pVoid = nullptr;

이게 void 포인터이고,
함수 앞에 void를 붙이면 리턴값이 없듯이,
어떤 자료형도 정의해놓지 않았다는 의미가 된다.
그래서 아래처럼
모든 자료형을 오류없이 선언이 가능하다.

pVoid = &ch;		// char형 사용가능
pVoid = &i;		// int형 사용가능
pVoid = &f;		// float형 사용가능

하지만 주소값을 참조하는 것은 불가능하다.
이유는, 몇바이트씩이나 읽고, 어떤자료형으로 해석해야하는지를 모르기 때문이다.

*pVoid = 99;	// 사용 불가능
pVoid + 1; // 연산 불가능	

주소에 +1하는 연산(int형이면 4바이트 건너뛰는)이 되지 않는다. 당연히.

그러면 왜쓰는걸까?
자료형에 상관없이 주솟값만 저장하는 포인터인데..

profile
Time Waits for No One

0개의 댓글