본문으로 바로가기

Day-12 C++ 이야기2

category Programming/TIPS 17기 2017. 8. 11. 12:17

 

 

class

 

C++ 언어에서 객체를 class라는 문법으로 다루고 있다.

C++언어의 struct는 c언어의 struct와 다르다. 오히려 C++언어의 class가 C언어의 struct와 유사하다.

 

C언어로 '이름', '나이', '몸무게'를 입력 받아 저장하는 프로그램을 만들어보자. 이 데이터는 한 사람에 대한 정보이기 때문에 그룹지어서 관리해야하고 세 데이터 타입이 모두 달라서 구조체를 사용해 정의해야 한다.

 

 

 

위와 같이 우선 구조체를 정의하고 구현해야하고 나열식으로 적는다. 하지만 이렇게 적으면 추후 변화에 대해 유지 보수가 힘들어진다.

 

초보 프로그래머는 아래와 같이 프로그램을 구현한다.

 

 

하지만 이렇게 프로그램을 구현하면 프로그램의 기능이 변경될 때 즉 변화가 올때 변화에 대처하기 힘들다. 따라서 이를 대비하기 위해 기능별로 함수를 구분해서 만든다.

 

 

위와 같이 코드를 작성하면 처음에 구현한 프로그램보다 변화에 대한 유지 보수가 상대적으로 쉽지만 코드가 복잡해 보인다.

함수를 나누면 어떤 변수를 넘겨주고 넘겨 받기 위해서는 포인터 기술을 사용해야 한다. 즉, C언어는 포인터를 많이 사용할 수 밖에 없다.

 

C언어 프로그래밍의 단점

 

1. 변화에 대처하기 힘들다.

2. 변화에 대처할 수준에 도달한 사람들은 포인터를 사용함으로써 전체적인 코드가 길어진다.

 

1, 2번을 해결하기 위해서 C++은 객체라는 개념을 도입한다. 다시 말하지만 객체란 일반적으로 일어나는 행위를 표준화 시키는 것이다.

 

C++ 언어에서 class 문법을 사용하는 법을 알아보자.

 

우선 struct 문법을 class 문법으로 변경한다. struct 대신 class를 써 주면 People이라는 단순한 자료형에서 객체로 변한다.

또한 class에 선언된 name, age, weight는 멤버 변수라고 부른다.

 

 

위와 같이 소스코드를 작성하면 된다. 하지만 주의할 점이 있다. 접근 제한자라는 개념이 있기 때문에 외부에서 참조할 수 없다.

 

 

위와 같이 소스를 작성하면 에러가 발생한다.

struct는 변수를 선언해서 변수 참조를 할 수 있지만 class는 접근 제한자라는 개념 때문에 외부에서 멤버변수를 참조하면 에러가 발생한다.

 

 

위와 같이 접근 제한자를 따로 명시하지 않으면 자동적으로 'private'라는 접근 제한자가 생략된 것으로 생각한다.

즉, private는 생략이 가능하다. People이라는 클래스 안에 있는 변수를 외부에서 사용할 수 없도록 막는 키워드이다. private 대신에 public을 사용하면 C언어의 struct와 같은 기능을 한다.class의 주된 기능을 하지 못한다는 뜻이다. 원칙은 private이다. 원칙을 잘 지켜야 C++의 철학을 잘 느낄수 있으니 원칙을 잘 지켜 사용하자.

 

접근 제한자

 

 

멤버 함수

 

class 문법은 지금까지 알아본 것처럼 변수에도 정의할 수 있지만 함수에도 정의할 수 있다. 멤버 함수에서는 같은 클래스에 선언된 멤버 변수를 접근 제한자와 상관없이 모두 사용할 수 있다. 멤버 함수를 사용하여 멤버 변수 age에 23을 대입하는 소스코드를 작성 해보자.

 

 

C언어에서 구조체는 데이터만 사용할 수 있다. 하지만 C++언어에서 class는 함수를 포함할 수 있다. 표준화된 행동을 모두 객체 속에 넣을 수 있다.

맴버 변수도 함수이기 때문에 지역변수를 사용할 수 있다. 하지만 멤버 변수의 이름과 멤버 함수의 지역 변수의 이름이 중복되면 버그가 발생한다. 따라서 멤버 변수의 이름 앞에 "m_"을 붙여 주는 것이 좋다.

 

멤버 함수가 추가된 이유

 

C++언어가 프로그램을 유지 보수 하는데 가장 중요한 것은 모든 행위가 데이터를 바라보고 있고 변하는 것은 데이터이다. 모든 프로그램은 데이터가 바뀌면서 문제가 생긴다. C++은 가능한 외부에서 데이터를 참조하지 못하도록 제한을 두었다. 행위는 함수로 표현되어 class와 같이 표현한다. 즉, 데이터를 보호하기 위해서 멤버 변수, 멤버 함수의 개념을 사용한다.

 

