본문 바로가기
대학강의정리/24.1 고급 c++

2주차 9장 객체와 클래스 3,4,5

by 피스타0204 2024. 3. 18.

 

3. 파일 분리

전 시간에서는 클래스를 디자인하고 객체를 만들어 실해하는 법을 배웠습니다. 이번 시간에는 파일 분리, 인라인 함수, 데이터 필드의 캡슐화를 배울 것입니다. 파일 분리 파트가 가장 중요하니 유의 깊게 봐주세요.

1. 클래스 정의(definition)와 구현(implementation) 분리

클래스의 정의 예제 ▼

#include <iostream>
using namespace std;

class Circle  
{
     public:
  	double radius;  //반지름

	Circle() {
    		radius = 1;
 	 }

	Circle(double newRadius){
   		 radius = newRadius;
  	}

	double getArea(){
    	   return radius * radius * 3.14;
  	}

}; // 세미콜론 있어야 함
int main()
{
          Circle c1;
      Circle c2(25);
      Circle c3(125);

      return 0;
 }

 

클래스 안의 멤버 함수는 선언과 구현을 따로 할 수 있습니다. 클래스 정의는 모든 데이터 필드와 생성자 원형(prototype), 함수 원형 목록을 만드는 것이고 클래스 구현은 생성자와 함수를 만드는 것입니다. 함수의 원형 목록을 분리하는 것이 내가 만든 클래스를 다른 사람이 사용하기에 편합니다.

클래스의 정의와 구현을 구분하면 첫째, 구현부분을 숨길 수 있어 보안을 지킬 수 있고 둘째, 정의가 변경되지 않는한 클라이언트(main파일) 파일을 변경할 필요가 없습니다.

 

클래스의 정의와 구현을 구분하는 법을 배워봅시다. 멤버 함수를 class를 선언 할때 안에 같이 선언하고, main함수 밑에 이항범위 지정 연산자(binary scope resolution operator), ::를 통해 구현 부분을 작성합니다. 생성자 함수는 Circle::Circle(){} 이런 식으로 바로 쓰이지만 다른 일반 멤버 함수는 double Circle::getArea()로 앞에 자료형을 적어주어야 합니다.

 

클래스의 정의와 구현을 분리한 코드 ▼

#include <iostream>
using namespace std;

class Circle  
{
public:
  	double radius;  //반지름
	Circle();
	Circle(double newRadius);
	double getArea();

}; // 세미콜론 있어야 함
int main()
{
    Circle c1;
    Circle c2(25);
    Circle c3(125);

      return 0;
 }
Circle::Circle() {
    		radius = 1;
 	 }

Circle::Circle(double newRadius){
   		 radius = newRadius;
  	}

double Circle::getArea(){
    	   return radius * radius * 3.14;
  	}

 

 

2. 파일 분리

파일 분리시 클래스의 정의 부분은 .h파일에 저장하고 구현 부분은 또 따로 .cpp에 저장해야 합니다. 클래스를 만들 때마다 정의, 구현 2개의 파일을 새로 만들어야 합니다. 예를 들어, 프로그램을 만들 때, 클래스를 3개 만들고 싶다면 main함수 파일(client파일) 1개, 클래스 정의 파일 3개, 클래스 구현 파일 3개로 총 7개의 파일을 만들어야 합니다.

 

파일을 나눴다면 이제 파일을 연결해야 합니다. main함수가 들은 src.cpp 파일에 #include "Hello.h"를 적어 Hello.h 헤더파일을 연결해줍니다. 클래스의 정의 파일(인터페이스)과 구현파일도 연결해주어야 합니다. Hello.cpp에 #include "Hello.cpp"로 연결해줍니다.( 기본 제공  클래스는 #include <iostream> 형태로 사용합니다.)

 

