메모리가 할당되는 방식
일반적으로 변수는 스택에서 변수 형태에 따라 다른 길이의 메모리 공간을 갖는다. 정수형 변수를 선언과 동시에 값을 대입하는 다음 코드는,
int myInt = 123;
스택에서 int 자료형의 크기인 4바이트만큼을 myInt 변수의 값을 쓰고 저장하고 읽기 위해서 공간할당해준다. 해당 공간에 123에 해당하는 2진수 값이 입력되게 된다. 123을 이진수로 표현하면 11111011이 되므로 int 형의 크기인 4byte 만큼 할당 받은 공간에 스택 메모리에 다음과 같은 일이 일어났을 것이다.
(00000000 / 00000000 / 00000000 / 11111011)
그리고 해당 메모리 구역의 이름을 컴퓨터가 myInt라고 부른다고 생각하면 된다.
값을 수정하려면?
사용자가 "myInt의 값을 5로 변경할래" 라고 하면 스택 메모리에서 해당 주소를 찾아가 11111011 값을 00000101 로 변경하게 된다.
각 바이트 (1Byte)마다 주소값이 다르게 구성되어있고, 컴퓨터는 이 주소값과 할당된 변수를 금방금방 찾아갈 수 있도록 따로 저장하고 있다.
사용자가 myInt 변수에 접근하게 되면 컴퓨터는 myInt의 주소를 변수가 할당받은 스택 메모리의 첫번째 바이트의 주소, 즉
(00000000 / 00000000 / 00000000 / 11111011)
볼드체 처리된 위치를 포인터 변수로써 반환한다.
이러한 포인터에는 어떤 변수나 객체의 주소를 저장할 수 있다.
int myInt=3;
int *p=&myInt;
cout<<p<<endl;
myInt 는 그냥 정수형 변수, p는 int* 형, 그러니까 정수형 변수의 포인터 변수를 저장할수 있는 정수형포인터변수 이고, 대입한 &myInt는 myInt라는 정수형 변수의 주솟값을 의미한다.
즉 위의 연산은 어떤 정수형 변수의 주소를 저장할 변수 p에 myInt의 주소를 저장한 것이 된다.
p에 입력된 값을 출력하면,
#include <iostream>
using namespace std;
int main()
{
int myInt=3;
int *p=&myInt;
cout<<p<<endl;
// >>> 0x7fc8219fdd4
}
변수 myInt가 저장된 메모리의 주솟값을 출력한다.
Null Pointer란?
포인터는 보통 선언과 동시에 초기화하는게 일반적이다.
하지만 당장 포인터 변수에 메모리를 할당하고 싶지 않다면,
int * ptr = nullptr; 처럼 널포인터로 초기화할 수 있다.
널 포인터는 bool 표현식에서는 false로 취급한다.
return !ptr → true
구조체와 포인터
Employee* emp = getEmployee();
cout << (*emp).salary << endl;
위의 코드의 동작은 getEmployee가 리턴한 Employee 구조체의 포인터변수 emp를 역참조하여 emp가 가리키고있는 Employee 구조체의 필드 salary를 dot 연산자로 접근함
아래처럼 더 간결한 표현도 가능하다.
Employee* emp = getEmployee();
cout << emp -> salary << endl; // 포인터 변수의 -> 연산자로 역참조 및 필드 접근 한번에
배열의 동적할당
int arraySize = 8;
int * arrptr = new int[arraySize];
위와 같이 선언한다면, 포인터변수는 스택안에 있지만, 동적으로 생성된 배열은 힙에 존재한다.
이렇게 선언한 배열은 아래와 같이 힙의 메모리 공간을 해제해줘야 메모리 누수가 발생하지 않는다.
delete[] arrptr;
arrptr = nullptr;
// new 가 있는 블록에서 delete 해야한다는 패러다임을 RAII 패러다임이라고 한다.
이차원 배열의 동적 할당
char** allocateCharacterArr(size_t xDimension, size_t yDimension){
char** myArray = new char*[xDimension]; // 원소를 배열의 포인터로 받을 배열을 동적 할당
for(size_t i = 0; i < xDimension; i++){
myArray[i] = new char[yDimension]; // 아까 할당받은 포인터 배열의 각 원소에 배열의 포인터를 대입한다.
}
return myArray;
}
NULL 과 nullptr
c++11 이전 버전의 경우 널포인터를 NULL이란 상수로 표현했는데 이는 메서드 오버로딩에서 의도하지 않은 현상이 발생할 수 있는 여지가 있다. 아래의 예제를 보면
void func(char* str) {cout << str << endl;}
void func(int i) {cout << i << endl;}
int main()
{
func(NULL);
return 0;
}
char*타입, int 타입에 대하여 정의된 func함수에 NULL을 매개변수로 지정하면
개발자는 null pointer의 의미로써 NULL을 매개변수로 전달했기에 char* 변수를 매개변수로 받는 func(char* str) 메서드가 실행되기를 원하지만, 실제로는int 인수를 받는 버전의 func(int i) 메서드가 호출된다.
이러한 문제가 발생할 수 있으므로 c++ 11 이후 버전에서는 NULL 대신 nullptr을 사용하도록 추가되었다.
unique_ptr 유니크 포인터
일반 포인터 변수는 동적 할당시, 힙의 메모리를 해제해주지 않으면 메모리 누수가 발생할 수 있다는 우려가 있는데, 유니크 포인터는 이를 해결함
유니크 포인터가 가리키는 객체는 일반 포인터로는 가리킬 수 없다.
#include <memory>
auto emp = make_unique<Employee>();
// C++14 이전에는 이렇게 했다
unique_ptr<Employee> emp(new Employee);
유니크 포인터로 배열 동적 할당
#include <memory>
auto emps = make_unique<Employee[]>(10);
shared_ptr 쉐어드 포인터
유니크 포인터와 달리 여러군데에서 하나의객체를 가리킬 수 있다. -> shared의 의미를 객체의 주소를 공유한다는 개념으로 받아들이면 된다!
유니크 포인터는 여러 포인터들이 하나의 데이터, 객체를 가리킬 수 없는데, shared_ptr은 가능하다. 대입 연산이 발생할때마다 참조횟수가 하나씩 증가하다가 shared_ptr이 스코프를 벗어나면 참조횟수가 감소하며, 0이 되면 객체를 해제한다.
#include <iostream>
#include "Resource.h"
#include "AutoPtr.h"
using namespace std;
// RAII : resource acquisition is initialization
// new를 선언한 블록에서 delete을 선언해야한다는 뜻
void doSomething(){
try{
//구현 오버로드를 최소화하며 RAII 패러다임 적용
AutoPtr<Resource> res(new Resource); // 스마트포인터
AutoPtr<Resource> res2;
cout << boolalpha;
cout << res.m_ptr << endl;
cout << res2.m_ptr << endl;
res2 = res;
//힙에 할당된 res의 메모리에 대한 소유권?을 res2가 공유하는 상
//소유권을 이동시켜야한다 move semantics
cout << res.m_ptr << endl;
cout << res2.m_ptr << endl;
if(false){
throw -1;
}
}
// systax vs semantics
// syntax : 컴파일이 되는가?
//semantics
catch(...)
{
}
}
int main(){
doSomething();
}
기존의 포인터 변수로 동적할당 받은 메모리는 delete로 해제해주지않으면 memory leak이 발생하는데,
소개할 Auto_ptr이나 unique_ptr같은 객체는 delete, 즉 RAII 패러다임의 적용 없이도 메모리를 해제해준다.
unique_ptr<type>
#include <iostream>
#include <vector>
#include <memory.h>
template <typename T>
void wrapper(T u) {
g(u);
}
class A {
private:
int* m_data = nullptr;
unsigned m_length = 0;
public:
A() {
std::cout << "ctor" << std::endl;
}
A(unsigned length) {
std::cout << "input ctor" << std::endl;
this->m_data = new int[length];
this->m_length = length;
}
A(const A& a) {
std::cout << "copy ctor" << std::endl;
A(a.m_length);
for (unsigned i = 0; i < m_length; i++) {
m_data[i] = a.m_data[i];
}
}
~A() {
std::cout << "destroy" << std::endl;
}
};
int main() {
A* a = new A(100000);
//delete a 가 필요하다
std::unique_ptr<A> b(new A(100000));
auto c = std::make_unique<A>(123124);
// delete a 가 필요없다.
}
string* 문자열포인터변수값을 직접 출력하려면?
사실 포인터 변수값을 직접 cout 으로 출력하려고한다는 것이 불필요한 작업이기 때문에 cout에는 문자열 포인터변수의 값을 직접 출력하려고 할때, 자동으로 디레퍼런싱을 해서 출력한다.
int iarr[5] = {1,2,3,4,5};
cout << iarr << endl; -> 콘솔창에 정적배열iarr의 시작값, 즉 iarr의 주소값이 출력된다.
char arr[] = "Hello";
cout << arr << endl; -> 콘솔창에 Hello가 출력된다.
char c = 'E';
cout << &c << endl; // c의 주소를 출력하는 것이 의도였지만,
->E쓰레기값쓰레기
정적 포인터 static pointer static ptr
C++에서 포인터를 정적으로 선언하려면, 변수 이름 앞에 static 키워드를 추가하기만 하면 된다. 예를 들어, int* ptr 대신 static int* ptr를 선언할 수 있다.
정적으로 선언된 포인터 변수는 프로그램이 시작될 때 메모리에 할당되며, 프로그램이 종료될 때까지 유지된다.
따라서, 정적으로 선언된 포인터 변수는 지역적인 함수 내에서 선언 된 지역적인 변수와는 달리 함수가 반환될 때 메모리가 해제되지 않는다.
정적으로 선언된 포인터 변수는 전역 변수와 비슷한 특성을 가지므로, 다른 함수에서도 사용될 수 있기 때문에 주의해야한다. 정적으로 선언된 포인터 변수는 초기화되지 않으면 0으로 자동 초기화된다.
메모리가 할당되는 방식
일반적으로 변수는 스택에서 변수 형태에 따라 다른 길이의 메모리 공간을 갖는다. 정수형 변수를 선언과 동시에 값을 대입하는 다음 코드는,
int myInt = 123;
스택에서 int 자료형의 크기인 4바이트만큼을 myInt 변수의 값을 쓰고 저장하고 읽기 위해서 공간할당해준다. 해당 공간에 123에 해당하는 2진수 값이 입력되게 된다. 123을 이진수로 표현하면 11111011이 되므로 int 형의 크기인 4byte 만큼 할당 받은 공간에 스택 메모리에 다음과 같은 일이 일어났을 것이다.
(00000000 / 00000000 / 00000000 / 11111011)
그리고 해당 메모리 구역의 이름을 컴퓨터가 myInt라고 부른다고 생각하면 된다.
값을 수정하려면?
사용자가 "myInt의 값을 5로 변경할래" 라고 하면 스택 메모리에서 해당 주소를 찾아가 11111011 값을 00000101 로 변경하게 된다.
각 바이트 (1Byte)마다 주소값이 다르게 구성되어있고, 컴퓨터는 이 주소값과 할당된 변수를 금방금방 찾아갈 수 있도록 따로 저장하고 있다.
사용자가 myInt 변수에 접근하게 되면 컴퓨터는 myInt의 주소를 변수가 할당받은 스택 메모리의 첫번째 바이트의 주소, 즉
(00000000 / 00000000 / 00000000 / 11111011)
볼드체 처리된 위치를 포인터 변수로써 반환한다.
이러한 포인터에는 어떤 변수나 객체의 주소를 저장할 수 있다.
int myInt=3;
int *p=&myInt;
cout<<p<<endl;
myInt 는 그냥 정수형 변수, p는 int* 형, 그러니까 정수형 변수의 포인터 변수를 저장할수 있는 정수형포인터변수 이고, 대입한 &myInt는 myInt라는 정수형 변수의 주솟값을 의미한다.
즉 위의 연산은 어떤 정수형 변수의 주소를 저장할 변수 p에 myInt의 주소를 저장한 것이 된다.
p에 입력된 값을 출력하면,
#include <iostream>
using namespace std;
int main()
{
int myInt=3;
int *p=&myInt;
cout<<p<<endl;
// >>> 0x7fc8219fdd4
}
변수 myInt가 저장된 메모리의 주솟값을 출력한다.
Null Pointer란?
포인터는 보통 선언과 동시에 초기화하는게 일반적이다.
하지만 당장 포인터 변수에 메모리를 할당하고 싶지 않다면,
int * ptr = nullptr; 처럼 널포인터로 초기화할 수 있다.
널 포인터는 bool 표현식에서는 false로 취급한다.
return !ptr → true
구조체와 포인터
Employee* emp = getEmployee();
cout << (*emp).salary << endl;
위의 코드의 동작은 getEmployee가 리턴한 Employee 구조체의 포인터변수 emp를 역참조하여 emp가 가리키고있는 Employee 구조체의 필드 salary를 dot 연산자로 접근함
아래처럼 더 간결한 표현도 가능하다.
Employee* emp = getEmployee();
cout << emp -> salary << endl; // 포인터 변수의 -> 연산자로 역참조 및 필드 접근 한번에
배열의 동적할당
int arraySize = 8;
int * arrptr = new int[arraySize];
위와 같이 선언한다면, 포인터변수는 스택안에 있지만, 동적으로 생성된 배열은 힙에 존재한다.
이렇게 선언한 배열은 아래와 같이 힙의 메모리 공간을 해제해줘야 메모리 누수가 발생하지 않는다.
delete[] arrptr;
arrptr = nullptr;
// new 가 있는 블록에서 delete 해야한다는 패러다임을 RAII 패러다임이라고 한다.
이차원 배열의 동적 할당
char** allocateCharacterArr(size_t xDimension, size_t yDimension){
char** myArray = new char*[xDimension]; // 원소를 배열의 포인터로 받을 배열을 동적 할당
for(size_t i = 0; i < xDimension; i++){
myArray[i] = new char[yDimension]; // 아까 할당받은 포인터 배열의 각 원소에 배열의 포인터를 대입한다.
}
return myArray;
}
NULL 과 nullptr
c++11 이전 버전의 경우 널포인터를 NULL이란 상수로 표현했는데 이는 메서드 오버로딩에서 의도하지 않은 현상이 발생할 수 있는 여지가 있다. 아래의 예제를 보면
void func(char* str) {cout << str << endl;}
void func(int i) {cout << i << endl;}
int main()
{
func(NULL);
return 0;
}
char*타입, int 타입에 대하여 정의된 func함수에 NULL을 매개변수로 지정하면
개발자는 null pointer의 의미로써 NULL을 매개변수로 전달했기에 char* 변수를 매개변수로 받는 func(char* str) 메서드가 실행되기를 원하지만, 실제로는int 인수를 받는 버전의 func(int i) 메서드가 호출된다.
이러한 문제가 발생할 수 있으므로 c++ 11 이후 버전에서는 NULL 대신 nullptr을 사용하도록 추가되었다.
unique_ptr 유니크 포인터
일반 포인터 변수는 동적 할당시, 힙의 메모리를 해제해주지 않으면 메모리 누수가 발생할 수 있다는 우려가 있는데, 유니크 포인터는 이를 해결함
유니크 포인터가 가리키는 객체는 일반 포인터로는 가리킬 수 없다.
#include <memory>
auto emp = make_unique<Employee>();
// C++14 이전에는 이렇게 했다
unique_ptr<Employee> emp(new Employee);
유니크 포인터로 배열 동적 할당
#include <memory>
auto emps = make_unique<Employee[]>(10);
shared_ptr 쉐어드 포인터
유니크 포인터와 달리 여러군데에서 하나의객체를 가리킬 수 있다. -> shared의 의미를 객체의 주소를 공유한다는 개념으로 받아들이면 된다!
유니크 포인터는 여러 포인터들이 하나의 데이터, 객체를 가리킬 수 없는데, shared_ptr은 가능하다. 대입 연산이 발생할때마다 참조횟수가 하나씩 증가하다가 shared_ptr이 스코프를 벗어나면 참조횟수가 감소하며, 0이 되면 객체를 해제한다.
#include <iostream>
#include "Resource.h"
#include "AutoPtr.h"
using namespace std;
// RAII : resource acquisition is initialization
// new를 선언한 블록에서 delete을 선언해야한다는 뜻
void doSomething(){
try{
//구현 오버로드를 최소화하며 RAII 패러다임 적용
AutoPtr<Resource> res(new Resource); // 스마트포인터
AutoPtr<Resource> res2;
cout << boolalpha;
cout << res.m_ptr << endl;
cout << res2.m_ptr << endl;
res2 = res;
//힙에 할당된 res의 메모리에 대한 소유권?을 res2가 공유하는 상
//소유권을 이동시켜야한다 move semantics
cout << res.m_ptr << endl;
cout << res2.m_ptr << endl;
if(false){
throw -1;
}
}
// systax vs semantics
// syntax : 컴파일이 되는가?
//semantics
catch(...)
{
}
}
int main(){
doSomething();
}
기존의 포인터 변수로 동적할당 받은 메모리는 delete로 해제해주지않으면 memory leak이 발생하는데,
소개할 Auto_ptr이나 unique_ptr같은 객체는 delete, 즉 RAII 패러다임의 적용 없이도 메모리를 해제해준다.
unique_ptr<type>
#include <iostream>
#include <vector>
#include <memory.h>
template <typename T>
void wrapper(T u) {
g(u);
}
class A {
private:
int* m_data = nullptr;
unsigned m_length = 0;
public:
A() {
std::cout << "ctor" << std::endl;
}
A(unsigned length) {
std::cout << "input ctor" << std::endl;
this->m_data = new int[length];
this->m_length = length;
}
A(const A& a) {
std::cout << "copy ctor" << std::endl;
A(a.m_length);
for (unsigned i = 0; i < m_length; i++) {
m_data[i] = a.m_data[i];
}
}
~A() {
std::cout << "destroy" << std::endl;
}
};
int main() {
A* a = new A(100000);
//delete a 가 필요하다
std::unique_ptr<A> b(new A(100000));
auto c = std::make_unique<A>(123124);
// delete a 가 필요없다.
}
string* 문자열포인터변수값을 직접 출력하려면?
사실 포인터 변수값을 직접 cout 으로 출력하려고한다는 것이 불필요한 작업이기 때문에 cout에는 문자열 포인터변수의 값을 직접 출력하려고 할때, 자동으로 디레퍼런싱을 해서 출력한다.
int iarr[5] = {1,2,3,4,5};
cout << iarr << endl; -> 콘솔창에 정적배열iarr의 시작값, 즉 iarr의 주소값이 출력된다.
char arr[] = "Hello";
cout << arr << endl; -> 콘솔창에 Hello가 출력된다.
char c = 'E';
cout << &c << endl; // c의 주소를 출력하는 것이 의도였지만,
->E쓰레기값쓰레기
정적 포인터 static pointer static ptr
C++에서 포인터를 정적으로 선언하려면, 변수 이름 앞에 static 키워드를 추가하기만 하면 된다. 예를 들어, int* ptr 대신 static int* ptr를 선언할 수 있다.
정적으로 선언된 포인터 변수는 프로그램이 시작될 때 메모리에 할당되며, 프로그램이 종료될 때까지 유지된다.
따라서, 정적으로 선언된 포인터 변수는 지역적인 함수 내에서 선언 된 지역적인 변수와는 달리 함수가 반환될 때 메모리가 해제되지 않는다.
정적으로 선언된 포인터 변수는 전역 변수와 비슷한 특성을 가지므로, 다른 함수에서도 사용될 수 있기 때문에 주의해야한다. 정적으로 선언된 포인터 변수는 초기화되지 않으면 0으로 자동 초기화된다.