F.R.I.D.A.Y.

[TIPS 19TH] 10 : 2018.07.26. (목) 본문

외부활동/TIPS 19th

[TIPS 19TH] 10 : 2018.07.26. (목)

F.R.I.D.A.Y. 2018. 7. 27. 15:33
반응형

1. 접근 제한자

 이전 시간에 짧게 접근 제한자라는 것에 대해 언급한 적이 있다.

 C++의 클래스는 접근 제한자라는 것을 지원하는데 이 접근 제한자는 말 그대로 어떠한 대상이 접근하는 것을 제한하는 것을 의미한다.


 아는 사람은 집에 들여보내주지만, 모르는 사람은 집에 들여보내주지 않는 것, 사적인 정보는 다른 사람들과 공유하지 않는 것과 마찬가지로 보면 된다.


 접근 제한자는 다음과 같은것이 있다.


 접근 제한자 

 의미 

 private 

 외부에서 멤버에 접근하지 못함.

 클래스 내부에서만 사용 가능. 

 protected 

 자신으로부터 파생되지 않은 외부에서 접근하지 못함. (제약적 개방)

 자신과 자신으로부터 파생된 클래스 내부에서만 사용 가능.

 public 

 외부에서 멤버에 접근할 수 있음. (완전 개방)

 외부에서 클래스 내부로 접근 가능. 


 이러한 접근 제한자는 [ const ] 키워드와 마찬가지로 컴파일 후 기계어에 아무런 영향을 주지 않는다. 단순히 프로그래머가 더 편하게 작업할 수 있도록 도와주는 녀석이다.

 [ private ]와 [ protected ]는 외부에서 접근할 수 없다고는 했지만, 완전 접근 불가는 아니고 직접적으로 접근했을 때는 접근을 허용하지 않지만 포인터 문법을 통해 접근할 경우에는 접근이 허용된다. 윗줄에서 설명한 것이 이유이다.


 일반적으로 [ private ]에는 멤버 변수를, [ public ]에는 멤버 함수를 작성한다. [ private ]영역은 외부에서 접근할 수  없기 때문에 이렇게 작성하게 되면 데이터를 대입시키는 인터페이스[각주:1]와 데이터를 반환받는 인터페이스, 각 하나씩 두개를 작성하게 된다.



2. 생성자

클래스 객체를 사용할 때, 생성할 당시 해당 클래스로 만들어지는 오브젝트가 정상 작동할 수 있도록 오브젝트 생성과 동시에 자동으로 호출되는 함수이다.


 이전 C언어에서는 변수를 만들 때, "선언한다.", "메모리에 할당한다." 얘기를 해왔다. 그 이유는 변수를 만들면 메모리에 적재가 되는 한가지 작업만 이뤄지기 때문이다. 그러나 C++에서는 클래스로 객체를 만들게 되면, 메모리에 적재가 되는 것은 물론, 지금 설명하는 생성자가 만듦과 동시에 호출되기 때문에 "인스턴스(객체)를 할당한다." 라고 얘기한다.


class Student{        // 정의 내리는 부분의 'Student'가 클래스
...
};

