반응형

1. 복사 생성자

복사 생성자는 객체의 복사가 이루어질 때 호출되는 생성자이다.

#include<iostream>
#include<string>

class Person
{
	std::string name;
	int age;
public:
	Person(std::string name, int age)
	{
		this->name = name;
		this->age = age;
	}
	void ShowInfo()
	{
		std::cout << name << "(" << age << "세)" << std::endl;
	}
};

int main()
{
	Person p1("김철수", 123);
	Person p2 = p1;
	Person p3(p1);

	p1.ShowInfo();
	p2.ShowInfo();
	p3.ShowInfo();

	return 0;
}

그림 1. 실행결과.

p1의 값을 복사하여 p2 p3를 초기화하는 소스이다.

실행 결과를 보면 모든 클래스의 멤버들이 같은 값을 가지는 것을 확인할 수 있다.

 

또한 매개변수로 객체가 인자로 전달될 때, 호출된다.

 

그런데, 위 소스를 확인해보면 따로 복사 생성자를 정의해주지 않았음에도 정상적으로 복사가 된 것을 볼 수 있다.

이는 컴파일러가 기본적인 복사 생성자를 생성자처럼 만들어 주기 때문이다. 이렇게 자동적으로 생성된 복사 생성자를 디폴트 복사 생성자라고 한다.

#include<iostream>
#include<string>

class Person
{
	std::string name;
	int age;
public:
	Person(std::string name, int age)
	{
		this->name = name;
		this->age = age;
	}
	Person(const Person& src)
	{
		this->name = src.name;
		this->age = src.age;
	}
	void ShowInfo()
	{
		std::cout << name << "(" << age << "세)" << std::endl;
	}
};

int main()
{
	Person p1("김철수", 123);
	Person p2 = p1;
	Person p3(p1);

	p1.ShowInfo();
	p2.ShowInfo();
	p3.ShowInfo();

	return 0;
}

위의 소스는 복사 생성자를 보여준다.

디폴트 복사 생성자와 여기서 정의한 복사 생성자는 같은 역할을 수행한다.

단순히 각 멤버에 대해 대입 연산을 수행하여 복사를 진행한다.

const 키워드를 매개변수에 붙여준 것은 값을 변경하지 않겠다는 것을 명시적으로 나타낸 것이라 보면 된다.


2. 깊은 복사와 얕은 복사

2.1. 얕은 복사

1의 경우에는 우리가 따로 복사 생성자를 정의할 필요가 없다.

그럼에도 복사 생성자는 재정의 가능하게 되어있는데, 왜 그럴까?

그것은 클래스 내부에서 동적 할당된 공간을 사용할 경우,

복사 과정에서 고려해주어야 할 부분이 생기기 때문이다.

#include <iostream>

class IArray
{
	int size;
	int* arr;
public:
	IArray(int size)
	{
		this->size = size;
		this->arr = new int[size];
	}
	IArray(const IArray& src)
	{
		this->size = src.size;
		this->arr = src.arr;
	}
	~IArray()
	{
		delete[] arr;
	}
	void InitArray()
	{
		std::cout << "Input(" << size << ") : ";
		for (int i = 0; i < size; i++)
			std::cin >> arr[i];
	}
	void ShowArray()
	{
		std::cout << "Array(" << size << ") : ";
		for (int i = 0; i < size; i++)
			std::cout << arr[i] << " ";
		std::cout << "\n";
	}
};

int main()
{
	IArray arr1(5);
	arr1.InitArray();
	arr1.ShowArray();

	IArray arr2 = arr1; // Shallow Copy
	arr2.ShowArray();

	return 0;
}

그림 2. 에러.

위 소스를 실행시키면, 분명 메인 함수의 내용은 전부 실행되었다.

하지만 에러가 발생한다. 왜 갑자기 에러가 발생했을까?

그림 3. 포인터 내부 모습.

 

복사 생성자 호출 후, 단순 대입 연산을 수행하므로,

arr1의 포인터가 가리키고 있던 힙 공간의 주소를 그대로 arr2의 포인터로 전달한다.

 

메인 함수의 작업이 끝나고 나면 스택에 들어간 객체들의 순서상으로

arr2의 소멸자가 먼저 호출되게 되고 다음 그림과 같은 상황이 된다.

그림 4. arr2의 소멸자 호출후 포인터 상황.

이제 arr1의 소멸자가 호출될 시간이다.

그런데 이미 지워버린 공간에 대해 또 해제를 진행하려고 하니 문제가 발생한다.그래서 프로그램 실행 시 에러가 발생했던 것이다.

 

이를 어떻게 해결할 수 있을까?소멸자를 제거하는 것도 에러를 띄우지 않는 방법 중의 하나이긴 하나, 이는 근본적인 해결방법이 아니다.에러는 발생하지 않지만, 우리가 할당한 힙 공간의 해제를 진행하지 않았기 때문에 메모리 누수가 발생한다.

 

또, 위처럼 해제만 진행하는 것이 아닌, arr2의 내용을 수정하는 경우를 생각해보자.

#include <iostream>

