해시테이블(Hashtable)

해시테이블은 해시맵(HashMap)과 함께 Map을 구현한 키와 값 쌍을 데이터로 저장한 컬렉션(Collection)입니다. 여기서 키는 유일해야만 합니다. 해시테이블은 이 키를 가지고 키에 해당하는 값을 찾습니다.

Hashtable과 HashMap의 차이

Hashtable과 HashMap과의 차이점은 Thread-Safe인지 아닌지가 그 차이점인데요. Hashtable은 동기화가 걸려있어서 Thread-Safe하다고 할 수 있으며 HashMap은 동기화가 없어 unsafe하다고 할 수 있습니다. 그래서 안전성을 추구한다면 Hashtable을 쓰시면 되고, 데이터의 빠른 처리를 위해서라면 HashMap을 사용하시면 됩니다.

여기서 Hastable의 Thread-Safe가 무슨 의미를 하는지 맨 아래의 예제를 통해서 알아보도록 하세요.

해시맵의 사용법을 알고싶으신 분은 아래의 링크를 참고해주세요.

reakwon.tistory.com/151

 

[자바] 해시맵(HashMap)의 개념과 사용 예, 기본 메소드와 같은 키인지 판별하는 방법

해시맵(HashMap) 해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서

reakwon.tistory.com

 

인터페이스인 Map을 구현한 만큼 HashMap과 사용법이 거의 비슷합니다. put으로 키와 값을 저장하고 get으로 키에 해당하는 값을 읽어옵니다. Hashtable에서 table은 소문자라는 점! 유의해주시길 바라요.

예 ) put, get 메소드 사용방법

import java.util.Hashtable;
import java.util.HashMap;
import java.util.Map;
public class Main {
	public static void main(String[] ar) {
		Map<String,Integer> ht=new Hashtable();
		ht.put("key1", 50);
		ht.put("key2",100);
		ht.put("key2",250);	//같은 키가 들어갈 경우에는?
		System.out.println(ht);
		System.out.println(ht.get("key1"));
		System.out.println(ht.get("key2"));
		System.out.println(ht.get("key3"));	//키가 없을 경우에는?
		
	}
}

 

결과

{key2=250, key1=50}
50
250
null

 

만약 동일한 키를 가지고 데이터를 집어넣게 된다면 가장 마지막의 키와 값 요소로 저장이 됩니다. 그리고 키가 존재하지 않으면 null을 반환합니다. 

예) 갖고 있는 모든 키를 순회(keySet)

만약 Hashtable이 갖고 있는 모든 키를 알고 싶다면 어떻게 할까요? 그럴때는 keySet()메소드로 모든 키를 가져올 수 있습니다. 이때 반환하는 자료형은 Set입니다. 아래의 코드같이 키와 값을 추출할 수 있습니다.

public static void main(String[] ar) {
	Map<String,Integer> ht=new Hashtable();
	ht.put("foo",100);
	ht.put("bar",250);
	for(String key:ht.keySet()) {
		System.out.println("{"+key+","+ht.get(key)+"}");
	}
}

 

결과

{bar,250}
{foo,100}

 

예) Hashtable에 Hashtable을 추가(putAll)

만약 두 Hashtable을 합치고 싶다면 putAll() 메소드를 사용하면 됩니다. 다만 putAll()의 인자로 전달한 Hashtable의 키, 값의 변화가 있다해도 합쳐진 Hastable에는 영향을 받지 않습니다.

public static void main(String[] ar) {
	Map<String,Integer> ht1=new Hashtable();
	ht1.put("key1",9999);
	ht1.put("key2",2020);
		
	Map<String,Integer> ht2=new Hashtable();
	ht2.put("key3",3030);
	ht2.put("key4",8888);
		
	ht2.putAll(ht1);
	System.out.println(ht2);
		
	ht1.put("key5",5555);	//ht1에 데이터 추가
	System.out.println(ht2);
}

 

결과

{key4=8888, key3=3030, key2=2020, key1=9999}
{key4=8888, key3=3030, key2=2020, key1=9999}

 

예) ForEach() 메소드로 키와 값 순회하기

Hashtable에는 ForEach메소드가 존재하는데, 이것을 가지고 키와 값으로 동작을 정의할 수 있습니다. 람다식을 알긴해야하는데, 사용법만 알아도 상관은 없습니다. 

public static void main(String[] ar) {
	Map<String,Integer> ht=new Hashtable();
	ht.put("application layer", 7);
	ht.put("presentation layer", 6);
	ht.put("session layer", 5);
	ht.put("tranport layer", 4);
	ht.put("network layer", 3);
	ht.put("datalink layer", 2);
	ht.put("physical layer", 1);
	ht.forEach((key,value)->
	{
		System.out.println("{"+key+","+value+"}");
		
	});
 }

 

결과

{session layer,5}
{physical layer,1}
{datalink layer,2}
{presentation layer,6}
{tranport layer,4}
{network layer,3}
{application layer,7}

 

 

Hashtable의 Thread-Safe

이제 여기서 Hashtable이 왜 Thread-Safe한지 설명하도록 하겠습니다. 우선 쓰레드에 대한 개념에 대해서 알 필요가 있습니다. 자바 쓰레드의 사용법은 아래의 링크를 참고해주세요.

reakwon.tistory.com/84

 

[JAVA/자바] 쓰레드(Thread) 다루기( Thread상속, Runnable 구현, join)

쓰레드(Thread) 쓰레드(Thread)는 간단히 정의하면 하나의 프로세스(실행중인 프로그램이라고 합니다.)에서 독립접으로 실행되는 하나의 일, 또는 작업의 단위를 말합니다. 뭐, 더 간단히 말해 쓰레

reakwon.tistory.com

 

우선 Thread-Safe하지 않은 HashMap의 코드로 아래의 코드를 작성해보고 결과를 봅시다.

public class Main {
	static Map<String,Integer> hm=new HashMap();	//thread-unsafe
	public static void main(String[] ar) throws InterruptedException {
	
		Runnable runnable=new Thread() {
			@Override
			public void run(){
				hm.put("http",80);
				hm.put("ssh", 22);
				hm.put("dns", 53);
				hm.put("telnet",23);
				hm.put("ftp", 21);
				hm.forEach((key,value)->
				{
					try {
						Thread.sleep(1000);
					}catch(InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("{"+key+","+value+"}");
				
				});
			}
		};
		Thread thread=new Thread(runnable);
		thread.start();
		Thread.sleep(1000);
		hm.put("dhcp",67);
		System.out.println("MainThread end");

	}
}

 

보세요. 이 코드의 대한 저의 의도는 생성된 쓰레드(이하 thread라고 칭하도록 하겠습니다.)가 1초마다 run() 메소드의 저장된 키와 값을 순회하면서 출력하는 겁니다. 물론 예외없이 정상적으로요. 그러니 출력은 아래와 같아야하는 것이 이 코드의 의도입니다. 물론 MainThread가 끝나는 시점은 언제인지 예측 불가능하므로 어디서 나오든 상관없습니다. 중요한 것은 ftp~ssh까지만 출력되어야하는 것이 저의 의도입니다.

MainThread end
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}

 

하지만 우리의 예상과는 다르게 아래와 같이 출력됩니다. ConcurrentModificationException이라는 예외(Exception)가 발생하게 됩니다. 쓰레드들이 동시에 데이터를 변경할때 이 예외가 발생합니다.

MainThread end
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}
{dhcp,67}
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1428)
	at aa.Main$1.run(Main.java:18)
	at java.base/java.lang.Thread.run(Thread.java:831)

 

왜 이런 예외가 발생할까요? 코드를 하나씩 해석해보도록 합시다.

자, HashMap은 두 스레드가 공용으로 사용할 수 있도록 static으로 지정해놓았고, 생성된 쓰레드는 해시맵에 http~ftp까지의 키와 값을 put하고 있습니다(여기서 value는 그 프로토콜의 포트번호를 의미합니다. 뭐 여기서는 중요한게 아니지요.) thread는 put으로 키,값을 저장하고 난 후에 forEach() 메소드를 이용해서 1초마다 갖고 있는 키와 값을 출력합니다. 

메인스레드에서는 thread가 forEach() 메소드를 수행할 시간을 벌어주기 위해서 1초간 잠재워줍니다. 그 후 thread는 자신의 루틴(routine) 중 forEach()메소드를 실행합니다. 

이때 thread가 forEach() 메소드가 수행하는 중에 메인 쓰레드에서 put으로 데이터를 집어넣고 있는 상황이죠. 즉, HashMap에 동시에 접근해서 변경이 발생하게 되는 그런 코드입니다. 

우리의 의도대로 동작하게 만드려면 어떻게 해야할까요? 이럴때 synchronized라는 키워드로 우리가 직접 동기화를 처리할 수도 있지만 더 간단한 방법으로는 바로 Hashtable을 사용하는 것입니다. 이 문제를 해결하려면 단 한줄만 바꾸면 됩니다.

static Map<String,Integer> hm=new Hashtable();	//thread-safe

 

물론 hm(HashMap)이라는 변수도 ht(Hashtable)로 바꾸면 더 일관성있겠죠. 그 후의 결과는 우리의 의도대로 아래와 같이 동작함을 알 수 있습니다.

{ftp,21}
{ssh,22}
{http,80}
{dns,53}
{telnet,23}
MainThread end

 

이처럼 스레드에 대해서 동시접근에 안전하려면 Hashtable을 쓰는 것이 좋고, 단일 쓰레드에서 사용한다면 HashMap을 쓰는 것이 좋습니다.

지금까지 Hashtable의 개념, 그리고 사용방법과 HashMap과의 차이점을 코드를 통해서 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

해시맵(HashMap)

해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서 키는 맵에 오직 유일하게 있어야합니다. 즉, 같은 맵에 두 개 이상의 키가 존재하면 안된다는 것입니다. 이름 그대로 열쇠이기 때문에 그 열쇠로 짝인 값(Value)를 찾아야하기 때문입니다. 값은 중복된 값이어도 상관이 없습니다. 

HashMap과 사용법이 거의 동일한 컬렉션(Collection)에는 Hashtable이 있습니다. 두 클래스의 차이점은 Thread 관점에서 안전하냐(Hashtable), 안전하지 않은 대신 속도가 빠르냐(HashMap)입니다. 여기서는 Thread-Safe하지 않은 HashMap사용법을 가장 아래에서 살펴보도록 하겠습니다. Hashtable의 개념과 사용법, Thread-Safe한 예제를 보려면 아래의 링크를 참고해주세요.

reakwon.tistory.com/152

 

자바 해시테이블(Hashtable) 개념과 사용법, HashMap과의 차이점을 코드로 이해할 수 있는 예제

해시테이블(Hashtable) 해시테이블은 해시맵(HashMap)과 함께 Map을 구현한 키와 값 쌍을 데이터로 저장한 컬렉션(Collection)입니다. 여기서 키는 유일해야만 합니다. 해시테이블은 이 키를 가지고 키에

reakwon.tistory.com

자신이 만든 객체도 키로 사용할 수 있는데, 그 사용법은 가장 아래쪽 부분에 설명되어있습니다.

 

 

map

 

Map 인터페이스를 구현한 HashMap은 키를 해싱하여 자료를 저장하고 꺼내오기 때문에 속도가 빠릅니다. 이번 포스팅에서는 사용방법 위주로 설명하도록 하겠습니다.

사용법

사용전에 HashMap과 Map은 java.util 안에 위치합니다. 두개를 import 하여 사용준비해주세요.

 

1. 키,값 저장(put), 읽기(get) 예

import java.util.HashMap;
import java.util.Map;
public class Main {
	public static void main(String[] ar) {
		Map<String,Integer> map=new HashMap();	//<키 자료형, 값 자료형>
		map.put("A", 100);
		map.put("B", 101);
		map.put("C", 102);
		map.put("C", 103); //중복된 key가 들어갈때는 이전 키,값을 지금의 것으로 업데이트
		System.out.println(map);
		System.out.println(map.get("A"));
		System.out.println(map.get("B"));
		System.out.println(map.get("C"));
	}
}

 

 

결과: 

{A=100, B=101, C=103}
100
101
103

map을 그냥 println으로 출력하게 되면 중괄호('{ }')로 묶여서 키와 값들이 출력됩니다. 기본적으로 map의 put과 get이 아주 많이 사용됩니다. map을 사용하려면 반드시 알아야하는 메소드입니다. put을 키와 값을 map에 저장하는 메소드이며 get은 입력받은 key와 대응되는 값을 돌려줍니다. 만약 해당하는 key가 없다면 null을 넘겨주게 됩니다.

 

2. containsKey 사용예 (이미 HashMap에 키가 있으면 값을 덮어쓰지 않는 예)