int main(void){
        Student data;
// 정의 내려진 클래스로 인스턴스를 만드는 위부분의 'Student'가 객체, 인스턴스. 인스턴스된 것이 오브젝트

        ...

        return 0;
}

 생성자에 매개변수가 없으면 기본 생성자이고, 매개변수가 존재하면 기본 생성자 이외에 추가로 생성자를 정의하는 오버로딩이 적용된다.

 이러한 생성자는 각 클래스에 0개 이상이어야 한다. 굳이 생성자를 만들지 않아도 된다는 소리다.


 이러한 생성자도 여러가지로 나뉘는데 "대입 생성자", "복사 생성자" 등 다양한 생성자가 있다고 한다. 여기서 복사 생성자를 특히 주의해야하는데, C++에서는 "얕은 복사"와 "깊은 복사"로 나뉘어 있다. 아래 코드를 보자.

 위 코드를 보게되면 [ p2 ]에 [ p1 ]의 값을 대입하는 것인데, 값은 대입되지만, [ name ]이나 [ phone ] 변수처럼 포인터 변수인경우에는 포인터 변수가 가리키는 값이 아니라 포인터 변수가 저장하고 있는 주소값이 복사가 되어 예상과는 다르게 프로그램이 작동한다.

 이러한 얕은 복사로 인해 발생하는 문제 때문에 복사 생성자가 추가로 필요한것이다.


	Person(const Person &cpyTarget) { // 복사 생성자. 매개변수를 레퍼런스로 지정함 (사용이 편하도록)
		setName(cpyTarget.name);
		setPhone(cpyTarget.phone);
		age = cpyTarget.age;
	}

	void setName(const char *name) {
		if (this->name) delete[] this->name;
		int len = strlen(name);
		this->name = new char[len + 1];
		memcpy_s(this->name, len + 1, name, len + 1);

	}

	void setPhone(const char *phone) {
		if (this->phone) delete[] this->phone;
		int len = strlen(phone);
		this->phone = new char[len + 1];
		memcpy_s(this->phone, len + 1, phone, len + 1);

	}

 위 모델과 같이 작성해야 생각한대로 정확하게 작동하도록 만들 수 있다.



3. 소멸자(객체 파괴자)

 앞서 객체를 생성할 때 생성자를 불러왔다면, 해당 객체를 다 사용하고 나면 소멸자도 만들어주어야한다.

 소멸자는 생성자와는 달리 하나 미만으로 정의해야 한다. 또한 매개변수는 지정할 수 없다. 만일 컵을 사용하는데 남이 이미 사용해서 더러운 컵이라면 깨끗이 씻어서 사용해야 하는것처럼 메모리를 다른 프로그램이 쉽게 사용할 수 있도록 남은 먼지들을 털어내주는 작업을 하는 것이라고 보면 된다.


 소멸자는 생성자와 마찬가지로 클래스 네임과 같지만 생성자와 구분을 위해 이름 앞에 ' ~ '물결 무늬를 추가해준다. 이 물결 무늬는 비트 NOT 연산자인데 반대[각주:2]를 의미하기 위해 사용한다.


 생성자에서 다뤘던 [ Person ]클래스의 소멸자는 이 부분이다. 


~Person(){
	if (this->name) delete[] this->name;
	if (this->phone) delete[] this->phone;

}

만일 [ name ]과 [ phone ]변수에 동적으로 할당해주었다면 할당된 힙 메모리 부분을 해제해주는 작업이 필요하다.


 이러한 작업은 프로그래머가 명시적으로 선언은 해주어야하지만 언제 호출하는지는 직접 정할 수 없다. 각 객체는 함수와 생명주기를 함께 하기때문에 함수가 끝나게 되면 프로그램이 알아서 소멸자를 호출해주기 때문에 함수가 끝나는 부분에 프로그래머가 명시적으로 소멸자 함수를 호출하게 되어버리면 결론적으로 소멸자가 두번 호출되기 때문에 불필요한 작업이 늘 뿐만 아니라 예기치 않은 오류를 발생시킬 수 있는 잠재적인 위협이 될 수 있다.



4. 메모리 동적할당

 C에서는 메모리 동적할당이 함수로 이루어져있다. [ malloc ]이 바로 그 함수인데, 이 함수는 C언어의 문법이 아니기 때문에 컴파일러가 함수 내부를 이해하지 못한다. 그래서 C++에서는 이러한 메모리 동적할당 함수를 자신의 문법을 끌어들였다.


Person *p = new Person;

delete p;

 위 코드에서 [ new ]가 동적할당을 해주는 연산자이며 이를 해제시켜줄 때는 아랫줄의 [ delete ]를 사용하면 된다. 또한, 연산자이기 때문에 따로 괄호를 사용한다던지 할 필요는 없다.


 여기에서 주의해야할 것은 배열 동적할당과 일반 동적할당을 구분해야한다.