class IArray
{
	int size;
	int* arr;
public:
	IArray(int size)
	{
		this->size = size;
		this->arr = new int[size];
	}
	IArray(const IArray& src)
	{
		this->size = src.size;
		this->arr = src.arr;
	}
	/*~IArray()
	{
		delete[] arr;
	}*/
	void InitArray()
	{
		std::cout << "Input(" << size << ") : ";
		for (int i = 0; i < size; i++)
			std::cin >> arr[i];
	}
	void ShowArray()
	{
		std::cout << "Array(" << size << ") : ";
		for (int i = 0; i < size; i++)
			std::cout << arr[i] << " ";
		std::cout << "\n";
	}
};

int main()
{
	IArray arr1(5);
	arr1.InitArray();
	arr1.ShowArray();

	IArray arr2 = arr1; // Shallow Copy
	arr2.ShowArray();
	arr2.InitArray();

	arr1.ShowArray();

	return 0;
}

그림 5. 실행결과.

소멸자를 제거해서 우선 에러를 발생하지 않게 했다고 해보자.

이제 메인 함수에서 arr2의 값을 수정하고 있다.

 

그런데 arr1의 내용을 출력해보면 arr2과 같은 내용으로 변한 것을 확인할 수 있다.

이런 문제점 때문에, 우리는 깊은 복사에 대해 이해할 필요가 있다.


2.2. 깊은 복사

깊은 복사라고 해서 어려운 내용은 없다.

기존 소스에서 복사 생성자 부분만 수정하면 된다.

#include <iostream>

class IArray
{
	int size;
	int* arr;
public:
	IArray(int size)
	{
		this->size = size;
		this->arr = new int[size];
	}
	IArray(const IArray& src)
	{
		this->size = src.size;
		this->arr = new int[size];
		for (int i = 0; i < size; i++)
		{
			this->arr[i] = src.arr[i];
		}
	}
	~IArray()
	{
		delete[] arr;
	}
	void InitArray()
	{
		std::cout << "Input(" << size << ") : ";
		for (int i = 0; i < size; i++)
			std::cin >> arr[i];
	}
	void ShowArray()
	{
		std::cout << "Array(" << size << ") : ";
		for (int i = 0; i < size; i++)
			std::cout << arr[i] << " ";
		std::cout << "\n";
	}
};

int main()
{
	IArray arr1(5);
	arr1.InitArray();
	arr1.ShowArray();

	IArray arr2 = arr1; // Deep Copy
	arr2.ShowArray();
	arr2.InitArray();

	arr1.ShowArray();

	return 0;
}

그림 6. 실행결과.

복사 생성자 부분만 살펴보자.

이번에는 단순 대입 연산을 수행하는 것이 아닌,

동적 할당으로 새로운 공간을 할당하고, 그 공간에 값만 복사시켜서 넣고 있는 것을 확인할 수 있다.

 

실행 결과를 보면, 우선 소멸자로 공간을 해제해도 아무런 문제가 없고,

또한 arr2의 값을 변경했을 때, arr1의 값을 출력시켜도 그대로 유지되는 것을 볼 수 있다.


반응형

'개념정리 > C++' 카테고리의 다른 글

6. Friend 키워드  (0) 2021.04.21
5. 참조자  (0) 2021.04.20
4. 동적할당과 소멸자  (0) 2021.04.19
3. 클래스  (0) 2021.04.18
2. Namespace  (0) 2021.04.17
반응형

1. Friend 키워드

Friend는 영어로 친구라는 의미를 가지듯, 클래스에서 사용할 수 있는 friend 키워드도 비슷한 의미를 지닌다.

#include<iostream>

class B;

class A
{
	int num;
	friend B;
public:
	A(int num)
	{
		this->num = num;
	}
	void Print()
	{
		std::cout << num << std::endl;
	}
};
class B
{
	int num;
public:
	B(int num)
	{
		this->num = num;
	}
	void Print(A& a)
	{
		std::cout << a.num + num << std::endl;
	}
};

int main()
{
	A a(100);
	B b(25);

	b.Print(a);
	return 0;
}

그림 1. 실행결과.

소소를 살펴보면 클래스 A 내에서 friend B라는 선언을 통해 B와 friend 관계에 있음을 명시했다.

이제 B 내부에서 A에 접근할 일이 생기면 A의 어떤 멤버든지 마음대로 접근이 가능해진다.

 

위에 class B를 먼저 선언해주고 나중에 구현하였는데,

이는 클래스 A가 정의된 시점에서 friend B라고 명시하여도 B에 대한 정보가 없어서 에러가 나기 때문이다.

 

friend 선언은 class내에 존재하는 어떤 접근제어 지시자 안에 위치하던 상관없다.


2. friend 함수

friend 키워드를 클래스 전체 이외에 특정 함수에만 한정 지을 수도 있다.

#include<iostream>
#include<string>

class Person;
void ChangeName(std::string, Person&);

class Person
{
	friend void ChangeName(std::string, Person&);
	std::string name;
	int age;
public:
	Person(std::string name, int age)
	{
		this->name = name;
		this->age = age;
	}
	void ShowInfo()
	{
		std::cout << name << "(" << age << "세" << ")" << std::endl;
	}
};

void ChangeName(std::string name, Person& p)
{
	p.name = name;
}

int main()
{
	Person p("김철수", 123);
	p.ShowInfo();

	ChangeName("홍길동", p);
	p.ShowInfo();

	return 0;
}

그림 2. 실행결과.

