iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Software Development

三十天內用C++寫出一個小遊戲系列 第 23

Day 23 - 字串又來了,我還是沒吃到串燒

Outline

可以把這三個東西理解成 class 的更多運用。

C++ Strings : 比之前教過的 c string 更模組化。

File I /O : 因為現在寫的程式越來越大,所以可能需要用這種方式。

Header files : 因為我們寫的 class 會越來越多,我們就需要利用 header files 來管理這些 classes。

C++ Strings

之前我們學到的 C string 會與 C++ string 不太一樣:

  • C string : 基本上就是一個 character array,會在最後加上 '\0'。(define 在 <cstring>裡面)
  • C++ string : 是一個 class 叫做 string,並且是 defined 在 <string> 裡面,因此可以做到模組化的效果。(因此比較好用),他內含:
    • A member variable, a pointer pointing to a dynamic character array
    • many member functions
    • many overloaded operators

Declaration

宣告

string myStr;//空字串                             
string yourStr = "your string";//傳入一個陣列
string herStr(yourStr);// copy contructor (已經是 deep copy)

透過這些方式可以讓我們宣告 string object(注意 每個都是)

這些函式是:

  • string::string()
  • string::string(const char* s);
  • string::string(const string& str);
    • string 是定義在<string> 裡面的一個 class
    • sting 不是 C++ keyword
    • myStr 是一個 object
    • 因為 encapsulation 的緣故,讓 C++ string 可以不再擔心 '\0'

函式

string myStr;
string yourStr = "your string";
cout << myStr.length() << endl; // 0
cout << yourStr.size() << endl; //11
  • size_t string:: length() const;
  • size_t string::size() const;
  • size_t string::max_size() const; (可以讓我們知道我們最多可以使用多大的字串)
    • 我的電腦裡是: 2147483647
    • 反正就是很大

指派

string myString = "my string";
string yourstring = myString; // 宣告的時候用的是 structure
string herString;
herString = yourString = "a new string"; // assignment 是使用 operator overloading

或是也可以用 C string 的方式做到 C++ string 裡面:

char hisString[100] = "oh yaaaa";
myString = hisString;

串接(concatenation)和標數(indexing)

string myStr = "my string";
string yourStr = myStr;
string herStr;
herStr = myStr + yourStr; // "my string my string" (會自動加上一個 space)

可以直接用 + 把兩個字串接起來(也是因為被 overloaded 過了)

這個 + 就像是 C 裡面的這個函式 strcat()

所以一樣的可以用 C string 的方式配上加號,一樣可以把字串接起來:

string s = "123";
char c[100] = "456";
string t = s + c; // 123456
string u = s + "789" + t; // 123789123456

取出一個字串的某的 character 就用 [ ]

string myStr = "hello there";
char a = myStr[0]; // h

string input: getline()

string myStr; 
cin >> myStr; // this is a book
cout << myStr; // this? why
cout << myStr[0];// t

為什麼我們myStr 印出來會是this?

這是因為C++在判定cin 的時候,space 會被當作一個delimiter

Review

char a[100];
cin.getline(a, 100); // Hi, this is John Cena
cout << a << "\n"; // Hi, this is John Cena

cin.getline() 是一個函數,且在切換字元方面,它是依照換行來區別不同的東西。(a, 100) → a 就是要傳入的陣列、100就是你想輸入的數量。

但是我們現在卻不能使用getline(),這是因為我們第一個傳入的是一個字元陣列,也就是所謂的C string,但我們現在呼叫的卻是一個C++ string( object)。

這次我們要用的,是一個define在<string>裡面的global function。

istream& getline(istream& is, string& str);
istream& getline(istream& is, string& str, char delimiter);

istream = input string

istream& = input stream 的 reference

使用:

string str;
getline(cin, str);
getline(cin, str, '#');  

可以想像 cin 就是一個 object,這一個function 需傳入兩個 object

而如果加入第三個則就是每個字串會以 '#' (以上面為例) 做分割。

string str;
getline(cin, str); // hello there

Substrings

string string::substr(size_t pos = 0, size_t len = npos) const;

pos : 從哪裡,len : 多長

