C++, Java, C#, Kotlin, Swift, TypeScript 등은 명시적으로 지원하며, 일부 언어는 간접적으로 구현 가능합니다.
🧠 함수 오버로딩을 명시적으로 지원하는 언어들
이들 언어는 컴파일러 수준에서 함수 이름은 같지만 매개변수의 타입이나 개수가 다른 함수를 구분할 수 있도록 설계되어 있습니다:
- C++: 함수 오버로딩의 대표적인 예. 매개변수 타입이나 개수로 구분.
- Java: 클래스 내에서 같은 이름의 메서드를 여러 개 정의 가능.
- C#: 메서드 오버로딩을 통해 다양한 시그니처를 지원.
- Kotlin: 함수 이름은 같되 매개변수로 구분하는 오버로딩 지원.
- Swift: 타입과 개수에 따라 함수 오버로딩 가능.
- TypeScript: 함수 시그니처를 명시적으로 선언하여 오버로딩 구현 가능.
- Ada: 오래된 언어지만 함수 오버로딩을 지원함.
⚙️ 함수 오버로딩을 간접적으로 구현 가능한 언어들
이들 언어는 기본적으로 오버로딩을 지원하지 않지만, 내부 로직으로 유사한 기능을 구현할 수 있습니다:
- Python: 기본적으로 오버로딩을 지원하지 않지만, *args, **kwargs, 데코레이터 등을 활용해 유사 기능 구현 가능.
- JavaScript: 동적 타입 언어로 오버로딩은 없지만, 매개변수 개수나 타입을 조건문으로 처리하여 유사하게 구현 가능.
- PHP: 명시적 오버로딩은 없지만, 기본값 인수나 가변 인수를 통해 유사한 동작 가능.
❌ 함수 오버로딩을 지원하지 않는 언어
- C: 함수 이름이 동일한 경우 컴파일 오류 발생. 함수 오버로딩은 지원하지 않음.
물론이죠! 아래는 함수 오버로딩을 명시적으로 또는 간접적으로 지원하는 여러 언어들의 예시입니다. 각 언어에서 같은 함수 이름을 사용하되 매개변수의 타입이나 개수에 따라 다르게 동작하는 방식입니다.
🧪 C++ 예시
#include <iostream>
using namespace std;
void print(int i) {
cout << "정수: " << i << endl;
}
void print(double d) {
cout << "실수: " << d << endl;
}
void print(string s) {
cout << "문자열: " << s << endl;
}
int main() {
print(10);
print(3.14);
print("Hello");
return 0;
}
☕ Java 예시
public class OverloadExample {
void greet(String name) {
System.out.println("Hello, " + name);
}
void greet(String name, int age) {
System.out.println("Hello, " + name + ". You are " + age + " years old.");
}
public static void main(String[] args) {
OverloadExample ex = new OverloadExample();
ex.greet("Alice");
ex.greet("Bob", 30);
}
}
💻 C# 예시
using System;
class Program {
static void Show(string message) {
Console.WriteLine("Message: " + message);
}
static void Show(int number) {
Console.WriteLine("Number: " + number);
}
static void Main() {
Show("Hello");
Show(42);
}
}
🧬 Kotlin 예시
fun describe(value: Int) {
println("정수: $value")
}
fun describe(value: String) {
println("문자열: $value")
}
fun main() {
describe(100)
describe("Kotlin")
}
🍎 Swift 예시
func greet(name: String) {
print("Hello, \(name)")
}
func greet(name: String, age: Int) {
print("Hello, \(name). You are \(age) years old.")
}
greet(name: "Emma")
greet(name: "Liam", age: 25)
🧠 TypeScript 예시
function greet(name: string): void;
function greet(name: string, age: number): void;
function greet(name: string, age?: number): void {
if (age !== undefined) {
console.log(`Hello, ${name}. You are ${age} years old.`);
} else {
console.log(`Hello, ${name}`);
}
}
greet("Alice");
greet("Bob", 30);
🐍 Python (간접 구현)
def greet(*args):
if len(args) == 1:
print(f"Hello, {args[0]}")
elif len(args) == 2:
print(f"Hello, {args[0]}. You are {args[1]} years old.")
else:
print("Invalid arguments")
greet("Alice")
greet("Bob", 30)
#include <iostream>
#include <string>
class Dog {
private:
std::string name;
int age;
public:
Dog();
~Dog();
std::string getName() const;
void setName(std::string name);
int getAge() const;
void setAge(int age);
};
Dog::Dog() {
age = 0;
name = "none";
}
Dog::~Dog() {
std::cout << "Dog " << name << " is destroyed\n";
}
std::string Dog::getName() const
{
return name;
}
void Dog::setName(std::string name)
{
this->name = name;
}
int Dog::getAge() const
{
return age;
}
void Dog::setAge(int age)
{
this->age = age;
}
int main()
{
Dog coco;
coco.setAge(1);
coco.setName("Coco");
std::cout << coco.getAge() << "\n";
return 0;
}
이 C++ 코드는 Dog라는 클래스를 정의하고, 객체를 생성하여 이름과 나이를 설정한 뒤 출력하는 간단한 예제입니다. 각 부분을 자세히 설명드릴게요.
🐶 클래스 정의: Dog
class Dog {
private:
std::string name;
int age;
public:
Dog(); // 기본 생성자
~Dog(); // 소멸자
std::string getName() const;
void setName(std::string name);
int getAge() const;
void setAge(int age);
};
- 멤버 변수
- name: 강아지의 이름 (문자열)
- age: 강아지의 나이 (정수)
- 생성자 (Dog())
- 객체가 생성될 때 자동으로 호출됨
- name을 "none", age를 0으로 초기화
- 소멸자 (~Dog())
- 객체가 소멸될 때 호출됨
- 콘솔에 "Dog [이름] is destroyed" 메시지 출력
- Getter/Setter
- getName(), getAge(): 현재 이름과 나이를 반환
- setName(string), setAge(int): 이름과 나이를 설정
🧱 생성자 구현
Dog::Dog() {
age = 0;
name = "none";
}
- Dog 객체가 생성될 때 기본값으로 초기화
🧹 소멸자 구현
Dog::~Dog() {
std::cout << "Dog " << name << " is destroyed\n";
}
- 객체가 메모리에서 해제될 때 호출됨
- 디버깅이나 리소스 해제 확인용으로 사용
🧪 메서드 구현
std::string Dog::getName() const { return name; }
void Dog::setName(std::string name) { this->name = name; }
int Dog::getAge() const { return age; }
void Dog::setAge(int age) { this->age = age; }
- const 키워드는 해당 메서드가 멤버 변수를 변경하지 않음을 보장
🧾 main() 함수
int main() {
Dog coco;
coco.setAge(1);
coco.setName("Coco");
std::cout << coco.getAge() << "\n";
return 0;
}
- Dog 객체 coco를 생성
- setAge(1), setName("Coco")로 속성 설정
- getAge()로 나이를 출력 → 1 출력
- 프로그램 종료 시 coco 객체가 소멸되며 소멸자 호출 → "Dog Coco is destroyed" 출력
🧵 실행 결과
1
Dog Coco is destroyed
C++에서 생성자에서 멤버 변수에 초기값을 지정하는 이유
좋은 질문이에요! C++에서 생성자에서 멤버 변수에 초기값을 지정하는 이유는 객체가 생성될 때 안정적이고 예측 가능한 상태로 시작하도록 보장하기 위해서입니다. 아래에 그 이유를 자세히 설명할게요.
🧱 1. 초기화는 객체의 안정성을 보장한다
- 생성된 객체가 의미 있는 값을 가지도록 하기 위해 초기값을 설정합니다.
- 초기화하지 않으면 멤버 변수는 **쓰레기 값(garbage value)**를 가질 수 있어, 예기치 않은 동작이나 버그가 발생할 수 있습니다.
class Dog {
int age; // 초기화하지 않으면 쓰레기 값
public:
Dog() {
age = 0; // 안정적인 초기값
}
};
🧠 2. 초기화 리스트를 사용하면 성능과 명확성이 향상된다
Dog::Dog() : age(0), name("none") {}
- 초기화 리스트는 생성자 본문보다 더 빠르고 효율적입니다.
- 특히 const 멤버나 참조형 멤버는 반드시 초기화 리스트로 초기화해야 합니다.
🧪 3. 클래스의 일관성을 유지한다
- 모든 객체가 생성될 때 동일한 초기 상태를 갖도록 하면, 디버깅과 유지보수가 쉬워집니다.
- 예를 들어, Dog 클래스의 모든 객체가 age = 0, name = "none"으로 시작하면 예측 가능한 동작을 합니다.
🔐 4. 보안과 안정성 측면에서도 중요
- 초기화되지 않은 변수는 보안 취약점을 유발할 수 있습니다.
- 민감한 데이터를 다루는 경우, 초기화는 필수입니다.
✨ 요약
이유 설명
| 안정성 | 쓰레기 값 방지, 예측 가능한 상태 |
| 성능 | 초기화 리스트는 효율적 |
| 일관성 | 모든 객체가 동일한 초기 상태 |
| 보안 | 초기화되지 않은 변수는 위험 |
물론입니다! 아래는 주어진 C++ 코드를 Java로 변환한 예입니다. Java에서는 소멸자(~Dog)가 없고, 대신 가비지 컬렉션이 자동으로 메모리를 관리합니다. System.out.println()을 사용해 콘솔 출력도 처리합니다.
🐾 Java 버전: Dog.java
public class Dog {
private String name;
private int age;
// 생성자
public Dog() {
this.name = "none";
this.age = 0;
}
// 소멸자 대신 finalize() (권장되진 않음, 예시용)
@Override
protected void finalize() throws Throwable {
System.out.println("Dog " + name + " is destroyed");
super.finalize();
}
// Getter
public String getName() {
return name;
}
public int getAge() {
return age;
}
// Setter
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
// main 메서드
public static void main(String[] args) {
Dog coco = new Dog();
coco.setAge(1);
coco.setName("Coco");
System.out.println(coco.getAge());
}
}
🔍 주요 차이점 요약
| 항목 | C++ | Java |
| 소멸자 | ~Dog() | finalize() (사용 권장 X) |
| 출력 | std::cout | System.out.println |
| 문자열 | std::string | String |
| 접근 제어 | private, public | 동일 |
| 객체 생성 | Dog coco; | Dog coco = new Dog(); (Java에서는 new 생략 가능할 때도 있음) |
Java에서는 finalize()는 더 이상 권장되지 않으며, 대신 명시적인 자원 해제가 필요한 경우 try-with-resources나 AutoCloseable을 사용하는 것이 좋습니다. 위 예시는 C++의 소멸자 동작을 흉내내기 위한 참고용입니다.
물론이죠! 아래는 주어진 C++ 코드를 **C#**으로 변환한 예입니다. C#은 Java와 유사한 구조를 가지며, 클래스 정의, 생성자, 속성 설정 등이 직관적으로 표현됩니다.
🐾 C# 버전: Dog.cs
using System;
public class Dog
{
private string name;
private int age;
// 생성자
public Dog()
{
name = "none";
age = 0;
}
// 소멸자 (C#에서는 거의 사용되지 않음, 예시용)
~Dog()
{
Console.WriteLine($"Dog {name} is destroyed");
}
// Getter
public string GetName()
{
return name;
}
public int GetAge()
{
return age;
}
// Setter
public void SetName(string name)
{
this.name = name;
}
public void SetAge(int age)
{
this.age = age;
}
// Main 메서드
public static void Main()
{
Dog coco = new Dog();
coco.SetAge(1);
coco.SetName("Coco");
Console.WriteLine(coco.GetAge());
}
}
🔍 주요 차이점 요약
항목 C++ C#
| 소멸자 | ~Dog() | ~Dog() (거의 사용되지 않음) |
| 출력 | std::cout | Console.WriteLine |
| 문자열 | std::string | string |
| 접근 제어 | private, public | 동일 |
| 객체 생성 | Dog coco; | Dog coco = new Dog(); |
C#에서는 ~Dog() 소멸자는 가비지 컬렉터가 호출할 때 실행되며, 직접 호출하거나 예측하기 어렵기 때문에 일반적으로는 IDisposable 인터페이스와 Dispose() 메서드를 사용하는 방식이 더 선호됩니다.
C++에서 **생성자 함수 중첩(Constructor Overloading)**은 같은 클래스 안에 여러 개의 생성자를 정의하여, 객체를 생성할 때 다양한 방식으로 초기화할 수 있도록 하는 기능입니다. 이는 **함수 오버로딩(Function Overloading)**의 일종으로, 생성자의 매개변수 개수나 타입이 다르면 여러 생성자를 정의할 수 있습니다.
🧱 기본 구조
class MyClass {
private:
int x;
std::string name;
public:
MyClass(); // 기본 생성자
MyClass(int value); // 정수 하나 받는 생성자
MyClass(int value, std::string n); // 정수와 문자열 받는 생성자
};
🧪 예제: 생성자 중첩 사용
#include <iostream>
#include <string>
class Dog {
private:
std::string name;
int age;
public:
Dog() {
name = "none";
age = 0;
}
Dog(std::string name) {
this->name = name;
age = 0;
}
Dog(std::string name, int age) {
this->name = name;
this->age = age;
}
void info() {
std::cout << "Name: " << name << ", Age: " << age << "\n";
}
};
int main() {
Dog d1; // 기본 생성자
Dog d2("Coco"); // 이름만 설정
Dog d3("Max", 3); // 이름과 나이 설정
d1.info(); // Name: none, Age: 0
d2.info(); // Name: Coco, Age: 0
d3.info(); // Name: Max, Age: 3
return 0;
}
🧠 생성자 중첩의 장점
- 유연성: 다양한 방식으로 객체를 초기화할 수 있음
- 가독성: 코드가 명확하고 직관적
- 재사용성: 공통 초기화 로직을 다른 생성자에서 호출 가능
🔁 생성자 위임 (C++11 이상)
C++11부터는 **생성자끼리 호출(delegate)**할 수 있습니다:
class Dog {
private:
std::string name;
int age;
public:
Dog() : Dog("none", 0) {} // 다른 생성자 호출
Dog(std::string name) : Dog(name, 0) {}
Dog(std::string name, int age) {
this->name = name;
this->age = age;
}
};
이렇게 하면 중복 코드를 줄이고, 모든 생성자가 하나의 메인 생성자를 통해 초기화되므로 유지보수가 쉬워집니다.
함수 중첩 - function overloading
의 장점은 오로지 편의성이다.
개발자들이 여러번 함수를 만드는 것보다 자료형만 다르거나 비슷하게 작동하는 함수들을 전부 묶어서 관리하는 것이 가독성도 좋고 개발자들이 덜 신경이 쓰인다. 사실 가독성도 개발자들의 편의 때문이다.
예를 들어
C에서는
int add_i(int numA, int numB) { return numA + numB; }
float add_f(float numA, float numB) { return numA + numB; }
double add_d(double numA, double numB) { return numA + numB; }
다른 형태를 원한다면 다 따로따로 작성을 해야한다.
이런 코드가 1~2줄이면 사용을 할 필요가 없지만 이게 100줄, 1000줄을 코드들이 있는 상황에서 오버로딩을 사용하는 것이 효율적이다.
C++에서는
int add(int numA, int numB) { return numA + numB; }
float add(float numA, float numB) { return numA + numB; }
double add(double numA, double numB) { return numA + numB; }
이렇게 함수 이름을 같게 해서 하는 것이 가독성도 있고 사용하기 편하다.
재사용성, 가독성, 유지보수성 이런 말들은 사실은 다 편의성이다.
개발자들이 100줄을 작성할 것을 50줄로 바꿔주는 것이 재사용성, 가독성이다.
이야기하신 관점, 굉장히 흥미롭고 통찰력 있어요.
실제로 재사용성, 가독성, 유지보수성 같은 소프트웨어 설계 원칙들은 결국 개발자의 편의성과 생산성을 높이기 위한 도구들이죠.
🧠 "재사용성 = 편의성"이라는 관점
- 재사용성은 "같은 기능을 반복해서 만들지 않아도 된다"는 뜻이죠.
- → 즉, 코드를 덜 쓰고, 더 빠르게 개발할 수 있게 해줍니다.
- 가독성은 "코드를 쉽게 읽고 이해할 수 있다"는 뜻인데,
- → 결국 유지보수나 협업이 편해진다는 말이기도 해요.
- 추상화, 캡슐화, 모듈화 같은 개념들도
- → 전부 사람이 코드를 다루기 쉽게 만들기 위한 편의 장치라고 볼 수 있어요.
✨ 예를 들어 보면
// 재사용성 없는 코드
int sum1 = a1 + b1;
int sum2 = a2 + b2;
int sum3 = a3 + b3;
// 재사용성 있는 코드
int sum(int a, int b) { return a + b; }
int sum1 = sum(a1, b1);
int sum2 = sum(a2, b2);
int sum3 = sum(a3, b3);
- 위처럼 함수로 추출하면 줄 수는 비슷해 보여도,의미가 명확해지고, 재사용이 가능해지고, 유지보수가 쉬워집니다.
🧩 결국 핵심은 "사람 중심"
컴퓨터는 100줄이든 1000줄이든 상관없어요.
하지만 사람은 읽고, 이해하고, 고쳐야 하니까
→ 줄 수를 줄이고, 의미를 명확히 하고, 중복을 제거하는 게 중요해지는 거죠.
💬 요약하면
재사용성, 가독성, 유지보수성… 결국 다 "사람이 편하자고 만든 개념"이다.
당신의 말처럼, 개발자가 100줄 쓸 걸 50줄로 줄이는 것,
그게 바로 편의성이고, 그 편의성을 실현하는 도구가 재사용성과 가독성이에요.
혹시 이 주제로 더 깊이 들어가서 "좋은 코드란 무엇인가?" 같은 철학적인 얘기도 해볼까요? 😄
🧠 “좋은 코드란 없다”는 말의 진짜 의미
- 재사용성만 추구하면 → 지나치게 추상화되어 이해하기 어려운 코드가 되고
- 가독성만 추구하면 → 중복이 많아지고 유지보수가 어려워지고
- 유지보수성만 추구하면 → 미래를 너무 의식한 설계로 현재가 복잡해지고…
결국 모든 소프트웨어 품질 속성은 서로 긴장 관계에 있어요.
하나를 극단적으로 추구하면 다른 하나가 희생되기 쉽죠.
⚖️ 그래서 “균형”이 핵심
좋은 코드는 절대적인 기준이 아니라, 상황에 맞는 균형의 산물이에요.
- 팀의 규모, 개발자의 숙련도, 프로젝트의 수명, 배포 주기, 도메인 복잡도… 이런 요소들에 따라 “좋음”의 기준은 계속 바뀝니다.
💬 그래서 “좋은 코드란 없다”는 말은…
사실은 이렇게 바꿔 말할 수도 있어요:
“좋은 코드란, 언제나 맥락 속에서만 존재한다.”
✨ 당신의 말에 덧붙이자면
- 좋은 코드를 쓰는 건 기술이 아니라 태도에 가까워요.
- 치우치지 않으려는 노력, 균형을 감각적으로 잡으려는 감수성, 그게 결국 좋은 개발자를 만드는 거죠.
생성자는 중첩이 가능하지만 소멸자는 불가능하다.
결론부터 말하면: C++에서 생성자는 중첩(오버로딩)이 가능하지만, 소멸자는 중첩이 불가능합니다.
🧱 왜 생성자는 중첩이 가능할까?
- 생성자는 함수처럼 매개변수의 개수나 타입이 다르면 여러 개 정의 가능합니다.
- 이를 통해 다양한 방식으로 객체를 초기화할 수 있어요.
class Dog {
public:
Dog(); // 기본 생성자
Dog(std::string name); // 이름만 받는 생성자
Dog(std::string name, int age); // 이름과 나이 받는 생성자
};
❌ 왜 소멸자는 중첩이 불가능할까?
- C++에서 소멸자는 이름이 고정되어 있고, 매개변수를 가질 수 없습니다.
- 즉, 클래스당 소멸자는 오직 하나만 정의할 수 있어요.
class Dog {
public:
~Dog(); // 매개변수 없음, 오버로딩 불가
};
- 소멸자는 객체가 소멸될 때 자동 호출되므로, 인자를 전달할 수 없고, 오버로딩할 필요도 없습니다.
🔍 요약
| 항목 | 생성자 | 소멸자 |
| 이름 | 클래스 이름과 같음 | ~클래스이름 |
| 오버로딩 | 가능 (매개변수 다르면 OK) | 불가능 (매개변수 없음) |
| 호출 시점 | 객체 생성 시 | 객체 소멸 시 |
| 목적 | 초기화 | 정리/해제 |
🧠 기본 개념
- default parameter는 함수 선언 또는 정의에서 매개변수에 기본값을 지정하는 것
- default argument는 함수 호출 시 인수를 생략하면 자동으로 사용되는 값
즉, 둘은 같은 개념을 다른 관점에서 부르는 말이에요.
✅ 기본 매개변수 예시
#include <iostream>
using namespace std;
void greet(string name = "Guest") {
cout << "Hello, " << name << "!\\n";
}
int main() {
greet(); // 인수 생략 → "Hello, Guest!"
greet("Hawon"); // 인수 제공 → "Hello, Hawon!"
return 0;
}
- greet()는 인수를 생략했지만 "Guest"가 자동으로 들어갑니다.
- greet("Hawon")은 기본값을 무시하고 직접 전달한 값을 사용합니다.
🧱 여러 개의 기본값
void printInfo(string name = "Unknown", int age = 0) {
cout << name << " is " << age << " years old.\\n";
}
printInfo(); // Unknown is 0 years old.
printInfo("Alice"); // Alice is 0 years old.
printInfo("Bob", 25); // Bob is 25 years old.
⚠️ 주의할 점
- 기본값은 오른쪽에서 왼쪽으로만 지정 가능
- void test(int a = 1, int b); // ❌ 오류: 오른쪽에 기본값이 없으면 안 됨 void test(int a, int b = 2); // ✅ OK
- 기본값은 선언부에만 쓰는 것이 일반적
- // 헤더 파일 void greet(string name = "Guest"); // 소스 파일 void greet(string name) { ... } // 여기엔 기본값 쓰지 않음
✨ 요약
| 용어 | 의미 |
| default parameter | 함수 선언 시 매개변수에 기본값을 지정 |
| default argument | 함수 호출 시 생략된 인자에 자동으로 들어가는 값 |
| 장점 | 코드 간결성, 함수 재사용성 증가 |
생성자에 default parameter가 있다면, 일반 생성자를 따로 만들 필요가 없는 경우가 많습니다.
하지만 이건 상황에 따라 다릅니다. 아래에서 예시와 함께 설명드릴게요.
✅ 기본값이 있는 생성자 하나로 충분한 경우
class Dog {
private:
std::string name;
int age;
public:
Dog(std::string name = "none", int age = 0) {
this->name = name;
this->age = age;
}
void info() {
std::cout << name << " is " << age << " years old.\\n";
}
};
int main() {
Dog d1; // 기본값 사용 → "none", 0
Dog d2("Coco"); // age는 기본값 0
Dog d3("Max", 3); // name과 age 모두 지정
d1.info();
d2.info();
d3.info();
}
- 하나의 생성자만으로 기본 생성자, 부분 초기화, 전체 초기화를 모두 처리할 수 있어요.
- 이게 바로 default parameter의 강력한 장점이죠.
⚠️ 하지만 일반 생성자를 따로 만드는 경우도 있음
1. 명확한 의도 표현
Dog() { std::cout << "Default dog created\\n"; }
Dog(std::string name, int age) { ... }
- 기본 생성자에서만 특정 로직을 실행하고 싶을 때
2. 오버로딩 기반의 코드 분리
- 각 생성자마다 다른 초기화 방식이나 검증 로직을 넣고 싶을 때
3. 템플릿이나 상속 구조에서 충돌 방지
- 복잡한 클래스 계층에서는 명시적인 기본 생성자가 필요할 수 있어요
✨ 요약
| 상황 기본값 있는 생성자 | 하나로 충분? |
| 단순한 클래스 | ✅ 가능 |
| 초기화 방식이 다양함 | ❌ 별도 생성자 필요 |
| 생성자마다 다른 로직 필요 | ❌ 별도 생성자 필요 |
| 상속/템플릿 구조 복잡함 | ❌ 명시적 생성자 필요 |
당신 말처럼, 기본값이 있다면 중복된 생성자는 줄일 수 있어요.
하지만 의도, 구조, 확장성에 따라 일부러 나누는 경우도 있다는 점이 핵심이에요.
default parameter, default argument를 잘 이용하면 함수 중첩도 더 줄일 수 있다.
100줄에 함수를 중첩(function overloading)을 이용하면 50줄이 될 수 있고 그 함수 중첩도 default parameter, default argument를 잘만 이용하면 10~30줄이 될 수도 있다.
그만큼 용이하다.
'1학년 대학 수업 > C++ 프로그래밍' 카테고리의 다른 글
| 12주차 예습 과제 (0) | 2025.11.16 |
|---|---|
| 11주차 예습 과제 (0) | 2025.11.08 |
| 10주차 복습 과제 (0) | 2025.11.06 |
| 10주차 예습 과제 (0) | 2025.11.05 |
| 9주차 복습 과제 (0) | 2025.10.30 |