static 키워드는 프로그램이 종료될 때 까지 할당 해제되지 않고 고정된 영역의 메모리의 공간을 할당할 때 사용한다. 누적 점수, 음량 등의 설정 값에 쓰일 수 있다. 그러나 프로그램 종료 시 까지 유지되므로 불필요한 메모리를 쓰지 않게 조절할 필요가 있다.
최초 호출 시점에 데이터 영역에 할당되며, 선언 시 프로그램 전역에서 호출 가능하다. (전역적인 접근이 가능하다)
그러나 메서드, 클래스는 표현 상 static이라고 하며 데이터 영역에 저장은 아니다. 정적 메서드는 코드영역, 정적 클래스는 정적 클래스 중 정적 필드는 데이터 영역, 정적 메서드는 코드 영역에 배치된다.
internal class Program
{
static void Main(string[] args)
{
Monster mon1 = new Monster();
Monster mon2 = new Monster();
mon1.TakeDamage(10);
mon2.TakeDamage(10);
}
}
class Monster
{
public static int hp = 100;
public void TakeDamage(int damage)
{
Console.WriteLine("몬스터가 데미지를 받았습니다.");
hp -= damage;
Console.WriteLine("몬스터의 체력은 {0}", hp);
}
}
// 출력 결과:
// 몬스터의 체력은 90
// 몬스터의 체력은 80
또한 static 변수는 인스턴스와 다르게 모두가 공유하기 때문에 각각의 설정을 필요로 할 때는 쓰면 안된다.
static 함수는 전역적으로 사용이 가능한 함수이다.
class Player
{
public void Swap(ref int left, ref int right) //
{
int temp = left;
left = right;
right = temp;
}
public void InventorySwap(int left, int right)
{
Swap(ref left, ref right);
}
}
class Monster
{
public void WeaponSwap(int left, int right)
{
}
}
위와 같은 상황에서는 Swap 함수를 여러 곳에서 사용할 수 있게 하면 유용하다. 그러나 Swap 기능을 쓰고 싶을 때마다 각 클래스에서 만들 면 귀찮을 뿐더러 해당 클래스의 책임 범위에서 벗어날 수도 있다.
class Player
{
public void InventorySwap(int left, int right)
{
Util.Swap(ref left, ref right);
}
}
class Monster
{
public void WeaponSwap(int left, int right)
{
Util.Swap(ref left, ref right);
}
}
class Util
{
public static void Swap(ref int left, ref int right)
{
int temp = left;
left = right;
right = temp;
}
}
따라서 위와 같이 만들면 다른 곳에서도 전역적으로 사용이 가능하다.
class Monster
{
public int hp;
public static void TakeDamage(int damage)
{
hp -= damage; // 멤버 변수를 정적 메서드에서 접근할 수 없음
}
}
그러나 위와 같은 상황에서는 다른 클래스에서도 monster의 체력을 깎을 수 있기에 전역 함수를 만드는 것은 좋지 않다.
또한 정적 메서드는 해당 클래스를 시작하지 않고도 호출할 수 있기에 멤버 변수에 접근할 수 없다.
모든 멤버변수와 멤버메서드가 static으로 선언된 클래스이다.
정적 클래스는 인스턴스를 만들 수 없고 상속도 안된다.
Console console = new Console();
Console.WriteLine();
Console 또한 정적 클래스의 일부로, 위와 같은 사용이 불가하다.
어차피 전역에서 사용할 수 있으니 메모리 낭비되게 인스턴스로 만들 수 없고 그럴 필요도 없다.
상속이란 객체지향 프로그래밍의 특징 중 하나로 코드의 중복을 줄이고 유지 보수성을 개선할 수 있는 기능이다. (부모클래스의 모든 기능을 가지는 자식클래스를 설계하는 방법)
상속을 사용하게 되면 부모 클래스에서 파생된 코드를 활용하여 중복되는 부분을 줄이면서 작업할 수 있다.
internal class Program
{
static void Main(string[] args)
{
Slime slime = new Slime();
Dragon dragon = new Dragon();
slime.Move();
dragon.Move();
}
}
class Monster
{
public string name;
public int hp;
public void Move()
{
Console.WriteLine("{0}이/가 움직입니다.");
}
}
class Slime : Monster
{
}
class Dragon : Monster
{
}
위와 같이 자식 클래스 오른쪽에 콜론 부모 클래스를 쓰면 부모의 모든 기능을 사용할 수 있다. 이러한 경우 부모 클래스에 필요한 기능을 추가하는 등 통합적인 관리가 가능하다.
private: 해당 코드 블록(클래스)에서만 접근할 수 있다.
public: 다른 클래스에서도 접근할 수 있다.
protected: 자식 클래스(상속해주는 클래스)까지만 접근할 수 있다.
sealed: 해당 클래스를 상속받을 수 없게 설정하는 것이다.
this는 자기 자신을 뜻하고 base는 부모 클래스를 뜻한다.
또한 부모 클래스와 자식 클래스에서 동일한 변수명을 선언할 수 있다.
class Dragon : Monster
{
public float speed;
public Dragon()
{
name = "드래곤";
hp = 100;
base.speed = 10f;
this.speed = 5.5f;
}
}
부모 클래스에 public float speed; 가 있다고 가정할 때, base.speed 는 부모 클래스의 변수를 뜻하고, this.speed 는 자식 클래스(본인)을 뜻한다.
Slime slime = new Slime();
Dragon dragon = new Dragon();
---
Monster slime = new Slime();
Monster dragon = new Dragon();
업캐스팅: 자식클래스는 부모클래스 자료형으로 암시적 형변환이 가능하다.
internal class Program
{
static void Main(string[] args)
{
Slime slime = new Slime();
Dragon dragon = new Dragon();
Player player = new Player();
player.Attack(slime); // 괄호 안의 내용은 Monster - 부모 클래스가 들어갈 자리인데, 자식클래스가 들어갈 수 있어 유연하게 사용가능하다.
player.Attack(dragon);
}
}
class Monster
{
public string name;
public int hp;
public float speed;
public void TakeDamage(int attackPoint)
{
hp -= attackPoint;
}
}
class Slime : Monster
{
}
class Dragon : Monster
{
public float speed;
public Dragon()
{
name = "드래곤";
hp = 100;
base.speed = 10f;
this.speed = 5.5f;
}
}
class Player
{
public int attackPoint;
public void Attack(Monster monster)
{
monster.TakeDamage(attackPoint);
}
}
부모클래스가 자식클래스를 포함하는 상위개념일 경우 상속관계가 적합하다.
internal class Program
{
static void Main(string[] args)
{
Truck truck = new Truck();
}
}
class Car
{
public Car()
{
Console.WriteLine("부모 생성자");
}
}
class Truck : Car
{
public Truck()
{
Console.WriteLine("자식 생성자");
}
}
// 출력:
// 부모 생성자
// 자식 생성자
상속의 경우 자식 클래스를 호출할 때, 부모 클래스도 먼저 실행한 후 자식 클래스가 실행된다. 부모가 준비돼야 자식이 초기화될 수 있기 때문이다. 메모리 구조상으로도 자식 클래스가 호출되면 부모 클래스 밑에 자식 클래스를 붙여 호출된다.
이름이 같은 메서드의 매개변수를 달리하여 동일한 이름의 메서드를 구성할 수 있는 방법이다. 사용될 메서드는 매개변수로 넣는 유형에 따라 결정된다.
internal class Program
{
static void Main(string[] args)
{
Player player = new Player();
player.Test(10);
player.Test(2.5f);
}
}
class Player
{
public void Test(float value)
{
}
public void Test(int value)
{
}
}
Console.WriteLine(); 또한 18개의 케이스로 오버로딩되어있다.
괄호 안에 int 넣으면 int 용 Console.WriteLine(); 을 사용하는 등 그에 대응하는 것을 사용한다.
internal class Program
{
static void Main(string[] args)
{
Animal dog1 = new Dog();
Dog dog2 = new Dog();
dog1.Run();
dog2.Run();
Animal cat1 = new Cat();
Cat cat2 = new Cat();
cat1.Run();
cat2.Run();
}
}
public class Animal
{
public void Run()
{
Console.WriteLine("동물이 달려요");
}
}
public class Dog : Animal
{
public void Run()
{
Console.WriteLine("강아지가 달려요");
}
}
public class Cat : Animal
{
public void Run()
{
Console.WriteLine("고양이가 달려요");
}
}
// 출력:
// 동물이 달려요
// 강아지가 달려요
// 동물이 달려요
// 고양이가 달려요
위와 같이 자식 클래스에서 함수를 재정의해도 부모클래스를 통해 선언된 인스턴스는 의도대로 출력되지 않는다.
static void Main(string[] args)
{
Animal dog1 = new Dog();
Dog dog2 = new Dog();
dog1.Run();
dog2.Run();
Animal cat1 = new Cat();
Cat cat2 = new Cat();
cat1.Run();
cat2.Run();
}
}
public class Animal
{
public virtual void Run() // virtual 추가
{
Console.WriteLine("동물이 달려요");
}
}
public class Dog : Animal
{
public override void Run() // override 추가
{
Console.WriteLine("강아지가 달려요");
}
}
public class Cat : Animal
{
public override void Run() // override 추가
{
Console.WriteLine("고양이가 달려요");
}
}
// 출력:
// 강아지가 달려요
// 강아지가 달려요
// 고양이가 달려요
// 고양이가 달려요
오버라이딩은 부모 클래스가 가진 함수를 자식 클래스가 상속받아서 자신의 동작으로 재정의하는 것이다.
부모 객체의 함수를 가상함수로 만들고 자식 객체에서는 물려받은 가상함수를 재정의하고 자신의 스타일대로 사용한다는 개념이다.
virtual 메서드는 자식 클래스에서 바꾸면 바뀌고 그대로 두면 그대로 출력한다. 또한 물려받은 virtual 메서드를 덮어쓰기 위해서는 자식 클래스에서 override를 써야된다.
class Skill
{
public string name;
public float cd;
public virtual void Execute()
{
Console.WriteLine("{0} 스킬을 사용합니다", name);
Console.WriteLine("{0} 쿨타임을 적용합니다.", cd);
}
}
public class Fireball : Skill
{
public Fireball()
{
name = "파이어볼";
cd = 3.5f;
}
public override void Execute()
{
base.Execute(); // base - 부모 클래스의 메서드를 덮어 쓰면서 구체화한다.
Console.WriteLine("화염구 발사!");
}
}
base. 의 역할은 부모 클래스의 메서드를 덮어 쓰면서 구체화하는 것이다. 만약 이것이 없다면 쿨타임과 관련된 텍스트는 출력되지 않는다. 또한 만약 자식클래스에서 execute를 재정의하지 않으면 그대로 부모 클래스의 메서드를 사용하게된다.
업캐스팅은 암시적으로(자동으로) 형변환이 가능하다. 그러나 다운캐스팅은 불가능한 경우가 있기 때문에 확인 후에 변환이 가능하다. 자식 클래스마다 각각의 메서드가 있을 수 있는데, 그것을 다른 자식 클래스에서 사용할 수도 있기 때문이다.
static void Main(string[] args)
{
Monster slime = new Slime();
{
if (slime is Slime)
{
Slime s = (Slime)slime;
s.Split();
}
else if (slime is Dragon)
{
Dragon s = (Dragon)slime;
s.Split();
}
}
}
is: 형변환이 가능하면 true, 불가능하면 false를 반환한다.
if (slime as Slime)
{
Slime slime = (Slime)slime;
slime.Split();
}
as: 형변환이 가능하면 바꿔서 주고, 불가능하면 null을 반환한다.
주의점: 캐스팅할 때 예외처리 하지 않으면 런타임 에러 생김
internal class Program
{
static void Main(string[] args)
{
Car sportsCar = new Car("스포츠카", 100);
Car Truck = new Car("트럭", 30);
sportsCar.Move();
Truck.Move();
}
}
class Car
{
public string name;
public int speed;
public Car(string name, int speed)
{
this.name = name;
this.speed = speed;
}
public void Move()
{
Console.WriteLine($"{name}자동차가 {speed}속도로 이동합니다.");
}
}
하나의 클래스(Car)가 스포츠카, 트럭 등 다양한 형태를 가질 수 있다.
다형성: 객체의 속성이나 기능이 상황에 따라 여러가지 형태를 가질 수 있는 성질
객체 지향의 특징 중 하나는 다형성이다. 위의 예시처럼 데이터를 다양하게 적용함으로써 다양한 형태를 띄게 할 수 있다.
또한 virtual, override 를 사용한 예제에서 볼 수 있듯이 기존의 코드는 수정하지 않고 새로운 것을 추가하며 확장할 수 있다.
추상클래스란 일반 클래스처럼 사용할 수 있지만 이를 상속받은 자식 객체들에게 정확하게 정의되지 않은 메서드를 물려주고, 자식 객체에게 반드시 구현하도록 하는 기능이다.
abstract class Animal
{
public abstract void Cry();
}
class Cat : Animal
{
public override void Cry()
{
Console.WriteLine("야옹");
}
}
class Dog : Animal
{
public override void Cry()
{
Console.WriteLine("멍멍");
}
}
추상화의 사용의미
class Bird : Animal
{
public override void Cry() { }
}
또한 추상클래스는 인스턴스로 실체화시킬 수 없다.