public static void main(String[] ar){
	Map<String,Integer> map=new HashMap();
	map.put("key1", 100);
	map.put("key2", 200);
	if(!map.containsKey("key2"))	//키가 들어있는지 확인. 있으면 덮어쓰지 않는다.
		map.put("key2", 300); 
	System.out.println(map);
	System.out.println(map.get("key1"));
	System.out.println(map.get("key2"));
}

 

 

 

결과:

{key1=100, key2=200}
100
200

 

containsKey메소드로 키가 존재하는지 존재하지 않는지 알 수 있습니다. 이것과 비슷한 메소드로는 containsValue가 있죠. 이것은 반대로 값이 존재하는지 알아보는 메소드입니다. 존재시 true, 없을때는 false를 반환합니다. 위의 if문과 put메소드를 한꺼번에 처리할 수 있는 메소드가 존재합니다. 그래서 두 라인을 아래와 같이 바꿔써도 같은 동작을 합니다.

//if(!map.containsKey("key2"))	//키가 들어있는지 확인. 있으면 덮어쓰지 않는다.
			//map.put("key2", 300); 
map.putIfAbsent("key2",300);

 

3. putAll 사용예 (Map에 다른 Map을 전부 포함)

public static void main(String[] ar) {
	Map<String,Integer> map1=new HashMap();
	Map<String,Integer> map2=new HashMap();
	//map1 put
	map1.put("map1-key1", 100);
	map1.put("map1-key2", 200);
		
	//map2 put
	map2.put("map2-key3", 300);
	map2.put("map2-key4", 400);
		
	System.out.println("map1:"+map1);
	System.out.println("map2:"+map2);
		
	//map2에 map1을 합침
	map2.putAll(map1);
	System.out.println("map2 includes map1:"+map2);
		
	//map1의 키, 값 변경
	map1.put("map1-key1", 1000);
	//map2에는 영향 없음.
	System.out.println("map2 includes map1:"+map2);
}

 

결과

map1:{map1-key1=100, map1-key2=200}
map2:{map2-key4=400, map2-key3=300}
map2 includes map1:{map2-key4=400, map1-key1=100, map1-key2=200, map2-key3=300}
map2 includes map1:{map2-key4=400, map1-key1=100, map1-key2=200, map2-key3=300}

 

아예 map을 통째로 인자로 넘겨주고 싶다면 putAll 메소드를 사용하면 됩니다. 주의해야할 점은 반드시 키와 값의 자료형이 같은 map이어야한다는 점입니다. 다른 자료형의 키, 값은 받을 수 없습니다.

 

putAll 대신 생성자를 이용해서 생성과 동시에 map의 데이터를 전부 넘겨줄 수도 있습니다.

Map<String,Integer> map2=new HashMap(map1);

 

 

 

4. keySet 사용예 (모든 키를 순회하는 코드)

list처럼 증가하는 index를 사용할 방법이 없지만 keySet메소드를 이용하여 키를 Set으로 넘겨주어 Map에 존재하는 키를 모두 순회할 수 있습니다. 

public static void main(String[] ar) {
	Map<String,Integer> map=new HashMap();
	map.put("key1",50);
	map.put("key2",100);
	map.put("key3",150);
	map.put("key4",200);
		
	System.out.println("All key-value pairs");
	for(String key:map.keySet()) {
		System.out.println("{"+key+","+map.get(key)+"}");
	}

}

 

결과

All key-value pairs
{key1,50}
{key2,100}
{key3,150}
{key4,200}

 

5. Foreach() 메소드로 순환하기

Foreach() 메소드를 사용하기전, 람다식을 이해하고 있어야합니다. 하지만 사용법만 알아도 유용하게 사용할 수 있습니다. 아래의 사용법을 보면서 익혀보세요.

public static void main(String[] ar) {
	Map<String,Integer> hm=new HashMap();
	hm.put("http",80);
	hm.put("ssh", 22);
	hm.put("dns", 53);
	hm.put("telnet",23);
	hm.put("ftp", 21);
	hm.forEach((key,value)->
	{
		System.out.println("{"+key+","+value+"}");
		
	});
	
}

 

위에 보이는 -> 가 있는 라인이 람다식입니다. key와 value를 사용하며 -> 이후 동작을 구현해주면 됩니다.

결과

{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}

 

6. 내가 만든 객체를 Key로 사용하기(나의 객체를 같은 키로 판단하는 방법)

public class Main {
	public static void main(String[] ar) {
		Person person1=new Person("reakwon","666666-7777777");
		Person person2=new Person("putty","123456-1234567");
		
		Person who=new Person("reakwon","666666-7777777");
		Map<Person,Integer> map=new HashMap();
		map.put(person1, 90);
		map.put(person2, 80);
		
		System.out.println("map includes "+who.getName()+"? "+map.containsKey(who));
	
		map.put(who, 70);
		System.out.println(map);
	}
}
class Person{
	private String name;
	private String id;
	public Person(String name,String id) {
		this.name=name;
		this.id=id;
	}
	public String getName() {
		return name;
	}
	
	@Override
	public String toString() {
		return this.name;
	}
}

 

결과

map includes reakwon? false
{putty=80, reakwon=70, reakwon=90}

 

위의 코드에서 약간 불편한 점을 눈치 채셨나요? 저는 Person의 name과 id가 같으면 같은 키로 보고 중복처리를 하지 않을 생각이었습니다. 즉, 위의 map에는 putty와 reakwon만 있는 것이 저의 의도였고, who와 person1은 같은 키로 map이 인식해줬으면 좋겠습니다. 어떻게 구현할까요?

 

● equals() 메소드 Overriding

Object 클래스의 equals는 서로 같은 객체인지 아닌지 판별해주는 메소드입니다. String이 바로 이 메소드를 오버라이딩했지요. 그래서 문자열이 같은 경우에 equals의 결과는 true입니다. 이처럼 equals를 통해서 같은지 아닌지를 판별해주면 됩니다. 그래서 아래의 코드를 Person에 추가하면 됩니다.

	@Override
	public boolean equals(Object o) {
		if(o instanceof Person) {
			Person p=(Person)o;
			return this.id.equals(p.id) && this.name.equals(p.name);
		}
		return false;
	}

 

결과

 

 

map includes reakwon? false
{putty=80, reakwon=70, reakwon=90}

 

분명 equals를 오버라이딩해서 같은 객체라고 명시했는대도 역시 false를 반환하고 있습니다. 어떻게 해결할 수 있을까요?

 

● hashCode() 메소드 Overriding

equals()와 쌍으로 기억해두셔야할 것은 바로 hashCode()입니다. hashCode는 각 객체가 갖는 유일한 값(Code)을 의미합니다. Object의 hashCode()는 원래 주소값에 의한 hashCode()로 각 객체가 전부 다른값을 가지고 있습니다. HashMap은 우선 hashCode를 비교하고 같을 때만 equals를 수행하여 정말 제대로 같은것인지 판별합니다. 그래서 HashMap은 애초에 hashCode()가 반환하는 값이 다르면 equals는 수행하지도 않습니다.

그래서 위 코드에서 같은 해시코드를 오버라이딩해서 name과 id에 따라 다른 hashCode를 갖게 만들어주어야합니다. 구현의 편의성을 위해서 String클래스를 사용합니다. String 클래스가 이미 그렇게 다 구현(문자열이 같으면 hashCode도 같고 equals도 true) 이 되어있습니다. 우리는 위의 예제에서 String을 키로 쓴적이 있었죠. String 클래스는 hashCode와 equals가 문자열이 같을때 같은 객체라고 판별할 수 있도록 두 메소드를 전부 재정의한 상태입니다.

그래서 우리는 아래의 코드를 위의 코드 대신 추가해주면 됩니다.

	@Override
	public int hashCode() {
		return name.hashCode()+id.hashCode();
	}	
	
	@Override
	public boolean equals(Object o) {
		return this.hashCode()==o.hashCode();
	}

 

 

 

 

결과

map includes reakwon? true
{reakwon=70, putty=80}

 

위의 코드는 대체적으로 잘 동작하지만 만약 운이 굉장히 좋지 않을 경우 다른 객체로 의도했던 것이 같은 객체로 인식될 수 있습니다. String 클래스의 hashCode의 구현 방식때문이지요. 하지만 이해를 돕기 위해서 더 완벽한 구현은 하지 않았습니다. 귀찮기도 하구요.

알아두셔야할 점은 은 객체를 판별하는 코드를 넣고자하여 equals를 재정의할때 반드시 hashCode도 다시 정의해주어야한다는 점을 기억하세요.

 

HashMap의 Thread-Unsafe 예

아래의 코드를 봅시다.

public class Main {
	static Map<String,String> hashmap=new HashMap();
	public static void main(String[] ar) throws InterruptedException {
	
		Runnable runnable=new Thread() {
			@Override
			public void run(){
				hashmap.put("seoul","02");
				hashmap.put("kyeongkido", "031");
				hashmap.put("busan", "051");
				hashmap.put("daejeon","042");
				hashmap.forEach((key,value)->
				{
					try {
						Thread.sleep(1000);
					}catch(InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("{"+key+","+value+"}");
				
				});
			}
		};
		Thread thread=new Thread(runnable);
		thread.start();
		Thread.sleep(1000);
		hashmap.put("jeju","064");
	}
}

 

우선 Thread의 개념부터 잡아야합니다. 자바 쓰레드의 개념과 사용방법을 알아보려면 아래의 링크로 가서 학습해봅시다.

reakwon.tistory.com/84

 

[JAVA/자바] 쓰레드(Thread) 다루기( Thread상속, Runnable 구현, join)

쓰레드(Thread) 쓰레드(Thread)는 간단히 정의하면 하나의 프로세스(실행중인 프로그램이라고 합니다.)에서 독립접으로 실행되는 하나의 일, 또는 작업의 단위를 말합니다. 뭐, 더 간단히 말해 쓰레

reakwon.tistory.com

 

코드에 대한 결과는 예외를 던지며 프로그램이 종료됩니다. 아래와 같이 말이죠.

{seoul,02}
{daejeon,042}
{busan,051}
{jeju,064}
{kyeongkido,031}
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1428)
	at aa.Main$1.run(Main.java:17)
	at java.base/java.lang.Thread.run(Thread.java:831)

위 코드에 대한 의도는 thread라는 쓰레드에서 hashmap의 저장된 데이터 seoul ~ daejeon의 데이터를 모두 출력하는 의도입니다. 하지만 메인 쓰레드와 thread간의 HashMap에 동시에 접근했기 때문에 위 코드에 예외가 발생하게 된것입니다. 왜 동시에 접근했을까요? 

메인 스레드는 thread의 run() 메소드가 forEach가 수행할 수 있도록 잠시 1초간 기다려줍니다. 그리고 thread는 1초마다 저장했던 데이터들을 출력합니다. 그런데 그때 메인스레드에서 put() 메소드로 데이터를 저장하는 상황이 발생했습니다. 그래서 두 쓰레드가 동시에 HashMap에 접근하게 된것이죠.

이처럼 HashMap은 쓰레드에 대해서 안전하지 않은 컬렉션입니다. 오직 단일 쓰레드에서만 사용하면 안전한 컬렉션이죠. 이를 보완한 컬렉션이 바로 Hashtable입니다. 위의 링크로 들어가서 Hashtable로 이 문제를 해결한 코드를 확인해보세요.

이상으로 HashMap에 대한 기본적인 개념과 사용법 위주로 알아보았습니다. 이 정도만 알고 있어도 HashMap을 잘 사용할 수 있을 것으로 생각합니다. 감사합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

디바이스 드라이버(Device Driver)

여러분들이 usb포트를 통해서 펜으로 그림을 그리거나, 무선 마우스를 이용해서 마우스를 조정할때 윈도우즈에서 usb드라이버를 설치해달라고 하지 않던가요? 물론 이미 드라이버가 설치되어있으면 상관없겠지만 처음 사용할 경우에는 usb드라이버를 설치해달라고 컴퓨터가 요청을 할겁니다. 윈도우즈는 그 장치가 무선 마우스인지, 키보드인지 구분할 수가 없으며 어떻게 조정을 해야하는지도 모르기 때문입니다. 그래서 컴퓨터가 이 장치를 동작시킬때 어떻게 동작되어야하는지에 대한 프로그램을 따로 설치해야합니다. 여러분들이 알게 모르게 설치했던 것이 바로 디바이스 드라이버입니다. 구경이나 한번 해볼까요? 시작 프로그램에서 장치관리자를 검색해서 들어가보세요.

 

장치 관리자

이후 사운드쪽에서 디바이스 하나의 속성을 보도록 합시다.

사운드 디바이스

드라이버 탭을 보면 정보와 업데이트, 사용안함 이런 버튼이 존재하는지 알 수 있습니다. 여러분들이 간혹가다가 멀쩡한 스피커에서 소리가 안들릴때 있지 않나요? 그때 한 방법은 컴퓨터를 reboot하는 방법이 있겠지만 아래의 디바이스 드라이버를 다시 구동시켜주면 됩니다. 어떻게 하냐구요? 간단합니다. 

