동기화(Synchronization)

동기화는 특정 객체를 동시에 접근하여 변경하기를 막는 기법입니다. 가장 빈번하게 사용되는 예를 들어볼까요? 은행 입출금과 화장실의 예가 있는데 저는 화장실이 더 좋더군요.

 

우리가 고깃집에서 소주와 맥주를 거나하게 마셔서 화장실을 갈때 열쇠가 있지요? 여기서 화장실은 한개이고 열쇠 또한 한개라고 칩시다. 열쇠를 갖고 화장실에서 볼일을 보려고 문을 잠글겁니다. 문을 잠그고 나서는 각자 취향대로 볼일을 보고 물을 내리고 열쇠를 제자리에 걸어놓습니다.  화장실의 키를 갖고 열고 잠그는 것이 일종의 동기화입니다.

여러분들은 알게 모르게 동기화를 실천하고 있는 것이지요.

 

만약 이 과정에서 동기화가 빠지게 된다면 어떻게 될까요? 제가 화장실을 쓰고 있는데, 다른 사람이 와서 제 무릎에다가 지리는 현상이 발생하겠죠. 

 

이렇듯 멀티쓰레드(Multithread) 환경에서 어떤 쓰레드(사람)가 자원(화장실 열쇠)을 이용하는데 동시에 그 자원을 사용하면 안될때 동기화가 필요합니다.

 

자바에서 이러한 동기화 기법을 적용하는 방법은 두가지가 있습니다.

첫번째로는 특정 객체의 메소드에 synchronized 예약어를 사용하는 방법과 synchronized lock을 사용하는 방법입니다.

 

synchronized 사용하여 동기화

우선 동기화를 걸지 않은 코드를 먼저 봅시다. key라는 객체는 원래 HAS-A 관계가 적절하지만 이해를 돕기위해서 그런건 고려하지 않았습니다. 코드는 그렇게 어렵지 않습니다. 

쓰레드 3개를 생성하고 각각 알기쉽게 사람 이름을 붙여줍니다. 이 쓰레드는 이제 사람이라고 치고 key를 사용하여 볼일을 본다면 어떻게 될까요?

 

 

 

 

class Key {
	
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public synchronized void useToilet(String name){
		
		open(name);
		defecate(name);
		close(name);
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key(); //이 객체를 쓰레드 3개에서 사용
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

아래 결과처럼 철수가 싸는 중에 영희가 철수 무릎위에 볼일을 보고, 그 다음 영철이가 들어와서 또 한번 철수 무릎에 볼일을 봅니다. 그 다음 철수가 볼일을 보네요.

철수이(가) 화장실 문을 연다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
철수이(가) 싼다.
철수이(가) 화장실 문을 닫는다.

 

철수를 위해 동기화를 할 필요가 있겠죠? 가장 간단한 방법은 동기화할 메소드에 synchronized 만 추가하면 됩니다. 

useKey메소드를 다음과 같이 변경합시다.

public synchronized void useKey(String name){
	open(name);
	defecate(name);
	close(name);
}

그리고 실행합시다.

철수이(가) 화장실 문을 연다.
철수이(가) 싼다.
철수이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.

저희는 이렇게 철수를 도울 수 있습니다.

 

Synchronized Block으로 원하는 코드부분만 동기화

Synchronized로 동기화를 거는 건 무척 쉽네요. 하지만 그 메소드 전부를 동기화하기 때문에 불필요한 곳까지 동시에 실행할 수가 없습니다.

다음의 예를 보세요. 위의 코드에서 몇줄밖에 추가하지 않았습니다.

class Key {
	
	public void lookIntoAMirror(String name){
		System.out.println(name+"이(가) 거울을 본다.");
	}
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public synchronized void useToilet(String name){
		lookIntoAMirror(name);
		open(name);
		defecate(name);
		close(name);
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key();
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

거울을 보는것까지 동기화를 걸었네요. 우리는 그럴 필요없습니다. 거울은 누가 먼저보든 상관없으니까요. 볼일 보는 것만 정확히 동기화를 하려고 한다면 block을 이용해야합니다.

 

 

 

 

아래와 같이 원하는 부분만을 동기화할 수 있습니다. 

class Key {
	
	public void lookIntoAMirror(String name){
		System.out.println(name+"이(가) 거울을 본다.");
	}
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public void useToilet(String name){
		lookIntoAMirror(name);
		synchronized(this){
			open(name);
			defecate(name);
			close(name);
		}
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key();
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

거울은 순서없이 먼저 들어온 사람이 봅니다.

철수이(가) 거울을 본다.
영희이(가) 거울을 본다.
철수이(가) 화장실 문을 연다.
철수이(가) 싼다.
영철이(가) 거울을 본다.
철수이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.

 

synchronized block의 인자는 사실 잠글(lock 시킬) 객체를 말합니다. this이니까 바로 key객체 자신이겠죠.

lock 시킬 객체를 한번 바꿔서 실행하도록 하지요.

 

1) 아래는 this(Counter객체)를 통해서 lock을 시켰습니다. 

class Counter{
	public Integer cnt=0;
	
	public void increase(String threadName){
		synchronized(this){ 
			for(int i=0;i<10;i++)
				System.out.println(threadName+", cnt:"+(cnt++));
			System.out.println();
		}
	}
	
}
class MyThread extends Thread{
	private String threadName;
	private Counter counter;
	public MyThread(String threadName,Counter counter){
		this.threadName=threadName;
		this.counter=counter;
	}
	public void run(){
		counter.increase(threadName);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Counter counter=new Counter();
		MyThread thread1=new MyThread("thread1",counter);
		MyThread thread2=new MyThread("thread2",counter);
		
		thread1.start();
		thread2.start();
		
		thread1.join();
		thread2.join();
		System.out.println(counter.cnt);
	}
	
}

 

 

2) 다음의 코드는 cnt객체로 lock을 시켰습니다.

public void increase(String threadName){
  synchronized(cnt){
  	for(int i=0;i<10;i++)
  		System.out.println(threadName+", cnt:"+(cnt++));
  		System.out.println();
	}
}

 

3) 마지막으로 동기화를 걸지 않은 코드입니다.

public void increase(String threadName){
	
	for(int i=0;i<10;i++)
		System.out.println(threadName+", cnt:"+(cnt++));
	System.out.println();
	
}

 

1)의 결과는 thread1의 increase, thread2의 increase 둘 중 먼저 실행되는 것이 끝난 이후에 다른 쓰레드의 메소드가 실행되며 마지막 결과도 20으로 항상 동일합니다. this로 lock을 걸었기 때문에 다른 쓰레드의 increase가 끝난 이후에 다른 쓰레드의 increase가 실행됩니다. 

 

2)의 결과는 thread1의 increase, thread2의 increase 둘 중 무작위로 실행되며 실행이 끝난 이후 마지막 결과가 20으로 항상 동일합니다. 이 경우 cnt로 lock을 걸었기 때문에 cnt가 쓰이지 않는 순간(cnt++가 되지 않는 찰나의 순간) 다른 쓰레드가 cnt++을 실행할 수 있습니다.

 

3)의 결과는 동기화를 걸지 않았기 때문에 결과가 매 수행마다 다를 수 있습니다.

 

보통은 1)의 방법으로 동기화를 많이 구현하게 됩니다.

 

각각의 결과를 비교해보시기 바랍니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

쓰레드(Thread)

쓰레드(Thread)는 간단히 정의하면 하나의 프로세스(실행중인 프로그램이라고 합니다.)에서 독립접으로 실행되는 하나의 일, 또는 작업의 단위를 말합니다. 뭐, 더 간단히 말해 쓰레드를 여러개 사용하면 동시에 여러 작업을 할 수 있는 뜻이 되는 것이죠. 

우리가 익히 알고 있는 main 함수 역시 쓰레드입니다. 프로그램 실행 시 실행되는 첫번째 쓰레드이기 때문에 main함수를 우리는 메인 쓰레드라고도 합니다.

 

쓰레드를 사용해서 얻을 수 있는 이점이 있을 텐데 어떤 이점들이 있을까요?

 

  • 우선 가장 두드러진 장점은 바로 동시성입니다. 동시에 여러 일들을 할 수 있습니다. 그렇기 때문에 작업의 효율성을 높일 수 있습니다.
  • 쓰레드끼리 메모리를 공유합니다. 그렇기 때문에 메모리가 절약되는 효과를 볼 수 있습니다. 경제적인 것이죠.

그렇다고 해서 장점만 있는 것은 아닙니다. 아래와 같은 상황을 고려해야하죠.

 

  • 프로그램의 실행 단위가 많아지면 프로그램이 상당히 복잡해질 수 있습니다.
  • 예상치 못한 버그가 생길 수 있습니다. 이를 위해서 적절히 동기화를 걸어주어야 하지요.
  • 교착상태나 기아상태로 빠질 수 있습니다.

 

 

 

자바 쓰레드 사용

자바에서는 대표적인 두 가지 쓰레드를 생성하는 방법이 있는데요. 아래의 두 가지 방법을 활용하여 쓰레드를 생성해봅시다.

 

1. Thread 클래스를 상속받는 방법

2. Runnable 인터페이스를 구현하는 방법

 

1. Thread 클래스를 상속받는 방법

아래의 코드처럼 Thread 클래스를 상속합니다.