소스를 보면 ChangeName이라는 함수에 한정 지어 Person클래스 내에서 friend 선언을 한 것을 볼 수 있다.

ChangeName 함수 내에서는 Person클래스의 private 멤버인 name에 접근하고 있는 것을 볼 수 있다.

 

위의 예제는 전역 함수에 friend 키워드를 사용했지만, 다른 클래스의 메서드에도 적용 가능하다.

 

friend 키워드는 클래스의 멤버에 더 쉽게 접근하게 해 주지만,

너무 과도하게 사용하면 정보 은닉성을 해치게 되고, 결과적으로 캡슐화를 망칠 수도 있다.

따라서 너무 과도하게 사용하지 않도록 항상 주의하여야 한다.


 

반응형

'개념정리 > C++' 카테고리의 다른 글

7. 복사 생성자  (0) 2021.04.21
5. 참조자  (0) 2021.04.20
4. 동적할당과 소멸자  (0) 2021.04.19
3. 클래스  (0) 2021.04.18
2. Namespace  (0) 2021.04.17
반응형

1. 참조자

참조자는 C에서 C++로 넘어오면서 생긴 새로운 개념이다.

#include<iostream>

int main()
{
	int num = 15;
	int& ref = num;

	std::cout << "num: " << num << std::endl;
	std::cout << "ref: " << ref << std::endl;
	
	ref += 5;

	std::cout << "num: " << num << std::endl;
	std::cout << "ref: " << ref << std::endl;

	return 0;
}

그림 1. 실행결과.

참조자는 변수의 다른 이름, 즉 별명과 같은 역할을 수행한다.

따라서 한번 할당되고 나면 ref와 num은 코드 내에서 같은 의미를 지닌다.

 

그런데, 별명을 붙이는 것은 좋으나 이를 어디에 유용하게 사용할 수 있을까?

참조자를 함수의 매개변수로 넘기는 것은 call-by-reference의 효과를 갖는다.

#include<iostream>

void swap(int& ref1, int& ref2)
{
	int temp = ref1;
	ref1 = ref2;
	ref2 = temp;
}

int main()
{
	int num1 = 15;
	int num2 = 20;

	std::cout << "호출 전: " << num1 << " , " << num2 << std::endl;

	swap(num1, num2);

	std::cout << "호출 후: " << num1 << " , " << num2 << std::endl;

	return 0;
}

그림 2. 실행결과.

함수를 보면 매개변수로 참조자 변수를 받고 있다.

함수 내부에서는 일반 변수처럼 사용되고, 인자의 값에 직접적으로 접근하여 바꾸는 것을 보여주고 있다.


2. 포인터와 참조자의 차이점

참조자를 전부 포인터로 교체해도 동일한 효과를 지닌다.

둘은 어떤 차이가 있을까?

참조자 포인터
선언과 동시에 초기화 되어야함. 선언만 해두고 나중에 값을 넣을 수 있음.
초기화 되고나면 값을 바꿀 수 없음. 값을 바꿀수 있음.
null값을 넣을 수 없음.  null값을 넣을 수 있음

가장 근본적인 차이점은 위와 같다.

항상 초기화되어야 하고, 값을 변경할 수 없기 때문에 포인터보다 안전하다.

 

C++ FAQ에서는 다음과 같이 언급되어 있다.

Use references when you can, and pointers when you have to.
사용할 수 있다면 참조자를 쓰고, 어쩔 수 없을 경우에만 포인터를 써라.
- https://isocpp.org/wiki/faq/references#refs-vs-ptrs 

그만큼 포인터보다 참조자를 쓰는 것이 안전하다는 의미를 나타내는 듯하다.


3. 참조자와 매개변수

참조자는 매개변수로 사용될 때 성능의 이득을 볼 수 있다.

#include<iostream>

int Sum1ToN(int n)
{
	std::cout << "variable address : " << &n << std::endl;
	return (n * (n + 1)) / 2;
}

int Sum1ToN_ref(int &n)
{
	std::cout << "ref address : " << &n << std::endl;
	return (n * (n + 1)) / 2;
}

int main()
{
	int n;
	std::cin >> n;
	std::cout << "n address : " << &n << std::endl;
	Sum1ToN(n);
	Sum1ToN_ref(n);

	return 0;
}

그림 3. 실행결과.

참조자를 매개변수로 사용한 경우와 그렇지 않은 경우의 결과이다.

참조자를 사용하지 않았을 경우는 인자와 주소 값이 다르고,

참조자를 사용했을 경우에는 주소 값이 동일하다.

 

따라서 인자의 크기가 커서 복사에 많은 비용이 사용될 경우,

참조자를 사용하면 성능에서 이득을 볼 수 있다.


읽을거리.

C언어에 Call by Reference는 없다.

- C에는 근본적으로 Call by Value만 존재하므로 Call by Address 혹은 Call by Reference의 흉내내기 정도로

불러야 한다는 내용이다. 말장난처럼 들릴 수도 있지만, 정확하게 짚고 넘어가면

Call by Reference가 아니므로 C에서 Call by Reference가 없다는 것을 명심하자.

반응형

'개념정리 > C++' 카테고리의 다른 글