string::npos 這是一個 static member,也就是指 size_t最大值

也就是說,若你妳二個參數不填,他就會從第一個 pos 切到這個字串結束。

使用 :

string s = "asdfgijjj";
cout << s.substr(2, 3) << endl; // "dfg";
cout << s.substr(2) << endl; // "dfgijjj" 

尋找威力 String

可以使用 find()

size_t find(const string& str, size_t pos = 0) const; // 傳 c++ string
size_t find(const char* s, size_t pos = 0) const; // c string
size_t find(char c, size_t pos = 0)const; // a character

pos 也是從哪裡開始找; 前面的 parameter 就是你想要尋找誰。

而他回傳的是 你想要的那個東西的 beginning index,若沒找到則是 string::npos

string s = "asdfgh";
if (s.find("fgh") != string::npos)
	cout << s.find("fgh"); // 3

比較

以前我們可能需要使用 strcmp()

但 C++ 裡面就可以直接使用 >, ≥, <, ≤, ==, != 來 compare。

怎麼查 ?

如果你想做 string concatenation,去google 或是 找一下 <string> 裡面的函式。

還有更多

insert(), replace(), erase(); 好用好用推推推!!

string& insert(size_t pos, const string& str);
string& replace(size_t pos, size_t len, const string& str);
string& erase(size_t pos = 0, size_t len = npos);
int main()
{
  cout << "01234567890123456789\n";
  string myStr = "Today is not my day.";
  cout << myStr << endl;
  myStr.insert(9, "totally "); // Today is totally not my day. 
  cout << myStr << endl;
  myStr.replace(17, 3, "NOT"); // Today is totally NOT my day. 
  cout << myStr << endl;
  myStr.erase(17, 4); // Today is totally my day. 
  cout << myStr << endl;
  return 0;
}

其實看一遍就大概會他們怎麼用了!!

C++ string 的轉換

  • C++ → C string c_str()
  • C++ → number stoi(), stof(), stod()
  • number → C++ to_string()

C++string for mandarin characters

就像是英文字母一樣,中文字也會使用編碼來紀錄,而在大部分的環境裡會利用兩種系統

  • Big-5
  • UTF-8

他們都是用 2 bytes 來存取中文的字元

int main()
{
	string s = "大家好";
	cout << s << endl; // 大家好
	char c[100] = "喔耶";
	cout << c << endl;  //喔耶

	cout << s[1] << endl; // j
	cout << c + 2 << endl; // 耶
}

在先前介紹的 function 用在中文字元也是行得通的 !

但是要注意的是,要把 elements 當作一個個分開的 char variables

像是如果我們要反轉一個字串

對於英文字串就可以這樣寫

int main()
{
	string s = "12345";
	int n = s.length(); //5
	string t = s;
	for (int i = 0; i < n; i++)
		t[n - i - 1] = s[i]; // good
	cout << t << endl; //54321
	return 0;
}

但是如果我們同樣用這樣的方式去做中文字就會變成

int main()
{
	string s = "大家好";
	int n = s.length(); //6
	string t = s;
	for (int i = 0; i < n; i++)
		t[n - i - 1] = s[i]; // nono
	cout << t << endl; // n地屐
	return 0;
}

這是因為中文字元是由兩個 byte 做編碼,大概會長這樣:

xxk xxu xxo xix xxo xox

編碼是我亂寫的。總之如果我們兩個編碼倒過來就會變得我們不知道是甚麼的中文字元

那麼要怎麼樣寫?

所以我們就必須要兩個兩個一組把他們傳過去

int main()
{
	string s = "大家好";
	int n = s.length(); //6
	string t = s;
	for (int i = 0; i < n - 1; i = i + 2)
	{
		t[n - i - 2] = s[i];
		t[n - i - 1] = s[i + 1];
	} //nice
	cout << t << endl; 
	return 0;
}

File I /O (focus on plain text file)

我們可以直接用程式匯入檔案或是匯出檔案,像是遊戲的成績、紀錄(在程式中也可以更改這個 file)

The von Neumann architecture:

在我們以前寫的程式,只會包含上面那三個,但是現在可以加入下面這一個 storage 了。

要使用 storage,我們就一定要跟我們的硬碟去溝通了。