실행되는 각 쓰레드를 구별 짓기 위해서 쓰레드 이름을 저장하고 단지 for문에서 100까지 도는 아주 간단한 쓰레드입니다. 주의 해야할 것은 쓰레드의 실제 override하는 메소드는 run이지만, 쓰레드 호출 시 실행하는 메소드는 start라는 점을 주의하시기 바랍니다. 

class MyThread extends Thread{
	
	public MyThread(String threadName){
		super(threadName);
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(this.getName()+":"+i);
		}
		System.out.println();
	}
}
public class ThreadTest {

	public static void main(String[] ar){
		System.out.println("MainThread Start");
		for(int i=1;i<=3;i++){
			new MyThread("Thread"+i).start();
		}
		System.out.println("MainThread End");	
	}
	
}

 

위의 코드에서 프로그램을 예상할 수 있나요? 평소의 프로그램(싱글쓰레드)같았다면 우리는 이렇게 예상을 했을 겁니다. 

MainThread Start

Thread1:0

Thread1:1

Thread1:2

Thread1:3

...

Thread2:0

Thread2:1

Thread3:2

...

Thread3:98

Thread3:99

MainThread End

하지만 우리는 동시성을 원하니까 쓰레드를 돌렸지요. 그래서 위의 결과가 될 수도 있고(진짜 운이 드럽게 좋다거나 없다면) 아닐 수도 있습니다. 

대부분의 결과는 아래와 같이 제 얼굴처럼 뒤죽박죽된 결과가 발생합니다. 아래 결과는 실제 제 똥컴에서 돌린 결과입니다.

MainThread Start 
MainThread End 
Thread1:0 
Thread1:1 
Thread1:2 
Thread1:3

...

Thread1:59 
Thread1:60 
Thread2:0 
Thread2:1

...

Thread2:98 
Thread2:99 

Thread1:61 
Thread3:0 
Thread3:1

...

Thread1:97 
Thread1:98 
Thread1:99

 

쓰레드의 실행 순서는 이렇게 예측할 수 없습니다.

 

 

 

2. Runnable 인터페이스 구현

실제로 쓰레드를 생성할때 많이 사용하는 방법입니다. Runnable 인터페이스를 구현한 클래스를 Thread의 생성자로 주입하여 실행하는 방법이죠. 아래의 코드를 봅시다. 

 

달라진게 있다고 한다면 implements Runnable 쪽인것 같네요. 이렇게 구현한 MyThread라는 클래스를 객체화하여 main 메소드에서 실행시키고 있습니다.

Thread라는 클래스의 생성자로 Runnable 객체를 전달하고 있습니다.

Thread thread=new Thread(new MyThread("Thread"+i));
thread.start();

 

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar){
		System.out.println("MainThread Start");
		for(int i=1;i<=3;i++){
			Thread thread=new Thread(new MyThread("Thread"+i));
			thread.start();
		}
		System.out.println("MainThread End");	
	}
	
}

 

결과는 1번의 Thread 클래스를 상속받는 방법과 비슷하게 순서를 예측할 수 없이 실행됩니다.

MainThread Start 
MainThread End 
Thread2:0

...

Thread1:62 
Thread2:93 
Thread2:94

...

Thread3:97 
Thread3:98 
Thread3:99

 

저는 항상 Main 쓰레드가 먼저 끝나버리는군요. 

이점에 대해서 불만을 가지고 있는데요. 메인 쓰레드는 항상 다른 쓰레드를 기다렸다가 종료할 수 없을까요?? 자기 할일 끝났다고 먼저 가버리는 것은 조금 매너가 없으니까요. 

 

그래서 join이라는 쓰레드 메소드가 존재합니다.

 

join

join이라는 메소드를 통해서 분기를 어떤 지점에 합칠 수 있습니다. 그러니까 쓰레드를 생성한 쓰레드는 그 지점에서 기다려야합니다. 아래의 코드를 통해서 알아보도록 합시다.

 

알아보려면 코드를 조금 변경해야하는데, 우선 join을 쓰게 되면 InterruptedException이 발생합니다. 처리하기 귀찮으니 throws를 통해서 그냥 던져줍시다. 누군가 알아서 먹든가 하겠죠.

아래 코드처럼 변경해서 실행하면 메인쓰레드는 자신이 생성한 3개의 쓰레드를 끝날때까지 기다렸다가 자신도 종료하게 됩니다.

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar) throws InterruptedException{
		System.out.println("MainThread Start");
		Thread[] thread=new Thread[4];
		for(int i=1;i<=3;i++){
			thread[i]=new Thread(new MyThread("Thread"+i));
			thread[i].start();
		}
		
		for(int i=1;i<=3;i++)
			thread[i].join();
		
		System.out.println("MainThread End");	
	}
	
}

 아래의 실행결과처럼 메인쓰레드는 나머지 3개가 종료할때까지 기다립니다.

MainThread Start 
Thread1:0 
Thread1:1

...

Thread3:99

MainThread End

왜 join으로 이름을 정했을까요? 아래의 그림을 보면 이해하기가 쉬울 겁니다. 위 코드의 상황을 그림으로 옮겨놓았습니다.

main은 thread1, thread2, thread3을 수행시킵니다. 이때 thread1, thread2, thread3이 끝나면 다시 자신을 실행시키는 main와 합쳐지지요. 이해하기 쉽죠?

 

 

아 이것도 불만이다. 난 쓰레드를 썼지만 순차적으로 실행하겠다! thread1끝나면 thread2실행하고 thread2끝나면 thread3실행하겠다 하시는 분들은 thread 실행시키자 마자 바로 join거시면 됩니다. 

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar) throws InterruptedException{
		System.out.println("MainThread Start");
		Thread[] thread=new Thread[4];
		for(int i=1;i<=3;i++){
			thread[i]=new Thread(new MyThread("Thread"+i));
			thread[i].start();
			thread[i].join();
		}
		
		
		System.out.println("MainThread End");	
	}
	
}

 

아래의 그림에서 점선이 join하는 구간입니다.

 

실행결과는 뭐.. 순차적입니다.

MainThread Start 
Thread1:0 
Thread1:1

...

Thread3:99

MainThread End

 

 

근데 이렇게 구현할 거면 쓰레드 안쓰는 것이 낫죠. 단지 join을 어떻게 사용하는지 보여드린겁니다.

 

간단하게 자바에서 쓰레드를 사용하는 방법을 알아보았습니다. 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

HashSet

자바 Collection 중 Set의 대표적인 HashSet 클래스를 다루어 보도록 하겠습니다. HashSet은 Set의 파생클래스로 Set은 기본적으로 집합으로 중복된 원소를 허용하지 않습니다. HashSet은 순서 역시 고려가 되지 않습니다. 그렇다면 다음의 예제로 HashSet의 기본적인 동작을 살펴보도록 하겠습니다.

 

public static void main(String[] args){
	Set hashSet=new HashSet();
	hashSet.add("F");
	hashSet.add("B");
	hashSet.add("D");
	hashSet.add("A");
	hashSet.add("C");
	
	/* 위와 같은 데이터들을 다시 add */
	hashSet.add("F");
	hashSet.add("B");
	hashSet.add("D");
	hashSet.add("A");
	hashSet.add("C");
	
	/* HasSet의 "C"라는 원소 삭제 */
	hashSet.remove("C");
	
	/* HashSet 모든 원소 출력 */
	System.out.println("HashSet 원소 출력");
	Iterator it=hashSet.iterator();
	while(it.hasNext()){
		System.out.print(it.next()+" ");
	}
	
	/* HashSet의 모든 원소를 ArrayList로 전달 */
	List arrayList=new ArrayList();
	arrayList.addAll(hashSet);
	
	/* ArrayList의 모든 원소 출력 */
	System.out.println();
	System.out.println("ArrayList 원소 출력");
	for(int i=0;i<arrayList.size();i++){
		System.out.print(arrayList.get(i)+" ");
	}
}
 

 

우선 HashSet에 "F", "B", "D", "A", "C"라는 문자열을 차례대로 넣었습니다. 그리고 나서 다시 같은 데이터들을 넣게 되죠.  Set에는 중복을 허용하지 않는다고 하였으니 Set에는 현재 { "F", "B", "D", "A", "C" }가 존재합니다.

 

그 후에 "C"라는 문자열을 지우는 군요. 뭐 쉽습니다. "C"라는 원소를 삭제했으니 { "F", "B", "D", "A"}만이 남는것을 알 수 있겠군요. HashSet을 출력하는 부분을 보고 확인해보세요.

아, HashSet에는 순서가 없으므로 Iterator를 사용해서 집합안의 원소를 출력하고 있습니다. Iterator의 메소드는 3개가 다인데요.

hasNext() : 다음 원소가 남아있는지 여부를 알아냅니다. 다음 원소가 남았다면 true를 반환합니다.

next() : 다음 원소를 가져옵니다. 

remove() : 현재 반복자가 가리키고 있는 원소를 삭제합니다.

 

장난삼아서 ArrayList에 HashSet의 원소를 모두 전달해서 확인해봤습니다. 아래는 그 결과입니다.

HashSet 원소 출력
A B D F 
ArrayList 원소 출력
A B D F 

 

이제 우리는 단순히 String 타입의 자료형이 아닌 우리가 정의한 클래스의 객체를 HashSet의 원소에 넣고 싶습니다. 그렇다면 어떻게 중복된 원소인지 아닌지를 확인할 수 있을까요?

