https://www.acmicpc.net/problem/2559

 

많은 깨달음을 주는 문제였다.

수열이 주어졌을 때, K(연속되는 수)의 합 중 가장 큰 값을 리턴해달라는 문제다.

 

N(수열의 개수) : 1 ~ 10만

K는 1과 N 사이의 정수이므로 최대는 10만 정도

 

단순하게 생각하면 10^10만이라 시간 안에 문제를 해결할 수 없다.

어떤 연속된 수의 합을 빠르게 구할 수 있는 방법은 누적합(prefix sum)을 이용하는 것이다.

 

누적합은 0 ~ k 구간의 합을 미리 차곡차곡 더해서 저장해 둔다.

 

문제의 예시를 보면

10 5

3 -2 -4 -9 0 3 7 13 8 -3

5개의 연속된 수열의 최댓값은 31이다.

 

각각의 누적합을 구해보면

1까지의 누적합은 0 + 3

2까지의 누적합은 0 + 3 - 2

3까지의 누적합은 0 + 3 -2 - 4

가만히 보면 3까지의 누적합은 2까지의 누적합 + 입력된 숫자다.

입력을 받을 때 누적합의 배열도 같이 채워나가면 된다.

 

-9 0 3 7 13의 구간 합을 알고 싶다면?

[13까지의 누적합] - [-4까지의 누적합]이다.

[13까지의 누적합] = 3 -2 -4 -9 0 3 7 13

[-4까지의 누적합] = 3 -2 -4

[구하려고 하는 것] = -9 0 3 7 13

 

누적합을 미리 정해놓고 가져다 쓰는 건 랜덤 접근이므로 O(1)

K는 최대 10만

10만 x O(1) => 시간 안에 해결이 가능하다.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int n, k, num, sum, ans = -10000004;
int arr[100005];
int main()
{
    cin >> n >> k;
    for (int i = 1; i <= n; i++)
    {
        cin >> num;
        arr[i] = arr[i - 1] + num;
    }
    
    for (int i = k; i <= n; i++)
    {
        ans = max(ans, arr[i] - arr[i - k]);
    }
    
    cout << ans << '\n';
    return 0;
}

https://www.acmicpc.net/problem/1620

 

지문이 엄청 길지만, 핵심은 마지막에 있다.

포켓몬 이름을 대면 그에 대응되는 숫자를

숫자를 대면 그에 대응되는 이름을 리턴해주면 된다.

 

그런데 경우의 수가 생각보다 크다.

포켓몬 개수는 1 ~ 10만

내가 맞춰야 하는 개수(쿼리의 수)도 1 ~ 10만

 

문자열을 숫자로 바꾸는 것

숫자를 문자열로 바꾸는 것

그런데 생각해 보면 둘 다 문자열로 저장해도 상관없다.

그래서 저장할 때에는 둘 다 문자열로 저장한다.

 

쿼리의 수가 최대 10만이니, 어떤 포켓몬 이름이나 숫자를 물어보더라도 빠르게 찾아서 리턴해줘야 하므로

unordered_map(정렬되지 않은 맵 = 해시맵)을 사용해서 find를 O(1)에 찾을 수 있도록 한다.

 

그런데, ios_base::sync_with_stdio(false)를 통해서 scanf, cin의 동기화를 꺼줘야 아슬아슬하게 시간 초과를 면할 수 있다.

#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;

int n, m;
string s;
unordered_map<string, string> mp;
int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> s;
        mp[s] = to_string(i);
        mp[to_string(i)] = s;
    }
    
    for (int i = 1; i <= m; i++)
    {
        cin >> s;
        cout << mp[s] << '\n';
    }
    return 0;
}

'Algorithm' 카테고리의 다른 글

[백준 2559] 수열  (0) 2024.08.16
[백준 9375] 패션왕 신해빈  (0) 2024.08.16
[백준 9996] 한국이 그리울 땐 서버에 접속하지  (0) 2024.08.16
[백준 11655] ROT 13  (0) 2024.08.15
[백준 11051] 이항 계수 2  (0) 2022.01.11

https://www.acmicpc.net/problem/9375

 

간단하면서도, 생각보다 어려웠던 문제.

 

같은 종류의 옷은 한 번만 사용할 수 있다.

여기서는 종류가 중요하지, 그 옷의 이름은 중요하지 않다.

 

# 모자가 1개라면?

(모자 착용), (모자 미착용) = 2가지

신해빈은 무조건 하나의 옷은 착용해야 하므로 마지막 미착용의 경우의 수는 빼야 한다.

2 - 1 = 1가지.

 

# 모자가 2개라면? 

같은 종류의 옷은 못 입는다고 하였으니,

(모자 A) (모자 B) (모자 미착용) = 3가지

신해빈은 무조건 하나의 옷은 착용해야 하므로 마지막 미착용의 경우의 수는 빼야 한다.

3 - 1 = 2가지.

 

# 모자가 2개이고, 렌즈가 1개라면?.

(모자A, 렌즈 미착용)

(모자 B, 렌즈 미착용)

(모자 미착용, 렌즈 착용)

(모자 A, 렌즈 착용)

(모자 B, 렌즈 착용)

(모자 미착용, 렌즈 미착용)

신해빈은 무조건 하나의 옷은 챙겨야 하기 때문에 아예 입지 않는 경우를 1개 빼야 한다.

6 - 1 = 5가지

 

#include <iostream>
#include <map>
#include <vector>
using namespace std;

int t, n, ans = 1;
string s1, s2;
map<string, int> mp;
int main()
{
    cin >> t;
    while (t--)
    {
        cin >> n;
        mp.clear();
        for (int i = 0; i < n; i++)
        {
            cin >> s1 >> s2;
            mp[s2]++;
        }
        
        ans = 1;
        for (auto item : mp)
        {
            ans *= (item.second + 1);
        }
        
        cout << ans - 1 << '\n';
    }
    return 0;
}

https://www.acmicpc.net/problem/9996

 

접미사(prefix), 접두사(suffix) 개념과 약간의 string 테크닉이 필요한 문제.

 

접미사 + * + 접두사

* 에는 어떠한 문자열이나 공백이 올 수 있다고 한다.

 

여러 개의 테스트 케이스 문자열이 들어온다고 했을 때,

접미사와 접두사가 같은 문자열이라면 중간의 문자열들은 * 로 대체가 되면서

맞으면 DA, 틀리면 NE를 출력하면 된다.

 

다만 주의할 점이, 접두사와 접미사가 같은 문자열인 경우에는 그 크기도 같이 생각해줘야 한다.

aba * aba 라면

들어온 문자열이 aba 일 경우에는 DA 를 뱉게 되는데,

문제에서는 임의의 문자열로 변환해 파일 이름을 같게 만들 수 있어야 한다는 조건이 있으므로

적어도 접두사 + 접미사의 크기가 들어온 문자열의 크기보다는 작아야 한다는 것을 알 수 있다.

 

#include <iostream>
#include <string>
using namespace std;

int N;
string pattern, str, pre, suf;
int main()
{
	cin >> N >> pattern;

	int pos = pattern.find('*');
	pre = pattern.substr(0, pos);
	suf = pattern.substr(pos + 1);
	for (int i = 0; i < N; i++)
	{
		cin >> str;

		if (pre.size() + suf.size() > str.size())
		{
			cout << "NE\n";
		}
		else
		{
			if (pre == str.substr(0, pre.size()) && 
				suf == str.substr(str.size() - suf.size()))
			{
				cout << "DA\n";
			}
			else
			{
				cout << "NE\n";
			}
		}
	}
	return 0;
}

'Algorithm' 카테고리의 다른 글

[백준 1620] 나는야 포켓몬 마스터 이다솜  (0) 2024.08.16
[백준 9375] 패션왕 신해빈  (0) 2024.08.16
[백준 11655] ROT 13  (0) 2024.08.15
[백준 11051] 이항 계수 2  (0) 2022.01.11
[백준 1654] 랜선 자르기  (0) 2021.12.23

https://www.acmicpc.net/problem/11655

 

ROT 13의 규칙은 소문자든 대문자든 알파벳이 오른쪽으로 13칸 이동한 알파벳을 출력하면 된다.

13칸을 이동할 때, a ~ z를 넘어가는 범위가 존재한다면, 다시 a부터 시작할 수 있도록 약간의 모듈러 연산을 추가해서 다른 문자의 아스키코드로 넘어가지 않도록 이를 방지한다.

 

#include <iostream>
#include <string>
#include <cstring>
using namespace std;

string str;
int pos;
int main()
{
	getline(cin, str);

	for (int i = 0; i < str.size(); i++)
	{
		if (isalpha(str[i]))
		{
			if (isupper(str[i]))
			{
				pos = ((str[i] - 'A') + 13) % 26;
				str[i] = pos + 'A';
			}
				
			else
			{
				pos = ((str[i] - 'a') + 13) % 26;
				str[i] = pos + 'a';
			}
		}
	}

	cout << str << '\n';
	return 0;
}

'Algorithm' 카테고리의 다른 글

[백준 9375] 패션왕 신해빈  (0) 2024.08.16
[백준 9996] 한국이 그리울 땐 서버에 접속하지  (0) 2024.08.16
[백준 11051] 이항 계수 2  (0) 2022.01.11
[백준 1654] 랜선 자르기  (0) 2021.12.23
[백준 2231] 분해합  (0) 2021.12.22

클래스 설계에 있어 몇 가지의 설계 지침이 존재한다.

컴파일러가 자동으로 생성하는 멤버 함수들

오늘은 그거에 대해서 정리해 보자.

 

디폴트 생성자

 

매개변수를 전혀 사용하지 않거나, 모든 매개변수가 디폴트 매개변수를 사용하는 생성자다.

