int findMacVal(int arr[], int len){
int max = arr[0];
for (int i = 1; i < len; i++) {
if (max < arr[i])
max = arr[i];
}
return max;
}
int main() {
int arr_len = 0;
cin >> arr_len;
int arr[arr_len];
for (int i = 0; i < arr_len; i++) {
cin >> arr[i];
}
int MaxVal = findMacVal(arr, arr_len);
cout << MaxVal << endl;
}
- 배열의 이름이 포인터 역할을 하기에 함수 호출 시 배열의 이름을 매개변수로 전달하면 호출되는 함수에서는 이 주소를 이용해 배열의 모든 요소들을 접근할 수 있다.
- 이때, 배열의 길이도 매개변수로 함께 전달해야 한다.
이는 2차원 배열도 마찬가지로 2차원 배열은 가로크기가 5일 때만 적용가능한 함수가 되어버린다.
int findMacVal(int arr[][5], int h, int w){
int max = 0;
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
if (max < arr[i][j])
max = arr[i][j];
}
}
return max;
}
Q. 만약 배열의 길이를 모르거나 매개변수로 전달하기 싫다면 어떻게 해야하나요?
A. Vector 클래스를 배열대신 사용한다면 가능하다.
※ array container (std::array) _ 고정크기배열
- memory를 자동으로 할당하고 해제하며 원소의 타입과 배열크기를 매개변수로 사용하는 템플릿클래스이며
다음과 같이 사용할 수 있다.
#include <iostream>
#include <array>
using namespace std;
int main(){
array<int, 3> arr1;
arr1[0] = 1;
cout << arr1[0] << endl;
array<int, 5> arr2;
for(int i=0; i < arr2.size(); i++){
cin >> arr2[i];
}
for (int i = 0; i < arr2.size(); i++) {
cout << arr2[i] << ' ';
}
}
위의 std::array에서는 배열원소에 접근할 때 [ ]를 사용했는데, arr.at() 함수를 사용해서 접근할 수도 있다.
C++의 STL에 내장되어 있는 템플릿 클래스로 배열과 유사하며 Java의 ArrayList와 비슷한 기능을 한다.
배열에 여러가지 기능을 추가한 것으로 기존의 배열에서 불편했던 점을 개선하여 일반화 시킨 것이다.
- 벡터 컨테이너는 동적배열의 클래스 템플릿이다.
- 벡터 객체는 요소가 추가,삭제될 때 마다 자동으로 메모리를 재할당해 크기를 동적으로 변경한다.
- 임의의 위치에 있는 원소 접근과, 뒤에서 원소를 추가하는 연산은 O(1) (분할상환분석)을 보장한다.
- vector<x> 타입 vec이 소요하는 메모리는 대략 sizeof(vector<X>) + vec.size()*sizeof(X)이다. (약 12byte)
※ vector 선언방법
#include <vector>
using namespace std;
vector<자료형> 변수명(숫자) //숫자만큼 벡터 생성 후 0으로 초기화
vector<자료형> 변수명(숫자, 변수1); //숫자만큼 벡터 생성 후 변수1으로 모든 원소 초기화
vector<자료형> 변수명{숫자, 변수1, 변수2, 변수3, ...}; // 벡터 생성 후 오른쪽 변수 값으로 초기화
vector<자료형> 변수명[]={ {변수1, 변수2}, {변수3, 변수4}, ...} //2차원 벡터생성 (열은 고정, 행 가변)
vector<vector <자료형>> 변수명 //2차원 벡터생성 (열, 행 가변)
ex);
vector<int> vec1; // 크기가 0인 벡터 선언
vector<int> vec2(10); // 크기가 10인 벡터 선언
vector<int> vec3(10, 3); // 크기가 10이고 모든 원소가 3으로 초기화된 벡터
vector<int> vec4 = { 1,2,3,4,5 }; // 크기가 지정한 초기값으로 이루어진 벡터
vector<int> vec5[] = { {1,2},{3,4} }; // 벡터 배열 생성(행은 가변인지만, 열은 고정)
vector<vector<int>> vec6; // 2차원 벡터 생성(행과 열 모두 가변)
※ vector 요소 삽입, 제거, 변경
- insert(),push_back()같은삽입연산자는 많은경우 list보다vector에 좀 더 효율적이다.
- 하나의 이름으로 오버로딩 된 수만큼 다양한 기능을 제공해 주기에 연산자 오버로딩을 통해 기존 연산자 외의 다른 기능을 추가할 수 있다.
- 연산자 오버로딩에는 2가지 방법이 있다.
1. 멤버함수에 의한 방법
2. 전역함수에 의한 방법
1. pos1.operator+(pos2); // by 멤버함수
2. operator+(pos1, pos2); // by 전역함수
ex)
const Animal& a를 인자로 받지만 사실 내부적으로는 오른쪽처럼 this도 함께 인자로 받는다는 사실!
단, this는 컴파일러가 가정하기 때문에 직접 명시하지 않는다.
아래의 경우에는 operator를 멤버함수로 구현할 경우 operator의 인자는 2가지이다.
1. 직접 명시된 인자
2. 현재의 object(this) //컴파일러가 가정해서 직접 명시하지 않음
따라서 아래의 경우에도 내부적으로는 Animal& operator+(this, const Animal& a) {...} 가 된다.
위와 아래의 예제에서 출력의 차이가 발생하는 이유는 다음과 같다.
위의 예제의 경우, string이라는 변수에 this->name과 a.name이 합성된 값을 저장하고,
newName을 return하므로 dog.name즉, this의 값은 원래 값이 유지되는 것이다.
아래 예제의 경우, name+=a.name은 this의 name값을 인자 a의 a.name과 합성한 것이므로 dog객체의 this->name값이 변조되는 것이다.
cf. 멤버함수 기반으로만! overloading이 가능한 연산자
= 대입연산자
() 함수 호출 연산자
[] 배열 접근 연산자(Index 연산자)
-> 멤버 접근을 위한 포인터 연산자
이들은 객체를 대상으로 진행해야 의미가 있는 연산자 => 멤버함수 기반으로만 연산자 오버로딩을 허용한다.
- 구조
Point operator= (const Point& ref){
Point pos( . . . );
return pos;
}
- early binding (정적 바인딩): 컴파일시간에 바인딩 되는 것 (초기화 리스트, 일반적 변수)
- late binding (동적 바인딩): 실행시간(run time)에 바인딩 되는 것 => virtual 선언된 모든 것!
간단히 말하자면 변수의 데이터 형이 결정되는 시점에 따라 나뉜다.
조금 더 자세히 말하자면 다음과 같다.
정적 바인딩: 컴파일시 관련 라이브러리, 객체와 링크해 실행 모듈을 만드는 것
동적 바인딩: 컴파일시 정보를 갖고 있지만 실제 실행시간에 해당 객체와 링크하여 실행 모듈을 만드는 것.
정적 바인딩을 사용하면 컴파일 시 데이터 형이 정해지므로실행에 효율적이고,
동적 바인딩을 사용하면 실행 시 데이터 형이 변경되므로 적응력이 뛰어난 프로그램을 제작할 수 있다.
※ 동적 바인딩의 발생
동적 바인딩의 발생은 다음과 같다
- 기본/파생클래스 내 멤버함수가 가상함수 호출
- 외부함수에서 기본클래스의 포인터로 가상함수 호출
- 다른 클래스에서 가상함수 호출
이렇게 가상함수가 호출되면 실행 중 객체 내에 오버라이딩(overriding)된 가상함수를 동적으로 찾아 호출한다.
§ 오버라이딩한 함수를 직접 호출하는 동적바인딩 예시코드 (객체지향 프로그래밍을 위한 C++의 이해)
컴퓨터 입장에서p와c모두Parent를 가리키는 포인터들이므로, 당연히 아래처럼 호출했을 때
p->paint();
c->paint();
모두 Parent 의 print() 가 호출되어야 하겠지만, 실제로는 print() 가 가상함수므로,
"실제로 p 와 c 가 가리키는 객체의 print()",
즉 p->f() 는 Parent 의 print()를, c->f() 는 Child 의 print()가 호출된다.
이와 같은 일이 가능한 이유는 print()를 가상함수로 만들었기 때문입니다.
※ virtual function (가상함수)
- 가상함수:기본 클래스 내의 함수를 파생클래스에서 재정의 (같은 이름의 함수를 overriding)하고자 할 때 사용.
- 가상함수가 포인터에 의해 호출된 경우, 포인터가 가리키는 객체의 클래스에 따라 컴파일러가 결정해 호출한다.
- 따라서 기본클래스 내의 멤버함수 앞에 virtual을 통해 overriding이 가능하다.
이런 가상함수가 나오게 된 계기는 다음과 같은 의문 때문이다.
Q. 포인터변수 자료형에 따라 호출되는 함수가 달라지는 것에 문제가 있지 않을까?
A. virtual을 이용하는 가상함수는 overriding하는 함수도 가상함수가 되어버린다.
즉, 가상함수로 선언되면 해당 함수호출시 포인터의 자료형 기반 호출이 아닌 포인터변수가 실제 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다.
※ 순수가상함수
- 함수의 몸체가 정의되지 않은, 구현이 없는 가상 함수로 구현 대신, 가상함수에 0(NULL)값을 대입하면 된다.
- 가상함수: 상속관계(기본/파생클래스)에서 오버라이딩을 할 수 있는 함수
- 순수가상함수: 파생클래스에서 구현되어야하는 함수, 인터페이스로 이 클래스를 사용하겠다는 의미
class MiddleDerived1 : virtual public Base { . . . };
class MiddleDerived2 : virtual public Base { . . . };
class LastDerived : public MiddleDerived1, public MiddleDerived2 { . . . };
※ 다형성 (polymorphism)
- 객체지향의 매우 중요한 요소로 동질이상(同質異像)을 의미. (모습은 같은데 형태가 다름)
즉, 문장은 같은데 결과는 다르다는 뜻이다.
ex) virtual로 선언된 같은 이름, 다른 클래스의 함수를 동일한 포인터 변수로 받아도 다른 결과값이 나올 수 있다.
- 이미 지워진 문자열을 대상으로 p1의 delete 연산을 진행해야 한다는 문제점이 발생한다.
★ 따라서 깊은 복사를 위한 복사생성자를 만들어 각각의 문자열을 참조해 문제가 발생하지 않게 해줘야 한다.
※깊은복사
- 멤버 뿐만 아니라 포인터로 참조하는 대상까지 복사하는 방법
- default 생성자가 불충분할 때 사용자 정의 복사생성자를 선언한다.
위의 코드에서 아래 코드를 추가해주면 된다.
- 멤버변수 age의 멤버 대 멤버 복사
- 메모리공간 할당 후 문자열 복사, 할당된 메모리 주소값을 멤버 name에 저장
즉, 두 객체를 각각 동적으로 할당된 배열을 갖게 하는데 이를 깊은 복사(deep copy)라 한다.
§ 또 다른 예제
DynamicArray (const DynamicArray& other) : mSize(other.mSize), mArray(nullptr){
// 자신만의 동적데이터공간 할당
mArray = new int[mSize];
// 다른 객체로부터의 데이터 복사
for (int i = 0; i < mSize; i++) {
mArray[i] = other.mArray[i];
}
}
※ 복사생성자 호출 시점:객체를 새로 생성하되 생성과 동시에 동일한 자료형의 객체로 초기화 할 때.
1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화 할 때. [Person p2 = p1;]
2. Call-by-Value 방식의 함수호출과정에서 객체를 인자로 전달할 때
3. 객체를 반환하지만 참조형으로 반환하지 않는 경우.
- 함수의 값을 반환하면 별도의 메모리공간이 할당되고 이 공간에 반환값이 저장된다. (반환값으로 초기화)
2. CBV 함수호출에서 객체를 인자로 전달하는 경우 예시
출처) 윤성우의 열혈C++프로그래밍
3. 객체를 반환하지만 참조형으로 반환하지 않는경우
출처) 윤성우의 열혈C++ 프로그래밍
cf. 생성자, 소멸자, 복사생성자 호출시점 명확히 알고가자!
Q. 어 그러면 생성자가 모두 출력된 후 소멸자가 출력되는 거면 되게 쉽게 생각해도 되겠네요?
A: 그렇게 생각하면 안된다. 객체 소멸은 여러 가지의 경우가 있는데 그 중 하나로는 객체는 함수가 종료되면 사라진 다는 점이다. 정확히는 함수 내부의 nested block이 끝나면 사라지게 되는 것이다.
예를 들어 아래와 같이 p1객체에 block을 씌웠다 가정하자.
{
Person p1(5);
cout << "부모 객체 p1 생성" << endl;
}
Person p2(p1); //오류 발생
cout << "부모 객체 p2 생성 및 객체 p1 복사" << endl;
만약 실행되었다면 다음과 같이 출력이될 것이다.
부모의 생성자
부모 객체 p1 생성
부모의 소멸자
부모의 생성자
부모 객체 p2 생성 및 객체 p1 복사
부모의 생성자
하지만 부모의 소멸자가 호출되어 객체 p1이 없어지니 객체 p2는 객체 p1을 복사하지 못하게 되는 것이다.
이해를 위해 또 다른 예시를 들어 보겠다.
{
Person p1(5);
}
cout << "부모 객체 p1 생성" << endl;
이렇게 된다면 출력은 어떻게 될까?
이는 아래와 같다.
부모의 생성자
부모의 소멸자
부모 객체 p1 생성
즉, 블럭이 종료되면 소멸자가 호출된다는 것을 알 수 있다.
cf. 만약 private멤버로 char* name과 같은 배열관련 변수가 들어갔다면 delete []name로 memory leak을 막아야 한다.
※ 임시객체를 이용한 obj.func().func(); 연산
※ 반환시 만들어진 (임시)객체의 소멸
1. 임시객체는 다음행으로 넘어가면 바로 소멸된다.
2. 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.
또 다른 예시)
※ r-value 참조
r-value 참조는 상수나 임의 저장소를 참조하는 개념으로 데이터 타입에 Double Ampersand(&&)를 붙여 표기한다.
이런 더블 엡퍼센드를 r-value 참조자라 부른다.
사실 이런 r-value 참조는 l-value랑 달리 OOP의 원칙을 준수하는 것보다
프로그램의 성능향상 및 프로그램 개발 생산성에 초점을 맞춰 생겨난 개념이다.
/*---------------일반 변수--------------- */
void f(){
int val = 10; // val 변수 생성 후 10 입력
}
// 함수 반환 시 변수를 소멸
/*---------------r value 참조 변수--------------- */
void f(){
int&& val = 10; // 저장소를 만들어 10을 입력, 저장소에 대해 r value 참조
}
// 함수 반환 시 임시저장소와 함께 r value 참조를 소멸
위의 경우, 표현은 다르지만 작업결과는 동일한데 이말은 즉, r value 참조자는 위와 같은 목적으로 만들어진 개념이 아니다
- 이동생성자가 호출되면얕은복사(shallow copy) => 원본의 소유권을 대상으로 이전하는(move)방식으로 객체를 생성.
- 그 이후 원본 객체를 NULL로 초기화 하여 접근할 수 없게 한다.
Q. 복사생성자가 있는데 굳이 이동생성자를 써야하는 이유는 뭘까?
A. 아래 코드를 통해 설명하겠다.
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class Animal {
private:
char *name;
int age;
public:
Animal(int age_, const char* name_) {
age = age_;
name = new char[strlen(name_) + 1];
strcpy(name, name_);
}
Animal(const Animal & a) { //복사생성자에 의한 깊은복사
age = a.age;
name = new char[strlen(a.name) + 1];
strcpy(name, a.name);
cout << "복사생성자" << endl;
}
Animal(Animal && a) noexcept { //이동생성자에 의한 얕은복사
age = a.age;
name = a.name;
cout << "이동생성자" << endl;
a.name = nullptr;
}
~Animal(){
cout <<"Destructor!!" << endl;
if(name)
delete [] name;
}
void changeName(const char *newName) {
strcpy(name, newName);
}
void printAnimal() {
cout << "Name: " << name << " Age: " << age << endl;
}
};
int main() {
Animal A(10, "Jenny");
A.printAnimal();
vector<Animal> vec; // Animal 벡터 타입
// Animal 벡터 객체 삽입
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
vec.push_back(A);
A.printAnimal();
for (int i = 0; i < 5; i++) {
vec[i].printAnimal();
}
return 0;
}
복사생성자만 있는 경우, 새 vector 생성마다 기존 vector에 저장된 객체들이
새로운 vector로 복사될 때, 복사생성자가 호출되고 성능저하가 일어남
이는 복사생성자의 new로 인한 메모리 할당의 반복으로 일어나는 현상임.
이런 문제를 해결하기 위해 shallow copy를 수행하는 이동생성자를 정의해주는데,
새로운 vector로 이동시, 이동생성자가 호출되며 불필요한 메모리할당을 줄여준다.
[이동생성자 사용 시 주의할 점]
1. 이동생성자에는 noexcept 키워드가 지정되어야 한다. (이동생성자 수행중, 예외가 없다는 것을 컴파일러가 인지해야함)
2. shallow copy가 일어나는 변수(포인터, 주소관련 변수)에 nullptr를 넣어줘야 함
클래스 외부:전역, namespace 유효범위의 상수를 선언, static 객체 등에도 const를 붙일 수 있다.
포인터: * 뒤에 const를 붙여 상수포인터로 하거나 * 앞에 const로 포인터가 가리키는 데이터를 상수로 할 수 있고 const char* const p 처럼 상수포인터, 상수데이터로도 만들 수 있다.
const의 가장 강력한 용도는 함수 선언을 할 때이다.
포인터, 반복자와 [포인터•반복자•참조자]가 가리키는 객체, 함수의 반환값과매개변수, 지역변수는 물론 멤버함수, 함수전체 등 여러곳에서 사용될 수 있다.
이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다는 의미로
const 함수 내에서는 const가 아닌 함수의 호출이 제한된다.
const 참조자를 이용한다면 const 함수만 호출이 가능하다.
class Numclass{
private:
int num;
public:
int GetNum() const { return num; }
};
class NumberClass{
private:
int number;
public:
void initNum(const Numclass& num){
number = num.GetNum();
}
};
const 반환값은 안전성과효율성의 증가, 에러돌발상황감소 등 여러 상황을 효과적으로 막을 수 있다.
기존의 const 보다 더 상수성에 충실하며 일반화된 상수표현식 (Generalized constant expression)을 사용할 수 있게 해줌.
변수, 함수, 생성자 함수에 대하여 일반화된 상수표현식을 통해 컴파일 타임에 평가도리 수 있도록 처리해주는 키워드이다.
C++17부터는 람다함수에서도 constexpr키워드의 사용이 가능해졌다.
§ 변수에서의 사용
const와 constexpr의 차이점
[const]:const변수의 초기화를 런타임까지 지연시킬 수 있다.
[constexpr]:반드시 컴파일 타임에 초기화 되어있어야 한다.
따라서 초기화가 안되어있거나 상수가 아닌 값으로 초기화 시도하면 컴파일이 되지 않는다.
constexpr float x = 42.f; // OK
constexpr float y { 108.f }; // OK
constexpr int i; // error C2737: 'i': 'constexpr' 개체를 초기화해야 합니다. (uninitialized 'const i' [-fpermissive])
int j = 0; // error: 'int j' is not const
constexpr int k = j + 1; // error C2131 : 식이 상수로 계산되지 않았습니다.(the value of 'j' is not usable in a constant expression)
§ 함수에서의 사용
constexpr을 함수반환값에 사용시 다음의 제약이 있다.
1. 함수에 constexpr을 붙일 때는 inline을 암시한다.
2. 즉, 컴파일타임에 평가하기에 inline함수들과 같이 컴파일 된다.
만약 constexr의 함수인자들이 constexpr규칙에 어긋나면 컴파일타임에 실행되지 못하고 런타임에 실행된다.
constexpr int factorial(int n) {
// 지역 변수 없음, 하나의 반환문
return n <= 1 ? 1 : (n * factorial(n - 1));
}
// C++11에서는 컴파일 에러 발생
// C++14부터 가능
constexpr int factorial(int n) {
// 지역 변수
int result = 0;
// 여러 개의 반환문
if (n <= 1)
result = 1;
else
result = n * factorial(n - 1);
return result;
}
C++의 입출력은 C언어처럼 printf();를 사용해도 무방하다. 다만 입출력에 대해 istream& operator >>(istream&, Coord&);와 ostream& operator<<(ostream&, const Coord&); 처럼 이미 C++의 #include<iostream>에 연산자 오버로딩이 되어 있기 때문에 std::cout << 출력하고 싶은 내용or변수 << std::endl;으로 printf(%d)와 같이 변수의 data성격에 따른 출력 타입을 고려해줄 필요가 없어졌다.
C와 C++의 달라진 점은 여러가지가 있는데 bool이라는 자료형 추가, 함수 및 연산자 오버로딩, 클래스, inline, Call-by-Reference 등이 있다.
※ C가 아닌 C++ 사용시의 장점
- classes
- inheritance (derived classes)
- strong typing
- inlining
- default arguments
※ C++은 언어들의 연합체로 바라봐야 한다.
C++은 여러 개의 하위 언어를 제공하며 C++의 어느 부분을 사용하느냐에 따라 programming이 달라진다.
1. C언어
2. 객체지향 개념의 C++ : class, encapsulation, inheritance, polymorphism, virtual function(Dynamic binding)
3. template C++ : 주류 C++과는 다르며 Effective C++의 item 48에서 다루고 있다.
4. STL : container, iterator, algorithm, 함수객체 등 template library를 중심으로 다룬다.
※ l-value와 r-value
l-value: 특정 메모리 위치를 가리키며 변수로 존재한다. (즉, 주소값을 취할 수 있는 연산자)
r-value: 일시적이고 주로 literal 상수이다. (주소값을 취할 수 없는 연산자)
int setValue(){ return 6; }
setValue() = 4; // Err
setValue함수는 rvalue인 숫자 6을 반환한다.
따라서 대입연산자의 왼쪽 피연산자로 사용할 수 없기에 오류가 발생한다.
int global = 100;
int& setValue(){ return global; }
setValue() = 4; // ok
여기서 setValue함수는 참조를 반환한다. 참조는 global 변수의 메모리 장소를 가리키기 때문에 lvalue로 할당할 수 있다.
lvalue에서 rvalue로 변환은 가능하다.
int x = 1; // l-value
int y = 2; // l-value
int z = x+y; // l-value -> r-vlaue
하지만 rvalue에서 lvalue로의 변환은 불가능하다.
※ 함수 오버로딩
이때, 함수 오버로딩은 C언어와 다르게 함수의 매개변수 선언형태가 다르면 동일한 이름의 함수정의 허용이 가능해 졌다.
예를 들어 매개변수가 다른것 뿐만 아니라 const선언만으로도
int Func(int num) { return this->num; }
int Func(int num)const { return this->num; }
두 함수의 이름은 같지만 단지 const를 붙였을 뿐인데 오류가 발생하지 않는다.
※ 함수와 Default 값
int Func (int num = 7) {…}
int main(){
Func() // 7이 전달 될 것으로 간주
}
함수전달인자가 왼쪽부터 채워지기에 함수 작성시에는 오른쪽부터 Default값을 채워야 한다.
※ inline 함수: C의 #define 즉, 매크로 함수와 동일한 기능을 한다.
[차이점]
- #define: preprocessor로 compile전에 실행되며 값의 치환을 주목적으로 전처리기에 의해 실행
- inline: 전처리가 아닌 compiler에 의한 처리과정으로 Effective C++에 의하면 주로 inline함수의 애용을 추천한다.
#define square(x) ((x)*(x))
inline int square(int x){
return x*x;
}
위의 예시에서 두 내용은 서로 같은 의미를 내포하며 출력 시 5의 제곱은 sqaure(5)로 출력할 수 있다는 매력이 있다.
매크로 함수도 자료형 정의가 필요없다는 장점이 존재하지만 아래와 같은 치명적인 단점이 존재한다.
#define CALL_MAX(a,b) f((a) > (b) ? (a) : (b))
...
int x = 5, y = 0;
CALL_MAX(++x, y); // a가 2번 증가
CALL_MAX(++x, y+10); // a가 1번 증가
위의 경우, (++a>b) ? ++a : b 로 해석되기에 a가 2번 증가하게 되어 버리는 것이다.
따라서 다음과 같이 매크로의 효율은 그대로 유지하면서 타입안전성까지 취하는 inline함수에 대한 template을 사용한다.
template<typename T>
inline void callMax (const T& x, const T& y){
f (x > y ? x : y);
}
따라서 Effective C++에서는 #define을 쓰려거든 const, enum, inline을 떠올리라는 내용을 item2에서 볼 수 있다.
매크로는 일단 정의되면 끝날 때 까지 유효하며 컴파일러로 넘어가기 전, 전처리기가 밀어버린 후 상수로 바뀌어 버린다.
따라서 매크로는 클래스 상수 정의에도 쓸 수 없을 뿐만 아니라 캡슐화 혜택도 받지 못한다.
이에 대해 매크로 대신 상수를 쓰는 총 3가지의 해결책을 제시한다.
1. const 사용
#define ASPECT_RATIO 1.653 를 아래와 같이 상수로 사용하는 것이다.
const double AspectRatio = 1.653; 이는 위와 달리 사본이 사용때 마다 생기지 않고 딱 1개만 생긴다.
주의할 점1) 포인터와 포인터가 가리키는 대상까지 const로 선언
const char* const name = "V2LLAIN";
주의할 점2) 클래스 멤버로 상수 정의 시, 상수의 사본개수를 1개 이하로 하고 싶을 때.
class Game {
private:
static const int Num = 5; //static 멤버로 만들어 상수로 선언한다.
};
2. enum hack을 이용한다. (const 보다는 #define에 가까운 방식, 쓸데없는 메모리 할당X)
class CostEstimate{
private:
enum { Num = 5 };
};
3. inline template (Effective C++_item 30)을 이용한다.
template<typename T>
inline void callMax (const T& a, const T& b) { f (a > b ? a : b); }
※ namespace:
범위지정연산자(::)를 이용해 namespace지정 시 사용하며 namespace는 변수명이 같아도 자신만의 namespace에 선언하면 충돌하지 않는다. 따라서 namespace를 중첩해서도 쓸 수 있다. (cf. ::를 변수앞에 붙이면 global variable을 의미.) 또한, std라는 namespace에 대해 using namespace std;라 선언해 std::cout을 cout이라 사용가능하다.
#include <iostream>
using namespace std;
namespace Parent{
int num = 0;
namespace Child{
int num1 = 1;
namespace GrandChild{
int num2 = 2;
}
}
}
int main(){
cout << Parent::num << endl;
cout << Parent::Child::num1 << endl;
namespace G = Parent::Child::GrandChild;
cout << G::num2 << endl;
}
※ reference variables (참조변수)
참조형 변수는 Ampersand 기호, &를 사용하여 나타내는데 우리는 이런 기호를 C언어에서 사용해본 적이 있다.
바로 포인터와 관련된 내용을 배울 때 사용한 적이 있는데, 아래와 같다.
int num = 3;
int *ptr = #
cout << ptr; // ptr변수로 num의 주소값인 0xc3dcbffb64 출력
cout << *ptr; // ptr변수로 num의 값인 3 출력
이때 사용된 &는 num의 주소값을 받아온다는 의미로 사용된 &연산자이다.
그렇다면 참조변수의 기호 &는 무엇일까?
자신이 참조하는 변수를 대신할 수 있는 일종의 별칭으로 참조자 수에는 제한이 없다.
★배열"요소"도 변수로 간주되어 참조자 사용이 가능하다.
int arr[10];
int &ref1 = arr[1];
※배열 전체를 참조값으로 함수에 전달하기
void func (const int (&arr)[3]){
for (int i = 0; i < 3; i++) {
cout << arr[i];
}
}
유의 사항: 괄호() 안에 &와 참조배열이름을 적어야 하며 [ ]안에는 반드시 배열 길이를 명시해야 한다.
※ 참조변수와 const
const변수를 참조하기위해서는 참조변수를 반드시 const로 선언해야 한다.
함수의 참조매개변수를 const로 선언하면 변수의 값을 변경할 수 없고 상수 또한 인자로 전달가능하다.
const int &ref = 30; //30이라는 const
※ Call - by - reference
참조변수로 같은 메모리 공간을 접근하면서 다른 변수의 이름으로 참조가능한 방식이다.
이로 인해 함수 밖의 메모리공간에서도 접근 및 변경이 가능하다.
변수를 매개변수로 하면 함수 호출 때 마다 메모리 할당이 발생한다.
참조변수를 매개변수로 하면 함수 호출을 해도 메모리 할당이 발생하지 않는다.
이때, 반환값은 참조형태여서 반드시 참조변수로 반환받아야 한다.
※ new와 delete
C언어의 malloc과 free를 대신해서 C++에서는 new와 delete를 사용한다.
객체 생성에서는 new를, 소멸에서는 delete를 사용하여 Heap segment에 할당, 반환을 하며
new를 이용해 heap에 할당된 변수에 대해 참조자 선언이 가능하다.
이와 관련된 설명은 뒤에 객체 생성 및 소멸과 관련하여 설명하겠다.
※ Prologue _ 이름짓기(Effective C++)
포인터의 이름을 지을 때, T에 대한 포인터 (pointer to T)를 줄여서 pt라고 하는 경우가 많다.
참조자에서도 비슷하게 T에 대한 참조자 (referenece to T)를 줄여서 rf라 사용할 수 있다.