위에서 String 객체를 HashSet은 어떻게 중복된 원소라고 감지했을까요? HashSet이 알아서 판단해 줄까요?

 

우리가 다음과 같은 Person 클래스가 있다고 합시다.

 

class Person{
	String name; //이름
	int residentNumber; //주민번호
}

 

사람이 같은지 여부를 판별하는 것은 주민등록번호를 보고 알 수 있지요? 허나 Set에 단순히 이 Person 클래스를 넘겨준다면 중복 여부를 판별하지 못할 겁니다. Set이 Person 객체의 어떤 메소드를 호출해서 같은지 말지를 판변할 수 있으니까요. 그 메소드가 바로 equals와 hashCode입니다.

 

1. equals(Object o)

만약 두 객체(현재 이 객체와 인자로 넘어온 객체 o)가 같다는 것을 알려주려면 equals 메소드에서 true를 반환해주어야합니다.

 

2. hashCode()

만약 equals에서 두 객체가 같다라고 true를 반환했다면 hashCode는 두 객체에서 항상 같은 값을 반환해야합니다. 만일 equals에서 false를 반환하여 같지 않다고 반환했다면 hashCode 역시 다르게 반환하는 것이 좋습니다.

 

위의 조건을 고려해서 구현한 Person 클래스를 다시 봅시다.

class Person{
	String name; //이름
	int residentNumber; //주민번호
	
	public Person(String name,int residentNumber){
		this.name=name;
		this.residentNumber=residentNumber;
	}
	@Override
	public int hashCode(){
		/* Objects.hash 메소드로 residentNumber의 해쉬값 반환 */
		return Objects.hash(residentNumber);
	}
	
	@Override
	public boolean equals(Object o){
		/* 주민 번호가 같은 Person은 true 반환 */
		Person p=(Person)o;
		return p.residentNumber==this.residentNumber;
	}
}

 

equals와 hashCode를 Override한 코드를 확인해봅시다.

equals에서는 단순히 residentNumber만 비교해서 같다면 true, 다르다면 false입니다.

hashCode에서는 Objects.hash 메소드로 residentNumber의 해쉬값을 반환합니다. 이 hash는 residentNumber가 다르다면 항상 다른 값이 반환됩니다.

 

 

이제 Set에 원소를 넣고 확인해보도록 하지요.

 

public static void main(String[] args){
	
	Set hashSet=new HashSet();
	hashSet.add(new Person("reakwon",111111));
	hashSet.add(new Person("KDC",222222));
	hashSet.add(new Person("KSG",333333));
	hashSet.add(new Person("reakwon",111112));
	hashSet.add(new Person("MJW",111111));
	
	Iterator it=hashSet.iterator();
	while(it.hasNext()){
		Person p=it.next();
		System.out.println(p.name+"/"+p.residentNumber);
	}
}

 

동명이인이 있군요. 주민 번호 111111인 reakwon과 주민 번호 111112인 reakwon인 객체들이네요. 우리는 이름이 같은것은 같은 객체로 보지 않습니다. 주민 번호가 같은 객체만 중복된 원소로 보는 것이죠.

그래서 이 두 객체는 집합에 추가가 됩니다.

 

하지만 이름이 주민번호가 111111의 "MJW"라는 객체와 주민번호 111111의 "reakwon"라는 객체는 같습니다. 주민번호가 같기때문입니다. 그래서 "MJW"라는 Person객체는 집합이 추가하지 않습니다.

 

그 집합의 모든 원소를 출력한 결과는 아래와 같습니다.

KSG/333333 
reakwon/111112 
reakwon/111111 
KDC/222222

 

이제 우리가 만든 객체들도 HashSet에 추가할 수 있습니다.

 

HashSet을 알아보았으니 다음은 HashMap에 대해서 알아볼 차례겠군요. 아래의 링크로 들어가서 개념과 사용법을 익혀보도록 합시다.

reakwon.tistory.com/151

 

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

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

reakwon.tistory.com

반응형
블로그 이미지

REAKWON

와나진짜

,

컬렉션(Collection)

이름에서도 알 수 있듯이 자료들을 효율적으로 모을 수 있는 자료구조를 의미합니다. Collection의 상속구조는 아래의 그림과 같습니다.

 

주황색 상자는 인터페이스, 파란색 상자는 클래스를 의미하며 파란색 화살표는 extends, 녹색 화살표는 implements의 관계를 나타냅니다.

 

Collection이라는 인터페이스는 Collection 계층의 최상위 인터페이스입니다. Iterable이라는 인터페이스를 상속했다는 것을 알 수 있고, Map은 Collection 인터페이스를 상속하지는 않지만 자바의 JCF(Java Collections Framework)에 포함됩니다. 

 

 

Collection 인터페이스의 메소드들을 간단히 살펴보도록 하겠습니다. 별로 어려운 메소드는 없을 것 같습니다.

boolean add(E e)

요소를 추가합니다.

boolean addAll(Collection<? extends E> c)

Collection 타입의 매개변수에 있는 모든 요소를 추가합니다.

void clear()

Collection의 모든 요소를 지웁니다.

boolean contains(Object o)

인자 o가 이 Collection에 속한다면 true를 반환하고 없다면 false를 반환합니다.

boolean containsAll(Collection<?> c)

Collection c의 원소들이 이 Collection에 모두 존재한다면 true, 그렇지 않으면 false를 반환합니다.

boolean equals(Object o)

o와 같은 객체인지 아닌지 확인합니다.

int hashCode()

이 Collection의 해쉬코드를 반환합니다.

boolean isEmpty()

이 Collection이 비어있는지 확인합니다.

Iterator<E> iterator()

이 Collection의 반복자(Iterator)를 반환합니다. Set같은 Collection은 순서를 고려하지 않기때문에 iterator로 원소를 순회합니다. Iterator의 대표적인 메소드는 hasNext와 next가 있습니다.

boolean remove(Object o)

o와 같은 원소가 이 Collection에 존재한다면 삭제합니다.

boolean removeAll(Collection<?> c)

이 Collection에 Collection c를 모두 제거합니다.

boolean retainAll(Collection<?> c)

매개변수 c의 요소들만을 남겨둡니다.

int size()

원소의 사이즈를 반환합니다.

Object[] toArray()

이 Collection의 모든 요소를 포함하는 배열로 반환합니다.

<T> T[] toArray(T[] a)

지정한 타입의 배열로 변환합니다.

 

 

이제 Collection을 상속받는 List, Set, Queue와 Map에 관한 간단한 설명을 시작하도록 하겠습니다. 한 번더 이야기하자면 아래의 Collection은 전부 인터페이스로 스스로 객체생성이 불가능하다는 것을 알아두세요.

1. List

순서가 중요한 Collection입니다. 순서가 있는 데이터의 집합으로 데이터의 중복을 허용합니다. 기존의 배열과는 다르게 크기가 동적으로 변합니다.

 

파생클래스

Vector, ArrayList : 두 클래스는 같이 설명하겠습니다. 우선 기본적인 동작은 원소를 추가, 삭제하는 것은 둘 다 비슷합니다. 하지만 Vector는 동기화 처리가되어있습니다. 즉, 하나의 스레드만이 Vector에 요소를 추가하거나 삭제가 가능하지요. 스레드에 대해서 안전하지만 느리다는 단점이 있습니다. 하지만 ArrayList는 동기화가 되어있지 않습니다. 그렇기 스레드에 대해 안전하지 않습니다. 하지만 추가, 삭제가 빠르다는 장점이 있습니다.

단일쓰레드 환경에서 개발할때에는 ArrayList를 쓰는것이 바람직하다고 할 수 있습니다.

LinkedList : 자료구조를 배웠다면 이해가 수월할 것인데, 간단히 말하면 각각의 요소는 다음 요소를 가리키고 있어 추가, 삭제 연산이 빠릅니다.

 

 

2. Set

순서가 없는 데이터의 집합으로 데이터의 중복을 허용하지 않습니다. 우리가 중딩시절에 배운 집합을 떠올리면 될텐데요. 참고로 저는 집합이 나오고 수학을 포기했습니다.

 

파생클래스

HashSet : 내부적으로 해싱을 이용해서 구현된 클래스입니다. Set 파생클래스에서 가장 성능이 우수합니다.

TreeSet : 내부적으로 레드-블랙 트리 방식으로 구현된 클래스입니다. 레드- 블랙 트리는 이진 탐색 트리의 일종으로 log(n)의 속도로 삽입, 삭제, 검색이 가능합니다. HashSet보다는 성능이 느립니다.

 

3. Queue

아마 너무나도 잘 알고 있을 Queue는 기본적으로 선입 선출(First-In First-Out) 형식의 자료구조입니다. 먼저 들어온 원소가 먼저 나간다는 Collection입니다.

 

파생클래스

PriorityQueue : 들어온 순서가 아니라 우선순위 별로 Queue에서 원소를 꺼내옵니다. 우선순위는 Comparable 인터페이스로 정할 수 있습니다.

ArrayDeque : 보통의 큐와는 다르게 큐의 양쪽에서 원소를 꺼내올 수 있는 Collection입니다.

 

4. Map

Map은 키-데이터(Key-Value) 쌍으로 자료를 보관하고 있습니다. Map에서 순서는 고려되지 않는 편이며 키는 중복을 허용할 수 없습니다.

 