7. 복사 생성자  (0) 2021.04.21
6. Friend 키워드  (0) 2021.04.21
4. 동적할당과 소멸자  (0) 2021.04.19
3. 클래스  (0) 2021.04.18
2. Namespace  (0) 2021.04.17
반응형

1. 동적 할당

동적 할당이란, 프로그래머가 원하는 만큼의 공간을 프로그램 실행시간 중에 할당하여 사용하는 것이다.

할당된 공간은 프로그램 종료까지 남아있으므로, 프로그래머가 사용이 끝나면 적절히 해제시켜주어야 한다.

C++에서 동적 할당은 다음과 같이 이루어진다.

#include<iostream>

int main()
{
	int* iNumber = new int;
	std::cin >> *iNumber;
	std::cout << "Number :" << *iNumber << std::endl;
	delete iNumber;
	return 0;
}

그림 1. 실행결과.

new라는 키워드를 사용해 공간을 할당하고,

delete라는 키워드를 사용해서 공간을 해제하고 있다.

 

배열의 경우에는 어떻게 할까?

#include<iostream>

int main()
{
	int* iArray = new int[5];
	for (int i = 0; i < 5; i++)
		std::cin >> iArray[i];

	std::cout << "iArray :";
	for (int i = 0; i < 5; i++)
		std::cout << iArray[i] << " ";
	std::cout << std::endl;

	delete[] iArray;
	return 0;
}

그림 2. 실행결과.

배열의 경우에는 []로 할당받을 공간의 크기를 추가적으로 알려주어서 공간을 할당받고,

delete []로 연속된 공간을 해제시키고 있다.


2. 소멸자

생성자는 클래스 초기화를 위해 만들어졌다면, 소멸자는 청소를 위해 만들어졌다.

소멸자는 반환형과 매개변수가 없고, 클래스 이름 앞에 ~를 붙여 나타낸다.

#include<iostream>

class iClass
{
	std::string name;
public:
	iClass(std::string name)
	{
		this->name = name;
		std::cout << name << "생성자 호출!" << std::endl;
	}
	~iClass()
	{
		std::cout << name << "소멸자 호출!" << std::endl;
	}

};

int main()
{
	iClass c("클래스1");
	iClass* cp = new iClass("클래스2");
	delete cp;
	return 0;
}

그림 3. 실행결과.

소멸자는 객체가 소멸할 때 호출된다.

클래스 2는 동적 할당으로 생성하여 delete로 명시적으로 해제하였으므로,

먼저 소멸자가 호출된 것을 볼 수 있다.

 

사실 위와 같은 예제에서는 소멸자가 필요가 없다.

객체 내부에서 동적 할당을 사용했다면, 그 공간을 해제시켜 주어야 한다.

그럼 언제 해제시키는 것이 가장 이상적일까?

객체가 더 이상 사용될 일이 없는 소멸 단계에서 해제되는 것이 가장 이상적일 것이다.

따라서 소멸자는 객체 내부에서 사용된 메모리를 정리할 때 사용하는 게 좋다.

#include<iostream>

class IArray
{
	int* arr;
	int size;
public:
	IArray(int size)
	{
		this->size = size;
		arr = new int[size];
	}
	~IArray()
	{
		delete[] arr;
	}
	void InitArray()
	{
		std::cout << "Input(" << size << ") : ";
		for (int i = 0; i < size; i++)
			std::cin >> arr[i];
	}
	void ShowArray()
	{
		std::cout << "Array(" << size << ") : ";
		for (int i = 0; i < size; i++)
			std::cout << arr[i] << " ";
		std::cout << std::endl;
	}
};

int main()
{
	IArray arr = IArray(5);
	arr.InitArray();
	arr.ShowArray();
	return 0;
}

그림 4. 실행결과.

소스를 보면, 생성자에서 동적 할당을 사용해서 공간을 할당하고,

소멸자에서 그 공간을 해제하고 있다.


읽을거리.

동적 할당 시 주의점

new []를 사용했다면 delete [],

new를 사용했다면 delete를 써야 한다는 것을 명심하자.

#include <iostream>

class A
{
public:
	A()
	{
		std::cout << "생성자 호출!" << std::endl;
	}
	~A()
	{
		std::cout << "소멸자 호출!" << std::endl;
	}
};

int main()
{
	A* ptr = new A[10];
	delete ptr; // 에러!
	return 0;
}

 

new []를 사용하여 배열로 공간을 할당했는데, delete만 사용하면

나머지 뒤에 연속된 공간을 해제하지 않아서 메모리 누수가 발생한다.

그림 5. 에러.

 

반응형

'개념정리 > C++' 카테고리의 다른 글

6. Friend 키워드  (0) 2021.04.21
5. 참조자  (0) 2021.04.20
3. 클래스  (0) 2021.04.18
2. Namespace  (0) 2021.04.17
1. 기본 자료형 및 입출력  (0) 2021.04.15
반응형

1. 클래스

클래스는 객체지향 프로그래밍의 핵심적인 요소이다.

객체지향 프로그래밍은 프로그램을 각 객체들 간의 상호작용을 통해 나타내는 것이 목적이다.

그런 객체들을 생성하기 위하여 객체들의 특징들만 모아 일반화시킨 것이 클래스이다.

클래스 내부의 변수를 멤버 변수, 함수를 메서드라고 한다.

