🌈 프로그래밍/C++

[C++] 1.2.1 ~ 1.2.2 스트링 & 포인터와 동적 메모리

반응형

 

안녕하세요? 수구리입니다.

 

 

지난 포스팅에서는 이니셜라이저 리스트에 대해서 알아봤었죠?

 

 

2021.08.17 - [C++] - [C++] 1.1.15 이니셜라이저 리스트

 

[C++] 1.1.15 이니셜라이저 리스트

안녕하세요! 수구리입니다. 저번 포스팅에서는 반복문에 대해서 알아보았습니다! 2021.08.13 - [C++] - 1.1.14 반복문 1.1.14 반복문 안녕하세요? 수구리에요. 드디어 반복문까지 도착했습니다.! 이번 포

tasddc.tistory.com

 

이번 포스팅에서부터는 1.2절로 넘어와서 C++의 고급 기능에 대해서 알아보려고 합니다!

 

 

확실히 1.1절보다는 내용이 많고 제가 처음보는 개념도 등장하네요..

 

 

그러면 바로 시작하겠습니다! 복습 필수

 


1.2 C++의 고급 기능

  • 이번 파트에서는 C++에서 제공하는 다양한 기능들에 대하여 알아보도록 하자.

 

1.2.1 C++의 스트링

C++에서 스트링을 다루는 방법

1. C와 같이 스트링을 문자배열로 표현한다.

2. 스트링 타입으로 감싸서 표현한다.

3. 비표준 제너릭 클래스를 사용한다.

- 위의 내용들은 나중에 2장에서 자세히..
// C++의 string 타입은 <string> 헤더 파일에 정의.
string myString = "hello!";
cout << "the value of myString is " << myString << endl;
cout << "the 2'th letter of myString is " << myString[1] << endl;

 


1.2.2 포인터와 동적 메모리

스택과 힙

  1. 스택
    • 주로 현재 실행중인 함수를 가리킨다.
    • 현재 실행중인 함수는 최상단 스택 프레임의 메모리 공간에 있다.
    • 새로운 함수를 호출한다면 스택 프레임이 올라온다.
    • 스택 프레임은 각각의 함수마다 독립적인 메모리 공간을 제공한다는 점에서 유용하다.
    • 스택에 할당된 변수는 프로그래머가 직접 할당 해제할 필요가 없이 자동으로 처리 되어진다.
    • 현재 함수 또는 스택 프레임과는 완전히 독립적인 메모리 공간 이다.
    • 함수가 끝난 뒤에도 사용하던 변수를 유지하고 싶을땐, 힙에 저장한다.
    • 힙에 할당된 메모리 공간은 직접 할당 해제해야 한다.

 

포인터 사용법

  • 어떤 정숫값을 힙에 저장하려면 힙에는 정수 타입에 맞는 메모리 공간을 할당해주어야 한다. 그럴때 사용하는 것이 포인터이다.
  • 만약 선언 후 초기화를 해주지 않았다면 오류가 날 수 있기 때문에 반드시 포인터 변수는 선언과 동시에 초기화를 해주는 것을 까먹지 말자.

 


// int(*)는 이 변수가 정수 타입에 대한 메모리 공간을 가리킨다
int* myIntPointer;

// 포인터 변수는 초기화하지 않으면 어느 메모리를 가리키는지 알 수 없기 때문에
// 항상 선언과 동시에 초기화를 nullptr로 해준다.
int* myIntPointer = nullptr;

// myIntPointer의 값이 널 포인터는 여기서 false로 취급
if (!myIntPointer) {
    // do something
}

// 포인터변수에 메모리를 동적으로 할당할 때에는 new 연산자를 사용함
myIntPointer = new int;

// 이 포인터가 가리키는 값에 접근하기 위해서는 역참조를 통해 접근.
// 역참조 : 포인터가 힙에 있는 실제값을 가리키는 화살표를 따라감.
*myIntPointer = 8; // myIntPointer = 8 와는 전혀 다른것임에 주의!

 

  • 동적으로 할당한 메모리를 사용 후 delete를 통해서 해당 공간을 해제해야 한다.
  • 해제 후 곧바로 포인터 변수의 값을 nullptr로 초기화!
// delete 후 nullptr 초기화
delete myIntPointer;
myIntPointer = nullptr;

 

  • 포인터는 힙 뿐만 아니라 스택과 같은 다른 종류의 메모리를 가리킬 수 있다.
  • 참조 연산자 &를 통해서 말이다.
// 변수 i의 포인터값을 알아보자.
int i = 8;
int* myPointer = &i; // 8이란 값을 가진 변수 i의 주소값을 가리키는 포인터

 

C++ 구조체에서의 포인터 사용

  • "*" 연산자로 역참조를 하여 구조체 자체 시작지점에 접근
  • 접근한 뒤 필드에 접근하려면 "." 연산자로 표기한다.
