-
Lecture 2 - noexcept, const and reference (L-value,R-value)DSP/Lecture 2024. 4. 23. 11:42
noexcept 키워드
noexcept는 C++11에서 도입된 키워드로, 함수가 예외를 발생시키지 않음을 명시하는 데 사용됩니다. 예외를 발생시키지 않는 함수는 프로그램의 성능을 향상시킬 수 있으며, 더 안정적인 코드를 작성하는 데 도움을 줍니다.
noexcept의 중요성
- 성능 최적화: 예외 처리는 실행 시간과 자원을 소모할 수 있습니다. noexcept를 사용함으로써 컴파일러는 예외 처리 로직을 생략할 수 있어, 함수 호출이 더 효율적으로 이루어질 수 있습니다.
- 예외 안전성 보장: noexcept가 명시된 함수에서 예외가 발생하면, 프로그램은 즉시 종료됩니다(std::terminate가 호출됨). 이는 개발자가 예외 안전성을 강제하고, 예외가 발생하지 않도록 보다 주의 깊게 코드를 작성하게 만듭니다.
noexcept 사용 예
void exampleFunction() noexcept { // 예외를 발생시키지 않는 코드 } int computeValue() noexcept { return 42; // 예외를 발생시키지 않음 }
조건부 noexcept
함수가 예외를 발생시키지 않는다는 것을 조건적으로 명시할 수도 있습니다. 예를 들어, 특정 템플릿 매개변수에 따라 함수의 예외 발생 여부가 결정될 수 있습니다.
template <typename T> void process(const T& value) noexcept(is_nothrow_copy_constructible<T>::value) { // T의 복사 생성자가 noexcept인 경우에만 noexcept로 표시 }
상수와 const 한정자
이 페이지에서는 루프에서 마법 숫자(128)를 사용하는 것에 대한 문제를 제기하고, BufferSize와 같은 명명된 상수를 사용하는 것이 더 읽기 쉽고 유지 보수가 용이하다는 것을 제안합니다. 여기 비교 내용이 있습니다:
이전 코드:
for (size_t index = 0; index < 128; index++) { ... }
이 코드에서 128은 "마법의 숫자"로, 이 숫자가 무엇을 의미하는지 코드만 보고는 알 수 없습니다.
개선된 코드:
const int BufferSize = 128; for (size_t index = 0; index < BufferSize; index++) { ... }
이렇게 변경하면 BufferSize라는 상수를 정의하여 코드의 가독성을 향상시키고, 128이라는 숫자가 버퍼의 크기를 의미한다는 것을 명확하게 합니다.
const 한정자의 중요성
상수를 정의할 때 const 키워드를 사용하면, 그 값이 컴파일 시간에 결정되고 실행 중에는 변경될 수 없음을 보장합니다. const를 사용하면 값이 변경되는 것을 방지하므로 프로그램의 안정성과 예측 가능성이 향상됩니다.
또한, 상수는 매크로와 달리 주소를 가지고 있어 프로그램의 디버깅과 검사가 더 쉬워집니다. 매크로는 단순히 컴파일러에 의해 코드에 직접 치환되는 반면, 상수는 실제 메모리 위치를 가지고 있습니다.
매크로 정의 (Macro Definitions)
매크로는 프로그램이 컴파일되기 전에 처리되는 지시어로, 주로 값을 치환하는 데 사용됩니다. 매크로는 프리프로세서에 의해 그 값이 코드 내에서 직접적으로 치환되기 때문에, 컴파일 시간에 실제 메모리 주소를 갖지 않습니다. 즉, 매크로는 코드 내에 텍스트로 존재하며 실행 시간에 별도의 메모리 공간을 차지하지 않습니다.
#define BufferSize = 128;
- 상수 멤버 함수는 수신 객체(즉, 이 객체)에 대한 우발적인 변경을 방지합니다.
- 상수 멤버 함수는 수신 객체의 멤버 변수에 대한 읽기 전용 접근 권한을 가집니다.
- 멤버 함수는 함수 프로토타입에 const 키워드를 추가함으로써 상수로 만들 수 있습니다.
- 모든 멤버 함수가 상수인 객체는 변경할 수 없습니다(불변).
- 참고: 비멤버 함수는 상수로 선언될 수 없습니다.
상수 멤버 함수 (Const Member Functions)
상수 멤버 함수는 객체의 상태를 변경하지 않고, 멤버 변수들을 읽기만 할 수 있게 제한합니다. 이러한 함수는 함수 선언 끝에 const 키워드를 추가하여 정의됩니다.
예시 코드:
class MyClass { private: int value; public: MyClass(int v) : value(v) {} // 상수 멤버 함수 int getValue() const { return value; } };
위 예시에서 getValue() 함수는 const 키워드가 붙어 있기 때문에, 이 함수 내에서는 value 멤버 변수의 값을 변경할 수 없습니다.
상수 멤버 함수의 특성
- 데이터 보호: 객체가 생성된 후 내부 상태가 변경되지 않아야 하는 경우 유용합니다. 예를 들어, 읽기 전용 접근이 필요할 때 사용할 수 있습니다.
- 멤버 변수 읽기 전용 접근: const 함수는 멤버 변수를 수정할 수 없으며, 이를 통해 함수가 객체의 상태를 변경하지 않음을 보증합니다.
- 객체의 불변성: 모든 멤버 함수가 const로 선언된 객체는 불변 객체로 간주될 수 있습니다. 이는 객체의 상태가 초기 생성 후 변하지 않음을 의미합니다.
비멤버 함수란?
비멤버 함수는 어떠한 클래스의 멤버도 아닌, 독립적으로 존재하는 함수입니다. 이러한 함수는 전역 함수(global functions)라고도 하며, 클래스나 객체의 상태와는 무관하게 작동합니다.
상수 멤버 함수와 비멤버 함수의 차이
- 상수 멤버 함수: 클래스의 내부 상태(멤버 변수)를 변경하지 않는다는 것을 보증하기 위해 'const' 키워드를 함수 선언의 끝에 붙여 사용합니다. 이 한정자는 함수가 클래스의 상태를 변경하지 않음을 의미하며, 주로 객체의 데이터를 안전하게 읽는 데 사용됩니다.
- 비멤버 함수: 이 함수들은 특정 클래스의 인스턴스와 연관되지 않기 때문에, 객체의 내부 상태를 변경할 수 있는 권한이 없으며 'const' 키워드를 사용할 필요가 없습니다. 그들은 어떤 객체의 상태에도 영향을 주지 않기 때문에 상수로 선언될 수 없습니다.
class MyClass { public: int data; int getData() const { return data; } // 상수 멤버 함수 }; int globalFunction(int x) { // 비멤버 함수 return x * 2; }
위의 예시에서 getData()는 MyClass의 멤버 함수로 'const'로 선언되어 MyClass 객체의 상태를 변경하지 않습니다. 반면, globalFunction()은 클래스 밖에 정의된 독립적인 함수로, 어떤 클래스의 멤버 변수에도 접근하지 않으므로 상수로 선언될 필요가 없습니다.
이러한 구분은 프로그래밍을 할 때 함수가 어떤 데이터에 접근하고, 어떤 데이터를 변경할 수 있는지를 명확히 이해하는 데 중요합니다. 이는 코드의 안정성과 예측 가능성을 높이는 데 도움을 줍니다.
class Vector2D { private: float fX; float fY; public: // 기본 생성자 및 입력 스트림 생성자 Vector2D(float aX = 1.0f, float aY = 0.0f) noexcept; Vector2D(std::istream& aIStream) : Vector2D() { aIStream >> *this; } // 상수 멤버 함수로서 벡터의 x, y 좌표 반환 float x() const noexcept { return fX; } float y() const noexcept { return fY; } // 벡터 연산자 오버로딩 Vector2D operator+(const Vector2D& aOther) const noexcept; Vector2D operator-(const Vector2D& aOther) const noexcept; Vector2D operator*(const float aScalar) const noexcept; // 벡터 연산 float dot(const Vector2D& aOther) const noexcept; float cross(const Vector2D& aOther) const noexcept; float length() const noexcept; Vector2D normalize() const noexcept; // 방향 및 정렬 관련 함수 float direction() const noexcept; Vector2D align(float aAngleInDegrees) const noexcept; // 입출력 연산을 위한 친구 함수 선언 friend std::istream& operator>>(std::istream& aIStream, Vector2D& aVector); friend std::ostream& operator<<(std::ostream& aOStream, const Vector2D& aVector); }; // 스칼라 곱셈을 위한 비멤버 함수 Vector2D operator*(const float aScalar, const Vector2D& aVector) noexcept;
- 생성자: 기본 생성자와 입력 스트림을 통해 벡터를 초기화하는 생성자가 제공됩니다. 이를 통해 사용자는 쉽게 벡터의 값을 입력받거나 기본값으로 초기화할 수 있습니다.
- 상수 멤버 함수: x(), y(), dot(), cross(), length(), normalize(), direction(), align()과 같은 함수들은 객체의 상태를 변경하지 않고 정보를 제공하거나 계산을 수행합니다. 이들은 noexcept로 선언되어 있어 예외를 발생시키지 않음을 보증합니다.
- 연산자 오버로딩: 벡터 간의 덧셈, 뺄셈 및 스칼라 곱셈을 위해 연산자 오버로딩이 사용됩니다. 이를 통해 코드 내에서 벡터 연산을 간편하게 수행할 수 있습니다.
- 친구 함수: istream과 ostream을 사용하여 객체의 입출력을 처리하는 함수들이 친구 함수로 선언되어, private 멤버에 접근할 수 있습니다.
L-value와 R-value 설명
- L-value (Left value):
- L-value는 메모리에 명확한 위치가 있는 객체를 참조합니다. 이것은 변수 이름, 배열의 요소, 또는 참조를 반환하는 함수 등일 수 있습니다.
- L-value는 주소를 가질 수 있으며, 대입 연산자의 왼쪽에 올 수 있습니다. 즉, 대입 연산에서 값을 받는 역할을 할 수 있습니다.
- R-value (Right value):
- R-value는 L-value가 아닌 모든 표현식을 의미합니다. 이는 일반적으로 리터럴 값(예: 42), 연산 결과, 또는 참조를 반환하지 않는 함수의 반환 값 등입니다.
- R-value는 일시적인 값으로, 대부분의 경우 메모리 상에 지속적인 위치를 갖지 않습니다. 따라서 주소를 얻을 수 없습니다.
예시
int x = 5; // 'x'는 L-value, '5'는 R-value int y = x + 2; // 'x + 2'는 R-value
- 이 페이지에서 설명하는 내용은 C++에서 변수를 어떻게 할당하고 사용하는지, 그리고 이들이 어떻게 메모리에 저장되는지를 이해하는 데 도움을 줍니다.
- L-value와 R-value의 구분은 특히 함수 오버로딩이나 템플릿 프로그래밍에서 중요하며, 효율적인 메모리 관리와 성능 최적화에 기여합니다 .
https://www.youtube.com/watch?v=GutCygNRi-I
// 이 동영상이 L,R-Value 이해와 사용법을 자세히 설명해놓음
- L-value 참조: L-value 참조는 객체의 별칭(다른 이름)으로 생각할 수 있습니다. 주소를 가진 객체는 해당 포인터로 변환될 수 있으며, 유사한 참조 유형으로도 변환될 수 있습니다. L-value 참조는 함수로 큰 객체를 효율적으로 전달할 때 자주 사용됩니다.
- 참조 선언과 주소 연산자 사용 구별: '&' 기호를 사용하여 참조를 선언할 때, 이는 주소 연산자의 사용과 혼동되어서는 안 됩니다. '&identifier'가 유형 앞에 오면 'identifier'는 그 유형의 참조로 선언됩니다. 유형 앞에 오지 않는 경우, 이는 'identifier'의 주소를 의미합니다
- 값에 의한 전달:
- 매개변수가 값에 의해 전달될 때, 함수는 인수의 복사본을 받습니다. 함수 내부에서 매개변수를 변경해도 원래 인수에는 영향을 미치지 않습니다. 이는 Assign 함수에서 aPar 매개변수를 함수 내에서 변경해도 전달된 원본 변수에는 변화가 없는 것을 보여줍니다.
- 참조에 의한 전달:
- 매개변수를 참조로 전달하는 경우(& 기호 사용), 함수는 원본 인수를 직접 수정할 수 있습니다. 이 방법은 큰 데이터 유형에 대해 메모리를 효율적으로 사용할 수 있도록 해줍니다, 데이터를 복사할 필요가 없기 때문입니다. AssignR 함수는 이를 보여주는 예로, aPar에 대한 수정이 원래 변수에 직접 영향을 미칩니다.
- 상수 참조 매개변수:
- 상수 참조 매개변수(const int&로 선언)를 사용하면 함수가 인수를 복사하지 않고 접근할 수 있지만 원본 변수를 수정할 수는 없습니다. 이는 큰 데이터 구조를 효율적으로 전달하면서 변경할 위험 없이 사용할 수 있게 해줍니다. 상상 속의 AssignCR 함수에서 이러한 매개변수를 수정하려고 하면 오류가 발생합니다.
'DSP > Lecture' 카테고리의 다른 글
Lecture3 Vector2D 입력 연산자 >> (0) 2024.05.23 Lecture 2 - 정적 캐스트(static_cast) (0) 2024.04.25 Lecture 2 - Operator, Overloading and Vector (1) 2024.04.25 Lecture 2 - Header file, Constructor (0) 2024.04.25 Lecture 2 - Vector2D Class (0) 2024.04.23