사용자가 생성자를 하나도 정의하지 않으면, 사용자를 대신하여 컴파일러는 디폴트 생성자를 정의한다.

디폴트 생성자의 존재는 사용자가 객체를 생성하는 것을 허용한다.

 

모든 기초 클래스를 위한 디폴트 생성자를 호출하는 것과, 다른 클래스의 객체인 멤버들을 호출하는 것도 수행한다.

멤버 초기자 리스트를 사용하여 기초 클래스 생성자를 명시적으로 호출하지 않고 파생 클래스 생성자를 작성하게 되면

컴파일러는 새 객체의 기초 클래스 부분을 생성하기 위해 기초 클래스 디폴트 생성자를 사용하게 된다.

 

사용자가 만약에 생성자를 정의하게 되면, 컴파일러는 디폴트 생성자를 정의하지 않음에 유의하자.

이때에는 디폴트 생성자를 제공하는 것이 프로그래머의 책임으로 된다.

 

생성자를 정의하는 이유는 객체가 항상 바르게 초기화되도록 하기 위한 것도 있다.

특히 클래스가 포인터 멤버를 가지고 있을 경우에는 확실하게 초기화되어야 한다.

모든 클래스 데이터 멤버를 의미 있는 값으로 초기화시키는, 명시적 디폴트 생성자를 제공하는 것이 바람직하다.

 

복사 생성자

 

클래스형의 객체를 매개변수로 사용하는 생성자.

  • 새 객체를 동일한 클래스의 다른 객체로 초기화할 때
  • 객체가 함수에 값으로 전달할 때
  • 함수가 객체를 값으로 리턴할 때
  • 컴파일러가 임시 객체를 생성할 때

프로그램이 복사 생성자를 (명시적 or 암시적) 사용하지 않으면, 컴파일러는 함수 정의가 아니라 원형만 제공한다.

그렇지 않으면, 프로그램은 멤버별 초기화를 수행하는 복사 생성자를 정의한다.

 

멤버별 초기화가 좋지 못한 상황이 존재하는데,

new에 의해 초기화되는 멤버 포인터들은, 일반적으로 깊은 복사를 요구한다.

이럴 때에는 복사 생성자를 직접 정의할 필요가 있다.

대입 연산자

 

디폴트 대입 연산자는 어떤 객체를 동일한 클래스의 다른 객체에 대입하는 것을 처리한다.

대입과 초기화를 혼동하면 안 된다!

구문이 새 객체를 생성한다면 그것은 초기화를 사용하는 것이고,

구문이 기존 객체의 값을 변경한다면, 그것은 대입이다.

Star sirius;
Star alpha = sirius; // 초기화

Star dogstar;
dogstar = sirius; // 대입

디폴트 대입은 멤버 간 대입을 사용한다.

어떤 멤버가 그 자체로 클래스 객체일 경우에는 디폴트 멤버간 대입은 해당 클래스에 정의된 대입 연산자를 사용하게 된다.

 

복사 생성자를 명시적으로 정의할 필요가 있다면, 같은 이유로 대입 연산자도 명시적으로 정의할 필요가 있다.

Star & Star::operator=(const Star&);

컴파일러는 하나의 데이터형을 다른 데이터형에 대입하는 대입 연산자는 생성하지 않는다.

 

클래스 메서드에 관련된 그 밖의 고려 사항

 

# 생성자 관련 사항

생성자들은 새로운 객체를 생성한다는 점에서 클래스의 다른 메서드들과 구별된다.

상속은 파생 클래스 객체가 기초 클래스 메서드를 사용할 수 있다는 것을 의미한다. 그런데 생성자의 경우에 그 객체는 생성자가 자신의 일을 끝낸 후까지 존재하지 않는다.

 

# 파괴자 관련 사항

클래스 생성자에서 new에 의해 대입되는 메모리를 해제하고, 클래스 객체의 파괴에 따르는 특별한 정리 작업을 처리하기 위해서

명시적 파괴자를 반드시 정의해야 한다는 것을 기억하자.

특히 그 클래스를 기초 클래스로 사용할 예정이라면, 가상 파괴자를 제공하자.

 

# 변환 관련 사항

정확히 하나의 매개변수를 사용하여 호출할 수 있는 생성자는, 그 매개변수의 데이터형을 클래스형으로 변환하는 것을 정의한다.

Star(const char *); // const char *를 Star로 변환
Star(const Spectral&, int members = 1); // Spectral을 Star로 변환

Star north;
north = "polaris"

1. Star::Star(const char *)를 사용해서 Star 객체를 생성하고,
2. 생성된 Star 객체를 매개변수로 사용하여 Star::operator=(const Star&) 메서드를 호출하게 된다.
단, 이것은 (char *)를 Star에 대입하는 대입 연산자를 정의하지 않았을 때에만 해당

만약에 explicit 키워드를 사용한다면

public:
	explicit Star(const char *);
    
    
north = "polaris";       // 허용되지 않고
north = Star("polaris"); // 허용된다.

 

클래스 객체를 다른 데이터형으로 변환하려면, 변환 함수를 정의해야 한다.

변환 함수는 매개변수를 사용하지 않고, 변환 결과 데이터형의 이름을 리턴형으로 선언하지 않는 클래스 멤버 함수다.

Star::Star double() const { ... } // Star를 double로 변환
Star::Star const char * () const {...} // Star를 const char *로 변환

변환 함수는 이치에 맞을 경우에만 신중하게 사용하는 것이 좋다. 그렇지 않으면 모호한 코드를 작성할 확률이 높아진다고 한다.

이때 explicit 키워드를 사용한다면 명시적 변환만 허용하기 때문에 조금 더 신중하게 작성될 수 있다.

 

값으로 전달, 참조로 전달

 

객체를 값으로 전달하지 않고 참조로 전달해야 한다. 이는 효율성

객체를 값으로 전달하면 임시 복사본이 생긴다. 복사 생성자를 호출하고, 나중에 파괴자를 호출한다는 것을 의미

큰 객체의 복사는 참조로 전달할 때보다 속도가 많이 느려질 수 있다.

그러므로 함수가 객체를 변경하지 않는다면, 그 함수의 매개변수를 const 참조로 선언해야 한다.

 

가상 함수를 사용하는 상속의 경우에, 기초 클래스 참조 매개변수를 받아들이도록 정의된 함수는,

파생 클래스에 대해서도 성공적으로 사용이 가능하다. (전정한 가상 함수의 힘 - 동적 결합이 실현된다.)

 

객체 리턴과 참조 리턴

 

참조를 리턴하는 것이 더 좋다.

객체를 리턴하게 되면 리턴된 객체의 임시 복사본을 생성하기 때문이다.

그 복사본은 호출한 프로그램에서 사용할 수 있는데, 객체를 리턴하면 복사 생성자를 호출하여 복사본을 생성하는 시간 부담과,

나중에 파괴자를 호출하여 그 복사본을 없애는 시간 부담이 따른다.

참조로 리턴하게 되면, 시간과 메모리가 절약된다.

 

객체를 직접 리턴하는 것은 객체를 값으로 전달하는 것과 비슷하다.

둘 다 임시 복사본을 생성하며, 참조를 리턴하는 것은 객체를 참조로 전달하는 것과 비슷하다.

 

주의할 점은

함수는 그 함수 안에서 생성된 임시 객체에 대한 참조를 리턴하면 안 된다.

함수가 종료되어 그 객체가 파괴되면 더 이상 참조가 유효하지 않기 때문이다.

이러한 코드의 경우 호출한 함수에서 사용할 수 있는 복사본을 생성할 수 있도록 객체를 리턴해야 옳다.

 

함수가 그 함수 안에서 생성된 임시 객체를 리턴한다면, 참조를 사용해서는 안 된다.

Vector Vector::operator+(const Vector& b) const
{
	return Vector(x + b.x, y + b.y);
}

 

참조나 포인터를 통해 전달된 객체를 함수가 리턴한다면, 그 객체를 참조로 리턴해야 한다.

const Stock& Stock::topval(const Stock& s) const
{
    if (s.total_val > total_val)
    	return s;     // 매개변수로 전달된 객체
    else
    	return *this; // 호출한 객체
}

 

const의 사용

 

const를 사용할 기회가 있을 때 신중해질 필요가 있다.

메서드가 매개변수를 변경하지 않는다는 확신이 있을 때, 그것을 사용할 수 있다.

Star::Star(const char * s) { ... } //s가 지시하는 문자열을 변경하지 않는다.

메서드가 그것을 호출한 객체를 변경하지 않는다는 확신이 있을 때, const를 사용할 수 있다.

void Star::show() const {...} // 호출한 객체를 변경하지 않는다.

여기서 사용된 const는 const Star * this를 의미하고, this는 호출한 객체를 지시한다.

일반적으로, 참조를 리턴하는 함수는 대입 구문의 왼쪽에 올 수 있다.

 

객체 안의 데이터를 변경하는 데 참조나 포인터 리턴값을 사용할 수 없도록 하기 위해서 const를 사용할 수 있다.

const Stock & Stock::topval(const Stock& s) const
{
}

함수의 매개변수를 const에 대한 참조나 const를 지시하는 포인터로 선언하면,

매개변수를 변경하지 않는다는 보장이 없는 경우에 그 매개변수를 다른 함수에 전달할 수 없다.

 

public 상속에 관련된 그 밖의 고려 사항들

 

# is - a 관계

파생 클래스가 기초 클래스의 특별한 종류가 아니면 public 파생을 사용하면 안 된다.

어떤 경우에는 순수 가상 함수를 가지고 있는 추상화 기초 클래스를 생성하고, 그것으로부터 다른 클래스를 파생시키는 것이 최선의 방법일 수 있다.

 

명시적 데이터형 변환 없이 기초 클래스 포인터가 파생 클래스 객체를 지시할 수 있고,