'디바이스 사용 안함' 을 눌러서 해제시키고, 다시 '디바이스 사용'을 눌러서 다시 장치를 구동시켜주면 어떨때는 해결되기도 합니다.

디바이스 속성

 

지금까지 간단하게 윈도우즈에서 디바이스를 살펴보았는데 자세히 그 내부를 들여다보면 아래의 도식과 같습니다. 사용자와 가장 접점에 있는 사용자 응용 프로그램 혹은 어플리케이션이 동작을 수행할때 내부적으로 System call을 사용합니다. open(), read(), write() 그런것들 있죠? 여러분들이 사용하는 printf()함수는 시스템 콜이 아니라 write를 잘 포장해서 만든 표준 입출력 라이브러리라고 합니다. 시스템 콜은 사용자 영역(kernel space)에서 호출이 가능합니다. 

 

여기서 사용자 영역이라고 하는 것은 실제 앱을 이용하는 사용자가 아니고 여러분들이 코딩할때 실행 프로그램을 만드는 그 영역을 말하는 겁니다. gcc로 실행 파일을 만들고 실행하죠. 여기서 printf나 메모리 할당(malloc), 그리고 포인터를 이용해서 데이터를 바꾸는데 심한 제약이 있었나요? 정말 알수없는 프로그램을 짜지 않는 이상 그런 경우는 없지요. 간단히 말해서 '내가 마음대로 프로그램을 짜고 실행할 수 있는 공간이구나' 라고 생각하시면 됩니다. 

device driver

시스템 콜이 호출이 되면 이제 커널 영역(kernel space) 내부로 호출이 전달됩니다. 커널 내부에 있는 가상 파일 시스템으로 전달, 그리고 아까 이야기했던 디바이스 드라이버가 인터페이스를 통해서 하드웨어를 제어합니다. 

여기서 또 커널 영역이라함은 무엇일까요? 일반 사용자가 접근할 수 없는 커널만의 영역을 이야기합니다. 여러분들이 커널 영역 메모리를 참조하거나 데이터를 변경할 수 없습니다. 디바이스 드라이버가 커널 쪽에 위치해있으므로 커널 영역의 프로그램이라고 보시면 되는데, 커널의 메모리를 손대는 곳이기 때문에 항상 안전성에 주의해야합니다. 혹시나 여러분들이 블루 스크린을 경험해본적 있죠? 디바이스 드라이버나 커널에 의해 시스템이 사용할 수 없는 상태가 됐기 때문입니다.

리눅스 디바이스는 세가지가 존재합니다. 캐릭터 디바이스, 블록 디바이스, 네트워크 디바이스가 있지요.

모듈(Module)

커널의 일부분인 프로그램으로 커널에 추가 기능이 모듈로 관리가 됩니다. 여러분들이 다바이스 드라이버를 만들고 추가할때 커널의 모듈로 끼워넣으면 됩니다. 모듈은 커널의 일부분입니다. 디바이스 드라이버가 하드웨어를 동작해야하기 때문에 커널의 일부분으로 동작해야합니다. 그래서 커널 모듈로 동작되어지는데, 이때 착각하지 말아야할 점은 모듈은 커널의 일부, 그리고 그 모듈에서 디바이스 드라이버가 동작됩니다. 즉, 모듈은 디바이스 드라이버가 아닙니다. 디바이스 드라이버가 모듈로 커널의 일부분으로 추가되어 동작하는 것이지요.

모듈이라는 개념이 없을때 디바이스 드라이버를 만들었다면  커널이 바뀌었기 때문에 다시 커널 컴파일을 해야하는 과정이 있었죠. 커널 컴파일 과정은 오래 걸리는 작업이라서 시간을 많이 소비하는 작업이기도 합니다. 모듈의 개념이 도입된 이후 위의 과정없이 모듈을 설치하고 해제할 수 있습니다. 바로 이 모듈에서 장치를 등록하거나 해제할 수 있습니다. 예를 들어 문자형 장치를 등록할때는 아래의 함수를 이용하죠. 

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

 

 

아직까지는 이 함수가 어떤 역할을 하는지 몰라도 됩니다. 우선 모듈이 어떻게 등록이 되는지부터 보도록 하겠습니다. 아주 간단한 모듈 하나를 만들어보도록 하겠습니다. 우선 여러분들이 C 프로그래밍을 하듯이 아래의 코드를 작성해줍니다.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>


static int __init my_init(void){
        printk("hello, kernel!\n");
        return 0;
}

static void __exit my_exit(void){
        printk("goodbye, kernel!\n");
}

module_init(my_init);
module_exit(my_exit);

 

맨 위의 3개의 헤더파일(linux/module.h, linux/kernel.h, linux/init.h)는 다 포함시켜주어야합니다.

우리가 맨 처음 모듈을 설치할때 초기화하는 module_init과 제거할때 호출되는 module_exit이 module.h에 정의되어있습니다. 이 매크로 함수의 매개변수를 우리가 정의한 함수로 전달해주면 됩니다. 이 무슨 역할을 하는지는 아래에서 보시면 됩니다.

#define module_init(x)  __initcall(x);
//...//
#define module_exit(exitfn)                                     \
        static inline exitcall_t __maybe_unused __exittest(void)                \
        { return exitfn; }                                      \
        void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));

 

혹시 객체지향 프로그래밍에서 생성자와 소멸자를 배우셨나요? 그때 객체의 초기설정은 생성자에서, 메모리해제 작업등 자원을 되돌려주는 마무리작업을 소멸자에서 하지 않나요? 그런 역할과 비슷하다고 보시면 됩니다. 

printk는 커널 모듈에서 메시지 내용을 출력할때 사용합니다. 커널 메시지는 dmesg 명령으로 볼 수 있습니다. 여기서 주의할 것은 printf가 아니라 printk라는 점!

어쨌든 코드를 짰다면 이제 컴파일을 해야하는데 Makefile을 이용해야합니다. 방법은 이렇습니다. 

KERNDIR=/lib/modules/$(shell uname -r)/build
obj-m+=mymodule.o
objs+=mymodule.o
PWD=$(shell pwd)

default:
        make -C $(KERNDIR) M=$(PWD) modules

clean:
        make -C $(KERNDIR) M=$(PWD) clean
        rm -rf *.ko
        rm -rf *.o

 

커널 모듈 빌드 디렉토리를 이용해야 하기 때문에 그 경로를 지정해줘야합니다. 이 디렉토리는 /lib/modules/커널 릴리즈 버전/build인데요. 여기서 커널 릴리즈 버전은 uname -r을 보면 알 수 있습니다. KERNDIR이 바로 그것이죠. 저의 경우는 아래와 같네요. 여기의 Makefile을 이용해야하기 때문에 기재해줘야하는 겁니다. 

uname -r

또 우리가 만든 모듈은 현재 디렉토리에 있죠? pwd의 결과를 넣어주면 됩니다. 

make를 하여 빌드합니다. 혹시 make 실행할 수 없다면 apt-get install make 해서 설치해주세요.

kernel object

make하고 빌드를 하면 산출물 중에서 .ko 파일이 보이실건데요. Kernel Object의 약자를 확장자로 갖고 있는 이것이 우리들이 설치할 모듈입니다. 설치해볼까요?

insmod

모듈을 설치하는 명령어는 insmod입니다. install module이죠. 자 이것을 이용해서 설치해봅시다. 사용법은 insmod [모듈명]으로 insmod mymodule.ko명령을 쳐보세요.

insmod

무소식이 희소식인 리눅스 세상에서 아무런 응답이 없으니 잘 설치된것 같은데요. 우리가 코드에서 init_module에서 printk로 문자열을 출력해주었죠? 모듈이 등록되었으니 분명 메시지가 나올테니 확인해보세요. dmesg | tail -1로 가장 마지막 줄을 확인해보도록 합시다. 

hello, kernel!

lsmod

우리가 설치한 모듈뿐만 아니라 설치된 다른 모듈도 보고싶다면 lsmod 명령을 사용하여 확인할 수 있습니다. 저의 머신에서는 아래와 같은 모듈들이 있습니다. 저의 모듈도 보이는군요.

lsmod

 

rmmod

마지막으로 모듈을 제거할때는 rmmod명령을 치면 됩니다. 사용법은 rmmod [제거할 모듈명] 으로 끝 확장자 .ko는 기재하지 않아도 상관없습니다.

rmmod

제거되었을까요? 우리가 코드에서 exit_module에서 전달한 함수가 있었죠? 그때도 우리는 printk로 메시지를 출력하게 만들었습니다. dmesg로 가장 마지막 메시지를 확인해보도록 합시다.

goodbye

 

커널 영역에서의 프로그래밍은 어려운 작업이고, 그것에 따른 세세한 조작을 할 수 있게 만들어줍니다. 예를 들어 커널 모듈을 통해서 우리는 시스템 콜을 후킹할 수도 있습니다. 아까 말씀드렸다시피 모듈은 커널의 일부분이며 반드시 디바이스 드라이버로 동작하지 않을 수 있지요.

이상으로 가장 간단한 리눅스 모듈을 만들어보았습니다. 최대한 쉽게 쓰려고 했는데 이해가 가셨나요? 

반응형
블로그 이미지

REAKWON

와나진짜

,

가양역 근처 오피스텔로 이사를 할 생각이신가요? 그렇다면 가양역 근처에 사는 저의 후기를 읽어보시고 생각해보세요. 우선 저는 가양역앞 근처에 오피스텔에 거주하고 있습니다. 왜 가양역 근처 오피스텔을 선택했을까요? 그 이유부터 말씀드리죠. 

1. 바로 앞 홈플러스가 있습니다. 장볼일이 있으면 옷 주섬주섬 걸치고 슬리퍼끌고 홈플러스로 쇼핑하러가면 되니 여러모로 편리합니다. 여기 이사오기 전에는 대형마트가려면 적어도 10분은 걸어가야했습니다. 그래서 물건 가져올때 무거우면 애 많이 먹었죠. 물론 역에서 얼만큼 떨어져있는지에 따라 다르지만 저의 집에서 홈플러스까지 3분이면 도착합니다. 

가양역

이처럼 대형마트가 가깝기 때문에 가볍게 장보고 올 수 있습니다.

바다의왕 장보고

2. 9호선 급행열차를 탈 수 있습니다. 가양역이 급행 열차가 서는 곳인데, 다른 급행역보다 좋은 점은 일반 열차가 급행 열차가 올때까지 기다리고, 급행열차가 떠난 후에 일반 열차가 출발합니다. 그래서 급행열차를 못탈 경우에는 일반 열차를 타면 됩니다. 하지만 도착역이 꽤 멀리 떨어져있는 경우는 급행 열차를 타야겠죠? 가양역에서 신논현역까지 급행으로 25분정도 걸리는 것 같습니다. 참고해주세요.

3. 공항과 가깝습니다. 9호선을 타고 공항철도를 탈 수 있는데, 김포공항까지 가는데 지하철타고 15분 내외로 도착할 수 있습니다. 그래서 주변에 항공업계 종사하는 분들이 많이 거주하고 있습니다.

4. 다른 오피스텔보다 전세가가 저렴했습니다. 구하는 사람마다 다르겠지만, 저의 오피스텔 기준 2020년 8월 당시 전세가가 1억 4500만원이었습니다. 가양역 근처에서도 제 오피스텔 전세가 싼편에 속했습니다. 발산역이나 마곡나루보다 싼편이었죠. 월세로는 60만원 정도였는데, 요즘 전제 구하기가 힘들어서 지금은 잘 모르겠네요. 잘 찾아보시면 있을 수 있습니다.

5. 바로 앞에 술집이 많습니다. 가양역에서 좀 밑으로 내려오면 강서구청 사거리가 있는데, 이곳에 술집, 고깃집이 있습니다. 주변 지인들이 오게되면 주로 이곳에서 한잔하곤 합니다. 물론 주변 지인들이 얼마 없어서 잘 나가지는 않습니다.

강서구청 사거리

 

그 중 제가 사는 오피스텔은 원룸에 복층이 있습니다. 한 두 달 생활한것으로 후기를 적기보다 생활 기간을 오래두어서 후기를 작성하는게 보다 도움이 될 것 같아 그곳에서 6개월 생활해온 후기를 남겨보도록 하겠습니다. 장점을 먼저 이야기해드린 후 제가 느낀 단점을 적어보도록 하겠습니다.

장점

1. 수납 공간이 넉넉하다.

계단이 있어서 계단 밑 공간이 전부 수납공간입니다. 그래서 별도로 수납장을 구할 필요가 없습니다. 워낙 많아서 전부 채우지도 못했습니다. 아마 복층 오피스텔은 대부분 빨래 건조대도 내장되어 있습니다. 계단 밑에 네모난 칸들이 보이시죠? 저것들이 전부 수납 공간입니다. 그리고 가장 오른쪽에는 빌트인 냉장고가 있습니다. 오피스텔들이 전부 이렇게 되어있다고 말할 수는 없지만, 대부분 이럴거에요.