함수는 변화에 대처할 수 있기 때문에 public 키워드를 사용하고 변수는 변화에 대처할 수 없기 때문에 private 키워드를 사용한다.

멤버 함수는 멤버 변수에 접근할 수 있다. 즉, 데이터를 가리키는 함수가 있기 때문에 변화가 왔을 때 함수를 수정해 주면 된다.

프로그램의 모든 변화는 데이터에서 오기 때문에 데이터에 접근할 수 있는 길목을 하나로 만들어 길목에서 데이터를 관리해주면 된다.

 

C++언어는 문법으로 함수를 만들게 강요한다. 문법으로서 프로그래머가 잘못된 스타일을 가지면 올바른 스타일을 가지도록 문법으로 제한한다. 프로그래머에게 데이터에 접근할 때는 무조건 함수를 만들어서 접근하라는 것이다. C언어는 함수를 만들지, 만들지 않을지는 프로그래머의 선택이다. 문법적으로 강요하지 않는다.

 

인터페이스란?

 

용어적 의미 : 대상이 실제로 어떻게 작동하는지 모르지만 내가 원하는 것을 얻도록 하는것이다.

자동차가 제공하는 인터페이스 : 핸들, 기어, 브레이크, 엑셀 등

 

 

C언어는 데이터와 데이터를 쓰는 행위를 별개로 보고 있고 이것을 포인터로 해결한다. C++언어는 데이터와 데이터를 쓰는 행위를 하나로 보고 있다. C++언어는 C와 비교했을 때 양은 많아지지만 코드 자체 포인터가 사라지면서 더 간결해진다.

class의 가장 큰 장점은 외부에서 직접적으로 만들지 못하게 해서 함수를 만들게 유도해서 데이터와 함수를 쌍으로 만든다는 것이다. 표현이 갈결해지고 변화와 대응이 좋다.

 

연산자 오버로딩

 

오버로딩은 "과적하다, 너무 많이 주다"라는 뜻을 가지고 있다. 과적이란 "자신이 해야할 것 보다 너무 많이 가지고 있다" 라는 뜻이다.

 

C언어에서 연산자, 함수의 의미는 단 하나만 가질 수 있다. 하지만 생각해보면, * 연산자는 두개의 의미를 가진다. '곱셈의 용도'로 사용하기도 하고, 포인터 문법에서는 '번지를 지정하는 용도'로 사용되어서 한개의 키워드가 두개의 의미를 가진다. 

하지만 * 연산자가 포인터 문법으로 사용할 때는 '단항 연산자'로 사용되고 곱셈을 할 때는 '이항 연산자'로 사용되어 같은 키워드를 사용했지만 문법적으로는 서로 다른 연산자이다. 문법에서 그 의미를 구별할 수 있으면 같이 쓰일 수 있다. 단, 사용자가 바꿀 수 없고 문법적으로 바꿀 수 없기 때문에 오버로딩이라고 하지 않는다. C언어는 데이터에 행위 표현을 못하지만 C++ 언어는 데이터에 대한 행위 표현을 class에서 할 수 있다.

 

단항 연산자 : 한 개의 연산자와 한 개의 피연산자로 구성된다.

이항 연산자 : 한 개의 연산자와 두 개의 피연산자로 구성된다.

 

문자열("abc")과 문자열("def")을 합치는 소스를 C와 C++의 입장에서 소스코드를 작성해 보자.

 

C언어의 입장

 

 

C++언어의 입장

 

 

C++ 언어는 멤버 함수의 이름에 연산자 키워드를 사용할 수 있다. 따라서 AppendString 대신 oeprator+ 라는 이름으로 멤버함수를 정의할 수 있다.

 

 

하지만 C++ 언어는 연산자를 멤버 함수로 정의한 경우에 한해서 아래와 같이 표기가 가능하다.

따라서 AppenString대신 + 를 사용함으로써 소스코드의 의미를 보다 확실하게 전달 할 수 있다.

 

 

C++ 컴파일러가 +연산자를 만났을 때 피연산자들을 정수로 단정 짓지 않고 피연산자들 중에 객체가 있는지를 확인해서 처리한다. 피연산자 중에 객체가 있는 경우 해당 객체에 + 연산자가 정의되어 있다면 기본 + 연산을 처리하지 않고 해당 객체의 + 함수를 호출한다.

 

C++ 언어의 생각

덧셈이라는 더한다는 의미가 왜 정수와 실수에서만 적용될까...? 프로그래머가 일반적인 상황에서 작명에 대한 스트레스를 줄여주려고 노력한 결과이다.

 