파생클래스

HashMap : 키-값의 쌍으로 값을 가져올 수 있는 대표적인 Map의 Collection입니다. 동기화를 보장하지 않습니다. 키-값으로 쉽게 데이터를 검색할 수 있습니다. 단일 스레드에서 개발한다면 HashMap을 사용합시다.

또한 HashMap은 특이하게도 키 또는 값에 null을 저장할 수 있다는 점입니다.

Hashtable : HashMap과 사용법이 거의 동일한데, 다른점은 동기화를 보장한다는 것입니다. 그렇기 때문에 HashMap보다는 무겁겠죠?

또 HashMap과는 다르게 키 또는 값에 null을 사용할 수 없습니다.

SortedMap : 이름에서도 알 수 있듯이 정렬이 된 Map구조입니다. 

 

다음 포스팅에는 파생클래스를 사용하는 방법에 대해 알아보도록 하겠습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

C언어와는 조금 다르게 C++의 함수는 조금 더 특별한 기능이 추가되었습니다. 뭐가 다를까요?

 

1. C언어에서는 함수명이 같으면 컴파일 에러가 나지만 C++에서는 함수명이 같아도 매개변수의 자료형, 갯수가 다르다면 같은 함수명을 쓸 수 있습니다. 이것이 함수 오버로딩(overloading)이라고 하지요.

2. C언어에서는 함수 인자에 default값을 줄 수 없습니다. 하지만 C++에서는 사용자가 매개변수에 값을 넘겨주지 않으면 자동으로 dafault값이 지정됩니다.

3. C언어에서와 다르게 참조자(reference)가 등장합니다. 변수의 별칭이라고나 할까요?

4. 함수의 호출시간을 줄이고자 inline함수가 등장합니다. inline함수는 일반 함수와는 다르게 함수의 호출부에 코드를 직접 삽입함으로써 함수가 호출되는 과정이 없습니다. 그러니까 실행속도가 일반함수 호출하는 것보다 빠르지요.

 

대략적인 설명은 여기까지 하겠습니다. 이제 하나하나 자세하게 살펴보도록 합시다.

 

 

1. 함수오버로딩(Overloading)

함수오버로딩은 개념이 꽤나 간단합니다. 예를 들어 두 수를 더해서 반환하는 sum이라는 함수가 있다고 합시다. 우리는 정수형, 실수형 모두 sum이라는 함수로 이름을 짓고 싶습니다. 왜냐면 자료형만 다를뿐 같은 기능을 하기 때문이죠. 이렇게 오버로딩의 개념이 등장합니다.

아래의 코드가 오버로딩을 보여줍니다.

 

#include <iostream>

using namespace std;

int sum(int a, int b) {
	return a + b;
}

double sum(double a, double b) {
	return a + b;
}

int sum(int a, int b, int c) {
	return a + b + c;
}

double sum(double a, double b, double c) {
	return a + b + c;
}

int main(void) {
	cout << sum(10, 20) << endl;
	cout << sum(10.1, 20.1) << endl;
	cout << sum(10, 20,30) << endl;
	cout << sum(10.1, 20.1,30.1) << endl;
}

 

함수의 인자들이 자료형이 다른 것도 있고, 인자의 갯수가 다른 것이 있습니다. 함수의 이름은 같은 것을 알 수 있네요.

여기서 주의해야할 점은 반환형이 달라도 오버로딩이 되지 않습니다. 항상 매개변수의 타입 또는 갯수가 달라야 오버로딩이 된다는 것을 알아두세요.

 

2. default 인자 값

인자를 전달하지 않으면 default로 지정된 값으로 인자 변수가 초기화됩니다. 이것도 역시 어렵지 않습니다. 

 

#include <iostream>
using namespace std;

int sum(int a, int b = 10) {
	return a + b;
}
int main(void) {
	cout << sum(20) << endl;
	cout << sum(20, 20) << endl;
}

 

함수의 인자 갯수는 2개이지만 sum(20)이라는 호출이 가능한 이유는 b가 10으로 default 값을 사용하기 떄문입니다. 또는 보통의 함수 호출과 같이 인자를 2개 주어서 사용할 수도 있습니다.

 

디폴트 인자값을 사용할때 주의할 점이라고 한다면 디폴트 인자값은 항상 오른쪽부터 왼쪽으로 써줘야한다는 것입니다. 

 

int sum(int a, int b = 10,int c) {
	return a + b + c;
}

int sum(int a, int b = 10, int c = 20) {
	return a + b + c;
}

 

첫번째 처럼 중간이 디폴트 인자값이 있다면 오류가 나게되고 항상 밑의 함수처럼 오른쪽부터 왼쪽으로 디폴트값을 써줘야한다는 겁니다.

 

 

3. 참조자(Reference)

참조자는 C++에서 새롭게 등장한 기능입니다. 변수의 별명이라고 생각하면 될 것 같습니다. 우리가 어떤 사람을 부를때도 별명을 부르곤 하잖아요? reference를 사용할땐 &를 사용하여 참조자라는 것을 알려줍니다.

예를 들어 이렇게 사용하죠.

 

int a=100;

int &b=a;

 

이렇게 b는 a와 같은 데이터를 가리키며 주소 역시 같습니다. 마치 포인터처럼요.

이것을 함수에 사용하게 되면 call-by-reference를 구현할 수 있게 됩니다. 우리는 call-by-reference를 C언어에서 포인터로 시험해봤지요.

이제 참조자를 통해서 구현해보도록 합시다.

 

#include <iostream>

using namespace std;

void swap(int &a, int &b) {
	int temp = a;
	a = b;
	b = temp;
}

int main(void) {
	int a = 100;
	int b = 200;

	cout << "a:" << a << ",b:" << b << endl;
	swap(a, b);
	cout << "a:" << a << ",b:" << b << endl;
}

 

실행시켜보면 아래와 같이 값이 바뀐 것을 알 수 있습니다. a와 b에 주소값을 swap함수에 넘기지 않고 말이죠.

 

결과

a:100,b:200
a:200,b:100
계속하려면 아무 키나 누르십시오 . . .

 

이 같이 참조자는 크기가 큰 구조체나 데이터를 매개변수로 전달할 때 유용합니다.

 

4. inline 함수

보통의 함수 호출 과정은 다음과 같습니다.

 

스택에 다시 돌아갈 주소를 저장. 매개변수 전달 -> 스택에 지역 변수 할당 -> 함수 실행 -> 반환값 전달 -> 다시 돌아갈 주소로 이동

 

함수의 실행부분이 꽤나 길다면 뭐 호출하는 시간은 신경쓰지 않아도 되지만 함수의 실행부가 짧지만 자주 호출된다면 호출되는 시간을 무시할 순 없겠죠.

 

inline함수는 함수의 호출시간을 줄여 보다 더 빠른 성능을 내기 위함입니다. inline함수는 함수 호출부에 함수 정의 부분을 직접 삽입하여 호출하는 방식으로 매크로 함수와 비슷합니다. 하지만 매크로 함수를 쓰면 괄호의 늪에 빠질 수도 있죠..

인라인 함수는 간단합니다. 기존의 함수를 선언하거나 정의하는 부분에 inline 키워드만 붙여주면 되니까요.

inline키워드는 선언 또는 정의하는 부분에 한번만 선언해주어도 됩니다.

 

 

#include <iostream>
using namespace std;

inline int sum(int, int);

int sum(int a, int b) {
	return a + b;
}
int main(void) {
	int sumValue;
    sumValue = sum(10, 20);
	cout << sumValue << endl;
}

 

실제 이 코드는 이렇게 동작하게 됩니다.

 

#include <iostream>
using namespace std;

int main(void) {

	int sumValue;
	{
		int a = 10; int b = 20;
		sumValue = a + b;
	}
	cout << sumValue << endl;
}

 

정말 단지 inline함수의 내용이 코드에 직접 삽입되는 형태지요.

 

지금까지 C++에서 함수의 특징에 대해서 알아보았습니다. 딱히 어려운건 없었죠?

반응형
블로그 이미지

REAKWON

와나진짜

,

String을 왜 인코딩하고 디코딩할까요? 인코딩과 디코딩을 해야하는 상황이 있습니다. 만일 DB가 한글을 지원하지 않는 경우 한글로 된 문자를 숫자로 encoding해서 DB에 저장하면 되고, 사용자에게 보여줄때는 다시 decoding해서 보여주면 됩니다.

또는 암호화할때 문자열을 encoding한 후에 암호화하게 됩니다.

이 밖에도 문자열을 encoding과 decoding을 해야할 상황이 있겠죠? 그러므로 문자열을 어떻게 encoding할지 decoding할지 알아보도록 하겠습니다.

 

byte[] getBytes()

byte[] getBytes(Charset charset)

byte[] getBytes(String charsetName)

문자열을 인코딩된 byte형태로 넘겨줍니다. 매개변수없이 그냥 getBytes()메소드를 사용하면 플랫폼에 따른 default charset을 사용합니다.

만일 특정 charset을 지정할 경우 인자를 받는 getBytes메소드를 사용하면 됩니다. ISO-8859-1, euc-kr, utf-8 등의 charset이 존재하는데 encoding과 decoding할때 이 chatset을 맞춰서 해야합니다. 그러지 않을 경우 문자가 깨지는 현상이 발생하게 됩니다. 

 