보통 클래스를 붕어빵 틀에 비유하고, 객체들을 붕어빵에 비유한다.
붕어빵 틀을 가지고 여러 가지 다양한 붕어빵을 만들어낼 수 있듯, 클래스 하나로 여러 속성만 변경시켜 여러 객체를 만들어 낼 수 있다.
#include <iostream>

class Person
{
public:
	std::string name;
	int age;
	void introduce()
	{
		std::cout << name << "(" << age << "세)" <<std::endl;
	}
};

int main()
{
	Person p;
	p.name = "김철수";
	p.age = 123;
	p.introduce();
	return 0;
}

그림 1. 실행결과.

위의 예제는 사람이 이름과 나이라는 특징(멤버)을 가지고, 자기 자신을 소개할 수 있다는 것을 메서드(함수)로

일반화하여 나타낸 클래스 Person을 정의하고, main함수에서 p라는 변수로 객체로 구체화하여,

각 특징들에 값을 부여하고 introduce 메서드를 호출하고 있다.

 

클래스 내부의 public이라는 키워드는 무엇을 의미할까?

이는 접근제어 지시자라고 하며, 어디서 접근할 수 있는지에 대해 제한해둔 것이다.

c++의 접근제어 지시자는 다음 3가지가 존재한다.

접근제어지시자 public private protected
접근 범위 어디서든지 접근가능 클래스 내부에서만 접근가능 상속받은 클래스에서 추가적으로 접근가능.
이외에는 private와 동일.

2. 생성자

위에서 정의한 클래스는 뭔가 엉성하다.

우리가 멤버 하나하나에 접근하여 초기화를 한다니 너무 불편하다.또 private로 멤버 변수 정의했을 경우, 내부에 접근할 수 없어 변숫값을 변경할 수 없을 것이다.객체지향 프로그래밍에서는 초기화를 담당하는 서브루틴인 생성자를 두어 이런 문제를 해결하였다.

#include <iostream>

class Person
{
// private: 가 생략되어있다.
	std::string name;
	int age;
public:
	Person(std::string name, int age)
	{
		this->name = name;
		this->age = age;
		std::cout << "생성자 호출!" << std::endl;
	}
	void introduce()
	{
		std::cout << name << "(" << age << "세)" << std::endl;
	}
};

int main()
{
	Person p("김철수",123);
	p.introduce();
	return 0;
}

위 코드는 그림 1과 같은 결과를 가진다.

 

Person 클래스를 살펴보면, name과 age 멤버 변수들이 public위에 선언된 것을 볼 수 있다.아무런 접근제어 지시자를 선언하지 않고 멤버 변수나 메서드를 정의하면, 자동으로 private로 지정된다.

 

public영역을 보면, Person(std::string, int)로 정의된 반환형이 없는 함수 형태의 서브루틴이 있다.이것을 생성자라고 하며, 객체가 생성될 때 한 번만 호출된다.생성자는 주로 멤버들의 초기화를 담당한다.


3. this 포인터

생성자 내부에는 this라는 키워드가 있는데, 이는 객체 자기 자신을 가리키는 포인터이다.왜 필요한지 의문이 들 수 있는데, 한 클래스로 여러 객체를 생성했다고 해보자.여러 객체들은 각각 조금씩 다른 특징(멤버)을 가지고 있으므로 멤버 변수는 객체들 각각의 독립적인 공간에 있게 될 것이다.

 

하지만 메서드의 경우, 객체에 따라 각각 다른 멤버들을 매개변수로 받지만, 내부 루틴은 같으므로 객체별로 일일이 메서드를 할당해주는 것은 비효율적이다.

 

따라서 this라는 객체별로 자신을 가리키는 포인터를 넘겨줌으로써 하나의 정의된 위치에서각 객체가 지니고 있는 다른 멤버들을 가지고 같은 루틴을 반복하게끔 한 것이다.

 

위의 예제에서는 비교적 간단한 이유로 사용한 것인데,Person클래스의 생성자의 매개변수가 멤버 변수의 이름과 동일하므로 구별하기 위해서 this를 사용했다.

 

이외에도, 자기 자신에 대한 포인터를 반환해야 하는 등의 이유로 this포인터를 사용해야 할 수도 있다.


4. 생성자와 디폴트 매개변수

C++에서는 디폴트 매개변수라는 것이 있다.

#include<iostream>

int Sum1ToN(int n = 10)
{
	return n * (n + 1) / 2;
}

int main()
{
	std::cout << "1부터 10까지의 합:" << Sum1ToN() << std::endl;
	std::cout << "1부터 100까지의 합:" << Sum1ToN(100) << std::endl;
	return 0;
}

그림 2. 실행결과.

1부터 N까지의 합을 구해주는 함수 Sum1ToN을 정의했다.

함수의 매개변수를 살펴보면,

int n = 10이라고 10을 기본적으로 대입하고 있다.

이는 해당 매개변수에 값이 할당되지 않을 경우 주어진 값을 넣어서 진행시키라는 의미이다.

그래서 메인 함수를 살펴보면 아무런 값을 전달하지 않았을 때,

n에 10이 들어가서 1부터 10까지의 합이 반환된 것을 확인할 수 있다.

 

이를 생성자에도 적용시키면 어떨까?

붕어빵 클래스를 만들었다고 해보자.