+ 연산자를 함수 이름으로 사용 가능하다. 컴파일러가 +만 사용할 수 있게 허락한다. C++ 컴파일러는 C와 달라서 모든 연산자가 오게 되면 좌, 우에 클래스가 있는지 확인한다. 클래스가 있으면, 연산자 오버로딩이 있는지 확인한다. 해당 클래스에 있는 연산자 오버로딩을 먼저 사용한다. 이렇게 되면 소스도 간단해지고 의미부여도 강해진다.

 

단, + 연산자를 정의한 객체가 + 연산보다 앞에 있어야 위와 같은 소스코드가 정상적으로 작동한다는 것을 알아두자.

 

함수의 오버로딩

 

C언어에서 함수가 하나의 의미만 존재해야만 한다. 그러나 C++언어에서는 함수의 이름이 같아도 여러개 존재 할수 있다.

 

C언어애서 아래의 두 함수를 동시에 선언하면 오류가 발생한다.

 

 

이미 Sum이라는 함수가 만들어져 있기 때문에 오류가 발생한다.

 

 

C언어에서 이 오류를 해결하려면 함수의 이름을 바꾸어주어야 한다.

 

C++언어에서 함수의 이름만으로 함수를 구분하지 않고 해당 함수가 사용하는 매개 변수의 개수나 매개 변수의 자료형이 동일한지를 추가로 비교해서 함수를 구분한다.

 

즉 아래의 두 함수를 선언해도 오류가 발생하지 않는다.

 

 

함수의 이름이 같아도 매개변수의 개수나 자료형이 다르기 때문에 함수를 정의할 수 있다.

 

 

이처럼 매개변수의 개수나 자료형이 다르면 동일한 함수가 두 개 이상 정의될 수 있는데 이것을 함수의 오버로딩이라고 한다.

즉, C++ 언어에서 함수를 구분할 때 이름으로 구분하지 않고 매개변수와 자료형으로 구분한다는 것을 알 수 있다.

단, 하나의 함수의 이름이 같은게 많아지면 나중에 소스코드를 이해하기 힘들다. 또한 함수의 오버로딩의 특성때문에 함수의 오버로딩을 사용하면 C언어와 호환이 되지 않는다. 오버로딩은 반환값과 상관이 없다 따라서 매개변수의 개수나 자료형이 같은데 반환형만 다르더라도 같은 함수로 인식해 오류가 발생한다. 가능하면 함수의 이름은 자기 자신이 구분지어 사용하는것이 좋다.

 

객체 생성자

 

클래스의 이름과 함수의 이름이 같으면 객체 생성자라고 한다. 반환값이 없다. 아무런 함수를 호출하지 않아도 객체를 선언하기만 하면 자동으로 호출하는 기능을 한다. 객체의 완성도가 올라가고 사용하는 사람이 객체를 잘 몰라도 사용할 수 있게 하는 기능을 한다.

객체 생성자가 함수의 오버로딩처럼 여러 개를 선언할 수 있다. 다양한 기능을 제공한다는 의미이다.

 

주로 객체를 생성하면 객체의 멤버 변수를 초기화 해야한다. 즉, 클래스로 객체를 만들면 객체의 멤버 변수를 초기화하는 함수를 반드시 호출해야 한다. 결국 클래스로 객체를 선언하는 작업과 초기화 함수를 호출하여 객체를 초기화하는 작업은 한 세트라고 생각할 수 있다. 항상 이런 세트과정을 적어주는 것은 비효율적이기 때문에 자동으로 호출하려고 해서 나온개념이 객체 생성자 개념이다.

 

C++ 언어는 객체가 생성될 때 객체를 초기화 하는 작업이 자동으로 진행될 수 있도록 '생성자'라는 자동 호출 함수를 제공한다.

아래와 같이 사용된다.

 

 

기본 생성자와 다른 생성자에 대하여 알아보자

 

아래와 같이 두 개의 생성자를 가진 MyObject 클래스를 정의했다고 하자. 이 두 생성자의 이름은 같지만 MyObject()는 매개 변수가 없고 MyObject(int a_value)는 정수형 매개 변수를 가지기 때문에 함수의 오버로딩이 적용되어 서로 다른 함수로 처리된다.

 

 

기본 생성자 사용하기

 

매개 변수가 없이 선언된 생성자를 기본생성자라고 한다. 따라서 MyObject 클래스를 사용하여 객체를 선언할 때 아래와 같이 아무런 추가 표현 없이 객체를 선언하면 기본 생성자가 호출된다.

 

기본 생성자는 기본적으로 함수 호출하듯이 사용할 수 있다. 하지만 아래를 보고 차이를 느껴보자.

 

MyObject test;           //  에러 X

MyObject test();        //  에러 O            MyObject test()는 MyObject를 반환값으로 가지는 함수의 원형이다. 따라서 컴파일러가 함수와 기본 생성자를

구분하지 못해서 에러가 발생한다.

 

 

다른 생성자 사용하기

 