一個 plain text file 會包含

  • 存入 characters
  • 沒有 format (MS word document)
  • 沒有顏色 (bitmap file)

這些字元是如何儲存的?

  • 每個字元會有自己的位置
  • 每個 file 會有一個 position pointer 指向 你現在要讀/寫的地點
  • 可以透過這個pointer 來控制 reading or writing

寫檔案

第一個 character 會存在 position 0。

每當寫一個 character 的時候,position pointer 會移到下一個位置,且原本存在position 0 的字元會被取代。

File Stream

就像我們以前可以在 console 中印出或是輸入時,我們使用 cout << , cin >>,而他們使用的 library 是 <iostream>

我們要在 console 裡面改動 file 中的資料,就會使用 ifstreamofstream 的 object 或是 function,而這兩個 class 是被 define 在 <fstream>裡面。

Output file stream:

ofstream file object;
file object.open(file name); //打開檔案
//.....
file object.close(); // 關掉檔案

file name可以是 C or C++ string

int main()
{
	ofstream myFile;
	myFile.open("temp.txt");
	myFile << "1 abc\n &%^ " << 123.45;
	myFile.close();

	return 0;
}

execute 之後就會發現在程式的同一個資料夾就會出現一個 txt 檔,裡面寫著

1 abc
&%^ 123.45

<< 這個 operator 被做了 overloaded,會 return ofstream&,來連續做 output stream。

different options:

open mode:

ofstream file object;
file object.open(file name, option); //打開檔案
//.....
file object.close(); // 關掉檔案
  • ios::out (default) : 從 0 開始,會覆蓋已存在的資料。
  • ios::app : 從檔案最後開始,不會更改資料
  • ios::ate : 從檔案最後開始,會把整個資料更改

其中 ios 是一個 class,out, app, ate 三個是 static variable

其他 function:

  • put(char c) : 把 char c 寫入 file 裡面

例子:

#include<iostream>
#include<cstdlib>
#include<fstream>
using namespace std;

int main()
{
	ofstream scoreFile("temp.txt", ios::out);
	char name[20] = { 0 };
	int score = 0;
	char notFin = 0;
	bool con = true;

	if (!scoreFile) // 如果 scoreFile 沒有被讀取
		exit(1); // 強制 terminate
	while (con)
	{
		cin >> name >> score;
		scoreFile << name << " " << score << "\n";
		cout << "Continue (Y/N)";
		cin >> notFin;
		con = ((notFin == 'Y') ? true : false);
	}
	scoreFile.close();
	return 0;
}

Input file stream:

ifstream file object;
file object.open(file name); //打開檔案
//.....
file object.close(); // 關掉檔案

option 只有 ios::int,沒有其他的。

就像 output 一樣,我們可以用 if(!myFile) 來確認檔案有沒有正確地打開,確認後再去其他事情。

狀況一:

如果input data 的資料型態是非常的整齊,就使用 >>

像是下面這樣,每個姓名成績中間都隔一個空白

Jeff 100
Charlie 95
Jack 88
Emily 99
Leo 53

可以求 平均、排序、....等等

#include<iostream>
#include<cstdlib>
#include<fstream>
using namespace std;

int main()
{
	ifstream inFile("score.txt");
	
	if (inFile)
	{
		string name;
		int score = 0;
		int sumScore = 0;
		int scoreCount = 0;

		while (inFile >> name >> score)
		{
			sumScore += score;
			scoreCount++;
		}
		if (scoreCount != 0)
			cout << static_cast<double>(sumScore) / scoreCount;
		else
			cout << "no grade!";
	}
	inFile.close();		
	return 0;
}

這樣就可以從我們的資料裡面找到他們的成績,再取平均了!

>> 會在兩個空白/ tab/ \n 之間讀取資料。

狀況二:

這個情況就是 file 裡面的資料沒有格式化,或不是非常的完美,例如資料有缺失(即沒辦法預測下一向 data type 是甚麼)

Jeff 100
Charlie 95
Jack
Emily 99
Leo 53

這時候我們就不能使用 >> 來讀取檔案,在這種情況,要把 data 當作 character 來看,並手動地找到我們要的 type,這個過程會被稱為 parsing。