자, 그러면 이제 실제 프로그램을 짜면서 사용법을 알아보도록 합시다. 여기서는 가장 많이 사용하는 UTF-8로 charset을 지정했습니다.

public static void main(String[] args){
	String str="reakwon의 블로그";
	//default charset으로 인코딩된 바이트 배열	
	byte[] bytes=str.getBytes();
    //인코딩된 바이트 출력
	System.out.print("Default charset encoding: ");
	for(int i=0;i<bytes.length;i++)
		System.out.print(bytes[i]+" ");
	System.out.println();
	//default charset으로 디코딩된 문자열 출력
	String decoded=new String(bytes);
	System.out.println(decoded);
		
	System.out.println();
	try {
    	//UTF-8로 인코딩된 바이트 배열
		bytes=str.getBytes("UTF-8");
		System.out.print("UTF-8 charset encoding: ");
		for(int i=0;i<bytes.length;i++)
			System.out.print(bytes[i]+" ");
		System.out.println();
        //이 바이트 배열을 default charset으로 디코딩된 문자열 출력 : charset이 다르므로 한글이 깨짐.
		decoded=new String(bytes);
		System.out.println(decoded);
		//인코딩된 UTF-8로 디코딩되어 한글이 깨지지 않음.
		decoded=new String(bytes,"UTF-8");
		System.out.println(decoded);
			
	} catch (UnsupportedEncodingException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}

 

문자열은 한글이 섞여있는 문자열입니다. 우선 default charset으로 바이트 배열을 얻어오고 출력해줍니다. 인코딩된 바이트 배열을 다시 디코딩하여 출력해줍니다. String으로 decoding하는 방법은 무척 간단합니다. 아래의 String 클래스의 생성자를 사용하면 됩니다.

 

 

String(byte[] bytes) : default charset으로 decoding한 문자열

String(byte[] bytes, String charsetName) : 특정 charset으로 decoding한 문자열

 

try...catch 안에서는 UTF-8로 인코딩하고 인코딩된 값을 출력해줍니다. 그 이후 String 인코딩 된 값을 출력해주는데요. 만약 default charset으로 디코딩한 후 문자열을 출력하면 어떻게 될까요? 예상하셨겠지만 영어를 제외한 한글을 깨져서 나옵니다.

그래서 인코딩된 방식과 같이 UTF-8로 디코딩해야합니다. 그래야만 한글이 깨지지 않지요. 그래서 바로 위의 String의 2번째 생성자를 사용해서 charset을 지정합니다. 그 후 출력하면 정상적으로 한글이 출력됩니다.

결과를 확인해보세요.

 

결과

Default charset encoding: 114 101 97 107 119 111 110 -64 -57 32 -70 -19 -73 -50 -79 -41 
reakwon의 블로그

UTF-8 charset encoding: 114 101 97 107 119 111 110 -20 -99 -104 32 -21 -72 -108 -21 -95 -100 -22 -73 -72 
reakwon?쓽 釉붾줈洹?
reakwon의 블로그

 

결과를 들여다보면 영어는 ASCII, 그리고 한글은 2바이트를 사용하는 것을 알 수 있네요.

반응형
블로그 이미지

REAKWON

와나진짜

,

자바에서 문자열을 다루기 위해서는 String 클래스의 메소드를 사용합니다. 이번 포스팅에서는 String 메소드를 설명하고 간단한 사용법울 알아보도록 하겠습니다.

 

int length()

문자열의 길이를 반환합니다. C언어로 치면 strlen과 같은 메소드겠네요. 사용법이 너무 간단하므로 예를 보이지는 않겠습니다.

 

String substring(int beginIndex)

String substring(int beginIndex, int endIndex)

substring 메소드는 beginIndex부터 문자열끝까지의 문자열을 반환합니다. 만약 끝을 가리키는 endIndex를 사용한다면 문자열의 특정 구간의 문자열을 반환합니다. 아래는 사용 예를 보여줍니다.

 

String str="AABBCCDD";
System.out.println(str.substring(2));
System.out.println(str.substring(2, 4));

"AABBCCDD"의 substring(2)는 처음 나오는 B의 자리를 인자로 전달합니다. 문자열의 첫번째 인덱스는 0부터 시작한다는 것을 잊지마세요. 그러니 처음 나오는 A의 인덱스는 0입니다. 결과는 B부터 문자열의 끝까지 새로운 문자열을 반환합니다.

또 substring(2,4)는 2번째 인덱스부터 4번째 인덱스 전까지 새로운 문자열을 반환합니다. 주의해야할 것은 4번 인덱스 앞까지라는 것입니다. 그러므로 4번 인덱스의 문자는 포함되지 않습니다. 결과는 "BB"라는 문자열이 반환되겠네요.

 

위의 결과는 아래와 같습니다.

 

결과

BBCCDD
BB

 

 

 

 

char charAt(int index)

문자열에서 char 값이 필요할때 사용하는 메소드입니다. 인자 index는 문자열에서 한 문자를 뽑아낼 위치를 말합니다.

String str="AABBCCDD";
System.out.println(str.charAt(4));
System.out.println(str.charAt(str.length()-1));

 

예를 들어 "AABBCCDD"라는 문자열에서 4번째 인덱스의 char 값을 얻고 싶다면 인자로 4를 넣어주면 됩니다. 그렇다면 처음 나오는 'C'가 될겁니다. 또는 마지막 문자열의 문자를 추출해내고 싶다면 length() 메소드의 반환값에서 1을 뺀 값을 전달하면 제일 마지막 문자를 가져올 수 있습니다.

 

결과

C
D

 

String concat(String str)

문자열을 합치는 메소드입니다. str을 현재 문자열 뒤에 추가합니다. 사실 저는 별로 이 메소드를 사용하지는 않는데요. 자바에서는 문자열을 합칠때 + 연산으로 합칠 수 있기 때문이지요.

String str="AABBCCDD";
System.out.println(str.concat("EE"));
System.out.println(str+"EE");

 

아래에서 똑같은 결과를 볼 수 있지요.

 

결과

AABBCCDDEE

AABBCCDDEE

 

boolean contains(CharSequence s)

문자열에서 특정 문자열이 포함되어있는지 여부를 확인하려면 이 메소드를 사용하면 됩니다. 매개변수에 포함됐는지의 문자열을 전달하고 문자열이 포함되어 있다면 true, 포함되지 않았다면 false를 반환합니다.

 

String str="AABBCCDD";
System.out.println(str.contains("BC"));
System.out.println(str.contains("AD"));

"BC"는 "AABBCCDD"에 포함되어 있으므로 true를 반환할 것이고, "AD"라는 문자열을 포함되어 있지 않으니 false를 반환합니다.

 

결과

true
false

 

 

 

 

static String copyValueOf(char []data)

static String copyValueOf(char []data, int offset, int count)

이 메소드는 static 메소드입니다. char형 배열을 문자열로 변환할때 사용하는 메소드인데요. 그냥 char 배열을 전달하면 그 배열 자체를 문자열로 반환합니다.

특정 위치에서 몇개의 문자까지 문자열로 받고 싶다면 offset에 특정 문자열을 얻고 싶은 위치를, count에 몇개의 문자를 받을 지를 전달하면 되겠습니다.

char []charArray={'A','B','C','D','E','F'};
System.out.println(String.copyValueOf(charArray));
System.out.println(String.copyValueOf(charArray,2,4));		

 

결과를 보면 직관적으로 이 메소드가 어떤 기능을 하는지 알 수 있을 겁니다.

 

결과

ABCDEF
CDEF

 

String[] split(String regex)

String[] split(String regex, int limit)

정규표현식을 기준으로 문자열을 쪼갭니다. 정규표현식을 몰라도 됩니다. 만약 limit에 인자를 전달하면 그 limit까지만 문자열을 쪼갭니다.

 

String str="AABB__CCDD__EEFF";
String []arr=str.split("__");
for(int i=0;i<arr.length;i++)
	System.out.println(arr[i]);

"AABB__CCDD__EEFF"를 "__"로 분리시켜보면 세개의 문자열이 나옵니다. "AABB", "CCDD", "EEFF"가 그것들이죠. 이것은 한가지 예이므로 쉼표로 분리하고 싶다면 쉼표로 분리할 수도 있습니다.

 

결과

AABB
CCDD
EEFF

 

String toLowerCase()

String toLowerCase(Locale locale)

String toUpperCase()

String toUpperCase(Locale locale)

문자열을 전부 소문자 또는 전부 대문자로 변환된 문자열을 얻고 싶을 때 위의 메소드를 사용하면 됩니다.

소문자로 변환된 문자열을 얻고 싶을때는 toLowerCase, 대문자로 변환된 문자열을 얻고 싶을때는 toUpperCase를 사용하면 되는 것이죠. 메소드 인자는 Locale은 사실 써본적은 없습니다. 인자없는 toLowerCase, toUpperCase로도 충분해 보입니다.

String str1="AABBCCDD";
String str2="ffgghhii";
System.out.println(str1.toLowerCase());
System.out.println(str2.toUpperCase());

 

"AABBCCDD"는 toLowerCase로 소문자로 출력하고, "ffgghhii"는 toUpperCase로 모두 대문자로 출력합니다.

 

결과

aabbccdd
FFGGHHII

 

 

int compareTo(String anotherString)

문자열을 사전순으로 비교합니다. 만약 anotherString이 사전순으로 앞에 등장할때는 양수를 반환하고 사전순으로 늦게 등장할때는 음수를 반환합니다. 만약 anotherString이 현재 문자열과 정확히 같다면 0을 반환하게 됩니다. 문자열을 비교할때는 equals를 주로 사용하는데요. 사전순으로 비교하고 싶을땐 compareTo를 사용하면 됩니다.

 

String str1="BCD";
String str2="ABC";
String str3="BCD";
String str4="CDE";
System.out.println(str1.compareTo(str2));
System.out.println(str1.compareTo(str3));
System.out.println(str1.compareTo(str4));

 

"ABC"는 "BCD" 기준으로 사전상 앞에 등장하니까 양수, "CDE"는 "BCD" 기준으로 사전상 뒤에 등장하니까 음수가 반환됩니다. str1과 str3은 문자열이 같으므로 0이 반환됩니다.

 

결과

1
0
-1

 

 

 

 

int compareToIgnoreCase(String str)

만약 소문자, 대분자를 무시하고 비교하고 싶을때는 이 메소드를 사용하면 됩니다. 원래 "ABC"와 "abc"를 compareTo 메소드로 비교하면 0이 반환되지 않습니다만 이 메소드를 사용해서 비교한다면 0이 반환됩니다.

String str1="abc";
String str2="ABC";
String str3="abc";
String str4="aaa";
System.out.println(str1.compareToIgnoreCase(str2));
System.out.println(str1.compareToIgnoreCase(str3));
System.out.println(str1.compareToIgnoreCase(str4));

 

str4와 비교하는 것을 제외하고는 0을 반환하겠군요.

 

결과

0
0
1

 

 

boolean equals(Object anObjec)

boolean equalsIgnoreCase(String anotherString)

사전순으로 비교할 필요없이 단순히 문자열이 같은지 다른지 비교할때는 이 메소드를 씁니다. 역시 대소문자를 구분하지 않는다면 equalsIgnoreCase를 사용하면 됩니다. 같다면 true, 다르면 false를 반환합니다.

 

 

static String format(String format, Object ...args)

특정 format으로된 문자열을 얻고 싶을때 사용하는 메소드입니다. C언어에서 이것과 비슷한 기능을 하는 sprintf가 되겠네요. 

String str=String.format("%d+%d=%d", 1,2,1+2);
System.out.println(str);

 

C언어를 사용했던 분들에게는 조금 익숙한 메소드겠네요.

 

결과

1+2=3

 

boolean startsWith(String prefix)

boolean startsWith(String prefix, int toffset)

boolean endsWith(String suffix)

startsWith는 문자열이 prefix로 시작하는지 확인하는 메소드입니다. prefix의 비교 위치를 지정하려면 startsWith의 두번째 메소드를 사용하면 됩니다.

endsWith는 문자열이 suffix의 문자열로 끝나는지 확인하는 메소드입니다. suffix로 끝난다면 true, 다르게 끝나면 false를 반환합니다.

		
String str="ABCDEF";
System.out.println(str.startsWith("AB"));
System.out.println(str.startsWith("BC",1));
System.out.println(str.endsWith("EF"));
System.out.println(str.endsWith("EE"));

결과

true
true
true
false

 

 

 

 

 

String replace(char oldChar, char newChar)

String replace(CharSequence target, CharSequence replacement)

String replaceAll(String regex, String replacement)

String replaceFirst(String regex, String replacement)

문자나 문자열을 다른 문자나 문자열로 바꾸고 싶을때 사용하는 메소드입니다. 메소드 이름이 직관적이기 때문에 사용법을 익히는데 그리 어렵지 않을 겁니다. 또는 아래의 예를 보고 이해하면 되겠지요.

String str="AB-----AB-----AB";
String str1=str.replaceFirst("AB", "ab");
String str2=str.replace("AB", "ab");
String str3=str.replaceAll("AB", "ab");
String str4=str.replace('A', 'a');
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
System.out.println(str4);

 

결과

ab-----AB-----AB
ab-----ab-----ab
ab-----ab-----ab
aB-----aB-----aB

 

사실 이 중에서도 자주 쓰이는거 외에는 별로 쓸 일이 없을 겁니다. 자주 필요한 몇가지만 기억해두면 될 것 같네요. 다음 포스팅에서도 String 메소드에 대해서 더 알아보도록 합시다.

반응형
블로그 이미지

REAKWON

와나진짜

,

시간 관련 함수

프로그램에서 시간은 거의 필수로 다루어질 만큼 중요한 요소입니다. 그래서 우리는 시간을 잘 다루어야 할 필요가 있습니다.  그래서 이번에는 C언어에서 시간과 관련된 함수 몇가지를 살표보도록 하지요. 지금부터 다룰 함수는 모두 시간과 관련된 함수이므로 time.h 헤더파일을 포함시켜야한다는 것을 잊지 마세요.

time

가장 기본적인 시간 관련 함수입니다. 함수의 원형은 다음과 같죠.

#include <time.h>

time_t time(time_t *timeptr);

 

이 함수는 1970년 1월 1일 0시 (UTC)부터 인자값(timeptr)까지 흐른 시간을 반환합니다. 그 단위가 초단위지요.  이 함수의 사용방법은 주로 2가지 입니다.

timeptr이라는 매개변수에 인자를 전달하여 현재까지 흐른 시간을 초단위로 구하거나, time함수에 NULL을 전달하여 반환값을 받거나 입니다.

 

 

 

그래서 현재시간까지 구한다면 time(NULL)을 호출해서 반환값을 받으면 되는 것이죠. 아래의 예처럼요.

time_t t; t = time(NULL); printf("%ld\n", t); 

그 결과는 아래처럼 큰 정수를 반환하게 되지요. 1970년 1월 1일 0시(UTC)부터 지금까지 초단위의 시간을 구하기 때문이지요.

1548588718

 

localtime

이 함수는 지역시간을 구해냅니다. 반환하는 형식은 구조체인데 tm이라는 구조체입니다. 다음 함수의 원형처럼 말이죠.

#include <time.h>

struct tm *localtime(const time_t *timeval);

 

우리는 위의 time함수에서 UTC기준으로 흐른 시간을 초단위로 구했지요. 초단위로 구하니 보기도 어렵고 지금이 몇월인지 몇년도인지 구분하기가 어렵습니다. 그래서 이것을 사람이 잘 알아보게끔 구조체로 변환할 수 있는 함수가 바로 localtime함수입니다.

 

 

 

 

우리는 반환값인 tm구조체를 다는 아니어도 필요한 몇가지 멤버는 알아야할 필요가 있습니다.

struct tm {

        int tm_sec;   //초

        int tm_min;   //분

        int tm_hour;   //시

        int tm_mday;  //일

        int tm_mon;    //월(0부터 시작)

        int tm_year;    //1900년부터 흐른 년

        int tm_wday;  //요일(0부터 일요일)

        int tm_yday;  //현재 년부터 흐른 일수

        int tm_isdst; 

};

필드명은 꽤 직관적입니다. 이 필드명만 보아도 무엇을 의미하는 것인지 잘 알것이라고 생각합니다.

자세한 것은 코드로 살펴보도록 합시다.

#include <stdio.h>
#include <time.h>

int main() { 
        time_t current;
        time(&current); 
        struct tm *t = localtime(&current);

        printf("%d년 %d월 %d일 ", 
                        1900 + t->tm_year, t->tm_mon + 1, t->tm_mday); 

        switch (t->tm_wday) {
                case 0:printf("일요일 "); 
                       break;
                case 1:printf("월요일 ");
                       break;
                case 2:printf("화요일 "); 
                       break; 
                case 3:printf("수요일 "); 
                       break;
                case 4:printf("목요일 "); 
                       break; 
                case 5:printf("금요일 ");
                       break;
                case 6:printf("토요일 "); 
                       break;
        } 

        printf("%d:%d:%d\n", t->tm_hour, t->tm_min, t->tm_sec);
        printf("1년 365일 중 %d일째\n", t->tm_yday + 1); 
}

 

그 결과는 다음과 같습니다.

2019년 1월 27일 일요일 20:55:9

1년 365일 중 27일째

각각 년, 월, 일 , 시, 분, 초, 요일을 각각 구할때, 또는 날짜계산, 시간계산을 할때 편리하겠군요.

 

ctime

 

#include <time.h>

char *ctime(const time_t *time);

구조체를 굳이 사용하고 싶지 않고 사용자가 시간을 읽을 수 있게끔 문자열로 변환하는 것이 ctime함수입니다. 마찬가지로 time함수에서 반환된 값을 ctime에 인자로 쏙 집어넣으면 현재 시간 정보를 다음과 같이 보기좋은 형식으로 반환해줍니다.

Www Mmm dd hh:mm:ss yyyy

 

Www 요일을 영어 세글자로 나타냅니다.

Mmm 월을 영어 세글자로 나타냅니다.

dd 날짜를 숫자 두글자로 나타냅니다.

hh 시를 숫자 두글자로 나타냅니다.

mm 분을 숫자 두글자로 나타냅니다.

ss 초를 숫자 두글자로 나타냅니다.

yyyy 년도를 숫자 네글자로 나타냅니다.

 

 

 

 

ctime이 어떤 함수인지 알았으면 어떻게 사용하는지 코드를 보세요. 몇 줄 안되는 코드입니다.

#include <stdio.h>
#include <time.h>

int main() { 
        time_t t = time(NULL); 
        printf("현재시간 :%s\n", ctime(&t));
}

 

코드는 상당히 간단합니다. 어떻게 나오는지 살펴보지요.

현재시간 :Sun Jan 27 21:07:50 2019

 

asctime

이 함수는 ctime과 같이 시간을 문자열로 출력해주지만 인자로 tm구조체 포인터를 받는 것과 ctime과 다른게 없습니다.

#include <time.h>

char *asctime(const struct tm *tm);

반환하는 문자열 포맷도 ctime과 똑같습니다. 사용방법은 그저 tm구조체 포인터를 전달하기만 하면 됩니다.

아래의 사용예를 보세요.

#include <stdio.h>
#include <time.h> 

int main() {
        time_t current = time(NULL);
        struct tm *t = localtime(&current); 
        printf("현재시간 :%s\n", asctime(t));
}
결과는 위의 ctime 결과와 같습니다.
여기까지 시간관련된 함수 4개를 살펴보았는데, 여기까지만 알면 충분할 것 같군요. 나중에 기회가 되면 더 살펴보도록 하지요.
반응형
블로그 이미지

REAKWON

와나진짜

,

비트연산자


컴퓨터가 사용하는 모든 데이터들은 전부 1과 0으로 이루어진 비트열이라는 것을 다들 잘 알겁니다.


C언어에서도 역시 그렇습니다. 우리는 정수형 변수 a에 16이라는 데이터를 집어넣는 것은 사실 코딩할때만 그렇습니다. 하지만 실행이 될때는 이진수로 메모리에 저장이 되어있죠.


만약

int a=16

이라고 정수를 메모리에 넣어준다면 이렇게 메모리에 잡히게 됩니다.

int는 4바이트의 메모리를 갖고 있으므로 

0000 0000 0000 0000 0000 0000 0001 0000

이렇게 저장이 되지요.


우리는 이러한 비트로 연산을 수행할 수 있습니다. 오늘은 이런 비트 연산에 대해서 알아보도록 하겠습니다.


NOT ( ~ )

NOT연산자는 비트열을 반전시키는 연산자입니다. 직관적으로 이해하기도 쉽습니다.

만약 비트가 1이면 0으로 반전하고, 0이면 1로 바꾸기만 하면 되니까요. 연산자 기호는 ~를 씁니다.


그래서 만약

~1000 0110 는 0111 1001로 바뀌게 됩니다.


 

OR( | )

OR 연산자는 | 입니다. 엔터 위쪽 \가 보이시나요? 이것을 쉬프트기로 눌러 입력한게 바로 OR연산자 | 입니다. A OR B는 A 또는 B가 1이라면 답은 1이 되는 겁니다.


0 | 0 = 0

0 | 1 = 1

1 | 0 = 1

1 | 1 = 1


두 비트가 모두 0일때 0이라는 것을 알 수 있네요.


다음의 계산 예를 봅시다. 


      0111 1001

 OR 1000 1010

---------------------

      1111 1011



AND( & )

AND 연산자는 두 비트열 모두 1일때 1이 됩니다. 


0 & 0 = 0

0 & 1 = 0

1 & 0 = 0

1 & 1 = 1


둘 다 1일때만 1인것을 알 수 있습니다.


다음의 예를 보고 AND연산에 대해서 보도록 합시다.


        0001 1001

AND  0111 1000

----------------------

        0001 1000


OR연산과 AND 연산은 정말 쉽습니다.


XOR( ^ )

XOR 연산자는 두 비트열 중 1이 하나만 있을때 답이 1이 됩니다. 또는 1이 홀수개 일때 답이 1이 된다고 기억하면 됩니다.


0 ^ 0 = 0

0 ^ 1 = 1

1 ^ 0 = 1

1 ^ 1 = 0


1이 홀수개일때만 1이 됩니다.



       1001 1010

XOR 0110 1111

---------------------

       1111 0101



SHIFT( <<, >>)

쉬프트 연산은 비트를 옮기는 연산을 수행합니다. 옮기는 방향에 따라 두 종류가 있습니다. 바로 left shift와 right shift가 그것이죠.

비트를 왼쪽으로 옮기려면 << 연산을 사용하고, 오른쪽으로 옮기려면 >>연산을 사용합니다.




옮길 기준이 되는 비트열은 항상 연산자의 왼쪽, 얼만큼 옮길 건지를 결정하는 건 연산자의 오른쪽에 위치합니다.


만약 Left Shift 연산으로 왼쪽으로 비트열을 옮긴다면 가장 오른쪽에서부터 옮긴 비트열 길이까지 0으로 채워집니다.


하지만 Right Shift 연산으로 비트열을 오른쪽으로 옮기면 가장 상위비트(MSB:Most Significant Bit 라고 합니다.)를 왼쪽에서부터 채웁니다.



아래 예를 보면서 이해합시다.


0011 0110 << 3 = 1011 0000


왼쪽으로 3비트를 이동시키니 001은 삭제되었습니다. 그리고 10110이라는 비트열이 왼쪽으로 3비트 이동이 되지요. 그렇다면 자리가 3개가 남겠군요. 그건 0으로 채웁니다. 이것을 패딩(padding)이라고 하지요.


0011 1100 >> 3 = 0000 0111


이제 오른쪽 비트이동입니다. 우선 이 비트를 오른쪽으로 3비트를 옮기니 100이 삭제가 됩니다. 그후 나머지 4비트는 오른쪽으로 3비트를 이동하면 왼쪽에 3비트를 추가로 채워야하지요. 옮기기전 가장 상위비트(MSB)는 빨간색으로 표시된 0입니다. 그러니 오른쪽으로 이동할때는 이 MSB를 가지고 왼쪽비트를 채우니 000으로 채워지는 겁니다.


1001 1011 >> 4 = 1111 1001


자, 이제 MSB는 1입니다. 이 비트를 오른쪽으로 4비트를 이동하니 1001은 없어지겠죠? 그후 나머지 4비트를 오른쪽으로 이동하고 비워진 4비트는 MSB로 채우니 1로 채워지는 것입니다.


아! 여기서 2의 보수법을 모르시는 분을 위해서 잠시 간략하게 설명하겠습니다.


자료형에는 음수를 나타낼 수 있는 signed 타입와 unsigned 타입이 있습니다. 우리가 int, char와 같이 그냥 사용하게 된다면 default로 signed 자료형입니다. 하지만 음수를 표현할 필요가 없다면 unsigned라는 키워드를 붙여 unsigned int와 같이 표현합니다.

이제 음수를 나타낼 수 있는 signed 자료형에 대해서 음수를 표현하는 방법을 설명하겠습니다.


signed자료형에서 컴퓨터는 MSB에 따라 음수인지 양수인지 구별하는데요.

0이면 양수, 1이면 음수를 나타냅니다.

만약 위의 예에서 0011 1100은 MSB가 0이므로 양수입니다. 

그러니 정수로 표현하면 MSB를 제외하고 32+16+8+4로 70이 됩니다.


하지만 위의 예처럼 1111 1001은 MSB가 1이므로 음수입니다. 이때 2의 보수를 사용하는데요. 방법은 이렇습니다.

1) MSB를 제외하고 비트를 반전시킨다(이것이 1의 보수법입니다.)