수납공간

아래의 사진과 같이 별도로 빨래 건조대를 구할 필요가 없습니다. 아래처럼 이것도 내장되어 있거든요. 그렇기 때문에 공간 효율성이 높아지는 효과가 있습니다. 

빨래 건조대

이뿐만 아니라 옷장도 엄청 넉넉합니다. 아랫층에 옷장 4개, 복층에 5개가 있습니다. 옷도 없는데, 옷장만 많습니다. 옷이 많으신 분들한테는 굉장한 장점이 될 수 있을 것 같습니다.

2. 복층을 이용해 생활 공간이 분리된다.

위쪽은 제가 자는 공간입니다. 그래서 아래층은 저만의 다른 공간으로 사용할 수 있습니다. 아래층까지 보여드리고 싶지만 지금 글을 쓰는 이 시점에는 돼지 우리라서 보여드릴 순 없고, 아래층에 쇼파와 TV, 테이블, 그리고 책상에 있습니다. 현관 바로 앞에는 화장실이 있습니다. 저의 오피스텔의 경우 전용면적이 7평정도 되고 복층이 2평정도 됩니다. 자는 공간이 분리되어 있다보니, 아랫층 7평은 넉넉하게 이용할 수 있습니다.

그래서 지인들이 집에 놀러오면 아랫층에 테이블에 앉아 옹기종기 모여 광란의 파티를 열곤 합니다. 물론 나중에 치우는 것은 저의 몫이죠.

 

3. 천장이 높아 탁트인 느낌이다.

복층이기 때문에 천장이 매우 높습니다. 3.5m정도는 될거 같아요. 그래서 탁트여서 시원스럽습니다. 천장에는 시스템 에어컨이 있습니다. 손을 뻗어도 아무것도 닿지 않습니다.

4. 창문이 크다.

위의 사진처럼 창문이 커서 바람이 잘 들어옵니다. 통풍이 잘되고 쾌적한 느낌이 장점이 되겠죠? 환기도 잘됩니다. 

 

단점

1. 천장이 높아 모기잡기 힘들다.

천장이 높아서 벌레를 잡을 수가 없어요. 모기는 양반입니다. 실제로 제 집에 말벌이 들어온적이 있습니다. 생명의 위협을 느껴서 말벌이 알아서 나갈 수 있도록 창문을 열어두고 집 밖으로 피신했습니다. 2시간이 지났을 무렵, 말벌이 세마리가 되었습니다. 편의점에서 살충제사서 죽일 수 밖에 없었습니다. 여름철 에프킬러는 필수가 되겠습니다.

2. 창문이 커서 커튼 달기가 함들다.

창문이 커서 커튼달기가 힘들었습니다. 키가 작으신 분들은 커튼달려면 의자 올라타서 달아야할 수 있습니다. 커튼달때는 항상 창문 닫고 하세요. 창문이 커서 혹시나 그러면 안되겠지만 밑으로 떨어질 수도 있어요. 아니면 주변에 키큰 분들한테 도움을 요청해서 다셔야할건데, 저는 친구가 없어서 저 혼자 달았습니다. 외롭고 힘들었습니다.

3. 술먹고 2층올라갈때 늘 조심해야한다.

애주가(알콜 중독)라서 술먹고 윗층에 올라갈때는 항상 조심해야합니다. 보통 복층생각하시는 분들이 이런 걱정을 많이 하시더라구요. 다행스럽게 저는 술먹고 올라가다가 떨어져본적은 없습니다. 오히려 술먹고 올라갈때 더 조심조심 올라가는 것 같아요. 제가 조심성 하나는 기가막힙니다. 문제는 올라갈때가 아니라 내려올때가 문제입니다.

4. 계단이 높아 추락해 다칠 수 있다.

보기보다 계단의 높이가 높습니다. 눈대중으로 제어봤을때는 각 계단이 30cm 가까이 되는 것 같습니다. 그래서 항상 내려올때 손잡이 잡고 조심스럽게 내려와야합니다.  그리고 바닥이 보기보다 미끄럽기 때문에 한칸 한칸 조심스럽게 내려와야합니다. 실제로 내려가다가 발을 헛디뎌 발목이 꺾이며 바로 앞 옷장 벽에 머리를 박은적이 있습니다. 머리는 원래 돌이라서 괜찮았는데, 발목이 좀 많이 꺾여 거동이 불편해 연차냈습니다.

5. 사랑을 나눌때 구사할 수 있는 자세가 제한적이다.

여러분들은 여자친구, 남자친구 있으시죠? 복층에 주로 잠을 잘 수 있는 공간으로 두셔서 침대가 윗층에 있는데요. 복층은 높이가 130cm정도 되는 것 같습니다. 이때 사랑을 나눌때 일어설수가 없습니다. 무릎을 구부린 자세가 최대 허용자세입니다. 뭐, 자세를 자유자제로 구사할 수 있는 분이라면 상관없을 수 있겠습니다. 참고로 저는 해당사항 없습니다.

자세 불안정

 

6. 윗층의 물소리가 적나라하게 들린다.

복층에서 잘때 윗층에서 물떨어지는 소리가 곧장 고막에 울려퍼집니다. 왜냐면 윗층 화장실 바로 밑이 자고 있는 공간이기 때문인데요. 특히 늦은 밤에 씻을때 물떨어지는 소리, 설겆이할때 물떨어지는 소리, 그리고 아침일찍 씻을때 물떨어지는 소리, 설겆이할때 물떨어지는 소리가 들립니다. 윗층 분은 저보다 부지런한 분인지 일찍일어나셔서 씻으시는지 물떨어지는 소리가 들립니다. 그래서 아침에 알람 맞출 필요없이 물떨어지는 소리들으면 저도 일어나서 씻으러 갑니다. 

7. 창문이 크기 때문에 커튼을 필수.

창문이 커서 밖에서 다 보입니다. 저의 경우 아파트와 정면에 있어 커튼은 필수입니다. 안그러면 사생활이 밖에 전부 공개될 수 있습니다. 그러니까 큰 커튼으로 전부 가려줘야합니다. 그래야 안부끄럽습니다.

8. 화장실이 멀다

복층에서 자고 있다가 신호가 올때가 있습니다. 그럴때 화장실을 가야하는데, 동선이 조금 깁니다. 과정은 이렇습니다. 일어난다. → 계단을 조심조심 내려간다. 화장실 들어가서 싼다. 다시 계단을 조심조심 올라온다. 누워서 다시 잠을 청한다. 잠이 다 깼다. 화장실 가느라 잠이 다 깰 수 있습니다. 이런 복잡한 과정이 싫으시다면 요강 하나 준비하셔서 해결하시길 추천드립니다.

9. 보일러가 닿지 않는다.

만약 복층이 침실이라면 겨울에는 조금 추울 수 있습니다. 보일러가 아랫층바닥만 데워주기때문에 복층바닥은 차가울 수 있습니다. 저같은 경우에는 추위를 많이 타지 않아 괜찮았지만 추위에 취약하신 분들은 복층을 자는 공간이 아닌 창고 아니면 다른 공간으로 활용해주셔야할 수 있습니다. 

이상으로 제가 반년동안 복층 오피스텔에서 생활한 후기를 남겨보았습니다. 적다보니 장점보다 단점이 더 많네요. 그렇긴 해도 장점이 단점을 커버할 수 있으니까 저는 만족하면서 지내고 있습니다. 느끼는 점은 사람마다 다르니까 그저 참고만 해주시고, 결정은 알아서 최선의 선택을 해보시기 바랍니다. 

반응형
블로그 이미지

REAKWON

와나진짜

,

하한(Lower bound), 상한(Upper bound)

하한, 상한은 배열이 이미 정렬된 상태에서 적용되어야합니다. 상한, 하한은 모두 이진 탐색(Binary Search)로 구현이 되어있기 때문이죠. 여기서 이진 탐색은 아래의 코드인 것을 알고 계시겠죠? 이진 탐색의 기본 코드는 아래와 같습니다. 이진 탐색의 탐색 속도는 O(lg 2)의 속도를 자랑하지요. 

 

 

int BinarySearch(int s, int e, int number)
{
    int left=s,right=e,mid;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] == number) return mid;
        if (arr[mid] > n) right = mid -1;
        else left = mid + 1;
    }
    return -1;
}

 

하한이란? (Lower Bound)

특정 값 K보다 같거나 큰 값이 처음 나오는 위치가 됩니다. 즉, K값 이상인 값이 맨 처음 나오는 위치여야합니다. 아래는 그 예를 나타내는 표입니다. 이때 맨 앞 요소의 index는 0으로 시작합니다.

배열
1 2 2 4 4 5 6 6 6 9 9
lower_bound
lower bound( 4 )  3
lower bound( 5 )  5
lower bound( 9 )  9

하한을 Binary Search방식으로 구현한 코드는 아래와 같은데요.  number값을 이상인 값이 mid 위치에서 발견되었다면  왼쪽으로 더 가다보면 아예 작은 수를 만나게 되고 left와 right의 역전 현상이 발생되어 멈춥니다. 멈추기 전에 우리는 가장 마지막에 arr[mid] >= number인 상황을 lower_bnd에 저장한 상황이죠. 

int Binary_Search_Lower_Bound(int s, int e, int number)
{
    int left=s,right=e,mid;
    int lower_bnd = -1;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] >= number)
        {
            lower_bnd = mid;
            right = mid - 1;
        }
        else left = mid + 1;
    }
    return lower_bnd;
}

범위를 좁혀가는 상황을 그림으로 표현했습니다.

 

- C++의 lower_bound 사용

C++에서는 라이브러리 함수 형태로 lower bound를 지원합니다. 아래와 같은 형식을 사용하게 되는데요. 사용법은 이렇습니다. 


lower_bound( 정렬한 배열 포인터, 정렬한 배열 포인터의 끝 주소, 값) - 정렬한 배열 포인터

여기서 정렬한 배열의 포인터는 알겠는데, 정렬한 배열 포인터의 끝 주소값은 어떻게 구할까요? 그냥 마지막 원소의 주소를 넣어주거나 배열의 주소 + 배열의 사이즈로 넣어주면 됩니다. 

lower_bound의 반환 값은 하한값의 주소를 돌려주기 때문에 만약 index를 얻으려면 정렬한 배열 포인터를 빼주면 됩니다. 이 함수의 사용법은 밑의 예제 코드에서 upper_bound의 함수와 같이 사용해보도록 하겠습니다.

상한이란? (Upper Bound)

특정 값 K보다 큰 값이 처음으로 나오는 위치가 됩니다. 즉, K값 초과인 값이 맨 처음으로 나오는 위치입니다. 몇 가지 예를 들어볼까요?

배열
1 2 2 4 4 5 6 6 6 9 9
upper_bound
upper bound( 4 ) 5
upper bound( 5 ) 6
upper bound( 9 ) -1 

이 역시 Binary Search로 구현되는데요. 구현한 모습은 아래와 같습니다. 중간의 비교문이 반대로 됐음을 알 수 있죠? 이것은 arr[mid]의 값이 number의 이하일때 number와 최대한 같은 값의 index를 찾기 위함입니다.

 

 

int Binary_Search_Upper_Bound(int s, int e, int number)
{
    int left=s,right=e,mid;
    int upper_bnd=-1;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] <= number)
        {
            upper_bnd = mid;
            left = mid + 1;
        }
        else right = mid - 1;
    }
    return upper_bnd+1 >= N ? -1: upper_bnd+1;
}

 

만약 이렇게 돼서 while루프가 멈추었다면 arr[upper_bnd]값은 number값 초과 직전의 값일 것입니다. 그렇다면 arr[upper_bnd+1]의 값은 number보다 큰 값일 테지요. 그래서 마지막 return에서 +1을 해주는데, 이것이 배열의 사이즈를 넘어갈 수 있으니까 사이즈 체크하여 return합니다. 

C++에서 당연히 lower_bound함수가 존재하듯 upper_bound의 함수 역시 존재하며 사용법은 동일합니다. 단, 아래와 같이 사용할때 상한 값을 못찾아줄때도 있는데, 이때는 배열의 인덱스를 넘어가는 값을 갖게됩니다.


  upper_bound(정렬한 배열 포인터, 정렬한 배열 포인터의 끝 주소, 값) - 정렬한 배열 포인터

 

이제까지 설명한 내용을 예제 코드를 통해서 확인해보세요. 직접 구현한 BinarySearch 형식의 upper_bound, lower_bound도 같이 구현되어있습니다.

#include <iostream>
#include <algorithm>

using namespace std;