기초 클래스 참조가 파생 클래스 객체를 참조할 수 있다는 것이 is-a 관계의 특성이다.

다만 그 역은 성립되지 않는다.

명시적인 데이터형 변환 없이는 파생 클래스 포인터나 참조가 기초 클래스 객체를 참조할 수 없다.

다운캐스팅이 이치에 맞을 수도 있고 아닐 수 도 있다.

 

# 상속되지 않는 것

생성자는 상속되지 않는다.

일반적으로 파생 클래스 생성자는 파생 클래스 객체의 기초 클래스 부분을 생성하기 위해, 

멤버 초기자 리스트 문법을 사용하여 기초 클래스 생성자를 호출한다.

 

파생 클래스 생성자가 멤버 초기자 리스트 문법을 사용하여 기초 클래스 생성자를 명시적으로 호출하지 않는다면

기초 클래스의 디폴트 생성자를 사용하게 된다.

 

파괴자는 상속되지 않는다.

컴파일러는 디폴트 파생 클래스 파괴자를 생성한다.

일반적으로 어떤 클래스가 기초 클래스의 역할을 한다면, 그 파괴자는 가상이어야 한다.

 

대입 연산자는 상속되지 않는다.

상속받은 메서드는 기초 클래스와 파생 클래스에서 동일한 함수 시그내처를 사용한다.

그러나 대입 연산자는 그 클래스형의 형식 매개변수를 가지기 때문에, 각 클래스마다 다른 함수 시그내처를 가진다.

 

# 대입 연산자 관련 사항

한 객체를 동일한 클래스의 다른 객체에 대입하는 것을 컴파일러가 발견하면, 

컴파일러는 그 클래스에 대해 자동으로 대입 연산자를 제공한다. (멤버별 대입, 암시적 디폴트 버전)

 

기초 클래스를 위한 대입 연산자를 명시적으로 제공하지 않으면, 그 연산자가 사용된다.

같은 논리로, 어떤 클래스가 다른 클래스의 객체를 멤버로 가지고 있을 때, 그 멤버의 대입을 위해서 그 멤버 클래스의 대입 연산자가 사용된다.

 

중요. 클래스 생성가 new를 사용하여 포인터를 초기화하고 있다면, 명시적 대입 연산자를 제공해야 한다.

 

파생 클래스 객체를 기초 클래스 객체에 대입한다면?

대입 구문은 왼쪽에 있는 객체에 의해 호출되는 메서드로 번역된다는 사실을 기억하자.

 

역으로, 파생 클래스 참조는 기초 클래스 객체를 자동으로 참조할 수 없기 때문에,

변환 생성자가 준비되어 있지 않다면 코드를 실행되지 않는다.

 

# 가상 메서드

파생 클래스에서 메서드를 다시 정의하려면, 기초 클래스에서 그 메서드를 가상으로 정의하자.

이것은 동적 결합을 가능케 한다.

 

코드를 동적 결합하게 하려면, 객체를 참조나 포인터 형태로 전달해야 한다.

 

# 파괴자 관련 사항

기초 클래스 파괴자는 가상이어야 한다.

객체에 대한 기초 클래스 포인터나 참조를 통해 파생 클래스 객체를 파괴할 때, 프로그램은 기초 클래스 파괴자만 사용하는 것이 아니라,

파생 클래스 파괴자를 먼저 사용하고, 뒤이어 기초 클래스 파괴자를 사용한다.

 

# 프렌드 관련 사항

프렌드 함수는 클래스 멤버가 아니기 때문에 상속되지 않는다.

다만 파생 클래스의 프렌드를 기초 클래스의 프렌드로 사용하기를 원할 수 있는데,

이때에는 데이터형 캐스팅을 실시하고 사용하면 된다.

 

# 기초 클래스 메서드의 사용

  • 파생 클래스 객체는 그 파생 클래스가 메서드를 다시 정의하지 않는다면, 상속된 기초 클래스 메서드를 자동으로 사용한다.
  • 파생 클래스 파괴자는 기초 클래스 파괴자를 자동으로 호출한다.
  • 파생 클래스 생성자는 멤버 초기자 리스트에서 다른 생성자를 지정하지 않는다면 기초 클래스 디폴트 생성자를 자동으로 호출한다.
  • 파생 클래스 생성자는 멤버 초기자 리스트에 지정된 기초 클래스 생성자를 명시적으로 호출한다.
  • 파생 클래스 메서드들은 public, protected 기초 클래스 메서드들을 호출하기 위해 사용 범위 결정 연산자를 사용할 수 있다.
  • 파생 클래스의 프렌드는 파생 클래스 참조나 포인터를 기초 클래스 참조나 포인터로 데이터형을 캐스트 하고 그렇게 변환된 참조나 포인터를 사용하여 기초 클래스의 프렌드를 호출할 수 있다. 

'CS > C++' 카테고리의 다른 글

클래스 상속  (0) 2024.07.27
접두어 방식과 접미어 방식의 차이  (1) 2023.03.05

항상 상속, 상속하지만 정말 어디까지 깊게 가봤을까..?

 

이번에 비록 공부했던 내용을 정리하면서, 조금 더 알아가고 싶어서 시작해 보자.

* 정리 내용은 C++ 기초 플러스를 기반으로 정리하고 있습니다.

 

C++에서는 클래스는 더 높은 수준의 재활용성을 제공합니다.

여기서는 C++는 클래스를 확장시키고, 수정하기 위해서 클래스 상속(class inheritance)을 사용하고 있다.

 

상속이라는 걸 여기에 담고 싶어서 본문의 내용이 엄~청 길어질 수 있다.

다만 여러 차례로 나눠서 하는 것보다 흐름이 끊기지 않게 보는 것이 더 이득이라 판단하여 계속해서 포스팅 예정

상속이 주는 힘이 모길래...

 

  1. 기존 클래스에 기능을 추가할 수 있다.
  2. 클래스가 나타내는 데이터에 다른 것을 더 추가할 수 있다.
  3. 클래스 메서드가 동작하는 방식을 변경할 수 있다.

즉, 상속은 새 기능을 간단히 추가하는 것으로 모든 것? 이 해결된다고 하는데,

제가 느끼기에는 기존 코드를 건드리지 않고, 기존 코드 + 내가 만든 확장성 기능으로 이미 사용된 것에 ++ 하는 느낌으로 개선된다라고 생각됩니다.

=> 세부 구현의 비밀은 유지하면서, 프로그래머들이 그 클래스에 기능을 추가하여 새로운 클래스를 파생시킬 수 있게

(기존 코드를 맘대로 수정했다가, 생각지도 못한 케이스에서 예외나 버그가 발생할 수 있기 때문에)

 

이렇듯 상속은 뛰어난 개념이지만, 기본적인 구현은 단순 

 

가볍게 상속을 들어가 보자.

일반적으로 오리지널 클래스를 기초 클래스(Base class)라고 하고,

이걸 상속받는 클래스를 파생 클래스(Derived class)라고 한다.

 

상속 관련 기본적인 코드를 살펴보자.

#ifndef TABTENN1_H_
#define TABTENN1_H_ 
#include <string>
using std::string;

class TableTennisPlayer
{
private:
	string firstname;
	string lastname;
	bool hasTable;

public:
	TableTennisPlayer(const string& fn = "none", const string& ln = "none", bool ht = false);
	void Name() const;
	bool HasTable() const { return hasTable; }
	void ResetTable(bool v) { hasTable = v; }
};

// 파생 클래스
// 두 클래스는 서로 연계되어 있기 때문에, 클래서 선언들을 함께 묶어두는 것이 더 체계적이라고 한다.
class RatedPlayer : public TableTennisPlayer
{
private:
	unsigned int rating;

public:
	RatedPlayer(unsigned int r = 0, const string& fn = "none", const string& ln = "none", bool ht = false);
	RatedPlayer(unsigned int r, const TableTennisPlayer& tp);
	unsigned int Rating() { return rating; }
	void ResetRating(unsigned int r) { rating = r; }
};

#endif
#include "tabtenn1.h"
#include <iostream>

TableTennisPlayer::TableTennisPlayer(const string& fn, const string& ln, bool ht)
	:firstname(fn), lastname(ln), hasTable(ht) {}

void TableTennisPlayer::Name() const
{
	std::cout << lastname << ", " << firstname;
}

RatedPlayer::RatedPlayer(unsigned int r, const string& fn, const string& ln, bool ht)
	: TableTennisPlayer(fn, ln, ht)
{
	rating = r;
}

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp)
	: TableTennisPlayer(tp), rating(r) {}

파생 클래스 RatedPlayer를 선언하게 되면, 다음과 같은 효과를 얻을 수 있다.

  • 파생 클래스형의 객체 안에는 기초 클래스형의 데이터 멤버들이 저장된다. (상속받았으니까)
  • 파생 클래스형의 객체는 기초 클래스형의 메서드들을 사용할 수 있다.

상속받은 기능에 어떤 것들을 추가해야 할까?

  • 파생 클래스는 자기 자신의 생성자를 필요로 한다.
  • 파생 클래스는 부가적인 데이터 멤버들과 멤버 함수들을 필요한 만큼 추가할 수 있다.

* 생성자

파생클래스는 기초 클래스의 private 멤버에 직접 접근이 불가능하다. (public, protected만 가능)

파생클래스 객체를 생성할 때,

 

기초 클래스의 객체를 생성하게 된다.

RatedPlayer::RatedPlayer(unsigned int r, const string& fn, const string& ln, bool ht)
	: TableTennisPlayer(fn, ln, ht)
{
	rating = r;
}

즉, 파생 클래스 생성자의 몸체 안으로 들어가기 전에, 기초 클래스 객체가 먼저 생성되어야 하기 때문에

