객체지향프로그래밍의 4대 원리와 예시,
그리고 힙, 스택 메모리 등 여태까지 배웠던 C++개념 복습하기
주요 자료구조 실제로 구현 해보고 동작원리 설명하기
map, unordered_map, list, vector, array
오늘 하기로 계획한것은 위의 것들이다.
한 번 해보자
먼저 8일간 배운 C++의 정리부터..
int는 cpu의 레지스터와 동일한 크기를 가지는 타입으로 정의가 되어있고, 운영체제에 따라 바뀔 수 있고,
long은 고정 4바이트라고 한다.
링크
하지만, 64비트 윈도우 운영체제인 내 컴퓨터에서 실행해보니 4바이트 똑같이 나온다.
포인터연산을 할 때 크기가 바뀌나? 일단 스킵
char을 정수형이라고 했는데, 아니다.
문자형이다.
char c = 1;
int i = i;
int에 char을 대입하는 연산은 가능하다. 실제 메모리상 값도 1이 들어가 있다.
하지만, 그냥 c를 출력하려고 하거나, 어떤 형변환 없이 c를 사용한다면,
문자형으로 아스키코드 해석하여 동작이 된다.
char[]는 size가 "\n"을 위해서 1칸 더 추가를 해야한다.
wchar_t는 window의 멀티바이트셋의 가변길이변환때문에 고정2바이트를 위해 사용하는 자료형.
char*도 추가해 보았는데, ROM에 존재하는 문자열의 주소를 받아서 사용.
string은 클래스자료형이고, 힙메모리에 속하며, 부족하면 resize하는 방식이다.
크기는 size라는 함수로 구할 수 있으며, 실제로 들어간 값만 계산된다.
string의 null문자열에 접근하는 이런 코드를 사용할 수 없다.
string의 기본크기는 28바이트이다.
보면 null문자열을 포함한 31바이트가 아닌 30바이트가 나온다.
실수에 f를 붙이면 float형, 붙이지 않으면 double형으로 인식한다.
컴퓨터에서 실수는 근삿값만을 찾아낸다.
그 이유는 10진수의 소수를 2진수로 변환하는 과정에서 생긴 차이 때문이다.
더하기는 그냥 각 비트끼리 10진수의 덧셈처럼 더하면 된다.
정리하다보니 unsigned int에서의 뺄셈은 어떻게 할지 헷갈려졌다 - 나중에 참고
unsigned에서의 뺄셈은 덧셈과 같다.
그 이유는 맨 앞의 비트가 MSB로, 부호를 표현하는 비트이다.
0이 양수고, 1이 음수다.
unsigned는 음수를 양수의 보수 + 1로 만든다.
그래야 더했을때, 0이 나오기 때문이다.
C++ 변수의 종류는 4가지
1. 지역변수
2. 전역변수
3. 정적변수 (static)
4. 외부변수 (extern)C++ 메모리영역은 4가지
1. 스택영역
2. 데이터 영역
3. 읽기 전용(코드, ROM)
4. 힙 영역
1 - 지역변수는 함수 내에서만 동작하는 변수이고, 스택영역에 존재하게 된다.
지역변수로 선언된 클래스나 구조체의 경우,
Main함수를 포함한 함수의 동작이 마친다면, 자동으로 소멸자를 호출한다.
2 - 전역변수는 해당 파일 내에서만 동작하는 변수이고, 데이터 영역에 존재하게 된다.
이름은 전역이지만, 그냥 파일 내에서만 사용가능하고,
다른 파일에 같은 이름의 전역변수가 존재한다면 오류가 발생한다.
3 - 정적변수는 특수한 변수로, static을 붙여야 하고,
데이터 영역에 존재한다.
static의 의미는, 해당 변수가 설정된 위치에서만 동작한다는 의미로,
설정된 위치에 따라 다른 기능을 갖는데,
지역변수처럼 쓰인다면,
해당 함수 내에서 초기화가 1번 실행되고, 해당 함수가 호출되어도 다시는 실행되지 않는다.
그리고 함수라는 스택영역이 함수종료와 같이 사라져도,
정적변수는 사라지지 않고 값을 그대로 보존한다.
전역변수처럼 쓰인다면, 다른 파일에 같은 이름의 변수가 존재해도,
해당 변수를 해당 파일에서만 쓴다는 보장을 해주어 문제가 없어지게 된다.
4 - 외부변수는 extern을 붙이고, 데이터영역에 존재한다.
한 파일에서 초기화를 했고, 다른 파일에서 선언을 했다면, 정말로 모든 파일에서 사용가능하다.
1 - 스택영역은 함수가 호출되면서 쌓이는 영역으로,
호출된 함수가 끝나면 스택의 위부터 메모리가 해제된다.
2 - 데이터영역은 프로그램이 시작과 동시에 생성되며,
끝나면 해제되는 영역이다.
3 - 읽기전용 메모리는 코드 자체를 저장해두고,
컴파일러는 변수에 대입을 하는 코드가 존재할 때, 대입되는 R-Value에 대한 값들을 따로 저장해두는데, 이를 위한 메모리공간이다.
const wchar_t* d = L"abcdef";
대표적인 문제되는 코드인데,
문자열을 wchar_t라는 포인터 변수에 받을 수 있다.
그렇게 되면, ROM이라는 읽기전용메모리 공간의 해당 R-Value에 해당하는 공간을 넘기게 되고,
해당 값을 포인터를 역참조하여 수정하게 되면, read only인데,
write를 해버리는 것이므로 심각한 문제를 발생시킨다.
4 - 힙영역은 malloc이나 보통은 new를 통해 사용자가 동적으로 생성하는 메모리의 영역으로,
사용자가 임의로 만들었기에, 사용자가 해제해주어야 한다.
포인터는 주솟값을 넣을 수 있는 변수이다.
해당 포인터변수를 역참조하면, 주솟값에 저장된 실제값에 접근할 수 있다.
주솟값의 단위는 BYTE이다.
(자료형의 최소단위가 1바이트기 때문인 것 같다)
int* pInt = &i;
포인터는 위처럼 초기화를 할 수 있다.
이 때 주의해야할 것은, 해당 구문은 i가 어떤 자료형인지 신경쓰지 않는다는 것이다.
i가 double으로 8바이트든, 어떤 구조체의 배열의 시작값이든 모르겠고,
pInt를 역참조한다면, 해당 주소로부터 int형의 크기 4바이트만큼 데이터를 읽어들이고,
int형으로 값을 해석할 것이다 라는 말이다.
int i[5] = {1,2,3,4,5};
int* pInt = &i;
위처럼 코드가 있다면,
pInt에는 i의 시작 인덱스가 들어가 있다. i[0]
pInt+1을 한다면 해당 주솟값은, i[1]을 가리키게 된다.
배열은 연속적인 메모리 공간을 차지하고 있고,
포인터에 +1을 한다는 것은, 해당 포인터 자료형만큼 알아서 추가해준다는 2가지 개념때문에,
위처럼 간편하게 사용할 수 있게 된다.
const를 붙이면, 변수는 한 번 대입한 값을 바꿀 수 없다.
일반적인 변수는
const int a = 10;
위처럼 가장 앞에 사용할 수 있지만,
포인터는 다르다
(const) int* (const) aptr = &a;
위처럼 const를 붙일 수 있는 곳이 2개가 존재한다.
1번 const는 해당 포인터가 참조하는 곳을 const한다는 의미로,
해당 포인터가 가리키는 공간의 값 변경 불가.
2번 const는 해당 포인터 자체를 const한다는 의미로,
해당 포인터에 담긴 주소값을 변경 불가.
둘을 같이 쓴다면 위의 2가지 효과가 모두 일어난다.
const를 사용하는 이유는,
사용자가 실제값이나, 연결된 주소를 임의로 수정하지 못하게 하기 위함이다.
void 포인터
void* p = &a;
위처럼 사용이 가능한데,
주소는 담을 수 있으나, 값을 참조할 수 없다.
그 이유는, 해당 주소로부터 얼만큼, 어떻게 읽어야할지 모르기 때문이다.
그렇기에 강제 형변환으로 (int*) p를 하여 a의 값을 역참조 하곤한다.
typedef struct one {
int i;
float f;
}AB;
int main()
{
AB str = {};
AB* pt = &str; //== one* pt = &str;
(*pt).i = 1;
pt->f = 1.5f;
return 0;
}
구조체를 포인터에 담고,
해당 멤버변수에 접근하는 경우, . 을 붙이지만,
그냥 ->를 사용하여 간편히 접근할 수 있다.
int main()
{
int* a = (int*)malloc(100);
if (nullptr != a) {
free(a);
}
return 0;
}
malloc(num)을 하게되면, 해당 num의 크기만큼의 공간을 가진
힙공간의 주소를 가리키는 void 포인터가 반환되게 되고,
강제 형변환을 하여, a라는 변수에 그 빈 공간을 할당하는 것이다.
할당하고 free를 통해서 a에 할당된 주소의 메모리를 해제시킨다.
C++에서 struct와 class는 거의 같은 개념이다.
하나 다른것은, struct의 기본 필드는 public이고,
class는 private라는 차이가 존재한다.
공통특징
생성자, 소멸자를 만들어주지 않으면, 기본 생성자가 자동으로 만들어진다.
기본 생성자 아무런 기능이 없는 형식상 코드이다.
하지만 자동으로 생성되는 기본 소멸자는 자동으로 해당 객체의 메모리를 해제한다.
그래서 대부분의 클래스에서는 소멸자를 굳이 구현하지 않는다.
구조체나 클래스는 멤버함수를 통해 접근할 때,
보이진 않지만 this포인터를 전달한다.
그래서 해당 멤버변수에서 this->이런식으로 아무 문제없이 사용이 가능하고,
this도 생략할 수 있어서 그냥 멤버변수의 명칭만으로 사용할 수 있게 된다.
클래스의 대입연산은,
클래스 멤버변수끼리의 대입으로 연산자가 오버로딩 되어있다.
자료형 + &
로 해당 변수를 선언한다.
포인터와 비슷한 개념이지만 다르다.
//a의 주소를 참조하는 포인터를
//역참조하여 100대입
int a = 10;
//1
int* p = &a;
*p = 100;
//2
int& iRef = a;
iRef = 100;
레퍼런스 변수는 포인터-const의 개념으로,
한 번 주소가 할당되면 절대 바꿀 수 없다.
iRef = a로 선언을 했으면, iRef를 a로 본다는 의미이고,
그냥 * 연산 없이 iRef라는 변수명만으로 역참조가 가능해진다.
그리고 값을 변경하면, 해당 변수의 값이 바뀌게 된다.
포인터변수에는 주소값이 들어가야하지만,
레퍼런스변수에는 그냥 변수의 객체만 존재하면 된다.
int* iarr = new int[2];
delete[] iarr;
new + 자료형[num]을 하면,
해당 자료형의 크기 * num 만큼의 공간을 힙에 생성하고,
해당 시작주소
를 iarr에 넘겨준다. 그리고 해당 메모리의 값을 int형으로 인식하도록 한다.
delete +포인터를 하면, 해당 주소에 할당된 메모리를 해제한다.
여기서 []를 덧붙이면, int[2]처럼 여러개를 설정한 메모리 공간이 자동으로 해제된다.
ddd
ddd
ddd
ddd
ddd
ddd
ddd
ddd
ㅇㅇㅇ