사람들이 보통 붕어빵이라고 하면 팥앙금이 들어간 붕어빵을 떠올린다고 하자.

그러면 붕어빵이라는 객체를 생성할 때 재료로 아무것도 전달하지 않으면 

팥이 들어간 붕어빵을 만드는 게 타당하지 않을까?

#include<iostream>

class FishBread
{
	std::string ingridient;
public:
	FishBread(std::string ingridient = "팥")
	{
		this->ingridient = ingridient;
	}
	void Eat()
	{
		std::cout << "냠냠! " << ingridient << "(이)가 들어간 붕어빵이다!" << std::endl;
	}
};

int main()
{
	FishBread redBean; // redBean()과 같은 의미
	FishBread chouxCream("슈크림");

	redBean.Eat();
	chouxCream.Eat();
	
	return 0;
}

 

그림 3. 실행결과.

메인 함수를 보면 생성자에 아무것도 전달하지 않았을 때,

팥이라는 문자열이 들어가서 객체가 생성된 것을 확인할 수 있다.


5. 기본 생성자

위의 소스에서 메인 함수의 첫 줄 코드를 살펴보면 redBean과 redBean()이 같은 의미라고 되어있다.

글의 맨 첫 번째 소스로 돌아가서 클래스를 살펴보면 생성자가 없는 것을 확인할 수 있다.

그러면 생성자가 없는데 우리는 생성자를 호출해서 객체를 생성했다는 말이 된다.

어떻게 가능한 것일까?

 

C++에서 컴파일러는 클래스에 어떠한 생성자도 정의되지 않을 경우

내용이 텅 비어 있는 기본 생성자를 암시적으로 생성시켜준다.

 

그래서 우리가 생성자를 정의하지 않아도 객체가 생성되었던 것이다.


6. 클래스 배열

클래스의 경우에도 배열로 객체들을 정의하여 사용할 수 있다.

다만, 일반적인 배열과 다르게 선언에서 생성자가 항상 호출된다는 것을 명심하여야 한다.

#include<iostream>

class Monster
{
	std::string name;
	int atk;
	int def;
public:
	Monster(std::string name, int atk, int def)
	{
		this->name = name;
		this->atk = atk;
		this->def = def;
	}
	void Info()
	{
		std::cout << "이름: " << name << std::endl;
		std::cout << "- 공격력: " << atk << std::endl;
		std::cout << "- 방어력: " << def << std::endl;
	}
};

int main()
{
	Monster list[3]; // 에러발생!
	for (int i = 0; i < 3; i++)
		list[i].Info();
	return 0;
}

이 소스는 정상적으로 실행되지 않는다.

그림 4. 에러

에러를 살펴보면 기본 생성자를 요구하고 있다.

이는 배열 생성과정에서 생성자가 호출되기 때문인데,

매개변수를 아무것도 주지 않아서 기본 생성자를 호출하고 있는 것이다.

해결 방법에 대해 알아보자.

6.1. 매개변수 전달하기

#include<iostream>

class Monster
{
	std::string name;
	int atk;
	int def;
public:
	Monster(std::string name, int atk, int def)
	{
		this->name = name;
		this->atk = atk;
		this->def = def;
	}
	void Info()
	{
		std::cout << "이름: " << name << std::endl;
		std::cout << "- 공격력: " << atk << std::endl;
		std::cout << "- 방어력: " << def << std::endl;
	}
};

int main()
{
	Monster list[3] = { Monster("괴물",20,10),Monster("큰괴물",40,20),Monster("아주큰괴물",100,50) };
	for (int i = 0; i < 3; i++)
		list[i].Info();
	return 0;
}

그림 5. 실행결과.

main함수를 살펴보면 배열의 각 원소에 알맞은 매개변수를 넣어 

생성자를 호출해 주고 있는 것을 확인할 수 있다.


6.2. 생성자에 디폴트 매개변수 사용

#include<iostream>

class Monster
{
	std::string name;
	int atk;
	int def;
public:
	Monster(std::string name = "괴물", int atk = 20, int def = 10)
	{
		this->name = name;
		this->atk = atk;
		this->def = def;
	}
	void Info()
	{
		std::cout << "이름: " << name << std::endl;
		std::cout << "- 공격력: " << atk << std::endl;
		std::cout << "- 방어력: " << def << std::endl;
	}
};

int main()
{
	Monster list[3];
	for (int i = 0; i < 3; i++)
		list[i].Info();
	return 0;
}

그림 6. 실행결과.

방법은 우선 배열만 생성되게 하는 의미가 더 크다.

여기서는 나중에 멤버 변수 값을 변경하여 사용하는 것이 더 유용할 것이라 판단된다.


7. 캡슐화

캡슐화는 객체지향 프로그래밍의 핵심적인 내용이다.

객체의 속성과 행위를 하나로 묶고, 실제 구현 내용 일부를 외부에 감추어 은닉한다. - 위키백과

이는 왜 기본적으로 멤버 변수를 선언하면 private으로 선언되는지와 연관이 있다.사용자는 어떤 클래스를 사용할 때, 클래스의 세부 정보인 멤버들에 대해 전부 알 필요가 없다.

 