這時候就可以使用在 ifstream 之中的函式(member function)

  • get() : read a character and return it
while (!inFile.eof()) // eof = end of line
{
	char c = inFile.get();
  cout << c;
}
  • getline() : read multiple character into a character array
  • getline(name, 20, ' ') 第三個 parameter 是 delimiter
while (!inFile.eof()) // eof = end of line
{
	char name[20] = 
	inFile.getline(name, 20); //name是要存入的矩陣; 20 代表取 20 個字
  cout << name << endl;
}

如果加上第三個參數的話,它代表的意思是讀到那就停(position pointer 停在 delimiter 的下一個字元)

  • ignore() 可以把指標指向下一個字元

但是如果我們每次都要使用 char array 這樣會非常的麻煩,因此我們也可以使用 C++ string。

但是這邊要注意的是,我們之後要使用的 getline() 會是定義在<string>底下的 global function:

header:

istream& getline(isream& is, string& str, char delim);

使用

while (!inFile.eof()) // eof = end of line
{
	string name;
	getline(inFile, name, ' '); // 直接使用 getline() 就可以
	cout << name << endl;
}

更新檔案

如果今天有一段資料像是這樣

Jeff 100
Charlie 95
Jack 87
Emily 99
Leo 53

我們要把 Jack 改成 Jackson 怎麼辦?

  • 可以先用 seekp() 找到我們想要的文字 : J
  • 在利用 copy-and-paste 把新的文字貼上去(因為 plain text files 是 sequential-access files 資料是連續的)

實作

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

int main()
{
	ifstream inFile("test.txt");
	ofstream outFile("test1.txt");
	string name; 
	int score;

	if (inFile && outFile)
	{
		while (inFile >> name >> score)
		{
			if (name == "Jack")
				name = "Jackson";
			outFile << name << " " << score << endl;
		}
	}
	inFile.close();
	outFile.close();

	return 0;
}

>> vs. getline()

- date type delimiter
>> 會把傳入的轉成容器的type 在第一個不是容器 type 時停止(pt停在下一個字元)
getline() 全部轉成 string 會取到delimiter而已(pt停在下個字元)

Header files

Libraries

<iostream><fstream><cmath><cctype><string>這些 library已經被我們用了行之有月了,那這些 library 要怎麼 define? 我們自己也可以 define 嗎?

當然是可以的 !

一個 library 包含了一個 header file(.h)與 一個或數個 source file(s) (.cpp)

  • header file 裡面包含了 declarations
  • source file 裡面包含了definition

例子

#include <iostream>
using namespace std; 
int myMax(int a[], int len);
int main() 
{
  int a[LEN] = {7, 2, 5, 8, 9};
  cout << myMax(a, 5)
  return 0;
}
int myMax(int a[], int len)
{
  int max = a[0];
  for(int i = 1; i < len; i++)
  {
    if(a[i] > max)
      max = a[i];
  }
  return max;
}

我們想要把這個程式定義在一個 library 裡面,可以這樣寫

這個header file就是放 function 或是 variable 的宣告。

myMax.h

const int LEN = 5; 
int myMax (int [], int);
void print(int);

這個檔案就是放 function 的定義(類似使用說明)

myMax.cpp

#include <iostream>
using namespace std; 

int myMax(int a[], int len)
{
  int max = a[0];
  for(int i = 1; i < len; i++)
  {
    if(a[i] > max)
      max = a[i];
  }
  return max;
}
void print(int i)
{
  cout << i; // cout undefined!
}

這邊就是放我們一般使用的編譯 main program

main.cpp

#include <iostream>
#include "myMax.h" //要傳入我們自定義的 header file
using namespace std;

int main() 
{
  int a[LEN] = {7, 2, 5, 8, 9};
  print(myMax(a, LEN));
  return 0;
}

要注意的是,每一個 source file 都要記得 include 它裡面所需的 library

心得

想到剩下五天就覺得好興奮!!!!!!

加油


上一篇
Day 22 - 運算過載,warning ! warning !
下一篇
Day 24 - 繼承家業
系列文
三十天內用C++寫出一個小遊戲30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言