멤버 초기자 리스트 문법을 사용해서, 기초 클래스의 생성자에게 매개 변수를 전달한다.

만약 멤버 초기자 리스트를 생략한다면? 기초 클래스 생성자가 생성되는 것은 확실하고 다만 디폴트 기초 클래스 생성자를 사용하게 된다.

 

디폴트 기초 생성자는 프로그래머가 아무것도 정의하지 않을 때에만 컴파일러가 자동적으로 생성해 주는 것이며, 만약에 이를 사용하기를 원치 않으면 직접 생성자를 구현해 주면 된다.

 

* 복사 생성자

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp)
	: TableTennisPlayer(tp), rating(r) {}

TableTennisPlayer(tp)

tp는 const TableTennisPlayer& 형이므로 이 호출은 기초 클래스의 복사 생성자를 호출하게 된다.

마찬가지로 복사 생성자를 정의하지 않았다면 컴파일러가 복사 생성자를 자동으로 생성하게 되는데

이게 좋은 점과 나쁜 점이 있다.

 

보통 기본 복사 생성자는 멤버별 복사가 이뤄지는데 (primitive type)이라면 상관없지만,

생성자 안에서 new를 이용한 동적 메모리 대입이 이뤄진다면? 문제가 된다.

여기서는 동적 메모리 대입이 없기 때문에 암시적 복사 생성자로 충분하다.

 

여기까지 정리하면,

파생 클래스 생성자의 요점은

  • 기초 클래스 객체가 먼저 생성된다.
  • 파생 클래스 생성자는 멤버 초기자 리스트를 통해 기초 클래스 생성자에게 정보를 제공해야 한다.
  • 파생 클래스 생성자에서는 새로 추가된 데이터 멤버들을 초기화해야 한다. (생성자의 의도는 초기화니까)
  • 객체의 생성은 기초 클래스 -> 파생 클래스 순이지만 파괴는 파생 클래스 -> 기초 클래스 순으로 이뤄진다.
파생 클래스와 기초 클래스의 특별한 관계?

기초 클래스 메서드들이 private가 아니라면, 파생 클래스 객체는 기초 클래스의 메서드를 사용할 수 있다.

이건 당연하다.

 

기초 클래스 포인터는 명시적 데이터형 변환 없이도 파생 클래스 객체를 지시할 수 있다.

기초 클래스 참조는 명시적 데이터형 변환 없이도 파생 클래스 객체를 참조할 수 있다.

RatedPlayer rplayer1(1140, "mallory", "Duck", true);
TableTennisPlayer * pt = &rplayer1;
TableTennisPlayer & rt = rplayer1;

pt->Name(); // 포인터
rt.Name();  // 참조

다만, 기초 클래스 포인터나 참조는 기초 클래스 메서드만 호출할 수 있다.

(파생 클래스의 메서드를 사용할 수 없다는 것이다. 처음에는 이게 이해하기 어려웠다.)

 

일반적으로 C++은 참조형과 포인터형이 대입되는 데이터형과 일치할 것을 요구한다.

그러나 상속에서는 이 규칙이 완화 적용되는데

파생 클래스의 참조와 포인터에 기초 클래스의 객체와 주소를 대입할 순 없지만 반대는 된다.

 

기초 클래스 참조와 포인터를 매개변수로 사용하는 함수는 기초 클래스 or 파생 클래스 모두 넘겨줄 수 있다.

즉, 메서드 하나만 이렇게 구현하면 파생된 모든 클래스를 담을 수 있다는 얘기다.

 

참조 호환성은

기초 클래스 객체를 파생 클래스 객체로 초기화도 간접적으로 허용한다.

RatedPlayer olaf1(...);
TableTennisPlayer olfa2(olaf1);

// 암시적 복사 생성자
TableTennisPlayer(const TableTennisPlayer&);

형식 매개변수를 보면 기초 클래스형에 대한 참조로 되어있다. 그래서 파생 클래스가 객체도 담을 수 있는 것이다.

 

비슷한 코드이지만 대입이라는 게 생성자와는 다르다.

RatedPlayer olaf1(...);
TableTennisPlayer winner;
winner = olaf1; // 파생 클래스 객체를 기초 클래스 객체에 대입

// 암시적인 오버로딩 대입 연산자
TableTennisPlayer& operator=(const TableTennisPlayer&) const;

 

암시적인 복사 생성자, 암시적인 오버로딩 대입 연산자.

편하지만, new라는 동적이 들어가면 명시적으로 구현해줘야 하는 게 핵심이다.

 

상속 is-a 관계

is-a 관계,

파생 클래스 객체가 기초 클래스 객체이기도 하다는 것을 뜻한다.

기초 클래스 객체를 대상으로 수행할 수 있는 모든 작업을, 파생 클래스 객체에서도 수행이 가능하다는 것이다.

 

has-a 관계도 존재하는데,

이거는 클래스가 다른 클래스를 가졌다는 의미이지 ~가 ~일 수는 없는 개념이므로 상속과는 다르다.

다만 서로 다른 객체 간의 통신(소통)을 다루기 위해서는 프렌드 함수나 클래스를 만드는 것은 가능하다.

public 다형 상속

호출하는 객체에 따라서 메서드의 행동이 달라질 수 있다는 것이 요점이다.

이렇게 복잡한 행동을 다형(polymorhpic)이라고 한다.

 

public 다형 상속으로는

  • 기초 클래스 메서드를 파생 클래스에서 다시 정의하는 것
  • 가상 메서드를 사용하는 것(virtual)

예시 코드를 살펴보자.

(사실 코드의 내용도 중요하지만, 전체적인 흐름 위주로 공부하는 것이 오히려 더 도움이 되는 듯하다.)

(즉, 외우는 것보다는 전체적인 그림과 왜 이렇게 선언했는지를 중점으로 보자.)

#ifndef BRASS_H_
#define BRASS_H_
#include <string>
class Brass
{
private:
	std::string fullName;
	long acctNum;
	double balance;

public:
	Brass(const std::string& s = "Nullbody", long an = -1, double bal = 0.0);
	void Deposit(double amt);
	virtual void Withdraw(double amt);
	double Balance() const;
	virtual void ViewAcct() const; // 가상 함수(가상 메서드)
	virtual ~Brass() {}
};

// Brass::ViewAcct()
// BrassPlus::ViewAcct()
// 호출한 객체가 어떤 클래스형이냐에 따라 어느 버전을 사용할 것인지를 결정
// Withdraw도 동일

// Deposit(), Balance()
// 두 클래스에 대해 똑같이 행동하는 메서드들은 기초 클래스에 단 한 번만 선언된다.

class BrassPlus : public Brass
{
private:
	double maxLoan;
	double rate;
	double owesBank;

public:
	BrassPlus(const std::string& s = "Nullbody", long an = -1, double bal = 0.0, double m1 = 500, double r = 0.11125);
	BrassPlus(const Brass& ba, double m1 = 500, double r = 0.11125);
	virtual void ViewAcct() const;
	virtual void Withdraw(double amt);
	void ResetMax(double m) { maxLoan = m; }
	void ResetRate(double r) { rate = r; }
	void ResetOwes() { owesBank = 0; }
};
#endif

헤더 파일에서만 살펴보면,

기초는 Brass, 파생은 BrassPlus

파생 클래스에서는 3개의 새로운 데이터 멤버와 멤버 함수를 추가하였다.

 

그런데, 기초/파생 모두 ViewAcct()와 Withdraw() 메서드를 선언하고 있는데

이는 각 객체에 대해 하는 행동이 다른 메서드들이라는 뜻이다.

그래서 virtual 키워드로 가상 메서드로 선언하고 있다.

Brass::ViewAcct() // BrassPlus::ViewAcct() 다른 내용...

프로그램은 호출한 객체가 어떤 클래스형이냐에 따라서 어느 버전을 사용할지 결정하게 된다.

 

Deposit과 Balance 메서드는 한 번만 기초 클래스에서 선언되었는데, 이는 두 클래스에 대해 똑같이 행동하는 메서드들이기 때문이다.

 

또 파괴자에서도 virtual 키워드를 사용하는데,

아무 일도 하지 않지만 가상 파괴자를 선언하고 있다.

#include "brass.h"
#include <iostream>
using std::cout;
using std::endl;
using std::string;

typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setFormat();
void restore(format f, precis p);

Brass::Brass(const string& s, long an, double bal)
{
	fullName = s;
	acctNum = an;
	balance = bal;
}

void Brass::Deposit(double amt)
{
	if (amt < 0)
		cout << "마이너스 입금은 허용되지 않습니다.\n그래서 입금을 취소되었습니다.\n";
	else
		balance += amt;
}

void Brass::Withdraw(double amt)
{
	// ###.## 형식으로 설정
	format initialState = setFormat();
	precis prec = cout.precision(2);

	if (amt < 0)
		cout << "마이너스 인출은 허용되지 않습니다.\n그래서 인출이 취소되었습니다.\n";
	else if (amt <= balance)
		balance -= amt;
	else
		cout << "인출을 요구한 금액 $" << amt
		<< "가 현재 잔액을 초과합니다.\n"
		<< "그래서 인출이 취소되었습니다.\n";

	restore(initialState, prec);
}

double Brass::Balance() const
{
	return balance;
}

void Brass::ViewAcct() const
{
	format initialState = setFormat();
	precis prec = cout.precision(2);
	cout << "고객 이름: " << fullName << endl;
	cout << "계좌 번호: " << acctNum << endl;
	cout << "현재 잔액: $" << balance << endl;
	restore(initialState, prec); // 원래 형식으로 복원
}

BrassPlus::BrassPlus(const string& s, long an, double bal, double m1, double r)
	: Brass(s, an, bal)
{
	maxLoan = m1;
	owesBank = 0.0;
	rate = r;
}