2) 1을 더해준다. (1을 더해주는 것이 2의 보수법입니다.)


1)+2)의 방법에 따라 1111 1001의 음수값을 구해보면

   1000 0110 (1111 1001의 반전)

+ 0000 0001 (1을 더해주는 2의 보수)

--------------------

   1000 0111


이렇게 7이 나오게 되지요. 이때 MSB가 여전히 1이므로 -7이 되는 것이죠.





다음의 코드는 비트 연산에 대한 해답을 제공하는 프로그램입니다. 


#include <stdio.h>
#define SIZE 8

void printBitArray(char bits) {
	
	int i;
	for (i = SIZE-1; i >= 0;i--) {
		char bit = ((1 << i)&bits) > 0 ? '1' : '0';
		printf("%c ", bit);
	}
	printf("\n");
}
int main() {

	char bits1=0b10110110;
	char bits2=0b01101101;
	printf("bits1:"); printBitArray(bits1);
	printf("bits2:"); printBitArray(bits2);
	printf("\n\n");

	printf("NOT bits1:");  printBitArray(~bits1);
	printf("bits1 | bits2 :");  printBitArray(bits1 | bits2);
	printf("bits1 & bits2 :");  printBitArray(bits1 & bits2);
	printf("bits1 ^ bits2 :");  printBitArray(bits1 ^ bits2);
	
	printf("bits1 << 4 :");  printBitArray(bits1 << 4);
	printf("bits2 >> 2 :");  printBitArray(bits2 >> 2);
	printf("bits1 >> 3 :"); printBitArray(bits1 >> 3);
	
}