Person *p1 = new Person[10];
Person *p2 = new Person;

delete[] p1;
delete p2;

 대괄호를 통해 인덱스를 생성한 경우에는 [ delete[] ]로, 대괄호가 없는 경우에는 [ delete ]로 작성해주어야 문제가 발생하지 않는다. 또, 동적할당을 해제하는 것으로 C언어의 [ free ]를 사용해서는 안된다. 마찬가지로 [ malloc ]으로 동적할당을 해준 뒤 [ delete ]로 해제해도안된다.


 여기서 예외사항이 있는데, 일반 자료형[각주:3]의 경우에는 위의 주의사항이 적용되지 않는다.


int *p = (int *)malloc(sizeof(int));

delete p;

int *p2 = new int;

free(p2);

 일반 자료형의 경우에만 위와같이 혼용이 가능하다.


 동적할당은 [ new ]를 통해 새로운 객체를 힙에 넣을 때 생성자가 호출되고 [ delete ]를 사용함으로써 소멸자가 호출된다.

 참고

C언어는 왜 메모리 동적할당을 함수로 만들었을까?

>> C언어가 만들어진 시점의 OS는 메모리 관리 구조가 제각각이었다. 이러한 상황에서 C언어가 메모리 동적할당을 언어 자체 문법으로 끌어들이게 되면 OS마다 컴파일러를 수정해야한다. 이러한 상황은 C언어의 철학과 어긋난다고 판단하여 언어 내부로 끌어들이기보다 함수로 만들어 사용하도록 했다.


C++은 왜 동적할당을 연산자에 넣었을까?

>> C언어와 같이 함수로 생성하게 되면 스택프레임을 생성해야하기 때문에 상대적으로 속도가 떨어질 수 있으므로 기계어로 직번역을 위해 연산자로 만들었다는 것이 존재한다. 또한, C언어와 같이 OS의 차이때문에 발생할 수 있는 메모리 관리 구조 역시 C++이 만들어지는 시점에는 대부분의 OS 메모리 관리 구조가 비슷하다고 판단하여 포함했다고 한다.



5. namespace

 소규모의 프로그램을 개발할 때는 문제가 되지 않지만, 대규모 프로그램을 만들게 되면 한사람이 모든 부분을 작성할 수 없기 때문에 각 기능들을 나누어 개발한다. 이러한 개발과정에서 발생할 수 있는 문제는 함수, 변수 이름의 중복이다. 이러한 문제를 해결하기 위해 C++의 경우에는 [ namespace ]라는 기능을 도입했다.


int data;

int main(void) {
        int data;

        data = 5;

        return 0;
}

 위와 같은 코드에서는 C언어 기준으로 [ main ]함수 밖의 전역변수 [ data ]를 사용하기 위해서는 지역변수의 이름을 바꾸거나 전역변수 이름을 변경해야했다. 그러나 C++은 [ namespace ]의 도입으로 문제를 해결했다.


 [ namespace ]는 다음과 같이 사용된다.


namespace A {
	int data;
}

namespace B {
	int data;
}
int main(void) {
	A::data = 5;
	B::data = 4;

	return 0;
}

 [ namespace ]는 중괄호로 범위를 지정하고, 안에 들어있는 변수를 사용할 때는  ' ::[각주:4] '를 이용한다.


 이러한 장점이 있지만, 매번 [ namespace ]의 이름을 함께 작성하기에는 불편함 감이 없잖아 있다. 따라서 C++은 이에 대한 보완책으로 [ using namespace ]라는 기능도 지원한다. 위 코드에서 [ main ]함수 위에 아래 코드를 작성한다.