// getEmployee() 함수가 Employee 구조체를 리턴한다고 가정.
Employee* anEmployee = getEmployee();
cout << (*anEmployee).salary << endl;

// 좀더 간결하게 "->"를 사용하여 표현
Employee* anEmployee = getEmployee();
cout << anEmployee->salary << endl;

// 단락 논리를 적용하면 잘못된 포인터에 접근하지 못하도록 함.
bool isValidSalary = (anEmployee && anEmployee->salary > 0);

// 좀더 구체적으로 표현
bool isValidSalary = (anEmployee != nullptr && anEmployee->salary > 0);

 

  • anEmployee의 포인터 값이 올바를 때에만 역참조하여 급여 정보를 가져온다.

 

동적으로 배열 할당하기

  • 힙을 사용하여 배열을 동적으로 할당이 가능하다.
  • 사용하는 연산자는 new[] 연산자이다.
  • 사용 후 힙에서 제거하기 위해 delete[] 연산 사용
  • new와 delete가 항상 한 쌍으로 이루어지지 않는다면 메모리 누수가 발생한다.
// size 만큼 배열 생성
int size = 8;
int* arr = new int[size];

// 일반 스택 기반 배열처럼 사용 가능.
arr[3] = 2;

// delete[] 연산 사용
delete[] arr;
arr = nullptr;

 

널 포인터 상수

  • NULL은 실제로 상수 0과 같기 때문에 문제가 발생할 수 있다.
void func(char* str) {
    cout << "char* version" << endl;
}

void func(int i) {
    cout << "int version" << endl;
}

int main() {
    func(NULL); // 아래쪽 func 호출
    func(nullptr); // 위쪽 func 호출
    return 0;
}
  • 위의 예제에서는 func()를 호출할 때 NULL 값으로 호출하고 있다. 그러면 어떤 func가 호출 될까?
  • NULL은 위에서 말했듯이 상수 0과 같기 때문에 아래의 func의 함수가 호출되어진다!
  • 이를 방지하기 위해서 정식 널 포인터 상수인 nullptr을 사용하는 것이 좋다.

스마트 포인터

  • 스마트 포인터를 사용하면 메모리와 관련하여 흔히 발생하는 문제를 해결할 수 있다.
  • 해당 포인터로 지정한 객체가 스코프를 벗어나면 메모리가 자동으로 해제된다.

스마트 포인터의 2가지 타입

  • 헤더 파일에 정의되어 있으며 std 네임스페이스에 속해있다.
  • std::unique_ptr
    • 가리키는 대상이 스코프를 벗어나거나 삭제될 때 할당된 메모리나 리소스도 자동으로 삭제
    • 그러나 unique_ptr가 가리키는 객체를 일반 포인터로는 가리킬 수 없다.
    • 함수의 return 문을 여러 개 작성하더라도 각각에 대해 리소스를 해제하는 코드를 작성할 필요가 없기 때문에 함수를 간결하게 작성 가능하다.
// 작성 예시
auto anEmployee = make_unique<Employee>();

주목할 점 : delete가 자동으로 호출되어지므로 따로 적을 필요가 없다. 그래서 smart!

 

  • std::shared_ptr
    • 데이터를 공유할 수 있다. shared_ptr에 대한 대입 연산이 발생할 때마다 레퍼런스 카운트(참조 횟수)가 하나씩 증가한다.
    • shared_ptr가 스코프를 벗어나면 레퍼런스 카운트가 감소한다. 그러다 0이 되면 해당 데이터를 아무도 가지고 있지 않기 때문에 포인터로 가리키던 객체를 해제한다.
// 작성 예시
auto anEmployee = make_shared<Employee>();
if (anEmployee) {
    cout << "Salary: " << anEmployee->salary << endl;
}
* shared_ptr에 배열도 저장할 수 있다. 하지만 C++17에서는 아래와 같이 작성해야 한다.
// C++17에서의 shared_ptr
shared_ptr<Employee[]> employees(new Employee[10]);
cout << "Salary: " << Employees[0].salary << endl;

 

 

이상으로 C++의 고급기능인 스트링과 포인터 그리고 동적 메모리에 대해 알아보았습니다.

 

 

감사합니다!

반응형

'🌈 프로그래밍 > C++' 카테고리의 다른 글

[C++] 1.2.4 ~ 1.2.5 레퍼런스와 익셉션  (2) 2021.08.19
[C++] 1.2.3 const의 이해  (3) 2021.08.18
[C++] 1.1.15 이니셜라이저 리스트  (0) 2021.08.17
[C++] 1.1.14 반복문  (2) 2021.08.13
[C++] 1.1.13 구조적 바인딩  (0) 2021.08.13