BrassPlus::BrassPlus(const Brass& ba, double m1, double r)
	: Brass(ba) // 암시적 복사 생성자를 사용한다.
{
	maxLoan = m1;
	owesBank = 0.0;
	rate = r;
}

void BrassPlus::ViewAcct() const
{
	format initialState = setFormat();
	precis prec = cout.precision(2);

	Brass::ViewAcct(); // 기초 부분 출력
	cout << "당좌 대월 한도액: $" << maxLoan << endl;
	cout << "상환할 원리금: $" << owesBank << endl;
	cout.precision(3);
	cout << "당좌 대월 이자율: " << 100 * rate << "%\n";
	restore(initialState, prec);
}

void BrassPlus::Withdraw(double amt)
{
	format initialState = setFormat();
	precis prec = cout.precision(2);

	// Balance() 메서드
	// 파생 클래스에서 다시 정의되지 않기 때문에,
	// 사용 범위 결정 연산자(::)를 사용할 필요가 없다.
	double bal = Balance();
	if (amt <= bal)
		Brass::Withdraw(amt);
	else if (amt <= bal + maxLoan - owesBank)
	{
		double advance = amt - bal;
		owesBank += advance * (1.0 + rate);
		cout << "당좌 대월 금액: $" << advance << endl;
		cout << "당좌 대월 이자: $" << advance * rate << endl;
		Deposit(advance);
		Brass::Withdraw(amt);
	}
	else
	{
		cout << "당좌 대월 한도가 초과되어 거래가 취소되었습니다.\n";
	}

	restore(initialState, prec);
}

format setFormat()
{
	return cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
}

void restore(format f, precis p)
{
	cout.setf(f, std::ios_base::floatfield);
	cout.precision(p);
}

 

중점적으로 봐야 하는 건, virtual 키워드를 사용한 녀석인데

 

  • 메서드가 객체에 의해서 호출되지 않고 참조나 포인터에 의해 호출되었을 때에는 어느 메서드를 사용할 것인지를 결정한다.
  • virtual 키워드를 사용하지 않을 경우에는 프로그램은 참조형이나 포인터형에 기초하여 메서드를 선택한다.

ViewAcct()가 가상이 아닐 때에는

참조형 혹은 포인터형에 기초하여 메서드가 선택된다.

# 가상 메서드가 아닌 케이스
Brass dom(...);
Brass dot(...);

Brass& b1 = dom;
Brass& b2 = dot;

b1.ViewAcct(); // Brass::ViewAcct() 호출
b2.ViewAcct(); // Brass::ViewAcct() 호출

참조형이나 포인터형에 기초하여 메서드를 선택하게 된다.

# 가상 메서드인 경우에
Brass dom(...);
BrassPlus dot(...);

Brass& b1 = dom;
Brass& b2 = dot;

b1.ViewAcct(); // Brass::ViewAcct();
b2.ViewAcct(); // BrassPlus::ViewAcct();

둘다 참조 변수 Brass형이지만?
b2는 BrassPlus의 객체를 참조한다.

파생 클래스에서 다시 정의되는 메서드들은, 기초 클래스에서 가상으로 선언하는 것이 일반적인 관행이라고 한다.

 

어떤 메서드가 기초 클래스에서 가상으로 선언되었을 때?

파생 클래스에서 자동으로 가상 메서드가 된다.

그러나 파생 클래스 선언에서도 virtual 키워드를 사용함으로써, 어떤 함수들이 가상인지 표시해 두는 것이 바람직하다.

프로그램이 거대해질수록 이런 사소한 키워드를 놓칠 수 있기 때문에 (휴먼 에러)

 

* 기초 클래스가 가상 파괴자를 선언한다는 것은

파생 클래스 객체가 파괴될 때, 파괴자들이 올바른 순서로 호출되도록 해준다.

 

* 참고로 virtual 키워드는 메서드 정의부가 아니라, 클래서 선언부에 메서드 원형에만 사용하면 된다. 

 

* 다시 한번 리마인드

파생 클래스는 기초 클래스의 private 데이터에 직접적으로 접근하지 못하는데,

파생 클래스 생성자들이 기초 클래스의 private 데이터를 초기화하기 위해서는 멤버 초기자 리스트 문법을 사용하는 것이다.

BrassPlus::BrassPlus(const string& s, long an, double bal, double m1, double r)
	: Brass(s, an, bal)
{
	maxLoan = m1;
	owesBank = 0.0;
	rate = r;
}

BrassPlus::BrassPlus(const Brass& ba, double m1, double r)
	: Brass(ba) // 암시적 복사 생성자를 사용한다.
{
	maxLoan = m1;
	owesBank = 0.0;
	rate = r;
}

 

* 멤버 초기자 리스트

생성자가 아니면, 멤버 초기자 리스트 문법을 사용할 수 없음에 주의하자.

 

* (::) 키워드

사용 범위 결정 연산자

어느 클래스의 어느 걸 사용할 건지를 알려주는 아주 중요한 녀석이다.

예를 들면 기초 클래스에도 있고, 파생 클래스에도 있을 때,

혹은 같은 파일에 정의하고 구현할 때, 등등

 

가상 메서드의 행동

객체에 의한 호출은 가상 함수의 기능을 온전히 사용하지 못한다.

 

Brass와 BrassPlus 객체를 둘 다 지시할 수 있도록 배열을 구성한다면? 이 역시 다형으로 인정된다.

이때, 그 배열에서 아래와 같은 코드를 작성한다면, 가상 메서드의 기능을 발휘할 수 있다.

for (int i = 0; i < CLIMITS; ++i)
{
	p_clients[i]->ViewAcct();
	cout << endl;
}

배열 원소의 포인터가 BrassPlus 객체를 지시하고 있다면 BrassPlus::ViewAcct()가 호출되고,

Brass 객체를 지시하고 있다면 Brass::ViewAcct()가 호출되게 된다.

이것이 다형이다.

 

가상 파괴자는 왜 필요할까

보통 파괴자는 new에 의해 대입된 객체들을 해제하기 위해서 delete를 이쪽에 구현하는데,

파괴자들이 가상이 아니라면, 포인터형에 해당하는 파괴자만 호출될 것이다.

 

BrassPlus 객체를 지시하는 경우에도 포인터형은 Brass이기 때문에, Brass 파괴자만 호출된다.

하지만 파괴자가 가상 파괴자면? 파괴자들이 올바른 순서로 호출되도록 만든다.

 

정적 결합과 동적 결합

소스 코드에 있는 함수 호출을 특정 블록에 있는 함수 코드를 실행하라는 뜻으로 함수 이름을 결합(Binding)이라고 한다.

C언어에서는 각각의 함수가 별개의 함수에 해당하기 때문에, 이 작업이 간단했었다.

 

C++에서는 함수 오버로딩 때문에 이 작업이 조금 복잡하다고 한다.

C or C++ 컴파일러는 결합을 컴파일하는 동안에 수행할 수 있는데,

 

컴파일하는 동안에 일어나는 결합을 정적 결합(Static binding),

가상 함수들은 이 작업을 조금 더 어렵게 만든다.

 

프로그램을 실행할 때, 사용자가 객체를 결정하기 때문에, 컴파일하는 동안에는 그 객체가 어떤 종류인지 알 수 없게 된다.

따라서 어떤 함수를 사용할 것인지 컴파일 시에 결정할 수 없기 때문에

프로그램을 실행할 때 올바른 가상 메서드가 선택되도록 하는 코드를 만들어내야 한다.

이 작업을 동적 결합(dynamic binding)라고 한다.

 

포인터와 참조형의 호환

일반적으로 C++은 한 데이터형의 주소를 다른 데이터형의 포인터에 대입하는 것을 허용하지 않는다.

또한 한 데이터형에 대한 참조가 다른 데이터형을 참조하는 것도 허용하지 않는다.

 

다만 기초 클래스를 지시하는 포인터나 참조는 명시적인 강제 데이터형 변환 없이도

파생 클래스 객체를 참조할 수 있다.

 

파생 클래스 참조나 포인터를 기초 클래스 참조나 포인터로 변환하는 것을 업캐스팅(upcasting)이라고 한다.

public 상속에서는 명시적인 데이터형 변환이 없이도 업캐스팅이 언제든지 가능하다. (is-a관계)

BrassPlus 객체는 Brass객체의 모든 데이터 멤버와 멤버 함수들을 상속받는다는 의미에서 Brass 객체다.

그래서 Brass 객체를 대상으로 할 수 있는 모든 작업을 BrassPlus 객체를 대상으로 할 수 있게 된다.

 

참고로 업캐스팅은 전이된다. 파생 클래스를 끊임없이 만들어도 Brass 포인터는 파생된 모든 객체를 모두 참조할 수 있게 된다.

 

다만,

기초 클래스 포인터나 참조를 파생 클래스 참조나 포인터로 변환하는 것을 다운캐스팅(downcasting)이라 하는데

다운캐스팅은 명시적인 데이터형 변환 없이는 허용되지 않는다.

이는 is-a 관계가 대칭적이지 않기 때문(일방통행)

 

업캐스팅의 힘은 기초 클래스에 대한 참조 또는 포인터를 매개변수로 가지는 함수 호출에서 유용하다.

void fr(Brass& rb);
void fp(Brass* pb);
void fv(Brass b);

Brass b(...);
BrassPlus bp(...);

fr(b);  // Brass::ViewAcct();
fr(bp); // BrassPlus::ViewAcct();
fp(b);  // Brass::ViewAcct();
fp(bp); // BrassPlus::ViewAcct();

fv(b);  // Brass::ViewAcct();
fv(bp); // Brass::ViewAcct();

참조나 포인터에 의해 발생하는 암시적 업캐스팅은 fr(), fp() 메서드가 Brass 객체에 대해서는 Brass::ViewAcct()로