여기서 주의할 점은 클래스의 이름과 헤더 파일의 이름, 구현 파일의 이름이 같아야 한다는 겁니다. 그래야 컴파일러가 프로그램을 실행할 때 이것을 보고, main함수 파일의 include "Hello.h"에 Hello.h를 붙여 줄 수 있고 딸려들어온 Hello.cpp가 클래스 Hello 의 구현부임을 알 수 있습니다.

 

구현 파일의 범위 지정 연산자(Hello::)도 반드시 붙여주어야 합니다. 그래야 컴파일러가 구현파일의 생성자와 함수가 Hello 클래스에 정의되어 있음을 알 수 있습니다.

 

실습 예제 ▼

인터페이스 파일

//Circle.h
class Circle {
     public:
  	double radius;  //반지름

	Circle(); 
	Circle(double);
	double getArea();  		
}; // 세미콜론 주의

 

구현 파일

//Circle.cpp
#include "Circle.h" //헤더파일 포함


Circle::Circle() { //범위지정 연산자 ::
    radius = 1;
}

Circle::Circle(double newRadius){
    radius = newRadius;
}

double Circle::getArea(){
    return radius * radius * 3.14;
}

 

클라이언트 파일

//TestCircle.cpp
#include <iostream>
using namespace std;
#include "Circle.h" //헤더파일 포함

int main()
{
           	Circle c1;
       	Circle c2(25);
       	Circle c3(125);
	
       	cout << "c1의 반지름 : " << c1.radius << 
		" 면적: " << c1.getArea() << endl;
       	cout << "c2의 반지름 : " << c2.radius << 
		" 면적: " << c2.getArea() << endl;       
 	cout << "c3의 반지름 : " << c3.radius << 
		" 면적: " << c3.getArea() << endl; 
  	
       	return 0;
 }

 

3. 다중포함 방지

클래스 구현 파일을 작성할 때에도 다른 클래스(A)를 include해야 하는 경우가 있습니다. 하지만 클라이언트 파일에서 구현 파일에서 include한 클래스(A)를 다시 include 하게 되면 다중 포함으로 오류가 발생합니다.

C++에서는 #ifndef, #define, #endif 를 "헤더 파일"에 추가해 다중 포함을 방지합니다.

//Circle.h
#ifndef CIRCLE_H //if not defined 정의 안되어 있으면
#define CIRCLE_H //define 정의하시오
class Circle
{
public:
  double radius;
  
  Circle();
  Circle(double);
  double getArea(); 
}; // Semicolon required
#endif // 정의 되어 있으면 넘겨

 

+) visual c++에서는 #pragma once가 inclusion guard 입니다. 하지만 visual c++외의 환경에서는 동작하지 않으므로 위의 정의로 알아둡시다.

4. 익명 객체

객체를 생성하고 한번만 사용하는 경우 익명 객체를 사용합니다. 이 개념은 포인터에서 한번 더 만나게 될 것이니 알아둡시다.

ClassName()나 ClassName(인수) 형식으로 사용됩니다.

cout << "Area is " << Circle().getArea() << endl ; // 인수 없는 익명객체
cout << " Area is " << Circle(5).getArea() << endl ; // 인수 있는 익명객체

 

4. 인라인 함수

 

1. 인라인 함수란

6장에서 인라인 함수에 대해 배웠습니다. 함수를 부를 때 발생하는 오버헤드를 줄이기 위해 코드가 짧고 여러번 호출하는 함수를 인라인함수로 작성합니다. 컴파일러는 inline 함수를 보면 내용을 사용위치에 그대로 붙여 넣습니다. 이제부터는 클래스에서 인라인 함수를 사용하는 법을 알아보겠습니다.

2. 클래스에서의 인라인 함수

클래스에서의 인라인 함수는 두 가지 방법으로 작성할 수 있습니다. 두 가지 방법 모두 header 파일에 작성되어야 합니다. 첫번째는 자동인라인 함수로 클래스의 정의 부분에 작서합니다. 

//Circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
class Circle {
   public:
      double radius;  //반지름