int arr[100];
int N = 11;
int BinarySearchLowerBound(int s, int e, int number)
{
    int left=s,right=e,mid;
    int lower_bnd = -1;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] >= number)
        {
            lower_bnd = mid;
            right = mid - 1;
        }
        else left = mid + 1;
    }
    return lower_bnd;
}

int BinarySearchUpperBound(int s, int e, int number)
{
    int left=s,right=e,mid;
    int upper_bnd=-1;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (arr[mid] <= number)
        {
            upper_bnd = mid;
            left = mid + 1;
        }
        else right = mid - 1;
    }
    return upper_bnd+1 >= N ? -1: upper_bnd+1;
}


int main(void) {


    arr[0] = 4; arr[1] = 2; arr[2] = 4; arr[3] = 1; arr[4] = 9;
    arr[5] = 2; arr[6] = 6; arr[7] = 6; arr[8] = 6; arr[9] = 9, arr[10] = 5;
    
    //정렬
    sort(arr, arr + N);

    printf("정렬된 배열\n");
    for (int i = 0; i < N; i++) printf("%d ",arr[i]);
    printf("\n");
    
    printf("Lower Bound\n");

    printf("%d lower bound:%d\n", 4, BinarySearchLowerBound(0, N - 1, 4));
    printf("%d lower bound:%d\n", 5, BinarySearchLowerBound(0, N - 1, 5));
    printf("%d lower bound:%d\n", 2, BinarySearchLowerBound(0, N - 1, 2));
    printf("%d lower bound:%d\n", 1, BinarySearchLowerBound(0, N - 1, 1));
    printf("%d lower bound:%d\n", 9, BinarySearchLowerBound(0, N - 1, 9));

    printf("%d lower bound:%d\n", 4, lower_bound(arr, arr + N, 4) - arr);
    printf("%d lower bound:%d\n", 5, lower_bound(arr, arr + N, 5) - arr);
    printf("%d lower bound:%d\n", 2, lower_bound(arr, arr + N, 2) - arr);
    printf("%d lower bound:%d\n", 1, lower_bound(arr, arr + N, 1) - arr);
    printf("%d lower bound:%d\n", 9, lower_bound(arr, arr + N, 9) - arr);
  

    printf("Upper Bound\n");

    printf("%d upper bound:%d\n", 4, BinarySearchUpperBound(0, N - 1, 4));
    printf("%d upper bound:%d\n", 5, BinarySearchUpperBound(0, N - 1, 5));
    printf("%d upper bound:%d\n", 2, BinarySearchUpperBound(0, N - 1, 2));
    printf("%d upper bound:%d\n", 1, BinarySearchUpperBound(0, N - 1, 1));
    printf("%d upper bound:%d\n", 9, BinarySearchUpperBound(0, N - 1, 9));

    printf("%d upper bound:%d\n", 4, upper_bound(arr, arr + N, 4) - arr);
    printf("%d upper bound:%d\n", 5, upper_bound(arr, arr + N, 5) - arr);
    printf("%d upper bound:%d\n", 2, upper_bound(arr, arr + N, 2) - arr);
    printf("%d upper bound:%d\n", 1, upper_bound(arr, arr + N, 1) - arr);
    printf("%d upper bound:%d\n", 9, upper_bound(arr, arr + N, 9) - arr);   //배열의 인덱스 초과된 값이 나옴
  
}

 

 

그리고 위 코드의 결과입니다. 마지막 upper_bound의 값이 다름을 확인해보세요.

 

 

전체 코드의 결과

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

상호배타적 집합(Disjoint Set)

오리엔테이션이나 수학 여행 등에서 서로 친해지기 위해서 하는 활동 중에 이런 활동들을 한번쯤 해보셨을 겁니다. 다들 서로 낯서고 모르는 상태이기 때문에 어색어색하죠. 이 순간은 서로가 각각 혼자서 있습니다. 그럴때 사회자가 혈액형이 같은 사람끼리 모이라고 했을때, A, B, AB, O 형으로 나뉘어진 각 사람들은 같은 혈액형을 찾으려고 시도하겠죠? 그래서 같으면 같은 조로 묶이게 됩니다.

이처럼 같은 특성끼리 서로 모이는 집합이 여러 집합이 있는데, 각 집합에서는 공통 원소가 존재하지 않는 그런 집합을 상호배타적 집합(Disjoint Set)이라고 합니다. 한 집합에서 다른 특성을 갖는 집합을 배제한다는 뜻입니다. 이때 쓰이는 자료구조가 유니온-파인드(Union-Find) 자료구조라고 합니다.

구현

상호배타적 집합을 구현하기 위해서는 트리의 자료구조를 사용합니다. 각 트리 노드들은 루트 노드를 가지고 있지요. 그래서 그 루트노드를 기준으로 같다면 같은 집합에 속해있는 것이고, 다르다면 다른 집합에 속해있다는 것을 알 수 있습니다.

1. 초기화

초기의 상태는 각 노드들이 자신이 루트가 됩니다. 아직 어떤 노드도 만난적이 없으니까 집합에서의 리더는 자기 자신일테니까요. 그러다가 같은 속성이 있다면 서로를 합치게 되는 것이죠. 아래의 코드가 초기화 코드를 나타냅니다. parent는 자신의 상위 트리 노드를 말하며 처음에는 자기 자신이 됩니다. 나머지 rank 변수는 이 후 설명하도록 하겠습니다.

 

struct DisjointSet {
	vector<int> parent, rank;
	DisjointSet(int n) :parent(n), rank(n, 1) {
		for (int i = 0; i < n; i++)
			parent[i] = i;
	}
};

 

아래는 두 트리가 있음을 보여줍니다. 두 트리의 루트는 1과 6이라서 이 트리들은 서로 다른 집합을 나타내는 것이라고 할 수 있습니다.

루트가 다른 트리

 

2. 루트 찾기

여기서 각 루트를 찾는 코드는 아래와 같습니다. 

int find(int node) {
	if (node == parent[node]) return node;
	return parent[node] = find(parent[node]);
}

 

find는 현재 노드가 속해있는 트리의 루트를 구하는 함수인데요. 여기서 재귀적으로 find를 호출해서 부모를 계속 찾다보면 나중에는 결국 부모가 자기 자신인 노드를 발견하게 됩니다. 왜냐면 루트의 부모는 없으며 루트는 위의 초기화에서 자기 자신이 부모 노드입니다. 그리고 반복적으로 find를 계속 호출하지 않기 위해서 parent[node] = find(parent[node]) 로 부모를 계산된 루트로 기록해둡니다. 그러면 나중에 루트를 찾을때 단번에 루트로 가서 반환되게 때문에 최적화를 할 수 있습니다.

3. 트리 합치기

루트가 다른 이 트리, 알보고니 공통 속성이 있어서 합쳐야될 것 같습니다. 합쳐보긴할텐데 어느쪽으로 합쳐야될까요? 우리는 루트가 1인 트리를 루트가 6인 트리에 합쳐야할까요? 아니면 6인 트리를 1인 트리 밑으로 가게 만들까요? 우리가 find를 구현했을때 루트를 찾이 위해서 계속 재귀적으로 부모를 호출해가면서 확인해보았습니다. 

그렇다면 루트를 빨리 찾기 위해서는 노드의 부모수가 적으면 속도가 빨라지겠죠. 그런 원리로 최적화하면 됩니다. 즉, 트리의 높이를 작게 만들수록 유리합니다. 

루트가 다른 트리

 

1이 루트인 트리에 6이 루트인 트리를 밑에 합쳐놓은 상황입니다. 트리의 높이가 증가했음을 알 수 있죠? 그렇다면 반대로 합쳐놓는다면, 즉 6이 루트인 트리에 1이 루트인 트리를 보면 어떨까요?

 

트리의 높이 증가

 

아래의 그림이 그 상황을 보여줍니다. 트리의 높이가 증가하지 않았음을 볼 수 있습니다. 그렇다면 결국에는 트리의 높이가 큰 트리 밑에 작은 트리를 합쳐 놓는게 높이를 증가시키지 않는 방법이네요. 이제 구현해봅시다.

트리의 높이 증가하지 않음

 

아래의 구현이 병합하는 코드를 구현한 것입니다.

void merge(int left, int right) {
	left = find(left), right = find(right);
	if (left == right) return;
	if (rank[left] > rank[right]) swap(left, right);
	parent[left] = right;
	if (rank[left] == rank[right]) ++rank[right];
}

 

왼쪽 트리가 항상 오른쪽 트리의 자식이 될 수 있도록 구현한 코드인데요. 왼쪽 트리의 루트를 구하고, 오른쪽 트리의 루트를 구해서 같으면 이미 같은 트리에 속해있는 것으로 종료합니다. 

그 후 rank라는 배열을 통해서 트리의 높이를 얻어옵니다. 항상 왼쪽에 작아서 오른쪽으로 합칠 구현을 해야하니까 값이 왼쪽이 크다면 교환해줍니다. 그래서 결국 왼쪽 부모는 오른쪽의 부모와 같게 만들죠.

트리의 높이가 증가하는 경우는 왼쪽, 오른쪽 트리의 rank값이 같을때만 존재합니다.

전체 소스코드는 아래와 같게됩니다. 아래의 코드는 알고리즘 문제해결 전략, 구종만 님의 코드를 참고했습니다.

 

struct DisjointSet {
	vector<int> parent, rank;
	DisjointSet(int n) :parent(n), rank(n, 1) {
		for (int i = 0; i < n; i++)
			parent[i] = i;
	}

	int find(int node) {
		if (node == parent[node]) return node;
		return parent[node] = find(parent[node]);
	}

	void merge(int left, int right) {
		left = find(left), right = find(right);
		if (left == right) return;
		if (rank[left] > rank[right]) swap(left, right);
		parent[left] = right;
		if (rank[left] == rank[right]) ++rank[right];
	}
};

 

예제 : BOJ 1717번, 집합의 표현

정확한 이해를 돕기 위해서 아래의 문제를 풀어봅시다. DisjointSet을 이용해서 풀 수 있는 문제로 위의 동작을 이해할 수 있으면 거뜬하게 풀 수 있는 문제입니다.

www.acmicpc.net/problem/1717

 

1717번: 집합의 표현

첫째 줄에 n(1 ≤ n ≤ 1,000,000), m(1 ≤ m ≤ 100,000)이 주어진다. m은 입력으로 주어지는 연산의 개수이다. 다음 m개의 줄에는 각각의 연산이 주어진다. 합집합은 0 a b의 형태로 입력이 주어진다. 이는

www.acmicpc.net

 

문제의 입력은 아래와 같습니다.

7 8
0 1 3
1 1 7
0 7 6
1 7 1
0 3 7
0 4 2
0 1 1
1 1 1

 

처음 N과 M이 주어지며 N은 집합의 원소 중 가장 마지막 원소, M은 질의를 나타냅니다.

다음은 질의가 M가 나오는데 질의의 유형은 0 a b, 1 a b가 있습니다. 0 a b는 a가 포함된 집합, b가 포함된 집합을 합치는 연산이며 1 a b는 a와 b가 같은 집합에 속해있는지 확인하는 연산입니다. 이때 1 a b의 질의에서 같은 집합에 속한다면 YES를 출력, 아니면 NO를 출력하면 됩니다.

위의 입력에 대한 출력은 아래와 같습니다.

NO
NO
YES

 

풀이 

맨 처음에는 각각의 수들은 다른 집합에 속해있다가 연산 0이 나오면 합치면 됩니다. 이 역할이 DisjointSet의 merge가 되겠군요. 그리고 연산 1이면 a와 b가 같은 집합에 속해있는지 확인하는 연산이므로 find(a), find(b)를 통해 두 집합의 root를 구한후 같은지 다른지 확인하면 되는 간단한 문제입니다.

문제의 정답 코드는 이렇습니다.

#include <cstdio>
#include <vector>
using namespace std;
#define MERGE 0
#define SAME_GROUP 1

struct DisjointSet {
	vector<int> parent, rank;
	DisjointSet(int n) :parent(n), rank(n, 1) {
		for (int i = 0; i < n; i++)
			parent[i] = i;
	}

	int find(int node) {
		if (node == parent[node]) return node;
		return parent[node] = find(parent[node]);
	}

	void merge(int left, int right) {
		left = find(left), right = find(right);
		if (left == right) return;
		if (rank[left] > rank[right]) swap(left, right);
		parent[left] = right;
		if (rank[left] == rank[right]) ++rank[right];
	}
};

int main() {
	int N, M;
	scanf("%d %d", &N, &M);
	DisjointSet djs(N+1);
	for (int i = 0; i < M; i++) {
		int query, a, b;
		scanf("%d %d %d", &query, &a, &b);
		if (query == MERGE) djs.merge(a, b);
		
		if (query == SAME_GROUP) 
			printf("%s\n", djs.find(a) == djs.find(b) ? "YES" : "NO");
	}

}

 