BrassPlus 객체에 대해서는 BrassPlus::ViewAcct()를 사용하게 한다.

 

하지만,

값 전달 방식은 Brass 컴포넌트만 함수로 전달하기 때문에 모두 Brass::ViewAcct()를 호출하게 된다.

 

암시적 업캐스팅은 기초 클래스 포인터나 참조가 기초 클래스 객체나 파생 클래스 객체를 참조하는 것을 가능하게 만든다.

이래서 동적 결합이 필요한 이유이며, 가상 멤버 함수는 이러한 필요성에 대한 C++의 해답으로 생겨놨다.

 

가상 멤버 함수와 동적 결합

컴파일러는 가상이 아닌 함수들에 대해서는 정적 결합을 사용한다.

그러나 기초 클래스에 가상으로 선언한다면?, 프로그램이 실행될 때 결정될 수 있게 변경된다. (동적 결합)

 

대부분의 경우에는 동적 결합이 더 좋다.

특별한 클래스형에 맞게 설계된 메서드를 프로그램이 선택하도록 허용하기 때문이다.

 

그런데, 왜 정적 결합이 있는 걸까?

효율성과 개념 모델이라는 2가지 이유가 있기 때문이다.

 

정적 결합이 더 효율적이기 때문에 C++에서 그것이 디폴트로 설정되어 있다.

(메모리 사용이나 처리 시간 등의) 부담을 떠안지 않는 것이 C++의 기본 철학

가상 함수는 프로그램 설계상 꼭 필요한 경우에만 사용해야 한다.

 

가상이 아닌 함수로 만듦으로써 2가지가 이뤄지는데

1. 효율이 좋아지고

2. 이 함수가 다시 정의되면 안 된다는 의도를 들어냄

기초 클래스에 있는 메서드를 파생 클래스에서 다시 정의할 예정이라면,

그것을 가상 함수로 만들어야 한다는 것이다.

 

가상함수는 어떻게 동작하는가?

C++은 가상 함수들이 어떻게 동작해야 하는지를 규정하고 있다. 이것을 사용해서 구현하는 것은 프로그래머의 몫이다.

다만 가상 함수의 개념을 조금 더 잘 이해하고 사용한다면 더 올바른 사용으로 이뤄진다.

 

컴파일러가 가상 함수를 다루는 일반적인 방법은, 각각의 객체에 숨겨진 멤버를 하나씩 추가하는 것이다.

숨겨진 멤버는 함수의 주소들로 이루어진 배열을 지시하는 포인터를 저장하는 것인데

일반적으로 그 배열을 가상 함수 테이블(virtual function table)이라고 한다.

 

vtbl는 그 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되어 있다.

기초 클래스의 한 객체는 그 클래스를 위한 모든 가상 함수들의 주소로 이루어진 테이블을 지시하는 포인터를 가진다.

파생 클래스의 한 객체는 가상 함수들의 주소로 이루어진 별개의 테이블을 지시하는 포인터를 가진다.

파생 클래스가 가상 함수에 대해 새로운 정의를 제공한다면, vtbl은 새로 정의된 함수의 주소를 저장하게 된다.

 

파생 클래스가 가상 함수를 다시 정의하지 않으면, vtbl은 그 함수의 오리지널 버전의 주소를 저장한다.

파생 클래스가 새로운 함수를 정의하고 그 함수를 가상으로 선언한다면 그 주소가 vtbl에 추가된다.

 

가상 함수를 1개를 정의하든 10개를 정의하든 간에, 주소 멤버는 한 객체에 하나만 추가되며

정의하는 가상 함수의 개수에 따라 테이블의 크기만 변하게 된다.

 

가상 함수를 호출하면, 프로그램은 객체의 vtbl 주소가 저장되어 있다는 것을 알게 되고,

함수 주소들로 이루어진 해당 테이블에 접근하게 된다.

사용하는 함수가 클래스 선언에 정의된 첫 번 째 가상 함수라면, 프로그램은 그 배열에 있는 첫 번째 주소를 사용하고,

그 주소에 있는 함수를 실행시킨다.

 

정리하면, 가상 함수를 사용하면 메모리와 실행 속도 면에서 약간의 부담이 따르게 되는데

  • 각 객체의 크기가 주소 하나를 저장하는 데 필요한 양만큼 커진다.
  • 각각의 클래스에 대해, 컴파일러는 가상 함수들의 주소로 이루어진 하나의 테이블(배열)을 만든다.
  • 각각의 함수 호출에 대해, 실행할 함수의 주소를 얻기 위해 테이블에 접근하는 가외의 단계가 더 필요하다.

가상이 아닌 함수들은 가상 함수보다 조금 더 효율적이지만, 동적 결합을 제공하지 않는다.

class Scientist
{
	...
    char name[40];
    
 public:
 	virtual void show_name();
    virtual void show_all();
    ...
};

class Physicist : public Scientist
{
	...
    char field[40];
public:
	void show_all(); // 다시 정의된 함수
    virtual void show_field(); // 새로 정의된 함수
    ...
};

# Scientist 가상 함수 테이블을 가리키는 테이블 주소 2008
4064(show_name), 6400(show_all)

# Physicist 가상 함수 테이블을 가리키는 테이블 주소 2096
4064(show_name), 6820(show_all, 다시 정의된 함수), 7280(show_field)

Physicist adam(...);
Scientist * psd = &adam;

psc->show_all();

1. psc->vptr의 값을 얻는다.
2. 2096의 테이블로 이동한다.
3. 그 테이블에 있는 두 번째 함수의 주소(6820)을 얻는다.
4. 그 주소(6820)로 가서 거기에 있는 함수를 실행한다.

Scientist 가상 함수 테이블을 지시하는 숨겨진 포인터 멤버 vptr를 가짐

Physict 가상 함수 테이블을 지시하는 숨겨진 포인터 멤버 vptr를 가짐

 

가상 메서드에 대해 알아야 할 사항들

  • 기초 클래스에서 클래스 메서드를 선언할 때, virtual로 시작하면, 그 함수는 기초 클래스, 기초 클래스에 파생된 클래스, 파생 클래스에서 다시 파생된 클래스 등 모든 클래스에 대해 가상이 된다.
  • 객체에 대한 참조를 사용하여 또는 포인터를 사용하여 가상 메서드가 호출되면, 프로그램은 그 참조나 포인터형을 위해 정의된 메서드를 사용하지 않고, 객체형을 위해 정의된 메서드를 사용한다. (동적 결합)
  • 상속을 위해 기초 클래스로 사용할 클래스를 정의할 때, 파생 클래스에서 다시 정의해야 하는 클래스 메서드들은 가상 함수로 선언해야 한다.

# 생성자

생성자는 가상으로 선언할 수 없다.

파생 클래스 객체의 생성은 기초 클래스 생성자가 아니라 파생 클래스 생성자를 호출한다.

그러고 나서 파생 클래스 생성자가 기초 클래스 생성자를 호출한다.

 

# 파괴자

클래스가 기초 클래스로 사용된다면, 파괴자는 가상으로 선언해야 한다.

파생 클래스의 수명이 다했을 때, 파생 클래스의 파괴자가 호출되어 그 메모리를 해제하는 것이 매우 중요하기 때문이다.

만약에 디폴트 정적 결합이 적용된다면, 기초 클래스의 파괴자만 호출될 것이다.

하지만 가상 파괴자라면 파생 클래스의 파괴자도 호출된다. (파생 클래스에서 new를 사용했다면)

 

기초 클래스가 명시적 파괴자의 서비스를 요구하지 않더라도 디폴트 파괴자에 의존하면 안 된다.

아무 일도 하지 않는 가상 파괴자를 반드시 제공해 주자.

어떤 클래스가 가상 파괴자를 가지는 것은 에러가 아닌, 효율성의 문제다.

즉, 파괴자가 필요 없는 기초 클래스라 하더라도 가상 파괴자를 제공해야 한다.

 

# 프렌드

프렌드는 가상 함수가 될 수 없다.

멤버 함수만 가상 함수가 될 수 있는데, 프렌드는 클래스 멤버가 아니기 때문이다.

 

# 가상 함수를 다시 정의하지 않는다면

파생 클래스가 (가상이든 아니든) 함수를 다시 정의하지 않는다면, 파생 클래스는 그 함수의 기초 클래스 버전을 사용한다.

 

# 가상 함수를 다시 정의하면 메서드가 은닉된다.

어떤 함수를 파생 클래스에서 다시 정의하면, 동일한 함수 시그내처를 가지고 있는 기초 클래스 선언만 가리는 것이 아니라, 매개변수 시그내처와는 상관없이 같은 이름을 가진 모든 기초 클래스 메서드들을 가리게 된다.

 

기초 클래스에서도 virtual void print() const;

파생 클래스에서도 virtual void print() const;

기초 메서드
virtual Dwelling& build(int n);

파생 메서드
virtual Hovel& build(int n); // 동일한 함수 시그내처

상속된 메서드를 재정의할 경우에는 오리지널 원형과 정확히 일치시킬 필요가 있다.
이 규칙에 비교적 새로운 예외가 있다면, 리턴형이 기초 클래스에 대한 참조나 포인터인 경우
파생 클래스에 대한 참조나 포인터로 대체될 수 있다는 것이다.

리턴형이 클래스형과 병행하여 바뀌는 것을 허용하기 때문에, 
이 기능을 리턴형의 공변(covariance)이라고 한다.

기초 클래스 선언이 오버로딩되어 있다면, 파생 클래스에서 모든 기초 클래스 버전들을 재정의해야 한다.

// 기초
virtual void showperks(int a) const;
virtual void showperks(double x) const;
virtual void showperks() const;