      Circle(); 
      Circle(double);
      double getPerimeter()//자동인라인함수
      {
          return 2* radius * 3.14159;
      };
      double getArea();	
};
#endif

 

두번째는 헤더 파일 안, 클래스 정의 밑에 작성하는 것입니다.

//Circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
class Circle {
   public:
      double radius;  //반지름

      Circle(); 
      Circle(double);
      double getPerimeter();
      double getArea();	
}; 

//인라인함수, 반드시 헤더파일에 있어야 함
inline double Circle::getArea()
{
   return radius * radius * 3.14159;
}
#endif

 

5. 데이터 필드 캡슐화

 

1. 변수의 범위와 접근 지정자

전역 변수는 모든 함수(main 함수 포함)의 바깥에 선언되고 모든 함수에서 접근이 가능한 변수(heap에 저장)입니다. 자주 사용하는 것을 추천하지 않습니다. 지역 변수는 함수 내부에서 정의된 것으로 그 함수 블록(scope) 안에서만 사용(stack에 저장)할 수 있습니다.

 

6장에서 알아봤던 전역변수와 지역변수 처럼 클래스 안에 선언되는 멤버 변수는 접근 지정자가 private일 때와 public일때 다르게 동작합니다private은 외부에서 접근 불가하고 자기 자신만 접근 가능하며, public은 누구나 접근 가능합니다. 추가로 protected는 자기 자신과 자식 클래스까지 접근 허용됩니다.

int a; //전역변수
class Foo 
{   
   public:
     Foo() 
     {
    	x = 10;
    	y = 10;
     }
     void print() 
     {
    	int x = 20; //지역변수(다른이름 권장) 
    	cout << "x is " << x << endl;
    	cout << "y is " << y << endl;
	cout << "a is " << a << endl;
     }

   private:
   	int x; // 멤버변수
  	int y; // 멤버변수
};
int main()
{
  	Foo foo;
  	foo.print();
  
  	return 0;
}
/*
결과:
	x is 20
  	y is 10
  	a is 0
*/
//안 좋은 프로그래밍 : 같은 이름의 멤버 변수, 지역 변수
// 제일 가까운 x부터 출력된다. int x = 20;

 

2. 데이터 필드의 캡슐화

전체 데이터 필드에 접근 지정자로 public을 지정하면 두가지 단점이 있습니다.

첫번째, (변하면 안되는) 데이터가 임의로 수정될 수 있다.

두번째, 클래스의 유지보수를 어렵게 만들고 버그 발생이 쉽다.

 

그럼 데이터 필드의 캡슐화를 배워봅시다. 클래스에서는 멤버 변수를 private으로 설정함으로써 데이터 필드를 캡슐화합니다. 그리고 변수에 접근하기 위해 변수를 초기화하는 set함수와 초기화한 값을 가져오는 get함수를 public으로 만들어 사용합니다. 캡슐화한 멤버변수는 일반 변수처럼 바로 접근할 수 없습니다. 반드시 set함수나 get 함수를 통해 접근해야 합니다.

//Circle.h
#ifndef CIRCLE_H
#define CIRCLE_H


class Circle {
  public:
     Circle(); 
     Circle(double);
     double getArea(); 
     double getRadius();
     void setRadius(double);
          
  private:
     double radius; 		
}; 

#endif
//Circle.cpp
#include "Circle.h"

Circle::Circle() { 
    	radius = 1;
 }

Circle::Circle(double newRadius){
   	 radius = newRadius;
 }

double Circle::getArea(){
    	return radius * radius * 3.14;
}

double Circle::getRadius(){
    	return radius;
}

void Circle::setRadius(double newRadius){
  radius = (newRadius >=0)?newRadius : 0;
}
//TestCircle.cpp
#include <iostream>
using namespace std;
#include "Circle.h" //헤더파일 포함