bits1과 bits2를 바꿔서 실행해보고 printBitArray의 매개변수로 비트연산을 해서 답을 확인해 보세요.


여기까지 비트연산자에 대해서 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

C언어 문자열 함수

문자열을 다룰때 어떤 문자열 단위로 자르고 싶은 경우나 어떤 문자열에서 임의의 문자열을 찾고 싶은 경우가 있지 않았나요?

그 경우에 사용할 수 있는 문자열 함수를 소개하려고 합니다. 문자열 함수를 사용하기 위해서는 항상 string.h 헤더 파일을 include해야한다는 것을 잊지 마세요.


strtok

이 함수가 문자열을 어떤 문자열 기준으로 자르는 역할을 하는 함수입니다. 일단 함수의 원형을 보시죠.


char *strtok(char *str, const char *delimiters);


2개의 파라미터를 갖고 있죠.


- str : 우리가 어떤 문자열을 자를지 넘겨받는 매개변수입니다.

- delimiters: 구분자라고 합니다. 여기서 자를 기준을 결정하는 것이지요.


예를 들어 str이 "show_me_the_money"라고 합시다. 그리고  문자열을 "_"(구분자)를 기준으로 자른다고 합시다. 그렇다면 show, me, the, money라는 4개의 문자열로 잘리겠죠.