이상으로 상호배타적 집합에 대한 개념과 코드 구현, DisjointSet을 이용해서 문제를 풀어보았습니다. DisjointSet 알고 있으면 도움이 될 것 같나요?

반응형
블로그 이미지

REAKWON

와나진짜

,

가끔 스마트폰 사용하다가 이상하게 용량을 잡아먹지 않나요? 제가 갤럭시 S8 기종을 쓰고 있는데, 이게 저장공간이 그렇게 충분하지가 않더군요. 바꿀때가 된건지... 그럴때 제가 사용하는 용량 확보방법 몇가지 소개시켜드리도록 하겠습니다. 제 스마트폰(갤럭시 S8)을 기준으로 설명할건데, 다른 기종도 비슷할거니까 따라해보세요.

1. 안쓰는 어플리케이션 삭제

가장 간단한 방법으로 누구나 다 알고있는 내용입니다. 설정 -> 디바이스 케어 -> 저장공간 으로 이동해보시면 저장공간에 남아있는 용량이 나와있으며 사진, 문서, 어플리케이션들이 어느정도의 저장공간을 차지하고 있는지 파악할 수 있습니다. 여기서 안쓰는 어플리케이션이나 사진을 삭제해주세요.

 

 

저장 공간 클릭

 

삭제할 카테고리 클릭

어플리케이션 말고도 문서도 있고 이미지, 동영상도 있는데, 저는 주기적으로 컴퓨터로 백업해서 핸드폰 저장공간을 확보하는 편입니다.

 

2. 어플리케이션 캐시 삭제

우선 어플리케이션을 사용하다보면 그 놈들이 사용하는 일종의 저장공간이 있는데요. 캐시 데이터는 자꾸자꾸 쌓이게 되니까 저는 주기적으로 지워줍니다. 그 중 가장 큰 용량을 차지했던 것은 구글 플레이 스토어였습니다. 저는 어플도 자주 설치하지 않는데, 왜 자꾸 캐시를 쳐먹는지는 모르겠지만요. 얘가 좀 양아치라서 필요하시면 캐시 삭제하시는 것을 추천합니다. 구글 플레이 스토어를 길게 누르면 어플리케이션 정보를 볼 수 있는데요. 그것을 터치해줍니다.

앱정보 들어가기

그러면 저장공간이 보이는데 이것을 눌러서 들어갑니다.

저장공간 들어가기

이후에 캐시 데이터가 보이시나요? 앱 용량은 100MB가 안되는데 캐시만 주구 장창쓰고 있음을 알 수 있습니다. 저는 2달 정도 캐시를 안지우니 4.5GB 정도 캐시가 쌓였군요. 캐시 삭제를 눌러 지워줍니다. 제가 실행한 바로는 캐시 지워도 별다른 치명적인 기능 저하는 없었습니다.

 

 

더럽게 많이 쳐먹네

저는 구글 플레이 스토어외에도 크롬, 유튜브도 이런식으로 캐시 데이터를 삭제했습니다. 그런데 자주 사용하는 앱들이라서 금방 차게 되더군요.

3. 카카오톡 데이터 지우기

국민 어플 카카오톡을 많이 사용하는 만큼 그 기능도 많은데요. 그만큼 데이터를 저의 핸드폰 저장 영역을 이용해 데이터를 저장해놓습니다. 저같은 아싸들이야 뭐 채팅창이 몇개 없으니까 상관없지만, 여러분들같은 사회성 좋으신 분들은 채팅방을 관리해주는 것이 좋습니다. 

우선 채팅방을 열도록 해요. 그러면 오른쪽 위에 메뉴(三)가 보이시나요? 누르면 여러메뉴가 있는데요. 

 

제가 관심있는 것은 사진, 동영상, 파일 등 주고받았던 파일들입니다. 저는 사진, 동영상을 삭제해보겠습니다. 사진, 동영상쪽으로 들어가봅시다. 주의하실점은 사진, 동영상은 삭제되면 복구할 수 없으니까 필요없는 파일이나 사진을 삭제하셔야합니다! 사진이나 동영상은 백업해주시고 삭제해주세요.

 

여기에 우측 상단에 관리가 있습니다. 여기서 모두 삭제를 누르시면 전부 삭제되서 저장공간이 확보됩니다. 

 

 

관리 클릭

 

아까도 이야기했지만 삭제한 파일은 되돌릴 수 없으니 주의하셔서 삭제해주세요.

 

여기까지만 하셔도 왠만하게 스마트폰 저장공간을 확보해보실 수 있을 겁니다. 혹시 이 방법외에도 본인들이 사용하는 저장공간 확보하는 방법이 있으시면 댓글로 공유부탁드립니다! 저도 배우고 요긴하게 사용하도록 해보겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

동적 메모리 할당,해제를 맡는 new, delete 키워드

C에서 메모리의 동적할당을 할때 malloc 함수를 사용하지요. 동적할당에 대해 모르신다면 저의 동적할당에 대한 포스팅을 보고 오시기 바랍니다.

reakwon.tistory.com/20

 

[C언어] 동적 메모리 할당의 세가지 방법 malloc, calloc, realloc

동적 메모리 할당 우리는 이제껏 메모리를 할당할때 정적으로 할당했습니다. 어떤 것이냐면 int arr[100]; 이렇게 할당을 했었죠. 뭐 문제없습니다. 실행도 잘 되구요. 하지만 이런 상황은 조금 불

reakwon.tistory.com

 

C언어에서 malloc을 사용할때 반환하는 void*를 형변환하여 사용하였습니다. 그리고 동적할당된 메모리를 해제할땐 free를 사용했습니다. C++에서는 이러한 동적할당에 관한 사용이 자주 이루어지기 때문에 아예 키워드로 지정해놓았습니다. malloc을 대체하는 키워드는 new, free를 대체하는 키워드는 delete입니다. 사용되는 방식은 다음과 같습니다.

MyClass* myclass = new MyClass();
delete(myclass);

여기서 malloc과 new는 동적할당하는 것은 같지만 malloc은 함수를 call하는 것이고 new는 C++에서 기본적으로 제공하는 키워드이기 때문에 별도의 라이브러리를 추가할 필요가 없습니다. 또한 new는 생성자를 자동호출하는 특징이 있지요. 이제 C++에서는 객체를 할당할때 malloc으로 하지 않고 new를 이용하여 할당합니다.

오버라이딩(Overriding)

Class의 상속에 대해서 배우셨나요? 그렇다면 이제 다형성을 배워볼 차례입니다. 다형성이라하면 여러 형태를 갖는 객체의 특징을 말하는데요. 아래의 코드를 가지고 다형성과 관련한 클래스의 특징들을 천천히 살펴보도록 합시다.

#include<iostream>
using namespace std;

class Animal {
private:
	int height;
	int weight;
public:
	Animal(int _height, int _weight) :height(_height), weight(_weight) {}
	void printInfo() {
		cout << "==============정보=============" << endl; ;
		cout << "키:" << height << "무게:" << weight << endl;
	}
	int getHeight() {
		return height;
	}
	int getWeight() {
		return weight;
	}
};

class Human :public Animal {
private:
	int race;
public:
	Human(int _height, int _weight, int _race) :Animal(_height, _weight) {
		race = _race;
	}
	void printInfo() {
		cout << "==============정보=============" << endl;
		cout << "키:" << getHeight() << "무게:" << getWeight() << endl;
		cout << "인종:";
		if (race == 0)
			cout << "황인" << endl;
		else if (race == 1)
			cout << "흑인" << endl;
		else if (race == 2)
			cout << "백인" << endl;
		else
			cout << "혼혈" << endl;
	}
};
int main() {
	
	Animal* animal = new Animal(50, 20);
	animal->printInfo();

	Human* human = new Human(150, 80, 3);
	human->printInfo();
	delete(animal);
	delete(human);
}

 

여기서 Animal클래스는 Human클래스의 부모 클래스입니다. main에서는 둘 다 new 키워드로 생성해서 정보를 출력하는 코드네요. Human 클래스에서 printInfo 메소드를 보면 cout 두줄이 정확히 Animal의 printInfo클래스의 메소드와 같은 것을 알 수 있습니다. 

어차피 Human클래스는 Animal클래스를 상속받고 있으니, Animal클래스의 printInfo를 재활용 할 순 없을까요? 그럴때 부모클래스::메소드명 을 호출하여 부모클래스의 메소드를 사용할 수 있습니다. Human 클래스의 메소드를 아래처럼 바꿔봅시다.

void printInfo() {
	Animal::printInfo();
	cout << "인종:";
	if (race == 0)
		cout << "황인" << endl;
	else if (race == 1)
		cout << "흑인" << endl;
	else if (race == 2)
		cout << "백인" << endl;
	else
		cout << "혼혈" << endl;
}

 

이처럼 부모클래스의 함수를 이용하여 자식 클래스에서 같은 메소드 이름으로 새로운 기능을 덧붙이는 방식을 오버라이딩이라고 합니다.

 

 

다형성(Polymorphism)

OOP(Object-Oriented Programming)은 현실 세계를 반영한다는 슬로건을 가지고 있습니다. 위의 코드를 현실세계와 연관지어 생각해봅시다. 우리 사람은 동물의 한 종이죠. 그래서 동물의 속성을 갖고 있습니다. 하지만 동물은 사람이 아니죠. 동물에는 사람외에도 개와 고양이 등 많이 있기 때문인데요. 정리하면 이렇게 되겠네요.

사람은 동물이다.(O)

동물은 사람이다.(X)

위에서 성립이 되는 상황(사람은 동물)을 C++에서는 아래와 같이 표현할 수 있습니다. main함수안의 내용을 아래와 같이 바꿔서 실행해보시기 바랍니다.

Animal* human = new Human(150, 80, 3);
human->printInfo();

 

이렇게 부모클래스를 통해 만들어진 객체가 자신을 상속받는 여러 클래스의 객체로 모양을 띄는 것을 다형성이라고 합니다. OOP에서 매우 중요한 개념입니다.

위의 코드를 수행한 결과에서 한가지 불편한 점이 있는데요. 저희는 human->printInfo()가 당연히 Human클래스의 printInfo를 출력한다는 믿음으로 프로그래밍을 했는데, 막상 결과는 Animal의 printInfo가 호출됩니다. 어떻게 해야지 우리가 원하는 동작을 할 수 있을까요?

virtual 키워드

만약 자신을 상속받는 자식 클래스의 객체가 자신의 메소드를 사용하는데, 이것을 오버라이딩했다면 자식 클래스의 객체 메소드를 호출하라고 지정하는 방법은 메소드 앞에 virtual 키워드를 사용하는 것입니다. 자신의 메소드는 가상으로 만들어져 있으니 자식의 메소드를 호출하라는 의미가 되겠죠. 그래서 우리의 목표를 달성하기 위해서는 Animal 클래스의 printInfo메소드 앞에 virtual 키워드를 추가하면 됩니다. 그렇게 되면 Human의 printInfo 메소드를 호출하게 됩니다.

class Animal {
private:
	int height;
	int weight;
public:
	Animal(int _height, int _weight) :height(_height), weight(_weight) {}
	virtual void printInfo() {
		cout << "==============정보=============" << endl; ;
		cout << "키:" << height << "무게:" << weight << endl;
	}
	int getHeight() {
		return height;
	}
	int getWeight() {
		return weight;
	}
};

 

virual키워드를 사용해야 자식 객체의 메소드를 사용한다고 했죠? 그렇다면 다음의 상황을 예측해봅시다.

class Human :public Animal {
private:
	int race;
public:
	//...생략...//
	void printInfo() {
		//...생략...//
	}
};

class Student :public Human {
private:
	char grade;
public:
	Student(int _height, int _weight, int _race,char _grade) :Human(_height, _weight, _race) {
		grade = _grade;
	}

	void printInfo() {
		Human::printInfo();
		cout << "성적:" << grade << endl;
	}
};

 

Human 클래스는 printInfo에 virtual키워드를 넣지 않았습니다. Student는 Human클래스를 상속받고 있는데, 이때 printInfo는 Human의 printInfo를 호출할까요? 메소드의 virtual이 지정되면 이후 자식은 자동으로 virtual 키워드가 적용이 됩니다. 그래서 결론은 Student의 printInfo가 호출이 되지요. 하지만 명시적으로 virtual을 지정해주는 것이 관례입니다. 코드의 가독성과 이해를 돕기 위해서지요.

자, 이런 다형성은 언제 필요할까요? 예를 들면 함수에서 해당 클래스를 상속하는 모든 객체를 받을 때가 그 예가 됩니다. 아래의 함수에서 변수로 animal를 가리키는 포인터를 받습니다. 이 함수의 목적은 animal인 객체의 printInfo를 출력하는 것인데, Animal클래스를 상속하는 모든 객체를 받을 수 있습니다. 