무슨 말이냐면, 누군가 TV를 본다고 할 때, 리모컨 버튼을 눌러 원하는 채널로 이동한다고 해보자.사용자는 리모컨이 적외선 신호를 어떻게 쏘고 TV가 어떻게 수신하는지에 대해 알 필요가 없고,그 중간 과정에 대한 지식이 없어도 버튼만 누르면 채널 이동이 가능하다.

 

이처럼 사용자에게 필요한 만큼의 인터페이스만 공개하고,나머지는 감추어 잘 묶어주는 것이 캡슐화이다.

 

이론적으로는 그리 어려운 내용이 아니지만, 캡슐화를 잘 적용하는 것은 상당히 힘들다.왜냐하면 캡슐화는 단순히 숨기기만 한다고 되는 것이 아니기 때문이다.

객체를 정의할 때 어디서부터 어디까지를 묶고 분리할지,

어떤 것은 감추고 어떤 것은 공개할지 정하는 것은 매우 추상적이기 때문이다.


 

읽을거리.

OOP(객체지향 프로그래밍) - OOP의 정의

반응형

'개념정리 > C++' 카테고리의 다른 글

6. Friend 키워드  (0) 2021.04.21
5. 참조자  (0) 2021.04.20
4. 동적할당과 소멸자  (0) 2021.04.19
2. Namespace  (0) 2021.04.17
1. 기본 자료형 및 입출력  (0) 2021.04.15
반응형

1. Namespace

namespace는 개발자가 코드의 부분을 명시적으로 이름을 붙여 나눠놓은 것이라고 보면 된다.

#include<iostream>

namespace A
{
	int number = 1234;
	void func()
	{
		std::cout << "func() from A" << std::endl;
	}
}

namespace B
{
	int number = 2345;
	void func()
	{
		std::cout << "func() from B" << std::endl;
	}
}

int main()
{
	std::cout << A::number << std::endl;
	A::func();
	std::cout << B::number << std::endl;
	B::func();
	return 0;
}

그림1. 실행결과.

위의 소스를 보면, 전역에 namespace A와 namespace B를 선언하여,

두 공간 안에 number라는 정수형 변수를 선언하고 값을 넣어준 것을 볼 수 있다.

또, func라는 함수를 선언해서 각각의 공간에 넣어주었다.

 

그리고 main 함수 안에서는 A::number, B::number와 같은 방법으로 각 공간 안의 변수에 접근하고 있는 것을 볼 수 있다. 두 변수는 이름이 같지만, 서로 다른 변수로 취급되는 것도 출력 결과를 통해 확인할 수 있다.

 

함수의 경우에도, A::func(), B::func()처럼, 이름은 같으나 서로 다른 함수라는 것을 namespace를 통해 나타내고 있다.


2. using 키워드

namespace로 공간을 명시적으로 만들어 나눌 수 있다는 것은 알겠는데,

사용할 때 명시적으로 공간의 이름을 앞에 붙여야 하기 때문에 불편하다. 생략해서 사용하는 방법은 없을까? 이는 using 키워드를 사용하면 가능하다.

#include<iostream>

namespace A
{
	int number = 1234;
	void func()
	{
		std::cout << "func() from A" << std::endl;
	}
}

using namespace A; // namespace A 사용!

int main()
{
	std::cout << number << std::endl; // 이 number는 A의 number
	func(); // 이 func()는 A의 func()
	return 0;
}

그림2. 실행결과.

 

using 키워드는 지정된 namespace 안의 내용을 모두 꺼내어 사용하게 해 준다고 보면 된다.

따라서 A라는 namespace에 using 키워드를 사용했으니,

이제 number와 func라고만 쓰면 당연히 A의 number와 func를 가져와 사용할 것이다.


3. Namespace의 모호성

using 키워드를 사용해서 공간의 이름을 생략하는 것도 좋지만, 너무 남용하는 것은 문제를 일으킬 수 있다. 다음 코드를 보자.

#include<iostream>

namespace A
{
	int number = 1234;
}

namespace B
{
	int number = 2345;
}

using namespace A; // namespace A 사용!
using namespace B; // namespace B 사용!

int main()
{
	std::cout << number << std::endl; // A? B?
	return 0;
}

namespace A와 namespace B 둘을 동시에 using으로 사용하고 있다.

이렇게 되면, 두 공간에 정의된 같은 이름의 number가 밖으로 나오게 되고, 컴파일러는 메인 함수에서 호출된 number가  두 공간 중에 어떤 number인지 알지 못하게 된다.

그림3. 에러

이런 경우에는 어떻게 해야 할까?

겹치는 경우에 대해 명시를 해주면 된다.

#include<iostream>

namespace A
{
	int number = 1234;
}

namespace B
{
	int number = 2345;
}

using namespace A; // namespace A 사용!
using namespace B; // namespace B 사용!

int main()
{
	std::cout << A::number << std::endl; // A!
	return 0;
}

그림4. 출력결과.

using을 썼더라도, 명시적으로 이름을 붙여줘서 공간을 구별할 수 있다.

위의 경우를 통해, using을 사용할 경우 각 namespace의 내용을 잘 숙지하고 사용해야 한다는 것을 알게 되었다.


4. namespace std

사실 namespace에 대해 공부하기 이전부터 우리는 namespace를 사용했다.