using namespace A;

 이렇게 되면 [ A ]라는 네임스페이스 영역이 기본이 되고 [ A ]안의 변수들은 굳이 네임스페이스명을 작성하지 않아도 정상적으로 인지해 사용할 수 있다.


namespace A {
	int data;
}

namespace B {
	int data;
}

using namespace A; // A 네임스페이스를 기본으로 지정

int main(void) {
	data = 5;
	B::data = 4;

	return 0;
}

 주의할 것은 [ using namespace ]를 사용하더라도 A와 B를 모두 적용시킬 수는 없다. 그렇게 되어버리면 결국 어디를 이용하는지 의미가 모호해지기 때문에 이름이 같은 네임스페이스 영역의 경우에는 [ using namespace ]는 하나만 선언[각주:5] 해주어야한다.


 참고

>> class도 넓은 의미에서 namespace라고 볼 수 있다.



6. scope ( :: ) 연산자

C에서는 전역변수와 지역변수 이름이 같은 경우에는 전역변수를 사용하기 위해 둘 중 하나의 이름을 변경했어야 했다. 그러나 C++에서는 스코프 연산자를 통해 해결할 수 있다.


int data;

int main(void) {
	int data;

	::data = 5;
	data = 8;

	return 0;
}

 위와 같이 작성하면 스코프 연산자를 앞에 작성해준 [ ::data ]변수는 전역변수를 가리킨다. 따라서 전역변수 [ data ]에는 5가 대입되어있다.


클래스에서도 지역이 우선되기 때문에 다음과 같은 경우에서 클래스 외부의 동명의 함수를 이용할 때는 스코프 연산자를 앞에 작성해준다.


    //student.h

    class Student{
    private:
        int my_age;
    public:
        void SetAge(int age);
        int GetAge();
    };



    //student.cpp

    void Student::SetAge(int age){
        my_age = age;
    }

    int Student::GetAge(){
        // 여기에 SetAge를 추가하면 Student::SetAge가 호출됨.
        // 만일 전역 SetAge를 추가하고싶다면 ::SetAge가 호출됨.

        return my_age;
    }


7. 상속(파생)

C++에서는 비슷한 기능을 하는 클래스를 복사해주는 기능이 존재한다. C언어에서는 비슷한 기능의 클래스를 복사하기 위해서는 개발자가 직접 복사해서 붙여넣기를 해야한다. 이 작업의 문제는 하나에서 문제가 발생하면 복사한 코드들도 모두 일일이 찾아 수정해주어야한다는 점이다. 생산성 하락에 영향을 줄수밖에 없다.


class A{

}

class B : public A{

}

 위 코드처럼 [ A ]클래스와 비슷한 기능을 하는 클래스 [ B ]를 만들기 위해 C++에서는 클래스 이름 뒤에 : [ 접근 제한자 ] [ 클래스 이름 ]을 작성해주었다. 접근 제한자는 [ B ]클래스가 사용될 때, 상속받은 클래스의 내부 데이터를 어떤 식으로 접근하는 방식을 결정하고, 접근 제한자 뒤의 클래스 이름은 [ B ]클래스가 상속받는 클래스를 지칭한다.


접근 제한자에 따른 부모 클래스 접근권한

 접근 제한자 

 public 

 protected 

 private 

 public 

 public 

 protected 

 private[각주:6] 

 protected 

 protected 

 protected 

 private 

 private 

 private 

 private 

 private 


 자식 클래스가 부모 클래스로부터 상속을 받을 때 접근 제한자가 [ protected ]인 경우에는 자식 클래스가 상속을 해줄 때 자식 클래스에게 [ A ]클래스의 자료를 사용할 수 있도록 상속해주지만, [ private ]인 경우에는 자신인 [ B ]클래스만 상속해주고 [ B ]클래스가 상속받은 [ A ]클래스의 자료는 상속해주지 않는다. 유통업계에서 중간업자가 마진을 남기기 위해 산 값에 되파는게 아니라 값을 올려서 파는 것으로 봐도 될듯.