void printInfo(Animal *animal) {
	animal->printInfo();
}

 

 

this 포인터

객체가 생성될때 자기 자신을 가리키는 포인터가 this 포인터라고 합니다. 만일 아래와 같은 상황에서 this를 사용할 수 있습니다. 저는 생성자에서 객체의 grade를 전달받은 grade의 값으로 입력하고 싶어서 아래와 같은 코드를 사용했습니다.

class Student :public Human {
private:
	char grade;
	Student(int _height, int _weight, int _race,char grade) :Human(_height, _weight, _race) {
		grade = grade;
	}
//...생략...//
};

 

여기서 객체의 grade는 절대 변할 수 없습니다. 객체의 grade보다 매개 변수인 grade가 우선시 되기 때문에 매개변수 grade에 다시 매개변수의 grade의 값을 넣기 때문입니다. 이럴때 사용할 수 있는 것이 바로 this포인터를 사용하는 것이죠.

Student(int _height, int _weight, int _race,char grade) :Human(_height, _weight, _race) {
	this->grade = grade;
}

this포인터는 객체가 생성되고 난 이후에 그 효력을 발휘합니다. 그 객체의 주소를 나타내야하기 때문이죠. 아래와 같이 한번 주소를 찍어봅시다. 정확히 같은 곳을 가리키는 것을 알 수 있습니다.

class Student :public Human {
private:
	char grade;
public:
	Student(int _height, int _weight, int _race,char grade) :Human(_height, _weight, _race) {
		cout <<"this 포인터의 주소"<< this << endl;
		this->grade = grade;
	}
	//...생략...//
};

int main() {
	
	Animal* student = new Student(165, 55, 0, 'A');
	cout <<"student의 주소"<< student << endl;
	student->printInfo();
	delete(student);
}

 

이상 다형성과 관련된 포스팅을 마치도록 하겠습니다. 이해가 되지 않는 부분은 댓글로 남겨주세요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

C++ 상속(Inheritance)

다른 여타의 객체지향언어와 같이 C++ 역시 상속을 할 수 있습니다. 여러분은 현실 생활에서 상속이라는 개념을 알고있으신가요? 부모님으로부터 100억을 상속을 받으셨다면 이 포스팅을 볼 필요없으십니다. 저랑 친구해요.

객체지향 프로그래밍에서는 부모 클래스의 맴버 변수와 메소드를 자식 클래스가 재사용하는 개념으로 알고 계시면 됩니다. 이 맴버 변수나 메소드에 접근 제한자를 더하여 아무나 접근할 수 없도록 할 수 있습니다. C++에서 상속하는 방법은 ' : ' 콜론을 사용하여 상속을 받을 수 있습니다. JAVA에서 extends를 사용하여 상속을 하는 것과 같습니다. 형식은 아래와 같습니다.

class 클래스_이름 : 접근제한자 부모_클래스명{
	//.. 내용 ..//
}

 

접근 제한자

접근 제한자는 세가지가 있습니다. private, protected, public이라는 접근 제한자가 있지요. 

접근 제한자 설명
private 오직 자신의 클래스안에서만 접근 가능한 멤버 변수, 메소드.
protected 자신을 상속하는 클래스까지만 접근 가능
public 어떤 클래스에서나 접근 가능

접근 범위

 

생성자 (Constructor)

클래스가 객체를 생성할때 항상 호출하는 일종의 메소드가 있습니다. 이 메소드는 생성자라는 이름을 갖고 있습니다. 여러분이 객체를 생성할때 초기화하는 방법을 생성자에 기재합니다. 이를 테면, 각 맴버 변수들의 초기화 등이 있지요. 형식은 아래와 같습니다. 자신의 클래스명과 동일하지만 반환이 없는 메소드가 바로 생성자입니다.  생성자를 사용하려면 반드시 public으로 해야합니다. 모든 곳에서 접근 가능해야 초기화를 진행할 수 있기 때문이죠. 

여러분들이 굳이 생성자를 정의해놓지 않아도 C++은 알아서 기본 생성자(Default Constructor) 만들어줍니다. 비록 아무런 동작을 하지 않는 껍데기에 불과하지만요.

class 클래스명 {
public :
	클래스명(파라미터){
    	//..동작 ..//
    }
}

 

소멸자(Destructor)

소멸자는 객체가 소멸할때 마지막 최종작업을 기록하는 메소드를 말하며 아래 형식과 같습니다. 소멸자는 반환값, 전달받는 인자가 없고 ~클래스이름() 형태여야합니다. 주로 free, delete 등의 메모리 누수가 발생하지 않게 메모리 해제 작업등이 여기에 포함됩니다.

class A{
public:
	~A(){
    	//..메모리 해제등의 작업 ..//
    }
}

 

 

 

이제 여기서 간단한 예를 들어보도록 하겠습니다. 

#include <iostream>
using namespace std;

class A {
private:
	int a;
	int b;
public:
	A(int _a, int _b){
		a = _a;
		b = _b;
	}
	int add() {
		return a + b;
	}
};

class B : A {
public :
	B(int _a, int _b) : A(_a, _b) {}
	void printResult() {
		//A를 상속받았으니, 부모클래스의 add라는 메소드를 사용할 수 있음.
		printf("%d\n", add());
	}
};

int main() {
	B b(1, 2);
	b.printResult();
}

 

A는 자신만이 접근한 맴버 변수 a와 b를 가지고 있습니다. 그리고 생성자를 갖고 있는데, 여기서 a와 b를 초기화시켜줍니다. 그리고 누구나 접근 가능한(public) 메소드 add를 갖고 있습니다. 이 메소드는 단순 a와 b를 더한 값을 전달하지요. 맴버를 초기화할 수 있는 방법은 이런 방법도 있습니다. 

public :
	A(int _a, int _b):a(_a),b(_b) {
		
	}

B는 A 클래스를 부모 클래스(또는 Super Class라고 합니다.)로 상속을 받습니다. 이때 부모 클래스의 초기화를 하는 방법은 : 부모클래스명(매개변수명1, 매개변수명2 ...)으로 초기화 시킬 수 있습니다. 자, 여기서 눈여겨 보셔야할 점은 B는 A클래스를 상속받았기 때문에 a와 b는 갖고 있지만, 접근할 수 없습니다. 

printResult라는 메소드에서는 add()라는 메소드를 사용할 수 있는데 A의 클래스에서 정의하고 public으로 접근할 수 있게 만들었기 때문에 B클래스에서 사용할 수 있습니다.

접근 제한자를 통한 상속

우리는 위의 예제에서 접근 제한자는 따로 지정하지 않고 상속을 받았습니다. 상속받는 부분에서 접근 제한자를 지정하게 되면 부모 클래스의 맴버 변수, 메소드를 지정한 접근 제한자보다 범위가 넓은 맴버 변수, 메소드에 접근할 수가 없습니다. public을 명시적으로 지정해주어야만 다형성의 속성을 사용할 수 있습니다.

private 상속 

#include <iostream>
using namespace std;

class A {
private:
	int a;
protected:
	int b;
public:
	int c;
};

class B : private A { //b,c맴버 변수는 private 맴버로 접근 범위 졻혀짐

};


int main() {
	B b;
    //a = private, b = private, c = private
	b.a;
	b.b;
	b.c;
}

 

 

 

B는 A를 private로 상속받습니다. A의 맴버변수 b,c는 private보다 범위가 넓으므로 b,c는 private로 제한합니다. 따라서 a,b,c 모두 접근할 수 없습니다.

protected 상속

#include <iostream>
using namespace std;

class A {
private:
	int a;
protected:
	int b;
public:
	int c;
};

class B : protected A { //c맴버 변수는 protected 맴버로 접근 범위 졻혀짐

};


int main() {
	B b;

	//a = private, b = protected, c = protected
	b.a;
	b.b;
	b.c;
}

A의 맴버 변수 중에 protected보다 범위가 넓은 맴버는 protected로 접근이 제한됩니다.

public 상속

#include <iostream>
using namespace std;

class A {
private:
	int a;
protected:
	int b;
public:
	int c;
};

class B : public A { //맴버 변수의 접근 제한에 변화없음

};


int main() {
	B b;

	//a = private, b = protected, c = public
	b.a;
	b.b;
	b.c;
}

public보다 접근 제한이 넓은 맴버는 public으로 맞춰지는데, 의미없죠? public보다 큰 제한자는 없으니까요. 

 

캡슐화(Encapsulation)

캡슐화라는 것은 맴버 변수를 직접 변경할 수 없도록 캡슐처럼 껍데기를 둘러싸는 과정을 말합니다. 캡슐안에서 특정 로직에 따라 맴버 변수가 적절하게 변경되어야 프로그램이 안전할 수 있기 때문이죠. 아래의 코드가 그 예를 보여줍니다.

#include <iostream>
using namespace std;

class A {
private:
	int a;
	int b;
public:
	void setA(int _a) {
		if (_a > 50)
			_a = 50;
		a = _a;
	}
	void setB(int _b) {
		if (_b > 100)
			_b = 100;
		b = _b;
	}

	int getA() {
		return a;
	}
	int getB() {
		return b;
	}
};

class B : A {
public :
	void setAB(int a,int b) {
		setA(a);
		setB(b);
	}
	void printResult() {
		printf("%d + %d = %d\n", getA(), getB(), getA() + getB());
	}
};


int main() {
	B b;
	b.setAB(100, 200);
	b.printResult();
}

 

간단한 코드인데요. B는 A를 상속받습니다. 이때 a,b는 직접 설정할 수 없게 private로 접근을 제한했구요. 메인 함수에서는 b에서 setAB를 호출해서 a의 값을 100, b의 값을 200으로 바꾸려고 합니다. 하지만 A 클래스는 그것을 용납하지 않습니다. a의 최대값은 50, b의 최대값은 100으로 제한을 해놓았기 때문이죠. B가 a와 b를 변경할 수 있는 수단은 A 클래스의 setA와 setB를 호출하여 인자 전달을 하는 방법밖에 없습니다. 이렇게 맴버의 값을 함부로 변경할 수 없도록 맴버 함수로 껍질을 입히는 작업을 캡슐화라고 합니다. 이렇게 Get, Set 메소드를 Getter, Setter메소드라고 합니다.

객체지향 언어에서 반드시 필요한 속성임을 기억하세요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

링크에 대한 성질, 그리고 이에 대한 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

링크

파일은 서로 참조하고 참조 받을 수 있습니다. 그러니까 어떤 파일이 어떤 파일을 삿대질해서 가리킬 수도 있고(무척 평화적이죠? 현실세계에서는 참교육 시전받을 수 있습니다.) 같은 내용을 바라볼 수도 있다는 뜻입니다. 이러한 성질을 링크라고 합니다. 링크를 본격적으로 설명하기 전에 I-node라는 개념을 먼저 알아야 할 팔요가 있습니다.

1. Inode

Inode는 아래에 보는바와 같이 i-node라는 구조체를 가리키게 됩니다. 이 i-node에는 파일에 대한 거의 모든 정보(파일 모드, 링크개수, UID, GID, 파일 크기, 파일 관련 시간 등)가 담겨 있는데, 내용은 담겨있지 않습니다. 파일의 메타정보라고 생각하시면 됩니다. i-node는 파일의 내용을 가리키는 일종의 포인터를 갖고 있을 뿐이죠. Single indirect, Double indirect,  Triple indirect 등이 보이죠? 이게 다 파일에 대한 내용을 가리키는 블록들이며, 마치 C언어에서 포인터, 더블 포인터 등과 같이 내용을 가리키게 됩니다.

출처 : https://www.systranbox.com/exploring-the-essential-role-of-inodes-in-linux-and-unix-operating-systems/

inode가 갖고 있는 정보는 아래와 같으니 참고하시기 바래요.

inode가 가지고 있는 정보 설명
inode 번호 inode의 고유 식별 번호입니다.
파일 모드 16비트의 플래그로 파일의 실행 권한입니다. 소유자의 권한, 소유자 그룹의 권한, 기타 사용자의 권한, 파일 형식, 실행 플래그 등을 나타냅니다.
링크 수 이 아이노드에 대한 참조 수를 나타냅니다.
소유자 아이디 파일의 소유자 아이디를 나타냅니다.
그룹 아이디 파일 소유자의 그룹 아아디를 나타냅니다.
파일 크기 파일의 크기(bytes)를 나타냅니다.
파일 주소 실 데이터가 나오는 파일 주소를 나타냅니다.
마지막 접근 마지막으로 파일에 접근한 시간을 나타냅니다.
마지막 수정 마지막으로 파일을 수정한 시간을 나타냅니다.
아이노드 수정 마지막으로 아이노드를 수정한 시간을 의미합니다.