- 반환값 : 잘린 문자열을 반환합니다. 만약 문자열이 전부 끝났다면 NULL을 반환하게 되지요.




이제 함수의 기본적인 설명은 여기까지하고 코드를 보면서 사용법을 확실히 알아보도록 하겠습니다.



strtok source code

#include <stdio.h>
#include <string.h>
int main() {
	
	char str[32] = "show_me_the_money";
	char *tok=strtok(str, "_");

	while (tok != NULL) {
		printf("token : %s\n", tok);
		tok = strtok(NULL, "_");
	}
	printf("기존 문자열 :%s\n", str);
}


우선 결과를 보고 왜 이런 결과가 나왔는지 알아보도록 하지요.


결과


token : show

token : me

token : the

token : money

기존 문자열 :show



이 코드에서는 위의 예와 마찬가지로 "show_me_the_money"라는 문자열을 자르고 있습니다.

strtok는 처음 str 매개변수에 NULL이 아닌 문자열을 사용하면 자를 문자열을 넘겨받은 문자열로 결정합니다.

이후 실행할때 str에 NULL을 전달하면 이전에 설정했던 문자열을 계속해서 자르는 것이죠.


그래서 반복문 while루프 안에서는 strtok에 str인자를 NULL로 넘겨주고 있는 것이죠. 잘 잘려지고 있기는 합니다.


하지만 마지막 줄을 보세요.

마지막 줄은 기존의 문자열 str을 출력하고 있는데 "show_me_the_money"가 출력되지 않고 "show"만 출력이 되고 있습니다. 왜 기존의 문자열인str[32]="show_me_the_money"가 출력이 되지 않는 것일까요?


strtok는 눈치채셨겠지만 자를 문자열을 변환시키면서 문자열을 잘라나갑니다.

우리는 문자열의 마지막 문자가 NULL문자로 끝난다는 것을 알고 있습니다. 그렇다면 마지막에 str이 "show"만을 출력했다는 것은 "show\0"가 된 것을 짐작할 수 있을까요?


"show"이후 문자는 바로 '_' 문자인데, '_'문자가 '\0'인 NULL문자로 바뀌게 된 것 아닐까요?

결론부터 얘기하자면 맞습니다. 우리는 이 한가지만 기억합시다.


문자열의 끝은 모두 '\0'(NULL) 문자로 끝이난다.



이거 하나만 기억하고 strtok가 어떻게 문자열을 자르게 되는지 그 과정을 살펴보도록 합시다.


우선 str이라는 문자열은 다음과 같이 메모리에 잡혀있을 겁니다.





이제 strtok(str,"_")를 호출하는 순간 str에서 "_"라는 문자열이 나올때 그 문자열 자리를 \0로 채우게 됩니다. 그 뒤에 ptr을 반환하게 됩니다. 바로 str[0]의 주소지요.


ptr은 위의 코딩에서 tok가 넘겨받게 되지요. 그래서 tok는 \0까지를 문자열로 인식하게 되므로 처음에는 "show"가 출력되게 되는 것이죠.




이후 ptr을 '\0'다음으로 위치시킵니다. 또 "_"가 나오면 그 자리를 NULL문자로 채우고 ptr의 주소를 반환합니다. 그렇다면 str[5]의 주소가 되겠지요.




이 후 ptr을 str[8]자리로 위치시킵니다. 이 자리는 '\0' 다음 위치지요. 다음에 나오는 "_"를 NULL로 채운 후 ptr을 반환시킵니다.




이제 '\0' 이후에 ptr을 위치시켜 다음 "_"를 찾는데 이제 "_"를 찾을 수 없고 '\0'문자를 만나게 되니까 "money"만을 출력하게 되는 것이죠. 




이 후에는 문자열이 종료되었으므로 strtok는 NULL을 반환하고 while반복문은 종료가 됩니다.


그렇다면 이제 다음 드는 의문은 strtok는 어떻게 ptr의 주소를 기억하고 있을까라는 점입니다. 그런 의문 안드세요?

왜냐면 함수는 종료가 되면 모든 지역변수를 반환하게 되는데 어떻게 ptr이라는 변수는 기억하고 있을까요?

바로 지역변수가 아니기 때문입니다. 변수나 자료형, 메모리 공간을 충분히 알고 있다면 ptr은 정적변수로 선언이 되었다는 것을 눈치챘을 겁니다. 그렇기 때문에 함수가 종료되어도 ptr은 다음 자를 문자열의 주소를 기억하고 있는 겁니다.




제가 한 설명이 의심이 된다면 한번 실험을 해보는 것도 나쁘지 않습니다.

다음의 코드를 실행시켜보세요.


strtok source code2

#include <stdio.h>
#include <string.h>

int main() {

	char str[32] = "show_me_the_money";
	int len = strlen(str);
	int i;
	char *tok;

	for (i = 0; i < len; i++)
		printf("'%c' : str[%d]의 주소:%p\n", str[i], i, &str[i]);
	printf("\n");

	tok = strtok(str, "_");
	while (tok != NULL) {
		printf("token : %s, address:%p\n", tok,tok);
		tok = strtok(NULL, "_");
	}
	printf("\n");

}


만일 제 설명이 맞다면 str을 자른 tok의 주소들이 "_" 이후의 주소들과 같을 겁니다. 왜냐면 "_"이후가 바로 자른 문자열의 시작주소이기 때문이죠.


결과를 보면서 확인해보세요.


결과

token : show, address:008FFC68

token : me, address:008FFC6D

token : the, address:008FFC70

token : money, address:008FFC74


's' : str[0]의 주소:008FFC68

'h' : str[1]의 주소:008FFC69

'o' : str[2]의 주소:008FFC6A

'w' : str[3]의 주소:008FFC6B

' ' : str[4]의 주소:008FFC6C

'm' : str[5]의 주소:008FFC6D

'e' : str[6]의 주소:008FFC6E

' ' : str[7]의 주소:008FFC6F

't' : str[8]의 주소:008FFC70

'h' : str[9]의 주소:008FFC71

'e' : str[10]의 주소:008FFC72

' ' : str[11]의 주소:008FFC73

'm' : str[12]의 주소:008FFC74

'o' : str[13]의 주소:008FFC75

'n' : str[14]의 주소:008FFC76

'e' : str[15]의 주소:008FFC77

'y' : str[16]의 주소:008FFC78



strstr

문자열에서 임의의 문자열을 찾을 수 있는 함수가 string.h에 존재합니다. 바로 strstr이라는 함수이지요.

char *strstr( char *str1, const char *str2);


- str1 : 전체 문자열을 의미합니다. str1이 이제 문자열을 찾을 대상이 되지요.

- str2 : 찾을 문자열을 의미합니다. 이 문자열을 str1에서 찾는 것입니다.


반환값 : str1에서 str2를 찾는다면 그 시작주소를 반환하게 됩니다. 찾지못하면 NULL을 반환합니다.


이제 예제를 보면서 함수를 어떻게 사용하는지 보도록 하지요.


▼strstr source code

#include <stdio.h>
#include <string.h>
int main() {

	char str[64] = "When I was young, I was ugly. But now, I'm still ugly";
	char *word = "ugly";
	char *ptr = strstr(str, word);
	int jump = strlen(word);
	int found = 0;
	while (ptr != NULL) {
		printf("%s\n", ptr);
		ptr = strstr(ptr + jump, word);
		found++;
	}

	printf("단어 갯수 :%d\n", found);
}

위의 코드는 str이라는 문자열에서 word라는 문자열을 찾습니다. 한번만 찾는게 아니고 계속해서 찾는거죠.
그러기 위해서 만약 단어를 찾으면 그 다음부터 찾아야하죠. 물론 ptr+1로 그냥 바로 다음 문자부터 찾으면 되겠지만 조금 더 많이 건너 뛰기 위해서 jump라는 변수를 사용한것 뿐입니다. 




그리고 found는 str에 그 word가 몇개나 존재하는지 알려줍니다.

아차, strstr 역시 str의 문자열 중 word와 일치한다면 일치한 str의 시작 주소를 넘겨주게 됩니다.
못 믿겠으면 직접 실험해보도록 하세요.

이제 결과를 보면서 확인해보세요.

결과

ugly. But now, I'm still ugly

ugly

단어 갯수 :2



여기까지 문자열 처리함수를 2개나 알아보았는데요. 물론 저의 설명이 허접해서 이해를 못하는 부분이 있을 수 있으니, 모르면 그냥 외워서 사용하도록 합시다.

반응형
블로그 이미지

REAKWON

와나진짜

,