int main()
{
           	Circle c1;
       	Circle c2(25);
	
       	cout << "c1의 반지름 : " << c1.getRadius() << 
		" 면적: " << c1.getArea() << endl;
       	cout << "c2의 반지름 : " << c2.getRadius() << 
		" 면적: " << c2.getArea() << endl; 

	c2.setRadius(50);      
 	cout << "c2의 반지름 : " << c2.getRadius() << 
		" 면적: " << c2.getArea() << endl; 	      
 	return 0;
 }

 

 

3. 클래스의 추상화와 캡슐화

클래스의 추상화(class abstraction)은 클래스의 구현과 클래스의 사용을 분리하는 것입니다. 6장의 함수의 추상화와 유사한 개녀으로 클래스의 캡슐화(encapsulation)를 통해 구현합니다.

클래스를 추상화, 캡슐화하여 데이터를 저장한 변수를 숨기고( information hiding ) 사용자에게 기능을 구현하는 함수만 제공하면

 

 

checkpoint)

9.10) 클래스의 정의와 구현을 어떻게 분리하는가?

: .h 확장자 파일과 .cpp확장자 파일로 분리할 수 있습니다.

 

9.11) 다음 코드의 출력은 무엇인가? (Circle.h와 circle.cpp에 정의된 Circle클래스를 사용한다.)

//a

int main()

{

Circle c1();

Circle c2(6);

c1=c2;

cout << c1.getArea() <<  endl;

return 0;

}

식에 클래스 형식이 있어야 한다는 오류가 뜹니다. default 생성자는 괄호가 있으면 안되기 때문에 Circle c1;로 바꾸어  실행하면 아래와 같은 결과가 나옵니다.

코드수정후 출력결과

//b
#include <iostream>
#include "Circle.h"
using namespace std;
Circle::Circle() {
	radius = 1;
}
Circle::Circle(double newRadius) {
	radius = newRadius;
}
double Circle::getArea() {
	return radius * radius * 3.14159;
}
int main()

{

	cout << Circle(8).getArea() << endl;

	return 0;

}

 

출력

 

9.12) 다중포함 오류가 발생하는 이유는 무엇인가? 헤더파일이 여러 번 포함되는 것을 방지하는 방법은 무엇인가?

: 헤더파일이 여러 번 포함되어 발생한다. 방지하기 위해 ifndef 지시자, endif 지시자 안에 define 지시자를 넣어 클래스를 정의한다

9.13) #define 지시사는 어떤 경우에 사용하는가?

: 상수나 문장에 대해 매크로명을 정의해주는 작업을 해준다. 매크로이름을 매크로몸체로 치환해준다.

 

9.15) 다음 코드에서 잘못된 부분은 무엇인가?(9.9의 코드를 사용한다.)

Circle c;

cout << c.radius <<endl;

:private으로 선언되어 있는 데이터필드를 직접 접근하려고 하기 때문에 동작하지 않는다.

Circle c;

cout << c.getRadius()<<endl;

로 수정해야 한다.

 

9.16) 접근자 함수란 무엇인가? 변경자 함수란 무엇인가? 이들 함수에 대한 이름 명명 규칙은 무엇인가?

일상적인 표현으로 get함수는 접근자(accessor)라 하고 set 함수는 변경자(mutator)이라고 한다.

get함수는 다음과 같은 형식을 사용한다.

returnType getPropertyName()

get함수를 bool형식으로 정의할 때는 관례상으로

bool isPropertyName()

 

set함수는 다음과 같은 형식을 사용한다.

vooid setPropertyName(dataType propertyValue)

 

9.17) 데이터필드 캡슐화의 장점은 무엇인가?

변수를 public으로 설정하는 것보다 유지보수가 쉽고 버그발생률이 낮다. 또, 데이터가 임의로 수정되지 않게 할 수 있다.

 

9.18) 클래스 안에서 데이터 필드와 함수는 어떤 방식으로 배치될 수 있는가?

클래스 안에 있기만 하면 순서는 상관없다. 물론 문법은 맞추어 작성해야 한다.