// 파생
virtual void showperks(int a) const;
virtual void showperks(double x) const;
virtual void showperks() const;

한 가지 버전만 재정의하면, 나머지 두 버전은 가려져 파생 클래스 객체가 그들을 사용할 수 없게 된다.
변경이 필요 없다면, 재정의는 단순히 기초 클래스 버전을 호출할 것

 

접근 제어: protected

가능하다면 protected 접근 제어보다는 private 접근 제어를 사용하여 클래스 데이터 멤버들에 접근해야 한다.

파생 클래스가 기초 클래스 데이터에 접근하게 하려면 기초 클래스 메서드를 사용해야 한다.

 

추상화 기초 클래스 (abstract base class = ABC)

길고 길게 왔다.

사실 지금까지는 복잡도가 낮은 간단한 상속과 다형 상속에 대해서 알았다면, 이제는 복잡도가 더 높아진 추상화 기초 클래스에 대해서 알아보자.

 

상속 관련해서 is-a 규칙을 적용하는 것이 생각처럼 쉽지 않을 때가 있는데,

공통적인 것들을 추출해서 그것들을 추상화 기초 클래스에 넣을 수 있다.

 

다만 추상화 기초 클래스는 필요한 데이터 멤버를 가지고 있지 않아서, 공통 메서드를 구현할 수 없는데

C++는 구현되지 않는 이러한 함수를 위해 순수 가상 함수(pure virtual function)를 제공하고 있다.

 

순수 가상 함수는 함수 선언 뒤에 = 0을 가지게 된다.

클래스 선언에 순수 가상 함수가 들어있다면, 그 클래스는 객체를 생성할 수 없다.

어떤 클래스가 진짜 추상화 기초 클래스가 되려면, 적어도 하나의 순수 가상 함수를 가져야 한다.

C++는 순수 가상 함수가 정의를 가지는 것을 허용한다.

void Move(int nx, int ny) = 0;

ABC는 적어도 하나의 순수 가상 함수를 사용하는 인터페이스를 서술한다.

ABC로부터 파생된 클래스들은 일반 가상 함수를 사용하여, 그 특별한 파생 클래스의 속성을 가지고 그 인터페이스를 구현한다.

 

아래 코드는 추상화에 관한 코드다.

#ifndef ACCTABC_H_
#define ACCTABC_H_
#include <iostream>
#include <string>

// 추상화 기초 클래스
class AcctABC
{
private:
	std::string fullName;
	long acctNum;
	double balance;

protected:
	struct Formatting
	{
		std::ios_base::fmtflags flag;
		std::streamsize pr;
	};
	const std::string& FullName() const { return fullName; }
	long AcctNum() const { return acctNum; }
	Formatting SetFormat() const;
	void Restore(Formatting& f) const;

public:
	AcctABC(const std::string& s = "Nullbody", long an = -1, double bal = 0.0);
	void Deposit(double amt);
	virtual void Withdraw(double amt) = 0; // 순수 가상 함수
	double Balance() const { return balance; }
	virtual void ViewAcct() const = 0; // 순수 가상 함수
	virtual ~AcctABC() {}
};

class Brass : public AcctABC
{
public:
	Brass(const std::string& s = "Nullbody", long an = -1, double bal = 0.0) : AcctABC(s, an, bal) {}
	virtual void Withdraw(double amt);
	virtual void ViewAcct() const;
	virtual ~Brass() {}
};

class BrassPlus : public AcctABC
{
private:
	double maxLoan;
	double rate;
	double owesBank;

public:
	BrassPlus(const std::string& s = "Nullbody", long an = -1, double bal = 0.0, double m1 = 500, double r = 0.10);
	BrassPlus(const Brass& ba, double m1 = 500, double r = 0.10);

	virtual void ViewAcct() const;
	virtual void Withdraw(double amt);
	void ResetMax(double m) { maxLoan = m; }
	void ResetRate(double r) { rate = r; }
	void ResetOwes() { owesBank = 0; }
};
#endif
#include <iostream>
#include <string>
#include "acctabc.h"
using std::cout;
using std::ios_base;
using std::endl;
using std::string;

AcctABC::AcctABC(const string& s, long an, double bal)
{
	fullName = s;
	acctNum = an;
	balance = bal;
}

void AcctABC::Deposit(double amt)
{
	if (amt < 0)
		cout << "마이너스 입금은 허용되지 않습니다.\n그래서 입금이 취소되었습니다.\n";
	else
		balance += amt;
}

void AcctABC::Withdraw(double amt)
{
	balance -= amt;
}

AcctABC::Formatting AcctABC::SetFormat() const
{
	Formatting f;
	f.flag = cout.setf(ios_base::fixed, ios_base::floatfield);
	f.pr = cout.precision(2);
	return f;
}

void AcctABC::Restore(Formatting& f) const
{
	cout.setf(f.flag, ios_base::floatfield);
	cout.precision(f.pr);
}

void Brass::Withdraw(double amt)
{
	if (amt < 0)
		cout << "마이너스 입금은 허용되지 않습니다.\n그래서 인출이 취소되었습니다.\n";
	else if (amt <= Balance())
		AcctABC::Withdraw(amt);
	else
		cout << "인출을 요구한 금액 $" << amt << "가 현재 잔액을 초과합니다.\n그래서 인출이 취소되었습니다.\n";
}

void Brass::ViewAcct() const
{
	Formatting f = SetFormat();
	cout << "Brass 고객: " << FullName() << endl;
	cout << "계좌 번호: " << AcctNum() << endl;
	cout << "현재 잔액: $" << Balance() << endl;
	Restore(f);
}

BrassPlus::BrassPlus(const string& s, long an, double bal, double m1, double r) : AcctABC(s, an, bal)
{
	maxLoan = m1;
	owesBank = 0.0;
	rate = r;
}

BrassPlus::BrassPlus(const Brass& ba, double m1, double r) : AcctABC(ba) // 암시적 복사 생성자를 사용된다.
{
	maxLoan = m1;
	owesBank = 0.0;
	rate = r;
}

void BrassPlus::ViewAcct() const
{
	Formatting f = SetFormat();
	cout << "BrassPlus 고객: " << FullName() << endl;
	cout << "계좌 번호: " << AcctNum() << endl;
	cout << "현재 잔액: $" << Balance() << endl;
	cout << "당좌 대월 한도: $" << maxLoan << endl;
	cout << "상환할 원리금: $" << owesBank << endl;
	cout.precision(3);
	cout << "당좌 대월 이자율: " << 100 * rate << "%\n";
	Restore(f);
}

void BrassPlus::Withdraw(double amt)
{
	Formatting f = SetFormat();

	double bal = Balance();
	if (amt <= bal)
		AcctABC::Withdraw(amt);
	else if (amt <= bal + maxLoan - owesBank)
	{
		double advance = amt - bal;
		owesBank += advance * (1.0 + rate);
		cout << "당좌 대월 금액: $" << advance << endl;
		cout << "당좌 대월 이자: $" << advance * rate << endl;
		Deposit(advance);
		AcctABC::Withdraw(amt);
	}
	else
		cout << "당좌 대월 한도가 초과되어 거래가 취소되었습니다.\n";
	Restore(f);
}

 

추상화 기초 클래스의 사용은,

컴포넌트 설계자가 인터페이스 약정을 만드는 것을 허용한다.

이 인터페이스 약정은 ABC로부터 파생되는 모든 컴포넌트들이 최소한 ABC가 규정하는 공통 기능을 지원한다는 것을 보장한다.

 

상속과 동적 메모리 대입

2가지 경우에 대해서 조금 더 자세하게 보면,

파생 클래스가 new를 사용하지 않는다.
class baseDMA
{
private:
	char * label;
    int rating;
    
public:
    baseDMA(const char * l = "null", int r = 0);
    baseDMA(const baseDMA & rs);
    virtual ~baseDMA();
    baseDMA & operator=(const baseDMA & rs);
};

생성자가 new를 사용할 때,

요구되는 특별한 메서드들은 파괴자 / 복사 생성자 / 오버로딩 대입 연산자를 가지고 있다.

class lacksDMA : public baseDMA
{
private:
	char color[40];
public:
	...
};

lacksDMA 클래스를 위해 명시적 파괴자, 복사 생성자, 대입 연산자를 정의해야 할까? No

lacksDMA 멤버들은 어떤 특별한 동작도 요구하지 않는다고 가정했기 때문에, 디폴트 파괴자만 있으면 충분하다.

 

디폴트 복사 생성자는 멤버별 복사를 하는데, 멤버별 복사는 동적 메모리 대입에서는 사용할 수 없다.

그렇지만 lacksDMA 멤버에 대해서는 멤버별 복사를 사용할 수 있다.

멤버별 복사는 데이터형에 맞게 정의된 복사 형식을 사용한다.

그러나 클래스 멤버 또는 상속받은 클래스 성분을 복사하는 것은, 그 클래스의 복사 생성자를 통하여 이뤄지므로

lacksDMA 클래스의 디폴트 복사 생성자는 lacksDMA 객체의 baseDMA 성분을 복사하기 위해

명시적인 baseDMA 복사 생성자를 사용한다.

lacksDMA 클래스의 복사 생성자는 새로운 lacksDMA 멤버에 대해 사용할 수 있고, 상속받은 baseDMA 객체에 대해서도 사용할 수 있다.

 

대입에 대해서도 동일한 논리가 적용된다.

마찬가지로 디폴트 대입 연산자만 있으면 충분하다.

 

흔히 자주 쓰이는 string 객체는 표준 string 클래스가 동적 메모리 대입을 사용한다.

이것이 문제가 일어나지 않는 이유는 대입 연산자가 string 대입 연산자를 사용하여 객체의 ~ 멤버를 대입하고,