기본 생성자를 제외한 생성자를 다른 생성자라고 한다. 다른 생성자를 호출하고 싶으면 아래와 같이 호출하기 원하는 생성자의 매개 변수 형식을 같이 적어주면 된다.

 

 

객체 파괴자

 

객체 생성자와 반대 역할을 한다. 객체 생성자는 초기화, 객체 파괴자는 정리를 한다. 객체가 사라질 때 자동으로 호출되어 객체의 뒷정리를 담당한다. 자동화 호출이 되고 필요하지 않으면 사용하지 않아도 된다. ~을 사용한다. 객체가 파괴될 때 자동으로 호출한다. !을 사용하지 않고 ~을 사용하지 않는 이유는 !는 논리 부정이다. ~은 부정이 아니라 반대값을 의미한다. 또한 객체 파괴자는 단 한개만 사용 가능하다.

 

객체 생성자는 변수를 선언하는 행위를 한다. 객체 파괴자는 명시적으로 일어나는것이 아니라 보통 암시적으로 일어난다.

 

객체 파괴자는 직접 호출을 할  수 있다. 하지만 객체 파괴자가 호출된 것이 아니라 명시적으로 호출한 것 뿐이다. 컴파일러가 인정하지 않고 또 호출한다. 즉, 명시적으로 호출을 하면 똑같이 두 번 호출되는 것이다. 객체 파괴자는 반드시 하나를 사용해야하고, 명시적으로 호출하지 않아도 된다.

 

아래와 같이 선언하면 된다.

 

 

객체 생성자와 객체 파괴자를 사용하는 소스코드를 작성 및 분석해보자.

 

 

 

Namespace와 Scope 연산자

 

큰 프로그램을 개발하는 경우에 여러 명의 개발자가 소스를 나누어 개발하기 때문에 함수나 변수의 이름이 중복되는 경우가 종종 발생한다.

이렇게 되면 중복된 이름을 수정하는 과정을 거쳐야 한다. 하지만 서로 다른 곳에서 구매한 두 개의 라이브러리에서 이름 중복 문제가 발생하면, 소스가 없어서 수정을 못하기 때문에 두 개의 라이브러리중 한 개는 사용하지 못하는 경우가 발생한다.

 

C++ 언어는 이런 문제를 해결하기 위해서 함수나 전역 변수를 새로운 이름으로 그룹 지을 수 있는 Namespace문법을 제공한다.

 

 

namespace를 사용하여 작업을 구분지어서 사용한다. namespace로 구분지어서 사용하기 때문에 전역 변수나 함수의 이름이 동일하더라도 오류가 발생하지 않는다.

 

어떤 namespace에 선언된 함수인지를 확실하게 하기 위해서 아래와 같이 사용할 함수를 포함하고 있는 namespace 이름과 ::(Scope 연산자)를 함께 적어주어야 한다.

 

 

하지만 매번 이렇게 namespace 이름과 ::  연산자를 같이 사용해주는 불편함이 발생한다 그래서 자주 사용하는 namespace를 생략할 수 있도록 해주는 using namespace라는 문법이 제공된다. using namespace를 사용하여 함수 호출 시 namespace::를 생략할 수 있다.

 

 

단 using namespace를 여러개 사용하면 namespace::를 생략할 수 없다.

 

class와 Scope 연산자

 

class 형태도 namespace의 일종이다. 따라서 class도 scope 연산자를 사용할 수 있다. namesapce는 객체와 상관이 없다.

 

아래와 같이 class를 계속 다 적으면 함수를 보기 힘들고 소스코드가 늘어난다.

 

 

이렇게 되면 class를 관리하기가 점점 더 힘들기 때문에 class 선언부 밖에서 ::(Scope)연산자를 활용하여 멤버 함수를 구현할 수 있다.

 

 

결국 class 선언부에는 멤버 함수의 원형만 남겨두고 실제 함수의 구현 부분은 밖에다가 '클래스이름::'을 멤버 함수의 이름 앞에 추가해서 옮긴 것이다.

 

상호 참조 구조

 

아래와 같이 참조 하면 에러가 발생한다.

 

 

조금만 더 생각해보면 아래와 같이 바꿀 수 있다. 하지만 아래의 소스코드도 위와 같이 오류가 발생한다.

 

 

해결법

 

 

 

Scope 연산자의 활용

 

:: 연산자를 사용할 때 namesapce 이름이나 class 이름을 적지 않고 :: 연산자만 변수 앞에 붙여서 사용하게 되면 전역의 의미를 가진다.

 

 

일반 함수와 멤버 함수의 이름이 같은 경우

 

 

클래스의 멤버 함수에서 자신과 이름이 동일한 일반 함수를 호출하고 싶다면 위에서 설명처럼 namespace나 클래스 이름을 적지 않고 호출할 함수 이름 앞에 :: 연산자를 사용하면 된다.