8. 함수의 오버라이딩

동물 '새'를 보자. 새는 날개를 가지고 있고, 그 날개를 이용해 하늘을 난다. 그러나 모든 새가 하늘을 날까? 아니다. 타조와 같은 새는 날개가 있지만 그 날개로 날 수는 없다. 펭귄의 경우에는 날개로 바닷물을 휘저어 수영을 하고 다닌다.


 위 예시처럼, 함수도 상황에 따라 다른 역할을 해야하는 경우가 있다. 이번에 설명할 오버라이딩은 이전 정리에서 다룬 오버로딩과는 비슷하면서도 다르다.

 오버로딩은 같은 기능을 수행함에 있어 여러 자료형을 효과적으로 받아들이기 위해[각주:7] 만들어졌다면, 오버라이딩은 같은 이름의 함수를 상황에 따라서 전혀 다른 기능[각주:8]으로 이용하기 위해 만들어졌다고 보면 된다.


 '새' 클래스를 만들어보자.


class bird {
public:
	void Eat() {
		cout << "작은 포유류, 양서류 등" << endl;
	};
	void Wing() {
		cout << "날기" << endl;
	};
};

 새는 늘 날개로 날기 때문에 저렇게 [ Wing ]함수를 만들어 두었는데, 펭귄은 날지 못해서 저 클래스로 사용할 수가 없다. 그래서 새롭게 [ bird ]를 상속받는 펭귄 클래스를 만들었다. 


class penguin : public bird {
public:
	void Eat() {
		cout << "물고기" << endl;
	}

	void Wing() {
		cout << "수영하기" << endl;
	}
};

 위 코드처럼 쓰게 되면 [ bird ]가 포함하는 멤버 함수와 멤버 변수를 포함하는 펭귄 클래스를 만들 수 있다. 이 때, 펭귄의 Wing 함수는 상속된 bird의 Wing함수를 덮어씌우게 되는데[각주:9] 이 함수를 오버라이딩 된 함수라고 한다.


 오버라이딩은 함수의 원형을 그대로 작성하는 것으로 사용할 수 있다.



Etc.

C++을 사용하는 가장 큰 이유라고 하면 다형성에 있다. 만일 다형성을 모른채로 C++을 사용한다면 1% 사용하는 수준이라고.. 다형성 공부 열심히 하자.


  1. 함수 [본문으로]
  2. ' ! ' 연산자도 논리 NOT인데 비트 NOT을 사용하는 이유는 의미가 더 명확하기 때문이다. 비트 NOT은 1 → 0, 0 → 1 이지만 논리 NOT은 0 = 거짓, 0이 아닌 값 = 참 으로 완전 반대가 아니기 때문이다. [본문으로]
  3. char, int, double 등 [본문으로]
  4. 콜론 두개 [본문으로]
  5. 만일 소수만 겹치게 된다면 겹치는 부분만 A::data; 와 같이 사용해서 효율을 상승시킬 수 있다. [본문으로]
  6. 부모 클래스에서 private로 설정되어있다면 자식 클래스에서 접근하는것은 애초에 안된다. [본문으로]
  7. 음식을 먹을 때 일반적으로 젓가락으로 집어서 먹기도 하지만, 옥수수나 불에구운 마시멜로를 먹을 때는 찔러서 먹는것과 비슷하다고 보면 된다. [본문으로]
  8. 일회용 젓가락을 음식 먹는데 사용하는 것이 아니라 무엇인가를 만들기 위한 재료로 사용하는 것을 오버라이딩으로 보면 되겠다. 같은 이름은데 전혀 다른 행동을 한다. [본문으로]
  9. 완벽하게 덮어 씌운다는 의미가 아니라 기본적으로 펭귄 클래스의 Wing을 사용하기 때문에 bird의 Wing이 호출되지 않는다. 직접 지정해 불러올 수는 있다. [본문으로]
728x90
반응형
Comments