string을 쓰는 클래스는 자동적으로 string 파괴자를 호출하게 되기 때문이다.

 

파생 클래스가 new를 사용한다.
class hasDMA : public baseDMA
{
private:
	char * style; // 생성자에서 new 사용한다면
public:
	...
};

이 경우에서는 파생 클래스를 위한 명시적 파괴자, 복사 생성자, 대입 연산자를 당연히 정의해야 한다.

 

파생 클래스의 파괴자는 기초 클래스 파괴자를 자동으로 호출한다.

hasDMA 파괴자는 style 포인터에 의해 관리되는 메모리를 해제해야 누수가 발생하지 않는다.

그리고 hasDMA 파괴자는 label 포인터에 의해 관리되는 메모리를 해제하기 위해서 baseDMA 파괴자에 의존할 수 있다.

baseDMA::~baseDMA() // baseDMA 관련 처리
{
	delete[] label;
}

hasDMA::~hasDMA()   // hasDMA 관련 처리
{
	delete[] style;
}

 

복사 생성자에 대해서는

strlen() 사용해서 string의 필요한 길이를 알아내고 충분한 메모리를 대입한다.

baseDMA::baseDMA(const baseDMA& rs)
{
    label = new char[std::strlen(rs.label) + 1]
    std::strcpy(label, rs.label);
    rating = rs.rating;
}

hasDMA 복사 생성자만이 hasDMA 데이터에 접근할 수 있어서,

hasDMA 생성자는 그 데이터의 baseDMA 성분을 다루기 위해서는 baseDMA 복사 생성자를 호출해야 한다.

hasDMA::hasDMA(const hasDMA& hs)
	: baseDMA(hs) // 기초 클래스 복사 생성자를 호출한다.
{
	style = new char[std::strlen(hs.style) + 1];
	std::strcpy(style, hs.style);
}

멤버 초기자 리스트가 baseDMA 생성자에 hasDMA 참조를 전달하는 것을 확인할 수 있다.

그런데 hasDMA 참조형을 매개변수로 사용하는 baseDMA 생성자가 없다면??

 

문제없음.

baseDMA 복사 생성자가 baseDMA 참조형의 매개변수를 사용하고 있고,

기초 클래스 참조가 파생형을 참조할 수 있기 때문에

그래서 baseDMA 복사 생성자가 hasDMA 매개변수의 baseDMA 부분을 사용해서 새 객체의 baseDMA 부분을 생성하게 된다.

 

대입 연산자의 고려도 중요한데

대입할 때에는 자기 자신인지 체크하고,

기존에 메모리를 점유하고 있던 부분들은 해제하고 다시 만들어서 복사해야 한다.

baseDMA& baseDMA::operator=(const baseDMA& rs)
{
	if (this == &rs)
		return *this;

	delete[] label;
	label = new char[std::strlen(rs.label) + 1];
	std::strcpy(label, rs.label);
	rating = rs.rating;
	return *this;
}
hasDMA& hasDMA::operator=(const hasDMA& rs)
{
	if (this == &rs)
		return *this;
	// 파생 클래스의 명시적 대입 연산자는
	// 상속받은 baseDMA 기초 클래스 객체를 위한 대입을 처리할 수 있어야 한다.
	baseDMA::operator=(rs); // 기초 클래스 부분을 복사한다.
	delete[] style;
	style = new char[std::strlen(rs.style) + 1];
	std::strcpy(style, rs.style);
	return *this;
}

hasDMA 역시 동적 메모리 대입을 사용하기 때문에, 명시적인 대입 연산자를 구현해야 한다.

하지만, hasDMA 메서드이므로 baseDMA에 접근할 수 없는데

파생 클래스의 명시적 대입 연산자는, 상속받은 baseDMA 기초 클래스 객체를 위한 대입을 처리할 수 있어야 한다.

baseDMA::operator=(rs);

기초 클래스 부분을 복사한다.

 

동적 메모리 대입과 프렌드를 사용하는 부분

파생 클래스들이 기초 클래스의 프렌드에 접근할 수 있는 테크닉

강제 데이터형 변환을 사용해서 원형을 일치시키고 올바른 함수를 선택할 수 있도록 하는 것이다.

baseDMA 프렌드 함수

friend std::ostream& operator<<(std::ostream& os, const baseDMA& rs); // 선언부

std::ostream& operator<<(std::ostream& os, const baseDMA& rs) // 구현부
{
	os << "상표: " << rs.label << std::endl;
	os << "등급: " << rs.rating << std::endl;
	return os;
}


hasDMA 프렌드 함수
friend std::ostream& operator<<(std::ostream& os, const hasDMA& rs); // 선언부

std::ostream& operator<<(std::ostream& os, const hasDMA& rs) // 구현부
{
	// hasDMA 클래스의 프랜드이기 때문에 style 접근 ok
	// baseDMA 클래스에 대해서는 프랜드가 아니기 때문에 접근할 수 없으나
	// 강제 데이터형 변환을 사용해서 원형 일치로 프랜드 함수를 호출해서 접근할 수 있다.
	// operator<<(ostream&, const baseDMA&)
	os << (const baseDMA&)rs;
	os << "스타일: " << rs.style << std::endl;
	return os;
}

 

어느 정도 상속에 대한 개념이 잡힌 것 같다.

꾸준한 반복으로 익숙해져 보자.

'CS > C++' 카테고리의 다른 글

클래스 설계  (0) 2024.07.28
접두어 방식과 접미어 방식의 차이  (1) 2023.03.05

안녕하세요~

 

오랜만에 글을 쓰네요!..

 

최근 들어 회사도 다니고, 여행도 많이 다녔고, 게임도 많이 하고, 자취방도 구하고...

이러저러한 일들이 많았고 또 나태지옥에 빠져서 한동안 포스팅을 안 하게 되었습니다;;

 

주변 사람들은 벌써부터 결혼하고,, 내 주머니는 털털이네요...

생각보다 시간은 정말 빠른 것 같아요.

머 이러저러한 핑계를 대놓지만 지나고 나면,

먼가 한게 없다랄까는 느낌이 많이 듭니다...

 

누군가에게 보이는 포스팅이 아닌 나 자신을 성장시키기로 해놓고,

바쁘다고, 힘들다고 놓았던 제 자신을 반성합니다... 😂

 

올해가 지나면 저도 이제 어엿한 4년 차 개발자가 되어가는데요. ( 겉으로만 4년 차;;;; )

속으로 알찬 3년 차를 올해 2024년까지 채우고 싶어서 다시 포스팅을 할 것 같습니다!!

 

보이는 포스팅도 좋지만, 우선 저의 내실을 다져보고자 퇴근하고 공부한 내용들을 계속해서 올릴 예정입니다.

물론 야근이 많아지면,,, 어쩔 수 없지만 (생업에 지장이 가니깐요..)

 

많은 분들이 제 글에 도움이 되셨으면 더 좋겠고, 

나 자신이 올해에 약간이나마 성장할 수 있다면 좋겠습니다.

 

최근에 GPT 등 여러 OpenAI가 등장하면서 궁금한 내용들을 바로바로 물어보며 피드백을 받을 수 있는데요.

제가 생각하기에는 급할 때는 정말 좋긴 하지만, 사실 엄청난 실력에는 베이스가 중요한 것 같아요. (꼰대;)

 

올해가 다 가기 전에 베이스를 충분히 다지고자 합니다.

 

잘 부탁드립니다.

또 잘 부탁한다 나 자신! 😤

 

'근황토크' 카테고리의 다른 글

드디어 프로그래머스 기초 트레이닝 완료!  (0) 2023.10.09

두뇌 회전 겸 꾸준히 풀다가 결국 다 풀어버렸다. ㅎㅎ

'근황토크' 카테고리의 다른 글

얼마 남지 않는 2024년  (1) 2024.07.20

벌써 추석이네요.

해피 추석!!! 😊

 

추석에 멍하니 있다가 갑자기 블로그가 생각나서 들어와 봤는데요.

그동안 머 열심히 이것저것 많이 썼다는 것을 보니까 옛날 생각도 나고 그렇습니다 하하..

 

회사에 취직한 지 벌써 곧 2년이 다 되어가는데요.

생각해 보니까 이런저런 일들도 많았고, 이슈도 터졌고,, 구현에 어려움도 많이 느껴서 이것저것 많이 찾아보고 공부하고.. 많은 시니어분들께 여러 가지 가르침과 팁 등등 많은 것들을 하면서 지금까지 시간을 보내지 않았나 싶습니다. (물론 게임도 했어요..)

읽으시는 분이 있을진 모르겠지만, 2023년 동안 어떤 것들을 해왔고 느꼈고 보고 있었는지는 모르겠지만 나름 다들 각자의 목표를 향해 달려 나갈 거라 믿습니다.

 

먼가 정서적인 감정이 풍푸한 글 같지만,,

오랜만에 블로그를 보니까 먼가 세월이 그냥 흐르는 것보다는 뭔가를 했다는 것과 깨달음을 얻는 것들 또... 기타 등등 기록하는 게 좋을 듯싶어서 오랜만에 글을 써 내려가 봅니다.

 

2024년이 얼마 남지 않았는데요.

곧 2년 차가 되어가기도 하고,, 회사에 적응했다고는 하지만 머 업무가 항상 쉬운 것도 있는 건 아니어서, 최근에 해킹 관련 이슈도 있었고.. 허영부영 흘러가기보다는 내실을 다듬는 게 좋지 않을까 합니다.

이렇게 보니까 내실은 게임이나 현실이나 둘 다 중요하네요.

얼마 남지 않은 올해를 남은 기간 동안 내실을 다지는 데에 주력해보려고 합니다.

 

항상 다들 건강하시고요.

행복한 하루가 되셨으면 좋겠습니다.

감사합니다. ❤️

 

+ Recent posts