주의 : 이 글은 C의 기초문법에 대해 상세하게 다루지 않습니다.
※ 명품 C++ Programming 의 책을 참고하여 개인적으로 정리한 글입니다.
이 글의 목적은 해당 책의 내용을 인용하여 더 쉽게 이해하고자 정리하고, 더 쉬운 예제를 통해 이해하는 것을 목표로 하고 있습니다.
명품 C++ Programming의 예제문제와 실습문제가 정말 좋으므로, 깊게 공부하고 싶다면 책을 구매하는 것을 추천드립니다.
책의 저작권 등등 각종 권한은 출판사와 지은이/옮긴이에 있습니다.
- 출판사: (주)생능 출판사
- 지음: 황기태
C++ 언어의 설계 목표
1. C++언어의 설계 목표는 첫번째 C언어로 작성된 프로그램과의 호환성을 유지하는 것이다.
C언어의 문법적인 부분을 계승하면서도 C언어의 라이브러리(파이썬의 모듈)을 그대로 사용할 수 있도로 해야한다.
2. 소프트웨어적 재사용을 통해 소프트웨어 생산적을 높히기 위해 객체지향개념을 도입한다.
3. 타입 채크를 엄격하게 하여 실행시간 오류의 가능성을 줄이고 디버깅을 줄인다.
4. 실행시간의 효율성 저하를 최소화 한다.
객체 지향의 단점으로는 멤버함수의 호출이 잦아지고, 이로 인해 실행시간이 저하되는 비효율성이 발생한다. C++에서는 이러한 비효율성을 막기 위해 인라인 함수를 도입하고 있다.
C 언어에서 추가된 기능
1. 함수중복 (function overloading) : 매개 변수의 개수나 타입이 서로 동일한 이름의 함수들을 선언 할 수 있게 한다.
2. 디폴트 매개 변수 (default parameter) : 매개 변수에 값이 전달되지 않는 경우 디폴트 값이 전달되도록 함수를 선언할 수 있게 한다.
3. 참조(reference)와 참조 변수 : 변수에 별명을 붙여서 변수 공간을 같이 사용할 수 있는 참조의 개념을 도입한다.
4. new와 delete 연산자 : 동적 메모리 할당, 해제를 위한 new, delete 연산자를 도입한다
5. 연산자 재정의 (operator overloading) : 기존의 연산자에 새로운 연산을 정의할 수 있다.
6. 제네릭 함수와 클래스 : 함수나 클래스를 데이터 타입에 의존하지 않고 일반화 시킬 수 있게 한다.
객체 지향의 특징
1. 캡슐화 : 클래스를 설계할 때 클래스안에는 다양한 맴버변수와 메소드가 존재하는데, 이에 대한 외부 접근을 어떻게할 것인지 설정이 가능하다.
2. 상속성과 다형성 : 상위 클래스의 내용을 그대로 상속받을 수 있다. 하위 클래스에서는 상위 클래스의 맴버변수와 메소드를 다시 작성할 필요가 없다. 여기서 중요한 개념 중 하나가 method overriding(함수 오버라이딩, 함수 재정의, 다형성의 실현) 이라는 개념으로 상위 클래스의 메소드를 하위메소드에서 재선언 하게되면 재선언된 메소드를 사용할 수 있다.
객체 지향은 왜 도입되었는가?
1. 소프트웨어 생산성 향상
짧은 시간안에 양질의 소프트웨어를 만드는 것이 중요해지고 있다. 이때 객체지향 프로그래밍 기법을 도입하면, 프로그램의 재사용성이 좋아지기 때문에 소프트웨어를 작성하는 부담을 대폭 줄일 수 있다.
2. 실세계에 대한 쉬운 모델링
응용 소프트웨어 시장에서는 하나의 절차로 모델링을 하는 것보다는 관련된 많은 물체(객체)들의 상호작용으로 묘사하는 것이 더 쉽고 적합하다.
객체지향 프로그래밍 (OOP)란 무엇일까요?
프로그램을 개발하는 한 가지 방법론으로 객체라고 하는 프로그램 기본 단위로 구성됩니다. 객체지향의 특징으로는 캡슐화와 상속석과 다형성이 있습니다. 캡슐화는 클래스 설계시 멤버변수와 메소드에 대한 외부 접근을 설정할 수 있다는 것을 의미하고 상속성은 상위 클래스의 내용을 상속받아 하위 클래스에서 재사용 하거나 메서드 오버로딩 또는 오버라딩을 통해서 수정하거는 방식으로 코드의 재사용성이나 유지보수성을 향상시킵니다.
객체 지향의 궁극적인 목표
1. 객체지향이 잘 설계되어 있는 경우 시스템을 이해하기 쉽고, 바꾸기도 쉽고, 재사용하기 쉽다. 다음의 경우처럼 나쁜 설계의 예시를 보자.
- 경직성 : 무엇이든 하나를 바꿀때에는 반드시 다른 것도 바꿔야한다. 이 변화가 끊이지 않는 경우
- 부서지기 쉬움 : 시스템에서 한 부분을 변경하면 전혀상관없는 다른 부분이 작동을 멈춘다.
- 부동성 : 시스템을 여러 모듈로 분해해서 다른 시스템에 재사용할 수 없는 경우
- 끈끈함 : 편집 - 컴파일 - 테스트의 순환이 너무 길다.
- 쓸데없이 복잡함 : 복잡하게 머리를 굴려서 짜는 경우, 나중에 알아보기도 힘들어진다.
- 필요 없는 반복 : 복사, 붙여넣기를 남발한 코드
- 불투명함 : 코드를 만든 의도와 설명이 서로 꼬여있다.
2. 객체지향 언어는 의존관계를 관리하는데 도움이 되는 도구를 제공한다.
의존관계를만들어 의존 관계를 끊거나 의존의 방향을 바꿀 수도 있다. 이 때 인터페이스가 사용된다. 다형성(메소드 오버라이딩) 을 잘 활용하면 어떤 함수를 포함한 모듈에 의존하지 않고 함수를 호출할 수 있다. 객체지향의 의존관계를 어떻게 설계하는 것이 바람직할까?
- 단 하나의 책임 원칙(The Single Responsibility Principle, SRP)
어떤 클래스를 변경해야하는 이유는 오직 하나뿐이여야한다.
하나의 클래스에 A, B, C, D 기능을 가지게 하는 경우 이 클래스는 부서지기 쉬워진다. 그렇기 때문에 A, B, C, D 기능을 각기 다른 클래스로 분리해서 사용해야한다.
클래스 돈{
int 내돈
string 돈의출처
돈이 증가 메소드(돈){
내돈 = 내 돈 + 돈
}
돈이 감소 메소드(돈){
내돈 = 내 돈 - 돈
}
}
이 상황에서 빚을 추가한다면 어떻게 될까?
클래스 돈{
int 내돈
inr 빚
string 돈의출처
돈이 증가 메소드(돈){
내돈 = 내 돈 + 돈
}
돈이 감소 메소드(돈){
내돈 = 내 돈 - 돈
}
빚이 증가 메소드(대출){
빚 = 빚 + 대출
}
빚이 감소 메소드(상환){
빚 = 빚 - 돈
}
}
여기서 돈이라는 클래스는 내돈과 빚을 관리하는 클래스이다. 예시는 정말 간단한 경우이지만 하나 상상을 해보자, 내 돈과 빚은 서로 독립적이다. 그러므로 두 클래스는 분리될 수 있다.
이 상황에서 나의 자산을 출력하게 하는 클래스를 하나더 작성하자.
클래스 돈{
int 내돈
string 돈의출처
생성자 돈{
내돈 = 0
}
돈이 증가 메소드(돈){
내돈 = 내 돈 + 돈
}
돈이 감소 메소드(돈){
내돈 = 내 돈 - 돈
}
내돈 리턴 메소드{
return 내돈
}
}
클래스 빚{
int 내빚
생성자 빚{
내빚 = 0
}
빚이 증가 메소드(대출){
내빚 = 내빚 + 대출
}
빚이 감소 메소드(상환){
내빚 = 내빚 - 돈
}
내빚 리턴 메소드{
return 내빚
}
}
클래스 자산 관리{
int A의돈 = 돈() // instantiated
int A의빚 = 빚() // instantiated
생성자 자산 관리(){
}
내 자산 출력 메소드(){
화면에 출력 (A의돈.내돈 리턴() + A의빚.내빚 리턴() )
}
}
이런느낌으로 만들어질 수 있을 것이다.
- 개방 폐쇄 원칙(The Open - Closed Principle, OCP)
소프트웨어의 클래스, 모듈, 함수 등은 확장에 대해서는 개방되어야하지만 변경에 대해서는 폐쇄되어야한다.
모듈 자체를 변경하지 않고도 그 모듈을 둘러싼 환경을 바꿀 수 있어야한다. 즉 기존의 코드를 변경하지 않고, 기능을 추가하도록 설계해야한다는 것이다. 예를들어
자동차 클래스
string 자동차 종류
자동차 생성자{
}
자동차 경적음 메소드(){
if 자동차종류 == "기아"
"빵~"
elif 자동차종류 == "현대"
"황~"
}
이런 상황에서 자동차 종류가 벤츠인 경우 경적음 메소드에는 새로운 if문을 넣어서 벤츠인지 확인해야하는 과정이 추가된다.
자동차 클래스
string 자동차 종류
자동차 생성자{
}
자동차 경적음 메소드(){
if 자동차종류 == "기아"
"빵~"
elif 자동차종류 == "현대"
"황~"
elif 자동차종류 == "벤츠"
"뿌앙~"
}
이 것이 개방폐쇄원칙을 어긴 경우이다. 개방폐쇄원칙을 지키는 경우 다음처럼 작성할 수 있다.
추상 자동차 클래스{
string 자동차 종류
추상 자동차 생성자{}
추상 자동차 경적음 메소드(){}
}
기아 클래스 상속 자동차{
string 자동차 종류 = "기아"
기아 생성자(){ }
자동차 경적음 메소드(){
출력 "빵~"
}
}
현대 클래스 상속 자동차{
string 자동차 종류 = "현대"
현대 생성자(){ }
자동차 경적음 메소드(){
출력 "황~"
}
}
이 상태라면 현재 작성된 코드를 수정하지 않고, 벤츠를 추가할 수 있다.
추상 자동차 클래스{
string 자동차 종류
추상 자동차 생성자{}
추상 자동차 경적음 메소드(){}
}
기아 클래스 상속 자동차{
string 자동차 종류 = "기아"
기아 생성자(){ }
자동차 경적음 메소드(){
출력 "빵~"
}
}
현대 클래스 상속 자동차{
string 자동차 종류 = "기아"
현대 생성자(){ }
자동차 경적음 메소드(){
출력 "황~"
}
}
벤츠 클래스 상속 자동차{
string 자동차 종류 = "벤츠"
벤츠 생성자(){ }
자동차 경적음 메소드(){
출력 "뿌앙~"
}
}
- 리스코프 치환 원칙(Liskov Substitution Principle, LSP)
서브타입은 언제나 자신의 기반타입으로 교체할 수 있어야한다.
상속을 받는 다면 자식 클래스는 자신의 부모 클래스에서 가능한 행위는 수행이 보장되어야한다. 다음의 예를 보도록하자. LSP는 한 마디로 다형성을 지원하기 위한 원칙이라고 생각하면된다. 즉 부모의 메소드를 자기 맘대로 해석해서 오버라이딩 하면 안된다는 이야기 이다.
책에서 드는 예시는 다음과 같다. 월급을 받지 않는 자원봉사자를 직원 클래스에 포함하게 되면, 월급을 받지 않는 자원봉사자에게 0원으로 찍힌 월급명세서가 날아간다는 것..
내 방식으로 다시 예제를 만들어보면 다음과 같다
클래스 식사{
string 음식
int 음식의 총량
식사 생성자()
음식을 먹는다 메소드(){
음식의 총량 = 음식의 총량 - 1
}
}
음식을 먹는다는 메소드가 있다고 하자. 이를 상속한 김치라는 클래스가 있다.
클래스 김치 상속 식사{
string 음식
int 음식의 총량
식사 생성자()
음식을 먹는다 메소드(){
음식의 총량 = 음식의 총량 + 1
}
}
다음의 경우를 보자 음식을 먹는데, 음식의 총량이 늘어나면 안된다. 이 경우를 리스코프 치환 원칙을 위반했다고 볼 수 있다.
- 의존관계 역전 원칙 (Dependency Inversion Principle, DIP)
고차원의 모듈은 저차원 모듈에 의존하면 안된다. 이 두 모듈 모두 추상화된 것에 의존해야한다.
추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야한다
변화되기 쉬운 것에 의존하지 말라는 원칙입니다. 즉 하위 클래스는 상위 클래스 (상속의 관계말고)에 지나치게 의존하지 말라는 뜻입니다. 내용이 상당히 어려워 진도를 더 나간 후에 상세하게 다루겠습니다.
SOLID. 의존관계 역전 원칙(Dependency inversion principle) (hexabrain.net)
- 인터페이스 격리 원칙
클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다.
거대한 클래스를 만들지 말아야합니다. 사용자에게 필요한 메서드만 제공해야합니다.
참고 문헌
Scott Douglas Meyers(2015). Effective C++. Protec Media(프로텍 미디어)
Robert C. Martin(2021).UML 실전에서는 이것만 쓴다(UML for Java Programmers). 인사이트
황기태(2021). 명품 C++ Programming. (주)생능출판사
'ㅇ 공부#언어 > (C++)기초' 카테고리의 다른 글
(기초 C++) 6장. C++에서의 함수 중복과 static 멤버 (0) | 2023.02.26 |
---|---|
(기초 C++) 5장. C++에서의 함수의 참조와 복사 생성자 (0) | 2023.02.26 |
(기초 C++) 4장. C++에서의 포인터 및 동적 생성 (string 클래스 활용) (0) | 2023.02.26 |
(기초 C++) 3장. C++에서 클래스 사용법 (0) | 2023.02.25 |
(기초 C++) 2장. C와는 다른 C++의 기초 문법 (0) | 2023.02.25 |