I-node는 각자 자신들의 고유한 번호를 가지고 있는데, 이 고유한 번호를 i-node 번호라고 합니다. ls 명령어에서 -i 옵션은 i-node번호를 같이 보여줍니다. 그래서 i-node번호와 같이 자세한 출력은 원하면 ls -li 을 입력하면 됩니다. 왼쪽에 나오는 숫자가 바로 i-node 번호입니다.

# ls -il /usr/lib
total 2952
6291630 drwxr-xr-x 105 root root   86016 Jun  8 06:17 aarch64-linux-gnu
6441489 drwxr-xr-x   2 root root    4096 Apr 18 00:48 apg
6291631 drwxr-xr-x   2 root root    4096 Feb 17 17:30 apparmor
6291632 drwxr-xr-x   5 root root    4096 Feb 17 17:30 apt
6442507 drwxr-xr-x   3 root root   12288 Apr 18 00:48 aspell

실제 inode의 내용을 확인해보고 싶다면 stat 명령을 사용해서 확인해보시기 바랍니다. stat명령도 내부적으로 inode에서 정보를 얻는 구현이 있기 때문입니다. 아래는 단순 빈 파일의 정보입니다.

# stat /etc/passwd
  File: /etc/passwd
  Size: 3212            Blocks: 8          IO Block: 4096   regular file
Device: fd00h/64768d    Inode: 5245840     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2023-08-14 09:04:09.872000001 +0000
Modify: 2023-07-27 17:40:33.552013428 +0000
Change: 2023-07-27 17:40:33.560013428 +0000
 Birth: 2023-07-27 17:40:33.552013428 +0000

같은 I-node를 가리키느냐, 아니면 데이터 블록이 원본 파일을 가리키느냐에 따라서 하드링크(Hard link)와 심볼릭(Symbolic link)로 나뉘어집니다. 파일에 링크를 걸어주는 명령이 ln 명령어입니다. ln 명령어를 통해서 하드링크와 심볼릭 링크가 무엇인지 확인해봅시다.

       ln [OPTION]... [-T] TARGET LINK_NAME
       ln [OPTION]... TARGET
       ln [OPTION]... TARGET... DIRECTORY
       ln [OPTION]... -t DIRECTORY TARGET...

 

2. 하드링크

하드링크는 파일이 같은 i-node를 가리켜 파일의 내용을 공유하는 링크 방식을 의미합니다. 하드 링크는 같은 i-node를 가리키기 때문에 i-node 번호가 같습니다. ln 명령어로 하드링크를 걸어봅시다. 하드 링크를 거는 방법은 아래와 같은 명령어 형식을 사용하면 됩니다. 

ln 원본파일 링크파일

우선 아래와 같이 문자열을 담고 있는 origin이라는 파일을 하나 생성하고, i-node 번호(1열)를 확인하면 464235라는 것을 확인할 수 있네요. 파일을 최초로 만들었을 땐 링크수(3열)가 1이라는 점! 

# echo "hard link test" > origin
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 15 Jun 14 01:35 origin

이후 ln 명령어를 통해서 하드링크를 걸어줍니다. I-node와 링크수를 확인해보니, i-node는 464235로 같고 링크수가 2로 하나 증가함을 알 수 있습니다. 그 외의 파일의 권한, 소유자, 시간 모두 정확히 같습니다. 그렇죠?

# ln origin  hardlink
# ls -il
total 8
464235 -----w--w- 2 root ubuntu 15 Jun 14 01:35 hardlink
464235 -----w--w- 2 root ubuntu 15 Jun 14 01:35 origin

그렇다면 우리는 이런 그림을 그려볼 수 있습니다. 링크수는 i-node를 가리키는 화살표의 갯수를 의미하며, 두 파일은 i-node를 같이 공유하고 있음을 알 수 있습니다.

2.1 하드링크의 원본 파일 삭제 시

그렇다면 만약 어느 하나의 파일을 삭제하게 된다면 어떻게 될까요? 여기서는 원본의 파일을 삭제해보도록 하겠습니다. 

# rm origin 
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 15 Jun 14 01:35 hardlink
# cat hardlink 
hard link test

결과에서 알 수 있듯이 i-node의 번호는 변하지 않고, 링크의 갯수가 하나 줄었습니다. 내용을 보면 심지어 내용도 보존이 됩니다. 원본 파일을 삭제했는데도 말이죠. 이런 상황입니다. 

그래서 파일을 삭제한다는 뜻은 링크를 제거한다는 것과 유사합니다. 

 

3. 심볼릭 링크(Symbolic Link)

심볼릭 링크는 ln 명령어에 -s 옵션을 추가하여 링크를 걸 수 있습니다. 위와 같은 과정으로 테스트를 해보면 하드링크와 다른점을 확인할 수 있습니다.

# echo "symbolic link test" > origin
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 19 Jun 14 01:54 origin
# ln -s origin symlink
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 19 Jun 14 01:54 origin
464240 lrwxrwxrwx 1 root ubuntu  6 Jun 14 01:55 symlink -> origin

하드링크와의 차이점을 정리해보면 이렇습니다.

  • origin의 i-node 번호(1열)와 symlink 파일의 i-node번호가 다르다.
  • 링크의 갯수(3열)가 변하지 않는다.
  • 다른 기타 정보(권한, 크기, 시간 정보 등)이 다르다.
  • 파일의 유형을 보면 정규 파일('-')이 아니라 링크('l')이다.

이 상태에서 원본 파일과 심볼릭 링크 파일의 관계를 그리면요. 아래와 같습니다. 

이 그림이 성립이 된다는 것을 확인시켜드리고자 이 상태에서 원본 파일을 과감하게 지우겠습니다. 

# rm origin 
# ls -il
total 0
464240 lrwxrwxrwx 1 root ubuntu 6 Jun 14 01:55 symlink -> origin
# cat symlink 
cat: symlink: No such file or directory

원본 파일인 origin을 삭제하고 심볼릭 링크를 읽으려해보면 그러한 파일을 찾을 수 없다는 에러 메시지를 볼 수 있습니다.  만약 다시 origin을 생성해보면 어떨까요?

# echo "new origin" > origin
# ls -il
total 4
464235 -----w--w- 1 root ubuntu 11 Jun 14 06:59 origin
464240 lrwxrwxrwx 1 root ubuntu  6 Jun 14 01:55 symlink -> origin
# cat symlink 
new origin

이러한 경우에는 다시 symlink로 파일을 읽을 수 있습니다. 다만 그 내용은 원래 이전의 origin의 내용이 아니라 다시 생성한 origin 파일의 내용인 것을 알 수 있습니다. 결국은 심볼릭 링크의 경우에 파일을 가리킨다는 것을 확인할 수 있습니다. 

4. ln 명령어 구현 

간단한 ln 명령어를 구현해보도록 하겠습니다. 그전에 우리가 알아야할 관현 함수들은 symlink와 linkat 함수입니다. 

  • symlink
#include <unistd.h>
int symlink(const char *target, const char *linkpath);

심볼릭 링크를 거는 시스템 콜입니다. target은 현재 프로세스 위치의 파일 이름입니다. 상대 경로일 수 있습니다. linkpath는 링크 파일의 이름입니다. 이때 이 파일이 존재하면 에러 -1이 발생합니다. 

  • linkat
#include <unistd.h>
int linkat(int olddirfd, const char *oldpath,
			int newdirfd, const char *newpath, int flags);

하드 링크, 심볼릭 링크 둘 다 걸 수 있는 시스템 콜입니다. linkat 함수는 조금 까다로울 수 있는데, newpath가 oldpath를 가리키도록 링크를 걸게 됩니다.

olddirfd와 oldpath, newdirfd와 newpath가 한 쌍으로 파일을 나타냅니다. oldpath가 절대 경로일 경우 olddirfd는 무시가 되고, oldpath가 상대 경로일 경우 olddirfd 기준의 상대경로가 됩니다. newdirfd와 newpath와의 관계도 마찬가지입니다. 함수를 호출한 프로세스를 기준으로 상대위치로 만들고 싶다면 fd인자에 AT_FDCWD 상수를 사용해야합니다.

flag는 AT_SYMLINK_FOLLOW를 사용할 수 있습니다. 만약 이 flag를 쓰게 되면 원본 파일이 심볼릭 링크라면 링크를 따라가서 하드 링크를 겁니다. 원본 파일이라니까 헷갈리실거 같은데 ln 명령어에서 첫번째 인수를 말해요. 이 flag를 쓰지 않으면 원본 파일인 심볼릭 링크를 따라가 심볼릭 링크를 겁니다. 

ln 원본파일 링크파일

즉, 정리하면

  • AT_SYMLINK_FOLLOW 를 사용할 경우 : symlink를 계속 따라가서 harlink
  • AT_SYMLINK_FOLLOW를 사용하지 않은 경우 : symlink를 계속 따라가서 symlink

 

4.1 소스코드 

이제 간단히 구현한 myln.c 코드를 보겠습니다.

//myln.c

#include <unistd.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

// -s 옵션 
bool symbolic = false;
// -L 옵션
bool logical = false;

int main(int argc, char *argv[]){

        int c;
        char **files_name;
        char *src, *dst;

        //옵션 파싱
        while((c = getopt(argc, argv, "sL")) != -1 ){
                switch(c){
                        case 's':
                                symbolic = true;
                                break;
                        case 'L':
                                logical = true;
                                break;
                }
        }

        files_name = argv + optind;

        //원본 파일
        src = files_name[0];
        //링크 파일
        dst = files_name[1];

        //심볼릭 링크시에 symlink 사용
        if(symbolic){
                return symlink(src, dst);
        }

        //심볼릭 링크가 아니면 -L 옵션 여부에 따라 AT_SYMLINK_FOLLOW 플래그 설정
        return linkat(AT_FDCWD, src, AT_FDCWD, dst, logical ? AT_SYMLINK_FOLLOW : 0);

}

linkat에 AT_FDCWD를 사용한 것을 보시기 바랍니다. AT_FDCWD는 현재 싱행되는 프로그램의 위치를 기준삼아 src를 찾고, dst링크 파일을 만듭니다. 위 코드는 실제 ln의 코드를 풀어 쓴 코드입니다. 아래는 실제 ln의 소스 코드 일부분입니다.

static bool
do_link (const char *source, const char *dest)
{
 ...
   ok = ((symbolic_link ? symlink (source, dest)
         : linkat (AT_FDCWD, source, AT_FDCWD, dest,
                   logical ? AT_SYMLINK_FOLLOW : 0))
        == 0);
 
 ...
 }

 

이제 코드 결과를 확인해봅시다. 

# gcc myln.c 
# echo "this is origin" > origin
# ./a.out -s origin symlink1
# ./a.out origin hardlink1
# ls -il
total 24
464557 -rwxr-xr-x 1 root root 9088 Aug 15 05:59 a.out
464558 -rw-r--r-- 2 root root   15 Aug 15 05:59 hardlink1
464648 -rw-r--r-- 1 root root 1121 Aug 15 05:52 myln.c
464558 -rw-r--r-- 2 root root   15 Aug 15 05:59 origin
464646 lrwxrwxrwx 1 root root    6 Aug 15 05:59 symlink1 -> origin

우선 원본 파일인 origin을 생성하고 심볼릭 링크와 하드링크를 하나씩 걸었습니다. symlink1은 심볼릭 링크로 잘 링크되어있고, hardlink1도 inode를 보니 origin과 같아서 하드링크가 잘 걸려있네요!

-L옵션도 확인해볼까요? 

# ./a.out -L symlink1 hardlink2
# ./a.out symlink1 symlink2
# ls -il
total 28
464557 -rwxr-xr-x 1 root root 9088 Aug 15 05:59 a.out
464558 -rw-r--r-- 3 root root   15 Aug 15 05:59 hardlink1
464558 -rw-r--r-- 3 root root   15 Aug 15 05:59 hardlink2
464648 -rw-r--r-- 1 root root 1121 Aug 15 05:52 myln.c
464558 -rw-r--r-- 3 root root   15 Aug 15 05:59 origin
464646 lrwxrwxrwx 2 root root    6 Aug 15 05:59 symlink1 -> origin
464646 lrwxrwxrwx 2 root root    6 Aug 15 05:59 symlink2 -> origin

-L옵션은 linkat 콜 인자 중 flag에 AT_SYMLINK_FOLLOW를 전달한 것과 같습니다. 그러니까 -L 옵션을 줄 경우 원본 파일은 symlink1을 따라가서 최종 원본 파일을 찾고 거기에 하드링크를 걸게 됩니다. -L 옵션을 뺀 경우, 그러니까 AT_SYMLINK_FOLLOW옵션을 주지 않았을 경우 링크를 계속 따라가서 최종 원본 파일에 심볼릭 링크를 걸게 되지요. 

여기까지 리눅스의 i-node가 대한 간단한 소개와 link의 방식인 hard link, symbolic link의 개념과 차이점을 확인해보았으며 직접 ln 명령어를 맛보기로 구현해보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,