바로 std라는 namespace에서 cout과 cin, 그리고 endl을 사용했다. 이제 using을 배웠으니 std 역시 생략이 가능하다는 것을 알 수 있다.

#include<iostream>

using namespace std;

int main()
{
	cout << "C++ programming" << endl;
	return 0;
}

그림5. 출력결과.

위 코드를 보면 cout과 endl의 앞에 std::를 붙이지 않고 사용한 것을 알 수 있다.

 

하지만 많은 사람들이 되도록이면 using을 namespace에 사용하지 않는 것을 권장한다.

간단한 실습 수준의 단계에서는 코드가 짧고, 겹칠 문제가 없지만 남이 만든 라이브러리를 사용하거나, 협업을 한다거나, 아주 긴 코드를 작성하게 된다면, 3번에서 나온 경우처럼 충돌하는 일이 발생할 것이다.

 

따라서 간단한 실습용 코드에서만 using namespace을 사용하도록 하자. 또는, std안에 있는 내용과 자신의 코드가 겹치지 않게 일부만 가져와서 사용하도록 하자.

#include<iostream>

using std::cout; // cout만 사용
using std::endl; // endl만 사용

int main()
{
	cout << "C++ programming" << endl;
	return 0;
}

위 코드는 그림 5와 같은 출력 결과를 가진다.


읽을거리.

why is "using namespace std;" considered bad practice?

- using namespace std에 관한 질문이다.

std namespace에 한해서만 using 키워드를 쓰지 말라는 내용이 아니라,

모든 네임스페이스에 대해 using을 사용하는 습관을 들이지 말라는 내용이다.

반응형

'개념정리 > C++' 카테고리의 다른 글

6. Friend 키워드  (0) 2021.04.21
5. 참조자  (0) 2021.04.20
4. 동적할당과 소멸자  (0) 2021.04.19
3. 클래스  (0) 2021.04.18
1. 기본 자료형 및 입출력  (0) 2021.04.15
반응형

1. 기본 자료형

#include<iostream>

int main()
{
	bool b = true; // 새로생긴 자료형

	char c = 'c';
	int n = 10;
	long l = 320000000;
	long long ll = 23132165465849848;
	double lf = 3.1415926535;
	float f = 12.1f;

	return 0;
}

C에서 C++로 넘어오면서 bool이라는 자료형이 생겼다.

bool은 1바이트의 크기를 지니는 true와 false의 두 가지 상태를 저장할 수 있는 자료형이다.

그 외에 C에서 사용하던 자료형은 그대로 가져온 것을 알 수 있다.

 

또, 달라진 점이 있다면 기본 제공되는 헤더 파일을 불러올 때 .h를 붙이지 않는다는 것이다. 기존의 C에서 사용하던 헤더를 사용하고 싶으면 다음과 같이 쓸 수 있다.

#include<cstdio>
#include<cstring>

...

원래 쓰던 라이브러리 이름에 c만 붙여주면 된다.


2. string 클래스

#include<iostream>
#include<string>

int main()
{
	std::string str = "Hello, World!"; // 새로 생긴 문자열 클래스
	return 0;
}

bool 이외에 또 새로 생긴 것이 있는데, 바로 string 클래스이다.

C에서 문자열을 불편하게 포인터를 사용하여 처리했던 것과 다르게,

이제는 문자열을 기본적으로 제공하므로 편리하게 사용할 수 있다.


3. 기본 출력

#include<iostream>

int main()
{
	std::cout << "Hello, World!" << std::endl;
	return 0;
}

그림1. 출력 결과.

위는 기본적인 c++의 출력방식이다.

변수의 경우의 출력도 한번 살펴보자.

#include<iostream>
#include<string>

int main()
{
	bool b = true;
	char c = 'c';
	int n = 10;
	long l = 320000000;
	long long ll = 23132165465849848;
	double lf = 3.1415926535;
	float f = 12.1f;
	std::string str = "A String";

	std::cout << b << std::endl << c << std::endl << n << std::endl << l;
	std::cout << ll;
	std::cout << std::endl;
	std::cout << lf << "\n" << f << '\n';
	std::cout << str << std::endl;
	return 0;
}

그림2. 출력 결과.

출력 결과를 보면 std::endl가 개행 문자와 같은 역할을 수행한다는 것을 알 수 있다.


4. 기본 입력

출력에 대해 알아보았으니 이제 입력받는 방법도 알아보자.

#include<iostream>

int main()
{
	int number;
	std::cin >> number;
	std::cout << "입력받은 정수: " << number << std::endl;
	return 0;
}

입력은 std::cin으로부터 가능하다. 이번에는 >>를 사용하여 뒤에 입력받고 싶은 변수명을 넣으면 된다.


읽을거리.

Why we should avoid using std::endl

- std::endl의 필요성에 대해 찾아보다가 찾은 글이다.

std::endl이 그냥 개행 문자를 출력하는 것보다 비 효율적이므로 과하게 사용하지 않는 것이 좋다는 글이다.

반응형

'개념정리 > C++' 카테고리의 다른 글

6. Friend 키워드  (0) 2021.04.21
5. 참조자  (0) 2021.04.20
4. 동적할당과 소멸자  (0) 2021.04.19
3. 클래스  (0) 2021.04.18
2. Namespace  (0) 2021.04.17

+ Recent posts